修复: 协议层与体验缺陷六件
- temperature 按模型适配,仅收 1 的模型不再 400 - 流式 error 帧不再被 choices 必填整帧丢弃 - 只读缓存写后失效 - git diff 中文截断撞 UTF-8 边界 panic - 非视觉模型纯图片直接拒绝 - 全局快捷键补输入焦点守卫
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
//! 后端 `app_settings` 表的 value 永远是 JSON 字符串。本层职责:
|
||||
//! - 缓存已解析(反序列化)值,按 key 索引,组件同步读
|
||||
//! - 写操作立即更新缓存(乐观),并按 key debounce ~300ms 合并连续快速写
|
||||
//! - 落库失败保留 pending 脏值待下次写入重试,经 lastWriteError 透出(见 set/flush)
|
||||
//! - 组合式 `useSetting` 让组件像用 ref 一样用 v-model 绑定某个 key
|
||||
//!
|
||||
//! 注意:此为「KV 偏好持久化」层,与 AI 提供商/连接配置(store 层)是两件事 ——
|
||||
@@ -24,6 +25,11 @@ const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
/// 载荷队列:debounce 触发时取最新值(而非定时器创建时的快照)
|
||||
const pendingValues = new Map<string, unknown>()
|
||||
|
||||
/// 最近一次落库失败(key + 错误),成功落库同一 key 后清 null。
|
||||
/// store 层无组件上下文,不直接弹 toast,对齐 knowledge.ts 的 state.error 范式,
|
||||
/// 上层 watch 此状态做行内红字/toast 通报。
|
||||
const lastWriteError = ref<{ key: string; error: unknown } | null>(null)
|
||||
|
||||
/** 启动时一次性拉回全部 key/value 并解析填充缓存(App.vue 调用) */
|
||||
async function loadAll(): Promise<void> {
|
||||
const all = await settingsApi.getAll()
|
||||
@@ -41,6 +47,9 @@ function get<T>(key: string, defaultValue: T): T {
|
||||
/**
|
||||
* 写入某 key:立即更新缓存(乐观,视图无延迟),再 debounce 后落库。
|
||||
* 同 key 连续快速写(如拖拽/输入)在窗口内合并,只把最后一次值 JSON.stringify 发后端。
|
||||
*
|
||||
* promise 契约:resolve 仅代表"已调度落库",不代表已落库;本 promise 永不
|
||||
* reject(调用方 void/await 均安全),落库成败经 lastWriteError 透出。
|
||||
*/
|
||||
async function set(key: string, value: unknown): Promise<void> {
|
||||
cache[key] = value
|
||||
@@ -51,18 +60,35 @@ async function set(key: string, value: unknown): Promise<void> {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
pendingTimers.delete(key)
|
||||
const latest = pendingValues.get(key)
|
||||
pendingValues.delete(key)
|
||||
// 落库失败仅记日志:缓存已是最新值,降级为内存态,避免回滚造成 UI 与库不一致
|
||||
settingsApi
|
||||
.set(key, JSON.stringify(latest))
|
||||
.catch((e) => console.error(`[appSettings] 写入 ${key} 失败:`, e))
|
||||
void flush(key)
|
||||
}, SET_DEBOUNCE_MS)
|
||||
pendingTimers.set(key, timer)
|
||||
}
|
||||
|
||||
/** 删除某 key:清缓存 + 取消 pending 写 + 调后端删除 */
|
||||
/**
|
||||
* 把 pending 中该 key 的最新值落库。失败时保留 pending 脏值不清除,
|
||||
* 下次该 key 任何 set 会合并进同一队列自然重试;cache 保持乐观值不回跳
|
||||
* (避免落库失败把用户已输入的视图打回去),失败经 lastWriteError 与
|
||||
* console.error 双通道透出,状态不再"假成功"。
|
||||
*/
|
||||
async function flush(key: string): Promise<void> {
|
||||
const latest = pendingValues.get(key)
|
||||
if (latest === undefined) return
|
||||
try {
|
||||
await settingsApi.set(key, JSON.stringify(latest))
|
||||
// 成功才清 pending;若落库期间又有新写合并进来,保留新值交由新定时器落库
|
||||
if (Object.is(pendingValues.get(key), latest)) pendingValues.delete(key)
|
||||
if (lastWriteError.value?.key === key) lastWriteError.value = null
|
||||
} catch (e) {
|
||||
console.error(`[appSettings] 写入 ${key} 失败,已标记待重试(下次改动该 key 时重试):`, e)
|
||||
lastWriteError.value = { key, error: e }
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除某 key:清缓存 + 取消 pending 写 + 调后端删除;删除失败恢复内存态后原样上抛 */
|
||||
async function remove(key: string): Promise<void> {
|
||||
const prevCache = cache[key]
|
||||
const prevPending = pendingValues.get(key)
|
||||
delete cache[key]
|
||||
pendingValues.delete(key)
|
||||
const t = pendingTimers.get(key)
|
||||
@@ -70,7 +96,15 @@ async function remove(key: string): Promise<void> {
|
||||
clearTimeout(t)
|
||||
pendingTimers.delete(key)
|
||||
}
|
||||
await settingsApi.delete(key)
|
||||
try {
|
||||
await settingsApi.delete(key)
|
||||
} catch (e) {
|
||||
// 删除失败:磁盘仍有该值,恢复内存态保持两端一致,避免重启后该 key 幽灵复活而视图已丢
|
||||
if (prevCache !== undefined) cache[key] = prevCache
|
||||
if (prevPending !== undefined) pendingValues.set(key, prevPending)
|
||||
lastWriteError.value = { key, error: e }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,6 +183,7 @@ function parse(raw: string): unknown {
|
||||
export function useAppSettingsStore() {
|
||||
return {
|
||||
cache,
|
||||
lastWriteError,
|
||||
loadAll,
|
||||
get,
|
||||
set,
|
||||
|
||||
Reference in New Issue
Block a user