@morlay/dsh-preset 0.0.1
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 +133 -0
- package/cordis.patch.yml +58 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/presets/ptc/agent.cordis.yml +133 -0
- package/dist/presets/ptc/preset.yml +3 -0
- package/dist/presets/standard/agent.cordis.yml +128 -0
- package/dist/presets/standard/preset.yml +3 -0
- package/package.json +48 -0
- package/src/__tests__/generated-presets.spec.ts +84 -0
- package/src/index.ts +8 -0
- package/tool/generate-presets.ts +180 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 morlay
|
|
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
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @morlay/dsh-preset
|
|
2
|
+
|
|
3
|
+
个人用 dsh profile bundle。包的实体是 `cordis.patch.yml` 与构建产出的
|
|
4
|
+
`dist/presets/`(bundle patch 由 `dsh.bundle.patch` 声明、profile 组合器经该
|
|
5
|
+
字段解析)。
|
|
6
|
+
|
|
7
|
+
## 内容
|
|
8
|
+
|
|
9
|
+
| 文件 | 作用 |
|
|
10
|
+
| -------------------------- | --------------------------------------------------------------------------------- |
|
|
11
|
+
| `cordis.patch.yml` | bundle patch:禁用官方 preset、注册本包 preset 为默认、声明个人 `llm-pi-ai` route |
|
|
12
|
+
| `tool/generate-presets.ts` | 从上游生成 preset 的模块 + tsdown hooks |
|
|
13
|
+
| `dist/presets/standard/` | 构建产物:自定义 preset「标准模式」(由上游 `standard` 生成) |
|
|
14
|
+
| `dist/presets/ptc/` | 构建产物:自定义 preset「PTC 模式」(由上游 `ptc` 生成) |
|
|
15
|
+
|
|
16
|
+
## 禁用官方 preset
|
|
17
|
+
|
|
18
|
+
patch 设 `includeShippedRoot: false`,官方 `minimal` / `cordis` 等不再出现在
|
|
19
|
+
roster;本包 `dist/presets/` 作为唯一 root 提供 `standard` / `ptc`(`default:
|
|
20
|
+
standard`)。`includeShippedRoot` 是上游 `dsh-agent-presets` 的正式配置项,
|
|
21
|
+
不是 hack。
|
|
22
|
+
|
|
23
|
+
### 为什么 id 沿用官方名
|
|
24
|
+
|
|
25
|
+
产物目录名 = 上游 preset id。`presetDisplayText` 对 `trust: system` 且 id 命中
|
|
26
|
+
`BUILT_IN_PRESET_KEYS` 的行走客户端语言字典,因此展示名自动本地化(zh「标准模式」
|
|
27
|
+
/ en「Standard mode」),`preset.yml` 的 `name` / `description` 会被字典遮蔽。
|
|
28
|
+
`preset.yml` 仍有意义:`order` 决定 roster 排序,`name` / `description` 作为
|
|
29
|
+
字典未覆盖 locale 的兜底。
|
|
30
|
+
|
|
31
|
+
## persona 走部署级 system-prompt,preset 不碰
|
|
32
|
+
|
|
33
|
+
个人提示词只在 `cordis.patch.yml` 的 `system-prompt` 行维护一份:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
- id: system-prompt
|
|
37
|
+
config:
|
|
38
|
+
includeHarnessIdentity: false # 关掉 harness identity
|
|
39
|
+
includeRuntimeContext: false # 关掉 runtime context 快照
|
|
40
|
+
personaSuffix: Your working directory is {{cwd}}.
|
|
41
|
+
personaPrefix: |- # 个人提示词
|
|
42
|
+
…
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
preset 自带的 `persona` 行会在 agent scope 注册 `deployment:persona-prefix` 并
|
|
46
|
+
**按 scope 遮蔽**部署级值(上游 `packages/core/system-prompt/tests/scoped.spec.ts`
|
|
47
|
+
固化了该语义),所以生成器把那一行**删掉**,而不是复制一份改过的 persona——同一份
|
|
48
|
+
文案只维护一处。
|
|
49
|
+
|
|
50
|
+
自建 preset 仍然必要,但只为别的事:`includeShippedRoot: false` 之后需要一份自己的
|
|
51
|
+
roster(id 沿用官方名以走客户端语言字典),以及桌面形态下 preset 必须落在 dsh 包的
|
|
52
|
+
`config/agent-presets` 挂载点(见 dsh-desktopify 的 `dsh.desktop.agentPresets`)。
|
|
53
|
+
|
|
54
|
+
## preset 由构建生成,不是手工副本
|
|
55
|
+
|
|
56
|
+
产物落在 `dist/`,由 tsdown 的 `build:done` hook 在每次 `pnpm build` 时生成
|
|
57
|
+
(`tsdown.config.ts` 挂 `presetHooks()`)。放 dist 而非源码树:dist 是 gitignore
|
|
58
|
+
的构建输出,既不与 oxfmt 互相改格式,也不把派生文件混进源码。
|
|
59
|
+
|
|
60
|
+
必须挂 `build:done` 而非 `build:prepare`——tsdown 时序是
|
|
61
|
+
`build:prepare` → `clean()`(清空 outDir)→ rolldown → `build:done`,写在 clean
|
|
62
|
+
之前会被删掉。
|
|
63
|
+
|
|
64
|
+
单独重生成(默认输出 `dist/presets`,可传目录):
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
pnpm --filter @morlay/dsh-preset run generate-presets [outDir]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
脚本把上游 composition 当**数据**读入:`js-yaml` 用 include 的
|
|
71
|
+
`entryListSchema` 解析(`!!js` 标签保留为表达式节点)→ 在 JS 里改两处(删掉
|
|
72
|
+
`persona` 行、把 `agent-instructions` 行指向本仓库的 fork)→ `dump` 回 YAML。
|
|
73
|
+
因此上游任何结构性改动(新增 / 重命名 row、改字段)都自动跟随,不依赖易碎的文本
|
|
74
|
+
锚点;上游若删掉 `persona` 行,目标(preset 不自带 persona)本就达成,脚本照常
|
|
75
|
+
通过。
|
|
76
|
+
|
|
77
|
+
脚本先**整目录清空**输出目录再生成:产物完全派生自脚本,残留目录(改过
|
|
78
|
+
`source`、旧命名)不该留下——否则 discovery 会把它们当有效 preset 扫出来。
|
|
79
|
+
`src/__tests__/generated-presets.spec.ts` 把产物与**现算的**期望值比较(生成到
|
|
80
|
+
临时目录,不依赖 build 是否跑过),并断言输出目录里没有脚本之外的目录。
|
|
81
|
+
|
|
82
|
+
代价:`dump` 不保留注释(上游 composition 的说明注释会丢)。
|
|
83
|
+
|
|
84
|
+
### 上游升级流程
|
|
85
|
+
|
|
86
|
+
1. 升级 `DEEPSEEK_HARNESS_VERSION` 并 sync/patch/build。
|
|
87
|
+
2. `pnpm --filter @morlay/dsh-preset run build`(build:done 会重新生成)
|
|
88
|
+
3. `pnpm exec vitest run packages/preset/dsh-preset` 确认无 drift。
|
|
89
|
+
|
|
90
|
+
## 装配
|
|
91
|
+
|
|
92
|
+
`agent-presets.roots` 需指向本包的 `dist/presets/`。因为该目录随包分发,
|
|
93
|
+
位置只能在运行期解析,故用 `!!js` 表达式从 profile 的 `baseUrl` 起
|
|
94
|
+
`createRequire` 解析包路径(`ctx.baseUrl` 即 root include 所在目录 = profile 根):
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
- id: agent-presets
|
|
98
|
+
config:
|
|
99
|
+
default: standard
|
|
100
|
+
includeShippedRoot: false
|
|
101
|
+
roots:
|
|
102
|
+
- path: !!js process.getBuiltinModule('node:path').join(
|
|
103
|
+
process.getBuiltinModule('node:path').dirname(
|
|
104
|
+
process.getBuiltinModule('node:module').createRequire(ctx.baseUrl)
|
|
105
|
+
.resolve('@morlay/dsh-preset/package.json')),
|
|
106
|
+
'dist/presets')
|
|
107
|
+
trust: system
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
用 `process.getBuiltinModule` 而非裸 `require` / `import`:求值环境是
|
|
111
|
+
`with (ctx) { eval(expr) }`,只有全局对象稳定可用。表达式含 `'` 与换行,
|
|
112
|
+
`--dump-config` 会把它输出成 folded scalar,往返后语义不变(已实测)。
|
|
113
|
+
|
|
114
|
+
`trust: system` 与 shipped root 同级:preset 是一份完整 composition,授予的
|
|
115
|
+
能力等价于 shell 访问,只应来自受控的安装包。
|
|
116
|
+
|
|
117
|
+
### 桌面形态
|
|
118
|
+
|
|
119
|
+
桌面宿主把 `agent-presets.roots` 固定为 dsh 包内的
|
|
120
|
+
`node_modules/@deepseek-ai/dsh/config/agent-presets`(`system` root),上面的
|
|
121
|
+
`!!js` roots 在桌面 profile 里会被覆盖,preset 列表因此为空。app 工作区改用
|
|
122
|
+
`dsh.desktop.agentPresets` 声明本包的 `dist/presets`,由
|
|
123
|
+
[dsh-desktopify](../../desktop/dsh-desktopify/README.md) 在 dev 项目与种子
|
|
124
|
+
profile 里把内容物化到该挂载点;`includeShippedRoot: false` 两种形态共用。
|
|
125
|
+
|
|
126
|
+
## 维护注意
|
|
127
|
+
|
|
128
|
+
- `package.json` 的 `files` 必须含 `dist`(preset 在其中)与 `tool`,否则发布产物缺内容。
|
|
129
|
+
- 生成器显式设 `quotingType: '"'`:`yaml.dump` 默认单引号而仓库 oxfmt 偏好双引号,
|
|
130
|
+
不指定会让生成器与 formatter 来回改;产物在 gitignore 的 dist 里,本就不参与 fmt。
|
|
131
|
+
- **dev 模式需要先构建**:`just dev` / `just desktop` / `just bundle` 都先跑
|
|
132
|
+
`preset-build`(`pnpm --filter @morlay/dsh-preset run build`)。dist/presets 只在
|
|
133
|
+
build 时生成,源码树里没有,新克隆下直接起 profile 会找不到 preset。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
- id: system-prompt
|
|
2
|
+
config:
|
|
3
|
+
includeHarnessIdentity: false
|
|
4
|
+
includeRuntimeContext: false
|
|
5
|
+
personaSuffix: "你的工作目录在 `{{cwd}}`"
|
|
6
|
+
personaPrefix: |-
|
|
7
|
+
你是一个经验丰富的编程专家,YAGNI 是你的编程哲学,PDCA 是你的行为规范
|
|
8
|
+
|
|
9
|
+
## 语言与行为
|
|
10
|
+
|
|
11
|
+
- 所有思考、分析、推理过程、工具调用描述等必须使用中文,专有名词除外
|
|
12
|
+
- 思考不要陷入重复循环,一旦循环,立即退出
|
|
13
|
+
- 思考聚焦需求理解与方案设计(要点、流程、验证策略),不预演具体代码实现;代码在 Do 阶段基于实际文件状态输出,正确性由 Check 验证。
|
|
14
|
+
|
|
15
|
+
- id: agent-presets
|
|
16
|
+
config:
|
|
17
|
+
default: standard
|
|
18
|
+
includeShippedRoot: false
|
|
19
|
+
roots:
|
|
20
|
+
- path: !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(ctx.baseUrl).resolve('@morlay/dsh-preset/package.json')), 'dist/presets')
|
|
21
|
+
trust: system
|
|
22
|
+
|
|
23
|
+
- id: llm-pi-ai
|
|
24
|
+
config:
|
|
25
|
+
providers:
|
|
26
|
+
ollama:
|
|
27
|
+
apiKeyEnv: OLLAMA_API_KEY
|
|
28
|
+
displayName: Ollama Cloud
|
|
29
|
+
api: openai-completions
|
|
30
|
+
baseURL: https://ollama.com/v1
|
|
31
|
+
reasoning: high
|
|
32
|
+
models:
|
|
33
|
+
- id: deepseek-v4.1-flash
|
|
34
|
+
name: "DeepSeek V4.1 Flash @ Ollama Cloud"
|
|
35
|
+
contextWindow: 1000000
|
|
36
|
+
input: [text, image]
|
|
37
|
+
reasoningEfforts:
|
|
38
|
+
off:
|
|
39
|
+
low: low
|
|
40
|
+
high: high
|
|
41
|
+
max: max
|
|
42
|
+
|
|
43
|
+
commandcode:
|
|
44
|
+
apiKeyEnv: COMMANDCODE_API_KEY
|
|
45
|
+
displayName: Command Code
|
|
46
|
+
api: openai-completions
|
|
47
|
+
baseURL: https://api.commandcode.ai/provider/v1
|
|
48
|
+
reasoning: high
|
|
49
|
+
models:
|
|
50
|
+
- id: deepseek/deepseek-v4.1-flash
|
|
51
|
+
name: DeepSeek V4.1 Flash @ Command Code
|
|
52
|
+
contextWindow: 1000000
|
|
53
|
+
input: [text, image]
|
|
54
|
+
reasoningEfforts:
|
|
55
|
+
off:
|
|
56
|
+
low: low
|
|
57
|
+
high: high
|
|
58
|
+
max: max
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# 本文件由 packages/dsh-preset/tool/generate-presets.ts 生成,请勿手工编辑
|
|
2
|
+
- id: agent-instructions
|
|
3
|
+
name: "@morlay/dsh-agent-instructions-as-prompt"
|
|
4
|
+
config:
|
|
5
|
+
maxBytes: 65536
|
|
6
|
+
- id: tool-bash
|
|
7
|
+
name: "@deepseek-ai/dsh-tool-bash"
|
|
8
|
+
disabled: !!js process.platform === 'win32'
|
|
9
|
+
- id: tool-pwsh
|
|
10
|
+
name: "@deepseek-ai/dsh-tool-pwsh"
|
|
11
|
+
disabled: !!js process.platform !== 'win32'
|
|
12
|
+
- id: tool-fs
|
|
13
|
+
name: "@deepseek-ai/dsh-tool-fs"
|
|
14
|
+
- id: tool-fs-search
|
|
15
|
+
name: "@deepseek-ai/dsh-tool-fs-search"
|
|
16
|
+
config:
|
|
17
|
+
sampleOverCapGlobResults: false
|
|
18
|
+
- id: tool-jobs
|
|
19
|
+
name: "@deepseek-ai/dsh-tool-jobs"
|
|
20
|
+
- id: skill-filesystem
|
|
21
|
+
name: "@deepseek-ai/dsh-skill-filesystem"
|
|
22
|
+
- id: tool-skill
|
|
23
|
+
name: "@deepseek-ai/dsh-tool-skill"
|
|
24
|
+
- id: command-goal
|
|
25
|
+
name: "@deepseek-ai/dsh-command-goal"
|
|
26
|
+
- id: tool-goal
|
|
27
|
+
name: "@deepseek-ai/dsh-tool-goal"
|
|
28
|
+
- id: planning
|
|
29
|
+
name: cordis:group
|
|
30
|
+
group: true
|
|
31
|
+
isolate:
|
|
32
|
+
planMode: true
|
|
33
|
+
config:
|
|
34
|
+
- id: plan-mode
|
|
35
|
+
name: "@deepseek-ai/dsh-plan-mode"
|
|
36
|
+
config:
|
|
37
|
+
section: |
|
|
38
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
39
|
+
|
|
40
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
41
|
+
|
|
42
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
43
|
+
|
|
44
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
45
|
+
|
|
46
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
47
|
+
|
|
48
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
49
|
+
- id: compaction
|
|
50
|
+
name: cordis:group
|
|
51
|
+
group: true
|
|
52
|
+
isolate:
|
|
53
|
+
compaction: true
|
|
54
|
+
toolResultPruner: true
|
|
55
|
+
config:
|
|
56
|
+
- id: compaction-basic
|
|
57
|
+
name: "@deepseek-ai/dsh-compaction-basic"
|
|
58
|
+
- id: command-compact
|
|
59
|
+
name: "@deepseek-ai/dsh-command-compact"
|
|
60
|
+
- id: tool-result-pruner
|
|
61
|
+
name: "@deepseek-ai/dsh-compaction-tool-result-pruner"
|
|
62
|
+
config:
|
|
63
|
+
thresholdChars: 8192
|
|
64
|
+
headChars: 4096
|
|
65
|
+
tailChars: 1024
|
|
66
|
+
- id: delegation
|
|
67
|
+
name: cordis:group
|
|
68
|
+
group: true
|
|
69
|
+
isolate:
|
|
70
|
+
workflowEngine: true
|
|
71
|
+
config:
|
|
72
|
+
- id: tool-subagent-control
|
|
73
|
+
name: "@deepseek-ai/dsh-tool-subagent-control"
|
|
74
|
+
- id: tool-subagent-list-agents
|
|
75
|
+
name: "@deepseek-ai/dsh-tool-subagent-control/list-agents"
|
|
76
|
+
- id: tool-subagent
|
|
77
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
78
|
+
config:
|
|
79
|
+
provider: spawn
|
|
80
|
+
toolName: subagent
|
|
81
|
+
modelSelectionSettings: true
|
|
82
|
+
backgroundMode: continuable
|
|
83
|
+
- id: tool-subagent-fork
|
|
84
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
85
|
+
config:
|
|
86
|
+
provider: fork
|
|
87
|
+
toolName: subagent_fork
|
|
88
|
+
backgroundMode: continuable
|
|
89
|
+
- id: tool-subagent-codex
|
|
90
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
91
|
+
disabled: true
|
|
92
|
+
config:
|
|
93
|
+
provider: codex
|
|
94
|
+
toolName: subagent_codex
|
|
95
|
+
backgroundMode: one-shot
|
|
96
|
+
maxDepth: provider-managed
|
|
97
|
+
- id: tool-subagent-claude-code
|
|
98
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
99
|
+
disabled: true
|
|
100
|
+
config:
|
|
101
|
+
provider: claude-code
|
|
102
|
+
toolName: subagent_claude_code
|
|
103
|
+
backgroundMode: one-shot
|
|
104
|
+
maxDepth: provider-managed
|
|
105
|
+
- id: workflow-worker-thread
|
|
106
|
+
name: "@deepseek-ai/dsh-workflow-worker-thread"
|
|
107
|
+
config:
|
|
108
|
+
provider: spawn
|
|
109
|
+
- id: tool-workflow
|
|
110
|
+
name: "@deepseek-ai/dsh-tool-workflow"
|
|
111
|
+
disabled: true
|
|
112
|
+
- id: tool-ralph
|
|
113
|
+
name: "@deepseek-ai/dsh-tool-ralph"
|
|
114
|
+
config:
|
|
115
|
+
subagentProvider: spawn
|
|
116
|
+
maxRounds: 64
|
|
117
|
+
- id: tool-ask-user
|
|
118
|
+
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
119
|
+
- id: tool-todo
|
|
120
|
+
name: "@deepseek-ai/dsh-tool-todo"
|
|
121
|
+
config:
|
|
122
|
+
allowParallelInProgress: true
|
|
123
|
+
- id: tool-web
|
|
124
|
+
name: "@deepseek-ai/dsh-tool-web"
|
|
125
|
+
config:
|
|
126
|
+
fetch: true
|
|
127
|
+
searchTimeoutMs: 60000
|
|
128
|
+
- id: tool-presentation
|
|
129
|
+
name: "@deepseek-ai/dsh-agent-tool-presentation"
|
|
130
|
+
config:
|
|
131
|
+
mode: ptc
|
|
132
|
+
- id: present
|
|
133
|
+
name: "@deepseek-ai/dsh-tool-present"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# 本文件由 packages/dsh-preset/tool/generate-presets.ts 生成,请勿手工编辑
|
|
2
|
+
- id: agent-instructions
|
|
3
|
+
name: "@morlay/dsh-agent-instructions-as-prompt"
|
|
4
|
+
config:
|
|
5
|
+
maxBytes: 65536
|
|
6
|
+
- id: tool-bash
|
|
7
|
+
name: "@deepseek-ai/dsh-tool-bash"
|
|
8
|
+
disabled: !!js process.platform === 'win32'
|
|
9
|
+
- id: tool-pwsh
|
|
10
|
+
name: "@deepseek-ai/dsh-tool-pwsh"
|
|
11
|
+
disabled: !!js process.platform !== 'win32'
|
|
12
|
+
- id: tool-fs
|
|
13
|
+
name: "@deepseek-ai/dsh-tool-fs"
|
|
14
|
+
- id: tool-fs-search
|
|
15
|
+
name: "@deepseek-ai/dsh-tool-fs-search"
|
|
16
|
+
config:
|
|
17
|
+
sampleOverCapGlobResults: false
|
|
18
|
+
- id: tool-jobs
|
|
19
|
+
name: "@deepseek-ai/dsh-tool-jobs"
|
|
20
|
+
- id: skill-filesystem
|
|
21
|
+
name: "@deepseek-ai/dsh-skill-filesystem"
|
|
22
|
+
- id: tool-skill
|
|
23
|
+
name: "@deepseek-ai/dsh-tool-skill"
|
|
24
|
+
- id: command-goal
|
|
25
|
+
name: "@deepseek-ai/dsh-command-goal"
|
|
26
|
+
- id: tool-goal
|
|
27
|
+
name: "@deepseek-ai/dsh-tool-goal"
|
|
28
|
+
- id: planning
|
|
29
|
+
name: cordis:group
|
|
30
|
+
group: true
|
|
31
|
+
isolate:
|
|
32
|
+
planMode: true
|
|
33
|
+
config:
|
|
34
|
+
- id: plan-mode
|
|
35
|
+
name: "@deepseek-ai/dsh-plan-mode"
|
|
36
|
+
config:
|
|
37
|
+
section: |
|
|
38
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
39
|
+
|
|
40
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
41
|
+
|
|
42
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
43
|
+
|
|
44
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
45
|
+
|
|
46
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
47
|
+
|
|
48
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
49
|
+
- id: compaction
|
|
50
|
+
name: cordis:group
|
|
51
|
+
group: true
|
|
52
|
+
isolate:
|
|
53
|
+
compaction: true
|
|
54
|
+
toolResultPruner: true
|
|
55
|
+
config:
|
|
56
|
+
- id: compaction-basic
|
|
57
|
+
name: "@deepseek-ai/dsh-compaction-basic"
|
|
58
|
+
- id: command-compact
|
|
59
|
+
name: "@deepseek-ai/dsh-command-compact"
|
|
60
|
+
- id: tool-result-pruner
|
|
61
|
+
name: "@deepseek-ai/dsh-compaction-tool-result-pruner"
|
|
62
|
+
config:
|
|
63
|
+
thresholdChars: 8192
|
|
64
|
+
headChars: 4096
|
|
65
|
+
tailChars: 1024
|
|
66
|
+
- id: delegation
|
|
67
|
+
name: cordis:group
|
|
68
|
+
group: true
|
|
69
|
+
isolate:
|
|
70
|
+
workflowEngine: true
|
|
71
|
+
config:
|
|
72
|
+
- id: tool-subagent-control
|
|
73
|
+
name: "@deepseek-ai/dsh-tool-subagent-control"
|
|
74
|
+
- id: tool-subagent-list-agents
|
|
75
|
+
name: "@deepseek-ai/dsh-tool-subagent-control/list-agents"
|
|
76
|
+
- id: tool-subagent
|
|
77
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
78
|
+
config:
|
|
79
|
+
provider: spawn
|
|
80
|
+
toolName: subagent
|
|
81
|
+
modelSelectionSettings: true
|
|
82
|
+
backgroundMode: continuable
|
|
83
|
+
- id: tool-subagent-fork
|
|
84
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
85
|
+
config:
|
|
86
|
+
provider: fork
|
|
87
|
+
toolName: subagent_fork
|
|
88
|
+
backgroundMode: continuable
|
|
89
|
+
- id: tool-subagent-codex
|
|
90
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
91
|
+
disabled: true
|
|
92
|
+
config:
|
|
93
|
+
provider: codex
|
|
94
|
+
toolName: subagent_codex
|
|
95
|
+
backgroundMode: one-shot
|
|
96
|
+
maxDepth: provider-managed
|
|
97
|
+
- id: tool-subagent-claude-code
|
|
98
|
+
name: "@deepseek-ai/dsh-tool-subagent"
|
|
99
|
+
disabled: true
|
|
100
|
+
config:
|
|
101
|
+
provider: claude-code
|
|
102
|
+
toolName: subagent_claude_code
|
|
103
|
+
backgroundMode: one-shot
|
|
104
|
+
maxDepth: provider-managed
|
|
105
|
+
- id: workflow-worker-thread
|
|
106
|
+
name: "@deepseek-ai/dsh-workflow-worker-thread"
|
|
107
|
+
config:
|
|
108
|
+
provider: spawn
|
|
109
|
+
- id: tool-workflow
|
|
110
|
+
name: "@deepseek-ai/dsh-tool-workflow"
|
|
111
|
+
- id: tool-ralph
|
|
112
|
+
name: "@deepseek-ai/dsh-tool-ralph"
|
|
113
|
+
config:
|
|
114
|
+
subagentProvider: spawn
|
|
115
|
+
maxRounds: 64
|
|
116
|
+
- id: tool-ask-user
|
|
117
|
+
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
118
|
+
- id: tool-todo
|
|
119
|
+
name: "@deepseek-ai/dsh-tool-todo"
|
|
120
|
+
config:
|
|
121
|
+
allowParallelInProgress: true
|
|
122
|
+
- id: tool-web
|
|
123
|
+
name: "@deepseek-ai/dsh-tool-web"
|
|
124
|
+
config:
|
|
125
|
+
fetch: true
|
|
126
|
+
searchTimeoutMs: 60000
|
|
127
|
+
- id: present
|
|
128
|
+
name: "@deepseek-ai/dsh-tool-present"
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morlay/dsh-preset",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Personal dsh profile bundle: disables the shipped agent presets, builds generated standard / ptc presets into dist, and declares personal llm-pi-ai provider routes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-bundle",
|
|
8
|
+
"llm-pi-ai",
|
|
9
|
+
"preset"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/morlay/better-session.git"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"tool",
|
|
20
|
+
"cordis.patch.yml"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.mjs",
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@morlay/dsh-agent-instructions-as-prompt": "^0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
33
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
|
|
34
|
+
"@types/js-yaml": "^4.0.9",
|
|
35
|
+
"js-yaml": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "pnpm exec tsdown"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成产物的回归测试:`dist/presets/standard` / `dist/presets/ptc` 必须等于
|
|
3
|
+
* 「上游 composition 的 persona prefix 替换版」。
|
|
4
|
+
*
|
|
5
|
+
* 断言方式是把产物与**现算的**期望值比较,而不是与一份快照比——上游升级后
|
|
6
|
+
* 忘记重跑构建时,这条测试会失败并指出 drift,而不是继续绿着。
|
|
7
|
+
*
|
|
8
|
+
* 产物在 dist(构建输出,gitignore),因此测试自身调用 `generatePresets` 到
|
|
9
|
+
* 临时目录,不依赖 build 是否跑过。
|
|
10
|
+
* @module @morlay/dsh-preset/__tests__/generated-presets
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
17
|
+
import {
|
|
18
|
+
PRESET_SOURCES,
|
|
19
|
+
INSTRUCTIONS_PLUGIN,
|
|
20
|
+
PERSONA_ROW_ID,
|
|
21
|
+
UPSTREAM_PRESETS,
|
|
22
|
+
generatePresets,
|
|
23
|
+
renderComposition,
|
|
24
|
+
renderMetadata,
|
|
25
|
+
} from "../../tool/generate-presets.ts";
|
|
26
|
+
|
|
27
|
+
const OUT_DIR = mkdtempSync(join(tmpdir(), "dsh-preset-"));
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
rmSync(OUT_DIR, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
generatePresets(OUT_DIR);
|
|
32
|
+
|
|
33
|
+
describe("generated presets", () => {
|
|
34
|
+
it.each(PRESET_SOURCES)(
|
|
35
|
+
"$source matches its upstream source with only the persona and instructions rows replaced",
|
|
36
|
+
(entry) => {
|
|
37
|
+
const upstream = readFileSync(
|
|
38
|
+
join(UPSTREAM_PRESETS, entry.source, "agent.cordis.yml"),
|
|
39
|
+
"utf8",
|
|
40
|
+
);
|
|
41
|
+
const actual = readFileSync(join(OUT_DIR, entry.source, "agent.cordis.yml"), "utf8");
|
|
42
|
+
expect(actual).toBe(renderComposition(entry.source, upstream));
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
it.each(PRESET_SOURCES)("$source loads the fork for workspace instructions", (entry) => {
|
|
47
|
+
const text = readFileSync(join(OUT_DIR, entry.source, "agent.cordis.yml"), "utf8");
|
|
48
|
+
// preset 是会话级 composition:不在这里换掉,上游插件仍会把 baseline 作为
|
|
49
|
+
// user 消息注入一次(profile 级的 disable 管不到 preset)。
|
|
50
|
+
expect(text).toContain(`name: "${INSTRUCTIONS_PLUGIN}"`);
|
|
51
|
+
expect(text).not.toContain('"@deepseek-ai/dsh-agent-instructions"');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each(PRESET_SOURCES)("$source carries generated metadata", (entry) => {
|
|
55
|
+
const actual = readFileSync(join(OUT_DIR, entry.source, "preset.yml"), "utf8");
|
|
56
|
+
expect(actual).toBe(renderMetadata(entry));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("drops the persona row: the deployment system-prompt owns the persona", () => {
|
|
60
|
+
for (const entry of PRESET_SOURCES) {
|
|
61
|
+
const text = readFileSync(join(OUT_DIR, entry.source, "agent.cordis.yml"), "utf8");
|
|
62
|
+
// preset 自带 persona 行会在 agent scope 遮蔽部署级 persona(system-prompt 的
|
|
63
|
+
// personaPrefix),所以产物里不能有它。
|
|
64
|
+
expect(text).not.toContain(`- id: ${PERSONA_ROW_ID}\n`);
|
|
65
|
+
expect(text).not.toContain("You are a coding agent powered by the {{model}} model.");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("the output dir holds nothing beyond the generated sources", () => {
|
|
70
|
+
const expected = PRESET_SOURCES.map((entry) => entry.source).sort();
|
|
71
|
+
const actual = readdirSync(OUT_DIR, { withFileTypes: true })
|
|
72
|
+
.filter((dirent) => dirent.isDirectory())
|
|
73
|
+
.map((dirent) => dirent.name)
|
|
74
|
+
.sort();
|
|
75
|
+
expect(actual).toEqual(expected);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("clears stale directories from a previous generation", () => {
|
|
79
|
+
// 同目录再生成一次,且先塞一个残留目录:它必须消失。
|
|
80
|
+
mkdirSync(join(OUT_DIR, "stale"), { recursive: true });
|
|
81
|
+
generatePresets(OUT_DIR);
|
|
82
|
+
expect(readdirSync(OUT_DIR)).not.toContain("stale");
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从上游 shipped preset 生成自定义 preset 到构建输出目录(`dist/presets/standard`
|
|
3
|
+
* 与 `dist/presets/ptc`)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么是生成而非复制:preset 是运行期被 discovery 扫描的 composition,
|
|
6
|
+
* 必须随包发布;但手工维护副本会与上游 drift。这里把上游 `standard` / `ptc`
|
|
7
|
+
* 当数据读入——`js-yaml` 用 include 的 `entryListSchema` 解析(`!!js` 标签
|
|
8
|
+
* 因此保留为表达式节点)——在 JS 里改两处(删掉自带的 `persona` 行、把
|
|
9
|
+
* `agent-instructions` 行指向本仓库的 fork),再 `dump` 回 YAML。上游任何结构性
|
|
10
|
+
* 改动(新增/重命名 row、改字段)都自动跟随,不依赖易碎的文本锚点。
|
|
11
|
+
*
|
|
12
|
+
* 产物落在 `dist/` 而非源码树:dist 是构建输出(gitignore),既不会与 oxfmt
|
|
13
|
+
* 互相改格式,也不会把派生文件混进源码。
|
|
14
|
+
*
|
|
15
|
+
* 代价:`dump` 不保留注释(上游 composition 的说明注释会丢)。
|
|
16
|
+
*
|
|
17
|
+
* 用法:由 `tsdown.config.ts` 的 `build:done` hook 在每次构建时调用
|
|
18
|
+
* (见 {@link presetHooks});也可单独跑
|
|
19
|
+
* `pnpm exec tsx packages/dsh-preset/tool/generate-presets.ts [outDir]`。
|
|
20
|
+
* 校验:`packages/dsh-preset/src/__tests__/generated-presets.spec.ts`
|
|
21
|
+
* @module @morlay/dsh-preset/tool/generate-presets
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
28
|
+
import yaml from "js-yaml";
|
|
29
|
+
|
|
30
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
31
|
+
|
|
32
|
+
// 上游 shipped preset 根目录:经上游包(devDependency)解析,不依赖仓库布局。
|
|
33
|
+
// 子目录不在上游 exports 内,故从 `package.json`(exports 显式导出)起拼。
|
|
34
|
+
export const UPSTREAM_PRESETS = join(
|
|
35
|
+
dirname(fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-agent-presets/package.json"))),
|
|
36
|
+
"presets",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** 每个自定义 preset:上游源 preset(同时是产物目录名)与展示元数据。 */
|
|
40
|
+
export const PRESET_SOURCES = [
|
|
41
|
+
{
|
|
42
|
+
source: "standard",
|
|
43
|
+
name: "标准模式",
|
|
44
|
+
description:
|
|
45
|
+
"功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。",
|
|
46
|
+
order: 1,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
source: "ptc",
|
|
50
|
+
name: "PTC 模式",
|
|
51
|
+
description:
|
|
52
|
+
"功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。",
|
|
53
|
+
order: 2,
|
|
54
|
+
},
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
/** 生成产物顶部的来源标注,插在文件最前,便于核对与追溯。 */
|
|
58
|
+
function generatedHeader(): string {
|
|
59
|
+
return `# 本文件由 packages/dsh-preset/tool/generate-presets.ts 生成,请勿手工编辑
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 上游 composition 的一行;只用到 id 与 name。 */
|
|
64
|
+
interface CompositionRow {
|
|
65
|
+
id?: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
config?: Record<string, unknown>;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 上游 preset 里自带 persona 的行 id(本仓库的 persona 走部署级 system-prompt)。 */
|
|
72
|
+
export const PERSONA_ROW_ID = "persona";
|
|
73
|
+
|
|
74
|
+
/** 上游 preset 里承载工作区指令的行 id。 */
|
|
75
|
+
export const INSTRUCTIONS_ROW_ID = "agent-instructions";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 承载工作区指令的插件:本仓库的 fork(baseline 走 system prompt)。
|
|
79
|
+
*
|
|
80
|
+
* preset 是**会话级** composition,profile 级的 patch(禁用上游行 + 挂 fork)
|
|
81
|
+
* 管不到它——不在这里替换,每个会话仍会由上游插件把 baseline 作为 user 消息
|
|
82
|
+
* 注入一次。
|
|
83
|
+
*/
|
|
84
|
+
export const INSTRUCTIONS_PLUGIN = "@morlay/dsh-agent-instructions-as-prompt";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 把上游 composition 文本渲染成产物文本:解析 → 改 persona 与 instructions 两行 → dump。
|
|
88
|
+
* @param source - 上游 preset id,用于 header 与诊断。
|
|
89
|
+
* @param upstream - 上游 composition 文本。
|
|
90
|
+
* @returns 带 header 的产物文本。
|
|
91
|
+
*/
|
|
92
|
+
export function renderComposition(source: string, upstream: string): string {
|
|
93
|
+
const rows = yaml.load(upstream, {
|
|
94
|
+
schema: entryListSchema,
|
|
95
|
+
}) as CompositionRow[];
|
|
96
|
+
const instructions = rows.find((row) => row.id === INSTRUCTIONS_ROW_ID);
|
|
97
|
+
if (instructions === undefined) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`generate-presets: upstream \`${source}\` has no \`${INSTRUCTIONS_ROW_ID}\` row; ` +
|
|
100
|
+
"upstream changed — re-check which plugin loads the workspace instructions",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
instructions.name = INSTRUCTIONS_PLUGIN;
|
|
105
|
+
// 删掉 preset 自带的 `persona` 行:它会在 agent scope 注册
|
|
106
|
+
// `deployment:persona-prefix` 并**遮蔽**部署级 persona(system-prompt 的
|
|
107
|
+
// personaPrefix,见上游 system-prompt 的 Config 文档)。个人提示词只由
|
|
108
|
+
// `cordis.patch.yml` 的 system-prompt 行提供,这里不再复制一份。
|
|
109
|
+
const persona = rows.findIndex((row) => row.id === PERSONA_ROW_ID);
|
|
110
|
+
if (persona >= 0) rows.splice(persona, 1);
|
|
111
|
+
// `quotingType: '"'` 是刻意选择:yaml.dump 默认用单引号,而仓库的 oxfmt 会把
|
|
112
|
+
// YAML 单引号改成双引号——不显式指定就会 fmt 与生成器来回改。指定后产物与
|
|
113
|
+
// `oxfmt --check` 零差异(实测),故 `presets/**` 无需 fmt 忽略。
|
|
114
|
+
const body = yaml.dump(rows, {
|
|
115
|
+
schema: entryListSchema,
|
|
116
|
+
lineWidth: -1,
|
|
117
|
+
quotingType: '"',
|
|
118
|
+
});
|
|
119
|
+
return `${generatedHeader()}${body}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 渲染 preset.yml 展示元数据(自定义 id 不走内置文案 fold)。 */
|
|
123
|
+
export function renderMetadata(entry: (typeof PRESET_SOURCES)[number]): string {
|
|
124
|
+
return `name: ${entry.name}\ndescription: ${entry.description}\norder: ${String(entry.order)}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 生成目录相对包根的默认位置(构建输出,随 files 发布)。 */
|
|
128
|
+
export const PRESETS_OUT_DIR = "dist/presets";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 生成全部自定义 preset 到 `outDir`。
|
|
132
|
+
* @param outDir - 输出目录绝对路径;缺省为包根下的 {@link PRESETS_OUT_DIR}。
|
|
133
|
+
* @returns 产物文件路径列表。
|
|
134
|
+
*/
|
|
135
|
+
export function generatePresets(outDir: string = join(PACKAGE_ROOT, PRESETS_OUT_DIR)): string[] {
|
|
136
|
+
// 先整目录清空:产物完全派生自本脚本,残留目录(改过 source、旧命名)不该留下
|
|
137
|
+
// ——否则 discovery 会把它们当有效 preset 扫出来。
|
|
138
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
139
|
+
const written: string[] = [];
|
|
140
|
+
for (const entry of PRESET_SOURCES) {
|
|
141
|
+
const upstream = readFileSync(join(UPSTREAM_PRESETS, entry.source, "agent.cordis.yml"), "utf8");
|
|
142
|
+
// 产物目录名 = 上游 preset id:canonical id 让展示名走客户端语言字典
|
|
143
|
+
// (`presetDisplayText` 对 trust=system 且 id 命中的行做本地化)。
|
|
144
|
+
const dir = join(outDir, entry.source);
|
|
145
|
+
mkdirSync(dir, { recursive: true });
|
|
146
|
+
const compositionPath = join(dir, "agent.cordis.yml");
|
|
147
|
+
writeFileSync(compositionPath, renderComposition(entry.source, upstream));
|
|
148
|
+
const metadataPath = join(dir, "preset.yml");
|
|
149
|
+
writeFileSync(metadataPath, renderMetadata(entry));
|
|
150
|
+
written.push(compositionPath, metadataPath);
|
|
151
|
+
}
|
|
152
|
+
return written;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* tsdown hooks:构建完成后把 preset 生成到构建输出目录。
|
|
157
|
+
*
|
|
158
|
+
* 必须挂 `build:done` 而不是 `build:prepare`——tsdown 的时序是
|
|
159
|
+
* `build:prepare` → `clean()`(清空 outDir)→ rolldown → `build:done`,
|
|
160
|
+
* 在 clean 之前写会被删掉。挂在 done 上则产物与 JS/d.ts 同批产出。
|
|
161
|
+
* @returns 供 `UserConfig.hooks` 使用的 hook 集合。
|
|
162
|
+
*/
|
|
163
|
+
export function presetHooks(): {
|
|
164
|
+
"build:done": (ctx: { options: { outDir: string } }) => void;
|
|
165
|
+
} {
|
|
166
|
+
return {
|
|
167
|
+
"build:done": (ctx) => {
|
|
168
|
+
// 用 tsdown 解析后的 outDir(尊重用户覆盖),而非硬编码 dist。
|
|
169
|
+
const written = generatePresets(join(ctx.options.outDir, "presets"));
|
|
170
|
+
for (const path of written) process.stdout.write(`generated ${path}\n`);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`) {
|
|
176
|
+
const outDir = process.argv[2];
|
|
177
|
+
for (const path of generatePresets(outDir)) {
|
|
178
|
+
process.stdout.write(`generated ${path}\n`);
|
|
179
|
+
}
|
|
180
|
+
}
|