@master0071/dsh-systemone 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +3 -2
- package/lib/auto.js +77 -45
- package/lib/client.js +25 -15
- package/lib/format.js +14 -4
- package/lib/index.js +29 -7
- 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,12 @@ 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
|
+
|
|
39
45
|
/**
|
|
40
46
|
* 安装自动决策。返回卸载函数。
|
|
41
47
|
*
|
|
@@ -56,9 +62,8 @@ export function installAutoDecide(ctx, deps) {
|
|
|
56
62
|
const cache = new Map()
|
|
57
63
|
/** stateHash → Promise:并发相同 state 只发一次上游请求。 */
|
|
58
64
|
const inFlight = new Map()
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const routeFloor = readNumber(config, 'autoRouteMinConfidence', 0)
|
|
65
|
+
/** 已告警过的无效固定场景 id:同一错误只提示一次,避免每步刷日志。 */
|
|
66
|
+
const warnedScenarios = new Set()
|
|
62
67
|
|
|
63
68
|
const log = (level, message) => {
|
|
64
69
|
try {
|
|
@@ -79,40 +84,44 @@ export function installAutoDecide(ctx, deps) {
|
|
|
79
84
|
}))
|
|
80
85
|
|
|
81
86
|
// ── context 通道:入箱预计算 + 动态上下文渲染(不写历史,第二步起稳定可见) ──
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
87
|
+
// 监听器无条件注册,autoInject 的门控放在事件回调里按当前配置判断:
|
|
88
|
+
// autoInject 是 volatile 字段,只有事件期判断才能让 message ↔ context
|
|
89
|
+
// 在运行期双向切换都立即生效(在装配期固化只支持一个方向)。
|
|
90
|
+
disposers.push(ctx.on('agent/created', (payload) => {
|
|
91
|
+
const agent = payload?.agent
|
|
92
|
+
const agentCtx = agent?.ctx
|
|
93
|
+
if (!agentCtx) return
|
|
94
|
+
// ① 消息一入箱就异步预计算,让决策尽可能在 assembly 之前就绪
|
|
95
|
+
if (typeof agentCtx.on === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
disposers.push(agentCtx.on('agent/inbox/inserted', (event) => {
|
|
98
|
+
if (readString(config, 'autoInject', 'message') !== 'context') return
|
|
99
|
+
const newText = extractUserText([event?.message])
|
|
100
|
+
if (!newText) return
|
|
101
|
+
if (newText.trimStart().startsWith('/')) return
|
|
102
|
+
if (isTrivialInput(newText, readNumber(config, 'autoMinChars', 0))) return
|
|
103
|
+
// 返回 promise 便于测试等待;宿主事件总线忽略返回值
|
|
104
|
+
return computeAndStore(agent, newText)
|
|
105
|
+
}))
|
|
106
|
+
} catch (error) {
|
|
107
|
+
log('warn', `注册入箱预计算失败:${messageOf(error)}`)
|
|
101
108
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
109
|
+
}
|
|
110
|
+
// ② 把最新决策渲染进作用域动态上下文
|
|
111
|
+
if (agentCtx?.systemPrompt?.context) {
|
|
112
|
+
try {
|
|
113
|
+
disposers.push(agentCtx.systemPrompt.context({
|
|
114
|
+
name: 'systemone-auto-decision',
|
|
115
|
+
order: 900,
|
|
116
|
+
text: () => (readString(config, 'autoInject', 'message') === 'context')
|
|
117
|
+
? (perAgent.get(agent?.id)?.text || '')
|
|
118
|
+
: '',
|
|
119
|
+
}))
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log('warn', `注册动态上下文失败:${messageOf(error)}`)
|
|
113
122
|
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
123
|
+
}
|
|
124
|
+
}))
|
|
116
125
|
|
|
117
126
|
// ── pre-step:同一步生效(仅 message 通道) ──
|
|
118
127
|
disposers.push(ctx.on('agent/pre-step', async (payload, next) => {
|
|
@@ -132,7 +141,7 @@ export function installAutoDecide(ctx, deps) {
|
|
|
132
141
|
const newText = extractUserText(claimed)
|
|
133
142
|
if (!newText) return decision // 工具结果步骤 / 无新用户消息
|
|
134
143
|
if (newText.trimStart().startsWith('/')) return decision // 斜杠命令
|
|
135
|
-
if (isTrivialInput(newText,
|
|
144
|
+
if (isTrivialInput(newText, readNumber(config, 'autoMinChars', 0))) return decision // 寒暄/致谢等无业务含义的输入
|
|
136
145
|
|
|
137
146
|
let injected = null
|
|
138
147
|
try {
|
|
@@ -151,6 +160,7 @@ export function installAutoDecide(ctx, deps) {
|
|
|
151
160
|
inFlight,
|
|
152
161
|
signal: timeout.signal,
|
|
153
162
|
log,
|
|
163
|
+
warnedScenarios,
|
|
154
164
|
}),
|
|
155
165
|
readNumber(config, 'autoTimeoutMs', 8000),
|
|
156
166
|
)
|
|
@@ -200,6 +210,7 @@ export function installAutoDecide(ctx, deps) {
|
|
|
200
210
|
inFlight,
|
|
201
211
|
signal: timeout.signal,
|
|
202
212
|
log,
|
|
213
|
+
warnedScenarios,
|
|
203
214
|
}),
|
|
204
215
|
readNumber(config, 'autoTimeoutMs', 8000),
|
|
205
216
|
)
|
|
@@ -232,7 +243,7 @@ export function installAutoDecide(ctx, deps) {
|
|
|
232
243
|
/**
|
|
233
244
|
* 计算决策(带并发去重)。相同 state 只发一次上游请求。
|
|
234
245
|
*/
|
|
235
|
-
async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log }) {
|
|
246
|
+
async function computeDecision({ agent, newText, provider, config, scenarios, cache, inFlight, signal, log, warnedScenarios }) {
|
|
236
247
|
const state = captureState(agent, newText, readNumber(config, 'autoMaxMessages', 6))
|
|
237
248
|
const key = hashString(state)
|
|
238
249
|
const existing = inFlight.get(key)
|
|
@@ -248,6 +259,7 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
|
|
|
248
259
|
cache,
|
|
249
260
|
signal,
|
|
250
261
|
log,
|
|
262
|
+
warnedScenarios,
|
|
251
263
|
}).finally(() => {
|
|
252
264
|
if (inFlight.get(key) === promise) inFlight.delete(key)
|
|
253
265
|
})
|
|
@@ -255,7 +267,7 @@ async function computeDecision({ agent, newText, provider, config, scenarios, ca
|
|
|
255
267
|
return promise
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log }) {
|
|
270
|
+
async function decide({ agent, newText, state, key, provider, config, scenarios, cache, signal, log, warnedScenarios }) {
|
|
259
271
|
const ttl = readNumber(config, 'autoCacheTtlMs', 0) || 0
|
|
260
272
|
if (ttl > 0) {
|
|
261
273
|
const hit = cache.get(key)
|
|
@@ -263,7 +275,7 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
|
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
// 1) 选场景:固定绑定,或让模型做一次自动路由
|
|
266
|
-
const routed = await resolveScenario({ state, provider, config, scenarios, signal })
|
|
278
|
+
const routed = await resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios })
|
|
267
279
|
if (!routed) return null
|
|
268
280
|
|
|
269
281
|
// 路由置信度不足:宁可不注入,也不要塞一个瞎猜的结论
|
|
@@ -297,16 +309,35 @@ async function decide({ agent, newText, state, key, provider, config, scenarios,
|
|
|
297
309
|
headline: result.recommendation || routed.scenario.title,
|
|
298
310
|
at: Date.now(),
|
|
299
311
|
}
|
|
300
|
-
if (ttl > 0)
|
|
312
|
+
if (ttl > 0) {
|
|
313
|
+
// 缓存上限:先清过期,再按插入序淘汰最旧,防止长会话无限增长
|
|
314
|
+
if (cache.size >= MAX_AUTO_CACHE) {
|
|
315
|
+
const now = Date.now()
|
|
316
|
+
for (const [k, v] of cache) {
|
|
317
|
+
if (now - v.at >= ttl) cache.delete(k)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
while (cache.size >= MAX_AUTO_CACHE) {
|
|
321
|
+
cache.delete(cache.keys().next().value)
|
|
322
|
+
}
|
|
323
|
+
cache.set(key, { at: Date.now(), result: injected })
|
|
324
|
+
}
|
|
301
325
|
return injected
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
/** 选择场景:固定绑定优先,否则用一次 choice 问句自动路由。 */
|
|
305
|
-
async function resolveScenario({ state, provider, config, scenarios, signal }) {
|
|
329
|
+
async function resolveScenario({ state, provider, config, scenarios, signal, log, warnedScenarios }) {
|
|
306
330
|
const fixed = readString(config, 'autoScenario', '').trim()
|
|
307
331
|
if (fixed) {
|
|
308
332
|
const scenario = findScenario(scenarios, fixed)
|
|
309
|
-
if (!scenario)
|
|
333
|
+
if (!scenario) {
|
|
334
|
+
// 配置错误不能静默:同一 id 只告警一次,避免每步刷日志
|
|
335
|
+
if (!warnedScenarios.has(fixed)) {
|
|
336
|
+
warnedScenarios.add(fixed)
|
|
337
|
+
log?.('warn', `固定场景 "${fixed}" 不在场景库中,自动决策已停用(请检查 autoScenario 配置;可用:${scenarios.map((s) => s.id).join('、')})`)
|
|
338
|
+
}
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
310
341
|
return { scenario, source: '固定绑定', confidence: null }
|
|
311
342
|
}
|
|
312
343
|
if (scenarios.length === 0) return null
|
|
@@ -365,8 +396,9 @@ function questionLabel(scenario, qid) {
|
|
|
365
396
|
/**
|
|
366
397
|
* 判断是否为"无业务含义"的输入,用于跳过自动决策。
|
|
367
398
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
399
|
+
* 两条独立的跳过规则(命中任一即跳过):
|
|
400
|
+
* 1. 长度阈值:短于 autoMinChars 的一律跳过(默认 4,"退款失败" 恰好 4 字不被误伤);
|
|
401
|
+
* 2. 寒暄黑名单:整体就是一句寒暄/致谢的跳过,与长度无关。
|
|
370
402
|
*/
|
|
371
403
|
const TRIVIAL_PATTERN = /^(你好|您好|哈喽|在吗|hi|hello|hey|ok|okay|好的|好嘞|收到|明白|嗯+|谢谢|多谢|感谢|thanks|thank you|thx|再见|拜拜|bye|测试|test)[\s!!。.~~,,、??]*$/i
|
|
372
404
|
|
|
@@ -387,7 +419,7 @@ function captureState(agent, newText, maxMessages) {
|
|
|
387
419
|
const history = readHistory(agent, limit)
|
|
388
420
|
const parts = []
|
|
389
421
|
if (history.length > 0) parts.push(`【近期对话】\n${history.join('\n')}`)
|
|
390
|
-
parts.push(`【当前请求】\n${newText}`)
|
|
422
|
+
parts.push(`【当前请求】\n${truncate(newText, MAX_CURRENT_CHARS)}`)
|
|
391
423
|
return parts.join('\n\n')
|
|
392
424
|
}
|
|
393
425
|
|
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
|
*
|
|
@@ -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: [
|
|
@@ -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
|
|
@@ -666,7 +684,6 @@ window.__ModuleLoader__.load({
|
|
|
666
684
|
children: [
|
|
667
685
|
h(ConfigCard, {
|
|
668
686
|
key: 'service',
|
|
669
|
-
t,
|
|
670
687
|
title: t('tabService'),
|
|
671
688
|
description: t('tabServiceHint'),
|
|
672
689
|
open: openService,
|
|
@@ -674,7 +691,7 @@ window.__ModuleLoader__.load({
|
|
|
674
691
|
// 注意:jsx(type, props, key) 的第三个参数是 key,正文必须作为 children 放进 props
|
|
675
692
|
children: hs(Fragment, {
|
|
676
693
|
children: [
|
|
677
|
-
...renderFields(SERVICE_FIELDS),
|
|
694
|
+
...renderFields(SERVICE_FIELDS, numberExtras),
|
|
678
695
|
hs('div', {
|
|
679
696
|
key: 'footer',
|
|
680
697
|
style: footerStyle,
|
|
@@ -706,7 +723,6 @@ window.__ModuleLoader__.load({
|
|
|
706
723
|
}),
|
|
707
724
|
h(ConfigCard, {
|
|
708
725
|
key: 'custom',
|
|
709
|
-
t,
|
|
710
726
|
title: t('tabCustom'),
|
|
711
727
|
description: t('tabCustomHint'),
|
|
712
728
|
open: openCustom,
|
|
@@ -803,14 +819,13 @@ window.__ModuleLoader__.load({
|
|
|
803
819
|
}),
|
|
804
820
|
h(ConfigCard, {
|
|
805
821
|
key: 'auto',
|
|
806
|
-
t,
|
|
807
822
|
title: t('tabAuto'),
|
|
808
823
|
description: t('tabAutoHint'),
|
|
809
824
|
open: openAuto,
|
|
810
825
|
onToggle: () => setOpenAuto((value) => !value),
|
|
811
826
|
children: hs(Fragment, {
|
|
812
827
|
children: [
|
|
813
|
-
...renderFields(AUTO_FIELDS),
|
|
828
|
+
...renderFields(AUTO_FIELDS, numberExtras),
|
|
814
829
|
hs('div', {
|
|
815
830
|
key: 'footer',
|
|
816
831
|
style: footerStyle,
|
|
@@ -872,8 +887,3 @@ window.__ModuleLoader__.load({
|
|
|
872
887
|
return module.exports
|
|
873
888
|
},
|
|
874
889
|
})
|
|
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)
|
|
@@ -17,7 +17,10 @@ import { createProvider } from './provider.js'
|
|
|
17
17
|
import { createTools } from './tools.js'
|
|
18
18
|
import { resolveScenarios } from './scenarios.js'
|
|
19
19
|
import { installAutoDecide } from './auto.js'
|
|
20
|
-
import { readBoolean, readString } from './volatile.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
|
* 把「当前提供商」包成一个稳定引用的对象,内部每次调用都转发到最新实例。
|
|
@@ -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.1",
|
|
4
4
|
"description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision,可切换提供商)接入软件开发、客服、内容审核、路由、销售线索、风控、招聘、数据治理、教育、需求变更等 10 大业务场景,为 Agent 返回带概率分布的决策与处置建议。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|