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
# 检查 web profile 是否安装了 @deepseek-ai 副本
ls ~/.dsh/profiles/web/node_modules/@deepseek-ai/ | head -20
# 对比全局安装树
ls $DSH_HOME/node_modules/@deepseek-ai/ | head -20
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.
DSH 双实例问题分析
问题背景
用户提问:为什么存在双实例?
核心结论
这是 DSH 架构的设计缺陷被 pnpm 触发的结果。官方设计(见 dsh-app-boot 源码注释)是"双锚点解析":
@deepseek-ai/*全部 201 个)只从 CLI 安装树解析——$DSH_HOME/profiles/node_modules是 CLI 启动时维护的符号链接农场,每个包一个链接指向全局安装,保证所有 profile 共享同一份物理拷贝(模块级状态如kScopesymbol 才能互通)@linxin666/*这类)但
dsh plugin add的实现是"在 profile 目录里跑 pnpm add"——pnpm 不知道这个去重契约,会把插件传递依赖里的@deepseek-ai/*以真实副本装进 profile 的node_modules。于是:同一个包从两个物理路径加载 → 两个模块实例 → 各自生成独立的
kScopesymbol → 写入方和读取方对不上 → 你看到的那个错误。为什么只有 web profile 中招:tui 和 open-design 从没跑过
plugin add,它们的node_modules里根本没有@deepseek-ai副本(只有 web 装了插件,把 70 个副本带进来了)。详细分析
正确的部分
双实例的根本原因:确实是同一个包从两个不同的物理路径加载,导致 Node.js 模块缓存命中两个不同的副本,各自生成独立的
kScopesymbol。pnpm 的行为:
dsh plugin add在 profile 目录执行pnpm add时,pnpm 的依赖提升(hoisting)策略会将传递依赖中的@deepseek-ai/*包以真实副本形式安装到 profile 的node_modules(或其 pnpm store 链接),破坏了 CLI 设计的"全局唯一拷贝"契约。加载路径分裂:
dsh-agent-loop)从$DSH_HOME/profiles/node_modules(符号链接农场)加载 → 命中全局拷贝dsh-agent-presets)从profile/node_modules加载 → 可能命中 profile 下的副本为什么只有 web profile 中招:因为
tui和open-design没有跑过plugin add,它们的node_modules里没有@deepseek-ai的副本。需要修正的关键细节
问题不在于"web 树拷贝",而在于"同一进程内两个模块解析器看到的路径不同":
dsh-scope同时出现在:$DSH_HOME/node_modules/@deepseek-ai/dsh-scope)node_modules(~/.dsh/profiles/web/node_modules/@deepseek-ai/dsh-scope)那么当 Web 插件代码执行
import { kScope } from '@deepseek-ai/dsh-scope'时,Node 的模块解析算法会根据调用栈的起点决定走哪条路径:两个副本各自维护自己的模块级状态,所以写入方用全局拷贝的 symbol,读取方用 profile 拷贝的 symbol,对不上。
验证方法
你可以运行以下命令确认是否存在双副本:
如果两者都有
dsh-scope,且路径不同,就证实了双实例问题。解决方案方向
dsh plugin add后手动删除 profile 下的@deepseek-ai/*副本,强制它们回退到全局拷贝。dsh plugin add的实现,使用pnpm --filter或自定义 resolver,确保@deepseek-ai/*永远不被安装到 profile 的node_modules。你的分析抓住了核心矛盾:pnpm 的依赖管理策略与 DSH 的"全局单例"架构假设不兼容。
这是否说明 DSH 本身有问题?
是的,这说明 DSH 的架构设计存在根本性的耦合问题,而不是简单的"实现 bug"。
核心设计缺陷
1. 隐式的全局状态依赖
DSH 假设
@deepseek-ai/*包在整个进程中只有一个模块实例,从而共享同一个kScopesymbol。这依赖于:但这是一个脆弱的假设:
当两个物理路径都包含这个文件时,Node.js 会创建两个独立的模块实例,各自持有不同的
kScope。写入方用全局实例的 symbol,读取方用 profile 实例的 symbol,自然对不上。2. 违反了自己的架构契约
官方文档说:
但
dsh plugin add的实现是:pnpm 不知道也不关心 DSH 的这个契约,它会按照自己的依赖提升策略安装所有传递依赖,包括
@deepseek-ai/*。这是典型的"文档说了但代码没 enforce"的问题。
3. 模块边界不清晰
DSH 的核心问题在于:它既是一个 CLI 工具,又是一个 Web 应用,还允许动态加载插件,但三者之间的模块隔离和共享机制没有清晰的边界。
这三者的交集处出现了冲突。
为什么这是个架构问题而非实现 bug?
DSH 的问题是:它依赖一个无法被工具链保证的不变量(
@deepseek-ai/*永远只有一个副本)。正确的架构应该是什么?
方案 A:显式的依赖注入
不再依赖模块级单例,而是通过构造函数或上下文对象传递
kScope:方案 B:强制的单例注册表
提供一个全局注册表,所有模块通过它获取共享状态:
方案 C:包管理器的硬约束
修改
dsh plugin add,使用 pnpm 的overrides或自定义 resolver,确保@deepseek-ai/*永远指向全局拷贝:{ "pnpm": { "overrides": { "@deepseek-ai/*": "link:$DSH_HOME/node_modules/@deepseek-ai/*" } } }结论
是的,DSH 本身有问题。 它的设计假设了一个无法被现代包管理器保证的不变量,导致在用户使用
plugin add时必然触发双实例问题。这不是"某个开发者写错了代码",而是架构层面的技术债务:为了简化开发体验(全局单例),牺牲了系统的可扩展性和鲁棒性。
要彻底解决,需要:
All reactions