dsh-plugin-prompt-tool 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * custom-bash — a Windows-capable `bash` tool that registers under the SAME
2
+ * tool-git-bash — a Windows-capable `bash` tool that registers under the SAME
3
3
  * name (`bash`) as the official persistent bash, with a Minimal-compatible
4
4
  * description, but executes through `ctx.subprocess.spawn` instead of a PTY.
5
5
  *
@@ -43,19 +43,41 @@
43
43
 
44
44
  import { access } from 'node:fs/promises'
45
45
  import { dirname, join } from 'node:path'
46
- import { validateConfig } from './shared.mjs'
46
+ import { bool, defineConfig, int } from './fields.mjs'
47
47
 
48
48
  /** Cordis plugin name used by loader diagnostics. */
49
- export const name = 'custom-bash'
49
+ export const name = 'tool-git-bash'
50
50
 
51
51
  /** The subprocess and tools services must exist before this tool can register. */
52
52
  export const inject = ['subprocess', 'tools']
53
53
 
54
- const DEFAULT_TIMEOUT_MS = 120000
55
- const DEFAULT_MAX_OUTPUT_BYTES = 64000
54
+ /**
55
+ * bashPath:显式非空路径无条件生效;缺键 / 空串 / 非字符串一律视为「未设置」走推断链。
56
+ *
57
+ * 迁移前是 `typeof source.bashPath === 'string' && source.bashPath.length > 0 ? … : undefined`,
58
+ * 非字符串是**静默忽略**而不是报错;B2 硬约束要求逐字段结果与消息逐字一致,故这里用直通字段
59
+ * 而不是 text()(text() 对非字符串 fail loud,会把既有的静默降级变成挂载期报错)。
60
+ */
61
+ const bashPathField = {
62
+ kind: 'passthrough',
63
+ parse: (value) => (typeof value === 'string' && value.length > 0 ? value : undefined),
64
+ }
56
65
 
57
- /** Every config key this plugin accepts — anything else is a typo. */
58
- const ALLOWED_KEYS = new Set(['bashPath', 'timeoutMs', 'maxOutputBytes'])
66
+ // 超时与输出上限无内置默认:取值归组合源 / 预设
67
+ // (见 engine/compositions/source/local/tool-git-bash.yml)。
68
+
69
+ /**
70
+ * 配置契约:字段声明是白名单(未知键 = 打字错误)与逐字段归一化的单一来源
71
+ * (导出供契约对拍测试使用)。
72
+ * `enabled` 是本模块的能力开关:未声明 = 关闭,唯一事实源是 config.enabled;
73
+ * 组合源 tool-git-bash.yml 显式写 enabled: true(该行原本「在组合里即启用」)。
74
+ */
75
+ export const configContract = defineConfig({
76
+ enabled: bool({ default: false }),
77
+ bashPath: bashPathField,
78
+ timeoutMs: int({ min: 1, required: true }),
79
+ maxOutputBytes: int({ min: 1, required: true }),
80
+ })
59
81
 
60
82
  /**
61
83
  * Git Bash candidate paths, in probe order (see the header): the `git`
@@ -122,10 +144,13 @@ const commandSchema = {
122
144
 
123
145
  /** Register the model-facing `bash` tool. */
124
146
  export function apply(ctx, config) {
125
- const source = validateConfig(name, config, ALLOWED_KEYS)
126
- const explicitBashPath = typeof source.bashPath === 'string' && source.bashPath.length > 0 ? source.bashPath : undefined
127
- const timeoutMs = Number.isSafeInteger(source.timeoutMs) && source.timeoutMs > 0 ? source.timeoutMs : DEFAULT_TIMEOUT_MS
128
- const maxOutputBytes = Number.isSafeInteger(source.maxOutputBytes) && source.maxOutputBytes > 0 ? source.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
147
+ // 校验入口:未知键 / enabled 非布尔 / timeoutMs 与 maxOutputBytes 缺失或越界一律挂载期 fail loud。
148
+ const source = configContract.parse(config, name)
149
+ // 能力开关:唯一事实源是 config.enabled;关闭时不注册工具(配置校验仍在上一步照常执行)。
150
+ if (!source.enabled) return
151
+ const explicitBashPath = source.bashPath
152
+ const timeoutMs = source.timeoutMs
153
+ const maxOutputBytes = source.maxOutputBytes
129
154
 
130
155
  // The inferred executable is memoized per plugin instance: candidate probing
131
156
  // walks the filesystem, and the answer cannot change within a mount. A
@@ -167,7 +192,7 @@ export function apply(ctx, config) {
167
192
  // PATH): name the remedies instead of leaking a raw ENOENT. Never
168
193
  // fall back to pwsh/cmd here — the schema promises `bash -c`
169
194
  // semantics; a different shell would silently break every command.
170
- throw new Error(`bash executable not found — install Git for Windows, expose a bash on PATH, or set the custom-bash \`bashPath\` config (${String((error && error.message) || error)})`)
195
+ throw new Error(`bash executable not found — install Git for Windows, expose a bash on PATH, or set the tool-git-bash \`bashPath\` config (${String((error && error.message) || error)})`)
171
196
  }
172
197
  }
173
198
 
@@ -269,7 +294,7 @@ export function apply(ctx, config) {
269
294
  // Collected readers may be unavailable on some backends; tolerate.
270
295
  }
271
296
  const note = usedFallback
272
- ? `[custom-bash] workdir ${requested} was unusable (ENOENT); fell back to session cwd\n`
297
+ ? `[tool-git-bash] workdir ${requested} was unusable (ENOENT); fell back to session cwd\n`
273
298
  : ''
274
299
  const text = [stdout, stderr].filter((part) => part.length > 0).join('\n')
275
300
  const tail = note + (text.length > 0 ? text : `exit code: ${outcome.exitCode} (no output)`)
@@ -0,0 +1,48 @@
1
+ /** 声明编辑目录:类别、支持面和执行点来自实际编译器,示例仅提供可编辑的起点。 */
2
+ import { ACTION_KINDS, actionExecutionPoint, actionSupportsWhen } from './actions.mjs'
3
+ import { COMPOSITE_OPERATORS, PREDICATE_FACTORIES, compileDeclaration, compileWhen } from './trigger-spec.mjs'
4
+ import { WATERFALL_POSITIONS } from './trigger.mjs'
5
+
6
+ const PREDICATE_EXAMPLES = {
7
+ text: { subject: 'userMessage', keys: ['keyword'] },
8
+ phase: { promoted: false },
9
+ source: { kind: 'user' },
10
+ count: { of: 'tool-call', per: 'turn', min: 1 },
11
+ names: { allow: ['bash'] },
12
+ session: { type: 'user/message', present: false },
13
+ preset: { presetId: 'preset-id' },
14
+ }
15
+
16
+ const ACTION_EXAMPLES = {
17
+ 'inject-text': { config: { id: 'notice', layer: 'pre-step', text: 'Context', modelScope: 'all' } },
18
+ assembly: { target: { sections: { add: [{ name: 'custom-context', text: 'Context' }] } } },
19
+ decision: { phase: 'pre', decision: 'ask' },
20
+ 'append-context': { mode: 'context', text: 'Context' },
21
+ guard: { mask: { deny: ['bash'] }, reason: 'Tool disabled by this preset' },
22
+ 'sdk-strip': { mask: { deny: ['bash'] } },
23
+ 'request-params': { patch: { maxTokens: 4096 }, modelScope: 'all' },
24
+ 'inbox-prepend': { target: 'next-turn', text: 'Context' },
25
+ 'pre-step-filter': { blockPlugins: ['plugin-id'] },
26
+ }
27
+
28
+ /** 每次返回独立的纯数据;调用方编辑草稿不会污染后续请求或引擎目录。 */
29
+ export function getTriggerEditorMeta() {
30
+ const predicates = Object.keys(PREDICATE_FACTORIES).map((kind) => {
31
+ if (!Object.hasOwn(PREDICATE_EXAMPLES, kind)) throw new TypeError(`trigger-editor-meta: missing ${kind} predicate example`)
32
+ return { kind, example: { [kind]: structuredClone(PREDICATE_EXAMPLES[kind]) } }
33
+ })
34
+ for (const kind of COMPOSITE_OPERATORS) {
35
+ const child = { phase: { promoted: false } }
36
+ predicates.push({ kind, example: { [kind]: kind === 'not' ? child : [child] } })
37
+ }
38
+ for (const { example } of predicates) compileWhen(example)
39
+
40
+ const actions = Object.keys(ACTION_KINDS).map((kind) => {
41
+ if (!Object.hasOwn(ACTION_EXAMPLES, kind)) throw new TypeError(`trigger-editor-meta: missing ${kind} action example`)
42
+ const example = { kind, ...structuredClone(ACTION_EXAMPLES[kind]) }
43
+ const { channel, phase } = actionExecutionPoint(example)
44
+ compileDeclaration({ id: kind, channel, phase, do: example })
45
+ return { kind, example, channel, phase, supportsWhen: actionSupportsWhen(kind) }
46
+ })
47
+ return { predicates, actions, composites: [...COMPOSITE_OPERATORS], waterfallPositions: [...WATERFALL_POSITIONS] }
48
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * trigger-spec — 声明编译器(B7 T1):把**纯数据**的触发器声明编译成可注册的形状。
3
+ *
4
+ * 分工:`predicates.mjs` 提供判断原语、`actions.mjs` 提供动作、`trigger.mjs` 提供调度机制,
5
+ * 本文件只做「数据 → 函数」这一步,**不含任何判断或执行的实现**。
6
+ *
7
+ * ## 词汇原样透传(这是本文件最重要的约束)
8
+ *
9
+ * `when` 的键要么是组合运算符(`any` / `all` / `not` / `notAny`),要么是七类原语之一
10
+ * (`text` / `phase` / `source` / `count` / `names` / `session` / `preset`),**值原样传给对应的
11
+ * `createXxxPredicate`**。声明层**不翻译、不改名、不补默认**——一旦这里长出第二套词汇,
12
+ * 「文档里写的选项」与「谓词真正接受的选项」就会开始漂移,而这正是本项目要收敛的问题。
13
+ *
14
+ * ## 为什么 `when` 省略 = 无条件
15
+ *
16
+ * `when` 是**收窄**条件:不写表示「这个动作在该通道上总是执行」。这与「写一个恒真谓词」
17
+ * 在行为上等价,但省略更好读;编译器对省略返回 `undefined`,注册侧据此跳过判定(零开销)。
18
+ *
19
+ * ## 与 `mountTriggers` 的关系
20
+ *
21
+ * `trigger.mjs` 的 `mountTriggers` 要求 `do` 是**函数**,而声明的 `do` 是**动作声明**;
22
+ * B3 收口时两者从未对接。本文件不去接那条线(查证过:`inject-text` 与 `guard` 不经
23
+ * `on(...)` 注册,"执行体"抽不出来),而是编译出 `{when, actions}` 交给
24
+ * `registerAction(ctx, action, { when })` ——判定由动作侧按固定执行阶段提供。
25
+ * 调度层面仍复用 `trigger.mjs` 的 `orderTriggers` / `registrationOptions`。
26
+ */
27
+ import {
28
+ composite,
29
+ createCountPredicate,
30
+ createNameListPredicate,
31
+ createPhasePredicate,
32
+ createPresetPredicate,
33
+ createSessionStatePredicate,
34
+ createSourcePredicate,
35
+ createTextPredicate,
36
+ } from './predicates.mjs'
37
+ import { ACTION_KINDS, actionExecutionPoint, prepareAction, registerAction } from './actions.mjs'
38
+ import { WATERFALL_POSITIONS, orderTriggers, registrationOptions, wireTriggerObservers } from './trigger.mjs'
39
+
40
+ /** 七类判断原语:声明里的键 → 工厂。键名与 `predicates.mjs` 的工厂一一对应。 */
41
+ export const PREDICATE_FACTORIES = Object.freeze({
42
+ text: createTextPredicate,
43
+ phase: createPhasePredicate,
44
+ source: createSourcePredicate,
45
+ count: createCountPredicate,
46
+ names: createNameListPredicate,
47
+ session: createSessionStatePredicate,
48
+ preset: createPresetPredicate,
49
+ })
50
+
51
+ /** 组合运算符(与 `composite` 的词汇同源,不另立别名)。 */
52
+ export const COMPOSITE_OPERATORS = Object.freeze(['any', 'all', 'not', 'notAny'])
53
+
54
+ /** 声明允许的字段(未知字段 fail loud:写错键名是最容易被静默忽略的一类错)。 */
55
+ const DECLARATION_FIELDS = Object.freeze(['id', 'channel', 'channelOrder', 'waterfallPosition', 'phase', 'when', 'do'])
56
+
57
+ /** 判定/动作在 `next()` 之前还是之后执行。 */
58
+ const ACTION_PHASES = Object.freeze(['before-next', 'after-next'])
59
+
60
+ const objectOrUndefined = (value, label) => {
61
+ if (value === undefined || value === null) return undefined
62
+ if (typeof value !== 'object' || Array.isArray(value)) {
63
+ throw new TypeError(`trigger-spec: ${label} must be an object`)
64
+ }
65
+ return value
66
+ }
67
+
68
+ /**
69
+ * 编译 `when`:声明对象 → 谓词函数;`undefined` / `null` → `undefined`(无条件)。
70
+ *
71
+ * 节点形状(两种,互斥):
72
+ * - 组合节点:`{ any: [节点…] }` / `{ all: [节点…] }` / `{ notAny: [节点…] }` / `{ not: 节点 }`
73
+ * - 原语节点:`{ <类别>: <选项对象> }`
74
+ * **一个节点只能声明一个键**——混合声明(如同时给 `all` 与 `not`)在编译期抛错,
75
+ * 于是优先级永远是显式的树形嵌套,而不是靠"先算 not 再算 and"的隐式约定。
76
+ *
77
+ * @param {unknown} node 声明里的 `when`
78
+ * @param {object} [context] `{ ctx, standingMountFor }`:`preset` 原语需要它们(挂载期注入)
79
+ * @returns {Function|undefined} 谓词函数
80
+ */
81
+ export function compileWhen(node, context = {}) {
82
+ if (node === undefined || node === null) return undefined
83
+ if (typeof node !== 'object' || Array.isArray(node)) {
84
+ throw new TypeError('trigger-spec: when must be an object (a composite node or a predicate node)')
85
+ }
86
+ const keys = Object.keys(node)
87
+ if (keys.length === 0) {
88
+ throw new TypeError('trigger-spec: when must not be an empty object — 空节点是「恒真」还是「写漏了」无法区分')
89
+ }
90
+ if (keys.length > 1) {
91
+ throw new TypeError(
92
+ `trigger-spec: when must declare exactly one key — got ${keys.sort().join(', ')}`
93
+ + `(allowed: ${[...COMPOSITE_OPERATORS, ...Object.keys(PREDICATE_FACTORIES)].sort().join(', ')})`,
94
+ )
95
+ }
96
+ const [key] = keys
97
+ const value = node[key]
98
+
99
+ if (COMPOSITE_OPERATORS.includes(key)) {
100
+ if (key === 'not') {
101
+ return composite({ not: compileWhen(value, context) })
102
+ }
103
+ if (!Array.isArray(value) || value.length === 0) {
104
+ throw new TypeError(`trigger-spec: when.${key} must be a non-empty array of nodes`)
105
+ }
106
+ return composite({ [key]: value.map((child) => compileWhen(child, context)) })
107
+ }
108
+
109
+ const factory = PREDICATE_FACTORIES[key]
110
+ if (factory === undefined) {
111
+ throw new TypeError(
112
+ `trigger-spec: unknown predicate ${JSON.stringify(key)}`
113
+ + ` — known predicates: ${Object.keys(PREDICATE_FACTORIES).join(', ')}; composite: ${COMPOSITE_OPERATORS.join(', ')}`,
114
+ )
115
+ }
116
+ const options = objectOrUndefined(value, `when.${key}`) ?? {}
117
+ // `preset` 原语的取法需要挂载期注入(见 predicates.mjs 的三条硬约束),其余原语只用选项。
118
+ if (key === 'preset') {
119
+ return createPresetPredicate({ ...options, ctx: context.ctx, standingMountFor: context.standingMountFor })
120
+ }
121
+ return factory(options)
122
+ }
123
+
124
+ /** `do` 归一化为动作声明数组;每个元素的 `kind` 必须是已知动作。 */
125
+ function compileActions(value) {
126
+ const list = Array.isArray(value) ? value : [value]
127
+ if (list.length === 0) throw new TypeError('trigger-spec: do must be an action declaration or a non-empty array of them')
128
+ return list.map((action, index) => {
129
+ if (action === null || typeof action !== 'object' || Array.isArray(action)) {
130
+ throw new TypeError(`trigger-spec: do[${index}] must be an action declaration object`)
131
+ }
132
+ if (typeof action.kind !== 'string' || !Object.hasOwn(ACTION_KINDS, action.kind)) {
133
+ throw new TypeError(
134
+ `trigger-spec: do[${index}].kind must be one of ${Object.keys(ACTION_KINDS).join(', ')} — got ${JSON.stringify(action.kind)}`,
135
+ )
136
+ }
137
+ return action
138
+ })
139
+ }
140
+
141
+ /**
142
+ * 编译一条声明:校验字段 + 编译 `when` + 归一化 `do`。
143
+ *
144
+ * @param {object} spec 声明(`{ id, channel, when?, do, channelOrder?, waterfallPosition?, phase? }`)
145
+ * @param {object} [context] 同 {@link compileWhen},另可提供 promptConfigOptions(createPromptConfigs 的模板/策略编译选项)。
146
+ * @returns {{id: string, channel: string, channelOrder: number, waterfallPosition: string, phase: string, when: Function|undefined, actions: object[], promptConfigOptions?: object}}
147
+ */
148
+ export function compileDeclaration(spec, context = {}) {
149
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
150
+ throw new TypeError('trigger-spec: trigger declaration must be an object')
151
+ }
152
+ const unknown = Object.keys(spec).filter((key) => !DECLARATION_FIELDS.includes(key))
153
+ if (unknown.length > 0) {
154
+ throw new TypeError(
155
+ `trigger-spec: unknown trigger field(s) ${unknown.sort().join(', ')} — allowed: ${[...DECLARATION_FIELDS].sort().join(', ')}`,
156
+ )
157
+ }
158
+ if (typeof spec.id !== 'string' || spec.id.length === 0) {
159
+ throw new TypeError('trigger-spec: trigger id must be a non-empty string')
160
+ }
161
+ if (typeof spec.channel !== 'string' || spec.channel.length === 0) {
162
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: channel must be a non-empty event name`)
163
+ }
164
+ if (spec.do === undefined) {
165
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: do is required`)
166
+ }
167
+ const channelOrder = spec.channelOrder ?? 0
168
+ if (!Number.isSafeInteger(channelOrder) || channelOrder < 0) {
169
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: channelOrder must be a non-negative safe integer`)
170
+ }
171
+ const waterfallPosition = spec.waterfallPosition ?? 'default'
172
+ if (!WATERFALL_POSITIONS.has(waterfallPosition)) {
173
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: waterfallPosition must be one of ${[...WATERFALL_POSITIONS].join(', ')}`)
174
+ }
175
+ const actions = compileActions(spec.do)
176
+ const when = compileWhen(spec.when, context)
177
+ // 保存期复用注册入口的纯准备阶段;不绑定 ctx,不创建预算或监听器。
178
+ for (const action of actions) prepareAction(action, {
179
+ when, promptConfigOptions: context.promptConfigOptions, ...registrationOptions({ waterfallPosition }),
180
+ })
181
+ const phase = spec.phase ?? actionExecutionPoint(actions[0]).phase
182
+ if (!ACTION_PHASES.includes(phase)) {
183
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: phase must be one of ${ACTION_PHASES.join(', ')}`)
184
+ }
185
+ for (const action of actions) {
186
+ const point = actionExecutionPoint(action)
187
+ if (spec.channel !== point.channel) {
188
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: action ${action.kind} channel must be ${point.channel}`)
189
+ }
190
+ if (phase !== point.phase) {
191
+ throw new TypeError(`trigger-spec: trigger ${spec.id}: action ${action.kind} phase must be ${point.phase}`)
192
+ }
193
+ }
194
+ return {
195
+ id: spec.id,
196
+ channel: spec.channel,
197
+ channelOrder,
198
+ waterfallPosition,
199
+ phase,
200
+ when,
201
+ actions,
202
+ ...(context.promptConfigOptions === undefined ? {} : { promptConfigOptions: context.promptConfigOptions }),
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 编译一组声明:先归一化,再按 `channelOrder` 做稳定排序(同值保持声明序)。
208
+ * 排序复用 `trigger.mjs` 的 `orderTriggers`——声明路径与内联函数路径共用同一套调度语义。
209
+ */
210
+ export function compileDeclarations(specs, context = {}) {
211
+ if (!Array.isArray(specs)) throw new TypeError('trigger-spec: triggers must be an array')
212
+ return orderTriggers(specs.map((spec) => compileDeclaration(spec, context)))
213
+ }
214
+
215
+ /** 该声明在其通道上的注册选项(只有 `outermost` 才 prepend),转发 `trigger.mjs` 的实现。 */
216
+ export function declarationRegistrationOptions(compiled) {
217
+ return registrationOptions(compiled)
218
+ }
219
+
220
+ /**
221
+ * 把**编译好的**声明逐条注册——声明路径的挂载入口。
222
+ *
223
+ * 一条声明的每个动作各注册一次,带上:
224
+ * - `when`:该声明的判定(`undefined` = 无条件,注册侧据此跳过判定);
225
+ * - `prepend`:`waterfallPosition === 'outermost'` 时落在外层。
226
+ * 会话态谓词(相位 / 计数)的 `observe` 喂入走 `wireTriggerObservers`——与
227
+ * `mountTriggers` 的内联路径**共用同一实现**,不各写一条 `session/event` 监听。
228
+ *
229
+ * @param ctx 挂载作用域
230
+ * @param compiled {@link compileDeclarations} 的产物
231
+ * @returns disposer:撤销全部注册(含 observe 接线与每个动作自己的释放)
232
+ */
233
+ export function mountDeclarations(ctx, compiled, { plugin, warnOnce } = {}) {
234
+ const disposers = []
235
+ const observer = wireTriggerObservers(ctx, compiled, { plugin, warnOnce })
236
+ if (observer !== undefined) disposers.push(observer)
237
+ for (const trigger of compiled) {
238
+ for (const action of trigger.actions) {
239
+ disposers.push(registerAction(ctx, action, {
240
+ plugin,
241
+ warnOnce,
242
+ promptConfigOptions: trigger.promptConfigOptions,
243
+ when: trigger.when,
244
+ prepend: trigger.waterfallPosition === 'outermost',
245
+ }))
246
+ }
247
+ }
248
+ return () => {
249
+ for (const dispose of disposers.splice(0)) {
250
+ try {
251
+ if (typeof dispose === 'function') dispose()
252
+ } catch {
253
+ // 释放失败不得反过来打断卸载流程(与 actions.mjs 同一纪律)。
254
+ }
255
+ }
256
+ }
257
+ }