@master0071/dsh-systemone 0.4.4 → 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 +15 -5
- package/lib/client.js +72 -33
- package/lib/index.d.ts +2 -0
- package/lib/index.js +28 -9
- package/lib/provider.js +106 -20
- package/lib/scenarios.js +97 -5
- package/lib/tools.js +34 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ SystemOne 协议一次请求携带业务状态与**结构化问题**(`choice`
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## 内置场景(
|
|
14
|
+
## 内置场景(11 大业务域)
|
|
15
15
|
|
|
16
|
-
每个场景 = 3 个结构化问题,覆盖
|
|
16
|
+
每个场景 = 3 个结构化问题,覆盖 33 项业务能力。
|
|
17
17
|
|
|
18
18
|
| 场景 id | 业务域 | 覆盖能力 |
|
|
19
19
|
| --- | --- | --- |
|
|
@@ -27,6 +27,7 @@ SystemOne 协议一次请求携带业务状态与**结构化问题**(`choice`
|
|
|
27
27
|
| `education` | 教育内容 | 题目知识点归类、难度分级评分、内容合规预检 |
|
|
28
28
|
| `requirements` | 需求与变更 | 优先级评分、变更风险评级、子任务派发 |
|
|
29
29
|
| `software_dev` | **软件开发** | 任务类型(缺陷/功能/重构/评审/测试/文档/构建/性能)、改动复杂度、是否需先探查代码库 |
|
|
30
|
+
| `requirement_clarity` | **需求预检** | 需求明确度评分、最缺信息(目标/范围/验收/约束)、不澄清就推进的返工风险 |
|
|
30
31
|
|
|
31
32
|
### `software_dev`:给编码 Agent 用的软件开发场景
|
|
32
33
|
|
|
@@ -126,7 +127,7 @@ systemone_decide(
|
|
|
126
127
|
|
|
127
128
|
- **提供商无关**:所有工具只依赖统一的 `provider.decide()` 接口。切换厂商只改一行配置,业务工具零改动。
|
|
128
129
|
- **图形配置页**:侧栏「插件 → @master0071/dsh-systemone → systemone」里可直接改提供商、Key、模型与自动决策参数,保存即生效(volatile 热更新,无需重启)。
|
|
129
|
-
- **场景可扩展**:内置
|
|
130
|
+
- **场景可扩展**:内置 11 大场景(含软件开发与需求预检),并可在配置页里用 JSON 追加或覆盖场景,**无需改代码、无需重启**
|
|
130
131
|
- **可自动决策**:开启后挂 `agent/pre-step`,每一步推理前自动捕获上下文并注入判断(fail-open、硬超时、可缓存)。
|
|
131
132
|
- **概率化输出**:返回每个选项的概率分布与置信度,而非单一答案。
|
|
132
133
|
- **置信度兜底**:任一答案置信度低于 `minConfidence`(默认 0.6)时标记 `needs_human_review: true`。
|
|
@@ -318,6 +319,7 @@ const decision = await waterfall('agent/pre-step', { // ③ 再跑瀑布
|
|
|
318
319
|
| `model` | `u2-decision` | 热更新 | 决策模型名 |
|
|
319
320
|
| `timeoutMs` | `30000` | 热更新 | 请求超时(毫秒) |
|
|
320
321
|
| `minConfidence` | `0.6` | 热更新 | 置信度阈值,低于则建议人工复核 |
|
|
322
|
+
| `redact` | `false` | 热更新 | 发送前对 `state` 脱敏:手机号/身份证/邮箱/银行卡号替换为 `[手机号]` 等类型标签(判断语义保留;处理含隐私数据的内容时建议开启) |
|
|
321
323
|
| `customScenarios` | 空 | 热更新 | 自定义场景 JSON 数组(见下)。保存后**场景库立即重建**,无需重启 |
|
|
322
324
|
| `autoDecide` | `false` | 需重启 | 是否开启自动决策(挂载 `agent/pre-step`) |
|
|
323
325
|
| `auto*` | 见「自动决策」 | 热更新 | 自动决策的运行参数(钩子挂载后实时生效) |
|
|
@@ -334,6 +336,14 @@ const decision = await waterfall('agent/pre-step', { // ③ 再跑瀑布
|
|
|
334
336
|
|
|
335
337
|
覆盖与别名的解析规则:
|
|
336
338
|
|
|
339
|
+
- **patch 式覆盖**:默认同 `id` 是**整体替换**。只想改一两个字段时,给条目加 `"patch": true` 即可做**字段级合并**——`title`/`description`/`recommendation` 给了才覆盖;`questions` 按 qid 合并(只换给出的字段,如只覆盖某个问题的 `criteria`,其余问题原样保留,新 qid 追加);`derive` 同名覆盖;`aliases` 求并集。例如给 `customer_service` 的部门问题加一个 VIP 通道:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
[{ "id": "customer_service", "patch": true,
|
|
343
|
+
"questions": { "department": { "criteria": { "vip": "VIP 专属通道", "billing": "账单", "technical": "技术", "account": "账号", "logistics": "物流", "other": "其他" } } } }]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
patch 目标不存在、或合并结果非法(如 choice 选项少于 2 个)时该条目会被跳过并记入日志。不带 `patch` 的条目维持整体替换语义,已有配置不受影响。
|
|
337
347
|
- **同 `id` 覆盖**:自定义场景替换同名内置场景,只写 `title`/`questions` 也够——被替换场景原有的 `aliases` 会被**继承**(省略 `aliases` 不等于清空别名),新声明的别名与继承的别名共存且自动去重。
|
|
338
348
|
- **别名冲突**:别名与 `id` 平权参与查找,冲突时**后登记者获胜**(自定义场景优先于内置场景,自定义之间后声明者优先)。因此自定义场景声明的别名一定指向它自己,不会被内置场景抢走。
|
|
339
349
|
- **冲突提示**:别名撞名是**非致命**的——场景照常可用,但插件会给出提示(宿主日志里带「(提示)」前缀,`systemone_scenario(action: "list")` 的 `warnings` 字段与「配置提示」小节里也会列出),说明谁遮蔽了谁,避免调用方把「别名被占」误判成「场景不存在」。
|
|
@@ -380,7 +390,7 @@ SystemOne 是「结构化问题 → 概率分布」的决策模型,**不是开
|
|
|
380
390
|
]
|
|
381
391
|
```
|
|
382
392
|
|
|
383
|
-
配合 `autoScenario: "intent"`,自动决策就不再受内置
|
|
393
|
+
配合 `autoScenario: "intent"`,自动决策就不再受内置 11 个业务场景的约束。
|
|
384
394
|
|
|
385
395
|
**容量上限**:每个场景最多 **16 个问题**(延迟随问题数线性增长),`choice` 的选项 **2~26 个**,所以意图标签集实践上限约 26 类——超了只能合并或落到 `other`。
|
|
386
396
|
|
|
@@ -473,7 +483,7 @@ SystemOne 是「结构化问题 → 概率分布」的决策模型,**不是开
|
|
|
473
483
|
```powershell
|
|
474
484
|
npm run check # 语法检查
|
|
475
485
|
npm run test # 59 项测试:
|
|
476
|
-
# - 场景库完整性、
|
|
486
|
+
# - 场景库完整性、11 大场景端到端、自定义场景合并/覆盖/patch/非法跳过
|
|
477
487
|
# - params 覆盖语义、确定性、失败路径(无网络、无 Key)
|
|
478
488
|
# - 插件入口装配(临时桩实例化:工具注册、降级、卸载)
|
|
479
489
|
# - 自动决策(开关、注入形状、fail-open、硬超时、缓存、去重、
|
package/lib/client.js
CHANGED
|
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
tabCustom: '自定义场景',
|
|
43
43
|
tabCustomHint: '用 JSON 定义自己的场景(含「通用意图识别」)。保存后场景库立即重建,无需重启。',
|
|
44
44
|
customScenarios: '场景定义(JSON 数组)',
|
|
45
|
-
customScenariosHint: '同 id
|
|
45
|
+
customScenariosHint: '同 id 默认整体覆盖内置场景;加 "patch": true 则只改给出的字段。留空 [] 表示只用内置 11 个。每个场景最多 16 个问题,choice 选项 2~26 个。',
|
|
46
46
|
customScenariosPlaceholder: '[\n {\n "id": "intent",\n "title": "通用意图识别",\n "questions": {\n "intent": {\n "type": "choice",\n "instructions": "用户最想做什么?",\n "criteria": { "query": "查询", "action": "执行操作", "chat": "闲聊" }\n }\n }\n }\n]',
|
|
47
47
|
insertTemplate: '插入意图识别模板',
|
|
48
48
|
jsonEmpty: '(空 = 只用内置场景)',
|
|
@@ -72,8 +72,12 @@ window.__ModuleLoader__.load({
|
|
|
72
72
|
timeoutMsHint: '单次决策请求的最长等待时间',
|
|
73
73
|
minConfidence: '置信度阈值',
|
|
74
74
|
minConfidenceHint: '任一答案低于该值时,结果会标记为建议人工复核(0~1)',
|
|
75
|
+
redact: '发送前脱敏',
|
|
76
|
+
redactHint: '开启后把 state 里的手机号/身份证/邮箱/银行卡号替换为 [类型] 标签再发给决策服务,判断语义保留',
|
|
77
|
+
redactOn: '开启(含隐私数据时建议)',
|
|
78
|
+
redactOff: '关闭',
|
|
75
79
|
autoScenario: '固定场景(可选)',
|
|
76
|
-
autoScenarioHint: '点右侧箭头可从
|
|
80
|
+
autoScenarioHint: '点右侧箭头可从 11 个内置场景里选,也可手填场景 id 或别名(如 software_dev / 开发);留空则每次自动选场景(多一次路由请求)',
|
|
77
81
|
autoInject: '注入通道',
|
|
78
82
|
autoInjectHint: 'message=同一步生效;context=走宿主动态上下文,不写入会话历史',
|
|
79
83
|
autoInjectMessage: '同步注入(message)',
|
|
@@ -112,7 +116,7 @@ window.__ModuleLoader__.load({
|
|
|
112
116
|
tabCustom: 'Custom scenarios',
|
|
113
117
|
tabCustomHint: 'Define your own scenarios in JSON (including general intent recognition). Saving rebuilds the library immediately.',
|
|
114
118
|
customScenarios: 'Scenario definitions (JSON array)',
|
|
115
|
-
customScenariosHint: 'An id matching a built-in overrides it;
|
|
119
|
+
customScenariosHint: 'An id matching a built-in overrides it; add "patch": true to merge only the given fields. Empty [] keeps only the 11 built-ins. Max 16 questions per scenario, 2~26 choice options.',
|
|
116
120
|
customScenariosPlaceholder: '[\n {\n "id": "intent",\n "title": "General intent",\n "questions": {\n "intent": {\n "type": "choice",\n "instructions": "What does the user want?",\n "criteria": { "query": "Look up", "action": "Do something", "chat": "Small talk" }\n }\n }\n }\n]',
|
|
117
121
|
insertTemplate: 'Insert intent template',
|
|
118
122
|
jsonEmpty: '(empty = built-in scenarios only)',
|
|
@@ -142,8 +146,12 @@ window.__ModuleLoader__.load({
|
|
|
142
146
|
timeoutMsHint: 'Longest wait for one decision request',
|
|
143
147
|
minConfidence: 'Confidence threshold',
|
|
144
148
|
minConfidenceHint: 'Below this value a result is flagged for human review (0~1)',
|
|
149
|
+
redact: 'Redact before send',
|
|
150
|
+
redactHint: 'Replace phone/ID-card/email/bank-card numbers in state with [type] tags before sending to the decision service; semantics preserved',
|
|
151
|
+
redactOn: 'On (recommended with PII)',
|
|
152
|
+
redactOff: 'Off',
|
|
145
153
|
autoScenario: 'Fixed scenario (optional)',
|
|
146
|
-
autoScenarioHint: 'Pick one of the
|
|
154
|
+
autoScenarioHint: 'Pick one of the 11 built-in scenarios from the dropdown, or type a scenario id/alias (e.g. software_dev); empty routes automatically on every step (one extra request)',
|
|
147
155
|
autoInject: 'Injection channel',
|
|
148
156
|
autoInjectHint: 'message=applies in the same step; context=host dynamic context, not written to history',
|
|
149
157
|
autoInjectMessage: 'Same step (message)',
|
|
@@ -268,7 +276,7 @@ window.__ModuleLoader__.load({
|
|
|
268
276
|
/**
|
|
269
277
|
* 内置场景候选(供「固定场景」输入框的下拉提示)。
|
|
270
278
|
*
|
|
271
|
-
* 用 `<datalist>` 而不是 `<select>`:既能把
|
|
279
|
+
* 用 `<datalist>` 而不是 `<select>`:既能把 11 个内置场景列出来点选,
|
|
272
280
|
* 又允许手填自定义场景 id 或别名(别名见 README 的场景表)。
|
|
273
281
|
* 这里只放 id + 中文标题;id 用别名也能命中(findScenario 会按别名匹配)。
|
|
274
282
|
*/
|
|
@@ -283,6 +291,7 @@ window.__ModuleLoader__.load({
|
|
|
283
291
|
{ id: 'education', label: '教育内容 · 知识点、难度与合规' },
|
|
284
292
|
{ id: 'requirements', label: '需求与变更 · 优先级、风险与派发' },
|
|
285
293
|
{ id: 'software_dev', label: '软件开发 · 任务类型、复杂度与处置' },
|
|
294
|
+
{ id: 'requirement_clarity', label: '需求预检 · 明确度与澄清点' },
|
|
286
295
|
]
|
|
287
296
|
|
|
288
297
|
const SERVICE_FIELDS = [
|
|
@@ -293,6 +302,7 @@ window.__ModuleLoader__.load({
|
|
|
293
302
|
{ key: 'model', kind: 'text' },
|
|
294
303
|
{ key: 'timeoutMs', kind: 'number' },
|
|
295
304
|
{ key: 'minConfidence', kind: 'number' },
|
|
305
|
+
{ key: 'redact', kind: 'select', options: ['false', 'true'] },
|
|
296
306
|
]
|
|
297
307
|
|
|
298
308
|
/** 自定义场景编辑区(JSON)。 */
|
|
@@ -328,35 +338,57 @@ window.__ModuleLoader__.load({
|
|
|
328
338
|
return { ok: false, error: `item:${at}`, ids, count: 0 }
|
|
329
339
|
}
|
|
330
340
|
const issues = []
|
|
341
|
+
const isPatch = spec.patch === true
|
|
331
342
|
if (typeof spec.id !== 'string' || spec.id.trim() === '') issues.push('缺少 id')
|
|
332
|
-
if (typeof spec.title !== 'string' || spec.title.trim() === '') issues.push('缺少 title')
|
|
343
|
+
if (!isPatch && (typeof spec.title !== 'string' || spec.title.trim() === '')) issues.push('缺少 title')
|
|
333
344
|
const questions = spec.questions
|
|
334
|
-
if (!
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
if (!isPatch || questions !== undefined) {
|
|
346
|
+
if (!questions || typeof questions !== 'object' || Array.isArray(questions)) {
|
|
347
|
+
issues.push(isPatch ? 'patch 的 questions 必须是对象' : '缺少 questions 对象')
|
|
348
|
+
} else if (isPatch) {
|
|
349
|
+
// patch 的问题允许省略 type/instructions/criteria(继承目标场景的同名字段),
|
|
350
|
+
// 深层合法性由宿主对「合并结果」统一校验;这里只拦立刻能发现的结构错误
|
|
351
|
+
for (const [qid, q] of Object.entries(questions)) {
|
|
352
|
+
if (qid === '__proto__' || qid === 'constructor' || qid === 'prototype') {
|
|
353
|
+
issues.push(`问题 id ${qid} 不能使用(与对象原型机制冲突),请换一个 id`)
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
if (!q || typeof q !== 'object') { issues.push(`问题 ${qid} 必须是对象`); continue }
|
|
357
|
+
if (q.type !== undefined && !['choice', 'noul', 'score'].includes(q.type)) {
|
|
358
|
+
issues.push(`问题 ${qid} 的 type 必须是 choice/noul/score`)
|
|
359
|
+
}
|
|
345
360
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
361
|
+
} else {
|
|
362
|
+
const qids = Object.keys(questions)
|
|
363
|
+
if (qids.length === 0) issues.push('questions 至少 1 个')
|
|
364
|
+
if (qids.length > 16) issues.push(`questions 最多 16 个(当前 ${qids.length})`)
|
|
365
|
+
for (const [qid, q] of Object.entries(questions)) {
|
|
366
|
+
// 与宿主侧 validateScenario 对齐:这类键会被宿主拒绝,
|
|
367
|
+
// 这里不拦的话配置页会显示「JSON 有效」、场景却静默不生效
|
|
368
|
+
if (qid === '__proto__' || qid === 'constructor' || qid === 'prototype') {
|
|
369
|
+
issues.push(`问题 id ${qid} 不能使用(与对象原型机制冲突),请换一个 id`)
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
if (!q || typeof q !== 'object') { issues.push(`问题 ${qid} 必须是对象`); continue }
|
|
373
|
+
if (!['choice', 'noul', 'score'].includes(q.type)) {
|
|
374
|
+
issues.push(`问题 ${qid} 的 type 必须是 choice/noul/score`)
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
if (typeof q.instructions !== 'string' || q.instructions.trim() === '') {
|
|
378
|
+
issues.push(`问题 ${qid} 缺少 instructions`)
|
|
379
|
+
}
|
|
380
|
+
if (q.type === 'choice') {
|
|
381
|
+
const keys = q.criteria && typeof q.criteria === 'object' && !Array.isArray(q.criteria)
|
|
382
|
+
? Object.keys(q.criteria)
|
|
383
|
+
: null
|
|
384
|
+
if (keys === null) issues.push(`问题 ${qid}(choice)需要 criteria 对象`)
|
|
385
|
+
else if (keys.length < 2) issues.push(`问题 ${qid}(choice)至少 2 个选项`)
|
|
386
|
+
else if (keys.length > 26) issues.push(`问题 ${qid}(choice)选项不能超过 26 个(当前 ${keys.length})`)
|
|
387
|
+
}
|
|
388
|
+
if (q.type === 'score'
|
|
389
|
+
&& (!Array.isArray(q.criteria) || q.criteria.length < 2)) {
|
|
390
|
+
issues.push(`问题 ${qid}(score)criteria 需为长度 ≥2 的数组`)
|
|
391
|
+
}
|
|
360
392
|
}
|
|
361
393
|
}
|
|
362
394
|
}
|
|
@@ -390,6 +422,9 @@ window.__ModuleLoader__.load({
|
|
|
390
422
|
if (value === 'message') return t('autoInjectMessage')
|
|
391
423
|
if (value === 'context') return t('autoInjectContext')
|
|
392
424
|
}
|
|
425
|
+
if (key === 'redact') {
|
|
426
|
+
return value === 'true' ? t('redactOn') : t('redactOff')
|
|
427
|
+
}
|
|
393
428
|
return value
|
|
394
429
|
}
|
|
395
430
|
|
|
@@ -599,11 +634,11 @@ window.__ModuleLoader__.load({
|
|
|
599
634
|
const dirty = draft !== null && Object.keys(draft).some((key) => draft[key] !== current[key])
|
|
600
635
|
const canSave = dirty && !busy && writable && customCheck.ok && Object.keys(numberErrors).length === 0
|
|
601
636
|
|
|
602
|
-
// 「固定场景」候选 =
|
|
637
|
+
// 「固定场景」候选 = 11 个内置 + 当前生效的自定义场景 id(patch 条目指向已有 id,去重避免重复项)
|
|
603
638
|
const scenarioOptions = [
|
|
604
639
|
...SCENARIO_OPTIONS,
|
|
605
640
|
...(customCheck.ok
|
|
606
|
-
? customCheck.ids
|
|
641
|
+
? [...new Set(customCheck.ids)]
|
|
607
642
|
.filter((id) => !SCENARIO_OPTIONS.some((option) => option.id === id))
|
|
608
643
|
.map((id) => ({ id, label: id }))
|
|
609
644
|
: []),
|
|
@@ -632,6 +667,8 @@ window.__ModuleLoader__.load({
|
|
|
632
667
|
if (!Number.isFinite(parsed)) continue
|
|
633
668
|
next = parsed
|
|
634
669
|
}
|
|
670
|
+
// redact 下拉在草稿里是字符串,schema 要的是布尔
|
|
671
|
+
if (key === 'redact') next = value === 'true'
|
|
635
672
|
ops.push({ op: 'set', path: [key], value: next })
|
|
636
673
|
}
|
|
637
674
|
if (ops.length === 0) {
|
|
@@ -834,6 +871,8 @@ window.__ModuleLoader__.load({
|
|
|
834
871
|
exports.name = name
|
|
835
872
|
exports.inject = inject
|
|
836
873
|
exports.apply = apply
|
|
874
|
+
// 仅供测试:暴露纯函数(宿主只读 name/inject/apply,多余导出对宿主无影响)
|
|
875
|
+
exports.__internals = { validateCustomScenarios, describeCustomError }
|
|
837
876
|
return module.exports
|
|
838
877
|
},
|
|
839
878
|
})
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 注册 2 个 Agent 工具:
|
|
8
8
|
* systemone_scenario — 统一场景调度(list / describe / run)
|
|
9
9
|
* systemone_decide — 自定义问题的低层入口
|
|
10
|
-
* - 场景库内置
|
|
10
|
+
* - 场景库内置 11 大业务域,并支持在配置里用 JSON 追加/覆盖/patch 场景
|
|
11
11
|
* - 暴露 `ctx.systemone.decide()` 供其他插件编程调用
|
|
12
12
|
* - 提供商可配置(unisound / http / mock),切换厂商不影响工具层
|
|
13
13
|
*/
|
|
@@ -27,16 +27,29 @@ const KNOWN_PROVIDERS = ['unisound', 'http', 'mock']
|
|
|
27
27
|
*
|
|
28
28
|
* 工具与自动决策在装配期就拿到了这个对象并长期持有;有了它,
|
|
29
29
|
* 配置页里切换 `provider` 后它们无需重新装配即可用上新提供商。
|
|
30
|
+
*
|
|
31
|
+
* 与 liveScenarios 同一原则:**不做 trap 白名单**,统一转发全部
|
|
32
|
+
* (对象目标上合法的)内部方法——白名单的毛病是「漏一个就静默拿不到
|
|
33
|
+
* 新实例的成员」。provider 将来新增 health()/close() 之类接口时,
|
|
34
|
+
* 走代理的调用方自动可见,不需要回来改这里。
|
|
35
|
+
* 注意 apply/construct 两个 trap 只对函数目标合法,provider 是普通对象,不适用。
|
|
30
36
|
*/
|
|
31
37
|
function createProviderProxy(current) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// trap 的第一个参数是创建时固定的代理目标(占位空对象),必须丢弃、
|
|
39
|
+
// 换成「当前提供商实例」——转发全部 trap 参数会把占位对象错当成属性名
|
|
40
|
+
const forward = (name) => (_target, ...args) => Reflect[name](current(), ...args)
|
|
41
|
+
return new Proxy({}, {
|
|
42
|
+
get: forward('get'),
|
|
43
|
+
set: forward('set'),
|
|
44
|
+
has: forward('has'),
|
|
45
|
+
deleteProperty: forward('deleteProperty'),
|
|
46
|
+
ownKeys: forward('ownKeys'),
|
|
47
|
+
getOwnPropertyDescriptor: forward('getOwnPropertyDescriptor'),
|
|
48
|
+
defineProperty: forward('defineProperty'),
|
|
49
|
+
getPrototypeOf: forward('getPrototypeOf'),
|
|
50
|
+
setPrototypeOf: forward('setPrototypeOf'),
|
|
51
|
+
isExtensible: forward('isExtensible'),
|
|
52
|
+
})
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/**
|
|
@@ -72,6 +85,12 @@ const Config = z.object({
|
|
|
72
85
|
timeoutMs: z.number().min(1000).default(30000).volatile(),
|
|
73
86
|
/** 置信度阈值:任一答案低于该值则建议人工复核 */
|
|
74
87
|
minConfidence: z.number().min(0).max(1).default(0.6).volatile(),
|
|
88
|
+
/**
|
|
89
|
+
* 发送前对 state 做内置脱敏:手机号/身份证/邮箱/银行卡号替换为
|
|
90
|
+
* `[手机号]` 等类型标签(判断语义保留)。state 会发给上游决策服务,
|
|
91
|
+
* 处理含隐私数据的场景(尤其 data_governance)时建议开启。
|
|
92
|
+
*/
|
|
93
|
+
redact: z.boolean().default(false).volatile(),
|
|
75
94
|
|
|
76
95
|
/* ── 结构性字段:不在配置页里,改动需要重启 ───────────────────────────────── */
|
|
77
96
|
|
package/lib/provider.js
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* - "mock" :本地确定性模拟(无需 API Key,用于测试与演示)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readNumber, readString } from './volatile.js'
|
|
18
|
+
import { readBoolean, readNumber, readString } from './volatile.js'
|
|
19
19
|
|
|
20
20
|
const DEFAULT_BASE_URL = 'https://maas-api.unisound.com/v1'
|
|
21
21
|
const DEFAULT_MODEL = 'u2-decision'
|
|
22
|
+
/** 临时故障(5xx/429/网络错误)自动重试次数与退避间隔;决策请求是幂等读,重发安全。 */
|
|
23
|
+
const RETRY_LIMIT = 1
|
|
24
|
+
const RETRY_DELAY_MS = 300
|
|
22
25
|
|
|
23
26
|
/** 创建提供商实例。config 来自插件配置(已由 Config schema 校验)。未知 provider 值回退到 unisound(入口层负责告警)。 */
|
|
24
27
|
export function createProvider(config) {
|
|
@@ -49,10 +52,10 @@ export class HttpProvider {
|
|
|
49
52
|
if (!ep) {
|
|
50
53
|
throw new Error('SystemOne: provider=http 需要配置 endpoint 或 baseUrl')
|
|
51
54
|
}
|
|
52
|
-
return ep
|
|
55
|
+
return assertValidUrl(ep)
|
|
53
56
|
}
|
|
54
57
|
const base = String(baseUrl || DEFAULT_BASE_URL).trim().replace(/\/+$/, '')
|
|
55
|
-
return `${base}/systemone`
|
|
58
|
+
return assertValidUrl(`${base}/systemone`)
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/** 解析 API Key:配置优先,其次环境变量。 */
|
|
@@ -78,6 +81,11 @@ export class HttpProvider {
|
|
|
78
81
|
const endpoint = this.resolveEndpoint()
|
|
79
82
|
const apiKey = this.resolveApiKey()
|
|
80
83
|
const timeoutMs = readNumber(this.config, 'timeoutMs', 30000) || 30000
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
model: model || readString(this.config, 'model', DEFAULT_MODEL) || DEFAULT_MODEL,
|
|
86
|
+
state: redactValue(state, readBoolean(this.config, 'redact', false)),
|
|
87
|
+
questions,
|
|
88
|
+
})
|
|
81
89
|
|
|
82
90
|
const controller = new AbortController()
|
|
83
91
|
const timer = setTimeout(() => controller.abort(new Error(`SystemOne: 请求超时(${timeoutMs}ms)`)), timeoutMs)
|
|
@@ -91,35 +99,49 @@ export class HttpProvider {
|
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
/** 单次请求:非 2xx 抛错并带上 status,供重试判定。 */
|
|
103
|
+
const sendOnce = async () => {
|
|
95
104
|
const res = await fetch(endpoint, {
|
|
96
105
|
method: 'POST',
|
|
97
106
|
headers: {
|
|
98
107
|
Authorization: `Bearer ${apiKey}`,
|
|
99
108
|
'Content-Type': 'application/json',
|
|
100
109
|
},
|
|
101
|
-
body
|
|
102
|
-
model: model || readString(this.config, 'model', DEFAULT_MODEL) || DEFAULT_MODEL,
|
|
103
|
-
state,
|
|
104
|
-
questions,
|
|
105
|
-
}),
|
|
110
|
+
body,
|
|
106
111
|
signal: controller.signal,
|
|
107
112
|
})
|
|
108
|
-
|
|
109
113
|
const text = await res.text().catch(() => '')
|
|
110
114
|
if (!res.ok) {
|
|
111
|
-
|
|
115
|
+
const error = new Error(
|
|
112
116
|
`SystemOne: HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''} — ${text.slice(0, 500) || '无响应体'}`,
|
|
113
117
|
)
|
|
118
|
+
error.status = res.status
|
|
119
|
+
throw error
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
let data
|
|
117
121
|
try {
|
|
118
|
-
|
|
122
|
+
return JSON.parse(text)
|
|
119
123
|
} catch {
|
|
120
124
|
throw new Error(`SystemOne: 响应不是合法 JSON — ${text.slice(0, 300) || '空响应'}`)
|
|
121
125
|
}
|
|
122
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
let lastError
|
|
130
|
+
for (let attempt = 0; attempt <= RETRY_LIMIT; attempt += 1) {
|
|
131
|
+
if (attempt > 0) {
|
|
132
|
+
// 已中止(超时/外部取消)不再重试,让上一次的原始错误穿透
|
|
133
|
+
if (controller.signal.aborted) throw lastError
|
|
134
|
+
await sleep(RETRY_DELAY_MS)
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return await sendOnce()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
lastError = error
|
|
140
|
+
if (attempt < RETRY_LIMIT && !controller.signal.aborted && isRetryable(error)) continue
|
|
141
|
+
throw error
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw lastError
|
|
123
145
|
} finally {
|
|
124
146
|
clearTimeout(timer)
|
|
125
147
|
if (signal && typeof signal.removeEventListener === 'function') {
|
|
@@ -195,11 +217,12 @@ function mockAnswer(id, q, seedSource) {
|
|
|
195
217
|
confidence: probabilities[top],
|
|
196
218
|
}
|
|
197
219
|
}
|
|
198
|
-
case 'noul':
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
220
|
+
case 'noul': {
|
|
221
|
+
// 与真实响应对齐:noul 也带 confidence(取倾向一侧的概率),
|
|
222
|
+
// 否则 mock 下 computeNeedsHumanReview 走不到置信度分支
|
|
223
|
+
const v = Math.round((rand() * 0.6 + 0.2) * 1000) / 1000
|
|
224
|
+
return { type: 'noul', noul: v, confidence: Math.max(v, 1 - v) }
|
|
225
|
+
}
|
|
203
226
|
case 'score': {
|
|
204
227
|
const list = Array.isArray(q.criteria) ? q.criteria : []
|
|
205
228
|
const len = Math.max(list.length, 1)
|
|
@@ -247,4 +270,67 @@ function mulberry32(seed) {
|
|
|
247
270
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
248
271
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
249
272
|
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ─── 敏感数据脱敏(redact=true 时,发送前对 state 生效) ──────────────────── */
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 内置脱敏规则:手机号/身份证/邮箱/银行卡号替换为类型标签。
|
|
279
|
+
* 替换成标签(而不是抹成 ***)是有意的:决策模型仍能看出「这段内容
|
|
280
|
+
* 包含手机号」——对 data_governance 这类场景,这个信号正是判断依据。
|
|
281
|
+
* 顺序有意义:身份证(18 位)先于银行卡(13~19 位)匹配,
|
|
282
|
+
* 环视断言保证不会在更长的数字串内部截取匹配。
|
|
283
|
+
*/
|
|
284
|
+
const REDACT_RULES = [
|
|
285
|
+
{ pattern: /(?<![\dXx])\d{17}[\dXx](?![\dXx])/g, tag: '[身份证]' },
|
|
286
|
+
{ pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, tag: '[邮箱]' },
|
|
287
|
+
{ pattern: /(?<!\d)1[3-9]\d{9}(?!\d)/g, tag: '[手机号]' },
|
|
288
|
+
{ pattern: /(?<!\d)\d{13,19}(?!\d)/g, tag: '[银行卡号]' },
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
function redactString(text) {
|
|
292
|
+
return REDACT_RULES.reduce((acc, rule) => acc.replace(rule.pattern, rule.tag), text)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 递归脱敏 state:字符串叶子做规则替换,对象/数组逐层下钻。
|
|
297
|
+
* 不修改原对象(调用方可能还要用),返回脱敏后的副本;非字符串叶子原样保留。
|
|
298
|
+
*/
|
|
299
|
+
function redactValue(value, enabled) {
|
|
300
|
+
if (!enabled) return value
|
|
301
|
+
if (typeof value === 'string') return redactString(value)
|
|
302
|
+
if (Array.isArray(value)) return value.map((item) => redactValue(item, enabled))
|
|
303
|
+
if (value !== null && typeof value === 'object') {
|
|
304
|
+
const out = {}
|
|
305
|
+
for (const [key, item] of Object.entries(value)) {
|
|
306
|
+
out[key] = redactValue(item, enabled)
|
|
307
|
+
}
|
|
308
|
+
return out
|
|
309
|
+
}
|
|
310
|
+
return value
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* ─── 通用内部工具 ─────────────────────────────────────────────────────────── */
|
|
314
|
+
|
|
315
|
+
/** URL 合法性在发请求前拦下:漏写协议时 fetch 的报错非常隐晦,不如直接说清楚。 */
|
|
316
|
+
function assertValidUrl(url) {
|
|
317
|
+
try {
|
|
318
|
+
new URL(url)
|
|
319
|
+
} catch {
|
|
320
|
+
throw new Error(`SystemOne: 端点不是合法 URL:${url}(需以 http:// 或 https:// 开头)`)
|
|
321
|
+
}
|
|
322
|
+
return url
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 仅临时故障值得重试:5xx、429 限流、网络层错误(fetch 失败抛 TypeError)。 */
|
|
326
|
+
function isRetryable(error) {
|
|
327
|
+
if (error instanceof TypeError) return true
|
|
328
|
+
return error?.status === 429 || (typeof error?.status === 'number' && error.status >= 500)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function sleep(ms) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const timer = setTimeout(resolve, ms)
|
|
334
|
+
timer.unref?.()
|
|
335
|
+
})
|
|
250
336
|
}
|
package/lib/scenarios.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* 场景定义格式:
|
|
8
8
|
* {
|
|
9
9
|
* id: string, // 唯一标识(必填)
|
|
10
|
-
* title: string, //
|
|
10
|
+
* title: string, // 展示名(必填;patch 条目可省略,继承目标场景)
|
|
11
11
|
* description?: string, // 场景说明
|
|
12
12
|
* aliases?: string[], // 别名,便于模型用中文名/英文名查找
|
|
13
|
-
* questions: { // 问题映射(必填,至少 1
|
|
13
|
+
* questions: { // 问题映射(必填,至少 1 个;patch 条目可省略)
|
|
14
14
|
* [qid]: {
|
|
15
15
|
* type: 'choice' | 'noul' | 'score',
|
|
16
16
|
* instructions: string, // 给模型的问题描述
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* [name]: { question: string, values?: string[], map?: object }
|
|
23
23
|
* },
|
|
24
24
|
* recommendation?: string, // 可选建议模板
|
|
25
|
+
* patch?: boolean, // true=与同 id 已有场景做字段级合并(只覆盖给出的字段),默认整体替换
|
|
25
26
|
* }
|
|
26
27
|
*
|
|
27
28
|
* 建议模板语法:
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
* {qid?文案A|文案B} → 条件文案(noul 为真 / 分值≥1 / choice 非空非 none 时取 A)
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
/** 内置场景:
|
|
36
|
+
/** 内置场景:11 个业务域,每个 3 个结构化问题。 */
|
|
36
37
|
export const BUILTIN_SCENARIOS = [
|
|
37
38
|
{
|
|
38
39
|
id: 'customer_service',
|
|
@@ -384,6 +385,44 @@ export const BUILTIN_SCENARIOS = [
|
|
|
384
385
|
derive: { effort: { question: 'complexity', values: ['S', 'M', 'L', 'XL'] } },
|
|
385
386
|
recommendation: '任务类型「{task_type}」,改动规模 {effort}。{needs_context?建议先检索代码库并确认相关实现|可直接动手}',
|
|
386
387
|
},
|
|
388
|
+
{
|
|
389
|
+
id: 'requirement_clarity',
|
|
390
|
+
title: '需求预检 · 明确度与澄清点',
|
|
391
|
+
description: '接到开发需求先做结构化预检:判断明确程度、最缺什么信息、不澄清就推进的返工风险。',
|
|
392
|
+
// 别名刻意避开 requirements 已占用的 requirement / 需求 / 变更管理
|
|
393
|
+
aliases: ['clarity', '预检', '需求预检'],
|
|
394
|
+
questions: {
|
|
395
|
+
clarity: {
|
|
396
|
+
type: 'score',
|
|
397
|
+
label: '明确度',
|
|
398
|
+
instructions: '该开发任务的需求明确程度是?',
|
|
399
|
+
criteria: [
|
|
400
|
+
'模糊:目标都不清楚',
|
|
401
|
+
'偏低:目标清楚但范围或关键约束不明',
|
|
402
|
+
'较高:目标范围清楚,细节可自行决策',
|
|
403
|
+
'明确:目标、范围、验收都清楚',
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
missing: {
|
|
407
|
+
type: 'choice',
|
|
408
|
+
label: '缺失项',
|
|
409
|
+
instructions: '当前最需要澄清的信息是?',
|
|
410
|
+
criteria: {
|
|
411
|
+
goal: '目标/预期结果',
|
|
412
|
+
scope: '范围与边界',
|
|
413
|
+
acceptance: '验收标准',
|
|
414
|
+
constraint: '技术约束或依赖',
|
|
415
|
+
none: '无明显缺失',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
proceed: {
|
|
419
|
+
type: 'noul',
|
|
420
|
+
label: '可直接推进',
|
|
421
|
+
instructions: '不做额外澄清就推进,返工风险是否可接受?',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
recommendation: '明确度 {clarity},最缺 {missing}。{proceed?可直接推进|建议先澄清目标再动手}',
|
|
425
|
+
},
|
|
387
426
|
]
|
|
388
427
|
|
|
389
428
|
const VALID_TYPES = new Set(['choice', 'noul', 'score'])
|
|
@@ -488,12 +527,24 @@ export function resolveScenarios(customJson) {
|
|
|
488
527
|
})
|
|
489
528
|
|
|
490
529
|
parsed.forEach((spec, i) => {
|
|
491
|
-
|
|
530
|
+
let candidate = spec
|
|
531
|
+
// patch 条目:先与同 id 已有场景做字段级合并,再走与整体替换完全相同的
|
|
532
|
+
// 校验/规范化/别名继承路径——patch 与替换的唯一区别就在这一步合并。
|
|
533
|
+
if (spec && typeof spec === 'object' && !Array.isArray(spec) && spec.patch === true) {
|
|
534
|
+
const id = typeof spec.id === 'string' ? spec.id.trim() : ''
|
|
535
|
+
const key = id.toLowerCase()
|
|
536
|
+
if (!id || !indexById.has(key)) {
|
|
537
|
+
problems.push(`自定义场景 #${i + 1} 已跳过:patch 目标 "${id || '(未提供)'}" 不在场景库中(patch 只能修改已有场景)`)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
candidate = mergeScenario(scenarios[indexById.get(key)], spec)
|
|
541
|
+
}
|
|
542
|
+
const issues = validateScenario(candidate)
|
|
492
543
|
if (issues.length > 0) {
|
|
493
544
|
problems.push(`自定义场景 #${i + 1} 已跳过:${issues.join(';')}`)
|
|
494
545
|
return
|
|
495
546
|
}
|
|
496
|
-
const entry = normalizeScenario(
|
|
547
|
+
const entry = normalizeScenario(candidate)
|
|
497
548
|
const key = entry.id.toLowerCase()
|
|
498
549
|
|
|
499
550
|
// 别名撞名是一条提示:声明者获胜(后写优先),被遮蔽者从此无法通过该别名查到。
|
|
@@ -537,6 +588,47 @@ export function resolveScenarios(customJson) {
|
|
|
537
588
|
return { scenarios, problems, warnings }
|
|
538
589
|
}
|
|
539
590
|
|
|
591
|
+
/**
|
|
592
|
+
* 把 patch 条目与已有场景做字段级合并,产出「完整场景定义」形状的对象:
|
|
593
|
+
* - title/description/recommendation:patch 给了才覆盖,否则保留目标的值;
|
|
594
|
+
* - questions:按 qid 合并——patch 里给出的字段(instructions/label/criteria)
|
|
595
|
+
* 覆盖目标同名问题的对应字段,其余问题原样保留,新 qid 直接追加;
|
|
596
|
+
* - derive:同名派生覆盖,其余保留;
|
|
597
|
+
* - aliases:目标与 patch 求并集(与整体替换的继承规则一致)。
|
|
598
|
+
*
|
|
599
|
+
* 返回值随后会重新过 validateScenario:patch 把合法问题改非法(如 criteria
|
|
600
|
+
* 少于 2 个选项)在校验处被拦下,而不是静默产出坏场景。
|
|
601
|
+
*/
|
|
602
|
+
function mergeScenario(target, spec) {
|
|
603
|
+
// Object.create(null):patch 里的 __proto__ 这类键只会长成普通属性,
|
|
604
|
+
// 由 validateScenario 的 isUnsafeKey 统一拦下,不会走到原型 setter
|
|
605
|
+
const questions = Object.create(null)
|
|
606
|
+
for (const [qid, question] of Object.entries(target.questions || {})) {
|
|
607
|
+
questions[qid] = question
|
|
608
|
+
}
|
|
609
|
+
for (const [qid, patch] of Object.entries(spec.questions || {})) {
|
|
610
|
+
questions[qid] = patch && typeof patch === 'object'
|
|
611
|
+
? { ...(questions[qid] || {}), ...patch }
|
|
612
|
+
: patch
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
id: target.id,
|
|
616
|
+
title: typeof spec.title === 'string' && spec.title.trim() ? spec.title : target.title,
|
|
617
|
+
description: typeof spec.description === 'string' ? spec.description : target.description,
|
|
618
|
+
aliases: [
|
|
619
|
+
...(target.aliases || []),
|
|
620
|
+
...(Array.isArray(spec.aliases) ? spec.aliases : []),
|
|
621
|
+
],
|
|
622
|
+
questions,
|
|
623
|
+
derive: {
|
|
624
|
+
...(target.derive || {}),
|
|
625
|
+
...(spec.derive && typeof spec.derive === 'object' && !Array.isArray(spec.derive) ? spec.derive : {}),
|
|
626
|
+
},
|
|
627
|
+
recommendation: typeof spec.recommendation === 'string' ? spec.recommendation : target.recommendation,
|
|
628
|
+
minConfidence: typeof spec.minConfidence === 'number' ? spec.minConfidence : target.minConfidence,
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
540
632
|
/**
|
|
541
633
|
* 规范化场景定义,补齐可选字段。
|
|
542
634
|
*
|
package/lib/tools.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 1. systemone_scenario — action: list / describe / run,覆盖全部内置与自定义场景
|
|
6
6
|
* 2. systemone_decide — 直接提交自定义 questions 的低层入口
|
|
7
7
|
*
|
|
8
|
-
* 场景全部来自 scenarios.js 的场景库(内置
|
|
8
|
+
* 场景全部来自 scenarios.js 的场景库(内置 11 个 + 配置追加),新增场景无需改代码。
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
computeNeedsHumanReview,
|
|
@@ -20,6 +20,36 @@ import { readNumber, readString } from './volatile.js'
|
|
|
20
20
|
|
|
21
21
|
const DEFAULT_MIN_CONFIDENCE = 0.6
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 工具入参 state 的硬上限。自动决策路径有自己的截断(见 auto.js 的
|
|
25
|
+
* MAX_CURRENT_CHARS / 每条历史 2000 字),这里只防工具路径被塞进超大输入
|
|
26
|
+
* 直接打到上游 API;超长内容对决策质量也没有增量。
|
|
27
|
+
*/
|
|
28
|
+
const MAX_TOOL_STATE_CHARS = 16000
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 给 state 套硬上限:超长字符串截断;对象/数组按序列化长度衡量,
|
|
32
|
+
* 超限降级为截断后的 JSON 文本(决策模型反正是按文本理解的)。
|
|
33
|
+
* undefined/null/空字符串原样返回,让「缺少 state」的校验路径保持不变。
|
|
34
|
+
*/
|
|
35
|
+
function capState(state) {
|
|
36
|
+
if (state === undefined || state === null) return state
|
|
37
|
+
if (typeof state === 'string') {
|
|
38
|
+
return state.length <= MAX_TOOL_STATE_CHARS
|
|
39
|
+
? state
|
|
40
|
+
: `${state.slice(0, MAX_TOOL_STATE_CHARS)}…(已截断,原 ${state.length} 字符)`
|
|
41
|
+
}
|
|
42
|
+
let serialized
|
|
43
|
+
try {
|
|
44
|
+
serialized = JSON.stringify(state)
|
|
45
|
+
} catch {
|
|
46
|
+
// 循环引用等无法序列化的结构:原样交给下游,由序列化失败的报错路径兜底
|
|
47
|
+
return state
|
|
48
|
+
}
|
|
49
|
+
if (serialized.length <= MAX_TOOL_STATE_CHARS) return state
|
|
50
|
+
return `${serialized.slice(0, MAX_TOOL_STATE_CHARS)}…(已截断,原 ${serialized.length} 字符)`
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
/**
|
|
24
54
|
* 创建全部工具定义。
|
|
25
55
|
*
|
|
@@ -45,7 +75,7 @@ function scenarioTool(provider, config, scenarios, hooks = {}) {
|
|
|
45
75
|
description: [
|
|
46
76
|
'SystemOne 场景决策:把业务状态与预置的结构化问题一次性提交给决策模型,返回带概率分布的判断与处置建议。',
|
|
47
77
|
'action=list 列出全部场景;action=describe 查看某个场景的问题定义与可覆盖参数;action=run 执行决策。',
|
|
48
|
-
'内置
|
|
78
|
+
'内置 11 大业务场景,可用 params 覆盖问题定义。',
|
|
49
79
|
].join(' '),
|
|
50
80
|
parameters: {
|
|
51
81
|
type: 'object',
|
|
@@ -93,7 +123,7 @@ function scenarioTool(provider, config, scenarios, hooks = {}) {
|
|
|
93
123
|
config,
|
|
94
124
|
scenarios,
|
|
95
125
|
scenario: args.scenario,
|
|
96
|
-
state: args.state,
|
|
126
|
+
state: capState(args.state),
|
|
97
127
|
params: args.params,
|
|
98
128
|
exec,
|
|
99
129
|
})
|
|
@@ -367,7 +397,7 @@ function decideTool(provider, config) {
|
|
|
367
397
|
return executeDecision({
|
|
368
398
|
title: 'SystemOne 通用决策',
|
|
369
399
|
scenarioId: null,
|
|
370
|
-
state: args.state,
|
|
400
|
+
state: capState(args.state),
|
|
371
401
|
questions: args.questions || {},
|
|
372
402
|
provider,
|
|
373
403
|
config,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@master0071/dsh-systemone",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision,可切换提供商)接入软件开发、需求预检、客服、内容审核、路由、销售线索、风控、招聘、数据治理、教育、需求变更等 11 大业务场景,为 Agent 返回带概率分布的决策与处置建议。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|