@master0071/dsh-systemone 0.0.0-stage → 0.4.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/lib/format.js ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * format.js — 决策结果的归一化、模板渲染与 Markdown 摘要。
3
+ */
4
+
5
+ /** 生成整个工具的 Markdown 摘要。 */
6
+ export function formatSummary({
7
+ title,
8
+ provider,
9
+ model,
10
+ requestId,
11
+ latencyMs,
12
+ lines = [],
13
+ recommendation,
14
+ needsHumanReview = false,
15
+ }) {
16
+ const out = [
17
+ `## ${title}`,
18
+ `- **决策服务**:${provider} / ${model}`,
19
+ ...lines,
20
+ ]
21
+ if (recommendation) out.push(`- **建议**:${recommendation}`)
22
+ out.push(`- **建议复核**:${needsHumanReview ? '需要人工复核' : '无需人工复核'}`)
23
+ if (requestId) out.push(`- **请求 ID**:${requestId}(延迟 ${latencyMs}ms)`)
24
+ return out.filter(Boolean).join('\n')
25
+ }
26
+
27
+ /**
28
+ * 把原始 answers 归一化为结构化结果。
29
+ * @param {object} answers - SystemOne 原始答案
30
+ * @param {object} questions - 场景问题定义(用于补 label / legend 缺失的情况)
31
+ * @returns {{ decision: object, labels: object, confidences: object }}
32
+ */
33
+ export function normalizeAnswers(answers, questions = {}) {
34
+ const decision = {}
35
+ const labels = {}
36
+ const confidences = {}
37
+
38
+ for (const [qid, question] of Object.entries(questions)) {
39
+ const raw = answers?.[qid]
40
+ const type = question?.type || raw?.type
41
+ if (!raw || typeof raw !== 'object') continue
42
+
43
+ if (type === 'choice') {
44
+ const key = raw.choice ?? null
45
+ decision[qid] = key
46
+ labels[qid] = key != null ? (question?.criteria?.[key] ?? raw.choice ?? String(key)) : '无法判断'
47
+ if (Number.isFinite(Number(raw.confidence))) confidences[qid] = Number(raw.confidence)
48
+ continue
49
+ }
50
+ if (type === 'score') {
51
+ const score = Number(raw.score)
52
+ const idx = Number.isFinite(score) ? Math.round(score) : null
53
+ decision[qid] = Number.isFinite(score) ? idx : null
54
+ const legend = raw.legend || {}
55
+ const scale = Array.isArray(question?.criteria) ? question.criteria : []
56
+ labels[qid] = idx != null ? (legend[String(idx)] ?? scale[Number(raw.score ?? 0)] ?? String(idx)) : '无法判断'
57
+ if (Number.isFinite(Number(raw.confidence))) confidences[qid] = Number(raw.confidence)
58
+ continue
59
+ }
60
+ if (type === 'noul') {
61
+ const v = Number(raw.noul)
62
+ const flag = Number.isFinite(v) ? v >= 0.5 : null
63
+ decision[qid] = flag
64
+ labels[qid] = flag === null ? '无法判断' : flag ? '是' : '否'
65
+ if (Number.isFinite(v)) confidences[qid] = flag ? v : 1 - v
66
+ continue
67
+ }
68
+ }
69
+ return { decision, labels, confidences }
70
+ }
71
+
72
+ /** 计算派生字段(如 severity → priority)。 */
73
+ export function deriveFields(deriveSpec, decision) {
74
+ const derived = {}
75
+ for (const [name, spec] of Object.entries(deriveSpec || {})) {
76
+ if (!spec || typeof spec !== 'object' || typeof spec.question !== 'string') continue
77
+ const value = decision?.[spec.question]
78
+ if (Array.isArray(spec.values)) {
79
+ const idx = Number(value)
80
+ if (!Number.isFinite(idx)) continue
81
+ const clamped = Math.min(Math.max(Math.round(idx), 0), spec.values.length - 1)
82
+ derived[name] = spec.values[clamped]
83
+ } else if (spec.map && typeof spec.map === 'object') {
84
+ if (value == null) continue
85
+ derived[name] = spec.map[String(value)] ?? String(value)
86
+ }
87
+ }
88
+ return derived
89
+ }
90
+
91
+ /**
92
+ * 渲染建议模板。支持 {qid}、{qid.key}、{qid.confidence}、{derived}、{qid?A|B}。
93
+ * 条件文案内的占位符会在第二遍替换;不支持嵌套条件。
94
+ */
95
+ export function renderTemplate(template, { labels = {}, decision = {}, confidences = {}, derived = {} } = {}) {
96
+ if (!template) return ''
97
+ const ctx = { labels, decision, confidences, derived }
98
+ let out = String(template)
99
+ out = out.replace(/\{(\w+)\?([^{}]*)\|([^{}]*)\}/g, (_, name, thenText, elseText) => (
100
+ truthy(name, ctx) ? thenText : elseText
101
+ ))
102
+ out = out.replace(/\{(\w+)(?:\.(\w+))?\}/g, (_, name, prop) => {
103
+ if (prop === 'key') return stringify(decision[name])
104
+ if (prop === 'confidence') {
105
+ const c = confidences[name]
106
+ return Number.isFinite(c) ? `${(c * 100).toFixed(1)}%` : ''
107
+ }
108
+ if (name in labels) return stringify(labels[name])
109
+ if (name in derived) return stringify(derived[name])
110
+ return stringify(decision[name])
111
+ })
112
+ return out.replace(/\s+/g, ' ').trim()
113
+ }
114
+
115
+ /** 把 answers 渲染为 Markdown 行(带问题显示名)。 */
116
+ export function formatAnswerLines(answers, questions = {}) {
117
+ const lines = []
118
+ for (const [qid, raw] of Object.entries(answers || {})) {
119
+ if (!raw || typeof raw !== 'object') continue
120
+ const label = questions?.[qid]?.label || qid
121
+ if (raw.type === 'choice') lines.push(choiceLine(label, raw, questions?.[qid]))
122
+ else if (raw.type === 'score') lines.push(scoreLine(label, raw, questions?.[qid]))
123
+ else if (raw.type === 'noul') lines.push(noulLine(label, raw))
124
+ else lines.push(`- **${label}**:${JSON.stringify(raw)}`)
125
+ }
126
+ return lines
127
+ }
128
+
129
+ export function choiceLine(label, answer, question) {
130
+ const { choice, confidence, probabilities } = answer
131
+ const text = choice != null ? (question?.criteria?.[choice] ?? choice) : '无法判断'
132
+ const dist = topProbs(probabilities, 3)
133
+ return `- **${label}**:${text}(置信度 ${fmtPct(confidence)}|分布 ${dist})`
134
+ }
135
+
136
+ export function scoreLine(label, answer, question) {
137
+ const { score, confidence, legend, probabilities } = answer
138
+ const idx = Number.isFinite(Number(score)) ? Math.round(Number(score)) : null
139
+ const text = idx != null
140
+ ? (legend?.[String(idx)] ?? (Array.isArray(question?.criteria) ? question.criteria[idx] : undefined) ?? String(idx))
141
+ : '无法判断'
142
+ const raw = Number.isFinite(Number(score)) ? ` ${Number(score).toFixed(2)}` : ''
143
+ const dist = topProbs(probabilities, 3)
144
+ return `- **${label}**:${text}${raw}(置信度 ${fmtPct(confidence)}|分布 ${dist})`
145
+ }
146
+
147
+ export function noulLine(label, answer) {
148
+ const v = Number(answer.noul)
149
+ if (!Number.isFinite(v)) return `- **${label}**:无法判断`
150
+ return `- **${label}**:${v >= 0.5 ? '是' : '否'}(${(v * 100).toFixed(1)}%)`
151
+ }
152
+
153
+ /** 是否需要人工复核:置信度低于阈值,或出现不确定项。 */
154
+ export function computeNeedsHumanReview(answers, minConfidence) {
155
+ for (const a of Object.values(answers || {})) {
156
+ if (!a || typeof a !== 'object') continue
157
+ if (typeof a.confidence === 'number' && a.confidence < minConfidence) return true
158
+ if (a.type === 'choice' && (a.choice == null || a.choice === 'uncertain' || a.choice === 'unknown')) return true
159
+ if (a.type === 'noul' && typeof a.noul === 'number' && a.noul > 0.45 && a.noul < 0.55) return true
160
+ }
161
+ return false
162
+ }
163
+
164
+ /* ─── 内部工具 ─────────────────────────────────────────────────────────────── */
165
+
166
+ function truthy(name, { decision, derived }) {
167
+ if (name in derived) {
168
+ const v = derived[name]
169
+ if (typeof v === 'boolean') return v
170
+ if (typeof v === 'number') return v >= 1
171
+ return !['', 'none', 'normal', 'no', 'false', '0'].includes(String(v).toLowerCase())
172
+ }
173
+ const value = decision[name]
174
+ if (typeof value === 'boolean') return value
175
+ if (typeof value === 'number') return value >= 1
176
+ if (typeof value === 'string') return !['', 'none', 'normal', 'no', 'false', '0'].includes(value.toLowerCase())
177
+ return Boolean(value)
178
+ }
179
+
180
+ function stringify(value) {
181
+ if (value === null || value === undefined) return ''
182
+ return String(value)
183
+ }
184
+
185
+ function fmtPct(value) {
186
+ const n = Number(value)
187
+ return Number.isFinite(n) ? `${(n * 100).toFixed(1)}%` : '?'
188
+ }
189
+
190
+ function topProbs(probabilities, n = 3) {
191
+ const entries = Object.entries(probabilities || {})
192
+ .filter(([, p]) => Number.isFinite(Number(p)))
193
+ .sort((a, b) => Number(b[1]) - Number(a[1]))
194
+ .slice(0, n)
195
+ if (!entries.length) return '—'
196
+ return entries.map(([k, p]) => `${k}=${(Number(p) * 100).toFixed(1)}%`).join(' ')
197
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @master0071/dsh-systemone 插件类型声明(手写,与 lib/index.js 对应)。
3
+ */
4
+ import { Context, Service } from '@deepseek-ai/cordis'
5
+
6
+ /** 插件配置。所有字段均有默认值。 */
7
+ export interface Config {
8
+ /** 提供商类型:unisound(官方)/ http(SystemOne 兼容端点)/ mock(本地模拟) */
9
+ provider?: string
10
+ /** Unisound API Key(也可用环境变量 UNISOUND_API_KEY / SYSTEMONE_API_KEY) */
11
+ apiKey?: string
12
+ /** Unisound API 基础地址 */
13
+ baseUrl?: string
14
+ /** provider=http 时的完整请求端点 */
15
+ endpoint?: string
16
+ /** 决策模型名 */
17
+ model?: string
18
+ /** 请求超时(毫秒) */
19
+ timeoutMs?: number
20
+ /** 置信度阈值 */
21
+ minConfidence?: number
22
+ /** 自定义场景:JSON 数组字符串 */
23
+ customScenarios?: string
24
+
25
+ /* ── 自动决策(默认关闭) ─────────────────────────────── */
26
+ /** 是否在每一步推理前自动捕获上下文并执行决策 */
27
+ autoDecide?: boolean
28
+ /** 自动决策固定使用的场景 id;留空则自动路由 */
29
+ autoScenario?: string
30
+ /** 注入通道:message=同一步生效并去重;context=宿主动态上下文(不写历史) */
31
+ autoInject?: string
32
+ /** 自动决策超时(毫秒) */
33
+ autoTimeoutMs?: number
34
+ /** 捕获的历史消息条数 */
35
+ autoMaxMessages?: number
36
+ /** 相同内容的结果缓存时长(毫秒),0 表示不缓存 */
37
+ autoCacheTtlMs?: number
38
+ /** 自动决策使用的置信度阈值 */
39
+ autoMinConfidence?: number
40
+ /** 自动路由的置信度门槛 */
41
+ autoRouteMinConfidence?: number
42
+ /** 触发自动决策的最小输入长度(字符) */
43
+ autoMinChars?: number
44
+ }
45
+
46
+ /** 编程式调用 SystemOne 的参数。 */
47
+ export interface DecideOptions {
48
+ state: unknown
49
+ questions: Record<string, unknown>
50
+ model?: string
51
+ signal?: AbortSignal
52
+ }
53
+
54
+ /** 场景摘要。 */
55
+ export interface ScenarioInfo {
56
+ id: string
57
+ title: string
58
+ source: 'builtin' | 'custom'
59
+ questions: string[]
60
+ }
61
+
62
+ declare module '@deepseek-ai/cordis' {
63
+ interface Context {
64
+ /** SystemOne 决策服务。 */
65
+ systemone: SystemOneService
66
+ }
67
+ }
68
+
69
+ export default class SystemOneService extends Service {
70
+ static Config: any
71
+ readonly config: Config
72
+ readonly provider: {
73
+ name: string
74
+ decide(options: DecideOptions): Promise<Record<string, unknown>>
75
+ }
76
+
77
+ constructor(ctx: Context, config: Config)
78
+
79
+ /** 列出当前全部场景(内置 + 自定义)。 */
80
+ listScenarios(): ScenarioInfo[]
81
+
82
+ /** 编程式调用决策模型。 */
83
+ decide(options: DecideOptions): Promise<Record<string, unknown>>
84
+ }
package/lib/index.js ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @master0071/dsh-systemone 插件入口。
3
+ *
4
+ * 架构:Cordis 插件(Service 形态)。依赖 harness 的 `tools`(工具注册表)
5
+ * 与 `logger`;无 Web 面。
6
+ *
7
+ * - 注册 2 个 Agent 工具:
8
+ * systemone_scenario — 统一场景调度(list / describe / run)
9
+ * systemone_decide — 自定义问题的低层入口
10
+ * - 场景库内置 10 大业务域,并支持在配置里用 JSON 追加/覆盖场景
11
+ * - 暴露 `ctx.systemone.decide()` 供其他插件编程调用
12
+ * - 提供商可配置(unisound / http / mock),切换厂商不影响工具层
13
+ */
14
+ import { Service } from '@deepseek-ai/cordis'
15
+ import z from '@deepseek-ai/schemastery'
16
+ import { createProvider } from './provider.js'
17
+ import { createTools } from './tools.js'
18
+ import { resolveScenarios } from './scenarios.js'
19
+ import { installAutoDecide } from './auto.js'
20
+ import { readBoolean, readString } from './volatile.js'
21
+
22
+ /**
23
+ * 把「当前提供商」包成一个稳定引用的对象,内部每次调用都转发到最新实例。
24
+ *
25
+ * 工具与自动决策在装配期就拿到了这个对象并长期持有;有了它,
26
+ * 配置页里切换 `provider` 后它们无需重新装配即可用上新提供商。
27
+ */
28
+ function createProviderProxy(current) {
29
+ return {
30
+ get name() {
31
+ return current().name
32
+ },
33
+ decide(options) {
34
+ return current().decide(options)
35
+ },
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 插件配置。所有字段均有默认值。
41
+ *
42
+ * 标了 `.volatile()` 的字段可以在「插件 → @master0071/dsh-systemone → systemone」配置页
43
+ * 里直接修改并立即生效,无需重启;读取时必须经 `unwrap()` 脱壳(见 volatile.js)。
44
+ * 结构性字段(provider 之外的场景库、autoDecide 开关)不在配置页里,改动需重启。
45
+ */
46
+ const Config = z.object({
47
+ /* ── 热更新字段(volatile):配置页可直接编辑 ────────────────────────────── */
48
+
49
+ /**
50
+ * 自定义场景:JSON 数组字符串,同 id 覆盖内置场景。
51
+ *
52
+ * 这是「通用意图识别」的入口——把意图标签集写成 choice 的 criteria 即可,
53
+ * 场景库会随保存立即重建(见 liveScenarios),无需重启。
54
+ */
55
+ customScenarios: z.string().default('').volatile(),
56
+ /** 提供商类型:unisound(官方)/ http(SystemOne 兼容端点)/ mock(本地模拟) */
57
+ provider: z.string().default('unisound').volatile(),
58
+ /** Unisound API Key(也可用环境变量 UNISOUND_API_KEY / SYSTEMONE_API_KEY) */
59
+ apiKey: z.string().default('').role('secret').volatile(),
60
+ /** Unisound API 基础地址(默认 https://maas-api.unisound.com/v1) */
61
+ baseUrl: z.string().default('https://maas-api.unisound.com/v1').volatile(),
62
+ /** provider=http 时的完整请求端点;留空则回退到 baseUrl */
63
+ endpoint: z.string().default('').volatile(),
64
+ /** 决策模型名 */
65
+ model: z.string().default('u2-decision').volatile(),
66
+ /** 请求超时(毫秒) */
67
+ timeoutMs: z.number().min(1000).default(30000).volatile(),
68
+ /** 置信度阈值:任一答案低于该值则建议人工复核 */
69
+ minConfidence: z.number().min(0).max(1).default(0.6).volatile(),
70
+
71
+ /* ── 结构性字段:改动需要重启(不在配置页里) ─────────────────────────────── */
72
+
73
+ /** 是否在每一步推理前自动捕获上下文并执行决策(挂载 pre-step,改动需重启) */
74
+ autoDecide: z.boolean().default(false),
75
+
76
+ /* ── 自动决策运行参数(volatile):钩子已挂载时实时生效 ────────────────────── */
77
+ /** 自动决策固定使用的场景 id;留空则用一次 choice 问句自动路由 */
78
+ autoScenario: z.string().default('').volatile(),
79
+ /** 注入通道:message=本步生效(pre-step 注入);context=官方动态上下文(晚一步生效) */
80
+ autoInject: z.string().default('message').volatile(),
81
+ /** 自动决策超时(毫秒),超时即放行,绝不阻塞会话 */
82
+ autoTimeoutMs: z.number().min(500).default(8000).volatile(),
83
+ /** 捕获的历史消息条数(不含本轮新消息) */
84
+ autoMaxMessages: z.number().min(1).max(50).default(6).volatile(),
85
+ /** 相同内容的结果缓存时长(毫秒),0 表示不缓存 */
86
+ autoCacheTtlMs: z.number().min(0).default(60000).volatile(),
87
+ /** 自动决策使用的置信度阈值 */
88
+ autoMinConfidence: z.number().min(0).max(1).default(0.6).volatile(),
89
+ /** 自动路由的置信度门槛:低于该值宁可不注入,避免塞入瞎猜的结论 */
90
+ autoRouteMinConfidence: z.number().min(0).max(1).default(0.35).volatile(),
91
+ /** 触发自动决策的最小输入长度(字符);低于该长度且整体是寒暄才跳过 */
92
+ autoMinChars: z.number().min(0).default(4).volatile(),
93
+ })
94
+
95
+ export default class SystemOneService extends Service {
96
+ /**
97
+ * 声明式依赖:Cordis 会等 `tools` 就绪后再启动本插件。
98
+ *
99
+ * 不能用构造期的 `ctx.get('tools')` 去赌加载顺序:tools 尚未提供时会静默
100
+ * 拿到 undefined,插件"激活成功但一个工具都没注册",且只有一条看不到的 warn。
101
+ */
102
+ static inject = ['tools']
103
+
104
+ static Config = Config
105
+
106
+ /**
107
+ * @param {import('@deepseek-ai/cordis').Context} ctx
108
+ * @param {import('zod').infer<typeof Config>} config
109
+ */
110
+ constructor(ctx, config) {
111
+ super(ctx, 'systemone')
112
+ this.ctx = ctx
113
+ this.config = config
114
+
115
+ // provider 字段是 volatile:配置页里切换 unisound / http / mock 时,
116
+ // 下面这个代理会按需重建真正的提供商,工具与自动决策无需改动即可跟随。
117
+ this._provider = createProvider(ctx, config)
118
+ this._providerKind = readString(config, 'provider', 'unisound')
119
+ this.provider = createProviderProxy(() => this.currentProvider())
120
+
121
+ // 场景库同样是热更新的:customScenarios 一变就原地重建,
122
+ // 工具与自动决策持有的是同一个 liveScenarios 引用,因此无需重新装配。
123
+ this._scenarioSource = null
124
+ this.scenarioProblems = []
125
+ this._scenarioArray = []
126
+ this._refreshScenarios()
127
+ const scenarios = this.liveScenarios()
128
+
129
+ // inject 保证 ctx.tools 存在;注册失败必须显式抛出,绝不静默降级
130
+ const tools = ctx.get('tools') ?? ctx.tools
131
+ if (!tools || typeof tools.register !== 'function') {
132
+ throw new Error('@master0071/dsh-systemone: 工具注册表不可用(inject: [\'tools\'] 未生效)')
133
+ }
134
+ this._disposers = createTools(this.provider, config, scenarios).map((tool) => tools.register(tool))
135
+
136
+ // 自动决策:默认关闭;开启后在每一步推理前自动捕获上下文并注入判断
137
+ this._autoDispose = () => {}
138
+ if (readBoolean(config, 'autoDecide', false)) {
139
+ try {
140
+ this._autoDispose = installAutoDecide(ctx, {
141
+ provider: this.provider,
142
+ config,
143
+ scenarios,
144
+ logger: ctx.logger,
145
+ })
146
+ } catch (error) {
147
+ ctx.logger?.warn?.(`@master0071/dsh-systemone: 自动决策安装失败,工具仍可用:${error?.message || error}`)
148
+ }
149
+ }
150
+
151
+ ctx.logger?.info?.(
152
+ `@master0071/dsh-systemone: 已注册 ${this._disposers.length} 个工具、${scenarios.length} 个场景`
153
+ + `(provider=${this.provider.name}, model=${readString(config, 'model', 'u2-decision')}`
154
+ + `, 自动决策=${readBoolean(config, 'autoDecide', false) ? `开(${readString(config, 'autoInject', 'message')})` : '关'})`,
155
+ )
156
+ }
157
+
158
+ /**
159
+ * 返回当前应当使用的提供商;`provider` 配置变化时按需重建。
160
+ *
161
+ * volatile 字段热更新后 `config.provider` 会变,但已构建的提供商实例不会,
162
+ * 所以这里做一次惰性比对——只有真的换了提供商类型才重建。
163
+ */
164
+ currentProvider() {
165
+ const kind = readString(this.config, 'provider', 'unisound')
166
+ if (kind !== this._providerKind) {
167
+ this._provider = createProvider(this.ctx, this.config)
168
+ this._providerKind = kind
169
+ }
170
+ return this._provider
171
+ }
172
+
173
+ /**
174
+ * 场景库的「活引用」。
175
+ *
176
+ * 返回一个 Proxy:任何读取(`.map`、`.length`、`findScenario` 的遍历……)都会
177
+ * 先按需重建,然后转发到真实数组。这样工具与自动决策在装配期捕获一次引用就够,
178
+ * 之后 `customScenarios` 怎么改都能立刻看到新场景。
179
+ */
180
+ liveScenarios() {
181
+ if (this._scenarioProxy === undefined) {
182
+ this._scenarioProxy = new Proxy(this._scenarioArray, {
183
+ get: (target, prop, receiver) => {
184
+ this._refreshScenarios()
185
+ return Reflect.get(target, prop, receiver)
186
+ },
187
+ has: (target, prop) => {
188
+ this._refreshScenarios()
189
+ return Reflect.has(target, prop)
190
+ },
191
+ ownKeys: (target) => {
192
+ this._refreshScenarios()
193
+ return Reflect.ownKeys(target)
194
+ },
195
+ getOwnPropertyDescriptor: (target, prop) => {
196
+ this._refreshScenarios()
197
+ return Reflect.getOwnPropertyDescriptor(target, prop)
198
+ },
199
+ })
200
+ }
201
+ return this._scenarioProxy
202
+ }
203
+
204
+ /**
205
+ * 按需重建场景库:`customScenarios` 的原始字符串变了才重建。
206
+ *
207
+ * 原地改写 `this._scenarioArray`(清空 + 填充),而不是换一个新数组——
208
+ * 因为工具与自动决策捕获的是那个数组本身。
209
+ */
210
+ _refreshScenarios() {
211
+ const source = readString(this.config, 'customScenarios', '').trim()
212
+ if (source === this._scenarioSource) return
213
+ const { scenarios, problems } = resolveScenarios(source)
214
+ this._scenarioArray.length = 0
215
+ this._scenarioArray.push(...scenarios)
216
+ this._scenarioSource = source
217
+ this.scenarioProblems = problems
218
+ for (const problem of problems) {
219
+ this.ctx?.logger?.warn?.(`@master0071/dsh-systemone: ${problem}`)
220
+ }
221
+ }
222
+
223
+ /** 列出当前全部场景(内置 + 自定义)。 */
224
+ listScenarios() {
225
+ return this.liveScenarios().map((s) => ({
226
+ id: s.id,
227
+ title: s.title,
228
+ source: s.source,
229
+ questions: Object.keys(s.questions),
230
+ }))
231
+ }
232
+
233
+ /**
234
+ * 编程式调用决策模型。
235
+ * @param {{ state: unknown, questions: object, model?: string, signal?: AbortSignal }} options
236
+ * @returns {Promise<object>} SystemOne 响应
237
+ */
238
+ async decide(options) {
239
+ return this.provider.decide(options)
240
+ }
241
+
242
+ /** 插件卸载时注销所有工具与自动决策挂钩。 */
243
+ dispose() {
244
+ try {
245
+ this._autoDispose?.()
246
+ } catch {
247
+ // 卸载竞态
248
+ }
249
+ this._autoDispose = () => {}
250
+ for (const dispose of this._disposers || []) {
251
+ try {
252
+ dispose()
253
+ } catch {
254
+ // 热卸载竞态:工具可能已被注销
255
+ }
256
+ }
257
+ this._disposers = []
258
+ }
259
+ }
260
+
261
+ // 供其他模块在 JS 侧引用
262
+ export { Config }