dsh-plugin-prompt-tool 0.4.2 → 0.5.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 +125 -222
- package/engine/compaction-epoch.mjs +139 -0
- package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
- package/engine/compositions/library/compaction.yml +52 -0
- package/engine/compositions/library/context-gate.yml +40 -0
- package/engine/compositions/library/custom-bash.yml +14 -0
- package/engine/compositions/library/delegation.yml +85 -0
- package/engine/compositions/library/official-agent-instructions.yml +17 -0
- package/engine/compositions/library/official-persistent-shell.yml +56 -0
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/official-tool-bash.yml +6 -0
- package/engine/compositions/library/official-tool-cordis.yml +13 -0
- package/engine/compositions/library/official-tool-presentation.yml +7 -0
- package/engine/compositions/library/official-tool-skill.yml +14 -0
- package/engine/compositions/library/persistent-shell.yml +36 -0
- package/engine/compositions/library/persona.yml +9 -0
- package/engine/compositions/library/planning.yml +41 -0
- package/engine/compositions/library/prompt-config-engine.yml +16 -0
- package/engine/compositions/library/router-first-turn.yml +9 -0
- package/engine/compositions/library/run-code-env.yml +15 -0
- package/engine/compositions/library/skill-filesystem.yml +13 -0
- package/engine/compositions/library/skill-search.yml +14 -0
- package/engine/compositions/library/str-replace-editor.yml +10 -0
- package/engine/compositions/library/tool-ask-user.yml +8 -0
- package/engine/compositions/library/tool-bash.yml +16 -0
- package/engine/compositions/library/tool-bootstrap.yml +16 -0
- package/engine/compositions/library/tool-filter.yml +12 -0
- package/engine/compositions/library/tool-fs-search.yml +18 -0
- package/engine/compositions/library/tool-fs.yml +10 -0
- package/engine/compositions/library/tool-goal.yml +19 -0
- package/engine/compositions/library/tool-jobs.yml +23 -0
- package/engine/compositions/library/tool-pwsh.yml +12 -0
- package/engine/compositions/library/tool-todo.yml +11 -0
- package/engine/compositions/library/tool-web.yml +9 -0
- package/engine/compositions/source/local/context-gate.yml +37 -0
- package/engine/compositions/source/local/custom-bash.yml +11 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
- package/engine/compositions/source/local/router-first-turn.yml +5 -0
- package/engine/compositions/source/local/run-code-env.yml +12 -0
- package/engine/compositions/source/local/skill-search.yml +11 -0
- package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
- package/engine/context-gate.mjs +305 -0
- package/{preset → engine}/custom-bash.mjs +243 -243
- package/engine/executor.mjs +271 -0
- package/engine/fillers.mjs +273 -0
- package/engine/layers.mjs +220 -0
- package/engine/prompt-config-engine.mjs +49 -0
- package/{preset → engine}/router-first-turn.mjs +85 -73
- package/engine/run-code-env.mjs +208 -0
- package/engine/schema.mjs +294 -0
- package/{preset → engine}/shared.mjs +54 -0
- package/engine/strategies.mjs +223 -0
- package/{preset → engine}/tool-bootstrap.mjs +350 -282
- package/engine/tool-filter.mjs +80 -0
- package/engine/vendor/yaml/LICENSE +13 -0
- package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
- package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
- package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
- package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
- package/engine/vendor/yaml/dist/compose/composer.js +219 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
- package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
- package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
- package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
- package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
- package/engine/vendor/yaml/dist/doc/Document.js +335 -0
- package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
- package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
- package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
- package/engine/vendor/yaml/dist/doc/directives.js +176 -0
- package/engine/vendor/yaml/dist/errors.js +57 -0
- package/engine/vendor/yaml/dist/index.js +17 -0
- package/engine/vendor/yaml/dist/log.js +11 -0
- package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
- package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
- package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
- package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
- package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
- package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
- package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
- package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
- package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
- package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
- package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
- package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
- package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
- package/engine/vendor/yaml/dist/parse/cst.js +98 -0
- package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
- package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
- package/engine/vendor/yaml/dist/parse/parser.js +975 -0
- package/engine/vendor/yaml/dist/public-api.js +102 -0
- package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
- package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
- package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
- package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
- package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
- package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
- package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
- package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
- package/engine/vendor/yaml/dist/schema/tags.js +96 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
- package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
- package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
- package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
- package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
- package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
- package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
- package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
- package/engine/vendor/yaml/dist/util.js +11 -0
- package/engine/vendor/yaml/dist/visit.js +233 -0
- package/engine/vendor/yaml/index.js +5 -0
- package/engine/vendor/yaml/package.json +11 -0
- package/lib/client.js +4421 -781
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +504 -51
- package/lib/index.mjs +3871 -731
- package/lib/preset-core.d.mts +7 -37
- package/lib/preset-core.mjs +44 -283
- package/lib/prompt-configs-BcP6miu9.d.mts +102 -0
- package/lib/prompt-configs-k7wuZs0C.mjs +719 -0
- package/package.json +104 -98
- package/preset/anchored/preset.yml +344 -0
- package/preset/creative/preset.yml +62 -0
- package/preset/custom/preset.yml +13 -0
- package/preset/liangshen/preset.yml +66 -0
- package/preset/minimal/preset.yml +41 -0
- package/preset/ptc/preset.yml +50 -0
- package/preset/standard/preset.yml +49 -0
- package/skills/manifest.json +7 -0
- package/skills/sandboxmod/SKILL.md +49 -49
- package/skills/web ui/SKILL.md +42 -0
- package/templates/10-pre-step.yml +28 -0
- package/templates/11-merged-a.yml +11 -0
- package/templates/13-anchor.yml +19 -0
- package/templates/14-first-turn-anchor.yml +27 -0
- package/templates/15-guide-auto.yml +25 -0
- package/templates/16-custom-fallback.yml +21 -0
- package/templates/17-instruction-hint.yml +23 -0
- package/templates/18-placeholder-env-facts.yml +11 -0
- package/templates/19-placeholder-skill-catalog.yml +19 -0
- package/templates/20-system-section.yml +14 -0
- package/templates/30-runtime-context.yml +10 -0
- package/templates/31-runtime-context-placeholder.yml +18 -0
- package/templates/40-agent-request.yml +11 -0
- package/templates/50-llm-stream.yml +9 -0
- package/templates/60-tool-pipeline.yml +12 -0
- package/AGENTS.md +0 -4
- package/plan.md +0 -312
- package/preset/agent.cordis.yml +0 -443
- package/preset/compaction-epoch.mjs +0 -81
- package/preset/context-gate.mjs +0 -165
- package/preset/instruction-hint.mjs +0 -217
- package/preset/near-anchor.mjs +0 -101
- package/preset/preset.yml +0 -3
- package/preset/prompt-injector.mjs +0 -112
- package/preset/router-guide.mjs +0 -79
- package/preset.md +0 -115
- package/upstream/dsh-anchored-standard/LICENSE +0 -22
- package/upstream/dsh-anchored-standard/NOTICE +0 -19
- package/upstream/dsh-anchored-standard/REVISION +0 -1
- package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
- package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
- package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
- package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
- package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
- package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
- package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
- package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
- /package/{preset → engine}/skill-search.mjs +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema — prompt-config-engine 的提示词配置加载、归一化与权威校验。
|
|
3
|
+
* 只负责"配置长什么样";执行语义见 executor.mjs,内容策略见 strategies.mjs。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
7
|
+
import { parse as parseYaml } from './vendor/yaml/index.js'
|
|
8
|
+
import { bindResolver } from './strategies.mjs'
|
|
9
|
+
|
|
10
|
+
const name = 'prompt-config-engine'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 内容模板加载:提示词配置可声明 templateFile,由外部 yml / json / 纯文本模板提供内容,
|
|
14
|
+
* 引擎只负责读取与注入(内容与执行分离)。
|
|
15
|
+
* - .json:解析为 { text, id?, role?, content?, source? },或纯字符串;
|
|
16
|
+
* - .yml/.yaml:用 vendored yaml 完整解析(顶层对象取 text 等字段);
|
|
17
|
+
* - 其他扩展名:整个文件内容作为 text。
|
|
18
|
+
*/
|
|
19
|
+
function readTextFile(url) {
|
|
20
|
+
return readFileSync(url, 'utf8')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadTemplate(file) {
|
|
24
|
+
if (typeof file !== 'string' || file.length === 0) return undefined
|
|
25
|
+
let raw
|
|
26
|
+
try {
|
|
27
|
+
raw = readTextFile(new URL(file, import.meta.url))
|
|
28
|
+
} catch {
|
|
29
|
+
throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} is not readable`)
|
|
30
|
+
}
|
|
31
|
+
if (/\.json$/i.test(file)) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw)
|
|
34
|
+
return typeof parsed === 'string' ? { text: parsed } : parsed
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} is not valid JSON: ${String(error?.message ?? error)}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (/\.ya?ml$/i.test(file)) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = parseYaml(raw)
|
|
42
|
+
return typeof parsed === 'string' ? { text: parsed } : parsed
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new TypeError(`${name}: templateFile ${JSON.stringify(file)} is not valid YAML: ${String(error?.message ?? error)}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { text: raw }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 完整 YAML 解析(vendored yaml 包):提示词配置文件直接使用标准 YAML。
|
|
52
|
+
* 支持缩进 map、列表、block scalar、行尾注释、引号转义等全部语法。
|
|
53
|
+
*/
|
|
54
|
+
export function parsePromptConfigYaml(raw) {
|
|
55
|
+
const parsed = parseYaml(raw)
|
|
56
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
57
|
+
throw new TypeError(`${name}: prompt config yaml must contain a single object`)
|
|
58
|
+
}
|
|
59
|
+
return parsed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 从提示词配置模块目录加载全部提示词配置描述:按文件名排序扫描 *.yml / *.yaml / *.json。
|
|
64
|
+
* 文件名用数字前缀表达引擎执行顺序(00-…、10-…)。
|
|
65
|
+
*/
|
|
66
|
+
export function loadPromptConfigFiles(dirUrl) {
|
|
67
|
+
let entries
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(dirUrl, { withFileTypes: true })
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new TypeError(`${name}: configsDir ${String(dirUrl)} is not readable: ${String(error?.message ?? error)}`)
|
|
72
|
+
}
|
|
73
|
+
const specs = []
|
|
74
|
+
const files = entries
|
|
75
|
+
.filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
|
|
76
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
77
|
+
for (const entry of files) {
|
|
78
|
+
const url = new URL(entry.name, dirUrl)
|
|
79
|
+
const raw = readFileSync(url, 'utf8')
|
|
80
|
+
if (/\.json$/i.test(entry.name)) {
|
|
81
|
+
specs.push(JSON.parse(raw))
|
|
82
|
+
} else {
|
|
83
|
+
specs.push(parsePromptConfigYaml(raw))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return specs
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const KNOWN_STRATEGIES = new Set(['static', 'placeholder', 'instruction-hint', 'first-turn-anchor', 'guide-auto', 'custom-fallback', 'world-book'])
|
|
90
|
+
export const KNOWN_SLOT_KINDS = new Set(['ordered', 'anchor'])
|
|
91
|
+
export const KNOWN_LAYERS = new Set(['pre-step', 'system-section', 'runtime-context', 'agent-request', 'llm-stream', 'tool-pipeline'])
|
|
92
|
+
export const KNOWN_POSITIONS = new Set(['after-user', 'before-all', 'after-all'])
|
|
93
|
+
export const KNOWN_DEDUPES = new Set(['session', 'batch', 'none'])
|
|
94
|
+
export const KNOWN_PROMOTIONS = new Set(['none', 'main', 'include-subagents'])
|
|
95
|
+
/** audience 专用标记:缺省(null/省略)= 公用(主会话+子代理通用);main=仅主会话;subagent=仅子代理。 */
|
|
96
|
+
export const KNOWN_AUDIENCES = new Set(['main', 'subagent'])
|
|
97
|
+
export const KNOWN_MERGE_MODES = new Set(['separate', 'merged'])
|
|
98
|
+
export const KNOWN_MODEL_SCOPES = new Set(['all', 'pro', 'flash'])
|
|
99
|
+
export const KNOWN_ROLES = new Set(['user', 'assistant'])
|
|
100
|
+
export const KNOWN_FILLS = new Set(['instruction-hint', 'env-facts', 'skill-catalog'])
|
|
101
|
+
|
|
102
|
+
/** 层能力矩阵:每个字段只在对应注入层生效。客户端表单据此动态渲染。 */
|
|
103
|
+
export const LAYER_FIELD_POLICIES = {
|
|
104
|
+
'pre-step': { position: true, dedupe: true, promotion: true, audience: true, modelScope: true, merge: true, order: true, role: true, placeholder: true },
|
|
105
|
+
'system-section': { position: false, dedupe: false, promotion: false, audience: false, modelScope: false, merge: true, order: true, role: false, placeholder: false },
|
|
106
|
+
'runtime-context': { position: false, dedupe: false, promotion: false, audience: false, modelScope: false, merge: true, order: true, role: false, placeholder: true },
|
|
107
|
+
'agent-request': { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false },
|
|
108
|
+
'llm-stream': { position: false, dedupe: false, promotion: false, audience: false, modelScope: true, merge: false, order: true, role: false, placeholder: false },
|
|
109
|
+
'tool-pipeline': { position: false, dedupe: false, promotion: false, audience: true, modelScope: true, merge: false, order: true, role: false, placeholder: false },
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 层显示名与说明:由引擎统一下发,客户端不再各自维护。 */
|
|
113
|
+
export const LAYER_LABELS = {
|
|
114
|
+
'pre-step': { title: '消息批层', detail: '官方默认层:agent/pre-step 消息批。支持 position / dedupe / promotion / audience / mergeMode 与文本插值。' },
|
|
115
|
+
'system-section': { title: '系统段层', detail: 'system-section 静态层:注册即全局,由 order 与 params.complete / sectionName 控制。' },
|
|
116
|
+
'runtime-context': { title: '运行上下文', detail: 'runtime-context 层:static 按 order 注册,placeholder 单条生效,由 params.contextName 控制。' },
|
|
117
|
+
'agent-request': { title: '调用配置层', detail: 'agent-request 层:按 order 注册,params.patch 改写请求配置。' },
|
|
118
|
+
'llm-stream': { title: '模型流层', detail: 'llm/stream 层:按 order 注册,params.mode = pass | replace。' },
|
|
119
|
+
'tool-pipeline': { title: '工具管线层', detail: 'tools/* 层:按 order 注册,params.toolNames 与 preDecision / postAction 控制。' },
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 引擎能力矩阵:作为 /meta 的唯一数据源,客户端表单据此动态渲染。 */
|
|
123
|
+
export function getEngineMeta() {
|
|
124
|
+
return {
|
|
125
|
+
layers: [...KNOWN_LAYERS].sort(),
|
|
126
|
+
strategies: [...KNOWN_STRATEGIES].sort(),
|
|
127
|
+
slotKinds: [...KNOWN_SLOT_KINDS].sort(),
|
|
128
|
+
positions: [...KNOWN_POSITIONS].sort(),
|
|
129
|
+
dedupes: [...KNOWN_DEDUPES].sort(),
|
|
130
|
+
promotions: [...KNOWN_PROMOTIONS].sort(),
|
|
131
|
+
audienceModes: [...KNOWN_AUDIENCES].sort(),
|
|
132
|
+
modelScopes: [...KNOWN_MODEL_SCOPES].sort(),
|
|
133
|
+
roles: [...KNOWN_ROLES].sort(),
|
|
134
|
+
mergeModes: [...KNOWN_MERGE_MODES].sort(),
|
|
135
|
+
fills: [...KNOWN_FILLS].sort(),
|
|
136
|
+
layerFieldPolicies: LAYER_FIELD_POLICIES,
|
|
137
|
+
layerLabels: LAYER_LABELS,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 从 YAML 提示词配置描述构造运行时提示词配置。配置错误必须在挂载时暴露(fail loud)。 */
|
|
142
|
+
export function createPromptConfigs(specs, options = {}) {
|
|
143
|
+
if (specs === undefined) return []
|
|
144
|
+
if (!Array.isArray(specs)) throw new TypeError(`${name}: config.configs must be an array`)
|
|
145
|
+
const configs = specs.map((spec, index) => {
|
|
146
|
+
const label = `configs[${index}]`
|
|
147
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
148
|
+
throw new TypeError(`${name}: ${label} must be an object`)
|
|
149
|
+
}
|
|
150
|
+
if (typeof spec.id !== 'string' || spec.id.length === 0) {
|
|
151
|
+
throw new TypeError(`${name}: ${label}.id must be a non-empty string`)
|
|
152
|
+
}
|
|
153
|
+
const rawStrategy = spec.strategy ?? 'static'
|
|
154
|
+
const strategy = rawStrategy
|
|
155
|
+
if (!KNOWN_STRATEGIES.has(strategy)) {
|
|
156
|
+
// 模板专属策略:声明了 strategyDir 时由 strategies.bindResolver 懒加载,
|
|
157
|
+
// 否则视为未知策略 fail loud。
|
|
158
|
+
if (typeof options.strategyDir !== 'string' || options.strategyDir.length === 0) {
|
|
159
|
+
throw new TypeError(`${name}: ${label} unknown strategy ${JSON.stringify(strategy)}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const configKind = spec.configKind ?? 'ordered'
|
|
163
|
+
if (!KNOWN_SLOT_KINDS.has(configKind)) {
|
|
164
|
+
throw new TypeError(`${name}: ${label} unknown configKind ${JSON.stringify(configKind)}`)
|
|
165
|
+
}
|
|
166
|
+
const layer = spec.layer ?? 'pre-step'
|
|
167
|
+
if (!KNOWN_LAYERS.has(layer)) {
|
|
168
|
+
throw new TypeError(`${name}: ${label} unknown layer ${JSON.stringify(layer)} — known layers: ${[...KNOWN_LAYERS].sort().join(', ')}`)
|
|
169
|
+
}
|
|
170
|
+
// 层能力矩阵同时是引擎校验源:矩阵标记 false 的字段在对应层不生效,
|
|
171
|
+
// 显式提供时 fail loud(UI 表单已按矩阵隐藏,此处兜底手写配置)。
|
|
172
|
+
const restricted = ['position', 'dedupe', 'promotion', 'audience', 'modelScope', 'merge', 'role']
|
|
173
|
+
.filter((field) => LAYER_FIELD_POLICIES[layer][field] === false && spec[field === 'merge' ? 'mergeMode' : field] != null)
|
|
174
|
+
if (restricted.length > 0) {
|
|
175
|
+
throw new TypeError(`${name}: ${label} layer ${JSON.stringify(layer)} does not support field(s): ${restricted.join(', ')}`)
|
|
176
|
+
}
|
|
177
|
+
const position = spec.position ?? 'after-user'
|
|
178
|
+
if (!KNOWN_POSITIONS.has(position)) {
|
|
179
|
+
throw new TypeError(`${name}: ${label} unknown position ${JSON.stringify(position)}`)
|
|
180
|
+
}
|
|
181
|
+
const dedupe = spec.dedupe ?? 'none'
|
|
182
|
+
if (!KNOWN_DEDUPES.has(dedupe)) {
|
|
183
|
+
throw new TypeError(`${name}: ${label} unknown dedupe ${JSON.stringify(dedupe)}`)
|
|
184
|
+
}
|
|
185
|
+
const promotion = spec.promotion ?? 'none'
|
|
186
|
+
if (!KNOWN_PROMOTIONS.has(promotion)) {
|
|
187
|
+
throw new TypeError(`${name}: ${label} unknown promotion ${JSON.stringify(promotion)}`)
|
|
188
|
+
}
|
|
189
|
+
const audience = spec.audience
|
|
190
|
+
if (audience !== undefined && audience !== null && !KNOWN_AUDIENCES.has(audience)) {
|
|
191
|
+
throw new TypeError(`${name}: ${label} unknown audience ${JSON.stringify(audience)}`)
|
|
192
|
+
}
|
|
193
|
+
const modelScope = spec.modelScope ?? 'all'
|
|
194
|
+
if (!KNOWN_MODEL_SCOPES.has(modelScope)) {
|
|
195
|
+
throw new TypeError(`${name}: ${label} unknown modelScope ${JSON.stringify(modelScope)}`)
|
|
196
|
+
}
|
|
197
|
+
const role = spec.role ?? 'user'
|
|
198
|
+
if (!KNOWN_ROLES.has(role)) {
|
|
199
|
+
throw new TypeError(`${name}: ${label} unknown role ${JSON.stringify(role)}`)
|
|
200
|
+
}
|
|
201
|
+
// identity 仅支持 plugin 命名空间(kind 模式与 sourceKind 重复,已归一)。
|
|
202
|
+
const identity = spec.identity ?? { field: 'plugin', value: spec.id }
|
|
203
|
+
if (identity === null || typeof identity !== 'object' || Array.isArray(identity)
|
|
204
|
+
|| identity.field !== 'plugin' || typeof identity.value !== 'string' || identity.value.length === 0) {
|
|
205
|
+
throw new TypeError(`${name}: ${label}.identity must be { field: 'plugin', value: string }`)
|
|
206
|
+
}
|
|
207
|
+
const order = spec.order ?? 0
|
|
208
|
+
if (typeof order !== 'number' || !Number.isFinite(order)) {
|
|
209
|
+
throw new TypeError(`${name}: ${label}.order must be a finite number`)
|
|
210
|
+
}
|
|
211
|
+
if (spec.group !== undefined && (typeof spec.group !== 'string' || spec.group.length === 0)) {
|
|
212
|
+
throw new TypeError(`${name}: ${label}.group must be a non-empty string when present`)
|
|
213
|
+
}
|
|
214
|
+
if (spec.exclusive !== undefined && typeof spec.exclusive !== 'boolean') {
|
|
215
|
+
throw new TypeError(`${name}: ${label}.exclusive must be a boolean when present`)
|
|
216
|
+
}
|
|
217
|
+
if (spec.name !== undefined && (typeof spec.name !== 'string' || spec.name.length === 0)) {
|
|
218
|
+
throw new TypeError(`${name}: ${label}.name must be a non-empty string when present`)
|
|
219
|
+
}
|
|
220
|
+
if (spec.variables !== undefined && (spec.variables === null || typeof spec.variables !== 'object' || Array.isArray(spec.variables))) {
|
|
221
|
+
throw new TypeError(`${name}: ${label}.variables must be an object when present`)
|
|
222
|
+
}
|
|
223
|
+
if (spec.texts !== undefined && (!Array.isArray(spec.texts) || spec.texts.some((item) => typeof item !== 'string'))) {
|
|
224
|
+
throw new TypeError(`${name}: ${label}.texts must be an array of strings when present`)
|
|
225
|
+
}
|
|
226
|
+
const mergeMode = spec.mergeMode ?? 'separate'
|
|
227
|
+
if (!KNOWN_MERGE_MODES.has(mergeMode)) {
|
|
228
|
+
throw new TypeError(`${name}: ${label} unknown mergeMode ${JSON.stringify(mergeMode)}`)
|
|
229
|
+
}
|
|
230
|
+
if (strategy === 'placeholder' && layer !== 'pre-step' && layer !== 'runtime-context') {
|
|
231
|
+
throw new TypeError(`${name}: ${label} strategy=placeholder supports layer pre-step or runtime-context only, got ${JSON.stringify(layer)}`)
|
|
232
|
+
}
|
|
233
|
+
let fill
|
|
234
|
+
if (strategy === 'placeholder') {
|
|
235
|
+
fill = typeof spec.fill === 'string' && spec.fill.length > 0 ? spec.fill : undefined
|
|
236
|
+
if (fill === undefined || !KNOWN_FILLS.has(fill)) {
|
|
237
|
+
throw new TypeError(`${name}: ${label} strategy=placeholder requires fill in [${[...KNOWN_FILLS].sort().join(', ')}]`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const template = loadTemplate(spec.templateFile)
|
|
241
|
+
const templatePatch = template !== null && typeof template === 'object'
|
|
242
|
+
? { id: template.id, role: template.role, content: template.content, source: template.source }
|
|
243
|
+
: undefined
|
|
244
|
+
const config = {
|
|
245
|
+
id: spec.id,
|
|
246
|
+
name: typeof spec.name === 'string' ? spec.name : spec.id,
|
|
247
|
+
enabled: spec.enabled !== false,
|
|
248
|
+
strategy,
|
|
249
|
+
configKind,
|
|
250
|
+
layer,
|
|
251
|
+
group: typeof spec.group === 'string' ? spec.group : undefined,
|
|
252
|
+
exclusive: spec.exclusive === true,
|
|
253
|
+
order,
|
|
254
|
+
role,
|
|
255
|
+
fill,
|
|
256
|
+
position,
|
|
257
|
+
dedupe,
|
|
258
|
+
promotion,
|
|
259
|
+
audience,
|
|
260
|
+
modelScope,
|
|
261
|
+
sourceKind: typeof spec.sourceKind === 'string' && spec.sourceKind.length > 0 ? spec.sourceKind : spec.id,
|
|
262
|
+
form: typeof spec.form === 'string' ? spec.form : 'notice',
|
|
263
|
+
summary: typeof spec.summary === 'string' ? spec.summary : '',
|
|
264
|
+
identity,
|
|
265
|
+
// text/texts 统一:text 为单块便捷写法,运行时与渲染只消费 texts。
|
|
266
|
+
texts: (() => {
|
|
267
|
+
const specText = typeof spec.text === 'string' && spec.text.length > 0 ? spec.text : undefined
|
|
268
|
+
const specTexts = Array.isArray(spec.texts) ? spec.texts.filter((item) => typeof item === 'string' && item.length > 0) : []
|
|
269
|
+
const templateText = typeof template?.text === 'string' ? template.text : ''
|
|
270
|
+
return specText !== undefined || specTexts.length > 0
|
|
271
|
+
? [...(specText !== undefined ? [specText] : []), ...specTexts]
|
|
272
|
+
: (templateText.length > 0 ? [templateText] : [])
|
|
273
|
+
})(),
|
|
274
|
+
mergeMode,
|
|
275
|
+
variables: spec.variables !== null && typeof spec.variables === 'object' && !Array.isArray(spec.variables) ? spec.variables : {},
|
|
276
|
+
templatePatch,
|
|
277
|
+
params: spec.params !== null && typeof spec.params === 'object' && !Array.isArray(spec.params) ? spec.params : {},
|
|
278
|
+
}
|
|
279
|
+
config.resolve = bindResolver(config, options.strategyDir)
|
|
280
|
+
return config
|
|
281
|
+
})
|
|
282
|
+
// 排序契约:anchor 提示词配置保持模块文件相对顺序(固定锚点),ordered 提示词配置按 order
|
|
283
|
+
// 稳定升序排在其后。默认 order=0 时等价于文件顺序。
|
|
284
|
+
return configs
|
|
285
|
+
.map((config, fileOrder) => ({ config, fileOrder }))
|
|
286
|
+
.sort((a, b) => {
|
|
287
|
+
const aAnchor = a.config.configKind === 'anchor' ? 0 : 1
|
|
288
|
+
const bAnchor = b.config.configKind === 'anchor' ? 0 : 1
|
|
289
|
+
if (aAnchor !== bAnchor) return aAnchor - bAnchor
|
|
290
|
+
if (aAnchor === 1 && a.config.order !== b.config.order) return a.config.order - b.config.order
|
|
291
|
+
return a.fileOrder - b.fileOrder
|
|
292
|
+
})
|
|
293
|
+
.map(({ config }) => config)
|
|
294
|
+
}
|
|
@@ -81,3 +81,57 @@ export function isDelegated(session) {
|
|
|
81
81
|
export function isFlashModel(modelId) {
|
|
82
82
|
return typeof modelId === 'string' && /flash/i.test(modelId)
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/** 模型范围过滤:flash=仅 Flash 家族模型;pro=仅非 Flash;all=全部。 */
|
|
86
|
+
export function matchesModel(scope, model) {
|
|
87
|
+
if (scope === 'all') return true
|
|
88
|
+
const isFlash = isFlashModel(model)
|
|
89
|
+
return scope === 'flash' ? isFlash : !isFlash
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 逗号分隔的 token 列表;空 = 空数组(调用方自行决定"全部"语义)。 */
|
|
93
|
+
export function parseToolNames(value) {
|
|
94
|
+
if (typeof value !== 'string' || value.trim() === '') return []
|
|
95
|
+
return value.split(',').map((item) => item.trim()).filter((item) => item.length > 0)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 读取可选服务;测试桩 / 极简组合里缺失时返回 undefined,由调用方降级。 */
|
|
99
|
+
export function getService(ctx, name) {
|
|
100
|
+
try {
|
|
101
|
+
return typeof ctx.get === 'function' ? ctx.get(name) : undefined
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 把服务注册返回的 disposer 挂到 fiber(资源注册契约)。 */
|
|
108
|
+
export function keepDisposer(ctx, disposer, label) {
|
|
109
|
+
if (typeof disposer !== 'function') return
|
|
110
|
+
try {
|
|
111
|
+
if (typeof ctx.effect === 'function') {
|
|
112
|
+
ctx.effect(() => disposer, label)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// 极简测试桩没有 effect:保持注册随进程,测试自行隔离。
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 模板变量插值:配置 variables 优先,内置 {{DSH_HOME}} / {{WORKSPACE}} / {{CWD}}。 */
|
|
121
|
+
export function interpolateVariables(text, variables, session) {
|
|
122
|
+
const builtins = {
|
|
123
|
+
DSH_HOME: process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : ''),
|
|
124
|
+
WORKSPACE: process.env.DSH_WORKSPACE ?? session?.header?.cwd ?? process.cwd(),
|
|
125
|
+
CWD: session?.header?.cwd ?? process.cwd(),
|
|
126
|
+
}
|
|
127
|
+
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) => {
|
|
128
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) return String(variables[key])
|
|
129
|
+
return Object.prototype.hasOwnProperty.call(builtins, key) ? builtins[key] : whole
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 仅做配置级静态变量替换(无 session 上下文的层)。 */
|
|
134
|
+
export function interpolateStatic(text, variables) {
|
|
135
|
+
return text.replace(/\{\{([A-Za-z0-9_.\u4e00-\u9fff-]+)\}\}/g, (whole, key) =>
|
|
136
|
+
Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key]) : whole)
|
|
137
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* strategies — 引擎内容策略绑定(config.resolve)。
|
|
3
|
+
* 内置策略: static / placeholder / instruction-hint / first-turn-anchor / guide-auto / custom-fallback。
|
|
4
|
+
* 策略参数全部来自 config.params(由 preset.yml 单一配置源下发),引擎只负责组装。
|
|
5
|
+
* 仍支持 strategyDir 懒加载自定义模板策略。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractText } from './shared.mjs'
|
|
9
|
+
import { createInstructionHintResolver, createPlaceholderResolver } from './fillers.mjs'
|
|
10
|
+
|
|
11
|
+
const name = 'prompt-config-engine'
|
|
12
|
+
|
|
13
|
+
/** 锚定词正则转义。 */
|
|
14
|
+
function escapeRegExp(text) {
|
|
15
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* first-turn-anchor:首条真实用户消息后的一次性任务锚点。
|
|
20
|
+
* 正则与锚句文本全部来自 config.params(由 preset.yml 单一配置源下发)。
|
|
21
|
+
*/
|
|
22
|
+
function createFirstTurnAnchorResolver(config) {
|
|
23
|
+
const useCustom = config.params?.useCustom === true
|
|
24
|
+
const customText = typeof config.params?.firstTurnText === 'string' ? config.params.firstTurnText : ''
|
|
25
|
+
const buildPattern = typeof config.params?.buildPattern === 'string' ? config.params.buildPattern : ''
|
|
26
|
+
const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
|
|
27
|
+
const firstTurnBuild = typeof config.params?.firstTurnBuild === 'string' ? config.params.firstTurnBuild : ''
|
|
28
|
+
const firstTurnInspect = typeof config.params?.firstTurnInspect === 'string' ? config.params.firstTurnInspect : ''
|
|
29
|
+
const firstTurnDeep = typeof config.params?.firstTurnDeep === 'string' ? config.params.firstTurnDeep : ''
|
|
30
|
+
const buildRe = buildPattern.length > 0 ? new RegExp(buildPattern, 'i') : undefined
|
|
31
|
+
const complexRe = complexPattern.length > 0 ? new RegExp(complexPattern, 'i') : undefined
|
|
32
|
+
return ({ messages }) => {
|
|
33
|
+
if (useCustom) {
|
|
34
|
+
const text = customText.trim()
|
|
35
|
+
return text.length > 0 ? { text } : null
|
|
36
|
+
}
|
|
37
|
+
if (buildRe === undefined || complexRe === undefined || (firstTurnBuild.length === 0 && firstTurnInspect.length === 0 && firstTurnDeep.length === 0)) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
|
|
41
|
+
if (userIndex < 0) return null
|
|
42
|
+
const taskText = extractText(messages[userIndex])
|
|
43
|
+
if (taskText.length === 0) return null
|
|
44
|
+
let anchor
|
|
45
|
+
if (complexRe.test(taskText)) anchor = firstTurnDeep
|
|
46
|
+
else if (buildRe.test(taskText)) anchor = firstTurnBuild
|
|
47
|
+
else anchor = firstTurnInspect
|
|
48
|
+
return anchor.length > 0 ? { text: anchor } : null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* guide-auto:晋升后每轮用户消息后的弱/深度引导。
|
|
54
|
+
* 正则与引导文本全部来自 config.params(由 preset.yml 单一配置源下发)。
|
|
55
|
+
*/
|
|
56
|
+
function createGuideAutoResolver(config) {
|
|
57
|
+
const useCustom = config.params?.useCustom === true
|
|
58
|
+
const customText = typeof config.params?.text === 'string' ? config.params.text : ''
|
|
59
|
+
const guideComplexPattern = typeof config.params?.guideComplexPattern === 'string' ? config.params.guideComplexPattern : ''
|
|
60
|
+
const guideWeak = typeof config.params?.guideWeak === 'string' ? config.params.guideWeak : ''
|
|
61
|
+
const guideDeep = typeof config.params?.guideDeep === 'string' ? config.params.guideDeep : ''
|
|
62
|
+
const guideComplexRe = guideComplexPattern.length > 0 ? new RegExp(guideComplexPattern, 'i') : undefined
|
|
63
|
+
return ({ messages }) => {
|
|
64
|
+
const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
|
|
65
|
+
if (userIndex < 0) return null
|
|
66
|
+
const text = extractText(messages[userIndex])
|
|
67
|
+
if (text.length === 0) return null
|
|
68
|
+
if (useCustom) {
|
|
69
|
+
const guide = customText.trim()
|
|
70
|
+
return guide.length > 0 ? { text: guide } : null
|
|
71
|
+
}
|
|
72
|
+
if (guideWeak.length === 0 && guideDeep.length === 0) return null
|
|
73
|
+
return { text: (text.length > 120 || (guideComplexRe !== undefined && guideComplexRe.test(text))) ? guideDeep : guideWeak }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 判断 reasoning 开头是否命中自定义锚定词:ASCII 词按词边界匹配,其他按前缀匹配。 */
|
|
78
|
+
function matchesAnchorWord(raw, firstTurnWord) {
|
|
79
|
+
const text = String(raw ?? '').trim()
|
|
80
|
+
if (text.length === 0 || firstTurnWord.length === 0) return false
|
|
81
|
+
if (/^[\x20-\x7E]+$/.test(firstTurnWord)) {
|
|
82
|
+
return new RegExp(`^${escapeRegExp(firstTurnWord.toLowerCase())}\\b`, 'i').test(text)
|
|
83
|
+
}
|
|
84
|
+
return text.startsWith(firstTurnWord)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* custom-fallback:自定义锚定词命中后注入一次,未命中最多两轮兜底。
|
|
89
|
+
* 参数全部来自 config.params(由 preset.yml 单一配置源下发)。
|
|
90
|
+
*/
|
|
91
|
+
function createCustomFallbackResolver(config) {
|
|
92
|
+
const promptText = config.texts.length > 0
|
|
93
|
+
? config.texts.join('\n\n')
|
|
94
|
+
: (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
|
|
95
|
+
const firstTurnWord = typeof config.params?.firstTurnWord === 'string' && config.params.firstTurnWord.length > 0
|
|
96
|
+
? config.params.firstTurnWord
|
|
97
|
+
: 'we'
|
|
98
|
+
|
|
99
|
+
const anchorScanned = new Map()
|
|
100
|
+
|
|
101
|
+
const isAnchorConfirmed = (agent) => {
|
|
102
|
+
const session = agent.session
|
|
103
|
+
const cached = anchorScanned.get(session.id)
|
|
104
|
+
if (cached !== undefined) return cached
|
|
105
|
+
const first = session.events.find((event) => event.type === 'assistant/message')
|
|
106
|
+
if (first === undefined) return false
|
|
107
|
+
const content = first.data?.message?.content ?? []
|
|
108
|
+
const reasoning = content.find((block) => block.type === 'reasoning')
|
|
109
|
+
const confirmed = reasoning !== undefined && matchesAnchorWord(reasoning.text, firstTurnWord)
|
|
110
|
+
anchorScanned.set(session.id, confirmed)
|
|
111
|
+
return confirmed
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const assistantRounds = (agent) =>
|
|
115
|
+
agent.session.events.filter((event) => event.type === 'assistant/message').length
|
|
116
|
+
|
|
117
|
+
return ({ agent }) => {
|
|
118
|
+
if (promptText === undefined) return null
|
|
119
|
+
const session = agent.session
|
|
120
|
+
const confirmed = isAnchorConfirmed(agent)
|
|
121
|
+
if (!confirmed && assistantRounds(agent) <= 1) return null
|
|
122
|
+
return {
|
|
123
|
+
text: promptText,
|
|
124
|
+
source: {
|
|
125
|
+
kind: 'plugin',
|
|
126
|
+
plugin: config.id,
|
|
127
|
+
form: 'notice',
|
|
128
|
+
summary: confirmed
|
|
129
|
+
? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)`
|
|
130
|
+
: `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)`,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* world-book:世界书条目(角色卡 lorebook)。constant=true 恒注入(不扫 keys);
|
|
138
|
+
* selective 条目扫描当前消息批文本,keys/secondaryKeys 任一命中即注入。
|
|
139
|
+
* 大小写/整词匹配可配(caseSensitive / wholeWords);未命中返回 null(不注入)。
|
|
140
|
+
*/
|
|
141
|
+
function createWorldBookResolver(config) {
|
|
142
|
+
const constant = config.params?.constant === true
|
|
143
|
+
const caseSensitive = config.params?.caseSensitive === true
|
|
144
|
+
const wholeWords = config.params?.wholeWords === true
|
|
145
|
+
const useRegex = config.params?.useRegex === true
|
|
146
|
+
const rawKeys = Array.isArray(config.params?.keys) ? config.params.keys : []
|
|
147
|
+
const rawSecondary = Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : []
|
|
148
|
+
const keys = [...rawKeys, ...rawSecondary]
|
|
149
|
+
.map((key) => String(key).trim())
|
|
150
|
+
.filter((key) => key.length > 0)
|
|
151
|
+
const needle = keys.length > 0
|
|
152
|
+
? (useRegex
|
|
153
|
+
// ST use_regex=true:keys 为正则表达式(原样拼接,模型提示词作者负责合法性)。
|
|
154
|
+
? new RegExp(keys.join('|'), caseSensitive ? '' : 'i')
|
|
155
|
+
: (wholeWords
|
|
156
|
+
? new RegExp(`(^|[^\\p{L}\\p{N}])(${keys.map((key) => escapeRegExp(key)).join('|')})(?![\\p{L}\\p{N}])`, `${caseSensitive ? '' : 'i'}u`)
|
|
157
|
+
: new RegExp(keys.map((key) => escapeRegExp(key)).join('|'), caseSensitive ? '' : 'i'))
|
|
158
|
+
)
|
|
159
|
+
: undefined
|
|
160
|
+
const promptText = config.texts.length > 0
|
|
161
|
+
? config.texts.join('\n\n')
|
|
162
|
+
: (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
|
|
163
|
+
return ({ messages }) => {
|
|
164
|
+
if (promptText === undefined) return null
|
|
165
|
+
if (constant || needle === undefined) return { text: promptText }
|
|
166
|
+
const haystack = (Array.isArray(messages) ? messages : [])
|
|
167
|
+
.map((message) => extractText(message))
|
|
168
|
+
.filter((text) => text.length > 0)
|
|
169
|
+
.join('\n')
|
|
170
|
+
if (haystack.length === 0) return null
|
|
171
|
+
needle.lastIndex = 0
|
|
172
|
+
return needle.test(haystack) ? { text: promptText } : null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 为归一化后的提示词配置绑定策略 resolve(策略状态随提示词配置对象,不随 apply)。
|
|
178
|
+
* strategyDir:外部策略模块目录(相对本文件 URL)。未声明时只允许内置策略。
|
|
179
|
+
*/
|
|
180
|
+
export function bindResolver(config, strategyDir) {
|
|
181
|
+
switch (config.strategy) {
|
|
182
|
+
case 'placeholder': return createPlaceholderResolver(config)
|
|
183
|
+
case 'instruction-hint': return createInstructionHintResolver(config)
|
|
184
|
+
case 'first-turn-anchor': return createFirstTurnAnchorResolver(config)
|
|
185
|
+
case 'guide-auto': return createGuideAutoResolver(config)
|
|
186
|
+
case 'custom-fallback':
|
|
187
|
+
return createCustomFallbackResolver(config)
|
|
188
|
+
case 'world-book':
|
|
189
|
+
return createWorldBookResolver(config)
|
|
190
|
+
case 'static': {
|
|
191
|
+
const texts = config.texts
|
|
192
|
+
const patch = config.templatePatch ?? {}
|
|
193
|
+
return () => {
|
|
194
|
+
if (texts.length > 0) return { ...patch, content: texts.map((item) => ({ type: 'text', text: item })) }
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
default: {
|
|
199
|
+
// 模板专属策略:懒加载 <strategyDir>/<strategy>.mjs,模块约定导出
|
|
200
|
+
// `createResolver(config)` 或默认导出同名工厂。加载失败按单配置失败语义
|
|
201
|
+
// 抛给调用方(executor 会跳过该配置并 warnOnce)。
|
|
202
|
+
if (typeof strategyDir !== 'string' || strategyDir.length === 0) {
|
|
203
|
+
throw new TypeError(`${name}: unknown config strategy ${JSON.stringify(config.strategy)}`)
|
|
204
|
+
}
|
|
205
|
+
const moduleUrl = new URL(`${config.strategy}.mjs`, strategyDir.endsWith('/') ? strategyDir : `${strategyDir}/`)
|
|
206
|
+
let loaded
|
|
207
|
+
return async (args) => {
|
|
208
|
+
if (loaded === undefined) {
|
|
209
|
+
loaded = await import(moduleUrl.href)
|
|
210
|
+
}
|
|
211
|
+
const make = typeof loaded.createResolver === 'function'
|
|
212
|
+
? loaded.createResolver
|
|
213
|
+
: typeof loaded.default === 'function'
|
|
214
|
+
? loaded.default
|
|
215
|
+
: undefined
|
|
216
|
+
if (typeof make !== 'function') {
|
|
217
|
+
throw new TypeError(`${name}: strategy module ${moduleUrl.href} must export createResolver(config)`)
|
|
218
|
+
}
|
|
219
|
+
return make(config)(args)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|