修复: 协议层与体验缺陷六件

- temperature 按模型适配,仅收 1 的模型不再 400
- 流式 error 帧不再被 choices 必填整帧丢弃
- 只读缓存写后失效
- git diff 中文截断撞 UTF-8 边界 panic
- 非视觉模型纯图片直接拒绝
- 全局快捷键补输入焦点守卫
This commit is contained in:
lxy
2026-10-04 16:27:31 +08:00
parent 01d231c39d
commit 7aa0da32f2
28 changed files with 491 additions and 52 deletions
+43 -8
View File
@@ -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,