dsh-client-ui-trajectory-plus2
v0.1.1
Published
Verbatim copy of @deepseek-ai/[email protected] (MIT) with a six-line rename: the platform trajectory view republished as a second tab, 轨迹plus2 (view id plus2, order 21). No style, no layout and no feature differs from the official tab.
Maintainers
Readme
dsh-client-ui-trajectory-plus2
面板上的第二个「轨迹」。它不是我理解的轨迹,是官方那份轨迹的逐字副本:官方
@deepseek-ai/[email protected]的客户端 bundle,只改了 6 行(模块 id / locale 命名空间 / 中英标签 / 视图 id / order)+ 3 处共存守卫(六处 register 与 provide、子槽改成"加入而不是抢",见下文),别的一个字符都没动,渲染行为没有任何改变。当前版本 0.1.1。
它是什么,不是什么
| | |
| --- | --- |
| 是 | 官方轨迹视图的一份逐字拷贝,注册成第二个标签页:视图 id plus2、order 21、标签 轨迹plus2 / Trajectory Plus 2 |
| 是 | 一个指纹可验的基线:node --test 会把 src/client.js 逆向还原成官方原始字节并比对 sha256,差一个字符就红 |
| 不是 | 手写的复刻、简化版或"抄个样子"。样式、布局、表格、泳道、折叠、JSON 树、右键菜单、图片子槽、虚拟滚动、历史加载、时长偏好——全部是官方原样 |
| 不是 | 宿主侧有路由的插件。宿主半侧是官方那个空 apply,一个字节没改 |
这个包的存在意义就是基线:官方怎么跳、怎么折、怎么排序、什么时候发请求,这里一模一样。你要改哪里,指哪打哪,不用先怀疑"是不是抄错了"。
出处与指纹
| | |
| --- | --- |
| 来源 | @deepseek-ai/[email protected](MIT,Copyright (c) 2026 DeepSeek) |
| 原始文件 | lib/client.js,421649 字节,8778 行(结尾没有换行),sha256 71fa00f0c3e92278e54e9203a819305f5abf7103d5b32114ad52afd467bb3b9c |
| 宿主半侧 | lib/index.js,184 字节,sha256 1feff7e32e81646ad0b7f1b865a089414d88ea88d4ff81e24f0680ec018530b9,逐字复制 |
| 本机原件 | /Users/337mu/.npm/_npx/7eff65a5cfe9d1d1/node_modules/@deepseek-ai/dsh-client-ui-trajectory/lib/client.js |
| 许可 | 官方的 LICENSE 原样放在本包 LICENSE(MIT 正文一字未改) |
改动清单:6 行改名 + 3 处共存守卫
src/client.js 顶部有 40 行出处声明(注释,不是 bundle 逻辑:原来的 32 行 + 声明共存守卫的 8 行)。守卫自己加了 39 行(它是本包唯一一处"不是改名"的改动),所以:
- 守卫之前的行:本文件行号 = 官方行号 + 40
- 守卫之后的行:本文件行号 = 官方行号 + 40 + 39
A. 6 行改名(和以前完全一样):
| # | 官方 lib/client.js | 本文件 | 改什么 | 为什么 |
| --- | --- | --- | --- | --- |
| 1 | 2 | 42 | 模块 id:"@deepseek-ai/dsh-client-ui-trajectory" → "dsh-client-ui-trajectory-plus2" | 模块加载器按 boot manifest 的行 id(= 裸包名)取工厂;不改就两个模块撞,且会被判成"加载后没注册"直接丢弃 |
| 2 | 56 | 96 | locale 命名空间:"trajectory" → "dsh-client-ui-trajectory-plus2" | 两个插件共用一个命名空间会互相覆盖字典;我们这份是官方那张表的私有副本 |
| 3 | 59 | 99 | 中文标签:"view.trajectory": "轨迹" → "轨迹plus2" | 面板标签 |
| 4 | 254 | 294 | 英文标签:"view.trajectory": "Trajectory" → "Trajectory Plus 2" | 面板标签 |
| 5 | 8738 | 8817 | 视图 id:id: "trajectory" → id: "plus2" | 与官方标签页共存,id 必须唯一 |
| 6 | 8739 | 8818 | order:order: 10 → order: 21 | 排在 官方对话 0 < 官方轨迹 10 < 轨迹plus 20 < 轨迹plus2 21 |
B. 3 处共存守卫(把"抢"改成"加入";白名单里是 coexistJoin / coexistProvide / coexistChildSlot 三行):
| # | 官方 lib/client.js | 本文件 | 改什么 | 为什么 |
| --- | --- | --- | --- | --- |
| 7 | 8726 | 8766 | 六个 register 调用改走 joining(...) / shared:官方已登记同名 key 就 return () => {},不抢(+39 行) | uiConversation.events.register / .views.register 重名必抛(dsh-client-ui-conversation/lib/client.js:2624) |
| 8 | 8732 | 8811 | ctx.uiSession.provide({…}) → joinHook({…}):只吞 .includes("duplicate hook") | dsh-client-ui-session/lib/client.js:454 同一个标准钩子名重名必抛 |
| 9 | 8736 | 8815 | ctx.slots.register({…}) → joinSlot({…}):只在 is already declared 时去掉 children 再注册一次 | dsh-client-ui-slots/lib/index.js:193 同名子槽必抛 |
守卫只改"重复注册时怎么办",不改任何渲染行为:那 9 个 Definition、1 个 target、那个 trajectory 钩子与我们登记的是同一份代码(test/shared-registries.test.js 逐键比对过),谁登记渲染出来的东西一样;子槽是按名字取用的,官方那条 entry 已经声明过 conversation.trajectory.images,我们退回"不带 children"的注册之后,官方 TrajectoryView 里的 renderSlot("conversation.trajectory.images", …)(官方 bundle 第 8324 行)照样解析到它。
没改的(刻意):inject 服务表(slots/sessions/uiSession/uiConversation/locale)、target("trajectory")、hooks: ["trajectory"]、子槽 children: {"conversation.trajectory.images"}、ctx.slots.inject("conversation.view")、9 个 Definition 的全部实现、4 段 CSS、persist 名(dsh.trajectory.duration / dsh.trajectory.jsonStringWrapping)。
那两处"没改"的含义要说清楚,因为它们是故意的:
target("trajectory")与hooks: ["trajectory"]:这是我们和官方共用同一条数据路径的原因——同一个 target、同一份 snapshot,所以它和官方一样快、一样准。persist名没改:两个标签页共用同一份"展开时长/JSON 换行"偏好(和官方行为一致)。- 那三处守卫也没有改任何一个 key、任何一份 Definition:官方不在场时(单独装本包、或本包排在官方之前加载),走的就是官方那条路径,注册结果与原样逐字节相同。
数据从哪来(为什么和官方一样快)
- 宿主半侧:
src/index.js是官方那个空apply——没有任何路由,一个 HTTP 请求都不发。 - 浏览器半侧:数据全部来自平台客户端服务
uiSession/uiConversation(inject声明的 5 个服务,官方原样)。它读的是平台已经在内存里的会话快照,不额外拉数据。 - 所以它和官方轨迹的区别只有两处:标签页 id/order/文字。渲染路径、数据源、刷新时机完全相同。
合规
- 来源包是 MIT,允许复制、修改、再分发;本包保留官方 LICENSE 正文,并在
src/client.js顶部逐字写明:本文件是@deepseek-ai/[email protected](MIT)的副本,改动仅限模块 id / 视图 id / order / 标签 / locale 命名空间,并附上原始 sha256。 src/index.js是官方宿主半侧的逐字副本(测试里比 sha256)。- 本包的 LICENSE 是官方 MIT 原文;本包自身同样是 MIT。
⚠️ 和官方「轨迹」共存:已知会撞四处(先读这一段)
为什么
官方那份 bundle 除了画界面,还往平台全局注册表里写三类东西:
ctx.uiConversation.events.register(...)× 9,键是kind:trajectory-assistant-step/trajectory-turn-end/trajectory-compaction/trajectory-session-end/trajectory-inbox-next-step/trajectory-input-message/trajectory-system-message/trajectory-request-header/trajectory-tool-callctx.uiConversation.views.register({ target: "trajectory", ... })× 1ctx.uiSession.provide({ hooks: ["trajectory"], ... })× 1,钩子名trajectory- 槽注册里的子槽声明
children: { "conversation.trajectory.images": { kind: "single", scope: "session" } }(@deepseek-ai/dsh-client-ui-attachment往这个子槽里塞图片)
这四个注册表/去重表是全页共享的(uiConversation、slots、uiSession 每页各一个实例),而且重名直接抛错:
dsh-client-ui-conversation/lib/client.js:2624
if (this.definitions.has(key)) throw new Error(duplicateMessage);
├─ events.register(:2656)→ conversation Definition "trajectory-inbox-next-step" is already registered
└─ views.register (:2883)→ conversation view target "trajectory" is already registered
dsh-client-ui-slots/lib/index.js:193
if (childRec?.spec) throw new Error('slot "' + childKey + '" is already declared (by ...)')
└─ → slot "conversation.trajectory.images" is already declared
(by an entry in "conversation.view" (@deepseek-ai/dsh-client-ui-trajectory))
dsh-client-ui-session/lib/client.js:454 (claimStandardProp,由 :261 rebuildBindings -> materializeAbsent() 在 provide 时就抛)
if (finalProps.has(propName)) throw new Error('uiSession.provide: duplicate <kind> ' + name + ' at prop ' + propName)
└─ → uiSession.provide: duplicate hook 'trajectory' at prop 'useTrajectory'这不是猜测:@deepseek-ai/dsh-web-app 的依赖里有 @deepseek-ai/dsh-client-ui-trajectory,而 @deepseek-ai/dsh-web-app 就在本机 profile 的 dsh.profile.bundles 里——官方轨迹标签永远在场(你看得到它,就是证据)。
会发生什么(没有守卫的时候)
我们的 apply() 按官方顺序先跑 registerTrajectoryMessageDefinitions(ctx),第一句 events.register 就撞上第 1 类,抛错 → 这个客户端插件加载失败 → 面板上不会出现「轨迹plus2」(第 2/3/4 类排在它后面,都走不到;控制台里看到的会是第 1 类那句)。这句推断现在是可复现的:test/coexistence.test.js 在一个平台形状的 stub 上下文里先跑官方那份 bundle,第二次 apply() 抛的就是 conversation Definition "trajectory-inbox-next-step" is already registered。
修法:已经打上去了(白名单第 7–9 行)
四处都改成"官方已经登记过,我就加入它,不再抢"。下面是设计稿(原来只贴在 README 里的那三段),落盘代码与它只有一处收窄,见代码块后面的说明:
// 1) apply(ctx) 里:六个 register 调用改走守卫层
const joining = (registry) => ({
register: (definition) => {
const key = definition.kind ?? definition.target;
if (registry.entries().some((entry) => (entry.kind ?? entry.target) === key)) return () => {};
return registry.register(definition);
}
});
const shared = {
uiConversation: {
events: joining(ctx.uiConversation.events),
views: joining(ctx.uiConversation.views)
}
};
registerTrajectoryMessageDefinitions(shared); // 原本传 ctx
registerTrajectoryRequestHeaderDefinition(shared);
registerTrajectoryAssistantDefinition(shared);
registerTrajectoryToolDefinition(shared);
registerTrajectoryCompactionDefinitions(shared);
registerTrajectoryConversationView(shared);
// 2) uiSession 钩子:钩子名是标准的,平台按名字去重、重复即抛;官方已经供了同一个钩子,加入即可
try {
ctx.uiSession.provide({
hooks: ["trajectory"],
resolve: (binding) => ({ hooks: { trajectory: trajectorySource(binding) } })
});
} catch (error) {
if (!String(error?.message ?? error).includes("duplicate hook")) throw error;
}
// 3) 槽注册处:子槽已被官方声明时,退回"不带 children"再注册一次
ctx.slots.inject("conversation.view", () => {
const entry = { /* 原来那一整块,含 children: { "conversation.trajectory.images": ... } */ };
try {
return ctx.slots.register(entry, TrajectoryView);
} catch (error) {
if (!String(error?.message ?? error).includes("is already declared")) throw error;
const { children: _joined, ...withoutChildren } = entry;
return ctx.slots.register(withoutChildren, TrajectoryView);
}
});落盘代码与设计稿的唯一差别(src/client.js 里的 shared):设计稿的 shared.uiConversation 是一个只带 events/views 的普通对象,但 registerTrajectoryRequestHeaderDefinition 的两条 Definition 在渲染时还会回手调用 ctx.uiConversation.inspectSystemPrompt(...) / inspectRequestPrompt(...)(官方 bundle 第 1443–1444 行:那两个箭头函数闭包住的 ctx 就是传进去的 shared)。照设计稿原样传,这两个调用会变成 undefined is not a function —— 那正好是"改变渲染行为" ✗。所以落盘版本用一个 Proxy:只替换 events/views,其余成员(inspectSystemPrompt、inspectRequestPrompt、binding、imageUrl…)一律原样转发给平台服务。
const shared = {
uiConversation: new Proxy(ctx.uiConversation, {
get: (target, property) => property === "events" ? joining(target.events) : property === "views" ? joining(target.views) : Reflect.get(target, property, target)
})
};第 2、3 段也没有整块包 try/catch,而是抽成 joinHook / joinSlot 两个小函数,调用点各自只改一个词:ctx.uiSession.provide({ → joinHook({,ctx.slots.inject("conversation.view", () => ctx.slots.register({ → … => joinSlot({。这样 id / order 那两行改名仍然是两行独立的白名单行,不会和守卫混在一起(守卫里只有第 7 行加了 39 行,第 8、9 行都是等长替换)。
"加入"为什么等于"自己做",有两处硬依据:
- 那 9 个 Definition + 1 个 target + 那个
trajectory钩子与我们登记的是同一份代码(副本逐字相同,test/shared-registries.test.js逐键比对过),谁登记渲染出的东西一样; - 子槽是按名字取用的:官方
TrajectoryView里写的是renderSlot("conversation.trajectory.images", {...})(官方 bundle 第 8324 行),名字已被官方那条 entry 声明过就能取到——所以退回"不带children"的注册,图片照样渲染。
另一处不报错但要知道的耦合:CSS
4 个 <style> 标签的注入守卫是 style[data-plugin-css="<官方包名>/<文件>.module.css"],标签属性写的是官方包名(副本逐字,所以它仍自称 @deepseek-ai/dsh-client-ui-trajectory)。后果:
- 页面上只会有一份样式(第二次注入被守卫挡住),不重复;
- 但
dsh-client-modules的removeOwnedStyles(id)按data-plugin属性删样式:官方包热重载/卸载会连带删掉轨迹plus2 赖以绘制的样式(我们不删官方的)。本地开发时如果看到"轨迹plus2 突然没样式了",先看官方那条 entry 是不是刚重建过。
守卫带来的一个"只影响热重载"的边角(不报错,但要知道)
官方那条 entry 被卸载/热重载时,它的子槽声明是跟着 entry 一起撤销的;本包退回的那次注册(不带 children)不会替它补上,所以从官方卸载到它重新注册之间,plus2 的图片会短暂不渲染(slots.snapshot() 对未声明的槽返回空——降级,不抛错)。官方 entry 回来后子槽自动恢复;本包自己的标签页、表格、泳道等不受影响。这是"共存"换来的代价里唯一一处看得见的,且只在开发时的热重载窗口内。
安装(宿主与 profile 由你来做)
~/.dsh/profiles/web/package.json:
"dependencies": {
"dsh-client-ui-trajectory-plus2": "link:/Users/337mu/Documents/Default Project/dsh-client-ui-trajectory-plus2"
},
"dsh": {
"profile": {
"bundles": [
"…原有的…",
"dsh-client-ui-trajectory-plus2"
]
}
}装完 pnpm install(或你惯用的流程)→ 重启 DSH → 刷新页面。order 21 会把它排在官方「轨迹」(10) 的右边、兄弟插件「轨迹plus」(20) 的右边。
测试
node --test test/*.test.js| 文件 | 管什么 |
| --- | --- |
| test/diff-whitelist.test.js | 差异白名单:把 9 条替换(6 处改名 + 3 处守卫)逆向还原,比对官方 sha256/字节数/行数;再逐条断言"官方行号对不对",并用两条 NEGATIVE 证明"改坏一个字符就会红" |
| test/registration.test.js | 在 stub 浏览器里真的执行 bundle、真的跑 apply():模块 id、服务表、视图 id/order/标签(中英)、子槽、hook provider、4 个 style 标签 |
| test/shared-registries.test.js | 把官方 bundle 和我们的副本放进同一个 stub 对比注册结果,把上面那条共存警告钉成可验证的事实:同一个 stub 里跑两份 bundle,逐键比对注册结果,并附上平台源码那四个抛错点的行号 |
| test/coexistence.test.js | 共存守卫的证明:在同一个"平台形状"的上下文里先挂官方、再挂本包,断言 apply() 不再抛错、9 个 key 仍是官方那一份(且只有一份)、plus2 标签照样注册(并按 is already declared 退掉 children)、单独安装时行为不变;外加两条负向控制(官方字节跑两遍 → 抛;provide/register 的非重复错误 → 照样抛且不重试) |
| test/sources.js / test/browser-stub.js | 读源文件的入口 + 那个只做"注册"不做"渲染"的 stub 浏览器(stub 的四个注册表按平台的行为拒绝重名,否则"官方已经在了"这种状态根本没法复现) |
"差异白名单"是怎么写的(最重要的那条):
test/sources.js里有一张白名单表PATCHES:每行是一个字面替换(from/to,带制表符)+ 它在官方文件里的行号 + 为什么。表就是授权:加一行 = 批准一处改动,删一行 = 禁止一处改动。9 行 = 6 处改名(moduleId/namespace/labelZh/labelEn/viewId/viewOrder)+ 3 处共存守卫(coexistJoin/coexistProvide/coexistChildSlot)。- 测试按这张表反向替换
src/client.js的正文,得到"被还原的官方字节",再比对OFFICIAL.sha256(实测71fa00f0…)。421 KB 里错一个字符就红。 - 顺带钉住:字节数 421649、行数 8778、没有结尾换行、每条替换在官方文本里的行号与表里一致、我们的文件 = 官方行数 8778 + 出处声明 40 行 + 守卫 39 行 = 8857 行(
ADDED_LINES这个数字由表自己算出来,改一个字符都对不上)。 - 如果这台机器上装着的官方原件被顺便找到了,就直接逐字节比对,并断言原件自身的 sha256 没变——平台升级了会报警而不是静默漂移。
- 两条负向验证:①正文改一个字符(
padding:0 8px→padding:0 9px)→ 还原结果 sha256 必变;②一份"没做替换"的原件 → 还原函数必须拒绝,而不是误判为原件。
我(这份包)验证不了的
- 没有浏览器:
轨迹plus2和官方「轨迹」画出来是不是一模一样,必须你重启后亲眼并排比一遍(表格行、泳道色条、折叠摘要、JSON 树、右键菜单、图片、历史加载、时长偏好、"展开 JSON 字符串")。测试只到"注册的东西一样"。 - 守卫"在真机上"的效果:
test/coexistence.test.js证明的是"同一份平台代码把重名当错误"这条规则之下的行为;真浏览器里两个标签都开、都能翻历史、都能看图片,仍然要你看一眼(尤其:plus2 退回"不带 children"的注册之后,图片子槽要能渲染出来)。 - 加载链路(
dsh.client扫描、boot manifest 行、模块图、profile 安装)是宿主侧的事,不在本包测试范围,由你验证。 - 上面那段"会发生什么"——守卫之前会在第一句
events.register上抛——现在在 stub 里可复现(test/coexistence.test.js的 NEGATIVE 那条),但真机没跑过:你重启后如果「轨迹plus2」没出现,控制台里应该是上面四句里的第 1 句,或者别的没被守卫覆盖的第 5 类;把那句发我。 - 加载顺序:守卫让"谁先注册谁拥有"。官方
dsh-web-app的依赖图把官方轨迹排在前面,所以正常路径是官方先注册、本包"加入";万一本包排到了官方前面,被拒的会是官方那个标签——这条已经写成测试钉住了(KNOWN LIMIT),profile 的bundles里别把本包挪到官方前面。
文件清单
| 文件 | 说明 |
| --- | --- |
| src/client.js | 官方 lib/client.js 的逐字副本 + 40 行出处声明 + 6 行改名 + 39 行共存守卫(8857 行) |
| src/index.js | 官方 lib/index.js 的逐字副本:空 apply,无宿主路由 |
| LICENSE | 官方 MIT 原文(Copyright (c) 2026 DeepSeek) |
| cordis.patch.yml | 把插件行插进 profile roster(条目名必须是裸包名) |
| package.json | dsh.client.inject 与官方逐字相同;dsh.bundle.patch 指向上面那个 yml |
| test/* | 见上一节 |
版本 0.1.1 写在三处并且有测试盯着它们一致:package.json 的 version、src/client.js 顶部声明的 VERSION 0.1.1、本 README。
