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.
- package/README.md +185 -46
- package/engine/THIRD_PARTY_LICENSES +25 -0
- package/engine/actions.mjs +931 -0
- package/engine/anchor-match.mjs +6 -3
- package/engine/character-tools.mjs +11 -0
- package/engine/classify-task.mjs +18 -0
- package/engine/compaction-epoch.mjs +41 -20
- package/engine/compositions/library/agent-instructions.yml +9 -0
- package/engine/compositions/library/command-goal.yml +2 -6
- package/engine/compositions/library/compaction.yml +2 -32
- package/engine/compositions/library/delegation-ptc.yml +58 -0
- package/engine/compositions/library/delegation.yml +5 -38
- package/engine/compositions/library/persistent-shell.yml +4 -13
- package/engine/compositions/library/planning.yml +8 -23
- package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/skill-filesystem.yml +2 -8
- package/engine/compositions/library/tool-ask-user.yml +2 -3
- package/engine/compositions/library/tool-bash.yml +4 -12
- package/engine/compositions/library/tool-cordis.yml +7 -0
- package/engine/compositions/library/tool-fs-search.yml +2 -11
- package/engine/compositions/library/tool-fs.yml +2 -5
- package/engine/compositions/library/tool-goal.yml +2 -6
- package/engine/compositions/library/tool-jobs.yml +2 -18
- package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
- package/engine/compositions/library/tool-plugin-manager.yml +8 -0
- package/engine/compositions/library/tool-present.yml +7 -0
- package/engine/compositions/library/tool-presentation.yml +9 -0
- package/engine/compositions/library/tool-pwsh.yml +2 -6
- package/engine/compositions/library/tool-skill.yml +7 -0
- package/engine/compositions/library/tool-todo.yml +2 -4
- package/engine/compositions/library/tool-web.yml +2 -1
- package/engine/compositions/source/local/character-tools.yml +5 -0
- package/engine/compositions/source/local/declared-triggers.yml +12 -0
- package/engine/compositions/source/local/filesystem-editor.yml +30 -0
- package/engine/compositions/source/local/instruction-hint.yml +15 -0
- package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
- package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
- package/engine/compositions/source/local/run-code-env.yml +4 -7
- package/engine/compositions/source/local/session-var-tools.yml +5 -0
- package/engine/compositions/source/local/skill-search.yml +3 -9
- package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
- package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
- package/engine/compositions/source/local/tool-config-engine.yml +8 -0
- package/engine/compositions/source/local/tool-git-bash.yml +15 -0
- package/engine/compositions/source/local/world-book-tools.yml +5 -0
- package/engine/condition.mjs +83 -0
- package/engine/declared-triggers.mjs +93 -0
- package/engine/executor.mjs +364 -185
- package/engine/fields.mjs +219 -0
- package/engine/fillers.mjs +12 -141
- package/engine/host-package.mjs +51 -0
- package/engine/instruction-hint.mjs +285 -0
- package/engine/interpolate.mjs +157 -29
- package/engine/layers.mjs +495 -75
- package/engine/predicates.mjs +753 -0
- package/engine/prompt-config-engine.mjs +63 -8
- package/engine/run-code-env.mjs +48 -31
- package/engine/schema.mjs +444 -103
- package/engine/sdk-strip.mjs +365 -0
- package/engine/session-var-tools.mjs +11 -0
- package/engine/session-vars.mjs +8 -6
- package/engine/shared.mjs +137 -0
- package/engine/skill-search.mjs +34 -21
- package/engine/st-macros.d.mts +22 -0
- package/engine/st-macros.mjs +208 -0
- package/engine/st-render.mjs +105 -0
- package/engine/st-world-book.d.mts +10 -0
- package/engine/st-world-book.mjs +259 -0
- package/engine/strategies.mjs +31 -20
- package/engine/subagent-tool-policy-core.mjs +400 -0
- package/engine/subagent-tool-policy.mjs +327 -0
- package/engine/tool-config-engine.mjs +207 -145
- package/engine/tool-definition.mjs +143 -0
- package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
- package/engine/trigger-editor-meta.mjs +48 -0
- package/engine/trigger-spec.mjs +257 -0
- package/engine/trigger.mjs +304 -0
- package/engine/world-book-tools.mjs +11 -0
- package/lib/client.js +16495 -5456
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +628 -382
- package/lib/index.mjs +5391 -2942
- package/lib/paths-CY5psBqE.d.mts +1233 -0
- package/lib/preset-package-BnJqrU4R.mjs +16792 -0
- package/lib/preset-transfer.d.mts +2 -0
- package/lib/preset-transfer.mjs +2 -0
- package/package.json +91 -49
- package/preset/pt-cordis/preset.yml +43 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
- package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
- package/preset/pt-custom/preset.yml +15 -0
- package/preset/pt-minimal/preset.yml +27 -0
- package/preset/pt-ptc/preset.yml +45 -0
- package/preset/pt-standard/preset.yml +43 -0
- package/templates/14-first-turn-anchor.yml +4 -4
- package/templates/15-guide-auto.yml +3 -3
- package/templates/16-custom-fallback.yml +1 -0
- package/templates/18-placeholder-env-facts.yml +3 -3
- package/templates/20-system-section.yml +3 -2
- package/templates/65-turn-stop.yml +18 -0
- package/templates/66-subagent-start.yml +14 -0
- package/templates/75-subagent-end.yml +15 -0
- package/templates/tools/10-shell-command.yml +1 -1
- package/templates/tools/40-delegate-character.yml +1 -1
- package/templates/tools/50-delegate-worldbook.yml +1 -1
- package/templates/tools/60-delegate-session-var.yml +1 -1
- package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
- package/engine/anchor-turn.mjs +0 -68
- package/engine/code-presentation.mjs +0 -103
- package/engine/compositions/library/anchor-turn.yml +0 -10
- package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
- package/engine/compositions/library/code-presentation.yml +0 -8
- package/engine/compositions/library/context-gate.yml +0 -39
- package/engine/compositions/library/cot-drip.yml +0 -11
- package/engine/compositions/library/custom-bash.yml +0 -14
- package/engine/compositions/library/deliberation-gate.yml +0 -11
- package/engine/compositions/library/official-agent-instructions.yml +0 -17
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
- package/engine/compositions/library/official-tool-bash.yml +0 -6
- package/engine/compositions/library/official-tool-cordis.yml +0 -13
- package/engine/compositions/library/official-tool-presentation.yml +0 -7
- package/engine/compositions/library/official-tool-skill.yml +0 -14
- package/engine/compositions/library/persona.yml +0 -13
- package/engine/compositions/library/prompt-config-engine.yml +0 -16
- package/engine/compositions/library/run-code-env.yml +0 -15
- package/engine/compositions/library/skill-search.yml +0 -14
- package/engine/compositions/library/str-replace-editor.yml +0 -10
- package/engine/compositions/library/tool-bootstrap.yml +0 -14
- package/engine/compositions/library/tool-config-engine.yml +0 -9
- package/engine/compositions/library/tool-filter.yml +0 -8
- package/engine/compositions/source/local/code-presentation.yml +0 -8
- package/engine/compositions/source/local/context-gate.yml +0 -36
- package/engine/compositions/source/local/custom-bash.yml +0 -11
- package/engine/compositions/source/local/str-replace-editor.yml +0 -10
- package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
- package/engine/compositions/source/local/tool-filter.yml +0 -8
- package/engine/context-gate.mjs +0 -305
- package/engine/cot-drip.mjs +0 -137
- package/engine/deliberation-gate.mjs +0 -127
- package/engine/tool-bootstrap.mjs +0 -476
- package/engine/tool-filter.mjs +0 -80
- package/lib/preset-core.d.mts +0 -10
- package/lib/preset-core.mjs +0 -77
- package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
- package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
- package/preset/anchored/preset.yml +0 -296
- package/preset/creative/preset.yml +0 -67
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
- package/preset/custom/preset.yml +0 -12
- package/preset/liangshen/preset.yml +0 -75
- package/preset/minimal/preset.yml +0 -45
- package/preset/ptc/preset.yml +0 -58
- package/preset/standard/preset.yml +0 -57
- package/skills/manifest.json +0 -7
- package/skills/sandboxmod/SKILL.md +0 -49
- package/skills/web ui/SKILL.md +0 -42
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** 数字运算保留 number;JSON 数组以字符串保存,与 ST 变量存储一致。 */
|
|
2
|
+
export type StVariableTable = Record<string, string | number>
|
|
3
|
+
|
|
4
|
+
export interface StTextOptions {
|
|
5
|
+
variables?: Readonly<Record<string, unknown>>
|
|
6
|
+
local?: StVariableTable
|
|
7
|
+
global?: StVariableTable
|
|
8
|
+
/** 原样传给 interpolateVariables,仅供读取会话事实。 */
|
|
9
|
+
session?: unknown
|
|
10
|
+
sourceId?: string
|
|
11
|
+
warn?: (message: string) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 清理注释/trim/ERA、归一宏写法、替换已知角色与默认用户;不求值变量。 */
|
|
15
|
+
export function prepareStText(text: string, cardName?: string): string
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 顺序求值,仅修改显式 local/global;getvar 的非空 fallback 初始化所属表。
|
|
19
|
+
* 未知普通引用保留;危险键拒绝并 warn;循环、超过 32 层或 1 MiB 抛 RangeError。
|
|
20
|
+
* 另设总展开工作量上限,防止零输出的重复展开耗尽资源。
|
|
21
|
+
*/
|
|
22
|
+
export function renderStText(text: string, options?: StTextOptions): string
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/** ST 文本预处理与顺序求值;状态只归调用方传入的 local/global 所有。 */
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { interpolateVariables, normalizeMacroSyntax } from './interpolate.mjs'
|
|
5
|
+
|
|
6
|
+
const MAX_BYTES = 1024 * 1024
|
|
7
|
+
const MAX_DEPTH = 32
|
|
8
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
9
|
+
const DISCARDED_MACRO = /^\s*(?:\/\/|(?:trim|ERA)(?=$|[:\s]))/i
|
|
10
|
+
|
|
11
|
+
function byteLength(text) {
|
|
12
|
+
const size = Buffer.byteLength(text, 'utf8')
|
|
13
|
+
if (size > MAX_BYTES) throw new RangeError('ST macro text exceeds 1 MiB')
|
|
14
|
+
return size
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 双花括号是宏边界;JSON 单花括号及字符串内的右括号不是。 */
|
|
18
|
+
function macroEnd(text, start, depth = 0) {
|
|
19
|
+
const frames = [{ braces: 0, brackets: 0, quote: false, escape: false }]
|
|
20
|
+
if (depth >= MAX_DEPTH) throw new RangeError('ST macro depth exceeds 32')
|
|
21
|
+
for (let index = start + 2; index < text.length; index++) {
|
|
22
|
+
const frame = frames.at(-1)
|
|
23
|
+
const char = text[index]
|
|
24
|
+
if (frame.escape) {
|
|
25
|
+
frame.escape = false
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (frame.quote && char === '\\') {
|
|
29
|
+
frame.escape = true
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (char === '"' && (frame.quote || frame.braces > 0 || frame.brackets > 0)) {
|
|
33
|
+
frame.quote = !frame.quote
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (text.startsWith('{{', index)) {
|
|
37
|
+
if (depth + frames.length >= MAX_DEPTH) throw new RangeError('ST macro depth exceeds 32')
|
|
38
|
+
frames.push({ braces: 0, brackets: 0, quote: false, escape: false })
|
|
39
|
+
index++
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (frame.quote) continue
|
|
43
|
+
if (char === '{') frame.braces++
|
|
44
|
+
else if (char === '}' && frame.braces > 0) frame.braces--
|
|
45
|
+
else if (text.startsWith('}}', index)) {
|
|
46
|
+
frames.pop()
|
|
47
|
+
if (frames.length === 0) return index + 2
|
|
48
|
+
index++
|
|
49
|
+
} else if (char === '[') frame.brackets++
|
|
50
|
+
else if (char === ']') frame.brackets = Math.max(0, frame.brackets - 1)
|
|
51
|
+
}
|
|
52
|
+
return -1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 先找到完整边界再求值,未闭合宏的内部指令不会被意外执行。 */
|
|
56
|
+
function transformText(text, macro) {
|
|
57
|
+
let work = 0
|
|
58
|
+
function walk(source, depth = 0, path = '') {
|
|
59
|
+
work += byteLength(source)
|
|
60
|
+
// 零输出的重复变量展开也必须有界,不能只限制最终输出。
|
|
61
|
+
if (work > MAX_BYTES * MAX_DEPTH) throw new RangeError('ST macro expansion work limit exceeded')
|
|
62
|
+
const chunks = []
|
|
63
|
+
let size = 0
|
|
64
|
+
const append = (value) => {
|
|
65
|
+
size += byteLength(value)
|
|
66
|
+
if (size > MAX_BYTES) throw new RangeError('ST macro output exceeds 1 MiB')
|
|
67
|
+
chunks.push(value)
|
|
68
|
+
}
|
|
69
|
+
let cursor = 0
|
|
70
|
+
while (cursor < source.length) {
|
|
71
|
+
const start = source.indexOf('{{', cursor)
|
|
72
|
+
if (start < 0) {
|
|
73
|
+
append(source.slice(cursor))
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
append(source.slice(cursor, start))
|
|
77
|
+
const end = macroEnd(source, start, depth)
|
|
78
|
+
if (end < 0) {
|
|
79
|
+
if (!DISCARDED_MACRO.test(source.slice(start + 2))) append(source.slice(start))
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
append(macro(source.slice(start + 2, end - 2), (value) => walk(value, depth + 1, `${path}/${start}`), `${path}/${start}`))
|
|
83
|
+
cursor = end
|
|
84
|
+
}
|
|
85
|
+
return chunks.join('')
|
|
86
|
+
}
|
|
87
|
+
return walk(text)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 只分割当前层的第一个 ::,嵌套宏中的分隔符属于嵌套宏。 */
|
|
91
|
+
function splitArgument(text) {
|
|
92
|
+
for (let index = 0; index < text.length; index++) {
|
|
93
|
+
if (text.startsWith('{{', index)) {
|
|
94
|
+
const end = macroEnd(text, index)
|
|
95
|
+
if (end < 0) break
|
|
96
|
+
index = end - 1
|
|
97
|
+
} else if (text.startsWith('::', index)) {
|
|
98
|
+
return [text.slice(0, index), text.slice(index + 2)]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [text, undefined]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 对齐 ST getVariable 的数值转换;空白文本仍是文本。 */
|
|
105
|
+
function stValue(value) {
|
|
106
|
+
return (typeof value === 'string' && value.trim() === '') || Number.isNaN(Number(value))
|
|
107
|
+
? (value || '')
|
|
108
|
+
: Number(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addValue(value, increment) {
|
|
112
|
+
const current = stValue(value) || 0
|
|
113
|
+
byteLength(String(current))
|
|
114
|
+
let parsed
|
|
115
|
+
try { parsed = JSON.parse(current) } catch { /* 非 JSON 值继续按数字或文本处理。 */ }
|
|
116
|
+
if (Array.isArray(parsed)) {
|
|
117
|
+
parsed.push(increment)
|
|
118
|
+
return JSON.stringify(parsed)
|
|
119
|
+
}
|
|
120
|
+
const amount = Number(increment)
|
|
121
|
+
if (Number.isNaN(amount) || Number.isNaN(Number(current))) return String(current || '') + increment
|
|
122
|
+
const result = Number(current) + amount
|
|
123
|
+
return Number.isNaN(result) ? undefined : result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 不求值变量/动态宏,不执行 ERA 内容;未知角色名留给运行时。 */
|
|
127
|
+
export function prepareStText(text, cardName = '') {
|
|
128
|
+
const name = cardName.trim()
|
|
129
|
+
return transformText(text, (body, expand) => {
|
|
130
|
+
if (DISCARDED_MACRO.test(body)) return ''
|
|
131
|
+
if (/^char$/i.test(body) && name) return name
|
|
132
|
+
if (/^user$/i.test(body)) return '用户'
|
|
133
|
+
return normalizeMacroSyntax(`{{${expand(body)}}}`)
|
|
134
|
+
}).replace(/(?:\r?\n){3,}/g, '\n\n').trim()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 按出现顺序求值;variables/session 只读,不持久化或缓存任何状态。 */
|
|
138
|
+
export function renderStText(text, { variables = {}, local = {}, global = {}, session, sourceId = '', warn = () => {} } = {}) {
|
|
139
|
+
const sourceKey = createHash('sha256').update(sourceId).update(text).digest('hex')
|
|
140
|
+
const tables = { local, global, variables }
|
|
141
|
+
const resolving = new Set()
|
|
142
|
+
const allowed = (key) => {
|
|
143
|
+
if (key && !FORBIDDEN_KEYS.has(key)) return true
|
|
144
|
+
warn(`ST variable key blocked: ${key}`)
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
const write = (table, key, value) => {
|
|
148
|
+
if (value === undefined) return
|
|
149
|
+
byteLength(String(value))
|
|
150
|
+
// 禁止危险键,并显式定义 own 数据属性,避免触发继承的 setter。
|
|
151
|
+
Object.defineProperty(table, key, { value, writable: true, enumerable: true, configurable: true })
|
|
152
|
+
}
|
|
153
|
+
const read = (scope, key, expand, numeric = false) => {
|
|
154
|
+
const id = `${scope}:${key}`
|
|
155
|
+
if (resolving.has(id)) throw new RangeError(`ST variable cycle: ${id}`)
|
|
156
|
+
resolving.add(id)
|
|
157
|
+
try {
|
|
158
|
+
const value = tables[scope][key]
|
|
159
|
+
return expand(String((numeric ? stValue(value) : value) ?? ''))
|
|
160
|
+
} finally {
|
|
161
|
+
resolving.delete(id)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return transformText(text, (body, expand, path) => {
|
|
166
|
+
if (DISCARDED_MACRO.test(body)) return ''
|
|
167
|
+
const [name, args] = splitArgument(body)
|
|
168
|
+
const operation = /^(set|add|get|inc|dec)(global)?var$/i.exec(name.trim())
|
|
169
|
+
if (operation && args !== undefined) {
|
|
170
|
+
const [rawKey, rawValue] = splitArgument(args)
|
|
171
|
+
const key = expand(rawKey).trim()
|
|
172
|
+
if (!allowed(key)) return ''
|
|
173
|
+
const action = operation[1].toLowerCase()
|
|
174
|
+
const scope = operation[2] ? 'global' : 'local'
|
|
175
|
+
const table = tables[scope]
|
|
176
|
+
const exists = Object.hasOwn(table, key)
|
|
177
|
+
if (action === 'get') {
|
|
178
|
+
if (exists) return read(scope, key, expand, true)
|
|
179
|
+
if (scope === 'local' && Object.hasOwn(variables, key)) return read('variables', key, expand, true)
|
|
180
|
+
if (rawValue === undefined) return ''
|
|
181
|
+
const fallback = expand(rawValue)
|
|
182
|
+
if (fallback !== '') write(table, key, fallback)
|
|
183
|
+
return fallback
|
|
184
|
+
}
|
|
185
|
+
if ((action === 'set' || action === 'add') && rawValue !== undefined) {
|
|
186
|
+
const value = expand(rawValue)
|
|
187
|
+
write(table, key, action === 'set' ? value : addValue(exists ? table[key] : '', value))
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
190
|
+
if ((action === 'inc' || action === 'dec') && rawValue === undefined) {
|
|
191
|
+
const value = addValue(exists ? table[key] : '', action === 'inc' ? 1 : -1)
|
|
192
|
+
write(table, key, value)
|
|
193
|
+
return String(value ?? '')
|
|
194
|
+
}
|
|
195
|
+
return `{{${body}}}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const whole = normalizeMacroSyntax(`{{${expand(body)}}}`)
|
|
199
|
+
const [key] = splitArgument(whole.slice(2, -2))
|
|
200
|
+
if (!allowed(key)) return whole
|
|
201
|
+
if (Object.hasOwn(local, key)) return read('local', key, expand)
|
|
202
|
+
if (Object.hasOwn(variables, key)) return read('variables', key, expand)
|
|
203
|
+
if (/^user$/i.test(key)) return '用户'
|
|
204
|
+
// 未解析的嵌套引用交给官方出口清洗,不让扁平插值器截断它。
|
|
205
|
+
if (whole.slice(2, -2).includes('{{')) return whole
|
|
206
|
+
return interpolateVariables(whole, {}, session, undefined, `${sourceKey}:${path}`)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** ST 模板变量帧:只求值显式导入的模板,不决定任何官方插入点的执行顺序。 */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { renderStText } from './st-macros.mjs'
|
|
4
|
+
import { sessionVarsSnapshot } from './session-vars.mjs'
|
|
5
|
+
import { sessionEvents, isDelegated, matchesModel } from './shared.mjs'
|
|
6
|
+
import { stripUnresolvedRefs } from './interpolate.mjs'
|
|
7
|
+
import { isSuccessfulCompactionEnd } from './compaction-epoch.mjs'
|
|
8
|
+
|
|
9
|
+
function visible(config, agent) {
|
|
10
|
+
if (config.enabled === false) return false
|
|
11
|
+
const delegated = isDelegated(agent?.session)
|
|
12
|
+
if (config.audience === 'main' && delegated) return false
|
|
13
|
+
if (config.audience === 'subagent' && !delegated) return false
|
|
14
|
+
return matchesModel(config.modelScope, agent?.options?.model)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 历史与尚未入库的真实输入使用同一消息身份;插件注入消息不触发第二次赋值。 */
|
|
18
|
+
function generationKey(session, pending) {
|
|
19
|
+
const events = sessionEvents(session)
|
|
20
|
+
// 宿主先 assemble/pre-step,之后才 append step/start;不能用 step/start 切帧。
|
|
21
|
+
const boundary = events.findLast(event => event?.type === 'turn/start' || event?.type === 'step/end' || isSuccessfulCompactionEnd(event))
|
|
22
|
+
if (boundary) return JSON.stringify([boundary.type, boundary.seq, boundary.data?.turn, boundary.data?.step])
|
|
23
|
+
const seen = new Set()
|
|
24
|
+
const hash = createHash('sha256')
|
|
25
|
+
const add = (message, kind) => {
|
|
26
|
+
if (!message || message.source?.plugin || (message.source?.kind && message.source.kind !== 'user')) return
|
|
27
|
+
const value = JSON.stringify([kind, message.id ?? null, message.content ?? []])
|
|
28
|
+
if (seen.has(value)) return
|
|
29
|
+
seen.add(value)
|
|
30
|
+
hash.update(value)
|
|
31
|
+
}
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
if (event?.type === 'user/message' || event?.type === 'assistant/message') {
|
|
34
|
+
add(event.data?.message ?? event.data, event.type)
|
|
35
|
+
} else if (event?.type === 'tool/call') hash.update(JSON.stringify([event.type, event.seq, event.data?.id]))
|
|
36
|
+
}
|
|
37
|
+
for (const message of pending) add(message, `${message.role ?? 'user'}/message`)
|
|
38
|
+
return hash.digest('hex')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function attachStRenderers(configs) {
|
|
42
|
+
const claimed = new Set()
|
|
43
|
+
const templates = configs.filter(config => {
|
|
44
|
+
if (config.enabled === false) return false
|
|
45
|
+
if (config.group !== undefined && config.exclusive === true) {
|
|
46
|
+
if (claimed.has(config.group)) return false
|
|
47
|
+
claimed.add(config.group)
|
|
48
|
+
}
|
|
49
|
+
return config.params?.stMacros === true
|
|
50
|
+
})
|
|
51
|
+
if (!templates.length) return configs
|
|
52
|
+
// 状态只活在本次 mount 与会话对象的生命周期内;不持有会话 id 的无界全局 Map。
|
|
53
|
+
const sessions = new WeakMap()
|
|
54
|
+
const tokens = new WeakMap()
|
|
55
|
+
for (const target of templates) {
|
|
56
|
+
target.renderSt = (agent, messages = [], warn = () => {}, token, eligible) => {
|
|
57
|
+
// 官方 assembly 只拥有两个文本插入点;pre-step 的资格由执行器确认。
|
|
58
|
+
const approved = config => visible(config, agent) && (eligible === undefined
|
|
59
|
+
? (config.layer === 'system-section' || config.layer === 'runtime-context') && config.promotion === 'none'
|
|
60
|
+
: eligible.has(config.renderSt))
|
|
61
|
+
if (!approved(target)) return ''
|
|
62
|
+
const session = agent?.session
|
|
63
|
+
const cached = token && tokens.get(token)
|
|
64
|
+
if (cached) {
|
|
65
|
+
if (!cached.text.has(target)) cached.evaluate(target)
|
|
66
|
+
return cached.text.get(target) ?? ''
|
|
67
|
+
}
|
|
68
|
+
const manual = sessionVarsSnapshot(session)
|
|
69
|
+
const generation = generationKey(session, messages)
|
|
70
|
+
const key = `${generation}:${JSON.stringify(manual)}`
|
|
71
|
+
const previous = session && sessions.get(session)
|
|
72
|
+
let frame = previous
|
|
73
|
+
if (!frame || frame.key !== key) {
|
|
74
|
+
const initialLocal = { ...(previous?.generation === generation ? previous.initialLocal : previous?.local), ...manual }
|
|
75
|
+
for (const key of Object.keys(previous?.manual ?? {})) if (!Object.hasOwn(manual, key)) delete initialLocal[key]
|
|
76
|
+
const initialGlobal = { ...(previous?.generation === generation ? previous.initialGlobal : previous?.global) }
|
|
77
|
+
frame = { key, generation, manual, initialLocal, initialGlobal, local: { ...initialLocal }, global: { ...initialGlobal }, text: new Map() }
|
|
78
|
+
const evaluate = config => {
|
|
79
|
+
try {
|
|
80
|
+
const local = { ...frame.local }, global = { ...frame.global }
|
|
81
|
+
const text = renderStText(config.texts.join('\n\n'), { variables: config.variables, local, global, session, sourceId: config.id, warn })
|
|
82
|
+
const cleaned = stripUnresolvedRefs(text)
|
|
83
|
+
if (cleaned.stripped.length) warn(`prompt-config-engine: ST 模板 ${config.id} 有 ${cleaned.stripped.length} 处未支持引用,已移除`)
|
|
84
|
+
frame.local = local
|
|
85
|
+
frame.global = global
|
|
86
|
+
frame.text.set(config, cleaned.text)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
frame.text.set(config, '')
|
|
89
|
+
warn(`prompt-config-engine: ST 模板 ${config.id} 求值失败:${String(error?.message ?? error)}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
frame.evaluate = evaluate
|
|
93
|
+
if (session) sessions.set(session, frame)
|
|
94
|
+
}
|
|
95
|
+
// 同一帧允许后到的 pre-step 资格补入;已求值模板不重放副作用或随机宏。
|
|
96
|
+
for (const config of templates.filter(config => approved(config)
|
|
97
|
+
&& config.strategy === 'static' && config.dedupe === 'none' && !frame.text.has(config))
|
|
98
|
+
.sort((a, b) => a.order - b.order)) frame.evaluate(config)
|
|
99
|
+
if (token && typeof token === 'object') tokens.set(token, frame)
|
|
100
|
+
if (!frame.text.has(target)) frame.evaluate(target)
|
|
101
|
+
return frame.text.get(target) ?? ''
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return configs
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 引擎侧 TS 类型声明(运行期实现在 st-world-book.mjs;仅 src/ 侧导入需要)。 */
|
|
2
|
+
export interface WorldBookDiagnosticsSnapshot {
|
|
3
|
+
records: Array<{ id: string; stage: string; reason: string; [key: string]: unknown }>
|
|
4
|
+
truncated: boolean
|
|
5
|
+
step: number
|
|
6
|
+
/** false = 该会话尚未求值;true = 已求值(records 为空表示本次没有参与/入选条目)。 */
|
|
7
|
+
evaluated: boolean
|
|
8
|
+
}
|
|
9
|
+
export function lastWorldBookDiagnostics(session: unknown): WorldBookDiagnosticsSnapshot
|
|
10
|
+
export function selectStWorldBook(...args: unknown[]): Set<Record<string, unknown>> & { diagnostics?: { records: unknown[]; truncated: boolean } }
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/** ST 世界书选择器;只消费显式 ST 导入字段,不改变原生 world-book 约定。 */
|
|
2
|
+
import { createAnchorMatcher } from './anchor-match.mjs'
|
|
3
|
+
import { sessionEvents } from './shared.mjs'
|
|
4
|
+
import { interpolateVariables } from './interpolate.mjs'
|
|
5
|
+
|
|
6
|
+
/** 只扫描真实对话的可见文本,排除插件注入与思维块。 */
|
|
7
|
+
export function stChatMessages(session, pending = []) {
|
|
8
|
+
const result = [], ids = new Set()
|
|
9
|
+
const add = (message, role) => {
|
|
10
|
+
if (!message || !['user', 'assistant'].includes(role) || message.source?.plugin
|
|
11
|
+
|| (message.source?.kind && message.source.kind !== 'user')) return
|
|
12
|
+
if (message.id && ids.has(message.id)) return
|
|
13
|
+
if (message.id) ids.add(message.id)
|
|
14
|
+
const text = (Array.isArray(message.content) ? message.content : []).filter(block => block.type === 'text' || block.type === undefined).map(block => block.text ?? '').join('')
|
|
15
|
+
result.push({ id: message.id, role, text })
|
|
16
|
+
}
|
|
17
|
+
for (const event of sessionEvents(session)) {
|
|
18
|
+
if (event?.type === 'user/message' || event?.type === 'assistant/message') add(event.data?.message ?? event.data, event.type.split('/')[0])
|
|
19
|
+
}
|
|
20
|
+
for (const message of pending) add(message, message.role ?? 'user')
|
|
21
|
+
return result
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sessions = new WeakMap()
|
|
25
|
+
const matchers = new WeakMap()
|
|
26
|
+
/** 每个会话只保留最近一次选择的诊断快照(随会话对象 GC 释放)。 */
|
|
27
|
+
const lastRuns = new WeakMap()
|
|
28
|
+
const count = (value, fallback = 0) => Number.isSafeInteger(value) && value >= 0 ? Math.min(value, 1000) : fallback
|
|
29
|
+
|
|
30
|
+
/** 只读诊断上限:只截断观测记录,绝不改变入选、抽样与时间窗状态。 */
|
|
31
|
+
const DIAGNOSTIC_LIMIT = 200
|
|
32
|
+
|
|
33
|
+
/** 最近一次世界书选择诊断(只读快照;无记录时返回空集合,不触发任何求值)。
|
|
34
|
+
* `evaluated: false` 表示该会话还没求值过,与"已求值但本次为空"区分开。 */
|
|
35
|
+
export function lastWorldBookDiagnostics(session) {
|
|
36
|
+
return lastRuns.get(session) ?? { records: [], truncated: false, step: 0, evaluated: false }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
40
|
+
const entries = configs.filter(config => config.enabled !== false && config.strategy === 'world-book' && config.params?.stWorldBook)
|
|
41
|
+
// 本次选择的有界观测快照:`selection.diagnostics` 与会话最近快照引用**同一对象**,
|
|
42
|
+
// 因此 commit 阶段追加记录或把 truncated 置真都写回同一份事实(不复制布尔值,
|
|
43
|
+
// 否则 commit 越限时读取端仍看到 truncated=false)。
|
|
44
|
+
const snapshot = { records: [], truncated: false, step: 0, evaluated: true }
|
|
45
|
+
const note = (config, stage, reason, extra) => {
|
|
46
|
+
if (snapshot.records.length >= DIAGNOSTIC_LIMIT) { snapshot.truncated = true; return }
|
|
47
|
+
snapshot.records.push({ id: String(config.id ?? ''), stage, reason, ...extra })
|
|
48
|
+
}
|
|
49
|
+
const finish = selection => {
|
|
50
|
+
selection.diagnostics = snapshot
|
|
51
|
+
// 最近一次求值总是替换快照(含空集合):空结果不能被读成"本次又注入了旧条目"。
|
|
52
|
+
lastRuns.set(session, snapshot)
|
|
53
|
+
return selection
|
|
54
|
+
}
|
|
55
|
+
// 被显式禁用的 ST 条目:由本层负责报告,不由 UI 猜测。
|
|
56
|
+
for (const config of configs) {
|
|
57
|
+
if (config.enabled === false && config.strategy === 'world-book' && config.params?.stWorldBook) note(config, 'excluded', 'disabled')
|
|
58
|
+
}
|
|
59
|
+
if (!entries.length) return finish(new Set())
|
|
60
|
+
const chat = stChatMessages(session, messages)
|
|
61
|
+
snapshot.step = chat.length
|
|
62
|
+
let state = sessions.get(session)
|
|
63
|
+
if (!state) { state = new WeakMap(); sessions.set(session, state) }
|
|
64
|
+
const generation = JSON.stringify([chat.length, chat.at(-1)])
|
|
65
|
+
const selected = new Set(), failed = new Set(), occupied = new Set(), stickyEntries = new Set(), candidates = []
|
|
66
|
+
const updates = new Map()
|
|
67
|
+
selected.commit = config => {
|
|
68
|
+
if (!selected.has(config)) return
|
|
69
|
+
note(config, 'committed', 'injected')
|
|
70
|
+
if (updates.has(config)) state.set(config, updates.get(config))
|
|
71
|
+
}
|
|
72
|
+
const priority = [...entries].sort((a, b) => b.order - a.order)
|
|
73
|
+
const recursiveText = []
|
|
74
|
+
// ST 的延迟层级池(world-info.js:4753-4762):只收真值,true 归一为 1,升序去重;
|
|
75
|
+
// 初始化即取走最小层级,因此「延迟到第 N 层」的条目在首次递归 pass 就能解锁;
|
|
76
|
+
// 层级只增不减(:5131),已打开的层级对后续 pass 保持打开。
|
|
77
|
+
const delayLevels = [...new Set(entries
|
|
78
|
+
.map(config => config.params.stWorldBook.delayUntilRecursion)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map(value => value === true ? 1 : value))].sort((a, b) => a - b)
|
|
81
|
+
let delayLevel = delayLevels.shift() ?? 0
|
|
82
|
+
const hasRecursive = entries.some(config => config.params.stWorldBook.recursive === true)
|
|
83
|
+
/** 键匹配器(按 params 对象缓存;签名含插值后的键与匹配选项)。 */
|
|
84
|
+
const matcherOf = (config, material) => {
|
|
85
|
+
const p = config.params
|
|
86
|
+
const signature = JSON.stringify([material.keys, material.secondaryKeys, p.caseSensitive, p.wholeWords, p.useRegex])
|
|
87
|
+
let compiled = matchers.get(p)
|
|
88
|
+
if (compiled?.signature !== signature) {
|
|
89
|
+
compiled = { signature, matcher: createAnchorMatcher({ keys: material.keys, secondaryKeys: material.secondaryKeys, caseSensitive: p.caseSensitive === true, wholeWords: p.wholeWords === true, useRegex: p.useRegex, stWords: true }) }
|
|
90
|
+
matchers.set(p, compiled)
|
|
91
|
+
}
|
|
92
|
+
return compiled.matcher
|
|
93
|
+
}
|
|
94
|
+
// 每个继续的 pass 至少新增一条或消耗一个延迟层级;预算覆盖两种推进,仍是有限闭包。
|
|
95
|
+
const maxPasses = entries.length + delayLevels.length
|
|
96
|
+
for (let pass = 0; pass <= maxPasses; pass++) {
|
|
97
|
+
candidates.length = 0
|
|
98
|
+
// 每 pass 的扫描材料缓存:递归文本随 pass 变化,材料必须按 pass 重新构造;
|
|
99
|
+
// 材料只被匹配与评分读取,构造本身不改变入选结果。
|
|
100
|
+
const scanCache = new Map()
|
|
101
|
+
const scanOf = (config, st) => {
|
|
102
|
+
let material = scanCache.get(config)
|
|
103
|
+
if (material !== undefined) return material
|
|
104
|
+
const depth = count(st.scanDepth, 2)
|
|
105
|
+
const parts = depth === 0 ? [] : chat.slice(-depth).reverse().map(message => message.text)
|
|
106
|
+
// 角色字段扫描(ST globalScanData,world-info.js:294-320):每个开关只在对应变量
|
|
107
|
+
// 有值时并入扫描文本;creator_notes / depth_prompt 由导入期登记(未开启不并入,
|
|
108
|
+
// 既有触发面不变)。
|
|
109
|
+
for (const [flag, key] of [['matchCharacterDescription', 'description'], ['matchCharacterPersonality', 'personality'], ['matchScenario', 'scenario'], ['matchPersonaDescription', 'persona'], ['matchCreatorNotes', 'creator_notes'], ['matchCharacterDepthPrompt', 'depth_prompt']]) {
|
|
110
|
+
if (depth > 0 && st[flag] === true && typeof config.variables[key] === 'string') parts.push(config.variables[key])
|
|
111
|
+
}
|
|
112
|
+
if (pass > 0 && depth > 0) parts.push(...recursiveText)
|
|
113
|
+
const p = config.params
|
|
114
|
+
const keys = (Array.isArray(p.keys) ? p.keys : []).map(key => interpolateVariables(String(key), config.variables, session)).filter(Boolean)
|
|
115
|
+
// 空宏仍是一项未命中的副键:匹配器忽略空值,但 AND_ALL/NOT_ALL 的总数不能缩水。
|
|
116
|
+
const secondaryKeys = (Array.isArray(p.secondaryKeys) ? p.secondaryKeys : []).map(key => interpolateVariables(String(key), config.variables, session))
|
|
117
|
+
material = { depth, keys, secondaryKeys, text: parts.length ? '\x01' + parts.join('\n\x01') : '' }
|
|
118
|
+
scanCache.set(config, material)
|
|
119
|
+
return material
|
|
120
|
+
}
|
|
121
|
+
// ST getScore 等价(world-info.js:428-473):只统计命中数,否定逻辑(NOT_ALL/NOT_ANY)
|
|
122
|
+
// 不参与加分,主键为空记 0 分。
|
|
123
|
+
const scoreOf = (config, st) => {
|
|
124
|
+
const material = scanOf(config, st)
|
|
125
|
+
const match = matcherOf(config, material).scan(material.text)
|
|
126
|
+
if (material.keys.length === 0) return 0
|
|
127
|
+
if (material.secondaryKeys.length === 0) return match.primary
|
|
128
|
+
const logic = config.params.selectiveLogic ?? 0
|
|
129
|
+
if (logic === 0) return match.primary + match.secondary
|
|
130
|
+
if (logic === 3) return match.secondary === material.secondaryKeys.length ? match.primary + match.secondary : match.primary
|
|
131
|
+
return match.primary
|
|
132
|
+
}
|
|
133
|
+
for (const config of priority) {
|
|
134
|
+
if (selected.has(config) || failed.has(config)) continue
|
|
135
|
+
const p = config.params, st = p.stWorldBook
|
|
136
|
+
const previous = state.get(config)
|
|
137
|
+
const sticky = previous?.last !== undefined && count(st.sticky) > 0 && chat.length < previous.last + count(st.sticky)
|
|
138
|
+
if (sticky) { stickyEntries.add(config); note(config, 'candidate', 'sticky') }
|
|
139
|
+
if (chat.length < count(st.delay)) { note(config, 'excluded', 'delay', { delay: count(st.delay), messages: chat.length }); continue }
|
|
140
|
+
if (!sticky && previous?.last !== undefined && count(st.cooldown) > 0 && chat.length > previous.last
|
|
141
|
+
&& chat.length < previous.last + count(st.sticky) + count(st.cooldown)) { note(config, 'excluded', 'cooldown'); continue }
|
|
142
|
+
// ST 的延迟门控(world-info.js:4860-4868):非递归 pass 一律抑制(sticky 例外,
|
|
143
|
+
// constant 也不例外);递归 pass 中层级未到同样抑制。比较用条目原始值。
|
|
144
|
+
const delayUntil = st.delayUntilRecursion
|
|
145
|
+
if (!sticky && delayUntil && (pass === 0 || delayUntil > delayLevel)) {
|
|
146
|
+
note(config, 'excluded', 'delay-until-recursion', { delayUntilRecursion: delayUntil, level: delayLevel, pass })
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
// 递归 pass 的参与资格:excludeRecursion 对任何条目生效;其余条目仍要求自身声明
|
|
150
|
+
// recursive(延迟条目由层级池驱动,ST 的门控不要求全局 recursive)。
|
|
151
|
+
if (pass > 0 && (st.excludeRecursion === true || (!delayUntil && st.recursive !== true))) { note(config, 'excluded', 'recursion'); continue }
|
|
152
|
+
let active = sticky || p.constant === true
|
|
153
|
+
let activation = sticky ? 'sticky' : p.constant === true ? 'constant' : 'key-match'
|
|
154
|
+
if (!active) {
|
|
155
|
+
const material = scanOf(config, st)
|
|
156
|
+
try {
|
|
157
|
+
const match = matcherOf(config, material).scan(material.text)
|
|
158
|
+
active = match.primary > 0
|
|
159
|
+
const logic = p.selectiveLogic ?? 0
|
|
160
|
+
const detail = { scanDepth: material.depth, primary: match.primary, secondary: match.secondary, secondaryKeys: material.secondaryKeys.length, logic }
|
|
161
|
+
if (active && st.selective === true && material.secondaryKeys.length > 0) {
|
|
162
|
+
active = logic === 1 ? match.secondary < material.secondaryKeys.length : logic === 2 ? match.secondary === 0
|
|
163
|
+
: logic === 3 ? match.secondary === material.secondaryKeys.length : match.secondary > 0
|
|
164
|
+
if (!active) note(config, 'rejected', 'secondary-miss', detail)
|
|
165
|
+
} else if (!active) {
|
|
166
|
+
note(config, 'rejected', 'primary-miss', detail)
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
note(config, 'rejected', 'match-error', { message: String(error?.message ?? error).slice(0, 120) })
|
|
170
|
+
warn(`prompt-config-engine: 世界书 ${config.id} 匹配失败:${String(error?.message ?? error)}`)
|
|
171
|
+
failed.add(config)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!active) continue
|
|
176
|
+
const probability = typeof st.probability === 'number' && Number.isFinite(st.probability) ? Math.max(0, Math.min(100, st.probability)) : 100
|
|
177
|
+
const roll = previous?.generation === generation ? previous.roll : Math.random() * 100
|
|
178
|
+
state.set(config, { ...previous, generation, roll })
|
|
179
|
+
if (!sticky && st.useProbability !== false && (probability <= 0 || roll >= probability)) {
|
|
180
|
+
note(config, 'rejected', 'probability', { probability, roll })
|
|
181
|
+
failed.add(config)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
note(config, 'candidate', activation, { probability, useProbability: st.useProbability !== false })
|
|
185
|
+
candidates.push(config)
|
|
186
|
+
}
|
|
187
|
+
if (!candidates.length && !delayLevels.length) break
|
|
188
|
+
const groups = new Map()
|
|
189
|
+
for (const config of candidates) {
|
|
190
|
+
const names = String(config.params.stWorldBook.group ?? '').split(',').map(name => name.trim()).filter(Boolean)
|
|
191
|
+
if (!names.length) { selected.add(config); note(config, 'selected', 'ungrouped'); continue }
|
|
192
|
+
if (names.some(name => occupied.has(name))) { note(config, 'rejected', 'group-occupied'); failed.add(config); continue }
|
|
193
|
+
for (const name of names) { const members = groups.get(name) ?? []; members.push(config); groups.set(name, members) }
|
|
194
|
+
}
|
|
195
|
+
for (const [name, members] of groups) {
|
|
196
|
+
if (occupied.has(name)) continue
|
|
197
|
+
let remaining = members.filter(config => !failed.has(config) && !String(config.params.stWorldBook.group).split(',').some(group => occupied.has(group.trim())))
|
|
198
|
+
if (!remaining.length) continue
|
|
199
|
+
const sticky = remaining.filter(config => stickyEntries.has(config))
|
|
200
|
+
// 组内评分(ST filterGroupsByScoring 5292-5328):无全局开关时,只要组内存在显式
|
|
201
|
+
// 开启 useGroupScoring 的条目就整组参与评分;组内有 sticky 时整组跳过(5300-5305)。
|
|
202
|
+
// 只有开启评分的条目会被淘汰(严格小于最高分);未开启者不被淘汰,但其分数计入最高分。
|
|
203
|
+
const scoreOut = new Set()
|
|
204
|
+
if (!sticky.length && remaining.some(config => config.params.stWorldBook.useGroupScoring === true)) {
|
|
205
|
+
let scores
|
|
206
|
+
try {
|
|
207
|
+
scores = remaining.map(config => scoreOf(config, config.params.stWorldBook))
|
|
208
|
+
} catch (error) {
|
|
209
|
+
scores = undefined
|
|
210
|
+
warn(`prompt-config-engine: 世界书分组评分失败:${String(error?.message ?? error)}`)
|
|
211
|
+
}
|
|
212
|
+
if (scores !== undefined) {
|
|
213
|
+
const maxScore = Math.max(...scores)
|
|
214
|
+
const survivors = []
|
|
215
|
+
for (const [index, config] of remaining.entries()) {
|
|
216
|
+
if (config.params.stWorldBook.useGroupScoring !== true || scores[index] >= maxScore) { survivors.push(config); continue }
|
|
217
|
+
note(config, 'rejected', 'group-score-lost', { group: name, score: scores[index], maxScore })
|
|
218
|
+
scoreOut.add(config)
|
|
219
|
+
failed.add(config)
|
|
220
|
+
}
|
|
221
|
+
remaining = survivors
|
|
222
|
+
if (!remaining.length) continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const overriding = sticky.length ? sticky : remaining.filter(config => config.params.stWorldBook.groupOverride === true)
|
|
226
|
+
const available = overriding.length ? overriding : remaining
|
|
227
|
+
const weight = config => typeof config.params.stWorldBook.groupWeight === 'number' ? Math.max(0, config.params.stWorldBook.groupWeight) : 100
|
|
228
|
+
let choice = available[0]
|
|
229
|
+
if (!overriding.length) {
|
|
230
|
+
let roll = (state.get(choice).roll / 100) * available.reduce((sum, config) => sum + weight(config), 0)
|
|
231
|
+
for (const config of available) { roll -= weight(config); if (roll < 0) { choice = config; break } }
|
|
232
|
+
}
|
|
233
|
+
selected.add(choice)
|
|
234
|
+
note(choice, 'selected', 'group-winner', { group: String(choice.params.stWorldBook.group ?? '') })
|
|
235
|
+
for (const group of String(choice.params.stWorldBook.group).split(',').map(value => value.trim()).filter(Boolean)) occupied.add(group)
|
|
236
|
+
for (const config of members) {
|
|
237
|
+
// 已按评分淘汰的成员只记 group-score-lost:它没有参与最后的组内竞争,
|
|
238
|
+
// 再记一条 group-lost 会让诊断给出两个互相矛盾的原因。
|
|
239
|
+
if (config === choice || scoreOut.has(config)) continue
|
|
240
|
+
note(config, 'rejected', 'group-lost', { group: name })
|
|
241
|
+
failed.add(config)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const added = candidates.filter(config => selected.has(config))
|
|
245
|
+
if (!added.length && !delayLevels.length) break
|
|
246
|
+
for (const config of added) {
|
|
247
|
+
const previous = state.get(config)
|
|
248
|
+
// sticky 期间的重发不延长有效期;新命中才开始下一段窗口。
|
|
249
|
+
if (previous.last === undefined || chat.length >= previous.last + count(config.params.stWorldBook.sticky)) updates.set(config, { ...previous, last: chat.length })
|
|
250
|
+
}
|
|
251
|
+
// 递归驱动优先于延迟层级(ST world-info.js:5097-5133):有新正文可递归时层级保持不变;
|
|
252
|
+
// 没有新正文而层级池仍有剩余时打开下一层继续扫描;两者都没有才结束。
|
|
253
|
+
const recursiveAdds = hasRecursive ? added.filter(config => config.params.stWorldBook.preventRecursion !== true) : []
|
|
254
|
+
if (recursiveAdds.length) recursiveText.push(...recursiveAdds.map(config => config.texts.join('\n')))
|
|
255
|
+
else if (delayLevels.length) delayLevel = delayLevels.shift()
|
|
256
|
+
else break
|
|
257
|
+
}
|
|
258
|
+
return finish(selected)
|
|
259
|
+
}
|