dsh-plugin-prompt-tool 0.7.2 → 0.8.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.
Files changed (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. package/skills/web ui/SKILL.md +0 -42
package/engine/schema.mjs CHANGED
@@ -4,8 +4,12 @@
4
4
  */
5
5
 
6
6
  import { readFileSync, readdirSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { sep } from 'node:path'
7
9
  import { parse as parseYaml } from './vendor/yaml/index.js'
8
10
  import { bindResolver } from './strategies.mjs'
11
+ import { attachStRenderers } from './st-render.mjs'
12
+ import { MATCH_LOGIC, createAnchorMatcher } from './anchor-match.mjs'
9
13
 
10
14
  const name = 'prompt-config-engine'
11
15
 
@@ -20,11 +24,29 @@ function readTextFile(url) {
20
24
  return readFileSync(url, 'utf8')
21
25
  }
22
26
 
23
- function loadTemplate(file) {
27
+ function loadTemplate(file, baseUrl = import.meta.url, presetRootUrl) {
24
28
  if (typeof file !== 'string' || file.length === 0) return undefined
29
+ // 预设根基准:显式注入优先(引擎由插件包提供、不再位于 <预设根>/.engine/ 时必需),
30
+ // 缺省沿用「引擎文件的上一级目录」这一历史布局推导;加载与边界检查复用同一基准。
31
+ // URL 相对解析要求基准按目录语义以 `/` 结尾,调用方传裸预设根时在此补齐。
32
+ const rootUrl = presetRootUrl === undefined
33
+ ? new URL('../..', baseUrl)
34
+ : new URL(String(presetRootUrl).replace(/\/?$/, '/'))
35
+ const presetRoot = fileURLToPath(rootUrl).replace(/[\\\\/]$/, '')
36
+ // templateFile 只允许 canonical 预设目录(引擎父目录)内:防配置声明任意
37
+ // 本地路径把文件内容带进模型上下文。非 file: 协议(绝对盘符被解析为 scheme)同样拒绝。
38
+ let resolved
39
+ try {
40
+ resolved = fileURLToPath(new URL(file, baseUrl))
41
+ } catch {
42
+ throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} escapes preset root`)
43
+ }
44
+ if (resolved !== presetRoot && !resolved.startsWith(presetRoot + sep)) {
45
+ throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} escapes preset root`)
46
+ }
25
47
  let raw
26
48
  try {
27
- raw = readTextFile(new URL(file, import.meta.url))
49
+ raw = readTextFile(new URL(file, baseUrl))
28
50
  } catch {
29
51
  throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} is not readable`)
30
52
  }
@@ -59,6 +81,27 @@ export function parsePromptConfigYaml(raw) {
59
81
  return parsed
60
82
  }
61
83
 
84
+ /**
85
+ * 枚举提示词配置候选文件:**两条加载路径共用同一份枚举规则**。
86
+ *
87
+ * 扩展名、排序与跳过名单必须两侧一致——否则 host(编辑/列举)与引擎(注入)会看到不同的
88
+ * 文件集:host 让人编辑 A 文件、引擎却加载 B 文件。`variables.yml` 是模板变量源而非配置,
89
+ * 两侧都跳过(引擎另行读取它做合并,见 `loadPromptConfigFiles`)。
90
+ *
91
+ * 只共享**枚举**这一层:解析、结构校验、错误类型与包装都留在各自边界——两边的入参类型
92
+ * (URL vs 字符串路径)、空路径短路与异常消息本来就不同,强行合并会改变其中一侧的行为。
93
+ *
94
+ * @param entries `readdirSync(dir, { withFileTypes: true })` 的结果
95
+ * @returns 按名排序的文件名(含扩展名),已剔除 `variables.yml`
96
+ */
97
+ export function promptConfigFileNames(entries) {
98
+ return entries
99
+ .filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
100
+ .sort((a, b) => a.name.localeCompare(b.name))
101
+ .map((entry) => entry.name)
102
+ .filter((fileName) => fileName !== 'variables.yml')
103
+ }
104
+
62
105
  /**
63
106
  * 从提示词配置模块目录加载全部提示词配置描述:按文件名排序扫描 *.yml / *.yaml / *.json。
64
107
  * 文件名用数字前缀表达引擎执行顺序(00-…、10-…)。
@@ -81,14 +124,9 @@ export function loadPromptConfigFiles(dirUrl) {
81
124
  // 无 variables.yml(旧产物/手写目录)或解析失败:保持空变量源。
82
125
  }
83
126
  const specs = []
84
- const files = entries
85
- .filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
86
- .sort((a, b) => a.name.localeCompare(b.name))
87
- for (const entry of files) {
88
- if (entry.name === 'variables.yml') continue
89
- const url = new URL(entry.name, dirUrl)
90
- const raw = readFileSync(url, 'utf8')
91
- if (/\.json$/i.test(entry.name)) {
127
+ for (const fileName of promptConfigFileNames(entries)) {
128
+ const raw = readFileSync(new URL(fileName, dirUrl), 'utf8')
129
+ if (/\.json$/i.test(fileName)) {
92
130
  specs.push(JSON.parse(raw))
93
131
  } else {
94
132
  specs.push(parsePromptConfigYaml(raw))
@@ -104,9 +142,133 @@ export function loadPromptConfigFiles(dirUrl) {
104
142
  return specs
105
143
  }
106
144
 
107
- export const KNOWN_STRATEGIES = new Set(['static', 'placeholder', 'instruction-hint', 'first-turn-anchor', 'guide-auto', 'custom-fallback', 'world-book'])
145
+ export const KNOWN_STRATEGIES = new Set(['static', 'placeholder', 'first-turn-anchor', 'guide-auto', 'custom-fallback', 'world-book'])
146
+ /**
147
+ * 策略 × 层支持矩阵:`config.resolve` 只在 pre-step(executor)与 runtime-context 的
148
+ * placeholder(layers)被消费,其他层声明非 static 策略会绑定 resolver 却无人调用,
149
+ * 表现为「配了没效果、也不报错」。这里把该事实写成校验:不支持的组合挂载期 fail loud。
150
+ * `static: null` 表示全层可用;模板专属策略(strategyDir 懒加载)与内置策略同样受限。
151
+ */
152
+ export const STRATEGY_LAYER_SUPPORT = {
153
+ static: null,
154
+ placeholder: ['pre-step', 'runtime-context'],
155
+ 'first-turn-anchor': ['pre-step'],
156
+ 'guide-auto': ['pre-step'],
157
+ 'custom-fallback': ['pre-step'],
158
+ 'world-book': ['pre-step'],
159
+ }
160
+ /** 模板专属策略允许的层:resolver 同样只在 pre-step / runtime-context 被调用。 */
161
+ const TEMPLATE_STRATEGY_LAYERS = ['pre-step', 'runtime-context']
108
162
  export const KNOWN_SLOT_KINDS = new Set(['ordered', 'anchor'])
109
- export const KNOWN_LAYERS = new Set(['pre-step', 'system-section', 'runtime-context', 'agent-request', 'llm-stream', 'tool-pipeline'])
163
+ /**
164
+ * 九个官方注入层的**唯一权威定义**(B5 T1:原 `LAYER_ORDER` / `LAYER_FIELD_POLICIES` /
165
+ * `LAYER_EDITING` / `LAYER_LABELS` / `LAYER_DEFAULT_SUBJECT` 五处事实合一)。
166
+ *
167
+ * 新增或改一层只动这里一处:
168
+ * - 数组顺序即 `layerOrder`(**产品定义**,改顺序等于改 UI 组织,不要只为排序调整);
169
+ * - `fields` 是层能力矩阵(11 项,客户端表单据此动态渲染);
170
+ * - `editing` 是字段能力表(subjects / content / variables / messageMetadata / params);
171
+ * - `label` 是由引擎统一下发给客户端的显示名与说明;
172
+ * - `defaultSubject` 是该条件层的缺省匹配对象(不声明 = 该层没有匹配对象)。
173
+ *
174
+ * 下面所有导出都由本表**派生**,并保持既有名字与形状,外部消费方不受影响。
175
+ * `defaultSubject` 必须落在本层 `editing.subjects` 内——就近声明 + 加载期断言,
176
+ * 避免「缺省值不在允许集合里」这种只能靠运行期才发现的错配。
177
+ */
178
+ const LAYER_DEFINITIONS = [
179
+ {
180
+ layer: 'pre-step',
181
+ fields: { position: true, dedupe: true, promotion: true, audience: true, modelScope: true, merge: true, order: true, role: true, placeholder: true, subject: true, match: true },
182
+ editing: { subjects: ['userMessage'], content: 'text', variables: true, messageMetadata: true, params: {} },
183
+ label: { title: '消息批层', detail: '官方默认层:agent/pre-step 消息批。支持 position / dedupe / promotion / audience / mergeMode 与文本插值。' },
184
+ defaultSubject: 'userMessage',
185
+ },
186
+ {
187
+ layer: 'system-section',
188
+ fields: { position: false, dedupe: false, promotion: false, audience: true, modelScope: false, merge: true, order: true, role: false, placeholder: false, subject: false, match: false },
189
+ editing: { subjects: [], content: 'text', variables: true, messageMetadata: false,
190
+ params: { sectionName: { type: 'string' }, complete: { type: 'boolean' }, suppressRuntimeContext: { type: 'boolean' } } },
191
+ label: { title: '系统段层', detail: 'system-section 静态层:注册即全局,由 order 与 params.complete / sectionName 控制。' },
192
+ },
193
+ {
194
+ layer: 'runtime-context',
195
+ fields: { position: false, dedupe: false, promotion: false, audience: false, modelScope: false, merge: true, order: true, role: false, placeholder: true, subject: false, match: false },
196
+ editing: { subjects: [], content: 'text', variables: true, messageMetadata: false, params: { contextName: { type: 'string' } } },
197
+ label: { title: '运行上下文', detail: 'runtime-context 层:static 按 order 注册,placeholder 单条生效,由 params.contextName 控制。' },
198
+ },
199
+ {
200
+ layer: 'agent-request',
201
+ fields: { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: false, match: false },
202
+ editing: { subjects: [], content: 'request', variables: false, messageMetadata: false,
203
+ params: { patch: { type: 'object' }, replace: { type: 'boolean' } } },
204
+ label: { title: '调用配置层', detail: 'agent-request 层:按 order 注册,params.patch 改写请求配置。' },
205
+ },
206
+ {
207
+ layer: 'llm-stream',
208
+ fields: { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: false, match: false },
209
+ editing: { subjects: [], content: 'stream', variables: false, messageMetadata: false,
210
+ params: { mode: { type: 'enum', values: ['pass', 'replace'] } } },
211
+ label: { title: '模型流层', detail: 'llm/stream 层:按 order 注册,params.mode = pass | replace。' },
212
+ },
213
+ {
214
+ layer: 'tool-pipeline',
215
+ fields: { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: true, match: true },
216
+ editing: { subjects: ['toolArgs', 'toolResult'], content: 'tool-result', variables: false, messageMetadata: false,
217
+ params: { toolNames: { type: 'string' }, preDecision: { type: 'enum', values: ['allow', 'deny', 'ask'] },
218
+ denyReason: { type: 'string' }, postAction: { type: 'enum', values: ['accept', 'replace', 'block'] } } },
219
+ label: { title: '工具管线层', detail: 'tools/* 层:按 order 注册,params.toolNames 与 preDecision / postAction 控制;subject / match 可选,命中才裁决。' },
220
+ defaultSubject: 'toolArgs',
221
+ },
222
+ {
223
+ layer: 'turn-stop',
224
+ fields: { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: true, match: true },
225
+ editing: { subjects: ['assistantText'], content: 'text', variables: true, messageMetadata: false, params: {} },
226
+ label: { title: '轮次停止层', detail: 'agent/turn-stopping 层:命中条件时强制续跑一步;引擎内置续跑上限,不可用配置关闭。' },
227
+ defaultSubject: 'assistantText',
228
+ },
229
+ {
230
+ layer: 'subagent-start',
231
+ fields: { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: true, match: true },
232
+ editing: { subjects: ['subagentInfo'], content: 'text', variables: true, messageMetadata: false, params: {} },
233
+ label: { title: '子代理启动层', detail: 'subagent/start 层:命中条件时向该子代理注入上下文。' },
234
+ defaultSubject: 'subagentInfo',
235
+ },
236
+ {
237
+ layer: 'subagent-end',
238
+ fields: { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false, subject: true, match: true },
239
+ editing: { subjects: ['subagentInfo'], content: 'subagent-result', variables: true, messageMetadata: false,
240
+ params: { action: { type: 'enum', values: ['observe', 'inject-main'] } } },
241
+ label: { title: '子代理结束层', detail: 'subagent/end 层:默认记录;action=inject-main 时向所属主会话注入配置文本,不改写子代理返回结果。' },
242
+ defaultSubject: 'subagentInfo',
243
+ },
244
+ ]
245
+
246
+ /** 就近断言:缺省匹配对象必须在本层允许的 subjects 内(新增层时最先撞到这里)。 */
247
+ for (const definition of LAYER_DEFINITIONS) {
248
+ if (definition.defaultSubject === undefined) continue
249
+ if (!definition.editing.subjects.includes(definition.defaultSubject)) {
250
+ throw new TypeError(
251
+ `schema: layer ${definition.layer} defaultSubject ${definition.defaultSubject} must be one of its subjects`,
252
+ )
253
+ }
254
+ }
255
+
256
+ /** 九个官方注入层的固定顺序:/meta 的 layerOrder、UI 层序与模板菜单共用这一份。 */
257
+ export const LAYER_ORDER = LAYER_DEFINITIONS.map((definition) => definition.layer)
258
+ export const KNOWN_LAYERS = new Set(LAYER_ORDER)
259
+ /**
260
+ * 条件判定的匹配对象:决定把哪段文本交给 anchor-match 匹配器。
261
+ * 取值与 DSH 扩展点一一对应,各层缺省值见 LAYER_DEFAULT_SUBJECT。
262
+ */
263
+ export const KNOWN_SUBJECTS = new Set(['toolArgs', 'toolResult', 'userMessage', 'assistantText', 'subagentInfo'])
264
+ /** 各条件层的缺省匹配对象;不在此表的层没有匹配对象。 */
265
+ export const LAYER_DEFAULT_SUBJECT = Object.fromEntries(
266
+ LAYER_DEFINITIONS
267
+ .filter((definition) => definition.defaultSubject !== undefined)
268
+ .map((definition) => [definition.layer, definition.defaultSubject]),
269
+ )
270
+ /** 支持 subject / match 的层;其余层声明这两个字段即 fail loud(避免误以为是门)。 */
271
+ export const CONDITIONAL_LAYERS = new Set(Object.keys(LAYER_DEFAULT_SUBJECT))
110
272
  export const KNOWN_POSITIONS = new Set(['after-user', 'before-all', 'after-all'])
111
273
  export const KNOWN_DEDUPES = new Set(['session', 'batch', 'none'])
112
274
  export const KNOWN_PROMOTIONS = new Set(['none', 'main', 'include-subagents'])
@@ -114,32 +276,38 @@ export const KNOWN_PROMOTIONS = new Set(['none', 'main', 'include-subagents'])
114
276
  export const KNOWN_AUDIENCES = new Set(['main', 'subagent'])
115
277
  export const KNOWN_MERGE_MODES = new Set(['separate', 'merged'])
116
278
  export const KNOWN_MODEL_SCOPES = new Set(['all', 'pro', 'flash'])
117
- export const KNOWN_ROLES = new Set(['user', 'assistant'])
279
+ /**
280
+ * pre-step 批次写成宿主 `user/message` 事件,配置只接受 user。
281
+ * 自定义策略或模板 patch 的角色仍由 executor 在出口校验,避免写出非法事件。
282
+ */
283
+ export const KNOWN_ROLES = new Set(['user'])
118
284
  export const KNOWN_FILLS = new Set(['instruction-hint', 'env-facts', 'skill-catalog'])
119
285
 
120
286
  /** 层能力矩阵:每个字段只在对应注入层生效。客户端表单据此动态渲染。 */
121
- export const LAYER_FIELD_POLICIES = {
122
- 'pre-step': { position: true, dedupe: true, promotion: true, audience: true, modelScope: true, merge: true, order: true, role: true, placeholder: true },
123
- 'system-section': { position: false, dedupe: false, promotion: false, audience: true, modelScope: false, merge: true, order: true, role: false, placeholder: false },
124
- 'runtime-context': { position: false, dedupe: false, promotion: false, audience: false, modelScope: false, merge: true, order: true, role: false, placeholder: true },
125
- 'agent-request': { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false },
126
- 'llm-stream': { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false },
127
- 'tool-pipeline': { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false },
128
- }
287
+ export const LAYER_FIELD_POLICIES = Object.fromEntries(
288
+ LAYER_DEFINITIONS.map((definition) => [definition.layer, definition.fields]),
289
+ )
290
+
291
+ /** 局部参数只登记真实消费字段;未知扩展键仍保留,策略参数由各策略消费。 */
292
+ const LAYER_EDITING = Object.fromEntries(
293
+ LAYER_DEFINITIONS.map((definition) => [definition.layer, definition.editing]),
294
+ )
295
+
296
+ export const LAYER_CONTRACTS = Object.fromEntries(LAYER_ORDER.map(layer => [layer, {
297
+ strategies: [...KNOWN_STRATEGIES].filter(strategy => STRATEGY_LAYER_SUPPORT[strategy] === null || STRATEGY_LAYER_SUPPORT[strategy].includes(layer)),
298
+ ...LAYER_EDITING[layer],
299
+ }]))
129
300
 
130
301
  /** 层显示名与说明:由引擎统一下发,客户端不再各自维护。 */
131
- export const LAYER_LABELS = {
132
- 'pre-step': { title: '消息批层', detail: '官方默认层:agent/pre-step 消息批。支持 position / dedupe / promotion / audience / mergeMode 与文本插值。' },
133
- 'system-section': { title: '系统段层', detail: 'system-section 静态层:注册即全局,由 order 与 params.complete / sectionName 控制。' },
134
- 'runtime-context': { title: '运行上下文', detail: 'runtime-context 层:static 按 order 注册,placeholder 单条生效,由 params.contextName 控制。' },
135
- 'agent-request': { title: '调用配置层', detail: 'agent-request 层:按 order 注册,params.patch 改写请求配置。' },
136
- 'llm-stream': { title: '模型流层', detail: 'llm/stream 层:按 order 注册,params.mode = pass | replace。' },
137
- 'tool-pipeline': { title: '工具管线层', detail: 'tools/* 层:按 order 注册,params.toolNames 与 preDecision / postAction 控制。' },
138
- }
302
+ export const LAYER_LABELS = Object.fromEntries(
303
+ LAYER_DEFINITIONS.map((definition) => [definition.layer, definition.label]),
304
+ )
139
305
 
140
306
  /** 引擎能力矩阵:作为 /meta 的唯一数据源,客户端表单据此动态渲染。 */
141
307
  export function getEngineMeta() {
142
308
  return {
309
+ // layerOrder = 固定九层顺序(UI 组织用);layers = 合法层集合(校验与旧消费方用)。
310
+ layerOrder: [...LAYER_ORDER],
143
311
  layers: [...KNOWN_LAYERS].sort(),
144
312
  strategies: [...KNOWN_STRATEGIES].sort(),
145
313
  slotKinds: [...KNOWN_SLOT_KINDS].sort(),
@@ -151,15 +319,223 @@ export function getEngineMeta() {
151
319
  roles: [...KNOWN_ROLES].sort(),
152
320
  mergeModes: [...KNOWN_MERGE_MODES].sort(),
153
321
  fills: [...KNOWN_FILLS].sort(),
322
+ subjects: [...KNOWN_SUBJECTS].sort(),
323
+ layerDefaultSubjects: { ...LAYER_DEFAULT_SUBJECT },
154
324
  layerFieldPolicies: LAYER_FIELD_POLICIES,
155
325
  layerLabels: LAYER_LABELS,
326
+ layerContracts: structuredClone(LAYER_CONTRACTS),
327
+ }
328
+ }
329
+
330
+ /**
331
+ * 归一化并预编译条件判定的 match 段:键集合直接交给 anchor-match 的匹配器,
332
+ * 非法 logic、非法正则与空键集合都在挂载期 fail loud——运行时静默不命中会让
333
+ * "配了门却没拦住"变成不可见的失效。
334
+ * @returns 归一化后的匹配参数;未声明 match 时返回 undefined(= 无条件)。
335
+ */
336
+ function normalizeMatch(raw, label) {
337
+ if (raw === undefined || raw === null) return undefined
338
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
339
+ throw new TypeError(`${label}.match must be an object when present`)
156
340
  }
341
+ const stringList = (value, field) => {
342
+ if (value === undefined || value === null) return []
343
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
344
+ throw new TypeError(`${label}.match.${field} must be an array of strings when present`)
345
+ }
346
+ return value.map((item) => item.trim()).filter((item) => item.length > 0)
347
+ }
348
+ const keys = stringList(raw.keys, 'keys')
349
+ const secondaryKeys = stringList(raw.secondaryKeys, 'secondaryKeys')
350
+ if (keys.length === 0 && secondaryKeys.length === 0) {
351
+ throw new TypeError(`${label}.match needs at least one non-empty key`)
352
+ }
353
+ const logic = raw.logic ?? MATCH_LOGIC.ANY
354
+ if (!Object.values(MATCH_LOGIC).includes(logic)) {
355
+ throw new TypeError(`${label}.match.logic must be one of ${Object.values(MATCH_LOGIC).join(', ')}`)
356
+ }
357
+ for (const field of ['caseSensitive', 'wholeWords', 'useRegex']) {
358
+ if (raw[field] !== undefined && typeof raw[field] !== 'boolean') {
359
+ throw new TypeError(`${label}.match.${field} must be a boolean when present`)
360
+ }
361
+ }
362
+ const match = {
363
+ keys,
364
+ secondaryKeys,
365
+ logic,
366
+ caseSensitive: raw.caseSensitive === true,
367
+ wholeWords: raw.wholeWords === true,
368
+ useRegex: raw.useRegex,
369
+ }
370
+ // 预编译:useRegex、/pattern/flags 形态与整词包装里的正则错误在此暴露。
371
+ try {
372
+ createAnchorMatcher(match)
373
+ } catch (error) {
374
+ throw new TypeError(`${label}.match is not compilable: ${error instanceof Error ? error.message : String(error)}`)
375
+ }
376
+ return match
377
+ }
378
+
379
+ function normalizeLayerParams(raw, layer, label) {
380
+ if (raw === undefined || raw === null) return {}
381
+ if (typeof raw !== 'object' || Array.isArray(raw)) throw new TypeError(`${label}.params must be an object`)
382
+ let params = raw
383
+ if (layer === 'tool-pipeline' && Array.isArray(raw.toolNames)) {
384
+ if (raw.toolNames.some(item => typeof item !== 'string')) throw new TypeError(`${label}.params.toolNames must contain only strings`)
385
+ params = { ...raw, toolNames: raw.toolNames.join(',') }
386
+ }
387
+ for (const [key, rule] of Object.entries(LAYER_CONTRACTS[layer].params)) {
388
+ const value = params[key]
389
+ if (value === undefined) continue
390
+ const valid = rule.type === 'enum' ? rule.values.includes(value)
391
+ : rule.type === 'object' ? value !== null && typeof value === 'object' && !Array.isArray(value)
392
+ : typeof value === rule.type
393
+ if (!valid) throw new TypeError(`${label}.params.${key} must be ${rule.type === 'enum' ? rule.values.join(' | ') : rule.type}`)
394
+ }
395
+ if (layer === 'agent-request') {
396
+ const patch = params.patch ?? {}
397
+ // 官方 0.1.6 LlmCallConfig;不能把消息、工具或 system 塞进请求配置。
398
+ const keys = new Set(['provider', 'model', 'reasoningEffort', 'temperature', 'maxTokens', 'stop'])
399
+ for (const [key, value] of Object.entries(patch)) {
400
+ if (!keys.has(key)) throw new TypeError(`${label}.params.patch.${key} is not a LlmCallConfig field`)
401
+ const valid = key === 'temperature' ? typeof value === 'number' && Number.isFinite(value)
402
+ : key === 'maxTokens' ? Number.isSafeInteger(value) && value > 0
403
+ : key === 'stop' ? Array.isArray(value) && value.every(item => typeof item === 'string')
404
+ : typeof value === 'string' && value.trim().length > 0
405
+ if (!valid) throw new TypeError(`${label}.params.patch.${key} has an invalid value`)
406
+ }
407
+ if (params.replace === true && (!patch.provider || !patch.model)) {
408
+ throw new TypeError(`${label}.params.patch requires provider and model when replace=true`)
409
+ }
410
+ }
411
+ return params
412
+ }
413
+
414
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value)
415
+ const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0
416
+
417
+ /**
418
+ * promptConfig 标量字段的校验与归一声明(B5 T2)。
419
+ *
420
+ * 原来 `createPromptConfigs` 里同一个模式手写了 15 遍(`const X = spec.X ?? D` 紧跟
421
+ * `if (!KNOWN_X.has(X)) throw …`)。这里把「枚举 / 缺省 / 形态」收进一份表,遍历一次完成。
422
+ *
423
+ * 三个旋钮覆盖现状里**逐字段不同**的三种 null 处置,必须逐条保留:
424
+ * - `known` + `default`:`null`/`undefined` 取缺省,其余必须是枚举成员(原 `spec.X ?? D`);
425
+ * - `keepNull`:显式 `null` 合法且**原样保留**(audience 专用:null = 公用,与缺省的
426
+ * undefined 不是同一档);
427
+ * - `optional`:缺失即不设置;一旦存在(**含 null**)就按 `validate` 校验形态。
428
+ *
429
+ * 表里需要额外上下文的项用 `resolve`(subject 有「全局集合 + 层内允许集」两条语义与层缺省
430
+ * 回退)。**表顺序 = 报错顺序**:与手写版逐个检查的先后完全一致,同一组非法输入必须先报同一处。
431
+ *
432
+ * 不在此表(各自领域的权威,留在原处):`params`(`normalizeLayerParams`)、`match`
433
+ * (`normalizeMatch`)、`text`/`texts`(多来源合并)、`strategy`/`configKind`/`layer`
434
+ * (各自带额外上下文校验)。
435
+ */
436
+ export const CONFIG_FIELDS = [
437
+ { field: 'position', known: KNOWN_POSITIONS, default: 'after-user' },
438
+ { field: 'dedupe', known: KNOWN_DEDUPES, default: 'none' },
439
+ { field: 'promotion', known: KNOWN_PROMOTIONS, default: 'none' },
440
+ { field: 'audience', known: KNOWN_AUDIENCES, keepNull: true, keepMissing: true },
441
+ { field: 'modelScope', known: KNOWN_MODEL_SCOPES, default: 'all' },
442
+ { field: 'role', known: KNOWN_ROLES, default: 'user' },
443
+ {
444
+ field: 'subject',
445
+ resolve: (spec, { label, layer }) => {
446
+ // 条件判定:subject 决定把哪段文本交给匹配器(缺省由层决定),match 缺省 = 无条件。
447
+ if (spec.subject !== undefined && !KNOWN_SUBJECTS.has(spec.subject)) {
448
+ throw new TypeError(`${name}: ${label} unknown subject ${JSON.stringify(spec.subject)} — known subjects: ${[...KNOWN_SUBJECTS].sort().join(', ')}`)
449
+ }
450
+ if (spec.subject !== undefined && !LAYER_CONTRACTS[layer].subjects.includes(spec.subject)) {
451
+ throw new TypeError(`${name}: ${label}.subject ${JSON.stringify(spec.subject)} is unavailable on layer ${JSON.stringify(layer)}`)
452
+ }
453
+ return spec.subject ?? LAYER_DEFAULT_SUBJECT[layer]
454
+ },
455
+ },
456
+ {
457
+ field: 'identity',
458
+ resolve: (spec, { label }) => {
459
+ // identity 仅支持 plugin 命名空间(kind 模式与 sourceKind 重复,已归一)。
460
+ const identity = spec.identity ?? { field: 'plugin', value: spec.id }
461
+ if (identity === null || typeof identity !== 'object' || Array.isArray(identity)
462
+ || identity.field !== 'plugin' || typeof identity.value !== 'string' || identity.value.length === 0) {
463
+ throw new TypeError(`${name}: ${label}.identity must be { field: 'plugin', value: string }`)
464
+ }
465
+ return identity
466
+ },
467
+ },
468
+ { field: 'order', default: 0, validate: isFiniteNumber, problem: 'must be a finite number' },
469
+ { field: 'group', optional: true, validate: isNonEmptyString, problem: 'must be a non-empty string when present' },
470
+ { field: 'exclusive', optional: true, validate: (value) => typeof value === 'boolean', problem: 'must be a boolean when present' },
471
+ { field: 'name', optional: true, validate: isNonEmptyString, problem: 'must be a non-empty string when present' },
472
+ {
473
+ field: 'variables',
474
+ resolve: (spec, { label }) => {
475
+ if (spec.variables !== undefined && (spec.variables === null || typeof spec.variables !== 'object' || Array.isArray(spec.variables))) {
476
+ throw new TypeError(`${name}: ${label}.variables must be an object when present`)
477
+ }
478
+ return spec.variables
479
+ },
480
+ },
481
+ {
482
+ field: 'texts',
483
+ resolve: (spec, { label }) => {
484
+ if (spec.texts !== undefined && (!Array.isArray(spec.texts) || spec.texts.some((item) => typeof item !== 'string'))) {
485
+ throw new TypeError(`${name}: ${label}.texts must be an array of strings when present`)
486
+ }
487
+ return spec.texts
488
+ },
489
+ },
490
+ { field: 'mergeMode', known: KNOWN_MERGE_MODES, default: 'separate' },
491
+ ]
492
+
493
+ /** 按 {@link CONFIG_FIELDS} 遍历一次,产出全部标量字段的归一值(或按表顺序 fail loud)。 */
494
+ function resolveConfigFields(spec, label, layer) {
495
+ const resolved = {}
496
+ for (const rule of CONFIG_FIELDS) {
497
+ if (typeof rule.resolve === 'function') {
498
+ resolved[rule.field] = rule.resolve(spec, { label, layer })
499
+ continue
500
+ }
501
+ const raw = spec[rule.field]
502
+ if (raw === undefined && (rule.optional === true || rule.keepMissing === true)) {
503
+ if (rule.keepMissing === true) resolved[rule.field] = undefined
504
+ continue
505
+ }
506
+ if (raw === null && rule.keepNull === true) {
507
+ resolved[rule.field] = null
508
+ continue
509
+ }
510
+ if (raw == null && rule.default !== undefined) {
511
+ resolved[rule.field] = rule.default
512
+ continue
513
+ }
514
+ if (rule.known !== undefined && !rule.known.has(raw)) {
515
+ throw new TypeError(`${name}: ${label} unknown ${rule.field} ${JSON.stringify(raw)}`)
516
+ }
517
+ if (rule.validate !== undefined && rule.validate(raw) !== true) {
518
+ throw new TypeError(`${name}: ${label}.${rule.field} ${rule.problem}`)
519
+ }
520
+ resolved[rule.field] = raw
521
+ }
522
+ return resolved
157
523
  }
158
524
 
159
525
  /** 从 YAML 提示词配置描述构造运行时提示词配置。配置错误必须在挂载时暴露(fail loud)。 */
160
526
  export function createPromptConfigs(specs, options = {}) {
161
527
  if (specs === undefined) return []
162
528
  if (!Array.isArray(specs)) throw new TypeError(`${name}: config.configs must be an array`)
529
+ // 重复 ID 拒绝:后者覆盖前者会静默丢卡,挂载前 fail loud。
530
+ const seenIds = new Set()
531
+ for (const spec of specs) {
532
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) continue
533
+ const id = spec.id
534
+ if (typeof id === 'string' && id.length > 0 && seenIds.has(id)) {
535
+ throw new TypeError(`${name}: duplicate prompt config id ${JSON.stringify(id)}`)
536
+ }
537
+ if (typeof id === 'string' && id.length > 0) seenIds.add(id)
538
+ }
163
539
  const configs = specs.map((spec, index) => {
164
540
  const label = `configs[${index}]`
165
541
  if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
@@ -185,69 +561,24 @@ export function createPromptConfigs(specs, options = {}) {
185
561
  if (!KNOWN_LAYERS.has(layer)) {
186
562
  throw new TypeError(`${name}: ${label} unknown layer ${JSON.stringify(layer)} — known layers: ${[...KNOWN_LAYERS].sort().join(', ')}`)
187
563
  }
564
+ // 策略只在消费它的层生效:不支持的组合必须报错,不能静默变成"配了没效果"。
565
+ const strategyLayers = KNOWN_STRATEGIES.has(strategy) ? STRATEGY_LAYER_SUPPORT[strategy] : TEMPLATE_STRATEGY_LAYERS
566
+ if (strategyLayers !== null && !strategyLayers.includes(layer)) {
567
+ throw new TypeError(`${name}: ${label} strategy ${JSON.stringify(strategy)} only takes effect on layer(s) ${strategyLayers.join(', ')} — got layer ${JSON.stringify(layer)}`)
568
+ }
188
569
  // 层能力矩阵同时是引擎校验源:矩阵标记 false 的字段在对应层不生效,
189
570
  // 显式提供时 fail loud(UI 表单已按矩阵隐藏,此处兜底手写配置)。
190
- const restricted = ['position', 'dedupe', 'promotion', 'audience', 'modelScope', 'merge', 'role']
571
+ const restricted = ['position', 'dedupe', 'promotion', 'audience', 'modelScope', 'merge', 'role', 'subject', 'match']
191
572
  .filter((field) => LAYER_FIELD_POLICIES[layer][field] === false && spec[field === 'merge' ? 'mergeMode' : field] != null)
192
573
  if (restricted.length > 0) {
193
574
  throw new TypeError(`${name}: ${label} layer ${JSON.stringify(layer)} does not support field(s): ${restricted.join(', ')}`)
194
575
  }
195
- const position = spec.position ?? 'after-user'
196
- if (!KNOWN_POSITIONS.has(position)) {
197
- throw new TypeError(`${name}: ${label} unknown position ${JSON.stringify(position)}`)
198
- }
199
- const dedupe = spec.dedupe ?? 'none'
200
- if (!KNOWN_DEDUPES.has(dedupe)) {
201
- throw new TypeError(`${name}: ${label} unknown dedupe ${JSON.stringify(dedupe)}`)
202
- }
203
- const promotion = spec.promotion ?? 'none'
204
- if (!KNOWN_PROMOTIONS.has(promotion)) {
205
- throw new TypeError(`${name}: ${label} unknown promotion ${JSON.stringify(promotion)}`)
206
- }
207
- const audience = spec.audience
208
- if (audience !== undefined && audience !== null && !KNOWN_AUDIENCES.has(audience)) {
209
- throw new TypeError(`${name}: ${label} unknown audience ${JSON.stringify(audience)}`)
210
- }
211
- const modelScope = spec.modelScope ?? 'all'
212
- if (!KNOWN_MODEL_SCOPES.has(modelScope)) {
213
- throw new TypeError(`${name}: ${label} unknown modelScope ${JSON.stringify(modelScope)}`)
214
- }
215
- const role = spec.role ?? 'user'
216
- if (!KNOWN_ROLES.has(role)) {
217
- throw new TypeError(`${name}: ${label} unknown role ${JSON.stringify(role)}`)
218
- }
219
- // identity 仅支持 plugin 命名空间(kind 模式与 sourceKind 重复,已归一)。
220
- const identity = spec.identity ?? { field: 'plugin', value: spec.id }
221
- if (identity === null || typeof identity !== 'object' || Array.isArray(identity)
222
- || identity.field !== 'plugin' || typeof identity.value !== 'string' || identity.value.length === 0) {
223
- throw new TypeError(`${name}: ${label}.identity must be { field: 'plugin', value: string }`)
224
- }
225
- const order = spec.order ?? 0
226
- if (typeof order !== 'number' || !Number.isFinite(order)) {
227
- throw new TypeError(`${name}: ${label}.order must be a finite number`)
228
- }
229
- if (spec.group !== undefined && (typeof spec.group !== 'string' || spec.group.length === 0)) {
230
- throw new TypeError(`${name}: ${label}.group must be a non-empty string when present`)
231
- }
232
- if (spec.exclusive !== undefined && typeof spec.exclusive !== 'boolean') {
233
- throw new TypeError(`${name}: ${label}.exclusive must be a boolean when present`)
234
- }
235
- if (spec.name !== undefined && (typeof spec.name !== 'string' || spec.name.length === 0)) {
236
- throw new TypeError(`${name}: ${label}.name must be a non-empty string when present`)
237
- }
238
- if (spec.variables !== undefined && (spec.variables === null || typeof spec.variables !== 'object' || Array.isArray(spec.variables))) {
239
- throw new TypeError(`${name}: ${label}.variables must be an object when present`)
240
- }
241
- if (spec.texts !== undefined && (!Array.isArray(spec.texts) || spec.texts.some((item) => typeof item !== 'string'))) {
242
- throw new TypeError(`${name}: ${label}.texts must be an array of strings when present`)
243
- }
244
- const mergeMode = spec.mergeMode ?? 'separate'
245
- if (!KNOWN_MERGE_MODES.has(mergeMode)) {
246
- throw new TypeError(`${name}: ${label} unknown mergeMode ${JSON.stringify(mergeMode)}`)
247
- }
248
- if (strategy === 'placeholder' && layer !== 'pre-step' && layer !== 'runtime-context') {
249
- throw new TypeError(`${name}: ${label} strategy=placeholder supports layer pre-step or runtime-context only, got ${JSON.stringify(layer)}`)
250
- }
576
+ // 标量字段统一校验与归一(B5 T2:原 15 段同型校验收敛为一次遍历,表顺序即报错顺序)。
577
+ // 传**裸 label**:前缀 `${name}: ` 由 resolveConfigFields 与各 resolve 自行补齐。
578
+ const fields = resolveConfigFields(spec, label, layer)
579
+ // subject 已由 fields 归一(含「全局集合 + 层内允许集」两条语义与层缺省回退)。
580
+ const match = normalizeMatch(spec.match, `${name}: ${label}`)
581
+ // placeholder 的层限制由上面的 STRATEGY_LAYER_SUPPORT 统一校验(此处不再重复)。
251
582
  let fill
252
583
  if (strategy === 'placeholder') {
253
584
  fill = typeof spec.fill === 'string' && spec.fill.length > 0 ? spec.fill : undefined
@@ -255,31 +586,38 @@ export function createPromptConfigs(specs, options = {}) {
255
586
  throw new TypeError(`${name}: ${label} strategy=placeholder requires fill in [${[...KNOWN_FILLS].sort().join(', ')}]`)
256
587
  }
257
588
  }
258
- const template = loadTemplate(spec.templateFile)
589
+ // 安装预检可把文件读取定向到尚未提交的候选目录,默认运行期仍走原解析器。
590
+ const template = (options.loadTemplate ?? loadTemplate)(spec.templateFile, options.templateBaseUrl, options.templatePresetRoot)
259
591
  const templatePatch = template !== null && typeof template === 'object'
260
592
  ? { id: template.id, role: template.role, content: template.content, source: template.source }
261
593
  : undefined
594
+ // tool-pipeline 的 toolNames 是逗号分隔字符串(parseToolNames 只认字符串):
595
+ // 数组写法会被静默解析为空 = 匹配所有工具,把一条定向门扩大成全工具门,
596
+ // 与条件判定叠加后危害更大,这里归一化而不是留给运行时。
597
+ const params = normalizeLayerParams(spec.params, layer, `${name}: ${label}`)
262
598
  const config = {
263
599
  id: spec.id,
264
- name: typeof spec.name === 'string' ? spec.name : spec.id,
600
+ name: fields.name ?? spec.id,
265
601
  enabled: spec.enabled !== false,
266
602
  strategy,
267
603
  configKind,
268
604
  layer,
269
- group: typeof spec.group === 'string' ? spec.group : undefined,
270
- exclusive: spec.exclusive === true,
271
- order,
272
- role,
605
+ group: fields.group,
606
+ exclusive: fields.exclusive === true,
607
+ order: fields.order,
608
+ role: fields.role,
273
609
  fill,
274
- position,
275
- dedupe,
276
- promotion,
277
- audience,
278
- modelScope,
610
+ position: fields.position,
611
+ dedupe: fields.dedupe,
612
+ promotion: fields.promotion,
613
+ audience: fields.audience,
614
+ modelScope: fields.modelScope,
615
+ subject: fields.subject,
616
+ match,
279
617
  sourceKind: typeof spec.sourceKind === 'string' && spec.sourceKind.length > 0 ? spec.sourceKind : spec.id,
280
618
  form: typeof spec.form === 'string' ? spec.form : 'notice',
281
619
  summary: typeof spec.summary === 'string' ? spec.summary : '',
282
- identity,
620
+ identity: fields.identity,
283
621
  // text/texts 统一:text 为单块便捷写法,运行时与渲染只消费 texts。
284
622
  texts: (() => {
285
623
  const specText = typeof spec.text === 'string' && spec.text.length > 0 ? spec.text : undefined
@@ -289,17 +627,20 @@ export function createPromptConfigs(specs, options = {}) {
289
627
  ? [...(specText !== undefined ? [specText] : []), ...specTexts]
290
628
  : (templateText.length > 0 ? [templateText] : [])
291
629
  })(),
292
- mergeMode,
293
- variables: spec.variables !== null && typeof spec.variables === 'object' && !Array.isArray(spec.variables) ? spec.variables : {},
630
+ mergeMode: fields.mergeMode,
631
+ variables: fields.variables ?? {},
294
632
  templatePatch,
295
- params: spec.params !== null && typeof spec.params === 'object' && !Array.isArray(spec.params) ? spec.params : {},
633
+ params,
296
634
  }
635
+ // 条件判定的匹配器在挂载期编译一次,执行侧(condition.mjs)直接复用:
636
+ // normalizeMatch 已用同一份参数试编译过,这里是同源的第二句(失败会抛出)。
637
+ config.matchScan = match === undefined ? undefined : createAnchorMatcher(match).scan
297
638
  config.resolve = bindResolver(config, options.strategyDir)
298
639
  return config
299
640
  })
300
641
  // 排序契约:anchor 提示词配置保持模块文件相对顺序(固定锚点),ordered 提示词配置按 order
301
642
  // 稳定升序排在其后。默认 order=0 时等价于文件顺序。
302
- return configs
643
+ return attachStRenderers(configs
303
644
  .map((config, fileOrder) => ({ config, fileOrder }))
304
645
  .sort((a, b) => {
305
646
  const aAnchor = a.config.configKind === 'anchor' ? 0 : 1
@@ -308,5 +649,5 @@ export function createPromptConfigs(specs, options = {}) {
308
649
  if (aAnchor === 1 && a.config.order !== b.config.order) return a.config.order - b.config.order
309
650
  return a.fileOrder - b.fileOrder
310
651
  })
311
- .map(({ config }) => config)
652
+ .map(({ config }) => config))
312
653
  }