You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这既是 store 水合策略的健壮性缺陷(上游应修),也是 slot 内一个 action 缺少空值兜底的直接触发点。
2. 现象与复现
2.1 现象
打开 dsh web 提供的 GUI(例:http://127.0.0.1:3080),主对话区正常、可以正常收发消息;
左侧边栏只有外壳(品牌区、新会话按钮、面板图标、底部设置),中间「工作区 / 会话」区域完全空白;
页面不会恢复,刷新后依旧空白;
控制台可见明确报错(见 2.3)。
2.2 浏览器控制台报错(原文)
TypeError: Cannot convert undefined or null to object
at Object.entries (<anonymous>)
at retainAccountKeys (client.js:218:1)
at index-BKQ_L1z6.js:56:69359
at index-BKQ_L1z6.js:56:68694
at produce (index-BKQ_L1z6.js:56:66201)
at Object.update (index-BKQ_L1z6.js:56:68674)
at a.<computed> [as retainAccountKeys] (index-BKQ_L1z6.js:56:69348)
at client.js:2017:1
at zi (index-BKQ_L1z6.js:56:24417)
at bn (index-BKQ_L1z6.js:56:42760)
client.js:526 slot entry crashed in 'sidebar.workspaces': TypeError: Cannot convert undefined or null to object
at Object.entries (<anonymous>)
at retainAccountKeys (client.js:218:1)
...
componentDidCatch @ client.js:526
即:slot entry crashed in 'sidebar.workspaces',崩溃点是 client.js:218 的 retainAccountKeys。
$ node repro-store-crash.mjs
extracted 1342 chars of the shipped store definition
A. no persisted snapshot (fresh install) -> OK
B. complete snapshot -> OK
C. snapshot missing sessionUpdatedAtByAccount -> THREW TypeError: Cannot convert undefined or null to object
D. snapshot with sessionUpdatedAtByAccount: null -> THREW TypeError: Cannot convert undefined or null to object
E. snapshot missing groupExpansion -> THREW TypeError: Cannot convert undefined or null to object
F. snapshot missing sessionOrderByAccount -> THREW TypeError: Cannot convert undefined or null to object
4 scenario(s) reproduced the sidebar crash
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
产品:DeepSeek Harness(
dsh)版本:Web UI 构建
0.1.5-rc.2-449af19(@deepseek-ai/dsh 0.1.5-rc.1CLI +profiles/node_modules内0.1.5-rc.2插件集)平台:Windows 10/11(
node v26.7.0,Edge)严重级:高(核心导航功能不可用,且不会自愈)
类型:客户端状态持久化缺少容错(前端崩溃),非数据损坏
1. 摘要
升级到 0.1.5-rc.2 后,Web GUI 左侧边栏的会话/工作区列表整体空白:既看不到已有会话,也看不到工作区分组或「添加工作区」入口。会话数据本身完好(主机侧可正常枚举、可正常读取),崩溃完全发生在前端。
根因是
@deepseek-ai/dsh-client-ui-workspace的持久化视图 store 在浏览器 localStorage 快照不完整时,水合(rehydrate)以「整体替换」方式覆盖初始状态,使sessionUpdatedAtByAccount等字段变成undefined;随后retainAccountKeys对该字段调用Object.entries(),抛出TypeError,sidebar.workspaces这个 slot 组件崩溃被错误边界捕获,整块区域渲染为空。这既是 store 水合策略的健壮性缺陷(上游应修),也是 slot 内一个 action 缺少空值兜底的直接触发点。
2. 现象与复现
2.1 现象
dsh web提供的 GUI(例:http://127.0.0.1:3080),主对话区正常、可以正常收发消息;2.2 浏览器控制台报错(原文)
即:
slot entry crashed in 'sidebar.workspaces',崩溃点是client.js:218的retainAccountKeys。2.3 触发条件
浏览器 localStorage 中持久化的视图快照
dsh.workspace.view.v5缺少groupExpansion/sessionOrderByAccount/sessionUpdatedAtByAccount三者之一,或该字段为null。凡是这类不完整快照(例如被外部工具裁剪过、写入中断留下残片、旧构建写入后被当前构建读取等),每次加载都会稳定复现。2.4 现场确认命令(在出问题页面的 DevTools Console 执行)
3. 根因分析
3.1 直接原因:
retainAccountKeys未做空值兜底@deepseek-ai/dsh-client-ui-workspace@0.1.5-rc.2→lib/client.js(压缩后行 214-219):调用点
client.js:2017(WorkspaceBrowser 的 effect,workspacePhase === 'ready'后执行):只要
d.sessionUpdatedAtByAccount为undefined/null,Object.entries就抛TypeError: Cannot convert undefined or null to object。3.2 根本原因:持久化 store 的「替换式水合」丢掉了
init()默认值前端 shell 内联的
@deepseek-ai/dsh-client-store(dist/assets/index-BKQ_L1z6.js)里,快照 store 的水合逻辑是:而
M6只是 immer 的freeze(function M6(t){return No(t,!0)},No是freeze),不是 merge。底层 zustand store 的setState语义是:由于水合传入
replace = true,快照里没有的键不会被init()的默认值补齐,于是init()里明明声明过的sessionUpdatedAtByAccount: {}在运行时是undefined。这是"schema 漂移/残缺快照"场景的通用隐患:任何持久化 store 一旦被旧版本或残缺内容写坏,就会让消费方拿到undefined。init()本身声明是完整的(同版本lib/client.js195-203 行):3.3 为什么"整块空白"而不是显示错误
sidebar.workspaces是single类型的 slot,由ui-workspace插件注册(client.js:2794)。该 slot 的组件在渲染期 effect 中抛错 → 被 slot 错误边界捕获(slot entry crashed in 'sidebar.workspaces')→ 该 slot 渲染为空。而这一块正是"工作区/会话浏览区域"(SidebarRoot里的renderSlot('sidebar.workspaces', …)),所以表现为会话列表整体消失,外壳仍在。3.4 与版本演进无关
已核对 npm 上
0.1.3-alpha.2、0.1.5-alpha.1/2、0.1.5-rc.1、0.1.5-rc.2、0.1.6-alpha.1的lib/client.js:三个字段与persist: "dsh.workspace.view.v5"在所有版本中都存在,即本崩溃不是"新增字段未迁移"造成,而是已有字段在持久化快照中缺失/为 null 时缺少兜底。4. 最小确定性复现(无需浏览器)
附件
repro-store-crash.mjs直接抽取已发布 bundle 中真实的createWorkspaceViewStore定义,配合按 shell 压缩代码逐行对齐复刻的createSnapshotStore/defineStore/zustandsetState语义,注入不同的 localStorage 快照运行retainAccountKeys:复现命令(PowerShell,Windows):
脚本内默认读取:
%USERPROFILE%\.dsh\profiles\node_modules\@deepseek-ai\dsh-client-ui-workspace\lib\client.js5. 建议修复
5.1 上游(推荐,两处)
(a)
packages/client/store:水合时以init()默认值兜底(治本,消除所有 store 的同类隐患)(b)
packages/client/ui-workspace/src/client/stores.ts:action 内对三个字段做空值兜底(防御性,成本极低)retainAccountKeys: (d, workspaceKeys) => { const retained = new Set(workspaceKeys); - d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion).filter(([key]) => retained.has(key))); - d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount).filter(([key]) => retained.has(key))); - d.sessionUpdatedAtByAccount = Object.fromEntries(Object.entries(d.sessionUpdatedAtByAccount).filter(([key]) => retained.has(key))); + d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion ?? {}).filter(([key]) => retained.has(key))); + d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount ?? {}).filter(([key]) => retained.has(key))); + d.sessionUpdatedAtByAccount = Object.fromEntries(Object.entries(d.sessionUpdatedAtByAccount ?? {}).filter(([key]) => retained.has(key))); },配套建议(防同类复发):
syncSessionOrderAccount同样写入这两张表,可在写入前??=初始化;persist键升版(v5→v6)或在水合时校验字段集合,比静默替换更安全。5.2 本地临时修补(已验证可用)
本次已在安装目录打上等价补丁(升级
dsh后会被覆盖):.dsh/profiles/node_modules/@deepseek-ai/dsh-client-ui-workspace/lib/client.jsObject.entries(d.xxx)→Object.entries(d.xxx ?? {}).dsh/profiles/node_modules/@deepseek-ai/dsh-web-frontend/dist/assets/index-*.jst.setState(M6(JSON.parse(i)),!0)→t.setState(M6(Object.assign({},gc(),JSON.parse(i))),!0)(gc即该文件内的getInitialState)验证结果:
node --check对两文件均通过;复现脚本 6 个场景(含缺失/为 null 的三种字段)由 4 个崩溃降为 0 个崩溃;retainAccountKeys全部返回 OK,快照缺失字段被init()默认值补齐。回滚方式:同目录保留有
*.orig-backup原文件,覆盖即可。6. 影响范围
dsh --profile web)且 localStorage 中dsh.workspace.view.v5不完整的用户;openSession导航(均挂在sidebar.workspaces区域);session/list主机接口均不受影响(实测主机侧 6 个会话全部可枚举、可 stat、可解析,含 v0 命名的历史会话);7. 验证过的"不是这些原因"
为避免社区重复排查,以下均已实测排除:
@deepseek-ai/dsh-session-persistence-jsonl@0.1.5-rc.2枚举.dsh/sessions,6 个会话(含session.jsonl.zstd的 v0 历史会话)全部LIST OK,stat正常;.dsh/storages/workspace.json中两个工作区、全部sessionIds完整,archivedSessionIds为空;dsh --dump-config --profile web组合树正常,ui-workspace、session-query-sqlite(openAt: never)、session-controller等行均在位;index.html引用的index-BKQ_L1z6.js/vendor-*.js与磁盘文件名一致,/plugins/...路由按rev精确匹配属预期行为。8. 环境明细
All reactions