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
@@ -0,0 +1,285 @@
1
+ /**
2
+ * instruction-hint — 通用内置指令文件提示引擎。
3
+ *
4
+ * 本模块同时是「可 import 的纯函数库」与「可被组合源挂载的 plugin」:
5
+ * - prompt-config 的 strategy=placeholder + fill=instruction-hint
6
+ * 直接使用本模块的探测与消息构造;
7
+ * - plugin 形态(见文件末尾 apply):**挂本行 = 开启**「晋升后把
8
+ * agent-instructions 全文替换为一次性 hint,后续全文消息丢弃」的转换。
9
+ *
10
+ * 只提示参考文件存在,不把文件正文塞进每轮上下文。文件探测失败时返回空结果,
11
+ * 不阻断会话;独立生成的 hint 使用随机 id,替换既有消息时保留其 id。
12
+ *
13
+ * PLUGIN CONFIG(挂载期校验;声明即校验,未声明 `enabled` = 关闭):
14
+ * - `enabled`: boolean,缺省 false(组合源须显式写 true)。
15
+ * - `promoteOn`: 'either'(缺省)| 'tool-call' | 'assistant-message'。
16
+ * - `includeSubagents`: boolean,缺省 false(子代理首次请求即视为已晋升)。
17
+ */
18
+
19
+ import { randomUUID } from 'node:crypto'
20
+ import { readFileSync } from 'node:fs'
21
+ import { createEpochPromotion } from './compaction-epoch.mjs'
22
+ import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
23
+
24
+ export const name = 'instruction-hint'
25
+
26
+ /** 无 inject:监听器只在使用时触碰 ctx 服务(同 tool-bootstrap / context-gate 纪律)。 */
27
+ export const inject = []
28
+
29
+ /** 项目目录链中按优先级探测的指令文件名。 */
30
+ export const PROJECT_INSTRUCTION_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
31
+ export const USER_GLOBAL_INSTRUCTION_CANDIDATE = 'AGENTS.md'
32
+ const REFERENCE_HINT_SUFFIX = "They are reference documents about the user's environment and workspace conventions, not task instructions. Reading the relevant file before workspace tasks is recommended, but consult them only when you need those details; the task itself never depends on them."
33
+
34
+ /** Join one path segment onto a directory (platform-agnostic string join). */
35
+ export function joinPath(dir, segment) {
36
+ if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
37
+ const sep = dir.includes('\\') ? '\\' : '/'
38
+ return dir + sep + segment
39
+ }
40
+
41
+ /** Parent of an absolute Windows or POSIX path. */
42
+ export function parentPath(path) {
43
+ const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
44
+ if (idx <= 0) return path
45
+ const parent = path.slice(0, idx)
46
+ return parent.length === 0 ? path : parent
47
+ }
48
+
49
+ /** Find the project root: first ancestor containing any root marker. */
50
+ export async function findProjectRoot(fs, cwd, signal) {
51
+ let current = cwd
52
+ for (;;) {
53
+ for (const marker of ['.git', '.hg', '.svn']) {
54
+ try {
55
+ const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
56
+ const info = await fs.stat(target, signal)
57
+ if (info !== undefined) return current
58
+ } catch {
59
+ // Probe failure = marker absent; continue.
60
+ }
61
+ }
62
+ const parent = parentPath(current)
63
+ if (parent === current || parent.length === 0) return cwd
64
+ current = parent
65
+ }
66
+ }
67
+
68
+ /** List instruction files present in one directory. */
69
+ export async function presentInDir(fs, dir, candidates, signal) {
70
+ const found = []
71
+ for (const candidate of candidates) {
72
+ try {
73
+ const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
74
+ const info = await fs.stat(target, signal)
75
+ if (info !== undefined && info.type === 'file') found.push(candidate)
76
+ } catch {
77
+ // Absent or unreadable — skip.
78
+ }
79
+ }
80
+ return found
81
+ }
82
+
83
+ /**
84
+ * 探测 cwd 到项目根的完整目录链,以及 DSH_HOME 下的用户级 AGENTS.md。
85
+ * home 未传时使用当前宿主的 DSH_HOME/USERPROFILE 推导值。
86
+ */
87
+ export async function collectInstructionFiles(fs, cwd, signal, home) {
88
+ const root = await findProjectRoot(fs, cwd, signal)
89
+ const projectFiles = []
90
+ let probed = cwd
91
+ for (;;) {
92
+ for (const candidate of await presentInDir(fs, probed, PROJECT_INSTRUCTION_CANDIDATES, signal)) {
93
+ projectFiles.push(probed === root ? candidate : joinPath(probed, candidate))
94
+ }
95
+ if (probed === root) break
96
+ const parent = parentPath(probed)
97
+ if (parent === probed || parent.length === 0) break
98
+ probed = parent
99
+ }
100
+
101
+ const userGlobalFiles = []
102
+ const dshHome = home ?? process.env.DSH_HOME
103
+ ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
104
+ if (dshHome !== undefined) {
105
+ userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_INSTRUCTION_CANDIDATE], signal))
106
+ }
107
+ return { root, projectFiles, userGlobalFiles, userGlobalHome: dshHome }
108
+ }
109
+
110
+ /**
111
+ * 将探测结果格式化为建议式 hint;无文件时返回空字符串。
112
+ * scope 限定只报告某一来源:all(默认)/ global($DSH_HOME/AGENTS.md)/ project(cwd→项目根链)。
113
+ */
114
+ export function buildInstructionHintText({ root, projectFiles = [], userGlobalFiles = [], userGlobalHome } = {}, scope = 'all') {
115
+ const sections = []
116
+ if (scope !== 'global' && projectFiles.length > 0) {
117
+ sections.push(`Reference documents exist: ${projectFiles.join(', ')} (project root: ${root}).`)
118
+ }
119
+ if (scope !== 'project' && userGlobalFiles.length > 0) {
120
+ const paths = typeof userGlobalHome === 'string' && userGlobalHome.length > 0
121
+ ? userGlobalFiles.map((name) => joinPath(userGlobalHome, name))
122
+ : userGlobalFiles
123
+ sections.push(`A user reference document exists: ${paths.join(', ')}.`)
124
+ }
125
+ if (sections.length === 0) return ''
126
+ sections.push(REFERENCE_HINT_SUFFIX)
127
+ return sections.join(' ')
128
+ }
129
+
130
+ const HINT_SCOPES = new Set(['all', 'global', 'project'])
131
+
132
+ function hintScope(value) {
133
+ return typeof value === 'string' && HINT_SCOPES.has(value) ? value : 'all'
134
+ }
135
+
136
+ /**
137
+ * prompt-config placeholder fill=instruction-hint 的 resolver。
138
+ * params.text(自定义提示)优先;params.file(文件卡绑定的单个指令文件)次之——
139
+ * 运行时读该文件正文并加 `Instructions from:` 头注入;最后按 params.scope 探测来源
140
+ * (all / global / project)只发「文件存在」提示。
141
+ * 文件缺失、不可读或空内容时返回 null,不注入任何消息。
142
+ */
143
+ export function createInstructionHintResolver(config = {}) {
144
+ const customText = typeof config?.params?.text === 'string' && config.params.text.trim().length > 0
145
+ ? config.params.text.trim()
146
+ : ''
147
+ const scope = hintScope(config?.params?.scope)
148
+ const file = typeof config?.params?.file === 'string' && config.params.file.trim().length > 0
149
+ ? config.params.file.trim()
150
+ : ''
151
+ const fileLabel = typeof config?.params?.displayPath === 'string' && config.params.displayPath.trim().length > 0
152
+ ? config.params.displayPath.trim()
153
+ : file
154
+ /** 文件即真相:每次注入都重读绑定文件,外部编辑立即生效。 */
155
+ const readBoundFile = () => {
156
+ try {
157
+ return readFileSync(file, 'utf8').trim()
158
+ } catch {
159
+ return ''
160
+ }
161
+ }
162
+
163
+ return async ({ ctx, agent, session }) => {
164
+ const id = `instruction-hint-${session.id}-${randomUUID()}`
165
+ if (customText.length > 0) {
166
+ return { id, text: customText, source: { kind: 'instruction-hint', form: 'hint' } }
167
+ }
168
+ if (file.length > 0) {
169
+ const content = readBoundFile()
170
+ return content.length > 0
171
+ ? { id, text: `Instructions from: ${fileLabel}\n\n${content}`, source: { kind: 'instruction-file', form: 'instructions' } }
172
+ : null
173
+ }
174
+ const fs = ctx.get('fs')
175
+ if (fs === undefined) return null
176
+ const cwd = session.header?.cwd ?? process.cwd()
177
+ const found = await collectInstructionFiles(fs, cwd, agent.signal)
178
+ const text = buildInstructionHintText(found, scope)
179
+ return text.length > 0
180
+ ? { id, text, source: { kind: 'instruction-hint', form: 'hint' } }
181
+ : null
182
+ }
183
+ }
184
+
185
+ const INSTRUCTION_FROM_RE = /(?:^|\n) *(?:Additional |Updated )?Instructions from: ([^\n]+)/g
186
+
187
+ /** 从 agent-instructions 注入消息提取参考文件路径。 */
188
+ export function extractInstructionPaths(message) {
189
+ const paths = []
190
+ const blocks = Array.isArray(message?.content) ? message.content : []
191
+ for (const block of blocks) {
192
+ if (block?.type !== 'text' || typeof block.text !== 'string') continue
193
+ for (const match of block.text.matchAll(INSTRUCTION_FROM_RE)) {
194
+ const path = match[1].trim()
195
+ if (path !== '' && !paths.includes(path)) paths.push(path)
196
+ }
197
+ }
198
+ return paths
199
+ }
200
+
201
+ /** 构造一次性非命令式 hint 消息;替换已有消息时保留其 id。 */
202
+ export function buildInstructionHint(original, paths, sourceName = 'instruction-hint') {
203
+ return {
204
+ id: typeof original?.id === 'string' && original.id !== '' ? original.id : `instruction-hint-${randomUUID()}`,
205
+ role: 'user',
206
+ content: [{
207
+ type: 'text',
208
+ text: '<system-reminder>\n'
209
+ + `Reference documents exist: ${paths.join(', ')}. ${REFERENCE_HINT_SUFFIX}`
210
+ + '\n</system-reminder>',
211
+ }],
212
+ source: { kind: 'instruction-hint', form: 'hint', plugin: sourceName },
213
+ }
214
+ }
215
+
216
+ /** 将 agent-instructions 全文替换为一次性 hint,后续全文消息丢弃。 */
217
+ export function instructionHintMessages(messages, state, sourceName = 'instruction-hint') {
218
+ const kept = []
219
+ for (const message of messages) {
220
+ if (message?.source?.kind !== 'agent-instructions') {
221
+ kept.push(message)
222
+ continue
223
+ }
224
+ if (state.instructionHinted) continue
225
+ const paths = extractInstructionPaths(message)
226
+ if (paths.length === 0) {
227
+ kept.push(message)
228
+ continue
229
+ }
230
+ state.instructionHinted = true
231
+ kept.push(buildInstructionHint(message, paths, sourceName))
232
+ }
233
+ return kept
234
+ }
235
+
236
+ // ── plugin 形态 ─────────────────────────────────────────────────────────────
237
+
238
+ /** Every config key this plugin accepts — anything else is a typo. */
239
+ const ALLOWED_KEYS = new Set(['enabled', 'promoteOn', 'includeSubagents'])
240
+
241
+ /** The visible surface, not the append-only log, owns hint lifetime. */
242
+ function hasVisibleInstructionHint(session) {
243
+ const messages = session?.deriveMessages?.()
244
+ return Array.isArray(messages) && messages.some((message) => message?.source?.kind === 'instruction-hint')
245
+ }
246
+
247
+ /**
248
+ * 注册晋升后的指令正文转换。
249
+ *
250
+ * `prepend: true` + 组合首选行:waterfall after-next 变换按注册逆序生效,本转换因此
251
+ * 是最外层——后注册的注入无法绕过它。`promotion.status(agent)` 兼作冷扫入口(resume /
252
+ * reload 后由 durable 事件流重建相位)。声明即校验:非法配置在挂载期暴露,未声明
253
+ * `enabled` 时同样报错。
254
+ */
255
+ export function apply(ctx, config) {
256
+ const source = validateConfig(name, config, ALLOWED_KEYS)
257
+ const promoteEvents = parsePromoteOn(name, source.promoteOn)
258
+ const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
259
+ // 开关语义:未声明 = 关闭(组合源为本模块显式写 enabled: true)。
260
+ const enabled = booleanOption(name, source.enabled, 'enabled', false)
261
+ if (!enabled) return
262
+
263
+ const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
264
+ const warnOnce = createWarnOnce(ctx, name)
265
+ ctx.on('session/event', (session, event) => promotion.observe(session, event))
266
+
267
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
268
+ // Downstream errors propagate untouched; only this filter's own logic is guarded.
269
+ const decision = await next()
270
+ if (decision.kind === 'reject') return decision
271
+ try {
272
+ if (!promotion.status(agent).promoted) return decision
273
+ if (!Array.isArray(decision.messages)) return decision
274
+ if (agent?.session === undefined) return decision
275
+ // 1 换 1 的转换不能按长度判断(长度相同仍可能已转换),
276
+ // instructionHintMessages 本身保留非目标消息,直接采用结果。
277
+ const hintState = { instructionHinted: hasVisibleInstructionHint(agent.session) }
278
+ return { ...decision, messages: instructionHintMessages(decision.messages, hintState, name) }
279
+ } catch (error) {
280
+ // 转换失败不阻断会话:保留原消息。
281
+ warnOnce(`${name}: instruction hint conversion failed, keeping messages: ${String((error && error.message) || error)}`)
282
+ return decision
283
+ }
284
+ }, { prepend: true })
285
+ }
@@ -13,10 +13,12 @@
13
13
  * - 键字符集:字母数字、下划线、点、中文、连字符(与 ST setvar/getvar 一致)。
14
14
  */
15
15
 
16
+ import { sessionEvents } from './shared.mjs'
17
+ import { createHash } from 'node:crypto'
18
+
16
19
  /** 会话事件中最后一条指定类型消息的文本(事件倒序扫描;无则空串)。 */
17
20
  function lastMessageOf(session, type) {
18
- const events = session?.events
19
- if (!Array.isArray(events)) return ''
21
+ const events = sessionEvents(session)
20
22
  for (let index = events.length - 1; index >= 0; index--) {
21
23
  const event = events[index]
22
24
  if (event?.type !== type) continue
@@ -31,22 +33,26 @@ function lastMessageOf(session, type) {
31
33
  return ''
32
34
  }
33
35
 
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)
36
+ /** ST 双冒号/逗号列表;有 seed 时稳定选择,否则每次采样。 */
37
+ function pickRandom(arg, seed) {
38
+ const input = String(arg ?? '')
39
+ const items = input.includes('::') ? input.split('::')
40
+ : input.split(/(?<!\\),/).map(item => item.trim().replaceAll('\\,', ','))
37
41
  if (items.length === 0) return ''
38
- return items[Math.floor(Math.random() * items.length)]
42
+ const random = seed === undefined ? Math.random() : createHash('sha256').update(seed).digest().readUInt32BE() / 2 ** 32
43
+ return items[Math.floor(random * items.length)]
39
44
  }
40
45
 
41
- /** 骰子表达式({{roll::2d6+3}} / {{roll::1d20}};非法表达式原样返回)。 */
46
+ /** 骰子表达式({{roll::2d6+3}} / {{roll::6}};非法或不安全输入为空)。 */
42
47
  function rollDice(arg) {
43
- const text = String(arg ?? '').replace(/\s+/g, '').toLowerCase()
48
+ const raw = String(arg ?? '').replace(/\s+/g, '').toLowerCase()
49
+ const text = /^\d+$/.test(raw) ? `1d${raw}` : raw
44
50
  const match = text.match(/^(\d*)d(\d+)([+-]\d+)?$/)
45
- if (match === null) return text
51
+ if (match === null) return ''
46
52
  const count = match[1] === '' ? 1 : Number.parseInt(match[1], 10)
47
53
  const sides = Number.parseInt(match[2], 10)
48
54
  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
55
+ if (!Number.isSafeInteger(count) || count <= 0 || count > 100 || !Number.isSafeInteger(sides) || sides <= 0 || !Number.isSafeInteger(modifier) || !Number.isSafeInteger(count * sides + Math.abs(modifier))) return ''
50
56
  let sum = 0
51
57
  for (let index = 0; index < count; index++) sum += 1 + Math.floor(Math.random() * sides)
52
58
  return String(sum + modifier)
@@ -64,7 +70,7 @@ function formatTime(now) {
64
70
  return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
65
71
  }
66
72
 
67
- /** UTC YYYY-MM-DD({{date}})。 */
73
+ /** 本地 YYYY-MM-DD({{date}});UTC 日期由 isodate 提供。 */
68
74
  function formatDate(now) {
69
75
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
70
76
  }
@@ -89,28 +95,150 @@ const DYNAMIC_MACROS = {
89
95
  pipe: () => '|',
90
96
  }
91
97
 
92
- /** 模板变量插值:配置 variables 优先,ST 运行时宏次之,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}} 兜底。 */
93
- export function interpolateVariables(text, variables, session) {
94
- const builtins = {
98
+ /** 内置变量:路径类事实(无会话上下文时回退进程 cwd);两条插值通道同源。 */
99
+ function builtinVariables(session) {
100
+ return {
95
101
  DSH_HOME: process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : ''),
96
102
  WORKSPACE: process.env.DSH_WORKSPACE ?? session?.header?.cwd ?? process.cwd(),
97
103
  CWD: session?.header?.cwd ?? process.cwd(),
98
104
  }
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
105
  }
106
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
- })
107
+ /** 模板变量插值:配置 variables 优先,ST 运行时宏次之,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}} 兜底。 */
108
+ export function interpolateVariables(text, variables, session, keep, sourceId = '') {
109
+ const builtins = builtinVariables(session)
110
+ const active = new Set()
111
+ // 限制展开增加的字符数、递归深度与工作量;原始正文不截断,失败引用留给出口清洗。
112
+ let remainingChars = 1024 * 1024
113
+ let remainingExpansions = 4096
114
+ function render(input, depth = 0) {
115
+ return normalizeMacroSyntax(input).replace(REFERENCE_RE, (whole, key, arg, offset) => {
116
+ if (arg === undefined && keep?.has(key)) return `{{${key}}}`
117
+ if (active.has(key)) return whole
118
+ let value
119
+ if (Object.hasOwn(variables, key)) value = String(variables[key] ?? '')
120
+ else if (key.toLowerCase() === 'pick') value = pickRandom(arg, JSON.stringify([session?.id ?? '', sourceId, input, offset]))
121
+ else if (Object.hasOwn(DYNAMIC_MACROS, key.toLowerCase())) value = DYNAMIC_MACROS[key.toLowerCase()](arg, session)
122
+ else if (Object.hasOwn(builtins, key)) value = builtins[key]
123
+ else return whole
124
+ const growth = Math.max(0, value.length - whole.length)
125
+ const nested = value.includes('{{')
126
+ if (growth > remainingChars || (nested && (depth >= 32 || remainingExpansions <= 0))) return whole
127
+ remainingChars -= growth
128
+ if (!nested) return value
129
+ remainingExpansions--
130
+ active.add(key)
131
+ try {
132
+ return render(value, depth + 1)
133
+ } finally {
134
+ active.delete(key)
135
+ }
136
+ })
137
+ }
138
+ return render(text)
139
+ }
140
+
141
+ /**
142
+ * 静态层插值:配置 variables 优先,ST 运行时宏取空串,内置路径变量按无会话语义解析。
143
+ * @param keep 需要交给官方变量通道按 assembly 求值的名字(运行时事实与已注册变量)——
144
+ * 命中即原样保留引用,不做静态替换。
145
+ */
146
+ export function interpolateStatic(text, variables, keep) {
147
+ return interpolateVariables(text, variables, undefined, keep)
148
+ }
149
+
150
+ /**
151
+ * 需要按 assembly 求值的运行时事实(官方变量注册用)。
152
+ * random/pick/roll/chance 是「每次出现各算一次」的求值宏,newline/pipe 是字面宏,
153
+ * 都不在其中——它们继续由 interpolate* 内联处理。
154
+ */
155
+ export const RUNTIME_FACTS = new Set([
156
+ 'lastusermessage',
157
+ 'lastcharmessage',
158
+ 'charifnotgroup',
159
+ 'time',
160
+ 'date',
161
+ 'weekday',
162
+ 'isotime',
163
+ 'isodate',
164
+ ])
165
+
166
+ /**
167
+ * 运行时事实求值(大小写不敏感)。非事实名返回 undefined。
168
+ * @param session 会话对象;缺省时按静态语义(运行时宏为空、时间取当前值)。
169
+ */
170
+ export function runtimeFactValue(name, session) {
171
+ const key = String(name ?? '').toLowerCase()
172
+ if (!RUNTIME_FACTS.has(key)) return undefined
173
+ return DYNAMIC_MACROS[key](undefined, session)
174
+ }
175
+
176
+ /**
177
+ * 引用正则:键允许字母数字、下划线、点、中文与连字符(与 ST setvar/getvar 一致),
178
+ * `::` 之后是本项目宏参数。官方插值语法更窄,故这两条通道各用各的。
179
+ */
180
+ const REFERENCE_RE = /\{\{\s*([A-Za-z0-9_.\u4e00-\u9fff-]+)\s*(?:::([^{}]*?))?\}\}/g
181
+
182
+ /** ST 宏形态 → 本项目语法(幂等)。 */
183
+ const MACRO_ALIASES = 'roll|random|pick|chance'
184
+ /** 空格形态:{{roll 1d6}}(ST 语料常见,本项目语法要求 ::)。 */
185
+ const MACRO_SPACE_RE = new RegExp(`\\{\\{\\s*(${MACRO_ALIASES})\\s+([^{}]*?)\\s*\\}\\}`, 'gi')
186
+ /** 单冒号形态:{{roll:1d6}}(`::` 已是本项目语法,用负向断言排除)。 */
187
+ const MACRO_SINGLE_COLON_RE = new RegExp(`\\{\\{\\s*(${MACRO_ALIASES})\\s*:(?!:)\\s*([^{}]*?)\\s*\\}\\}`, 'gi')
188
+
189
+ /**
190
+ * ST 宏写法归一:{{roll 1d6}} / {{roll:1d6}} → {{roll::1d6}}。
191
+ * 与 `src/host/sillytavern.ts` 的导入期归一同源(两处物理隔离,改动需同步)。
192
+ */
193
+ export function normalizeMacroSyntax(text) {
194
+ return text
195
+ .replace(MACRO_SPACE_RE, (_whole, macro, arg) => `{{${String(macro).toLowerCase()}::${arg}}}`)
196
+ .replace(MACRO_SINGLE_COLON_RE, (_whole, macro, arg) => `{{${String(macro).toLowerCase()}::${arg}}}`)
197
+ }
198
+
199
+ /**
200
+ * 官方插值通道(system-section / runtime-context)专用清洗。
201
+ *
202
+ * 官方 renderPrompt 对 section/context 文本做严格插值:畸形引用、`{{}}`、未注册名、
203
+ * 值为 undefined 都抛错并让整轮 assembly 失败,而 runtime-context 在 0.1.6 没有
204
+ * `interpolate:false`(PromptContext 无该字段)。因此交给官方之前必须保证文本里
205
+ * 不再有本项目解析后残留的引用:
206
+ * - `{{…}}` 成组引用(含畸形名字)→ 整段剥离(决策:未命中引用剥离);
207
+ * - 有 `{{` 却没成组、但后文存在 `}}` → 只移除 `{{`(官方判畸形),保留可见文本;
208
+ * - 只有 `{{` 且其后无 `}}` → 官方按字面处理,原样保留。
209
+ * @returns 清洗后文本与已剥离片段(供调用方一次性告警)。
210
+ */
211
+ export function stripUnresolvedRefs(text, keep) {
212
+ const stripped = []
213
+ let out = ''
214
+ let index = 0
215
+ while (index < text.length) {
216
+ const open = text.indexOf('{{', index)
217
+ if (open < 0) {
218
+ out += text.slice(index)
219
+ break
220
+ }
221
+ out += text.slice(index, open)
222
+ const rest = text.slice(open)
223
+ const group = /^\{\{([^{}]*)\}\}/.exec(rest)
224
+ if (group !== null) {
225
+ // 白名单(已注册的官方变量名)原样保留,交给官方按 assembly 求值。
226
+ if (/^[a-z][a-z0-9_]*$/.test(group[1]) && keep?.has(group[1])) {
227
+ out += group[0]
228
+ index = open + group[0].length
229
+ continue
230
+ }
231
+ stripped.push(group[0])
232
+ index = open + group[0].length
233
+ continue
234
+ }
235
+ if (rest.indexOf('}}', 2) >= 0) {
236
+ stripped.push(rest.slice(0, 2))
237
+ index = open + 2
238
+ continue
239
+ }
240
+ out += '{{'
241
+ index = open + 2
242
+ }
243
+ return { text: out, stripped }
116
244
  }