dsh-plugin-prompt-tool 0.5.0 → 0.6.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/README.md +18 -12
- package/engine/anchor-match.mjs +117 -0
- package/engine/compositions/library/context-gate.yml +2 -2
- package/engine/compositions/library/delegation.yml +2 -2
- package/engine/compositions/library/str-replace-editor.yml +1 -1
- package/engine/compositions/library/tool-bootstrap.yml +2 -2
- package/engine/compositions/library/tool-filter.yml +3 -3
- package/engine/compositions/source/local/context-gate.yml +2 -2
- package/engine/compositions/source/local/tool-bootstrap.yml +3 -3
- package/engine/executor.mjs +9 -2
- package/engine/interpolate.mjs +116 -0
- package/engine/layers.mjs +26 -22
- package/engine/schema.mjs +18 -0
- package/engine/session-vars.mjs +52 -0
- package/engine/shared.mjs +0 -19
- package/engine/strategies.mjs +28 -38
- package/lib/client.js +471 -162
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +35 -9
- package/lib/index.mjs +376 -92
- package/lib/preset-core.d.mts +2 -2
- package/lib/preset-core.mjs +3 -6
- package/lib/{prompt-configs-BcP6miu9.d.mts → prompt-configs-B4vH09wx.d.mts} +1 -3
- package/lib/{prompt-configs-k7wuZs0C.mjs → prompt-configs-ThS4iXPg.mjs} +85 -33
- package/package.json +105 -104
- package/preset/anchored/preset.yml +40 -42
- package/preset/creative/preset.yml +33 -30
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +420 -0
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +165 -0
- package/preset/liangshen/preset.yml +24 -18
- package/preset/minimal/preset.yml +27 -24
- package/preset/ptc/preset.yml +23 -17
- package/preset/standard/preset.yml +23 -17
- package/templates/20-system-section.yml +5 -0
- package/templates/70-subagent-maintenance.yml +38 -0
- package/engine/compositions/library/router-first-turn.yml +0 -9
- package/engine/compositions/source/local/router-first-turn.yml +0 -5
- package/engine/router-first-turn.mjs +0 -85
package/README.md
CHANGED
|
@@ -21,14 +21,16 @@ dsh --profile prompt-tool # 首次启
|
|
|
21
21
|
- 🔌 **六层一次接线**:一个引擎注册全部可注入层级,共享同一套过滤与降级语义
|
|
22
22
|
- ✍️ **一切皆可配置**:`layer / strategy / position / promotion / subagents / modelScope / mergeMode / order / text / texts / fill / variables / params` 全开放
|
|
23
23
|
- 🧑🤝🧑 **子代理三态**:`subagents: none / inherit / only`,身份类提示词可只注入子代理
|
|
24
|
-
- 🗂️
|
|
25
|
-
- 🧩
|
|
24
|
+
- 🗂️ **内容与执行分离**:每条提示词配置渲染为 `~/.dsh/.agent-presets/<预设>/prompt-configs/` 下的 yml,引擎按文件名数字前缀顺序扫描
|
|
25
|
+
- 🧩 **三层合并**:引擎默认(按 params 生成)< 模板默认 promptConfigs < 预设 promptConfigs,同名 `id` 覆盖
|
|
26
26
|
- 🖥️ **独立工作台**:侧边栏「提示词工具」入口(主会话/子代理/注入层/技能设置/预设配置五页)+ 设置面板提示词配置页(列表/表单/模板插入/保存前校验)
|
|
27
|
-
- 🧪 **七种内容策略**:`static / first-turn-anchor / guide-auto / custom-fallback / instruction-hint / placeholder / world-book
|
|
27
|
+
- 🧪 **七种内容策略**:`static / first-turn-anchor / guide-auto / custom-fallback / instruction-hint / placeholder / world-book`(world-book 支持 ST selectiveLogic 选择性触发:任一/副键全中/排除)
|
|
28
28
|
- 🛡️ **失败不伤会话**:单条失败跳过 + `warnOnce`;配置错误挂载时 fail loud;`dedupe: session` 持久幂等
|
|
29
|
-
- 🎭 **SillyTavern 导入**:JSON 预设卡片一键转换为本地预设——`prompts[]`
|
|
29
|
+
- 🎭 **SillyTavern 导入**:JSON 预设卡片一键转换为本地预设——`prompts[]` 映射提示词配置、setvar/getvar 收集进顶层 `variables`(未定义自定义宏自动登记空值占位)、`enable_web_search` 按开关装配工具;采样参数剥离(模型设置 UI 管理)
|
|
30
30
|
- 🎴 **角色卡库**:SillyTavern 角色卡(PNG tEXt chunk `ccv3`/`chara`,或 chara_card JSON)导入独立库(`.characters/<id>/`,含原图/转换参数/角色记忆),按需「导入到当前预设」(`chara-<卡>-` 前缀合并、幂等可移除),多文件自动合并
|
|
31
|
-
- 📚 **世界书**:`character_book` 转 world-book 策略配置(`keys` 命中触发 / `constant` 常驻 / `useRegex`
|
|
31
|
+
- 📚 **世界书**:`character_book` 转 world-book 策略配置(`keys` 命中触发 / `constant` 常驻 / `useRegex` 正则 / `selectiveLogic` 组合逻辑),与模块卡片同一存储与编辑(模块列表「世界书」过滤 + 批量启用/禁用)
|
|
32
|
+
- 🧩 **模板变量**:预设级 `variables` 段(`{{key}}` 插值源)——模块列表顶部「模板变量」卡片统一编辑(可折叠/清空/停用/失焦自动保存);锚定匹配引擎(anchor-match)统一 custom-fallback 与 world-book 的匹配语义
|
|
33
|
+
- 💬 **会话变量工具**:`session_var`(list/get/set/clear)——模型维护角色状态(`{{心情}}` 等),会话级覆盖预设默认;ST 运行时宏(`{{lastusermessage}}` / `{{lastcharmessage}}`)从会话事件提取
|
|
32
34
|
|
|
33
35
|
## 预设参数体系
|
|
34
36
|
|
|
@@ -50,7 +52,7 @@ dsh --profile prompt-tool # 首次启
|
|
|
50
52
|
| PTC/门控 | `usePtcMode` `bootstrapMaxTokens` `injectPrompt` `allowKinds` |
|
|
51
53
|
| 主对话模型 | `modelProvider` `modelName` `modelReasoningEffort` `modelTemperature` `modelMaxTokens` |
|
|
52
54
|
| 子代理模型 | `subagentModelProvider` `subagentModelName` `subagentReasoningEffort` `subagentTemperature` `subagentMaxTokens` |
|
|
53
|
-
| 人设 | `
|
|
55
|
+
| 人设 | 模块列表的 `persona-main`(system-section + `deployment:persona`,complete 互斥 + suppressRuntimeContext);`subagentPersona`(子代理显式,缺省经 scope 链继承主会话) |
|
|
54
56
|
| 工具集 | `toolFilterAllow` `toolFilterDeny`(子代理 toolFilter;主对话 tool-filter 模块共用) |
|
|
55
57
|
| 深度 | `maxDepth`(0 禁止委派 / `provider-managed` / 正整数) |
|
|
56
58
|
|
|
@@ -71,7 +73,7 @@ dsh --profile prompt-tool # 首次启
|
|
|
71
73
|
|
|
72
74
|
- `mergeMode`:`separate`(默认)同位置多条为独立消息;`merged` 同位置拼接为一条
|
|
73
75
|
- `order`:数值小者更靠近插入锚点,同时决定 `merged` 组内拼接顺序
|
|
74
|
-
-
|
|
76
|
+
- 文本插值:`{{key}}` 全层支持——配置/预设 `variables` 优先,ST 运行时宏(lastusermessage 等)次之,内置 `{{DSH_HOME}}/{{WORKSPACE}}/{{CWD}}` 兜底,未注册保留字面(system-section 注册期无会话时运行时宏替换为空,不残留)
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
## SillyTavern 导入
|
|
@@ -79,7 +81,8 @@ dsh --profile prompt-tool # 首次启
|
|
|
79
81
|
工作台「预设配置」页导入 SillyTavern JSON 预设卡片(导入包无定义文件、仅含单个 `.json` 时自动识别转换),按注入层级映射为本地预设:
|
|
80
82
|
|
|
81
83
|
- `prompts[]` → `promptConfigs`:`system_prompt + role=system` → `system-section`(多条可 `mergeMode: merged` 拼接);其余 → `pre-step`(`injection_position=0` → `before-all`,否则 `after-user`);OFF 状态与 `injection_order` 原样保留
|
|
82
|
-
-
|
|
84
|
+
- 采样参数(`temperature` / `openai_max_tokens` / `reasoning_effort`)**剥离**——模型参数统一由「模型设置」UI / 宿主默认管理
|
|
85
|
+
- ST 变量:`setvar`/`getvar`(含默认值)收集进顶层 `variables`;未定义自定义宏自动登记空值占位(不留字面)
|
|
83
86
|
- `enable_web_search`:`true` → 组装 `tool-web`(fetch 启用);`false` → 不组装,改加 `tool-filter` 黑名单 `web_search / web_fetch`
|
|
84
87
|
- `modules` 按需装配:`prompt-config-engine` 始终;含 system-section 时补 `persona`(`complete: false` 允许 system 段生效)
|
|
85
88
|
|
|
@@ -92,7 +95,7 @@ dsh --profile prompt-tool # 首次启
|
|
|
92
95
|
- **PNG**:tEXt chunk(`ccv3` 优先 / `chara` 兜底)base64 解析,原图存 `avatar.png`(字节无损)
|
|
93
96
|
- **JSON**:chara_card_v2/v3 直接转换;多文件(角色卡 × 响应预设)自动合并
|
|
94
97
|
- 正文映射:`first_mes` → 开场白(`dedupe: session`)、`alternate_greetings` → 备用开场白、
|
|
95
|
-
`description/personality/scenario` →
|
|
98
|
+
`description/personality/scenario` → 角色设定;采样参数剥离(模型设置 UI 管理)
|
|
96
99
|
- **导入到当前预设**:参数合并进当前预设 promptConfigs(`chara-<卡>-` 前缀、幂等);可一键移除
|
|
97
100
|
- **角色记忆**:`memory.md` 跟随角色卡跨预设,应用时合并为 world-book constant 配置注入
|
|
98
101
|
|
|
@@ -104,8 +107,11 @@ dsh --profile prompt-tool # 首次启
|
|
|
104
107
|
- **匹配选项**:`caseSensitive` / `wholeWords` / `useRegex`(keys 按正则)
|
|
105
108
|
- **管理**:模块列表顶部下拉选「世界书」过滤(完整模块卡片编辑 + 批量启用/禁用);
|
|
106
109
|
模型工具 `world_book_list/upsert/delete`(`note` 写入角色卡记忆)
|
|
107
|
-
- **ST 变量**:`setvar`
|
|
108
|
-
`trim`/注释/ERA 剥离,`{{user}}`/`{{char}}`
|
|
110
|
+
- **ST 变量**:`setvar`/`getvar` 收集进顶层 `variables`、未定义自定义宏自动登记空值占位;
|
|
111
|
+
`trim`/注释/ERA 剥离,`{{user}}`/`{{char}}` 替换;运行时宏(lastusermessage/lastcharmessage)
|
|
112
|
+
从会话事件提取;TavernHelper 扩展注入物自动剥离
|
|
113
|
+
- **会话变量**:`session_var` 工具(list/get/set/clear)维护角色状态(会话级覆盖预设默认,
|
|
114
|
+
结束即失);跨会话长期记忆用 `world_book` note(持久 memory.md 跟随角色卡)
|
|
109
115
|
|
|
110
116
|
详细转换规则见 [SillyTavern.md](SillyTavern.md)。
|
|
111
117
|
|
|
@@ -113,7 +119,7 @@ dsh --profile prompt-tool # 首次启
|
|
|
113
119
|
|
|
114
120
|
```sh
|
|
115
121
|
pnpm install && pnpm build
|
|
116
|
-
pnpm test #
|
|
122
|
+
pnpm test # 287 单测:渲染/合并/六层接线/preset 生成/锚定匹配/插值/会话变量
|
|
117
123
|
pnpm typecheck && pnpm lint
|
|
118
124
|
pnpm sync:anchored # 刷新 upstream/dsh-anchored-standard 内联快照
|
|
119
125
|
pnpm sync:yaml # 刷新 engine/vendor/yaml(生成目录运行时 YAML 解析器)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* anchor-match — 关键词锚定匹配引擎(纯函数,无状态,无依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 从 strategies 拆解:custom-fallback(自定义锚定词)与 world-book(关键词触发)
|
|
5
|
+
* 共用同一套匹配语义——主键/副键、大小写、整词、正则、组合逻辑。
|
|
6
|
+
*
|
|
7
|
+
* 逻辑对齐 ST world_info_logic:
|
|
8
|
+
* any = AND_ANY(0):主键或副键任一命中即激活(world-book 默认 / custom-fallback 单锚);
|
|
9
|
+
* all = AND_ALL(3):主键命中且副键全部命中;
|
|
10
|
+
* not = NOT_ALL(1):主键命中且副键全部未命中(排除);
|
|
11
|
+
* notAny = NOT_ANY(2):主键命中且至少一个副键未命中(部分排除)。
|
|
12
|
+
*
|
|
13
|
+
* 模式:
|
|
14
|
+
* scan = 全文扫描(world-book 消息批匹配);
|
|
15
|
+
* prefix = 文本开头匹配(custom-fallback 首轮 reasoning 锚定确认:ASCII 词边界前缀,
|
|
16
|
+
* 非 ASCII 直 prefix)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 组合逻辑(ST world_info_logic 映射)。 */
|
|
20
|
+
export const MATCH_LOGIC = {
|
|
21
|
+
ANY: 'any',
|
|
22
|
+
ALL: 'all',
|
|
23
|
+
NOT: 'not',
|
|
24
|
+
NOT_ANY: 'notAny',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 正则转义(关键词原样匹配时用)。 */
|
|
28
|
+
export function escapeRegExp(text) {
|
|
29
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 单键正则编译(逐键匹配:any/all/not 需要精确的命中键数,捕获组会干扰 match 计数)。 */
|
|
33
|
+
function compileSingle(key, { caseSensitive, wholeWords, useRegex }) {
|
|
34
|
+
const flags = caseSensitive ? '' : 'i'
|
|
35
|
+
if (useRegex) {
|
|
36
|
+
// ST use_regex=true:键原样作为正则(作者负责合法性)。
|
|
37
|
+
return new RegExp(key, flags)
|
|
38
|
+
}
|
|
39
|
+
if (wholeWords) {
|
|
40
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])(${escapeRegExp(key)})(?![\\p{L}\\p{N}])`, `${flags}u`)
|
|
41
|
+
}
|
|
42
|
+
return new RegExp(escapeRegExp(key), flags)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 编译键列表 → [{ key, re }](剔除空键)。 */
|
|
46
|
+
function compileKeyList(list, options) {
|
|
47
|
+
return (Array.isArray(list) ? list : [])
|
|
48
|
+
.map((key) => String(key).trim())
|
|
49
|
+
.filter((key) => key.length > 0)
|
|
50
|
+
.map((key) => ({ key, re: compileSingle(key, options) }))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 创建锚定匹配器。
|
|
55
|
+
* @param {object} options
|
|
56
|
+
* @param {string[]} options.keys 主键
|
|
57
|
+
* @param {string[]} [options.secondaryKeys] 副键
|
|
58
|
+
* @param {boolean} [options.caseSensitive]
|
|
59
|
+
* @param {boolean} [options.wholeWords]
|
|
60
|
+
* @param {boolean} [options.useRegex]
|
|
61
|
+
* @param {'any'|'all'|'not'|'notAny'} [options.logic] 组合逻辑(缺省 any)
|
|
62
|
+
* @param {'scan'|'prefix'} [options.mode] 匹配模式(缺省 scan)
|
|
63
|
+
* @returns {{ scan: (text: string) => { primary: number, secondary: number, active: boolean } }}
|
|
64
|
+
*/
|
|
65
|
+
export function createAnchorMatcher(options = {}) {
|
|
66
|
+
const {
|
|
67
|
+
keys = [],
|
|
68
|
+
secondaryKeys = [],
|
|
69
|
+
caseSensitive = false,
|
|
70
|
+
wholeWords = false,
|
|
71
|
+
useRegex = false,
|
|
72
|
+
logic = MATCH_LOGIC.ANY,
|
|
73
|
+
mode = 'scan',
|
|
74
|
+
} = options
|
|
75
|
+
const primaryList = compileKeyList(keys, { caseSensitive, wholeWords, useRegex })
|
|
76
|
+
const secondaryList = compileKeyList(secondaryKeys, { caseSensitive, wholeWords, useRegex })
|
|
77
|
+
|
|
78
|
+
const scan = (raw) => {
|
|
79
|
+
const text = String(raw ?? '')
|
|
80
|
+
if (mode === 'prefix') {
|
|
81
|
+
// custom-fallback 锚定确认:仅主键首词,文本开头匹配。
|
|
82
|
+
const word = (Array.isArray(keys) ? keys : []).map((key) => String(key).trim())
|
|
83
|
+
.find((key) => key.length > 0)
|
|
84
|
+
if (word === undefined || text.length === 0) return { primary: 0, secondary: 0, active: false }
|
|
85
|
+
const hit = /^[\x20-\x7E]+$/.test(word)
|
|
86
|
+
? new RegExp(`^${escapeRegExp(word.toLowerCase())}\\b`, 'i').test(text)
|
|
87
|
+
: text.startsWith(word)
|
|
88
|
+
return { primary: hit ? 1 : 0, secondary: 0, active: hit }
|
|
89
|
+
}
|
|
90
|
+
if (primaryList.length === 0 && secondaryList.length === 0) {
|
|
91
|
+
return { primary: 0, secondary: 0, active: false }
|
|
92
|
+
}
|
|
93
|
+
const countHits = (list) => {
|
|
94
|
+
let hits = 0
|
|
95
|
+
for (const { re } of list) {
|
|
96
|
+
re.lastIndex = 0
|
|
97
|
+
if (re.test(text)) hits += 1
|
|
98
|
+
}
|
|
99
|
+
return hits
|
|
100
|
+
}
|
|
101
|
+
const primary = countHits(primaryList)
|
|
102
|
+
const secondary = countHits(secondaryList)
|
|
103
|
+
let active = false
|
|
104
|
+
if (logic === MATCH_LOGIC.ALL) {
|
|
105
|
+
active = primary > 0 && (secondaryList.length === 0 || secondary === secondaryList.length)
|
|
106
|
+
} else if (logic === MATCH_LOGIC.NOT) {
|
|
107
|
+
active = primary > 0 && secondary === 0
|
|
108
|
+
} else if (logic === MATCH_LOGIC.NOT_ANY) {
|
|
109
|
+
active = primary > 0 && secondaryList.length > 0 && secondary < secondaryList.length
|
|
110
|
+
} else {
|
|
111
|
+
active = primary > 0 || secondary > 0
|
|
112
|
+
}
|
|
113
|
+
return { primary, secondary, active }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { scan }
|
|
117
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
config:
|
|
7
7
|
promoteOn: either
|
|
8
8
|
includeSubagents: false
|
|
9
|
-
allowKinds:
|
|
9
|
+
allowKinds: __allowKinds__
|
|
10
10
|
|
|
11
11
|
# ── bootstrap ───────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
#
|
|
38
38
|
# `includeSubagents: true` keeps the subagent phase in sync with the
|
|
39
39
|
# context-gate row: a subagent's first request sees the bootstrap pair, then
|
|
40
|
-
# its own first reply or tool call promotes it to the assembled catalog.
|
|
40
|
+
# its own first reply or tool call promotes it to the assembled catalog.
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
provider: spawn
|
|
38
38
|
toolName: subagent
|
|
39
39
|
backgroundMode: continuable
|
|
40
|
-
|
|
40
|
+
__subagentConfig__
|
|
41
41
|
|
|
42
42
|
- id: tool-subagent-fork
|
|
43
43
|
name: '@deepseek-ai/dsh-tool-subagent'
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
provider: fork
|
|
46
46
|
toolName: subagent_fork
|
|
47
47
|
backgroundMode: continuable
|
|
48
|
-
|
|
48
|
+
__subagentConfig__
|
|
49
49
|
|
|
50
50
|
# Production dsh does not install these optional providers. An opting-in
|
|
51
51
|
# Profile mounts each provider once on the host plane; copy this preset,
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
bootstrapTools: [bash, str_replace_editor]
|
|
8
8
|
promoteOn: either
|
|
9
9
|
includeSubagents: false
|
|
10
|
-
usePtcMode:
|
|
11
|
-
|
|
10
|
+
usePtcMode: __usePtcMode__
|
|
11
|
+
__bootstrapMaxTokens__
|
|
12
12
|
# Post-compaction core work set: the model is mid-task and needs to keep
|
|
13
13
|
# working, but faces a small catalog instead of the full Standard set.
|
|
14
14
|
compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
- id: tool-filter
|
|
8
8
|
name: ./engine/tool-filter.mjs
|
|
9
9
|
config:
|
|
10
|
-
allow:
|
|
11
|
-
deny:
|
|
12
|
-
includeSubagents:
|
|
10
|
+
allow: __mainToolFilterAllow__
|
|
11
|
+
deny: __mainToolFilterDeny__
|
|
12
|
+
includeSubagents: __mainToolFilterSubagents__
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
config:
|
|
4
4
|
promoteOn: either
|
|
5
5
|
includeSubagents: false
|
|
6
|
-
allowKinds:
|
|
6
|
+
allowKinds: __allowKinds__
|
|
7
7
|
|
|
8
8
|
# ── bootstrap ───────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -34,4 +34,4 @@
|
|
|
34
34
|
#
|
|
35
35
|
# `includeSubagents: true` keeps the subagent phase in sync with the
|
|
36
36
|
# context-gate row: a subagent's first request sees the bootstrap pair, then
|
|
37
|
-
# its own first reply or tool call promotes it to the assembled catalog.
|
|
37
|
+
# its own first reply or tool call promotes it to the assembled catalog.
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
bootstrapTools: [bash, str_replace_editor]
|
|
5
5
|
promoteOn: either
|
|
6
6
|
includeSubagents: true
|
|
7
|
-
usePtcMode:
|
|
8
|
-
|
|
7
|
+
usePtcMode: __usePtcMode__
|
|
8
|
+
__bootstrapMaxTokens__
|
|
9
9
|
# Post-compaction core work set: the model is mid-task and needs to keep
|
|
10
10
|
# working, but faces a small catalog instead of the full Standard set.
|
|
11
11
|
compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
|
|
12
12
|
|
|
13
|
-
# ── prompt-tool 模板自带行(由 write-preset 按 manifest 变量物化) ─────────────
|
|
13
|
+
# ── prompt-tool 模板自带行(由 write-preset 按 manifest 变量物化) ─────────────
|
package/engine/executor.mjs
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import {
|
|
8
8
|
PROMOTE_EVENTS,
|
|
9
9
|
createWarnOnce,
|
|
10
|
-
interpolateVariables,
|
|
11
10
|
isDelegated,
|
|
12
11
|
matchesModel,
|
|
13
12
|
newMessageId,
|
|
14
13
|
} from './shared.mjs'
|
|
14
|
+
import { interpolateVariables } from './interpolate.mjs'
|
|
15
15
|
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
16
16
|
import { wireLayers } from './layers.mjs'
|
|
17
|
+
import { sessionVarsSnapshot } from './session-vars.mjs'
|
|
17
18
|
|
|
18
19
|
const name = 'prompt-config-engine'
|
|
19
20
|
|
|
@@ -169,7 +170,13 @@ export function applyPromptConfigs(ctx, configs, options = {}) {
|
|
|
169
170
|
const patched = { ...resolved }
|
|
170
171
|
// params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
|
|
171
172
|
//(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
|
|
172
|
-
|
|
173
|
+
// 会话变量(session_var 工具维护)覆盖配置/预设默认:resolved > 会话 > params > 配置。
|
|
174
|
+
const mergedVars = {
|
|
175
|
+
...config.variables,
|
|
176
|
+
...config.params,
|
|
177
|
+
...sessionVarsSnapshot(session),
|
|
178
|
+
...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}),
|
|
179
|
+
}
|
|
173
180
|
if (typeof patched.text === 'string') {
|
|
174
181
|
// 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
|
|
175
182
|
patched.text = interpolateVariables(patched.text, mergedVars, session)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interpolate — 模板变量插值引擎(纯函数,无依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 从 shared 拆出:{{key}} 插值是引擎官方对齐的核心语义(variables 优先 + 内置
|
|
5
|
+
* 兜底),独立为纯能力包,与 anchor-match 同级;消费方(layers / executor /
|
|
6
|
+
* fillers)按需引用。
|
|
7
|
+
*
|
|
8
|
+
* 插值规则:
|
|
9
|
+
* - {{key}}:配置 variables 有值 → 替换;否则 ST 运行时宏(lastusermessage /
|
|
10
|
+
* lastcharmessage 等,大小写不敏感)→ 会话事件提取;否则动态宏(roll/random/
|
|
11
|
+
* pick/chance/time/date 等,支持 {{name::arg}} 参数)→ 运行时计算;否则内置
|
|
12
|
+
* 变量(DSH_HOME / WORKSPACE / CWD)→ 替换;否则保留字面(宽容)。
|
|
13
|
+
* - 键字符集:字母数字、下划线、点、中文、连字符(与 ST setvar/getvar 一致)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 会话事件中最后一条指定类型消息的文本(事件倒序扫描;无则空串)。 */
|
|
17
|
+
function lastMessageOf(session, type) {
|
|
18
|
+
const events = session?.events
|
|
19
|
+
if (!Array.isArray(events)) return ''
|
|
20
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
21
|
+
const event = events[index]
|
|
22
|
+
if (event?.type !== type) continue
|
|
23
|
+
const content = event.data?.message?.content
|
|
24
|
+
if (!Array.isArray(content)) continue
|
|
25
|
+
const text = content
|
|
26
|
+
.map((block) => (block !== null && typeof block === 'object' && typeof block.text === 'string') ? block.text : '')
|
|
27
|
+
.join('')
|
|
28
|
+
.trim()
|
|
29
|
+
if (text.length > 0) return text
|
|
30
|
+
}
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 逗号分隔随机选一个({{random::a,b,c}} / {{pick::a,b,c}})。 */
|
|
35
|
+
function pickRandom(arg) {
|
|
36
|
+
const items = String(arg ?? '').split(',').map((item) => item.trim()).filter((item) => item.length > 0)
|
|
37
|
+
if (items.length === 0) return ''
|
|
38
|
+
return items[Math.floor(Math.random() * items.length)]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 骰子表达式({{roll::2d6+3}} / {{roll::1d20}};非法表达式原样返回)。 */
|
|
42
|
+
function rollDice(arg) {
|
|
43
|
+
const text = String(arg ?? '').replace(/\s+/g, '').toLowerCase()
|
|
44
|
+
const match = text.match(/^(\d*)d(\d+)([+-]\d+)?$/)
|
|
45
|
+
if (match === null) return text
|
|
46
|
+
const count = match[1] === '' ? 1 : Number.parseInt(match[1], 10)
|
|
47
|
+
const sides = Number.parseInt(match[2], 10)
|
|
48
|
+
const modifier = match[3] === undefined ? 0 : Number.parseInt(match[3], 10)
|
|
49
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > 100 || !Number.isSafeInteger(sides) || sides <= 0) return text
|
|
50
|
+
let sum = 0
|
|
51
|
+
for (let index = 0; index < count; index++) sum += 1 + Math.floor(Math.random() * sides)
|
|
52
|
+
return String(sum + modifier)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 百分比概率({{chance::50}} → true/false)。 */
|
|
56
|
+
function chancePercent(arg) {
|
|
57
|
+
const value = Number(String(arg ?? '').replace('%', '').trim())
|
|
58
|
+
if (!Number.isFinite(value)) return ''
|
|
59
|
+
return Math.random() * 100 < value ? 'true' : 'false'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 本地 HH:MM({{time}})。 */
|
|
63
|
+
function formatTime(now) {
|
|
64
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** UTC YYYY-MM-DD({{date}})。 */
|
|
68
|
+
function formatDate(now) {
|
|
69
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** ST 动态宏(大小写不敏感;函数签名 (arg) => string;无会话上下文时同样可用)。 */
|
|
73
|
+
const DYNAMIC_MACROS = {
|
|
74
|
+
lastusermessage: (_arg, session) => lastMessageOf(session, 'user/message'),
|
|
75
|
+
lastcharmessage: (_arg, session) => lastMessageOf(session, 'assistant/message'),
|
|
76
|
+
// charIfNotGroup:ST 群聊时空、单聊为角色名;dsh 会话 header 无角色名
|
|
77
|
+
//(单角色会话),统一返回空串(不残留字面,也不注入错误内容)。
|
|
78
|
+
charifnotgroup: () => '',
|
|
79
|
+
random: (arg) => pickRandom(arg),
|
|
80
|
+
pick: (arg) => pickRandom(arg),
|
|
81
|
+
roll: (arg) => rollDice(arg),
|
|
82
|
+
chance: (arg) => chancePercent(arg),
|
|
83
|
+
time: () => formatTime(new Date()),
|
|
84
|
+
date: () => formatDate(new Date()),
|
|
85
|
+
weekday: () => '星期' + '日一二三四五六'[new Date().getDay()],
|
|
86
|
+
isotime: () => new Date().toISOString().slice(11, 19),
|
|
87
|
+
isodate: () => new Date().toISOString().slice(0, 10),
|
|
88
|
+
newline: () => '\n',
|
|
89
|
+
pipe: () => '|',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 模板变量插值:配置 variables 优先,ST 运行时宏次之,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}} 兜底。 */
|
|
93
|
+
export function interpolateVariables(text, variables, session) {
|
|
94
|
+
const builtins = {
|
|
95
|
+
DSH_HOME: process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : ''),
|
|
96
|
+
WORKSPACE: process.env.DSH_WORKSPACE ?? session?.header?.cwd ?? process.cwd(),
|
|
97
|
+
CWD: session?.header?.cwd ?? process.cwd(),
|
|
98
|
+
}
|
|
99
|
+
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)(?:::(.*?))?\}\}/g, (whole, key, arg) => {
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) return String(variables[key])
|
|
101
|
+
const dynamic = DYNAMIC_MACROS[key.toLowerCase()]
|
|
102
|
+
if (dynamic !== undefined) return dynamic(arg, session)
|
|
103
|
+
return Object.prototype.hasOwnProperty.call(builtins, key) ? builtins[key] : whole
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 仅做配置级静态变量替换(无 session 上下文的层)。 */
|
|
108
|
+
export function interpolateStatic(text, variables) {
|
|
109
|
+
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)(?:::(.*?))?\}\}/g, (whole, key, arg) => {
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) return String(variables[key])
|
|
111
|
+
// ST 运行时宏在无会话上下文(system-section 注册期)时替换为空串——
|
|
112
|
+
// 不残留字面,也不触发官方 unknown variable 渲染报错。
|
|
113
|
+
const dynamic = DYNAMIC_MACROS[key.toLowerCase()]
|
|
114
|
+
return dynamic !== undefined ? dynamic(arg, undefined) : whole
|
|
115
|
+
})
|
|
116
|
+
}
|
package/engine/layers.mjs
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* 所有注册都只作用于本插件提示词配置;单条失败 warnOnce 后继续。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
getService,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './shared.mjs'
|
|
6
|
+
import {
|
|
7
|
+
getService,
|
|
8
|
+
isDelegated,
|
|
9
|
+
keepDisposer,
|
|
10
|
+
matchesModel,
|
|
11
|
+
parseToolNames,
|
|
12
|
+
} from './shared.mjs'
|
|
13
|
+
import { interpolateVariables, interpolateStatic } from './interpolate.mjs'
|
|
15
14
|
|
|
16
15
|
const name = 'prompt-config-engine'
|
|
17
16
|
|
|
@@ -54,8 +53,8 @@ function textLayerGroups(configs) {
|
|
|
54
53
|
return groups
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
58
|
-
function wireSystemSections(ctx, configs, warnOnce) {
|
|
56
|
+
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
57
|
+
function wireSystemSections(ctx, configs, warnOnce) {
|
|
59
58
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
60
59
|
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
61
60
|
if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — system-section configs skipped`)
|
|
@@ -66,17 +65,22 @@ function wireSystemSections(ctx, configs, warnOnce) {
|
|
|
66
65
|
try {
|
|
67
66
|
const text = group.map((config) => configText(config)).filter((item) => item.length > 0).join('\n\n')
|
|
68
67
|
if (text.length === 0) continue
|
|
69
|
-
keepDisposer(ctx, systemPrompt.section({
|
|
70
|
-
name: typeof base.params?.sectionName === 'string' && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
|
|
71
|
-
order: base.order,
|
|
72
|
-
text,
|
|
73
|
-
...(base.params?.complete === true ? { complete: true } : {}),
|
|
74
|
-
}), `${name}: section ${base.id}`)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
68
|
+
keepDisposer(ctx, systemPrompt.section({
|
|
69
|
+
name: typeof base.params?.sectionName === 'string' && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
|
|
70
|
+
order: base.order,
|
|
71
|
+
text,
|
|
72
|
+
...(base.params?.complete === true ? { complete: true } : {}),
|
|
73
|
+
}), `${name}: section ${base.id}`)
|
|
74
|
+
// persona 语义透传:suppressRuntimeContext 抑制该 scope 的动态 runtime-context
|
|
75
|
+
// 快照(官方 dsh-persona includeRuntimeContext:false 等价;多段重复调用幂等)。
|
|
76
|
+
if (base.params?.suppressRuntimeContext === true) {
|
|
77
|
+
keepDisposer(ctx, systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
80
84
|
|
|
81
85
|
/** runtime-context:注册动态运行时上下文(晋升后由 context-gate 差分投影;支持 merged 拼接与 placeholder 函数 provider)。 */
|
|
82
86
|
function wireRuntimeContexts(ctx, configs, warnOnce) {
|
package/engine/schema.mjs
CHANGED
|
@@ -70,11 +70,22 @@ export function loadPromptConfigFiles(dirUrl) {
|
|
|
70
70
|
} catch (error) {
|
|
71
71
|
throw new TypeError(`${name}: configsDir ${String(dirUrl)} is not readable: ${String(error?.message ?? error)}`)
|
|
72
72
|
}
|
|
73
|
+
// 预设级模板变量(writePreset 生成 variables.yml):读入后合并进每条配置
|
|
74
|
+
// variables(配置自身优先);variables.yml 本身不当作配置解析。缺失或损坏
|
|
75
|
+
// 时为空变量源,不阻断加载。
|
|
76
|
+
let presetVariables = {}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = parseYaml(readFileSync(new URL('variables.yml', dirUrl), 'utf8'))
|
|
79
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) presetVariables = parsed
|
|
80
|
+
} catch {
|
|
81
|
+
// 无 variables.yml(旧产物/手写目录)或解析失败:保持空变量源。
|
|
82
|
+
}
|
|
73
83
|
const specs = []
|
|
74
84
|
const files = entries
|
|
75
85
|
.filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
|
|
76
86
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
77
87
|
for (const entry of files) {
|
|
88
|
+
if (entry.name === 'variables.yml') continue
|
|
78
89
|
const url = new URL(entry.name, dirUrl)
|
|
79
90
|
const raw = readFileSync(url, 'utf8')
|
|
80
91
|
if (/\.json$/i.test(entry.name)) {
|
|
@@ -83,6 +94,13 @@ export function loadPromptConfigFiles(dirUrl) {
|
|
|
83
94
|
specs.push(parsePromptConfigYaml(raw))
|
|
84
95
|
}
|
|
85
96
|
}
|
|
97
|
+
for (const spec of specs) {
|
|
98
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) continue
|
|
99
|
+
const own = spec.variables !== null && typeof spec.variables === 'object' && !Array.isArray(spec.variables)
|
|
100
|
+
? spec.variables
|
|
101
|
+
: {}
|
|
102
|
+
spec.variables = { ...presetVariables, ...own }
|
|
103
|
+
}
|
|
86
104
|
return specs
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-vars — 会话变量(ST setvar/getvar 运行时语义)。
|
|
3
|
+
*
|
|
4
|
+
* 变量挂在 session 对象上(SESSION_VARS_KEY 属性),因此 .engine 引擎实例与
|
|
5
|
+
* 插件进程工具实例(不同模块副本)操作同一 session 即共享同一份数据——
|
|
6
|
+
* 无跨实例状态同步问题。WeakMap 不需要:session 释放后属性随对象回收。
|
|
7
|
+
*
|
|
8
|
+
* 插值优先级:resolved 运行时 > 会话变量 > 配置 params > 配置 variables(含预设)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** session 对象上的变量属性键(字符串常量,跨模块实例一致)。 */
|
|
12
|
+
export const SESSION_VARS_KEY = '__pt_session_vars__'
|
|
13
|
+
|
|
14
|
+
function varsOf(session) {
|
|
15
|
+
if (session === null || typeof session !== 'object') return undefined
|
|
16
|
+
let vars = session[SESSION_VARS_KEY]
|
|
17
|
+
if (vars === undefined) {
|
|
18
|
+
vars = {}
|
|
19
|
+
session[SESSION_VARS_KEY] = vars
|
|
20
|
+
}
|
|
21
|
+
return vars
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 会话变量快照(无会话/未设置 → 空对象)。 */
|
|
25
|
+
export function sessionVarsSnapshot(session) {
|
|
26
|
+
const vars = varsOf(session)
|
|
27
|
+
return vars === undefined ? {} : { ...vars }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 读取单个会话变量(未设置 → undefined)。 */
|
|
31
|
+
export function getSessionVar(session, key) {
|
|
32
|
+
const vars = varsOf(session)
|
|
33
|
+
return vars === undefined ? undefined : vars[String(key)]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 设置会话变量(值转字符串;空值仍记录)。 */
|
|
37
|
+
export function setSessionVar(session, key, value) {
|
|
38
|
+
const vars = varsOf(session)
|
|
39
|
+
if (vars === undefined) return
|
|
40
|
+
vars[String(key)] = String(value ?? '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 清除会话变量;key 缺省时清空全部。 */
|
|
44
|
+
export function clearSessionVars(session, key) {
|
|
45
|
+
const vars = varsOf(session)
|
|
46
|
+
if (vars === undefined) return
|
|
47
|
+
if (key === undefined || key === '') {
|
|
48
|
+
for (const name of Object.keys(vars)) delete vars[name]
|
|
49
|
+
} else {
|
|
50
|
+
delete vars[String(key)]
|
|
51
|
+
}
|
|
52
|
+
}
|
package/engine/shared.mjs
CHANGED
|
@@ -116,22 +116,3 @@ export function keepDisposer(ctx, disposer, label) {
|
|
|
116
116
|
// 极简测试桩没有 effect:保持注册随进程,测试自行隔离。
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
/** 模板变量插值:配置 variables 优先,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}}。 */
|
|
121
|
-
export function interpolateVariables(text, variables, session) {
|
|
122
|
-
const builtins = {
|
|
123
|
-
DSH_HOME: process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : ''),
|
|
124
|
-
WORKSPACE: process.env.DSH_WORKSPACE ?? session?.header?.cwd ?? process.cwd(),
|
|
125
|
-
CWD: session?.header?.cwd ?? process.cwd(),
|
|
126
|
-
}
|
|
127
|
-
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) => {
|
|
128
|
-
if (Object.prototype.hasOwnProperty.call(variables, key)) return String(variables[key])
|
|
129
|
-
return Object.prototype.hasOwnProperty.call(builtins, key) ? builtins[key] : whole
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** 仅做配置级静态变量替换(无 session 上下文的层)。 */
|
|
134
|
-
export function interpolateStatic(text, variables) {
|
|
135
|
-
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) =>
|
|
136
|
-
Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key]) : whole)
|
|
137
|
-
}
|