Skip to content

dsh-client-ui-trajectory-plus2

Verified

dsh-client-ui-trajectory-plus2 · v0.1.1 · MIT · Web UI

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.

Install

dsh plugin add dsh-client-ui-trajectory-plus2

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

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 除了画界面,还往平台全局注册表里写三类东西:

  1. 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-call
  2. ctx.uiConversation.views.register({ target: "trajectory", ... }) × 1
  3. ctx.uiSession.provide({ hooks: ["trajectory"], ... }) × 1,钩子名 trajectory
  4. 槽注册里的子槽声明 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 的四个注册表按平台的行为拒绝重名,否则"官方已经在了"这种状态根本没法复现)

"差异白名单"是怎么写的(最重要的那条):

  1. test/sources.js 里有一张白名单表 PATCHES:每行是一个字面替换(from/to,带制表符)+ 它在官方文件里的行号 + 为什么。表就是授权:加一行 = 批准一处改动,删一行 = 禁止一处改动。9 行 = 6 处改名(moduleId/namespace/labelZh/labelEn/viewId/viewOrder)+ 3 处共存守卫(coexistJoin/coexistProvide/coexistChildSlot)。
  2. 测试按这张表反向替换 src/client.js 的正文,得到"被还原的官方字节",再比对 OFFICIAL.sha256(实测 71fa00f0…)。421 KB 里错一个字符就红。
  3. 顺带钉住:字节数 421649、行数 8778、没有结尾换行、每条替换在官方文本里的行号与表里一致、我们的文件 = 官方行数 8778 + 出处声明 40 行 + 守卫 39 行 = 8857 行(ADDED_LINES 这个数字由表自己算出来,改一个字符都对不上)。
  4. 如果这台机器上装着的官方原件被顺便找到了,就直接逐字节比对,并断言原件自身的 sha256 没变——平台升级了会报警而不是静默漂移。
  5. 两条负向验证:①正文改一个字符(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。