dsh-prompt-customizer 0.5.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +26 -2
- package/README.md +129 -15
- package/build.mjs +219 -0
- package/client/client.js +8160 -3465
- package/client/client.js.map +7 -1
- package/cordis.patch.yml +6 -3
- package/lib/index.js +16360 -905
- package/lib/index.js.map +7 -0
- package/package.json +50 -38
- package/scripts/fixtures/rc2-mode.mjs +12 -0
- package/scripts/fixtures/rc2-preview.snapshot.json +35 -0
- package/scripts/fixtures/rc2-runtime.mjs +85 -0
- package/scripts/fixtures/rc2.cordis.yml +31 -0
- package/scripts/fixtures/rc2.overlay.yml +9 -0
- package/scripts/smoke-client.mjs +382 -0
- package/scripts/smoke-host.mjs +130 -0
- package/scripts/test-mcp-layering.mjs +441 -0
- package/scripts/test-mcp-tool-disable.mjs +420 -0
- package/scripts/test-rc2-runtime.mjs +42 -0
- package/scripts/test-skill-manager.mjs +170 -0
- package/scripts/test-skill-toggles.mjs +100 -0
- package/lib/catalog.js +0 -131
- package/lib/effective.js +0 -161
- package/lib/promotion.js +0 -80
- package/lib/schema.js +0 -125
- package/lib/sectionOps.d.mts +0 -67
- package/lib/sectionOps.mjs +0 -269
- package/lib/store.js +0 -124
- package/lib/vars.js +0 -147
- package/lib/zh/cordis.mjs +0 -110
- package/lib/zh/core.mjs +0 -47
- package/lib/zh/index.d.mts +0 -8
- package/lib/zh/index.mjs +0 -23
- package/lib/zh/sdk.mjs +0 -22
- package/lib/zh/teams.mjs +0 -19
- package/lib/zh/undo.mjs +0 -18
- package/src/client/Panel.tsx +0 -532
- package/src/client/PresetsPane.tsx +0 -298
- package/src/client/PreviewPane.tsx +0 -73
- package/src/client/PreviewTools.tsx +0 -33
- package/src/client/SectionsPane.tsx +0 -331
- package/src/client/ToolsPane.tsx +0 -230
- package/src/client/error-boundary.tsx +0 -48
- package/src/client/index.ts +0 -97
- package/src/client/locales.ts +0 -301
- package/src/client/nav.tsx +0 -284
- package/src/client/preset-io.ts +0 -226
- package/src/client/presets.ts +0 -413
- package/src/client/shell.tsx +0 -179
- package/src/client/styles.ts +0 -59
- package/src/client/types.ts +0 -125
package/README.en.md
CHANGED
|
@@ -95,6 +95,16 @@ Two things that bite:
|
|
|
95
95
|
- **`always` injections are present in all three phases**, and each phase has its own ordering space — which is why this plugin emits phase entries _after_ the `always` group: the assembly applies inject records in array order and the last write wins, so the phase order is what survives.
|
|
96
96
|
- **A phase view is "what this state resolves to", not "what the session has been".** Native preset phase rules (zero-tool bootstrap, warmup, …) are in play at the same moment and may still change the result after other plugins — see [Known limitations](#known-limitations).
|
|
97
97
|
|
|
98
|
+
## What's in the box
|
|
99
|
+
|
|
100
|
+
One sidebar entry — **capability panel** — with three top-level tabs:
|
|
101
|
+
|
|
102
|
+
- **SKILL** — skill bundles (grouping, categories, import from ZIP/folder, rename/delete), per-skill and per-bundle toggles on a global layer plus a per-agent-preset layer, skill-root health, and a `/` slash source that feeds skills into a session.
|
|
103
|
+
- **MCP** — paste-to-add MCP servers (JSON / native `mcpServers` YAML / `mcp-client` line), per-preset server rows, layered masking (global row + per-preset own/inherit ledgers, with a global veto), per-server and per-tool enable/disable, and a recommended-server catalog.
|
|
104
|
+
- **Prompt** — everything documented below.
|
|
105
|
+
|
|
106
|
+
> The Chinese README is the primary one and covers the SKILL / MCP tabs in more detail(见「功能特性 → 技能集合管理 / MCP Server 接入」)。
|
|
107
|
+
|
|
98
108
|
## UI
|
|
99
109
|
|
|
100
110
|

|
|
@@ -114,8 +124,9 @@ Two things that bite:
|
|
|
114
124
|
- **Block** — remove a section by name. A blocked row does not disappear: it stays draggable and editable and can be restored at any time. Blocking only means "not injected into the model".
|
|
115
125
|
- **Replace** — edit a section's text; the editor is pre-filled with the text currently in effect (dynamically generated sections are resolved first so you see real content).
|
|
116
126
|
- **Inject** — every phase part has its own name + text box to add a brand-new section, badged `manual` and deletable.
|
|
117
|
-
- **Reorder** — drag
|
|
127
|
+
- **Reorder** — drag the handle at the row's left edge (an insert line shows where it will land) or use ↑/↓. Order is stored as a contiguous 0-based virtual index, so there are never duplicate or fractional orders and no number to type by hand. Dragging a row out onto _All system sections_ removes it from the current phase (an injected section is un-injected, a native one becomes blocked); dropping it on a phase button in the header copies it into that phase.
|
|
118
128
|
- **Independent per phase** — bootstrap blocks go to `sectionsBootstrap`, compaction blocks to `sectionsCompaction`, resident blocks to `sections`; each phase has its own ordering space, so "5 sections during bootstrap, everything when resident" is expressible.
|
|
129
|
+
- **Sync phases** (header switch, off by default) — ticking it immediately overwrites the other two phases with the resident period's **union row set** (written to the edit draft; press Save to apply). From then on all three phases share one section list and text: add / remove / reorder / check / text edits are written to all three at once. The resident period is the backbone order, sections that only exist in the other two phases are appended and also injected into the resident period (a same-named row follows the phase where it natively exists), and the three block lists are unified into one. Runtime-generated dynamic sections cannot be materialised into the other phases; the list says so. Tool hiding / showing and adding back also apply to all three phases.
|
|
119
130
|
|
|
120
131
|
### Tool catalog
|
|
121
132
|
|
|
@@ -151,7 +162,15 @@ The cost of strict rendering: referencing an unregistered name (such as a key th
|
|
|
151
162
|
|
|
152
163
|
### Cross-preset registry pool
|
|
153
164
|
|
|
154
|
-
_All system sections_ / _All system tools_ do not follow the edit target: they are a **union across presets that only ever grows** (same name — last seen wins; new name — appended),
|
|
165
|
+
_All system sections_ / _All system tools_ do not follow the edit target: they are a **union across presets that only ever grows** (same name — last seen wins; new name — appended), stored in the derived cache `catalog.yaml`, which rebuilds itself if deleted.
|
|
166
|
+
|
|
167
|
+
The pool is filled from three sources, and **the first two hold the moment the panel opens**:
|
|
168
|
+
|
|
169
|
+
- **the current scope's registry** (the preset you are editing);
|
|
170
|
+
- **sections registered in the agent scope**: several plugins register their section per agent (`tool:subagent`, `context:file-reference`, …). Those live in neither the global layer nor any preset scope — they only exist in a real session assembly. They hold for *every* agent and belong to no particular mode, so the inventory merges the live agents' scopes on request: they are listed as soon as you open the panel, with no session round needed;
|
|
171
|
+
- **presets you have browsed**: sections and tools registered only inside one preset (`tools:ptc-only`, `raw-html-v2:vcp`, …) join as you switch edit targets. That part grows on demand — mounting every preset tree eagerly takes tens of seconds each, which is not worth doing on every panel open.
|
|
172
|
+
|
|
173
|
+
A real session assembly also feeds the registry, as the fallback for when no agent is alive.
|
|
155
174
|
|
|
156
175
|
## How it works
|
|
157
176
|
|
|
@@ -261,6 +280,11 @@ npm run check # typecheck + build + full test suite
|
|
|
261
280
|
|
|
262
281
|
The browser half is built with [tsdown](https://github.com/rolldown/tsdown) into `client/client.js` (a `__ModuleLoader__` factory bundle); the host half lives in `lib/`. Phase state logic sits in `lib/sectionOps.mjs`: the same pure functions are bundled into the UI and exercised directly by `node --test`, so the tests are the shipped code.
|
|
263
282
|
|
|
283
|
+
## Credits
|
|
284
|
+
|
|
285
|
+
- The **SKILL and MCP halves (UI and host modules) come from [Kr-ATG/dsh-triad](https://github.com/Kr-ATG/dsh-triad)** — skill bundle management, global/per-preset toggles, layered MCP masking and per-tool enable/disable, the sidebar capability entry, and the `/` skill source all follow that project. Since 0.6.0 they ship merged with the prompt customizer in this plugin.
|
|
286
|
+
- The prompt panel derives from this repository's own 0.5.x implementation.
|
|
287
|
+
|
|
264
288
|
## License
|
|
265
289
|
|
|
266
290
|
[MIT](LICENSE) © 2026 DreamsTOF
|
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">dsh-prompt-customizer</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>DeepSeek Harness
|
|
9
|
-
按名称屏蔽 / 替换 / 注入 /
|
|
8
|
+
<strong>DeepSeek Harness 的能力台:提示词定制 + 技能集合管理 + MCP Server 接入。<br>
|
|
9
|
+
按名称屏蔽 / 替换 / 注入 / 排序提示词段(按会话阶段与 agent 预设分层)、导入 / 分组 / 开关技能、接入 MCP 并做分层遮蔽与工具级启停 —— 全部在侧边栏的「能力」面板里完成。</strong>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
<p align="center">
|
|
26
|
-
<a href="https://github.com/DreamsTOF/dsh-prompt-customizer/releases"><img src="https://img.shields.io/badge/version-0.
|
|
26
|
+
<a href="https://github.com/DreamsTOF/dsh-prompt-customizer/releases"><img src="https://img.shields.io/badge/version-0.6.0-blue.svg" alt="Version"></a>
|
|
27
27
|
<a href="https://github.com/DreamsTOF/dsh-prompt-customizer/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"></a>
|
|
28
28
|
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek_Harness-Plugin-blueviolet.svg" alt="DeepSeek Harness Plugin"></a>
|
|
29
29
|
<a href="https://www.npmjs.com/package/dsh-prompt-customizer"><img src="https://img.shields.io/npm/v/dsh-prompt-customizer" alt="npm version"></a>
|
|
@@ -41,6 +41,19 @@
|
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## 在线试玩:仓库概览 galgame
|
|
45
|
+
|
|
46
|
+
本仓库被做成了可游玩的 [WebGAL](https://github.com/OpenWebGAL/WebGAL) 视觉小说:**138 个节拍、一次分支选择**,由项目化身「dsh-prompt-customizer」与「JavaScript」带你走完 —— 这个插件是什么、核心功能怎么用、怎么安装、目录地图、工作原理和已知限制。
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://dreamstof.github.io/dsh-prompt-customizer/"><b>▶ 立即在线游玩</b></a>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
- 在线版:<https://dreamstof.github.io/dsh-prompt-customizer/>(站点源在 [`gh-pages`](https://github.com/DreamsTOF/dsh-prompt-customizer/tree/gh-pages) 分支)
|
|
53
|
+
- 本地版:克隆 `gh-pages` 单分支后在目录里起一个静态服务器,例如 `python -m http.server 8000`,再打开 `http://localhost:8000`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
44
57
|
一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 插件,让你在侧边栏的独立面板里控制**系统提示词**和**工具目录**(仿宿主能力管理页:顶部 agent 预设 Tab、左栏勾选列表 + 底部三态过滤、右栏实时预览)。
|
|
45
58
|
|
|
46
59
|
其他插件注入的提示词段可能污染你的系统提示词。本插件让你按名称**屏蔽**、**替换**、**注入**、**排序**提示词段,并把工具从**模型可见目录**里隐藏 —— 全程实时生效,不需要改动任何其他插件。
|
|
@@ -48,7 +61,19 @@
|
|
|
48
61
|
两件事贯穿整个设计:
|
|
49
62
|
|
|
50
63
|
- **按会话阶段分层**:引导期(未晋级)/ 常驻期(晋级后)/ 压缩受控期(压缩后仍未晋级)各有**独立的屏蔽名单、独立的排序空间、独立的工具目录**。同一个预设可以做到首轮只给模型 1 个工具和几段提示词,晋级后放开全套。
|
|
51
|
-
- **按 agent 预设分层**:顶部预设 Tab
|
|
64
|
+
- **按 agent 预设分层**:顶部预设 Tab 选中某个 agent 预设后,改动只对该预设生效;**0.9.0 起「全部 Agent」全局层停用**——提示词必须按具体预设修改,预设未定制的字段 = 官方默认(旧全局层数据保留在 config.yaml 里不再生效,可手编回滚)。
|
|
65
|
+
|
|
66
|
+
## 0.9.2:按模式预设修正 rc.2 适配
|
|
67
|
+
|
|
68
|
+
- 预设目录使用 `agentPresets.acquireScope()`,每次读取选中模式的当前修订;结束释放租约。不匹配已有对话,不创建伪 Agent,不切换对话的预设。
|
|
69
|
+
- 顶部模式 Tab 决定提示词、工具和排序的编辑目标。同名提示词在不同模式下的正文分别来自各自作用域,不能拿另一个模式或旧会话替代。
|
|
70
|
+
- 预设预览中的 `{{model}}`、`{{cwd}}` 等未绑定变量保留为模板引用,并显示非致命说明;目录和草稿编辑不因此失败。真正对话中的严格插值行为不变。
|
|
71
|
+
- 独立展示 `contexts`(user-role 运行时快照、只读)。纯预设作用域无法得到依赖真实会话状态的贡献;这不允许把另一个对话的数据当成当前模式定义。
|
|
72
|
+
- 三阶段模拟本插件的独立规则;已有对话的模式绑定不被编辑目标切换改变。此预览不是完整历史 LLM 请求回放。
|
|
73
|
+
|
|
74
|
+
本地源码更新后执行 `npm run build`。链接安装无需复制;已启动宿主可能持有旧模块,保存工作后重启原来的 `dsh web` 并硬刷新,确认不再有「预览会话」,来源显示为「当前模式预设」。
|
|
75
|
+
|
|
76
|
+
目标版本冷启动测试:设置 `PC_DSH_MANIFEST` 为已安装 **0.2.0-rc.2** 的 `package.json`,运行 `node scripts/test-rc2-runtime.mjs`。测试先在零对话状态查看两个模式的独立工具、正文、相反顺序和未绑定变量,再完成离线用户消息 → 工具 → 回复,并断言预览切换不改变对话模式。测试只使用临时 DSH_HOME,不使用真实模型凭据,不修改用户 profile。
|
|
52
77
|
|
|
53
78
|
## 会话阶段怎么流转
|
|
54
79
|
|
|
@@ -97,7 +122,11 @@
|
|
|
97
122
|
|
|
98
123
|
## 界面
|
|
99
124
|
|
|
100
|
-
|
|
125
|
+
侧边栏点 **能力** 打开独立面板,顶层三个 tab:**SKILL**(技能集合管理)、**MCP**(Server 接入与工具级启停)、**提示词**(本节其余内容)。会话里 `/` 取技能与技能调用的工具行由 DSH 内核的 `@deepseek-ai/dsh-client-ui-skill` 提供 —— 本插件不重复注册(`skill` 命名空间 / 同名槽位键 / 同名斜杠源三处都归内核,重复注册会让内核插件的 apply 抛错)。
|
|
126
|
+
|
|
127
|
+
下面两张图是**提示词 tab**(截图截于 0.6.0 之前:顶部预设行当时还是纯文字 Tab,功能一致;SKILL / MCP 两个 tab 的截图待补)。
|
|
128
|
+
|
|
129
|
+
以下为**提示词 tab** 的细节 —— 侧边栏点 **能力 → 提示词** 打开独立面板。**第一行是 agent 预设 chips**(0.9.0 起无「全部 Agent」),每枚带图标、名称与该预设的定制项数(橙色数字 = 这个预设单独改过东西),点一下即切换编辑目标,进入面板时默认选中第一个可用预设。**第二行是定制头部**:模式切换(提示词 / 工具 / 配置)+ 阶段 Tab(引导期 / 压缩受控期 / 常驻期,左右两栏联动)+ 三态同步与中文提示词开关 + 保存 / 刷新 / 关闭。
|
|
101
130
|
|
|
102
131
|
**提示词模式**:左栏当前阶段的勾选列表(屏蔽 / 替换 / 注入 / 排序,底部三态过滤),右栏实时预览装配后的最终系统提示词:
|
|
103
132
|
|
|
@@ -109,22 +138,40 @@
|
|
|
109
138
|
|
|
110
139
|
## 功能特性
|
|
111
140
|
|
|
141
|
+
### 技能集合管理(SKILL tab)
|
|
142
|
+
|
|
143
|
+
- **技能包**:把散装技能归组(账本 `.bundles.json` 记规范名与目录名两套写法,目录名不一致也能命中);包可以有分类(最多 8 个、每个 ≤24 字符),改名会同步 `SKILL.md` frontmatter 里的 `name`,删除按规范名走,成员悬空时如实上报。
|
|
144
|
+
- **导入**:拖入 / 选择 ZIP 或文件夹,自动剥掉顶层文件夹、落成 `<技能根>/<名称>/SKILL.md`,并立刻归组到目标技能包;重名给明确错误,不带路径分隔的名字一律拒绝(不越界读技能根外的文件)。
|
|
145
|
+
- **开关(全局 / 预设两层)**:单个技能与整个技能包都能开关,预设层写自己那份账(规范名条目),只对该 agent 预设生效 —— 与「全局」层互不覆盖。
|
|
146
|
+
- **技能健康**:`/api/skill-health` 给出技能根的可读性与成员一致性体检。
|
|
147
|
+
- **与内核分工**:会话里的 `/` 技能源与技能工具行由 DSH 内核的 `@deepseek-ai/dsh-client-ui-skill` 负责,本插件不重复注册(重复注册会让内核 apply 抛错并拖垮整条客户端插件加载链);本插件提供的是「能力」面板里的管理面(导入 / 归组 / 开关 / 体检)与它背后的宿主路由。
|
|
148
|
+
|
|
149
|
+
### MCP Server 接入(MCP tab)
|
|
150
|
+
|
|
151
|
+
- **一键接入**:粘贴 JSON / DSH 原生 YAML(`mcpServers` 形态)/ `mcp-client` 行片段,面板先给待写入预览,再写进预设组合或 profile patch —— **写前备份**,重名 server 跳过,非法 serverName / 非布尔 `enabled` 直接拒绝。
|
|
152
|
+
- **预设专属 Server 行**:直接在面板里新增 / 删除某个 agent 预设自带的 Server 行(写进该预设声明行的 `config.plugins`,经 DSH 的 `configEditor` 落盘 profile patch)。
|
|
153
|
+
- **分层遮蔽**:全局行 + 预设层两本账(预设层分「继承全局」与「自带」两侧,各记各的账);预设自带的同名 Server 优先于继承条目;全局关闭 = 一票否决,预设层不能反开(返回 409 并给出说明)。
|
|
154
|
+
- **工具级启停**:按最长匹配把工具归到所属 Server(`my` 与 `my_server` 各自成组),可整台 Server 关掉,也可只关其中几个工具;装配过滤、状态路由、原生目录三处同源。
|
|
155
|
+
- **推荐清单**:`/api/mcp-recommended` 给出常见 MCP Server 的配置模板,照抄即可接入。
|
|
156
|
+
|
|
112
157
|
### 提示词段
|
|
113
158
|
|
|
114
159
|
- **屏蔽**:按名称从装配结果里移除该段。被屏蔽的行不会消失,仍可编辑、恢复 —— 屏蔽只意味着不注入模型。
|
|
115
160
|
- **替换**:编辑该段文本,编辑框预填当前生效文本(动态段会尽力解析出真实内容再回显)。
|
|
116
161
|
- **注入**:每个阶段里「+ 注入」展开表单,按名称 + 文本新建一段,带 `手动` 徽标,随时可删除。
|
|
117
|
-
-
|
|
118
|
-
-
|
|
162
|
+
- **排序**:行首抓手拖动排序,或用行内 ↑/↓ 箭头逐格挪。顺序以 0 起始的连续虚拟下标存储,不会出现重复或小数 order,也不需要手填数字。
|
|
163
|
+
- **拖拽**:把「本系统全部提示词」池里的行拖进列表 = 加入当前阶段(带上池里原文);把行拖回池 = 从当前阶段移除(注入进来的撤销注入,原生段转为屏蔽);把行拖到头部某个**阶段按钮**上 = 复制到那个阶段。拖动中有插入位置提示(行上下的蓝色线),投放区高亮;列表比视口长时,把指针拖到列表上/下边缘会自动滚动(越靠边越快),所以池在折叠区里、目标行在视口外也照样能拖到。
|
|
164
|
+
- **每阶段独立**:引导期屏蔽写 `sectionsBootstrap`,压缩受控期写 `sectionsCompaction`,常驻期写 `sections`;三份名单互不继承、互不影响,排序空间也互不干扰,因此「引导期只留 5 段、常驻期全开」可以分别表达 —— 在一个阶段屏蔽 / 恢复某段,绝不会改变它在其它阶段的状态。从「本系统全部提示词」池把段**拖入**当前阶段即成为当前阶段的独立副本(带原文写入本阶段注入条目)—— 加入阶段只有拖拽这一条路径,池里的行不再挂按钮,界面因此干净一半。(勾选「三态同步」后三态共用同一份列表,见下。)
|
|
119
165
|
- **强制覆盖(`forceSections`,默认开)**:包装宿主的装配入口,最终提示词段直接从注册表原始段重建 —— 预设插件自身的阶段裁段(如 liangshen 引导期 20→1)与 persona 的 `complete: true` 整段接管都无法再改写本插件的屏蔽 / 替换 / 注入 / 排序结果;工具目录与动态上下文保持预设与宿主行为。设为 `false` 退回瀑布流内过滤(此时完整段接管仍会压制段级定制)。
|
|
120
|
-
- **中文提示词开关(面板头部工具栏,刷新按钮旁)**:开启 = 把名字对得上内置译本(`lib/zh
|
|
121
|
-
-
|
|
166
|
+
- **中文提示词开关(面板头部工具栏,刷新按钮旁)**:开启 = 把名字对得上内置译本(`lib/zh/`,25 条目)的段一次性覆盖为中文;关闭 = 清掉这些段的替换文本,回归英文原文。两个方向都写入编辑草稿,与手动编辑同一条保存路径;开关状态从配置探测,不新增字段。自定义段与对不上名字的段不受影响,段数与注入不增不减。`{{...}}` 提示词变量(如 persona 的 `{{model}}` / `{{cwd}}`)、机器路径与代码块逐字保留;`tools:sdk` 只替换前导散文,TS 代码块取自原文。**`tools:sdk` 与 `context:file-reference` 是函数译本**:按该行原文决定译不译(原文为空 = 该段的守卫已把它抹掉,就放弃替换,绝不用译本把一段被守卫消掉的内容复活);开关开启时从「本系统全部提示词」池把段拖进某阶段,也优先取译本 —— 加入即中文,一步接管。
|
|
167
|
+
- **三态同步(头部开关,默认关)**:勾选瞬间即把另外两态覆写为**常驻期的并集行集**(写入编辑草稿,点「保存」生效),之后三个阶段共用同一份段列表与文本 —— 增加 / 删除 / 排序 / 勾选 / 编辑文本都一次写全三个阶段,不再逐阶段各点一遍。行序以常驻期为骨架,只存在于另外两态的段追加在尾部并补进常驻期(同名段以它原生所在那一阶段的行为准);三份屏蔽名单也统一为同一份。动态段(运行时才生成正文,取不到原文)无法凭空建进别的阶段,列表里会给出说明。工具两栏的隐藏 / 显示与加回同样对三个阶段一起生效(只作用于同名的那一项)。
|
|
122
168
|
|
|
123
169
|
### 工具目录
|
|
124
170
|
|
|
125
171
|
- **勾选 = 该阶段对模型可见**(`exclude`):取消勾选即在**该阶段**隐藏这个工具,勾回来就恢复,被隐藏的行半透明原位保留。没有白名单模式 —— 它会让 `exclude` 整体失效,一次勾选就能悄悄关掉该阶段其它所有工具,而表达能力并不比反选更多。
|
|
126
172
|
- **「加回」名单(`add`)**:被该阶段裁掉、但**注册表里仍有**的工具,可从「本系统全部工具」池加入该阶段 —— 装配时在过滤**之后**从注册表查回 schema 追加(`exclude` 优先:同一名字两边都在 = 隐藏)。注册表里根本没有的工具(别的预设独有)加不进来,界面会明确说明。
|
|
127
173
|
- **阶段目录**:引导期与压缩受控期各有独立的 exclude / add 名单,运行时优先级为 **压缩受控期 > 引导期 > 静态** —— 压缩后仍未晋级且配了压缩目录就用它,否则未晋级时用引导目录,其余情况用静态过滤。
|
|
174
|
+
- **拖拽**:把池里的工具拖进列表 = 让它在当前阶段出现(加回或从黑名单放出来);把行拖回池 = 从当前阶段拿掉(已加回的撤销加回,原生工具则该阶段隐藏);拖到头部某个**阶段按钮**上 = 复制到那个阶段,**隐藏 / 启用状态一起带过去**,源阶段不动。加回阶段同样只有拖拽这一条路径。
|
|
128
175
|
- **扩充的天花板是注册表**:只能把注册表里已有的工具加回某个阶段;要让一个全新工具出现,得改预设的组成,不是这里。
|
|
129
176
|
- 只影响面向模型的目录;工具本身与路由照常工作。
|
|
130
177
|
|
|
@@ -154,7 +201,15 @@
|
|
|
154
201
|
|
|
155
202
|
### 跨预设的注册表池
|
|
156
203
|
|
|
157
|
-
「本系统全部提示词 /
|
|
204
|
+
「本系统全部提示词 / 本系统全部工具」不随编辑目标切换:它是一个**跨预设、只增不减的并集**(同名后见到者赢,新名追加),存放在 `catalog.yaml` 这个派生缓存里,删掉会自行重建。
|
|
205
|
+
|
|
206
|
+
池子的内容来自三处,**面板一打开就成立**:
|
|
207
|
+
|
|
208
|
+
- **当前 scope 的注册表**(你正在编辑的那个预设);
|
|
209
|
+
- **agent 作用域里注册的段**:有一批段是插件按 agent 动态 `section()` 的(例如 `tool:subagent`、`context:file-reference`),它们既不进全局层也不进任何预设 scope,只在真实会话装配里出现。它们是「每个 agent 都成立」的段、**不是某个模式独有的**,所以清单在请求时就按 agent 作用域现 merge 出来 —— 打开面板就能看到,不必先跑一轮会话;
|
|
210
|
+
- **浏览过的其它预设**:只在某个预设里注册的段 / 工具(如 `tools:ptc-only`、`raw-html-v2:vcp`)随你切换编辑目标逐步并进来 —— 这部分按需增长,是本插件的取舍(逐个挂载预设 tree 慢到十几秒,不值得每次开面板都跑)。
|
|
211
|
+
|
|
212
|
+
真实会话装配那次也会把看到的段 / 工具并进登记表,作为没有活 agent 时的兜底。
|
|
158
213
|
|
|
159
214
|
## 工作原理
|
|
160
215
|
|
|
@@ -171,7 +226,7 @@
|
|
|
171
226
|
<td width="34%"><b>④ 装配瀑布流</b><br><code>system-prompt/assemble</code> 钩子按会话推导阶段,<br>解析出生效配置并过滤</td>
|
|
172
227
|
</tr>
|
|
173
228
|
<tr>
|
|
174
|
-
<td colspan="9" align="center"><b>④ 内部依次是</b>:<code>
|
|
229
|
+
<td colspan="9" align="center"><b>④ 内部依次是</b>:<code>effectiveConfig(overrides[会话所属预设])</code>(0.9.0 起无全局基底)→ 段:屏蔽 → 替换 → 注入 → 按 order 排序;工具:<code>pickToolsFilter</code>(压缩 > 引导 > 静态)→ <code>applyToolFilter</code></td>
|
|
175
230
|
</tr>
|
|
176
231
|
<tr>
|
|
177
232
|
<td colspan="9" align="center">
|
|
@@ -210,11 +265,55 @@ dsh plugin --profile web add dsh-prompt-customizer
|
|
|
210
265
|
dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
211
266
|
```
|
|
212
267
|
|
|
268
|
+
### 版本要求
|
|
269
|
+
|
|
270
|
+
当前版本(0.9.0)对齐 **DSH 0.2.0-rc.2**(`peerDependencies` 为 `^0.2.0-rc.2`)。
|
|
271
|
+
|
|
272
|
+
**0.9.0 行为变更:全局层停用。** 提示词必须按具体 agent 预设修改——「全部 Agent」
|
|
273
|
+
编辑入口移除,`/config/apply` 无 `target` 一律 400,`/config/set|unset` 拒绝六个
|
|
274
|
+
提示词字段;运行时生效配置 = 该会话所属预设的 `overrides[id]`,未定制的字段 =
|
|
275
|
+
官方默认。config.yaml 顶层的旧全局层数据**保留不再生效**(可手编回滚;0.8.x 及
|
|
276
|
+
以前的行为请用 0.8.1)。
|
|
277
|
+
0.1.7 起的图标代际(`IconXxxOutlineRegular/Medium`,尺寸由 `size` 属性给)在
|
|
278
|
+
0.2.0 沿用,本插件的宿主接触面(`system-prompt` 装配 waterfall、`agentPresets`、
|
|
279
|
+
`configEditor`、`tools.restrict/schemas`、`webServer.register`、客户端模块加载缝)
|
|
280
|
+
在 0.1.7-rc.1 → 0.2.0-rc.2 走廊内均无破坏性变更,故客户端 bundle 与 0.7.x 同代。
|
|
281
|
+
不支持 0.1.7 之前的主机(旧宿主会按 DSH 的插件兼容性策略拒绝加载,而不是半静默
|
|
282
|
+
地坏掉)。需要旧版的请用 0.6.x。
|
|
283
|
+
|
|
284
|
+
0.8.1 起 MCP 全局 Server 的管理目标(`/api/triad/mcp-status` / `mcp-config` 读写
|
|
285
|
+
的 `cordis.patch.yml`)随宿主 `profileContext` 解析——Web 端(profile `web`)与
|
|
286
|
+
**桌面端**(profile `desktop`,默认端口 19387)各管各的 profile,不再写死 `web`。
|
|
287
|
+
宿主面缺失时按 `DSH_DESKTOP_PROFILE` 环境变量回落(桌面壳 v4 形态),最后回落
|
|
288
|
+
`web` 保持旧行为。注意桌面 profile 没有 `patchReload: live`:切换 / 粘贴 / 删除
|
|
289
|
+
MCP Server 写盘成功后需**重启桌面端**才生效(Web 端热重载语义不变)。
|
|
290
|
+
|
|
291
|
+
预设专属 MCP Server 的存储位置自 0.1.7 起:预设不再是磁盘上的组合
|
|
292
|
+
文件,而是声明式插件行(`name: '@deepseek-ai/dsh-agent-preset'`),专属 Server
|
|
293
|
+
写在它的 `config.plugins` 里;本插件经 DSH 的 `configEditor` 把改动写回 profile
|
|
294
|
+
的 `cordis.patch.yml`(首次改动会整份覆盖该预设行的 plugins,此后该预设不再跟随
|
|
295
|
+
DSH 升级更新 —— 与 DSH 自带编辑器同一语义,面板上有提示)。
|
|
296
|
+
|
|
297
|
+
### 桌面端(DeepSeek Harness Desktop)
|
|
298
|
+
|
|
299
|
+
桌面端是 Electron 壳包着同一套 dsh Web 应用,宿主 profile 固定为 `desktop`
|
|
300
|
+
(默认端口 19387,与 Web 的 3080 互不相扰)。安装走同一条官方命令,只是 profile
|
|
301
|
+
名不同:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
dsh plugin --profile desktop add dsh-prompt-customizer
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
客户端 bundle、侧边栏「能力」入口、SKILL / MCP / 提示词三个 tab 在桌面端行为与
|
|
308
|
+
Web 端一致(桌面端经 `dsh-app://` 协议转发请求并附带认证 cookie,插件全部使用
|
|
309
|
+
相对路径请求,无需任何特判)。唯一语义差异是上文提到的:桌面 profile 不做
|
|
310
|
+
`patchReload: live`,MCP 全局 Server 变更需要重启桌面端。
|
|
311
|
+
|
|
213
312
|
安装后打开 dsh Web UI,侧边栏浏览区上方就是 **提示词** 入口(点击打开覆盖主区的「提示词管理」面板)。
|
|
214
313
|
|
|
215
314
|
## 使用说明
|
|
216
315
|
|
|
217
|
-
|
|
316
|
+
侧边栏点 **能力**,在面板顶部选 **提示词** tab。第一行是 agent 预设 chips(0.9.0 起「全部 Agent」已移除——提示词必须按具体预设修改),选中某个预设后改动只对该预设生效,预设未定制的字段 = 官方默认;chips 上的橙色数字告诉你它已经单独改了几项。第二行是定制头部:模式切换(提示词 / 工具 / 配置)、阶段 Tab(引导期 / 压缩受控期 / 常驻期 —— 左栏列表与右栏预览联动切换同一个阶段)、三态同步与中文提示词开关、保存 / 刷新 / 关闭。
|
|
218
317
|
|
|
219
318
|
### 提示词模式
|
|
220
319
|
|
|
@@ -227,11 +326,13 @@ dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
|
227
326
|
- **「+ 注入」**:展开表单按名称 + 文本新建一段(阶段锁定为当前 Tab)
|
|
228
327
|
- **底部三态过滤**:全部 / 已启用 / 已停用,只统计当前阶段
|
|
229
328
|
|
|
230
|
-
|
|
329
|
+
「本系统全部提示词」只读池收在列表末尾的折叠区:**把行拖进上面的列表**即加入当前阶段,**拖到头部阶段按钮**即加入那个阶段(行上不再有按钮)。右栏实时预览当前阶段装配后的最终系统提示词 —— 编辑草稿的效果同步呈现,面板会标注「预览尚未保存草稿」。
|
|
330
|
+
|
|
331
|
+
勾选头部的**三态同步**后,三个阶段共用同一份段列表与文本(常驻期为骨架,另外两态独有的段追加在尾部并补进常驻期):切哪个阶段 Tab 看到的都是同一份列表,增删 / 排序 / 勾选 / 编辑文本一次写全三态 —— 适合「三个阶段要一样的提示词」;不勾选则三态各管各的,适合「引导期只留几段、晋级后全开」。
|
|
231
332
|
|
|
232
333
|
### 工具模式
|
|
233
334
|
|
|
234
|
-
左栏列出该阶段进入过滤的目录 ∪ 你加回的工具:勾选 = 该阶段对模型可见,取消 =
|
|
335
|
+
左栏列出该阶段进入过滤的目录 ∪ 你加回的工具:勾选 = 该阶段对模型可见,取消 = 在该阶段隐藏。「本系统全部工具」池收在折叠区:**把行拖进列表**即让它在当前阶段出现,**拖到阶段按钮**即复制到那个阶段(隐藏状态一起带过去),**拖回池**即从当前阶段拿掉。想看模型真正拿到几个工具,看右栏预览的「模型可见 / 注册表总数」对照。
|
|
235
336
|
|
|
236
337
|
### 配置模式
|
|
237
338
|
|
|
@@ -245,6 +346,8 @@ dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
|
245
346
|
|
|
246
347
|
- `~/.dsh/prompt-customizer/config.yaml` —— 唯一权威配置。首次启动时若该文件不存在而旧版 `settings.yaml` 里有对应段落,会自动迁移一次(主文档原样保留,可回滚)。手工编辑即时生效,无需重启。
|
|
247
348
|
- `~/.dsh/prompt-customizer/catalog.yaml` —— 跨预设的段 / 工具累积登记表,纯派生缓存,删掉会随浏览重建。
|
|
349
|
+
- 技能侧:技能根目录下的 `.bundles.json`(技能包账本,记规范名与目录名两套写法)与技能开关账本(全局层 + 各预设层)。
|
|
350
|
+
- MCP 侧:`$DSH_HOME` 下本插件目录里的 `preset-masks.json`(分层遮蔽账本)与 `tool-disable.json`(工具级启停账本);预设自带 Server 行写在该 agent 预设声明行的 `config.plugins` 里,由 DSH 的 `configEditor` 原子写回 profile 的 `cordis.patch.yml`(全局 Server 仍由本插件直接编辑该 patch,写前备份)。
|
|
248
351
|
|
|
249
352
|
## 已知限制
|
|
250
353
|
|
|
@@ -256,6 +359,7 @@ dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
|
256
359
|
- **替换动态生成的段会固定其内容**。像 `app:web-surface` 这类段在装配时实时生成(嵌入当前 Web 端口等)。替换后文本冻结为你编辑时的值,不再跟随运行时变化;想保持跟随请改用屏蔽。
|
|
257
360
|
- **动态段文本解析是尽力而为**。面板读取动态段时会尝试调用其生成函数(只传最小上下文)来回显真实内容;依赖更复杂上下文、一调就抛的段会显示 `<动态生成>`,其余功能不受影响。另外,带 `{{model}}` / `{{cwd}}` 之类变量的段在列表里显示的是**未替换的模板**,右栏预览里才是替换后的最终文本。
|
|
258
361
|
- **导出依赖宿主允许下载**。Web 回退走浏览器下载:若宿主 webview 静默丢弃下载,面板仍会提示「导出成功」而文件没有落地。导入(文件选择)不受影响。
|
|
362
|
+
- **池子是「见过才知道」的并集**。当前 scope 与 agent 作用域的段当场就能列出来,但**只在别的预设里注册**的段 / 工具要等你切到那个编辑目标才进池(逐个挂载预设 tree 太慢,不做预热)。池子对「某个名字属于哪个阶段」没有权威信息 —— 加入阶段时按当前 Tab 落,之后可以随意拖动改位置。
|
|
259
363
|
- **工具目录的扩充以注册表为天花板**,见前文「扩充的天花板是注册表」。
|
|
260
364
|
|
|
261
365
|
## 开发
|
|
@@ -265,7 +369,17 @@ npm install
|
|
|
265
369
|
npm run check # typecheck + build + 全量单测
|
|
266
370
|
```
|
|
267
371
|
|
|
268
|
-
|
|
372
|
+
两侧都由 [esbuild](https://esbuild.github.io/) 一次构建(`node build.mjs`):
|
|
373
|
+
|
|
374
|
+
- `lib/index.js` —— 宿主半身,ESM、自包含,只留 `node:*` 内建依赖;`@deepseek-ai/*` 一律走 `vendor/`(内置叶子模块)或构建期内联,`assertHostExternals()` 会在构建末尾把违规的裸导入直接判失败;
|
|
375
|
+
- `client/client.js` —— 浏览器半身,`__ModuleLoader__` factory bundle;`react*` 与 `@deepseek-ai/*` 走宿主模块表,其余全部内联。
|
|
376
|
+
|
|
377
|
+
阶段性与可测的纯逻辑集中在 `vendor/prompt-customizer/`(提示词策略 / 清单登记表 / 阶段投影)与 `vendor/skill-manager/`:同一份纯函数既被两侧打包使用,也被 `node --test` 直接覆盖,测试即上线代码。`npm run check` = 构建 + 全量单测 + 两半 smoke。
|
|
378
|
+
|
|
379
|
+
## 致谢
|
|
380
|
+
|
|
381
|
+
- 技能(SKILL)与 MCP 两块的**界面与宿主模块来自 [Kr-ATG/dsh-triad](https://github.com/Kr-ATG/dsh-triad)**:技能集合管理、全局 / 预设双层开关、MCP 分层遮蔽与工具级启停,以及侧边栏「能力」入口与 `/` 技能源的交互和视觉都参考该项目;0.6.0 起这两块与提示词定制合并为本插件。
|
|
382
|
+
- 提示词面板派生自本仓库 0.5.x 的独立实现。
|
|
269
383
|
|
|
270
384
|
## License
|
|
271
385
|
|
package/build.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-prompt-customizer — build script.
|
|
3
|
+
*
|
|
4
|
+
* Two bundles from one esbuild run:
|
|
5
|
+
*
|
|
6
|
+
* lib/index.js host half ESM, node platform, self-contained
|
|
7
|
+
* lib/client.js browser half CJS, browser platform, wrapped in the
|
|
8
|
+
* `window.__ModuleLoader__.load` factory contract
|
|
9
|
+
*
|
|
10
|
+
* The host half must stay **resolvable from an installed location**, i.e. from
|
|
11
|
+
* inside `~/.dsh/profiles/<p>/node_modules/`. That rules out importing most
|
|
12
|
+
* `@deepseek-ai/*` packages at runtime: DSH ships those as source only (their
|
|
13
|
+
* `lib/` holds `.d.ts` plus a couple of hand-built entries, no `lib/index.js`),
|
|
14
|
+
* and resolves them at runtime through tsx + `tsconfig.base.json` `paths`.
|
|
15
|
+
* tsx only applies `paths` to importers *outside* node_modules, so a plugin
|
|
16
|
+
* that was installed with `dsh plugin add` gets plain node resolution and
|
|
17
|
+
* lands on a missing file. See vendor/README.md for the full rule and
|
|
18
|
+
* `assertHostExternals()` below for the guard that enforces it.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node build.mjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createRequire } from 'node:module'
|
|
24
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
25
|
+
import { resolve, dirname, join, basename } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
// 可选:指向一份 DSH 检出,用于在没装 esbuild 时借用它 pnpm store 里的副本。
|
|
30
|
+
// 缺省为空 = 只认本仓库 node_modules(正常安装路径)。
|
|
31
|
+
const DSH_CHECKOUT = process.env.DSH_CHECKOUT ?? ''
|
|
32
|
+
const PLUGIN_ID = 'dsh-prompt-customizer'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve esbuild.
|
|
36
|
+
*
|
|
37
|
+
* Three tiers, in order — the first two keep this plugin buildable both for
|
|
38
|
+
* a fresh clone (`pnpm install` puts esbuild in node_modules) and for a
|
|
39
|
+
* contributor hacking inside a DSH checkout:
|
|
40
|
+
*
|
|
41
|
+
* 1. the plugin's own `node_modules` (normal install path; esbuild is a
|
|
42
|
+
* devDependency so `pnpm install` provides it),
|
|
43
|
+
* 2. a local DSH checkout's pnpm store (contributor convenience — pnpm's
|
|
44
|
+
* strict layout keeps esbuild out of the root node_modules, so scan
|
|
45
|
+
* `node_modules/.pnpm/esbuild@*` and take the highest version),
|
|
46
|
+
* 3. a clear, actionable error.
|
|
47
|
+
*
|
|
48
|
+
* Tier 3 matters: a bare `require('esbuild')` failure message does not tell
|
|
49
|
+
* the user to run `pnpm install`.
|
|
50
|
+
*/
|
|
51
|
+
function loadEsbuild() {
|
|
52
|
+
const localRequire = createRequire(resolve(HERE, 'package.json'))
|
|
53
|
+
try {
|
|
54
|
+
return localRequire('esbuild')
|
|
55
|
+
} catch {
|
|
56
|
+
// Not installed locally; fall through to the checkout scan.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const store = join(DSH_CHECKOUT, 'node_modules', '.pnpm')
|
|
60
|
+
const candidates = []
|
|
61
|
+
if (existsSync(store)) {
|
|
62
|
+
for (const entry of readdirSync(store)) {
|
|
63
|
+
if (!entry.startsWith('esbuild@')) continue
|
|
64
|
+
candidates.push(join(store, entry, 'node_modules', 'esbuild'))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (candidates.length > 0) {
|
|
68
|
+
const pick = candidates.sort().at(-1)
|
|
69
|
+
return createRequire(resolve(pick, 'package.json'))(pick)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
'dsh-prompt-customizer: cannot find esbuild.\n'
|
|
74
|
+
+ ' Run `pnpm install` in this directory (esbuild is a devDependency).\n'
|
|
75
|
+
+ ` Or set DSH_CHECKOUT to a DSH checkout to borrow its copy (currently: ${DSH_CHECKOUT}).`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const esbuild = loadEsbuild()
|
|
80
|
+
|
|
81
|
+
/** Platform packages + react come from the DSH module table at runtime. */
|
|
82
|
+
const CLIENT_EXTERNAL = [
|
|
83
|
+
'react',
|
|
84
|
+
'react/jsx-runtime',
|
|
85
|
+
'react-dom',
|
|
86
|
+
'react-dom/client',
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
/** Browser half: one CJS factory registered with the host module loader. */
|
|
90
|
+
const clientBundle = {
|
|
91
|
+
entryPoints: [resolve(HERE, 'src/client/index.ts')],
|
|
92
|
+
// 浏览器半身落在 client/:package.json 的 exports.\"./client\" 指向它。
|
|
93
|
+
outfile: resolve(HERE, 'client/client.js'),
|
|
94
|
+
bundle: true,
|
|
95
|
+
format: 'cjs',
|
|
96
|
+
platform: 'browser',
|
|
97
|
+
target: ['es2020'],
|
|
98
|
+
jsx: 'automatic',
|
|
99
|
+
sourcemap: true,
|
|
100
|
+
logLevel: 'info',
|
|
101
|
+
external: CLIENT_EXTERNAL,
|
|
102
|
+
// Everything under @deepseek-ai/ stays a runtime require.
|
|
103
|
+
plugins: [{
|
|
104
|
+
name: 'pc-external-platform',
|
|
105
|
+
setup(build) {
|
|
106
|
+
build.onResolve({ filter: /^@deepseek-ai\// }, args => ({ path: args.path, external: true }))
|
|
107
|
+
},
|
|
108
|
+
}],
|
|
109
|
+
banner: {
|
|
110
|
+
js: [
|
|
111
|
+
`window.__ModuleLoader__.load({ id: ${JSON.stringify(PLUGIN_ID)}, factory: (require) => {`,
|
|
112
|
+
'var module = { exports: {} };',
|
|
113
|
+
'var exports = module.exports;',
|
|
114
|
+
'Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });',
|
|
115
|
+
].join('\n'),
|
|
116
|
+
},
|
|
117
|
+
footer: {
|
|
118
|
+
js: 'return module.exports; } });',
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The only bare specifiers the host bundle may leave to runtime resolution.
|
|
124
|
+
*
|
|
125
|
+
* A plugin installed into a profile's node_modules can only resolve packages
|
|
126
|
+
* that DSH actually ships runtime JS for. `@deepseek-ai/cordis` does. Anything
|
|
127
|
+
* else under `@deepseek-ai/` must be vendored into `vendor/` instead — see
|
|
128
|
+
* vendor/README.md.
|
|
129
|
+
*
|
|
130
|
+
* This is an allowlist rather than "whatever resolved at build time" on
|
|
131
|
+
* purpose: the build runs from a source checkout where *every* `@deepseek-ai/*`
|
|
132
|
+
* name resolves to a source dir, so a build-time check would pass and the
|
|
133
|
+
* installed plugin would still crash at startup.
|
|
134
|
+
*/
|
|
135
|
+
const HOST_RUNTIME_EXTERNAL_ALLOWLIST = new Set([
|
|
136
|
+
'@deepseek-ai/cordis',
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
/** Host half: ESM, self-contained except node builtins and the allowlist above. */
|
|
140
|
+
const hostBundle = {
|
|
141
|
+
entryPoints: [resolve(HERE, 'src/host.ts')],
|
|
142
|
+
outfile: resolve(HERE, 'lib/index.js'),
|
|
143
|
+
bundle: true,
|
|
144
|
+
format: 'esm',
|
|
145
|
+
platform: 'node',
|
|
146
|
+
target: 'node22',
|
|
147
|
+
sourcemap: true,
|
|
148
|
+
logLevel: 'info',
|
|
149
|
+
// node:* builtins stay external. js-yaml is bundled in on purpose: the
|
|
150
|
+
// plugin's node_modules is a junction into the DSH profile (so @deepseek-ai/*
|
|
151
|
+
// resolves to the very same module instances the host uses), and a junction
|
|
152
|
+
// cannot also hold a private js-yaml copy.
|
|
153
|
+
//
|
|
154
|
+
// `@deepseek-ai/*` is external *at bundle time* only so esbuild keeps the
|
|
155
|
+
// names verbatim; `assertHostExternals()` then checks every surviving name
|
|
156
|
+
// against the allowlist, so an unvendored dependency fails the build here
|
|
157
|
+
// instead of at plugin startup on someone else's machine.
|
|
158
|
+
external: [],
|
|
159
|
+
// js-yaml is inlined (see above) and is CJS: it calls bare `require('fs')`.
|
|
160
|
+
// In an ESM bundle that throws "Dynamic require ... is not supported", so
|
|
161
|
+
// hand the bundle a real CommonJS require bound to its own URL.
|
|
162
|
+
banner: {
|
|
163
|
+
js: [
|
|
164
|
+
"import { createRequire as __pcCreateRequire } from 'node:module';",
|
|
165
|
+
'const require = __pcCreateRequire(import.meta.url);',
|
|
166
|
+
].join('\n'),
|
|
167
|
+
},
|
|
168
|
+
plugins: [{
|
|
169
|
+
name: 'pc-external-platform',
|
|
170
|
+
setup(build) {
|
|
171
|
+
build.onResolve({ filter: /^(@deepseek-ai\/|node:)/ }, args => ({ path: args.path, external: true }))
|
|
172
|
+
},
|
|
173
|
+
}],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fail the build if the host bundle still hands an unresolvable specifier to
|
|
178
|
+
* runtime resolution.
|
|
179
|
+
*
|
|
180
|
+
* Catches exactly the regression that motivated `vendor/`: adding
|
|
181
|
+
* `import { X } from '@deepseek-ai/dsh-llm'` to a host module looks fine on a
|
|
182
|
+
* dev machine (esbuild externalises it, the source checkout resolves it, the
|
|
183
|
+
* smoke test under tsx passes) and then throws ERR_MODULE_NOT_FOUND for every
|
|
184
|
+
* user who installed the plugin.
|
|
185
|
+
*/
|
|
186
|
+
function assertHostExternals(outfile) {
|
|
187
|
+
const source = readFileSync(outfile, 'utf8')
|
|
188
|
+
const specifiers = new Set()
|
|
189
|
+
for (const m of source.matchAll(/(?:^|[;\n])\s*(?:import|export)[\s\S]*?from\s*["']([^"']+)["']/g)) {
|
|
190
|
+
specifiers.add(m[1])
|
|
191
|
+
}
|
|
192
|
+
for (const m of source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)) {
|
|
193
|
+
specifiers.add(m[1])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const violations = [...specifiers].filter((spec) => {
|
|
197
|
+
if (spec.startsWith('node:')) return false
|
|
198
|
+
return !HOST_RUNTIME_EXTERNAL_ALLOWLIST.has(spec)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (violations.length > 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
'dsh-prompt-customizer: host bundle imports packages that an installed plugin cannot resolve.\n'
|
|
204
|
+
+ violations.map(v => ` - ${v}`).join('\n')
|
|
205
|
+
+ '\n\n'
|
|
206
|
+
+ 'DSH ships @deepseek-ai/* as source only; a plugin inside a profile\'s\n'
|
|
207
|
+
+ 'node_modules gets plain node resolution and finds no lib/index.js.\n'
|
|
208
|
+
+ 'Vendor the leaf modules you need into vendor/ (see vendor/README.md),\n'
|
|
209
|
+
+ `or add the name to HOST_RUNTIME_EXTERNAL_ALLOWLIST in ${basename(fileURLToPath(import.meta.url))}\n`
|
|
210
|
+
+ 'after verifying the package really ships runtime JS.',
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
return [...specifiers]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await Promise.all([esbuild.build(clientBundle), esbuild.build(hostBundle)])
|
|
217
|
+
const hostExternals = assertHostExternals(resolve(HERE, 'lib/index.js'))
|
|
218
|
+
console.log('[dsh-prompt-customizer] built lib/index.js + client/client.js')
|
|
219
|
+
console.log(`[dsh-prompt-customizer] host runtime imports: ${hostExternals.length === 0 ? '(none)' : hostExternals.join(', ')}`)
|