From 0f34d67cc8427c06b2a415f82ee78429c8df790a Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 27 Aug 2026 00:34:51 -0600 Subject: [PATCH] =?UTF-8?q?fix(=E7=AB=99=E7=82=B9=E9=85=8D=E7=BD=AE):=20We?= =?UTF-8?q?bSocket=20=E5=B9=BF=E6=92=AD=E7=9A=84=20key=20=E6=98=AF=20snake?= =?UTF-8?q?=EF=BC=8Cstore=20=E5=AD=97=E6=AE=B5=E6=98=AF=E9=A9=BC=E5=B3=B0?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E6=97=B6=E7=94=9F=E6=95=88=E4=B8=80=E7=9B=B4?= =?UTF-8?q?=E7=A9=BA=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端推的是 options 表里的 snake_case 键: {"type":"config_update","key":"enable_maxkb","value":false} 前端 configUpdate.ts 收到后写的是 `if (data.key in configStore.config)`,而 store 里的字段是驼峰(enableMaxkb)。这个 `in` 判断**永远是 false**,一条配置 都写不进去 —— 站点名称、页脚、班级名单、允许注册、提交列表看全部、知识库挂件, 全部都要刷新才生效,且没有任何报错。`conf.ts` 里那句「前端 configStore.config 用的就是这套键名」是写错的,八成就是照着它写的这段代码。 加一层 snake → 驼峰的转换。八个键都符合通用规则,所以用正则转而不是列表, 将来加键不用改这里;转完认不出来的键直接忽略,别把 store 撑出野字段。 站点改名时顺便同步 document.title —— getConfig() 里本来就是这么设的, 只更新页面里那份、标签页还是旧名字说不过去。 顺带把 MaxKB 挂件那块理干净: - 加载条件改成「已登录 且 后台开关打开」。挂件本来就要登录才能用,原来对匿名 访客也照样注入。 - 删掉它自己那条 /ws/config 连接和 handleConfigUpdate。store 修好之后, 原有的 watch 自然就跟着动了;多开一条连接是重复的,而且没登录时会撞 401 (后端 /ws/config 要求会话),控制台常年一条「[WebSocket] 连接错误」就是它。 匿名访客不做实时生效:/ws/config 保持要求登录,不对外开不鉴权的 WS 端点。 没登录的人下次刷新页面时拿到新配置。 验证:起 dev 栈,用一个假的 MaxKB embed 端点(返回建出 #maxkb-chat-button 的 脚本)顶掉真实地址,headless chromium 走 CDP 看 DOM: ① 开关=开、未登录 → 挂件不在,script 标签 0 ② 开关=开、已登录 → 挂件在, script 标签 1 ③ 后台关掉(页面不刷新) → 挂件消失,script 标签 0 ④ 后台开回来(不刷新) → 挂件重现,script 标签 1 另外把其余三条 WS 通道的键名也对了一遍,没有同类问题:submission_update (submission_id / time_cost / memory_cost / err_info)两边都是 snake; flowchart 的 criteriaDetails 两边都是驼峰且 handler 没读它;成就通知的字段 全是单词,不涉及大小写。 Co-Authored-By: Claude Opus 5 --- apps/api/src/routes/admin/conf.ts | 4 +- .../src/shared/composables/configUpdate.ts | 23 +++- apps/web/src/shared/composables/maxkb.ts | 120 +++++------------- 3 files changed, 54 insertions(+), 93 deletions(-) diff --git a/apps/api/src/routes/admin/conf.ts b/apps/api/src/routes/admin/conf.ts index 507f1f3..23b3f81 100644 --- a/apps/api/src/routes/admin/conf.ts +++ b/apps/api/src/routes/admin/conf.ts @@ -83,7 +83,9 @@ adminConfRoutes.post("/website", requireSuperAdmin, async (c) => { set: { value: sql`excluded.value` }, }) // 广播给所有开着页面的人,改完立刻生效不必刷新,对齐旧 push_config_update。 - // 推的是 options 表里的 snake_case key —— 前端 configStore.config 用的就是这套键名。 + // 推的是 options 表里的 snake_case key。**前端 store 的字段是驼峰**, + // 换名在前端 configUpdate.ts 里做 —— 这里曾经注释成「前端用的就是这套键名」, + // 结果前端照着直接 `key in store` 判断,一条也命中不了,整个实时生效空转了很久。 for (const entry of entries) await publishConfigUpdate(entry.key, entry.value) return success(c, null) }) diff --git a/apps/web/src/shared/composables/configUpdate.ts b/apps/web/src/shared/composables/configUpdate.ts index 4974f36..9efbd75 100644 --- a/apps/web/src/shared/composables/configUpdate.ts +++ b/apps/web/src/shared/composables/configUpdate.ts @@ -5,17 +5,30 @@ import { type ConfigUpdate, } from "shared/composables/websocket" +/** + * 后端推的是 options 表里的 snake_case 键(`enable_maxkb`), + * store 里的字段是驼峰(`enableMaxkb`)—— 两边对不上,这里负责换。 + * + * 原来是直接 `data.key in configStore.config`,snake 键永远命中不了驼峰字段, + * 于是**一条配置都写不进去**,整个「改完不必刷新」是空转的:站点名称、页脚、 + * 班级名单、允许注册、提交列表看全部、知识库挂件,全都要刷新才生效。 + */ +function toCamel(key: string) { + return key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase()) +} + export function useConfigUpdate() { const configStore = useConfigStore() const userStore = useUserStore() // 处理 WebSocket 配置更新 const handleConfigUpdate = (data: ConfigUpdate) => { - // 更新全局配置 - 使用响应式方式 - if (data.key in configStore.config) { - // 直接修改 ref 的值来触发响应式更新 - ;(configStore.config as any)[data.key] = data.value - } + const field = toCamel(data.key) + // 认不出来的键直接忽略:后端将来多推一个字段,不该把 store 撑出个野字段 + if (!(field in configStore.config)) return + ;(configStore.config as any)[field] = data.value + // getConfig() 里也是这么设的,站点改名后标签页跟着变,别只更新页面里那份 + if (field === "websiteName") document.title = data.value } // 初始化 WebSocket - handler 会在 onMounted 时自动添加 diff --git a/apps/web/src/shared/composables/maxkb.ts b/apps/web/src/shared/composables/maxkb.ts index 480d009..2741466 100644 --- a/apps/web/src/shared/composables/maxkb.ts +++ b/apps/web/src/shared/composables/maxkb.ts @@ -1,126 +1,72 @@ import { useConfigStore } from "shared/store/config" import { useUserStore } from "shared/store/user" -import { - useConfigWebSocket, - type ConfigUpdate, -} from "shared/composables/websocket" +/** + * MaxKB 知识库挂件的加载 / 卸载。 + * + * 两个前提同时满足才加载:**已登录**(挂件本身就要登录才能用)且**后台开关打开**。 + * 任一条件变假就把脚本和它建出来的 DOM 一起撤掉。 + * + * 脚本**只在这里注入**。原来 vite 有个 inject-maxkb 插件把