@master0071/dsh-systemone 0.4.0 → 0.4.2
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/cordis.patch.yml +3 -2
- package/lib/auto.js +97 -53
- package/lib/client.js +52 -92
- package/lib/format.js +14 -4
- package/lib/index.js +36 -14
- package/lib/provider.js +8 -9
- package/lib/tools.js +36 -22
- package/package.json +1 -1
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @master0071/dsh-systemone bundle patch:把 SystemOne 决策插件插入任何将本包列入
|
|
2
2
|
# `dsh.profile.bundles` 的 profile。插件依赖 harness 的 `tools`(注册 Agent
|
|
3
|
-
# 工具)与 `logger
|
|
3
|
+
# 工具)与 `logger`;浏览器侧配置页由 client.js 提供。配置项全部有默认值,
|
|
4
|
+
# 可在插件设置面板中覆盖。
|
|
4
5
|
- insert:
|
|
5
6
|
- id: systemone
|
|
6
7
|
name: '@master0071/dsh-systemone'
|
|
7
|
-
config: {}
|
|
8
|
+
config: {}
|
package/lib/auto.js
CHANGED
|
@@ -36,6 +36,22 @@ import { readBoolean, readNumber, readString } from './volatile.js'
|
|
|
36
36
|
/** 宿主运行时上下文使用的 source.kind(见 dsh-agent-loop 的 SOURCE 常量)。 */
|
|
37
37
|
const RUNTIME_CONTEXT_SOURCE = 'runtime-context'
|
|
38
38
|
|
|
39
|
+
/** 自动决策结果缓存的最大条目数;达到后先清过期再按插入序淘汰最旧。 */
|
|
40
|
+
const MAX_AUTO_CACHE = 200
|
|
41
|
+
|
|
42
|
+
/** 当前请求注入 state 的最大字符数(历史逐条限 2000,这里限制本轮原文)。 */
|
|
43
|
+
const MAX_CURRENT_CHARS = 8000
|
|
44
|
+
|
|
45
|
+
/** 自动决策数字配置的兜底默认值(与 lib/index.js Config schema 的 default 保持同一份来源)。 */
|
|
46
|
+
export const AUTO_DEFAULTS = {
|
|
47
|
+
autoMaxMessages: 6,
|
|
48
|
+
autoTimeoutMs: 8000,
|
|
49
|
+
autoCacheTtlMs: 60000,
|
|
50
|
+
autoRouteMinConfidence: 0.35,
|
|
51
|
+
autoMinConfidence: 0.6,
|
|
52
|
+
autoMinChars: 4,
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
/**
|
|
40
56
|
* 安装自动决策。返回卸载函数。
|
|
41
57
|
*
|
|
@@ -56,9 +72,8 @@ export function installAutoDecide(ctx, deps) {
|
|
|
56
72
|
const cache = new Map()
|
|
57
73
|
/** stateHash → Promise:并发相同 state 只发一次上游请求。 */
|
|
58
74
|
const inFlight = new Map()
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const routeFloor = readNumber(config, 'autoRouteMinConfidence', 0)
|
|
75
|
+
/** 已告警过的无效固定场景 id:同一错误只提示一次,避免每步刷日志。 */
|
|
76
|
+
const warnedScenarios = new Set()
|
|
62
77
|
|
|
63
78
|
const log = (level, message) => {
|
|
64
79
|
try {
|
|
@@ -79,40 +94,44 @@ export function installAutoDecide(ctx, deps) {
|
|
|
79
94
|
}))
|
|
80
95
|
|
|
81
96
|
// ── context 通道:入箱预计算 + 动态上下文渲染(不写历史,第二步起稳定可见) ──
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
97
|
+
// 监听器无条件注册,autoInject 的门控放在事件回调里按当前配置判断:
|
|
98
|
+
// autoInject 是 volatile 字段,只有事件期判断才能让 message ↔ context
|
|
99
|
+
// 在运行期双向切换都立即生效(在装配期固化只支持一个方向)。
|
|
100
|
+
disposers.push(ctx.on('agent/created', (payload) => {
|
|
101
|
+
const agent = payload?.agent
|
|
102
|
+
const agentCtx = agent?.ctx
|
|
103
|
+
if (!agentCtx) return
|
|
104
|
+
// ① 消息一入箱就异步预计算,让决策尽可能在 assembly 之前就绪
|
|
105
|
+
if (typeof agentCtx.on === 'function') {
|
|
106
|
+
try {
|
|
107
|
+
disposers.push(agentCtx.on('agent/inbox/inserted', (event) => {
|
|
108
|
+
if (readString(config, 'autoInject', 'message') !== 'context') return
|
|
109
|
+
const newText = extractUserText([event?.message])
|
|
110
|
+
if (!newText) return
|
|
111
|
+
if (newText.trimStart().startsWith('/')) return
|
|
112
|
+
if (isTrivialInput(newText, readNumber(config, 'autoMinChars', AUTO_DEFAULTS.autoMinChars))) return
|
|
113
|
+
// 返回 promise 便于测试等待;宿主事件总线忽略返回值
|
|
114
|
+
return computeAndStore(agent, newText)
|
|
115
|
+
}))
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log('warn', `注册入箱预计算失败:${messageOf(error)}`)
|
|
101
118
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
119
|
+
}
|
|
120
|
+
// ② 把最新决策渲染进作用域动态上下文
|
|
121
|
+
if (agentCtx?.systemPrompt?.context) {
|
|
122
|
+
try {
|
|
123
|
+
disposers.push(agentCtx.systemPrompt.context({
|
|
124
|
+
name: 'systemone-auto-decision',
|
|
125
|
+
order: 900,
|
|
126
|
+
text: () => (readString(config, 'autoInject', 'message') === 'context')
|
|
127
|
+
? (perAgent.get(agent?.id)?.text || '')
|
|
128
|
+
: '',
|
|
129
|
+
}))
|
|
130
|
+
} catch (error) {
|
|
131
|
+
log('warn', `注册动态上下文失败:${messageOf(error)}`)
|
|
113
132
|
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
133
|
+
}
|
|
134
|
+
}))
|
|
116
135
|
|
|
117
136
|
// ── pre-step:同一步生效(仅 message 通道) ──
|
|
118
137
|
disposers.push(ctx.on('agent/pre-step', async (payload, next) => {
|
|
@@ -132,13 +151,14 @@ export function installAutoDecide(ctx, deps) {
|
|
|
132
151
|
const newText = extractUserText(claimed)
|
|
133
152
|
if (!newText) return decision // 工具结果步骤 / 无新用户消息
|
|
134
153
|
if (newText.trimStart().startsWith('/')) return decision // 斜杠命令
|
|
135
|
-
if (isTrivialInput(newText,
|
|
154
|
+
if (isTrivialInput(newText, readNumber(config, 'autoMinChars', AUTO_DEFAULTS.autoMinChars))) return decision // 寒暄/致谢等无业务含义的输入
|
|
136
155
|
|
|
137
156
|
let injected = null
|
|
138
157
|
try {
|
|
139
158
|
// 双保险:既给上游传 abort signal,也用 race 兜底,
|
|
140
159
|
// 即使提供商忽略 signal 也不会让这一步的推理被无限阻塞。
|
|
141
|
-
const
|
|
160
|
+
const timeoutMs = readNumber(config, 'autoTimeoutMs', AUTO_DEFAULTS.autoTimeoutMs)
|
|
161
|
+
const timeout = combineSignal(payload?.signal, timeoutMs)
|
|
142
162
|
try {
|
|
143
163
|
injected = await raceTimeout(
|
|
144
164
|
computeDecision({
|
|
@@ -151,8 +171,9 @@ export function installAutoDecide(ctx, deps) {
|
|
|
151
171
|
inFlight,
|
|
152
172
|
signal: timeout.signal,
|
|
153
173
|
log,
|
|
174
|
+
warnedScenarios,
|
|
154
175
|
}),
|
|
155
|
-
|
|
176
|
+
timeoutMs,
|
|
156
177
|
)
|
|
157
178
|
} finally {
|
|
158
179
|
timeout.dispose()
|
|
@@ -187,7 +208,8 @@ export function installAutoDecide(ctx, deps) {
|
|
|
187
208
|
/** 入箱预计算:异步算好并存进 perAgent,供 context 通道渲染。 */
|
|
188
209
|
async function computeAndStore(agent, newText) {
|
|
189
210
|
try {
|
|
190
|
-
const
|
|
211
|
+
const timeoutMs = readNumber(config, 'autoTimeoutMs', AUTO_DEFAULTS.autoTimeoutMs)
|
|
212
|
+
const timeout = combineSignal(undefined, timeoutMs)
|
|
191
213
|
try {
|
|
192
214
|
const injected = await raceTimeout(
|
|
193
215
|
computeDecision({
|
|
@@ -200,8 +222,9 @@ export function installAutoDecide(ctx, deps) {
|
|
|
200
222
|
inFlight,
|
|
201
223
|
signal: timeout.signal,
|
|
202
224
|
log,
|
|
225
|
+
warnedScenarios,
|
|
203
226
|
}),
|
|
204
|
-
|
|
227
|
+
timeoutMs,
|
|
205
228
|
)
|
|
206
229
|
if (injected) perAgent.set(agent?.id ?? 'unknown', injected)
|
|
207
230
|
} finally {
|
|
@@ -232,8 +255,8 @@ export function installAutoDecide(ctx, deps) {
|
|
|
232
255
|
/**
|
|
233
256
|
* 计算决策(带并发去重)。相同 state 只发一次上游请求。
|
|
234
257
|
*/
|
|
235
|
-
async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log }) {
|
|
236
|
-
const state = captureState(agent, newText, readNumber(config, 'autoMaxMessages',
|
|
258
|
+
async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log, warnedScenarios }) {
|
|
259
|
+
const state = captureState(agent, newText, readNumber(config, 'autoMaxMessages', AUTO_DEFAULTS.autoMaxMessages))
|
|
237
260
|
const key = hashString(state)
|
|
238
261
|
const existing = inFlight.get(key)
|
|
239
262
|
if (existing) return existing
|
|
@@ -248,6 +271,7 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
|
|
|
248
271
|
cache,
|
|
249
272
|
signal,
|
|
250
273
|
log,
|
|
274
|
+
warnedScenarios,
|
|
251
275
|
}).finally(() => {
|
|
252
276
|
if (inFlight.get(key) === promise) inFlight.delete(key)
|
|
253
277
|
})
|
|
@@ -255,19 +279,19 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
|
|
|
255
279
|
return promise
|
|
256
280
|
}
|
|
257
281
|
|
|
258
|
-
async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log }) {
|
|
259
|
-
const ttl = readNumber(config, 'autoCacheTtlMs',
|
|
282
|
+
async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log, warnedScenarios }) {
|
|
283
|
+
const ttl = readNumber(config, 'autoCacheTtlMs', AUTO_DEFAULTS.autoCacheTtlMs) || 0
|
|
260
284
|
if (ttl > 0) {
|
|
261
285
|
const hit = cache.get(key)
|
|
262
286
|
if (hit && Date.now() - hit.at < ttl) return hit.result
|
|
263
287
|
}
|
|
264
288
|
|
|
265
289
|
// 1) 选场景:固定绑定,或让模型做一次自动路由
|
|
266
|
-
const routed = await resolveScenario({ state, provider, config, scenarios, signal })
|
|
290
|
+
const routed = await resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios })
|
|
267
291
|
if (!routed) return null
|
|
268
292
|
|
|
269
293
|
// 路由置信度不足:宁可不注入,也不要塞一个瞎猜的结论
|
|
270
|
-
const floor = readNumber(config, 'autoRouteMinConfidence',
|
|
294
|
+
const floor = readNumber(config, 'autoRouteMinConfidence', AUTO_DEFAULTS.autoRouteMinConfidence)
|
|
271
295
|
if (Number.isFinite(floor) && floor > 0
|
|
272
296
|
&& Number.isFinite(routed.confidence) && routed.confidence < floor) {
|
|
273
297
|
log?.('info', `路由置信度 ${(routed.confidence * 100).toFixed(1)}% 低于阈值 ${(floor * 100).toFixed(0)}%,跳过注入`)
|
|
@@ -282,7 +306,7 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
|
|
|
282
306
|
scenario: routed.scenario.id,
|
|
283
307
|
state,
|
|
284
308
|
exec: { signal },
|
|
285
|
-
minConfidence: readNumber(config, 'autoMinConfidence',
|
|
309
|
+
minConfidence: readNumber(config, 'autoMinConfidence', AUTO_DEFAULTS.autoMinConfidence),
|
|
286
310
|
})
|
|
287
311
|
if (!result?.ok) {
|
|
288
312
|
log?.('warn', `场景 ${routed.scenario.id} 执行失败,已放行:${result?.error || '未知错误'}`)
|
|
@@ -297,16 +321,35 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
|
|
|
297
321
|
headline: result.recommendation || routed.scenario.title,
|
|
298
322
|
at: Date.now(),
|
|
299
323
|
}
|
|
300
|
-
if (ttl > 0)
|
|
324
|
+
if (ttl > 0) {
|
|
325
|
+
// 缓存上限:先清过期,再按插入序淘汰最旧,防止长会话无限增长
|
|
326
|
+
if (cache.size >= MAX_AUTO_CACHE) {
|
|
327
|
+
const now = Date.now()
|
|
328
|
+
for (const [k, v] of cache) {
|
|
329
|
+
if (now - v.at >= ttl) cache.delete(k)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
while (cache.size >= MAX_AUTO_CACHE) {
|
|
333
|
+
cache.delete(cache.keys().next().value)
|
|
334
|
+
}
|
|
335
|
+
cache.set(key, { at: Date.now(), result: injected })
|
|
336
|
+
}
|
|
301
337
|
return injected
|
|
302
338
|
}
|
|
303
339
|
|
|
304
340
|
/** 选择场景:固定绑定优先,否则用一次 choice 问句自动路由。 */
|
|
305
|
-
async function resolveScenario({ state, provider, config, scenarios, signal }) {
|
|
341
|
+
async function resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios }) {
|
|
306
342
|
const fixed = readString(config, 'autoScenario', '').trim()
|
|
307
343
|
if (fixed) {
|
|
308
344
|
const scenario = findScenario(scenarios, fixed)
|
|
309
|
-
if (!scenario)
|
|
345
|
+
if (!scenario) {
|
|
346
|
+
// 配置错误不能静默:同一 id 只告警一次,避免每步刷日志
|
|
347
|
+
if (!warnedScenarios.has(fixed)) {
|
|
348
|
+
warnedScenarios.add(fixed)
|
|
349
|
+
log?.('warn', `固定场景 "${fixed}" 不在场景库中,自动决策已停用(请检查 autoScenario 配置;可用:${scenarios.map((s) => s.id).join('、')})`)
|
|
350
|
+
}
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
310
353
|
return { scenario, source: '固定绑定', confidence: null }
|
|
311
354
|
}
|
|
312
355
|
if (scenarios.length === 0) return null
|
|
@@ -365,8 +408,9 @@ function questionLabel(scenario, qid) {
|
|
|
365
408
|
/**
|
|
366
409
|
* 判断是否为"无业务含义"的输入,用于跳过自动决策。
|
|
367
410
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
411
|
+
* 两条独立的跳过规则(命中任一即跳过):
|
|
412
|
+
* 1. 长度阈值:短于 autoMinChars 的一律跳过(默认 4,"退款失败" 恰好 4 字不被误伤);
|
|
413
|
+
* 2. 寒暄黑名单:整体就是一句寒暄/致谢的跳过,与长度无关。
|
|
370
414
|
*/
|
|
371
415
|
const TRIVIAL_PATTERN = /^(你好|您好|哈喽|在吗|hi|hello|hey|ok|okay|好的|好嘞|收到|明白|嗯+|谢谢|多谢|感谢|thanks|thank you|thx|再见|拜拜|bye|测试|test)[\s!!。.~~,,、??]*$/i
|
|
372
416
|
|
|
@@ -387,7 +431,7 @@ function captureState(agent, newText, maxMessages) {
|
|
|
387
431
|
const history = readHistory(agent, limit)
|
|
388
432
|
const parts = []
|
|
389
433
|
if (history.length > 0) parts.push(`【近期对话】\n${history.join('\n')}`)
|
|
390
|
-
parts.push(`【当前请求】\n${newText}`)
|
|
434
|
+
parts.push(`【当前请求】\n${truncate(newText, MAX_CURRENT_CHARS)}`)
|
|
391
435
|
return parts.join('\n\n')
|
|
392
436
|
}
|
|
393
437
|
|
package/lib/client.js
CHANGED
|
@@ -89,12 +89,14 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
autoRouteMinConfidence: '自动路由置信度门槛',
|
|
90
90
|
autoRouteMinConfidenceHint: '低于该值时宁可不注入,避免塞入瞎猜的结论',
|
|
91
91
|
autoMinChars: '最小触发长度(字符)',
|
|
92
|
-
autoMinCharsHint: '
|
|
92
|
+
autoMinCharsHint: '低于该长度、或整体就是寒暄/致谢时跳过',
|
|
93
93
|
save: '保存',
|
|
94
94
|
saving: '保存中…',
|
|
95
95
|
revert: '撤销修改',
|
|
96
96
|
saved: '已保存,立即生效',
|
|
97
97
|
saveFailed: '保存失败,请检查填写内容',
|
|
98
|
+
numberInvalid: '必须是有效数字',
|
|
99
|
+
numberRequired: '数字字段不能为空',
|
|
98
100
|
conflict: '配置已被其他地方改动,已刷新为最新值,请重新修改',
|
|
99
101
|
readOnly: '当前不可编辑',
|
|
100
102
|
noForm: '宿主要求该条目声明 volatile 配置字段后才会显示表单。',
|
|
@@ -157,12 +159,14 @@ window.__ModuleLoader__.load({
|
|
|
157
159
|
autoRouteMinConfidence: 'Auto routing confidence floor',
|
|
158
160
|
autoRouteMinConfidenceHint: 'Below this value nothing is injected rather than guessing',
|
|
159
161
|
autoMinChars: 'Minimum trigger length (chars)',
|
|
160
|
-
autoMinCharsHint: '
|
|
162
|
+
autoMinCharsHint: 'Skipped when shorter than this, or when it is a bare greeting',
|
|
161
163
|
save: 'Save',
|
|
162
164
|
saving: 'Saving…',
|
|
163
165
|
revert: 'Revert changes',
|
|
164
166
|
saved: 'Saved — applied immediately',
|
|
165
167
|
saveFailed: 'Save failed — check the values',
|
|
168
|
+
numberInvalid: 'Must be a valid number',
|
|
169
|
+
numberRequired: 'Number fields cannot be empty',
|
|
166
170
|
conflict: 'The configuration changed elsewhere; the newest values were reloaded',
|
|
167
171
|
readOnly: 'Read-only right now',
|
|
168
172
|
noForm: 'The host exposes a form only when the entry declares volatile config fields.',
|
|
@@ -261,7 +265,6 @@ window.__ModuleLoader__.load({
|
|
|
261
265
|
|
|
262
266
|
/* ────────────────────────────── 字段定义 ────────────────────────────── */
|
|
263
267
|
|
|
264
|
-
/** 数字字段在草稿里保持字符串,保存时再转数字,避免用户输入中途被强制转换。 */
|
|
265
268
|
/**
|
|
266
269
|
* 内置场景候选(供「固定场景」输入框的下拉提示)。
|
|
267
270
|
*
|
|
@@ -349,7 +352,7 @@ window.__ModuleLoader__.load({
|
|
|
349
352
|
: null
|
|
350
353
|
if (keys === null) issues.push(`问题 ${qid}(choice)需要 criteria 对象`)
|
|
351
354
|
else if (keys.length < 2) issues.push(`问题 ${qid}(choice)至少 2 个选项`)
|
|
352
|
-
else if (keys.length > 26) issues.push(`问题 ${qid}(choice
|
|
355
|
+
else if (keys.length > 26) issues.push(`问题 ${qid}(choice)选项不能超过 26 个(当前 ${keys.length})`)
|
|
353
356
|
}
|
|
354
357
|
if (q.type === 'score'
|
|
355
358
|
&& (!Array.isArray(q.criteria) || q.criteria.length < 2)) {
|
|
@@ -401,7 +404,7 @@ window.__ModuleLoader__.load({
|
|
|
401
404
|
/**
|
|
402
405
|
* 一个可折叠的配置卡片:标题 + 说明,展开后是字段表单。
|
|
403
406
|
*/
|
|
404
|
-
function ConfigCard({
|
|
407
|
+
function ConfigCard({ title, description, open, onToggle, children }) {
|
|
405
408
|
return hs('li', {
|
|
406
409
|
style: cardStyle,
|
|
407
410
|
children: [
|
|
@@ -492,7 +495,7 @@ window.__ModuleLoader__.load({
|
|
|
492
495
|
control = h('input', {
|
|
493
496
|
id: controlId,
|
|
494
497
|
style: inputStyle,
|
|
495
|
-
type: field.kind === 'password' ? 'password' :
|
|
498
|
+
type: field.kind === 'password' ? 'password' : 'text',
|
|
496
499
|
inputMode: field.kind === 'number' ? 'decimal' : undefined,
|
|
497
500
|
value,
|
|
498
501
|
disabled,
|
|
@@ -579,8 +582,22 @@ window.__ModuleLoader__.load({
|
|
|
579
582
|
const customCheck = validateCustomScenarios(values.customScenarios)
|
|
580
583
|
const customErrorText = customCheck.ok ? undefined : describeCustomError(customCheck.error, t)
|
|
581
584
|
|
|
585
|
+
// 数字字段同样先校验:非法值直接拦住保存,
|
|
586
|
+
// 而不是保存时静默丢弃却仍提示「已保存」
|
|
587
|
+
const numberErrors = {}
|
|
588
|
+
for (const field of ALL_FIELDS) {
|
|
589
|
+
if (field.kind !== 'number') continue
|
|
590
|
+
const raw = String(values[field.key] ?? '').trim()
|
|
591
|
+
if (raw === '') numberErrors[field.key] = t('numberRequired')
|
|
592
|
+
else if (!Number.isFinite(Number(raw))) numberErrors[field.key] = t('numberInvalid')
|
|
593
|
+
}
|
|
594
|
+
const numberExtras = {}
|
|
595
|
+
for (const [key, error] of Object.entries(numberErrors)) {
|
|
596
|
+
numberExtras[key] = { error }
|
|
597
|
+
}
|
|
598
|
+
|
|
582
599
|
const dirty = draft !== null && Object.keys(draft).some((key) => draft[key] !== current[key])
|
|
583
|
-
const canSave = dirty && !busy && writable && customCheck.ok
|
|
600
|
+
const canSave = dirty && !busy && writable && customCheck.ok && Object.keys(numberErrors).length === 0
|
|
584
601
|
|
|
585
602
|
// 「固定场景」候选 = 10 个内置 + 当前生效的自定义场景 id
|
|
586
603
|
const scenarioOptions = [
|
|
@@ -602,6 +619,7 @@ window.__ModuleLoader__.load({
|
|
|
602
619
|
setBusy(true)
|
|
603
620
|
setNotice(null)
|
|
604
621
|
try {
|
|
622
|
+
// 数字字段在草稿里保持字符串,这里再转数字(合法性已由上方 numberErrors 拦截)
|
|
605
623
|
const ops = []
|
|
606
624
|
for (const [key, value] of Object.entries(draft ?? {})) {
|
|
607
625
|
if (value === current[key]) continue
|
|
@@ -661,12 +679,23 @@ window.__ModuleLoader__.load({
|
|
|
661
679
|
],
|
|
662
680
|
})
|
|
663
681
|
|
|
682
|
+
// 三张卡片共用同一套 footer:save(可选 revert / 额外状态)+ 状态行
|
|
683
|
+
const renderFooter = ({ showRevert, extra }) => hs('div', {
|
|
684
|
+
key: 'footer',
|
|
685
|
+
style: footerStyle,
|
|
686
|
+
children: [
|
|
687
|
+
h('button', { key: 'save', type: 'button', style: { ...primaryButton, opacity: canSave ? 1 : 0.5 }, disabled: !canSave, onClick: onSave, children: busy ? t('saving') : t('save') }),
|
|
688
|
+
...(showRevert ? [h('button', { key: 'revert', type: 'button', style: ghostButton, disabled: !dirty || busy, onClick: () => { setDraft(null); setNotice(null) }, children: t('revert') })] : []),
|
|
689
|
+
...(extra ? [extra] : []),
|
|
690
|
+
statusLine,
|
|
691
|
+
],
|
|
692
|
+
})
|
|
693
|
+
|
|
664
694
|
return hs('ul', {
|
|
665
695
|
style: listStyle,
|
|
666
696
|
children: [
|
|
667
697
|
h(ConfigCard, {
|
|
668
698
|
key: 'service',
|
|
669
|
-
t,
|
|
670
699
|
title: t('tabService'),
|
|
671
700
|
description: t('tabServiceHint'),
|
|
672
701
|
open: openService,
|
|
@@ -674,39 +703,13 @@ window.__ModuleLoader__.load({
|
|
|
674
703
|
// 注意:jsx(type, props, key) 的第三个参数是 key,正文必须作为 children 放进 props
|
|
675
704
|
children: hs(Fragment, {
|
|
676
705
|
children: [
|
|
677
|
-
...renderFields(SERVICE_FIELDS),
|
|
678
|
-
|
|
679
|
-
key: 'footer',
|
|
680
|
-
style: footerStyle,
|
|
681
|
-
children: [
|
|
682
|
-
h('button', {
|
|
683
|
-
key: 'save',
|
|
684
|
-
type: 'button',
|
|
685
|
-
style: { ...primaryButton, opacity: canSave ? 1 : 0.5 },
|
|
686
|
-
disabled: !canSave,
|
|
687
|
-
onClick: onSave,
|
|
688
|
-
children: busy ? t('saving') : t('save'),
|
|
689
|
-
}),
|
|
690
|
-
h('button', {
|
|
691
|
-
key: 'revert',
|
|
692
|
-
type: 'button',
|
|
693
|
-
style: ghostButton,
|
|
694
|
-
disabled: !dirty || busy,
|
|
695
|
-
onClick: () => {
|
|
696
|
-
setDraft(null)
|
|
697
|
-
setNotice(null)
|
|
698
|
-
},
|
|
699
|
-
children: t('revert'),
|
|
700
|
-
}),
|
|
701
|
-
statusLine,
|
|
702
|
-
],
|
|
703
|
-
}),
|
|
706
|
+
...renderFields(SERVICE_FIELDS, numberExtras),
|
|
707
|
+
renderFooter({ showRevert: true }),
|
|
704
708
|
],
|
|
705
709
|
}),
|
|
706
710
|
}),
|
|
707
711
|
h(ConfigCard, {
|
|
708
712
|
key: 'custom',
|
|
709
|
-
t,
|
|
710
713
|
title: t('tabCustom'),
|
|
711
714
|
description: t('tabCustomHint'),
|
|
712
715
|
open: openCustom,
|
|
@@ -763,69 +766,31 @@ window.__ModuleLoader__.load({
|
|
|
763
766
|
}),
|
|
764
767
|
},
|
|
765
768
|
}),
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}),
|
|
778
|
-
h('button', {
|
|
779
|
-
key: 'revert',
|
|
780
|
-
type: 'button',
|
|
781
|
-
style: ghostButton,
|
|
782
|
-
disabled: !dirty || busy,
|
|
783
|
-
onClick: () => {
|
|
784
|
-
setDraft(null)
|
|
785
|
-
setNotice(null)
|
|
786
|
-
},
|
|
787
|
-
children: t('revert'),
|
|
788
|
-
}),
|
|
789
|
-
customCheck.ok
|
|
790
|
-
? h('span', {
|
|
791
|
-
key: 'ok',
|
|
792
|
-
style: statusStyle,
|
|
793
|
-
children: customCheck.count === 0
|
|
794
|
-
? t('jsonEmpty')
|
|
795
|
-
: `${t('jsonOk', { count: customCheck.count })} ${t('customIdsUsed')}`,
|
|
796
|
-
})
|
|
797
|
-
: null,
|
|
798
|
-
statusLine,
|
|
799
|
-
],
|
|
769
|
+
renderFooter({
|
|
770
|
+
showRevert: true,
|
|
771
|
+
extra: customCheck.ok
|
|
772
|
+
? h('span', {
|
|
773
|
+
key: 'ok',
|
|
774
|
+
style: statusStyle,
|
|
775
|
+
children: customCheck.count === 0
|
|
776
|
+
? t('jsonEmpty')
|
|
777
|
+
: `${t('jsonOk', { count: customCheck.count })} ${t('customIdsUsed')}`,
|
|
778
|
+
})
|
|
779
|
+
: null,
|
|
800
780
|
}),
|
|
801
781
|
],
|
|
802
782
|
}),
|
|
803
783
|
}),
|
|
804
784
|
h(ConfigCard, {
|
|
805
785
|
key: 'auto',
|
|
806
|
-
t,
|
|
807
786
|
title: t('tabAuto'),
|
|
808
787
|
description: t('tabAutoHint'),
|
|
809
788
|
open: openAuto,
|
|
810
789
|
onToggle: () => setOpenAuto((value) => !value),
|
|
811
790
|
children: hs(Fragment, {
|
|
812
791
|
children: [
|
|
813
|
-
...renderFields(AUTO_FIELDS),
|
|
814
|
-
|
|
815
|
-
key: 'footer',
|
|
816
|
-
style: footerStyle,
|
|
817
|
-
children: [
|
|
818
|
-
h('button', {
|
|
819
|
-
key: 'save',
|
|
820
|
-
type: 'button',
|
|
821
|
-
style: { ...primaryButton, opacity: canSave ? 1 : 0.5 },
|
|
822
|
-
disabled: !canSave,
|
|
823
|
-
onClick: onSave,
|
|
824
|
-
children: busy ? t('saving') : t('save'),
|
|
825
|
-
}),
|
|
826
|
-
statusLine,
|
|
827
|
-
],
|
|
828
|
-
}),
|
|
792
|
+
...renderFields(AUTO_FIELDS, numberExtras),
|
|
793
|
+
renderFooter({ showRevert: false }),
|
|
829
794
|
],
|
|
830
795
|
}),
|
|
831
796
|
}),
|
|
@@ -872,8 +837,3 @@ window.__ModuleLoader__.load({
|
|
|
872
837
|
return module.exports
|
|
873
838
|
},
|
|
874
839
|
})
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
package/lib/format.js
CHANGED
|
@@ -11,6 +11,7 @@ export function formatSummary({
|
|
|
11
11
|
latencyMs,
|
|
12
12
|
lines = [],
|
|
13
13
|
recommendation,
|
|
14
|
+
notes = [],
|
|
14
15
|
needsHumanReview = false,
|
|
15
16
|
}) {
|
|
16
17
|
const out = [
|
|
@@ -18,10 +19,11 @@ export function formatSummary({
|
|
|
18
19
|
`- **决策服务**:${provider} / ${model}`,
|
|
19
20
|
...lines,
|
|
20
21
|
]
|
|
22
|
+
for (const note of notes) out.push(`- **注意**:${note}`)
|
|
21
23
|
if (recommendation) out.push(`- **建议**:${recommendation}`)
|
|
22
24
|
out.push(`- **建议复核**:${needsHumanReview ? '需要人工复核' : '无需人工复核'}`)
|
|
23
25
|
if (requestId) out.push(`- **请求 ID**:${requestId}(延迟 ${latencyMs}ms)`)
|
|
24
|
-
return out.
|
|
26
|
+
return out.join('\n')
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -53,7 +55,7 @@ export function normalizeAnswers(answers, questions = {}) {
|
|
|
53
55
|
decision[qid] = Number.isFinite(score) ? idx : null
|
|
54
56
|
const legend = raw.legend || {}
|
|
55
57
|
const scale = Array.isArray(question?.criteria) ? question.criteria : []
|
|
56
|
-
labels[qid] = idx != null ? (legend[String(idx)] ?? scale[
|
|
58
|
+
labels[qid] = idx != null ? (legend[String(idx)] ?? scale[idx] ?? String(idx)) : '无法判断'
|
|
57
59
|
if (Number.isFinite(Number(raw.confidence))) confidences[qid] = Number(raw.confidence)
|
|
58
60
|
continue
|
|
59
61
|
}
|
|
@@ -150,13 +152,21 @@ export function noulLine(label, answer) {
|
|
|
150
152
|
return `- **${label}**:${v >= 0.5 ? '是' : '否'}(${(v * 100).toFixed(1)}%)`
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/** noul 落在该区间视为「摇摆不定」(接近 0.5),建议人工复核。 */
|
|
156
|
+
const NOUL_UNCERTAIN_LOW = 0.45
|
|
157
|
+
const NOUL_UNCERTAIN_HIGH = 0.55
|
|
158
|
+
|
|
159
|
+
/** choice 返回这些取值时视为「无法判断」。 */
|
|
160
|
+
const UNCERTAIN_CHOICES = new Set(['uncertain', 'unknown'])
|
|
161
|
+
|
|
153
162
|
/** 是否需要人工复核:置信度低于阈值,或出现不确定项。 */
|
|
154
163
|
export function computeNeedsHumanReview(answers, minConfidence) {
|
|
155
164
|
for (const a of Object.values(answers || {})) {
|
|
156
165
|
if (!a || typeof a !== 'object') continue
|
|
157
166
|
if (typeof a.confidence === 'number' && a.confidence < minConfidence) return true
|
|
158
|
-
if (a.type === 'choice' && (a.choice == null ||
|
|
159
|
-
if (a.type === 'noul' && typeof a.noul === 'number'
|
|
167
|
+
if (a.type === 'choice' && (a.choice == null || UNCERTAIN_CHOICES.has(a.choice))) return true
|
|
168
|
+
if (a.type === 'noul' && typeof a.noul === 'number'
|
|
169
|
+
&& a.noul > NOUL_UNCERTAIN_LOW && a.noul < NOUL_UNCERTAIN_HIGH) return true
|
|
160
170
|
}
|
|
161
171
|
return false
|
|
162
172
|
}
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @master0071/dsh-systemone 插件入口。
|
|
3
3
|
*
|
|
4
4
|
* 架构:Cordis 插件(Service 形态)。依赖 harness 的 `tools`(工具注册表)
|
|
5
|
-
* 与 `logger
|
|
5
|
+
* 与 `logger`;浏览器侧配置页由 client.js 提供(经 dsh.client 注入)。
|
|
6
6
|
*
|
|
7
7
|
* - 注册 2 个 Agent 工具:
|
|
8
8
|
* systemone_scenario — 统一场景调度(list / describe / run)
|
|
@@ -16,8 +16,11 @@ import z from '@deepseek-ai/schemastery'
|
|
|
16
16
|
import { createProvider } from './provider.js'
|
|
17
17
|
import { createTools } from './tools.js'
|
|
18
18
|
import { resolveScenarios } from './scenarios.js'
|
|
19
|
-
import { installAutoDecide } from './auto.js'
|
|
20
|
-
import { readBoolean, readString } from './volatile.js'
|
|
19
|
+
import { installAutoDecide, AUTO_DEFAULTS } from './auto.js'
|
|
20
|
+
import { readBoolean, readString, unwrap } from './volatile.js'
|
|
21
|
+
|
|
22
|
+
/** 合法的提供商类型;其他值会按 unisound 处理并告警一次。 */
|
|
23
|
+
const KNOWN_PROVIDERS = ['unisound', 'http', 'mock']
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* 把「当前提供商」包成一个稳定引用的对象,内部每次调用都转发到最新实例。
|
|
@@ -79,17 +82,17 @@ const Config = z.object({
|
|
|
79
82
|
/** 注入通道:message=本步生效(pre-step 注入);context=官方动态上下文(晚一步生效) */
|
|
80
83
|
autoInject: z.string().default('message').volatile(),
|
|
81
84
|
/** 自动决策超时(毫秒),超时即放行,绝不阻塞会话 */
|
|
82
|
-
autoTimeoutMs: z.number().min(500).default(
|
|
85
|
+
autoTimeoutMs: z.number().min(500).default(AUTO_DEFAULTS.autoTimeoutMs).volatile(),
|
|
83
86
|
/** 捕获的历史消息条数(不含本轮新消息) */
|
|
84
|
-
autoMaxMessages: z.number().min(1).max(50).default(
|
|
87
|
+
autoMaxMessages: z.number().min(1).max(50).default(AUTO_DEFAULTS.autoMaxMessages).volatile(),
|
|
85
88
|
/** 相同内容的结果缓存时长(毫秒),0 表示不缓存 */
|
|
86
|
-
autoCacheTtlMs: z.number().min(0).default(
|
|
89
|
+
autoCacheTtlMs: z.number().min(0).default(AUTO_DEFAULTS.autoCacheTtlMs).volatile(),
|
|
87
90
|
/** 自动决策使用的置信度阈值 */
|
|
88
|
-
autoMinConfidence: z.number().min(0).max(1).default(
|
|
91
|
+
autoMinConfidence: z.number().min(0).max(1).default(AUTO_DEFAULTS.autoMinConfidence).volatile(),
|
|
89
92
|
/** 自动路由的置信度门槛:低于该值宁可不注入,避免塞入瞎猜的结论 */
|
|
90
|
-
autoRouteMinConfidence: z.number().min(0).max(1).default(
|
|
93
|
+
autoRouteMinConfidence: z.number().min(0).max(1).default(AUTO_DEFAULTS.autoRouteMinConfidence).volatile(),
|
|
91
94
|
/** 触发自动决策的最小输入长度(字符);低于该长度且整体是寒暄才跳过 */
|
|
92
|
-
autoMinChars: z.number().min(0).default(
|
|
95
|
+
autoMinChars: z.number().min(0).default(AUTO_DEFAULTS.autoMinChars).volatile(),
|
|
93
96
|
})
|
|
94
97
|
|
|
95
98
|
export default class SystemOneService extends Service {
|
|
@@ -105,7 +108,7 @@ export default class SystemOneService extends Service {
|
|
|
105
108
|
|
|
106
109
|
/**
|
|
107
110
|
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
108
|
-
* @param {
|
|
111
|
+
* @param {object} config - 经 Config schema 校验的插件配置(volatile 字段是引用对象,读取须经 volatile.js 脱壳)
|
|
109
112
|
*/
|
|
110
113
|
constructor(ctx, config) {
|
|
111
114
|
super(ctx, 'systemone')
|
|
@@ -114,13 +117,16 @@ export default class SystemOneService extends Service {
|
|
|
114
117
|
|
|
115
118
|
// provider 字段是 volatile:配置页里切换 unisound / http / mock 时,
|
|
116
119
|
// 下面这个代理会按需重建真正的提供商,工具与自动决策无需改动即可跟随。
|
|
117
|
-
this.
|
|
118
|
-
this._providerKind =
|
|
120
|
+
this._warnedProviders = new Set()
|
|
121
|
+
this._providerKind = null
|
|
122
|
+
this._provider = null
|
|
119
123
|
this.provider = createProviderProxy(() => this.currentProvider())
|
|
124
|
+
this.currentProvider()
|
|
120
125
|
|
|
121
126
|
// 场景库同样是热更新的:customScenarios 一变就原地重建,
|
|
122
127
|
// 工具与自动决策持有的是同一个 liveScenarios 引用,因此无需重新装配。
|
|
123
128
|
this._scenarioSource = null
|
|
129
|
+
this._scenarioRawValue = undefined
|
|
124
130
|
this.scenarioProblems = []
|
|
125
131
|
this._scenarioArray = []
|
|
126
132
|
this._refreshScenarios()
|
|
@@ -164,12 +170,22 @@ export default class SystemOneService extends Service {
|
|
|
164
170
|
currentProvider() {
|
|
165
171
|
const kind = readString(this.config, 'provider', 'unisound')
|
|
166
172
|
if (kind !== this._providerKind) {
|
|
167
|
-
this.
|
|
173
|
+
this._warnUnknownProvider(kind)
|
|
174
|
+
this._provider = createProvider(this.config)
|
|
168
175
|
this._providerKind = kind
|
|
169
176
|
}
|
|
170
177
|
return this._provider
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
/** 未知 provider 值会在 provider.js 里静默回退到 unisound,这里负责告警(每个未知值只提示一次)。 */
|
|
181
|
+
_warnUnknownProvider(kind) {
|
|
182
|
+
if (KNOWN_PROVIDERS.includes(kind) || this._warnedProviders.has(kind)) return
|
|
183
|
+
this._warnedProviders.add(kind)
|
|
184
|
+
this.ctx?.logger?.warn?.(
|
|
185
|
+
`@master0071/dsh-systemone: 未知 provider "${kind}",将按 unisound 处理(可选:${KNOWN_PROVIDERS.join(' / ')})`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
/**
|
|
174
190
|
* 场景库的「活引用」。
|
|
175
191
|
*
|
|
@@ -208,7 +224,13 @@ export default class SystemOneService extends Service {
|
|
|
208
224
|
* 因为工具与自动决策捕获的是那个数组本身。
|
|
209
225
|
*/
|
|
210
226
|
_refreshScenarios() {
|
|
211
|
-
|
|
227
|
+
// 先做引用比较快速返回:Proxy 的每次属性访问都会走到这里,
|
|
228
|
+
// 字符串不可变,volatile 热更新换值时会换引用,引用相同即配置未变,
|
|
229
|
+
// 避免对大段自定义场景 JSON 反复做全量 String() + trim + 比较。
|
|
230
|
+
const value = unwrap(this.config?.customScenarios) ?? ''
|
|
231
|
+
if (value === this._scenarioRawValue) return
|
|
232
|
+
this._scenarioRawValue = value
|
|
233
|
+
const source = String(value).trim()
|
|
212
234
|
if (source === this._scenarioSource) return
|
|
213
235
|
const { scenarios, problems } = resolveScenarios(source)
|
|
214
236
|
this._scenarioArray.length = 0
|
package/lib/provider.js
CHANGED
|
@@ -20,23 +20,21 @@ import { readNumber, readString } from './volatile.js'
|
|
|
20
20
|
const DEFAULT_BASE_URL = 'https://maas-api.unisound.com/v1'
|
|
21
21
|
const DEFAULT_MODEL = 'u2-decision'
|
|
22
22
|
|
|
23
|
-
/** 创建提供商实例。config 来自插件配置(已由 Config schema
|
|
24
|
-
export function createProvider(
|
|
23
|
+
/** 创建提供商实例。config 来自插件配置(已由 Config schema 校验)。未知 provider 值回退到 unisound(入口层负责告警)。 */
|
|
24
|
+
export function createProvider(config) {
|
|
25
25
|
const kind = readString(config, 'provider', 'unisound')
|
|
26
26
|
if (kind === 'mock') return new MockProvider(config)
|
|
27
|
-
if (kind === 'http') return new HttpProvider(
|
|
28
|
-
return new UnisoundProvider(
|
|
27
|
+
if (kind === 'http') return new HttpProvider(config, 'http')
|
|
28
|
+
return new UnisoundProvider(config)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** 通用 HTTP 提供商:请求 SystemOne 兼容端点,Bearer 认证。 */
|
|
32
32
|
export class HttpProvider {
|
|
33
33
|
/**
|
|
34
|
-
* @param {object} ctx - Cordis 上下文
|
|
35
34
|
* @param {object} config - 插件配置
|
|
36
35
|
* @param {string} [name] - 提供商显示名
|
|
37
36
|
*/
|
|
38
|
-
constructor(
|
|
39
|
-
this.ctx = ctx
|
|
37
|
+
constructor(config, name = 'http') {
|
|
40
38
|
this.config = config || {}
|
|
41
39
|
this.name = name
|
|
42
40
|
}
|
|
@@ -83,6 +81,7 @@ export class HttpProvider {
|
|
|
83
81
|
|
|
84
82
|
const controller = new AbortController()
|
|
85
83
|
const timer = setTimeout(() => controller.abort(new Error(`SystemOne: 请求超时(${timeoutMs}ms)`)), timeoutMs)
|
|
84
|
+
timer.unref?.()
|
|
86
85
|
const onOuterAbort = () => controller.abort()
|
|
87
86
|
if (signal && typeof signal.addEventListener === 'function') {
|
|
88
87
|
signal.addEventListener('abort', onOuterAbort, { once: true })
|
|
@@ -128,8 +127,8 @@ export class HttpProvider {
|
|
|
128
127
|
|
|
129
128
|
/** Unisound 官方提供商(默认)。 */
|
|
130
129
|
export class UnisoundProvider extends HttpProvider {
|
|
131
|
-
constructor(
|
|
132
|
-
super(
|
|
130
|
+
constructor(config) {
|
|
131
|
+
super({ ...config, provider: 'unisound' }, 'unisound')
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
|
package/lib/tools.js
CHANGED
|
@@ -79,7 +79,15 @@ function scenarioTool(provider, config, scenarios) {
|
|
|
79
79
|
if (action === 'describe') {
|
|
80
80
|
return describeScenario(scenarios, args.scenario)
|
|
81
81
|
}
|
|
82
|
-
return
|
|
82
|
+
return runScenarioDecision({
|
|
83
|
+
provider,
|
|
84
|
+
config,
|
|
85
|
+
scenarios,
|
|
86
|
+
scenario: args.scenario,
|
|
87
|
+
state: args.state,
|
|
88
|
+
params: args.params,
|
|
89
|
+
exec,
|
|
90
|
+
})
|
|
83
91
|
},
|
|
84
92
|
presentCall: (args) => ({
|
|
85
93
|
card: 'generic',
|
|
@@ -156,18 +164,6 @@ function describeScenario(scenarios, id) {
|
|
|
156
164
|
return { ok: true, action: 'describe', scenario: scenario.id, title: scenario.title, questions, summary }
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
async function runScenario({ provider, config, scenarios, args, exec }) {
|
|
160
|
-
return runScenarioDecision({
|
|
161
|
-
provider,
|
|
162
|
-
config,
|
|
163
|
-
scenarios,
|
|
164
|
-
scenario: args.scenario,
|
|
165
|
-
state: args.state,
|
|
166
|
-
params: args.params,
|
|
167
|
-
exec,
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
|
|
171
167
|
/**
|
|
172
168
|
* 执行一次场景决策(工具与自动决策共用的核心入口)。
|
|
173
169
|
*
|
|
@@ -179,7 +175,6 @@ async function runScenario({ provider, config, scenarios, args, exec }) {
|
|
|
179
175
|
* @param {unknown} options.state - 业务上下文
|
|
180
176
|
* @param {object} [options.params] - 按问题 id 覆盖问题定义
|
|
181
177
|
* @param {object} [options.exec] - 工具执行上下文(取 signal)
|
|
182
|
-
* @param {string} [options.model] - 覆盖模型
|
|
183
178
|
* @param {number} [options.minConfidence] - 覆盖置信度阈值
|
|
184
179
|
* @returns {Promise<object>} 与工具输出同构的决策结果
|
|
185
180
|
*/
|
|
@@ -191,7 +186,6 @@ export async function runScenarioDecision({
|
|
|
191
186
|
state,
|
|
192
187
|
params,
|
|
193
188
|
exec,
|
|
194
|
-
model,
|
|
195
189
|
minConfidence,
|
|
196
190
|
}) {
|
|
197
191
|
const scenario = findScenario(scenarios, scenarioRef)
|
|
@@ -212,7 +206,7 @@ export async function runScenarioDecision({
|
|
|
212
206
|
}
|
|
213
207
|
}
|
|
214
208
|
|
|
215
|
-
const questions = applyParams(scenario.questions, params)
|
|
209
|
+
const { questions, ignored } = applyParams(scenario.questions, params)
|
|
216
210
|
const configThreshold = readNumber(config, 'minConfidence', NaN)
|
|
217
211
|
const threshold = Number.isFinite(Number(minConfidence))
|
|
218
212
|
? Number(minConfidence)
|
|
@@ -231,7 +225,9 @@ export async function runScenarioDecision({
|
|
|
231
225
|
config,
|
|
232
226
|
minConfidence: threshold,
|
|
233
227
|
exec,
|
|
234
|
-
|
|
228
|
+
warnings: ignored.length > 0
|
|
229
|
+
? [`以下 params 覆盖无效,已忽略:${ignored.join(';')}`]
|
|
230
|
+
: [],
|
|
235
231
|
finalize(answers) {
|
|
236
232
|
const { decision, labels, confidences } = normalizeAnswers(answers, questions)
|
|
237
233
|
const derived = deriveFields(scenario.derive, decision)
|
|
@@ -243,10 +239,16 @@ export async function runScenarioDecision({
|
|
|
243
239
|
})
|
|
244
240
|
}
|
|
245
241
|
|
|
246
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* 按 params 覆盖问题定义,并收集无法应用的覆盖项(类型不匹配、未知问题 id),
|
|
244
|
+
* 让调用方能把「传了但没生效」反馈给模型,而不是静默丢弃。
|
|
245
|
+
*
|
|
246
|
+
* @returns {{ questions: object, ignored: string[] }}
|
|
247
|
+
*/
|
|
247
248
|
function applyParams(questions, params) {
|
|
248
|
-
if (!params || typeof params !== 'object') return questions
|
|
249
|
+
if (!params || typeof params !== 'object') return { questions, ignored: [] }
|
|
249
250
|
const merged = {}
|
|
251
|
+
const ignored = []
|
|
250
252
|
for (const [qid, question] of Object.entries(questions)) {
|
|
251
253
|
const override = params[qid]
|
|
252
254
|
if (!override || typeof override !== 'object') {
|
|
@@ -268,14 +270,23 @@ function applyParams(questions, params) {
|
|
|
268
270
|
)
|
|
269
271
|
} else if (question.type === 'score' && Array.isArray(override.criteria) && override.criteria.length >= 2) {
|
|
270
272
|
next.criteria = override.criteria.map((v) => String(v))
|
|
273
|
+
} else {
|
|
274
|
+
ignored.push(`${qid}.criteria(与问题类型 ${question.type} 不匹配)`)
|
|
271
275
|
}
|
|
272
276
|
}
|
|
273
|
-
if (override.addCriteria
|
|
274
|
-
|
|
277
|
+
if (override.addCriteria) {
|
|
278
|
+
if (question.type === 'choice' && !Array.isArray(override.addCriteria)) {
|
|
279
|
+
next.criteria = { ...(next.criteria || {}), ...override.addCriteria }
|
|
280
|
+
} else {
|
|
281
|
+
ignored.push(`${qid}.addCriteria(仅支持 choice 问题)`)
|
|
282
|
+
}
|
|
275
283
|
}
|
|
276
284
|
merged[qid] = next
|
|
277
285
|
}
|
|
278
|
-
|
|
286
|
+
for (const qid of Object.keys(params)) {
|
|
287
|
+
if (!(qid in questions)) ignored.push(`${qid}(未知问题 id)`)
|
|
288
|
+
}
|
|
289
|
+
return { questions: merged, ignored }
|
|
279
290
|
}
|
|
280
291
|
|
|
281
292
|
/* ─── 2. 通用决策工具 ─────────────────────────────────────────────────────── */
|
|
@@ -357,6 +368,7 @@ async function executeDecision({
|
|
|
357
368
|
exec,
|
|
358
369
|
model,
|
|
359
370
|
finalize,
|
|
371
|
+
warnings = [],
|
|
360
372
|
}) {
|
|
361
373
|
try {
|
|
362
374
|
const started = Date.now()
|
|
@@ -383,6 +395,7 @@ async function executeDecision({
|
|
|
383
395
|
latencyMs,
|
|
384
396
|
lines,
|
|
385
397
|
recommendation,
|
|
398
|
+
notes: warnings,
|
|
386
399
|
needsHumanReview,
|
|
387
400
|
})
|
|
388
401
|
return {
|
|
@@ -399,6 +412,7 @@ async function executeDecision({
|
|
|
399
412
|
confidences,
|
|
400
413
|
needs_human_review: needsHumanReview,
|
|
401
414
|
recommendation,
|
|
415
|
+
warnings,
|
|
402
416
|
summary,
|
|
403
417
|
}
|
|
404
418
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@master0071/dsh-systemone",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision,可切换提供商)接入软件开发、客服、内容审核、路由、销售线索、风控、招聘、数据治理、教育、需求变更等 10 大业务场景,为 Agent 返回带概率分布的决策与处置建议。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|