dsh-chat-preset 0.0.0-stage → 1.2.0
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/LICENSE +21 -0
- package/README.md +280 -2
- package/cordis.patch.yml +49 -0
- package/index.js +480 -0
- package/package.json +21 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lwb6
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,281 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-chat-preset
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> ChatOnly —— 本项目提供的预设 agent。抑制工具注入与工作目录注入,使请求仅包含用户输入,以及可选的自定义系统提示词。
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Xxx-Bin/dsh-chat-preset/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/dsh-chat-preset)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
> **安装前须知。** 本包不依赖公开插件 API,其行为由 DSH 的三处内部语义决定(作用域同名 shadow 覆盖、`system-prompt/assemble` 瀑布、`refreshContext()` 的合并顺序)。上述语义变更时,本包会**静默失效**:不报错、不崩溃,仅退回默认请求形态。详见[兼容性与脆弱性](#兼容性与脆弱性)。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 概述
|
|
15
|
+
|
|
16
|
+
DSH 默认的最小配置是精简模式。即便在该模式下,请求仍包含两类注入,且均无法在预设配置层消除:
|
|
17
|
+
|
|
18
|
+
1. **工具 schema。** 工具在全局层收集,与预设作用域无关,预设只能追加、不能移除。
|
|
19
|
+
2. **工作目录消息。** DSH 默认在用户输入之后追发一条 `runtime-context` 用户消息,内容为当前工作目录 `Current working directory: C:\…`;该 contribution 以 `required: true` 注册,`includeRuntimeContext: false` 只过滤可选 contribution。
|
|
20
|
+
|
|
21
|
+
本包提供 ChatOnly 预设 agent,在装配瀑布与 context 注册层分别消除这两类注入,并将系统提示词改为文件热更新,用于受控条件下的模型能力评测。
|
|
22
|
+
|
|
23
|
+
## 请求形态
|
|
24
|
+
|
|
25
|
+
测量环境:DSH 桌面端,`desktop` profile。
|
|
26
|
+
|
|
27
|
+
```diff
|
|
28
|
+
# 抑制前
|
|
29
|
+
- tools: 9 个 — acp_cache, acp_read_original_image, acp_status, compress,
|
|
30
|
+
- decompress, external_plugins, load_workspace_dependencies,
|
|
31
|
+
- search_context, working_directory
|
|
32
|
+
- messages: [ …, { source: { kind: "runtime-context" } } ]
|
|
33
|
+
- # 用户输入之后追发的工作目录消息
|
|
34
|
+
# 抑制后
|
|
35
|
+
+ tools: 该键不存在
|
|
36
|
+
+ messages: 仅用户输入
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
工具数量随已安装插件变化,本机观测区间为 **8–14 个**。
|
|
40
|
+
|
|
41
|
+
## 预设层的限制
|
|
42
|
+
|
|
43
|
+
| 目标 | 预设层的限制 |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 移除工作目录消息 | 该 contribution 注册为 `required: true`;`suppressRuntimeContext()` 仅过滤可选项 |
|
|
46
|
+
| 移除全局工具 schema | 工具在全局层收集,预设作用域只可追加;不存在 `suppressTools()` |
|
|
47
|
+
| 改写 `agent/pre-step` 决策 | `dsh-agent-loop` 的 `step()` 会重新计算上下文并写入 surface,从 decision 中移除消息只会使其在 fallback 分支重建 |
|
|
48
|
+
|
|
49
|
+
本包因此改由三处机制实现:agent 作用域内的同名空 shadow、`system-prompt/assemble` 瀑布、以及 `buildRequest()` 在工具列表为空时省略该键的行为。推导过程与源码行号见 [`index.js`](./index.js) 头部注释。
|
|
50
|
+
|
|
51
|
+
## 安装
|
|
52
|
+
|
|
53
|
+
依赖:**dsh**(桌面端或 CLI)、`pnpm` 位于 `PATH`(`dsh plugin add` 内部转发 `pnpm add`)、**Node.js ≥ 18**。
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# npm
|
|
57
|
+
dsh plugin --profile desktop add dsh-chat-preset
|
|
58
|
+
|
|
59
|
+
# GitHub
|
|
60
|
+
dsh plugin --profile desktop add github:Xxx-Bin/dsh-chat-preset
|
|
61
|
+
|
|
62
|
+
# 本地路径(修改代码后需重启 dsh,模块存在缓存)
|
|
63
|
+
git clone git@github.com:Xxx-Bin/dsh-chat-preset.git
|
|
64
|
+
dsh plugin --profile desktop add /absolute/path/to/dsh-chat-preset
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--profile` 为 profile 名,即 `~/.dsh/profiles/` 下的目录名;桌面端默认为 `desktop`。`add` 在该目录执行 `pnpm add`,随后核对 `dsh.profile.bundles`:凡已安装且 `package.json` 声明 `dsh.bundle` 的依赖会被追加进 layer stack。本包声明 `dsh.bundle.patch`,安装后自动登记。
|
|
68
|
+
|
|
69
|
+
**安装后须重启 dsh。** bundles 层仅在启动时合成,运行期不监听变更。重启后在预设选择中启用 **ChatOnly**。
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>手动安装(不依赖 dsh CLI)</summary>
|
|
73
|
+
|
|
74
|
+
1. 在 `~/.dsh/profiles/desktop/package.json` 的 `dependencies` 中添加 `"dsh-chat-preset": "link:/absolute/path/to/dsh-chat-preset"`
|
|
75
|
+
2. 在同一文件的 `dsh.profile.bundles` 数组中添加 `"dsh-chat-preset"`
|
|
76
|
+
3. 在该目录执行 `pnpm install`
|
|
77
|
+
4. 重启 dsh
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
## 使用
|
|
82
|
+
|
|
83
|
+
### 系统提示词:`/chatprompt`
|
|
84
|
+
|
|
85
|
+
提示词存于 `~/.dsh/chatonly-system-prompt.md`,命令仅作为读写入口,亦可直接编辑文件。
|
|
86
|
+
|
|
87
|
+
| 命令 | 行为 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `/chatprompt <text>` | 写入文件,下一条消息生效 |
|
|
90
|
+
| `/chatprompt` | 输出预览、字符数与文件路径 |
|
|
91
|
+
| `/chatprompt show` | 输出全文 |
|
|
92
|
+
| `/chatprompt clear` | 清空文件;此后不发送系统提示词 |
|
|
93
|
+
|
|
94
|
+
文件为空时不发送任何系统消息,请求中仅含用户输入。`deployment:persona-prefix` 段由本包以 `complete: true` 独占,不再拼接部署层其它文本。
|
|
95
|
+
|
|
96
|
+
> 该命令全局注册,可在任意预设下调用,但仅影响 ChatOnly;在其它预设下修改不会生效,亦无提示。
|
|
97
|
+
|
|
98
|
+
### 工具抑制开关:`/chattools`
|
|
99
|
+
|
|
100
|
+
默认抑制,即请求不携带任何工具 schema。
|
|
101
|
+
|
|
102
|
+
| 命令 | 行为 |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `/chattools` 或 `/chattools status` | 输出当前模式与状态文件路径 |
|
|
105
|
+
| `/chattools on` | 抑制工具注入 |
|
|
106
|
+
| `/chattools off` | 放行,`assembly.tools` 原样传递 |
|
|
107
|
+
| `/chattools reset` | 删除状态文件,回退出厂默认 |
|
|
108
|
+
|
|
109
|
+
状态存于 `~/.dsh/chatonly-strip-tools.state`,内容为 `strip` 或 `pass`。每次装配重新读取,切换后下一条消息生效;文件不存在时采用 `config.stripTools`。参数接受 `on`/`strip`/`intercept`/`block`/`true`/`1` 与 `off`/`pass`/`allow`/`bypass`/`false`/`0` 两组同义词,无法识别时返回错误且不改变状态。
|
|
110
|
+
|
|
111
|
+
放行状态下,本包原样返回下游装配结果,其它插件的工具追加与过滤逻辑均按原有顺序生效。本包不提供工具,仅在抑制状态下清空列表。
|
|
112
|
+
|
|
113
|
+
> 该命令全局注册且作用于所有 ChatOnly 会话:状态是单一文件,不按会话或 agent 区分。
|
|
114
|
+
|
|
115
|
+
#### 两种抑制相互独立
|
|
116
|
+
|
|
117
|
+
`/chattools off` 仅放行工具。`working-directory:current` 在该会话中仍为空,模型不获得工作目录及其它环境信息。
|
|
118
|
+
|
|
119
|
+
若启用文件类工具而未提供工作目录,模型缺少定位依据,评测结果将混入环境缺失的影响。因此放行工具时应同时满足两项条件:
|
|
120
|
+
|
|
121
|
+
1. 通过 `dsh-tool-manager` 等插件将工具集限制为本次评测所需的最小集合。全局注入的 8–14 个工具签名不属于评测变量。
|
|
122
|
+
2. 在系统提示词中补充工作目录与环境约束。抑制状态下,该文件是唯一的环境信息来源。
|
|
123
|
+
|
|
124
|
+
### 配置
|
|
125
|
+
|
|
126
|
+
`cordis.patch.yml` 以 `config.role` 分流两处挂载:
|
|
127
|
+
|
|
128
|
+
| role | 挂载位置 | 职责 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `policy`(默认) | 全局 | 抑制工作目录注入、按开关抑制工具、注册 `/chatprompt` 与 `/chattools` |
|
|
131
|
+
| `section` | ChatOnly 预设 agent 的 `plugins` | 占用 persona 槽位,从文件读取系统提示词 |
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
- insert:
|
|
135
|
+
# 行 1:全局挂载(policy)
|
|
136
|
+
- id: chatonly-context-policy
|
|
137
|
+
name: 'dsh-chat-preset'
|
|
138
|
+
config:
|
|
139
|
+
role: 'policy'
|
|
140
|
+
presetId: 'chatonly' # 须与预设行的 config.id 一致
|
|
141
|
+
contextName: 'working-directory:current' # 被覆盖的 contribution 名
|
|
142
|
+
stripTools: true # 出厂默认;运行期由 /chattools 切换
|
|
143
|
+
toolsStateFile: '~/.dsh/chatonly-strip-tools.state'
|
|
144
|
+
file: '~/.dsh/chatonly-system-prompt.md' # /chatprompt 写入目标
|
|
145
|
+
|
|
146
|
+
# 行 2:定义预设 ChatOnly,并在其 plugins 内挂载 section
|
|
147
|
+
- id: preset-chat
|
|
148
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
149
|
+
config:
|
|
150
|
+
id: chatonly
|
|
151
|
+
name: ChatOnly
|
|
152
|
+
order: 5
|
|
153
|
+
plugins:
|
|
154
|
+
- id: prompt
|
|
155
|
+
name: 'dsh-chat-preset'
|
|
156
|
+
config:
|
|
157
|
+
role: 'section'
|
|
158
|
+
file: '~/.dsh/chatonly-system-prompt.md' # 装配读取来源
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**`presetId` 必须与预设行的 `config.id` 一致。** 两者不同时,`policy` 行不会作用于该预设,注入照常存在。
|
|
162
|
+
|
|
163
|
+
**两处 `file` 必须一致。** 写入与读取路径不同时,`/chatprompt` 会报告保存成功而系统提示词从未生效。
|
|
164
|
+
|
|
165
|
+
**`toolsStateFile` 不得与 `file` 指向同一路径。** 开关文件按单行关键字解析,读到提示词全文时识别失败并永久回退出厂默认;反向更糟,`/chattools on|off` 会把提示词覆盖成一行 `strip`/`pass`。
|
|
166
|
+
|
|
167
|
+
**`stripTools` 是出厂默认值,不是总开关。** 状态文件存在时以其为准;设为 `false` 不会禁用 `/chattools`,仅改变未切换时的默认行为。需要彻底禁用抑制行为时应卸载本包。
|
|
168
|
+
|
|
169
|
+
## 兼容性与脆弱性
|
|
170
|
+
|
|
171
|
+
本包依赖三处非公开行为:
|
|
172
|
+
|
|
173
|
+
| 依赖 | 失效后果 |
|
|
174
|
+
|---|---|
|
|
175
|
+
| agent 作用域同名 context 覆盖全局同名 context | shadow 不生效,工作目录消息恢复 |
|
|
176
|
+
| `required: false` + `text: ''` 使 contribution 被过滤、投影返回 `undefined` | 空字符串被视为内容,请求中多出一条空 `runtime-context` |
|
|
177
|
+
| `buildRequest()` 在 `tools` 为空时省略该键 | 该键变为 `tools: []`,多数模型 API 会拒绝 |
|
|
178
|
+
|
|
179
|
+
失效时无报错、无异常,仅退回默认形态。升级 dsh 后应依次确认:bundles 仍然登记(见[排障](#确认生效)第 1 条);新建 ChatOnly 会话的请求中不存在 `tools` 键。
|
|
180
|
+
|
|
181
|
+
开发与测量环境为 DSH 桌面端 `desktop` profile;CLI profile 原理相同,未逐项验证。
|
|
182
|
+
|
|
183
|
+
## 限制
|
|
184
|
+
|
|
185
|
+
1. **无上下文压缩。** 未挂载压缩插件,不支持自动压缩与 `/compact`。对话超出模型上下文窗口时请求直接失败。
|
|
186
|
+
2. **与 `dsh-prompt-setting` 的「我的 Prompt」互斥。** `complete: true` 会覆盖同作用域内的其它段(参见该项目 `core/experiments.js` E3),两者不可共存。
|
|
187
|
+
3. **同一作用域内同名 section 只能注册一次。** 预设内不应再挂载 `dsh-persona`,否则抛错。
|
|
188
|
+
4. **会话内切换预设不撤销工作目录抑制。** shadow 在 agent 作用域一次性注册且无卸载路径。由 ChatOnly 切换至其它预设后,该会话的 `working-directory:current` 仍为空,直至新建会话。
|
|
189
|
+
5. **旧会话不追溯。** 修复前已落库的工作目录消息仍然存在;此类会话下次对话会多出一条一次性的 `Current runtime context: none.`。
|
|
190
|
+
6. **工具开关为全局状态。** 其值为单一文件,`/chattools off` 对所有已打开的 ChatOnly 会话同时生效。工作目录抑制则按 agent 注册且不可撤销。两者机制不同,行为不对称。
|
|
191
|
+
|
|
192
|
+
## 卸载与清理
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# 1. 从 profile 移除:dependencies 与 dsh.profile.bundles 各删除一条
|
|
196
|
+
# ~/.dsh/profiles/desktop/package.json
|
|
197
|
+
# 随后在该目录执行 pnpm install 并重启 dsh
|
|
198
|
+
|
|
199
|
+
# 2. 系统提示词文件(用户内容,按需保留)
|
|
200
|
+
rm ~/.dsh/chatonly-system-prompt.md
|
|
201
|
+
|
|
202
|
+
# 3. 工具开关状态
|
|
203
|
+
rm ~/.dsh/chatonly-strip-tools.state
|
|
204
|
+
|
|
205
|
+
# 4. 调试日志
|
|
206
|
+
rm ~/.dsh-filter-rtc-debug.log
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`dsh plugin remove`(或桌面端插件页的卸载)**不会**动上面第 2–4 项。它的流程是:从 `dsh.profile.bundles` 摘除 → 卸载运行期 contribution → `pnpm remove` → 重新解析。插件 API 没有卸载钩子,流程也不扫描插件写过哪些文件,因此这几步只能手动执行。
|
|
210
|
+
|
|
211
|
+
不清理不算残留故障:卸载会 dispose 全部运行期注册(命令、section、监听器、shadow),行为当场回到默认;上述文件此后无人读取,只是留在主目录。唯一的真实影响是**重装沿用旧开关** —— `chatonly-strip-tools.state` 仍在时 `/chattools` 不回出厂默认,而是上次写入的 `strip`/`pass`;要拿到干净初值,先执行 `/chattools reset`。
|
|
212
|
+
|
|
213
|
+
Windows 下 `rm` 不可用:PowerShell 用 `Remove-Item`,cmd 用 `del`。
|
|
214
|
+
|
|
215
|
+
### 调试日志
|
|
216
|
+
|
|
217
|
+
`index.js` 向 `~/.dsh-filter-rtc-debug.log` 同步追加 JSON 行,记录每次装配、每次工具剥离、每次模式切换与每次 shadow 注册。日志无大小上限与轮转机制,长期使用会持续增长。
|
|
218
|
+
|
|
219
|
+
`{"evt":"strip-tools","count":9,...}` 为抑制生效的记录,可按 `sessionId` 核对具体会话。该日志为调试期脚手架,删除后会在下次触发时重建。
|
|
220
|
+
|
|
221
|
+
## 确认生效
|
|
222
|
+
|
|
223
|
+
按序检查,用 `desktop` 举例:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# 1. 依赖与 bundles 登记状态(可同时区分"未安装"与"已安装未登记")
|
|
227
|
+
node -e "const os=require('os'),fs=require('fs');const f=os.homedir()+'/.dsh/profiles/desktop/package.json';if(!fs.existsSync(f)){console.log('profile 不存在: '+f);process.exit(0)}const p=JSON.parse(fs.readFileSync(f,'utf8'));console.log('dep :',p.dependencies?.['dsh-chat-preset']??'<未安装>');const b=p.dsh?.profile?.bundles;console.log('bundles:',Array.isArray(b)?(b.includes('dsh-chat-preset')?'已登记':'未登记'):'<无此字段>')"
|
|
228
|
+
|
|
229
|
+
# 2. 依赖树中存在该包(应输出 dsh-chat-preset@<版本>)
|
|
230
|
+
|
|
231
|
+
# Linux / macOS
|
|
232
|
+
dsh plugin --profile desktop ls | grep dsh-chat-preset
|
|
233
|
+
|
|
234
|
+
# Windows(PowerShell,无 grep)
|
|
235
|
+
dsh plugin --profile desktop ls | Select-String dsh-chat-preset
|
|
236
|
+
# Windows(cmd,无 grep)
|
|
237
|
+
dsh plugin --profile desktop ls | findstr dsh-chat-preset
|
|
238
|
+
|
|
239
|
+
# 3. 重启 dsh —— bundles 层仅在启动时合成
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
前两步仅确认安装状态,第三步之后才涉及生效。Windows 用户以 git-bash 执行时可沿用 `grep` 写法。
|
|
243
|
+
|
|
244
|
+
| 第 1 步输出 | 结论 |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `dep: <未安装>` | 依赖未写入,重新安装 |
|
|
247
|
+
| `dep: ^1.2.0` + `bundles: 未登记` | 已安装但未进入 layer stack,手动补充 `dsh.profile.bundles` |
|
|
248
|
+
| 两项均正常,但 ChatOnly 不在预设列表 | 未重启,或重启的 profile 不符 |
|
|
249
|
+
|
|
250
|
+
> `dsh --profile desktop --dump-config` 会报 *profile "desktop" is managed exclusively by the Electron application*。桌面端 profile 不允许 CLI 渲染配置树;如需检查合成结果,可在 CLI 管理的 profile(`web` / `headless`)中安装一次。
|
|
251
|
+
|
|
252
|
+
## 开发
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
node test.mjs
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
25 项测试,全部通过。基于 `@deepseek-ai/dsh-system-prompt` 与 `agent-loop step()` 的等价模型,覆盖作用域隔离、预设判定、幂等性、能力缺失时的降级、工具瀑布的两种模式(抑制清空/放行后其它插件的增删可见)以及 `/chattools` 的切换语义。仅依赖 `node:` 内置模块。
|
|
259
|
+
|
|
260
|
+
测试使用等价装配模型而非 DSH 本体;真实链路仍以新建 ChatOnly 会话检查请求为准。
|
|
261
|
+
|
|
262
|
+
CI 在 Node 18/20/22 上运行同一套测试(`.github/workflows/ci.yml`)。
|
|
263
|
+
|
|
264
|
+
## 发布
|
|
265
|
+
|
|
266
|
+
推版本标签即触发 npm 发布(`.github/workflows/publish.yml`):
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npm version patch # 或 minor / major,同时打 tag(如 v1.2.0)
|
|
270
|
+
git push --follow-tags
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
工作流依次执行:升级 npm(OIDC 需 ≥ 11.5.1)→ 测试 → 校验 tag 与 `package.json` 版本一致 → `npm publish --access public --provenance`。
|
|
274
|
+
|
|
275
|
+
发布使用 npm **Trusted Publishing**(OIDC),不需要任何仓库 Secret。npm 侧需为该包登记 publisher:GitHub 仓库 `Xxx-Bin/dsh-chat-preset`、workflow 文件名 `publish.yml`;两者之一对不上,认证即失败。工作流内 `NODE_AUTH_TOKEN` 被显式置空——`actions/setup-node` 会注入占位值,非空时会压过 OIDC。
|
|
276
|
+
|
|
277
|
+
> 本机 `~/.npmrc` 若将 registry 指向镜像(如 `registry.npmmirror.com`),手动发布需显式指定 `--registry https://registry.npmjs.org`;工作流的 `setup-node` 已固定为官方源。
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
[MIT](./LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ChatOnly in one package: the preset row, the context policy, and the system
|
|
2
|
+
# prompt that belongs to it.
|
|
3
|
+
#
|
|
4
|
+
# Row 1 (`chatonly-context-policy`, mounted globally) loads THIS package's
|
|
5
|
+
# index.js with role `policy`:
|
|
6
|
+
# - shadows the required `working-directory:current` contribution, so the
|
|
7
|
+
# runtime-context snapshot never reaches the request or the session log;
|
|
8
|
+
# - clears `assembly.tools`, because global tool providers are collected for
|
|
9
|
+
# every preset and a preset row cannot remove them. That interception is a
|
|
10
|
+
# LIVE switch, not this file: `stripTools` is only the factory default, the
|
|
11
|
+
# current value lives in `toolsStateFile` and is flipped with
|
|
12
|
+
# `/chattools on|off`. With the switch off the assembly is passed through
|
|
13
|
+
# untouched, so other plugins own the tool list again;
|
|
14
|
+
# - registers `/chatprompt`, the composer-side editor for the prompt file.
|
|
15
|
+
#
|
|
16
|
+
# Row 2 (`preset-chat`) defines the preset. Its `plugins` list mounts THIS
|
|
17
|
+
# package again with role `section`, which owns the `deployment:persona-prefix`
|
|
18
|
+
# slot (complete: true) and reads its text from
|
|
19
|
+
# `~/.dsh/chatonly-system-prompt.md` on every assembly — edit the file (or send
|
|
20
|
+
# `/chatprompt …`) and the next message uses it. No restart.
|
|
21
|
+
#
|
|
22
|
+
# Do NOT add a `dsh-persona` row next to it: one section name may be registered
|
|
23
|
+
# once per scope.
|
|
24
|
+
#
|
|
25
|
+
# No compaction: no auto-condensation, no /compact — a conversation that
|
|
26
|
+
# exceeds the model context window fails instead of shrinking.
|
|
27
|
+
- insert:
|
|
28
|
+
- id: chatonly-context-policy
|
|
29
|
+
name: 'dsh-chat-preset'
|
|
30
|
+
config:
|
|
31
|
+
role: 'policy'
|
|
32
|
+
presetId: 'chatonly'
|
|
33
|
+
contextName: 'working-directory:current'
|
|
34
|
+
stripTools: true # 出厂默认;运行中用 /chattools 切
|
|
35
|
+
toolsStateFile: '~/.dsh/chatonly-strip-tools.state' # 当前开关值(strip/pass),缺省即默认
|
|
36
|
+
file: '~/.dsh/chatonly-system-prompt.md'
|
|
37
|
+
- id: preset-chat
|
|
38
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
39
|
+
config:
|
|
40
|
+
id: chatonly
|
|
41
|
+
name: ChatOnly
|
|
42
|
+
description: '纯对话:默认不注入工具与工作区上下文;系统提示词来自 ~/.dsh/chatonly-system-prompt.md(留空则仅发送用户输入);/chattools 可放行工具'
|
|
43
|
+
order: 5
|
|
44
|
+
plugins:
|
|
45
|
+
- id: prompt
|
|
46
|
+
name: 'dsh-chat-preset'
|
|
47
|
+
config:
|
|
48
|
+
role: 'section'
|
|
49
|
+
file: '~/.dsh/chatonly-system-prompt.md'
|
package/index.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-preset — the ChatOnly preset row PLUS the context policy that belongs
|
|
3
|
+
* to it (merged from the former `@local/dsh-filter-runtime-context` package:
|
|
4
|
+
* one package, one place to look when ChatOnly's request looks wrong).
|
|
5
|
+
*
|
|
6
|
+
* Keeps the loop-owned runtime-context snapshot out of the model request for
|
|
7
|
+
* agents owned by ONE preset (default: the local `chatonly` preset), and drops
|
|
8
|
+
* the globally registered tool schemas for that preset.
|
|
9
|
+
*
|
|
10
|
+
* Why the obvious approach does NOT work
|
|
11
|
+
* -------------------------------------
|
|
12
|
+
* Rewriting the `agent/pre-step` waterfall decision (`{ ...decision, messages }`
|
|
13
|
+
* without the snapshot) has no effect, neither on the request nor on the
|
|
14
|
+
* committed session log. `dsh-agent-loop`'s `step()` recomputes the context
|
|
15
|
+
* itself and re-admits it:
|
|
16
|
+
*
|
|
17
|
+
* const sections = renderContextSections(
|
|
18
|
+
* this.loopCtx.systemPrompt.refreshContext(assembly, assembleContextFor(this, signal)));
|
|
19
|
+
* const context = this.runtimeContext.project(joinContextSections(sections), sections);
|
|
20
|
+
* ...
|
|
21
|
+
* if (firstAttempt) for (const message of decision.messages)
|
|
22
|
+
* if (message.source.kind === 'runtime-context') { ... sets contextAdmitted only ... }
|
|
23
|
+
* if (context !== void 0 && !contextAdmitted)
|
|
24
|
+
* this.session.append('user/message', context, { surfaceOp: 'append' }); // <-- always
|
|
25
|
+
*
|
|
26
|
+
* `decision.messages` is consulted solely to decide *whether* to admit the
|
|
27
|
+
* freshly projected context — dropping the message from that list only makes
|
|
28
|
+
* the admission happen on the fallback line, with identical text. Evidence on
|
|
29
|
+
* this machine: all 9 chatonly sessions that ran the previous version still
|
|
30
|
+
* carry a `source.kind === 'runtime-context'` user message on the surface.
|
|
31
|
+
*
|
|
32
|
+
* Why the preset cannot remove it either
|
|
33
|
+
* -------------------------------------
|
|
34
|
+
* `@deepseek-ai/dsh-working-directory` registers its contribution as
|
|
35
|
+
* `required: true`. Suppression (`systemPrompt.suppressRuntimeContext()`, which
|
|
36
|
+
* is what the chatonly preset's `includeRuntimeContext: false` does) removes
|
|
37
|
+
* only optional contributions, and `refreshContext()` keeps every
|
|
38
|
+
* `required === true` entry regardless.
|
|
39
|
+
*
|
|
40
|
+
* The lever that works
|
|
41
|
+
* --------------------
|
|
42
|
+
* Register a scoped shadow of that contribution under the SAME name in the
|
|
43
|
+
* agent's own scope (`agent.ctx`, scope key = the agent — see
|
|
44
|
+
* `assembleContextFor` in `dsh-agent`: `{ scope: agent }`). Scoped named entries
|
|
45
|
+
* shadow global ones in both `assemble()` and `refreshContext()`, so the shadow
|
|
46
|
+
* wins the name:
|
|
47
|
+
*
|
|
48
|
+
* - `required: false` drops it from the suppression-filtered assembly, and
|
|
49
|
+
* - `text: ''` renders to nothing, so `renderContextSections()` filters it out,
|
|
50
|
+
* `joinContextSections()` yields `''`, and `RuntimeContextProjection.project()`
|
|
51
|
+
* returns `undefined` (fresh session) — nothing is appended to the surface
|
|
52
|
+
* and nothing reaches the request.
|
|
53
|
+
*
|
|
54
|
+
* Registration is owned by the agent scope fiber, so it is disposed with the
|
|
55
|
+
* agent; one registration per agent is enforced locally because a duplicate
|
|
56
|
+
* name inside one layer throws.
|
|
57
|
+
*
|
|
58
|
+
* Caveat: for a session that already committed a snapshot under an older
|
|
59
|
+
* version, `RuntimeContextProjection.project()` first emits its one-shot
|
|
60
|
+
* `Current runtime context: none.` marker (retained -> CLEARED). New chatonly
|
|
61
|
+
* sessions never see it.
|
|
62
|
+
*
|
|
63
|
+
* Tools (switch: `/chattools`, factory default = intercept)
|
|
64
|
+
* ---------------------------------------------------------
|
|
65
|
+
* A preset row cannot remove tools: `assemble()` collects every GLOBAL tool
|
|
66
|
+
* provider unconditionally (`dsh-system-prompt`:364-365) and there is no
|
|
67
|
+
* `suppressTools()` counterpart to `suppressRuntimeContext()`. With tools
|
|
68
|
+
* registered by the desktop bundles (acp_*, sidebar_open, working_directory,
|
|
69
|
+
* search_context, …) a ChatOnly request therefore still carried 10 tool
|
|
70
|
+
* schemas. The assembly waterfall fixes it: `buildRequest(…, assembly.tools, …)`
|
|
71
|
+
* emits `{ tools }` only when the list is non-empty (`dsh-agent-loop`:1217-1220),
|
|
72
|
+
* so clearing `assembly.tools` means the model is offered no tools at all.
|
|
73
|
+
*
|
|
74
|
+
* Interception is a live switch, not a build-time config:
|
|
75
|
+
* - `~/.dsh/chatonly-strip-tools.state` — one word (`strip` / `pass`), read on
|
|
76
|
+
* every assembly and written by `/chattools on|off`. Missing = the factory
|
|
77
|
+
* default, `config.stripTools` (true).
|
|
78
|
+
* - With the switch OFF the listener returns the downstream assembly verbatim,
|
|
79
|
+
* so any other plugin's tool provider or filter behaves exactly as it would
|
|
80
|
+
* without this package installed. That is the "let someone else manage it"
|
|
81
|
+
* mode — this package never adds tools of its own, it only clears the list.
|
|
82
|
+
* - `config.stripTools: false` therefore means "ship turned off", not "toggle
|
|
83
|
+
* disabled": `/chattools on` can still turn interception on later.
|
|
84
|
+
*
|
|
85
|
+
* @module dsh-chat-preset
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
// --- self-diagnostics: one JSON line per install/miss into ~/.dsh-filter-rtc-debug.log ---
|
|
89
|
+
// (temporary; delete dbg() and this block once the filter is verified in the app)
|
|
90
|
+
import fs from 'node:fs';
|
|
91
|
+
import os from 'node:os';
|
|
92
|
+
import path from 'node:path';
|
|
93
|
+
const DBG_PATH = `${os.homedir()}/.dsh-filter-rtc-debug.log`;
|
|
94
|
+
function dbg(obj) {
|
|
95
|
+
// Read lazily: tests import this module (which runs before their own statements)
|
|
96
|
+
// and set DSH_FILTER_RTC_DEBUG=0 to keep their runs out of the app's log.
|
|
97
|
+
if (process.env.DSH_FILTER_RTC_DEBUG === '0') return;
|
|
98
|
+
try {
|
|
99
|
+
fs.appendFileSync(DBG_PATH, `${JSON.stringify({ t: new Date().toISOString(), ...obj })}\n`);
|
|
100
|
+
} catch {
|
|
101
|
+
/* diagnostics must never break the loop */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const name = 'chat-preset';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Cordis resolves a service on a context only for fibers that declare it.
|
|
109
|
+
* Without this the plugin's own `ctx.sessionProjections` throws
|
|
110
|
+
* `cannot get property "sessionProjections" without inject` — which is exactly
|
|
111
|
+
* what the first live run produced (evt:"error" in the debug log).
|
|
112
|
+
*/
|
|
113
|
+
export const inject = ['sessionProjections', 'systemPrompt', 'commands'];
|
|
114
|
+
|
|
115
|
+
const DEFAULT_PRESET_ID = 'chatonly';
|
|
116
|
+
/**
|
|
117
|
+
* The tool-interception switch. One line of text; absent/empty = factory default
|
|
118
|
+
* (`config.stripTools`, itself defaulting to true). Read on every assembly, so
|
|
119
|
+
* `/chattools off` applies to the next message — no restart.
|
|
120
|
+
*/
|
|
121
|
+
const DEFAULT_TOOLS_STATE_FILE = '~/.dsh/chatonly-strip-tools.state';
|
|
122
|
+
/** Accepted spellings in the state file / `/chattools` argument. */
|
|
123
|
+
const SWITCH_ON = new Set(['on', 'strip', 'intercept', 'block', 'true', '1']);
|
|
124
|
+
const SWITCH_OFF = new Set(['off', 'pass', 'allow', 'bypass', 'false', '0']);
|
|
125
|
+
/** The working-directory contribution owned by `dsh-working-directory`. */
|
|
126
|
+
const DEFAULT_CONTEXT_NAME = 'working-directory:current';
|
|
127
|
+
/** Fallback placement = `CONTEXT_ORDERS.WORKING_DIRECTORY` in `dsh-system-prompt`. */
|
|
128
|
+
const DEFAULT_CONTEXT_ORDER = 100;
|
|
129
|
+
/** Where the ChatOnly system prompt lives (role `section`). */
|
|
130
|
+
const DEFAULT_PROMPT_FILE = '~/.dsh/chatonly-system-prompt.md';
|
|
131
|
+
/** Section names owned by `dsh-system-prompt` / `dsh-persona`. */
|
|
132
|
+
const PERSONA_PREFIX_SECTION = 'deployment:persona-prefix';
|
|
133
|
+
const PERSONA_SUFFIX_SECTION = 'deployment:persona-suffix';
|
|
134
|
+
|
|
135
|
+
function applyPolicy(ctx, config = {}) {
|
|
136
|
+
const presetId = config.presetId ?? DEFAULT_PRESET_ID;
|
|
137
|
+
const contextName = config.contextName ?? DEFAULT_CONTEXT_NAME;
|
|
138
|
+
const toolsStateFile = expandHome(config.toolsStateFile ?? DEFAULT_TOOLS_STATE_FILE);
|
|
139
|
+
/** Factory default for the switch; the state file (see `/chattools`) overrides it. */
|
|
140
|
+
const stripToolsDefault = config.stripTools ?? true;
|
|
141
|
+
/** Agents whose shadow contribution is already installed (per agent, never reused). */
|
|
142
|
+
const installed = new WeakSet();
|
|
143
|
+
/** Last observed switch value, so diagnostics fire on flips instead of per step. */
|
|
144
|
+
let lastStrip;
|
|
145
|
+
|
|
146
|
+
const stripToolsNow = () => readSwitch(toolsStateFile, stripToolsDefault);
|
|
147
|
+
|
|
148
|
+
dbg({ evt: 'applied', presetId, contextName, stripTools: stripToolsNow(), toolsStateFile, pid: process.pid });
|
|
149
|
+
|
|
150
|
+
ctx.on('agent/pre-step', (payload, next) => {
|
|
151
|
+
const agent = payload?.agent;
|
|
152
|
+
if (agent === undefined || agent === null) return next();
|
|
153
|
+
|
|
154
|
+
// 1) Only the owning preset is touched; every other agent passes through.
|
|
155
|
+
// A diagnostic or capability failure must never break the loop, so every
|
|
156
|
+
// step below is defensive and the decision is always handed back as-is.
|
|
157
|
+
try {
|
|
158
|
+
if (readPresetId(ctx, agent) === presetId) {
|
|
159
|
+
// 2) Shadow the required contribution before `step()` refreshes it.
|
|
160
|
+
suppress(agent, contextName, installed);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
dbg({ evt: 'error', message: String(error), sessionId: agent?.session?.id ?? null });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3) The decision is returned untouched: the snapshot is not a message we
|
|
167
|
+
// can filter, it is a projection `step()` recomputes (see header).
|
|
168
|
+
return next();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// --- tools -----------------------------------------------------------------
|
|
172
|
+
// `assemble()` collects every GLOBAL tool provider unconditionally
|
|
173
|
+
// (dsh-system-prompt/lib/index.js:365) and a preset's scoped layer can only
|
|
174
|
+
// add, never remove — so a preset row cannot drop tools. The assembly
|
|
175
|
+
// waterfall is the one place that can: `buildRequest(..., assembly.tools, ...)`
|
|
176
|
+
// omits `tools` entirely when the list is empty.
|
|
177
|
+
//
|
|
178
|
+
// The listener is ALWAYS registered, even when the switch is off: that is what
|
|
179
|
+
// makes `/chattools off|on` a live toggle. With the switch off the assembly is
|
|
180
|
+
// returned verbatim, so every other plugin's tool provider / filter keeps
|
|
181
|
+
// working exactly as it would without this package.
|
|
182
|
+
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
183
|
+
const out = await next();
|
|
184
|
+
try {
|
|
185
|
+
const agent = context?.agent;
|
|
186
|
+
if (agent === undefined || agent === null) return out;
|
|
187
|
+
if (readPresetId(ctx, agent) !== presetId) return out;
|
|
188
|
+
|
|
189
|
+
const strip = stripToolsNow();
|
|
190
|
+
if (strip !== lastStrip) {
|
|
191
|
+
lastStrip = strip;
|
|
192
|
+
dbg({ evt: 'tools-mode', strip, file: toolsStateFile, sessionId: agent?.session?.id ?? null });
|
|
193
|
+
}
|
|
194
|
+
if (!strip) return out; // pass-through: other plugins own the tool list
|
|
195
|
+
|
|
196
|
+
const tools = out?.tools;
|
|
197
|
+
if (!Array.isArray(tools) || tools.length === 0) return out;
|
|
198
|
+
dbg({
|
|
199
|
+
evt: 'strip-tools',
|
|
200
|
+
count: tools.length,
|
|
201
|
+
names: tools.map((tool) => tool?.name).slice(0, 20),
|
|
202
|
+
sessionId: agent?.session?.id ?? null,
|
|
203
|
+
});
|
|
204
|
+
return { ...out, tools: [] };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
dbg({ evt: 'error', stage: 'strip-tools', message: String(error) });
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
registerToolsCommand(ctx, toolsStateFile, stripToolsDefault);
|
|
212
|
+
registerCommand(ctx, config.file ?? DEFAULT_PROMPT_FILE);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- the tool switch ---------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/** Last unrecognized state value already reported, per file. */
|
|
218
|
+
const complained = new Map();
|
|
219
|
+
|
|
220
|
+
/** The persisted switch, falling back to the factory default. Never throws. */
|
|
221
|
+
function readSwitch(file, fallback) {
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = fs.readFileSync(file, 'utf8').trim().toLowerCase();
|
|
225
|
+
} catch {
|
|
226
|
+
return fallback; // missing file = never toggled
|
|
227
|
+
}
|
|
228
|
+
if (raw.length === 0) return fallback;
|
|
229
|
+
if (SWITCH_ON.has(raw)) return true;
|
|
230
|
+
if (SWITCH_OFF.has(raw)) return false;
|
|
231
|
+
// Assembled on every request: complain once per distinct bad value, not per turn.
|
|
232
|
+
if (complained.get(file) !== raw) {
|
|
233
|
+
complained.set(file, raw);
|
|
234
|
+
dbg({ evt: 'miss', stage: 'tools-state', reason: `unrecognized "${raw}"`, file });
|
|
235
|
+
}
|
|
236
|
+
return fallback;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function describeSwitch(strip, file) {
|
|
240
|
+
return strip
|
|
241
|
+
? `当前:**拦截工具** —— ChatOnly 请求里不带任何工具 schema。\n文件:${file}`
|
|
242
|
+
: `当前:**放行工具** —— 工具由其他插件/预设决定,本包不干预。\n文件:${file}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `/chattools [on | off | status | reset]` — the live switch for tool
|
|
247
|
+
* interception. Writing the file is enough: the assembly listener re-reads it on
|
|
248
|
+
* the next request.
|
|
249
|
+
*/
|
|
250
|
+
function registerToolsCommand(ctx, file, fallback) {
|
|
251
|
+
ctx.effect(() => ctx.commands.register({
|
|
252
|
+
name: 'chattools',
|
|
253
|
+
description: '切换 ChatOnly 是否拦截工具(默认拦截;关闭后由其他插件管理工具)',
|
|
254
|
+
input: { hint: 'on | off | status | reset' },
|
|
255
|
+
handler: async (invocation) => {
|
|
256
|
+
const arg = String(invocation?.rawInput ?? '').trim().toLowerCase();
|
|
257
|
+
if (arg.length === 0 || arg === 'status') {
|
|
258
|
+
return { kind: 'success', text: `${describeSwitch(readSwitch(file, fallback), file)}\n用法:/chattools on | off | reset` };
|
|
259
|
+
}
|
|
260
|
+
if (arg === 'reset') {
|
|
261
|
+
try {
|
|
262
|
+
fs.rmSync(file, { force: true });
|
|
263
|
+
return { kind: 'success', text: `已恢复默认(${fallback ? '拦截' : '放行'}),下一条消息生效。` };
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return { kind: 'error', text: `删除失败:${String(error)}` };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
let value;
|
|
269
|
+
if (SWITCH_ON.has(arg)) value = true;
|
|
270
|
+
else if (SWITCH_OFF.has(arg)) value = false;
|
|
271
|
+
else return { kind: 'error', text: `不认识的参数「${arg}」。用法:/chattools on | off | status | reset` };
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
275
|
+
fs.writeFileSync(file, value ? 'strip' : 'pass', 'utf8');
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return { kind: 'error', text: `写入失败:${String(error)}` };
|
|
278
|
+
}
|
|
279
|
+
dbg({ evt: 'tools-toggle', strip: value, file, via: 'command' });
|
|
280
|
+
return {
|
|
281
|
+
kind: 'success',
|
|
282
|
+
text: value
|
|
283
|
+
? `已切换为**拦截**:下一条消息起 ChatOnly 不带工具。\n文件:${file}`
|
|
284
|
+
: `已切换为**放行**:下一条消息起工具由其他插件/预设决定,本包不再清空。\n文件:${file}`,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
}), 'chat-preset.tools-command()');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The preset a Session currently runs under, from the `agentPreset` Session
|
|
293
|
+
* projection (`dsh-agent-preset-registry`): initialized from the creation
|
|
294
|
+
* header, advanced by the last `agent-preset/selected` event. Falls back to
|
|
295
|
+
* the header alone when the projection is unavailable.
|
|
296
|
+
*/
|
|
297
|
+
function readPresetId(ctx, agent) {
|
|
298
|
+
const session = agent?.session;
|
|
299
|
+
let state;
|
|
300
|
+
try {
|
|
301
|
+
// Declared via `inject`, but a resolution failure here must fall back to
|
|
302
|
+
// the header rather than skip the whole filter (the first live run lost the
|
|
303
|
+
// preset entirely to a "without inject" throw).
|
|
304
|
+
state = typeof ctx?.sessionProjections?.stateOf === 'function' && session !== undefined
|
|
305
|
+
? ctx.sessionProjections.stateOf(session, 'agentPreset')
|
|
306
|
+
: undefined;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
dbg({ evt: 'error', stage: 'preset-projection', message: String(error), sessionId: session?.id ?? null });
|
|
309
|
+
state = undefined;
|
|
310
|
+
}
|
|
311
|
+
if (typeof state === 'string' && state !== '') return state;
|
|
312
|
+
const header = session?.header?.agentPreset;
|
|
313
|
+
return typeof header === 'string' && header !== '' ? header : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Install the empty, non-required shadow of `contextName` in the agent's own
|
|
318
|
+
* scope. Never registers through the plugin's own context: that layer is
|
|
319
|
+
* global and would strip the contribution for every preset.
|
|
320
|
+
*
|
|
321
|
+
* The registration runs inside a tiny plugin MOUNTED INTO the agent scope, so
|
|
322
|
+
* (a) `systemPrompt` resolves there (its fiber declares the dependency — the
|
|
323
|
+
* agent scope fiber does not, so a bare `agent.ctx.systemPrompt` lookup fails
|
|
324
|
+
* with "without inject"), and (b) the registration is owned by that fiber, so
|
|
325
|
+
* it disappears with the agent.
|
|
326
|
+
*/
|
|
327
|
+
function suppress(agent, contextName, installed) {
|
|
328
|
+
if (installed.has(agent)) return;
|
|
329
|
+
const scopeCtx = agent?.ctx;
|
|
330
|
+
const sessionId = agent?.session?.id ?? null;
|
|
331
|
+
const register = (c) => {
|
|
332
|
+
c.systemPrompt.context({
|
|
333
|
+
name: contextName,
|
|
334
|
+
order: typeof c.systemPrompt.getContextOrder === 'function'
|
|
335
|
+
? (c.systemPrompt.getContextOrder('WORKING_DIRECTORY') ?? DEFAULT_CONTEXT_ORDER)
|
|
336
|
+
: DEFAULT_CONTEXT_ORDER,
|
|
337
|
+
required: false,
|
|
338
|
+
interpolate: false,
|
|
339
|
+
text: '',
|
|
340
|
+
});
|
|
341
|
+
dbg({ evt: 'suppress', contextName, sessionId, agentId: agent?.id ?? null, via: 'agent-scope-plugin' });
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (typeof scopeCtx?.plugin === 'function') {
|
|
345
|
+
scopeCtx.plugin({
|
|
346
|
+
name: 'chat-preset:context-shadow',
|
|
347
|
+
inject: ['systemPrompt'],
|
|
348
|
+
apply(c) {
|
|
349
|
+
try {
|
|
350
|
+
register(c);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
dbg({ evt: 'error', stage: 'register', message: String(error), sessionId });
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
installed.add(agent);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fallback for a scope object that is not a Cordis context.
|
|
361
|
+
const prompt = scopeCtx?.systemPrompt;
|
|
362
|
+
if (typeof prompt?.context !== 'function') {
|
|
363
|
+
dbg({ evt: 'miss', reason: 'agent.ctx has neither plugin() nor systemPrompt', sessionId });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
register({ systemPrompt: prompt });
|
|
368
|
+
installed.add(agent);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
dbg({ evt: 'error', stage: 'register-fallback', message: String(error), sessionId });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// --- /chatprompt -------------------------------------------------------------
|
|
375
|
+
// The composer-side editor for the system prompt above. Registered from the
|
|
376
|
+
// GLOBAL mount only: `commands.register()` scopes by the calling context, so a
|
|
377
|
+
// per-agent registration would collide as soon as a second ChatOnly agent exists.
|
|
378
|
+
|
|
379
|
+
/** `/chatprompt [text | show | clear]` */
|
|
380
|
+
function registerCommand(ctx, file) {
|
|
381
|
+
const target = expandHome(file);
|
|
382
|
+
ctx.effect(() => ctx.commands.register({
|
|
383
|
+
name: 'chatprompt',
|
|
384
|
+
description: '查看/设置 ChatOnly 的系统提示词(写入文件,下一条消息生效)',
|
|
385
|
+
input: { hint: '文本 | show | clear' },
|
|
386
|
+
handler: async (invocation) => {
|
|
387
|
+
const raw = String(invocation?.rawInput ?? '');
|
|
388
|
+
const arg = raw.trim();
|
|
389
|
+
if (arg.length === 0) {
|
|
390
|
+
const text = readPrompt(target);
|
|
391
|
+
return {
|
|
392
|
+
kind: 'success',
|
|
393
|
+
text: text.length === 0
|
|
394
|
+
? `当前系统提示词为空。\n文件:${target}\n用法:/chatprompt 你是一个……`
|
|
395
|
+
: `当前系统提示词(${text.length} 字):\n${preview(text)}\n\n文件:${target}`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (arg === 'show') {
|
|
399
|
+
const text = readPrompt(target);
|
|
400
|
+
return { kind: 'success', text: text.length === 0 ? '(空)' : `${text}\n\n— 文件:${target}` };
|
|
401
|
+
}
|
|
402
|
+
if (arg === 'clear') {
|
|
403
|
+
try {
|
|
404
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
405
|
+
fs.writeFileSync(target, '', 'utf8');
|
|
406
|
+
return { kind: 'success', text: `已清空,下一条消息起不再发送系统提示词。\n文件:${target}` };
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return { kind: 'error', text: `写入失败:${String(error)}` };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
413
|
+
const body = raw.trim();
|
|
414
|
+
fs.writeFileSync(target, body, 'utf8');
|
|
415
|
+
return { kind: 'success', text: `已保存(${body.length} 字),下一条消息生效。\n文件:${target}` };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return { kind: 'error', text: `写入失败:${String(error)}` };
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
}), 'chat-preset.command()');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- role: 'section' -------------------------------------------------------------
|
|
424
|
+
// Mounted INSIDE the preset (see the `plugins` list in cordis.patch.yml), where it
|
|
425
|
+
// shadows the deployment persona by section name. `text` is a FUNCTION, so
|
|
426
|
+
// `SystemPrompt.assemble()` re-reads the file on every assembly
|
|
427
|
+
// (`dsh-system-prompt/lib/index.js:388`) — edit the file and the next message uses
|
|
428
|
+
// it, no restart.
|
|
429
|
+
//
|
|
430
|
+
// The persona row must NOT be mounted next to this one: two registrations of one
|
|
431
|
+
// section name inside one scope throw.
|
|
432
|
+
|
|
433
|
+
/** Cordis rejects two `complete` sections, so this role owns the slot alone. */
|
|
434
|
+
function applySection(ctx, config = {}) {
|
|
435
|
+
const file = expandHome(config.file ?? DEFAULT_PROMPT_FILE);
|
|
436
|
+
const prompt = ctx.systemPrompt;
|
|
437
|
+
ctx.effect(() => prompt.section({
|
|
438
|
+
name: PERSONA_PREFIX_SECTION,
|
|
439
|
+
order: prompt.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),
|
|
440
|
+
complete: true,
|
|
441
|
+
text: () => readPrompt(file),
|
|
442
|
+
}), 'chat-preset.section()');
|
|
443
|
+
ctx.effect(() => prompt.section({
|
|
444
|
+
name: PERSONA_SUFFIX_SECTION,
|
|
445
|
+
order: prompt.getSectionOrder('DEPLOYMENT_PERSONA_SUFFIX'),
|
|
446
|
+
text: '',
|
|
447
|
+
}), 'chat-preset.suffix()');
|
|
448
|
+
// What the persona row's `includeRuntimeContext: false` used to do.
|
|
449
|
+
ctx.effect(() => prompt.suppressRuntimeContext(), 'chat-preset.suppress()');
|
|
450
|
+
dbg({ evt: 'prompt-section', file, length: readPrompt(file).length, pid: process.pid });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The current prompt; a missing or unreadable file means "no system prompt". */
|
|
454
|
+
function readPrompt(file) {
|
|
455
|
+
try {
|
|
456
|
+
return fs.readFileSync(file, 'utf8').trim();
|
|
457
|
+
} catch {
|
|
458
|
+
return '';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function preview(text) {
|
|
463
|
+
return text.length <= 400 ? text : `${text.slice(0, 400)}\n…(共 ${text.length} 字,用 /chatprompt show 查看全文)`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function expandHome(file) {
|
|
467
|
+
if (file === '~') return os.homedir();
|
|
468
|
+
if (file.startsWith('~/') || file.startsWith('\\')) return path.join(os.homedir(), file.slice(2));
|
|
469
|
+
return file;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* `policy` (default, mounted globally): keeps the runtime context and the global
|
|
474
|
+
* tool schemas out of ChatOnly requests, and owns the `/chatprompt` command.
|
|
475
|
+
* `section`: the system-prompt slot, mounted inside the preset.
|
|
476
|
+
*/
|
|
477
|
+
export function apply(ctx, config = {}) {
|
|
478
|
+
if ((config.role ?? 'policy') === 'section') applySection(ctx, config);
|
|
479
|
+
else applyPolicy(ctx, config);
|
|
480
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-chat-preset",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "DSH 的纯净对话预设 ChatOnly:拦截工具注入与工作区上下文,模型只收到你输入的内容;/chatprompt 设定系统提示词,/chattools 可关闭工具拦截。",
|
|
6
|
+
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "cordis", "agent-preset", "chatonly", "system-prompt", "runtime-context", "tools", "toggle"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"registry": "https://registry.npmjs.org"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/Xxx-Bin/dsh-chat-preset.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/Xxx-Bin/dsh-chat-preset#readme",
|
|
18
|
+
"bugs": "https://github.com/Xxx-Bin/dsh-chat-preset/issues",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "index.js",
|
|
21
|
+
"files": ["index.js", "cordis.patch.yml"],
|
|
22
|
+
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
|
23
|
+
}
|