@master0071/dsh-systemone 0.7.2 → 0.8.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 CHANGED
@@ -365,7 +365,8 @@ const decision = await waterfall('agent/pre-step', { // ③ 再跑瀑布
365
365
  | `model` | `u2-decision` | 热更新 | 决策模型名 |
366
366
  | `timeoutMs` | `30000` | 热更新 | 请求超时(毫秒) |
367
367
  | `minConfidence` | `0.6` | 热更新 | **次要**复核信号:置信度低于该值时也标记需复核。主要判据是分布平坦度,见「复核判定」 |
368
- | `redact` | `false` | 热更新 | 发送前对 `state` 脱敏:手机号/身份证/邮箱/银行卡号替换为 `[手机号]` 等类型标签(判断语义保留;处理含隐私数据的内容时建议开启) |
368
+ | `redact` | `false` | 热更新 | 发送前对 `state` **与 `questions`** 脱敏:手机号/身份证/邮箱/银行卡号替换为 `[手机号]` 等类型标签(判断语义保留;`questions` 只脱敏 `instructions`/`label`,不动 qid 与 criteria 的键。处理含隐私数据的内容时建议开启) |
369
+ | `allowInsecureHttp` | `false` | 热更新 | 是否允许向 **非 https** 端点发请求。带 `Bearer` 凭据的请求走明文等于把密钥发到网络上,所以默认只放行 `https` 与本机回环(`127.0.0.1` / `localhost` / `[::1]`);连内网明文网关时才显式打开 |
369
370
  | `customScenarios` | 空 | 热更新 | 自定义场景 JSON 数组(见下)。保存后**场景库立即重建**,无需重启 |
370
371
  | `autoDecide` | `false` | 需重启 | 是否开启自动决策(挂载 `agent/pre-step`) |
371
372
  | `auto*` | 见「自动决策」 | 热更新 | 自动决策的运行参数(钩子挂载后实时生效) |
@@ -580,6 +581,7 @@ SystemOne 是「结构化问题 → 概率分布」的决策模型,**不是开
580
581
  插件与厂商解耦:只要目标服务实现相同协议(`POST {endpoint}`,请求体 `{ model, state, questions }`,响应 `{ answers: { [id]: { type, ... } } }`)即可切换。
581
582
 
582
583
  - **换一家云厂商**:配置 `provider: "http"` + `endpoint` 指向该厂商或自建网关,`apiKey` 填对应凭证。
584
+ **端点必须是 `https`**(或本机回环):请求带 `Authorization: Bearer <key>`,明文 `http` 会把密钥发到网络上,插件会直接拒绝;连内网明文网关需显式设置 `allowInsecureHttp: true`。
583
585
  - **本地联调**:`provider: "mock"`,无需 Key,确定性输出。
584
586
  - **编程扩展**:参考 `lib/provider.js`,实现 `{ name, decide() }` 后注册到 `createProvider()`。
585
587
 
@@ -587,10 +589,12 @@ SystemOne 是「结构化问题 → 概率分布」的决策模型,**不是开
587
589
 
588
590
  ```powershell
589
591
  npm run check # 9 个 lib/*.js 语法检查通过
590
- npm run test # 203 项测试:
592
+ npm run test # 206 项测试:
591
593
  # - 场景库完整性、11 大场景端到端(有断言强制 sample 覆盖全部内置场景)、
592
594
  # 自定义场景合并/覆盖/patch/非法跳过
593
595
  # - params 覆盖语义、确定性、失败路径(无网络、无 Key)
596
+ # - provider 端点安全(非 https 拒绝 / 回环放行 / allowInsecureHttp 逃生口)
597
+ # 与 state+questions 脱敏
594
598
  # - 插件入口装配(临时桩实例化:工具注册、降级、卸载)
595
599
  # - 自动决策(开关、注入形状与自有 source.kind、fail-open、硬超时、
596
600
  # 缓存与"改配置即失效"、去重、跳过规则、历史捕获、自动路由、
package/lib/client.js CHANGED
@@ -78,9 +78,13 @@ window.__ModuleLoader__.load({
78
78
  minConfidence: '置信度阈值',
79
79
  minConfidenceHint: '任一答案低于该值时,结果会标记为建议人工复核(0~1)',
80
80
  redact: '发送前脱敏',
81
- redactHint: '开启后把 state 里的手机号/身份证/邮箱/银行卡号替换为 [类型] 标签再发给决策服务,判断语义保留',
81
+ redactHint: '开启后把 state 与 questions 里的手机号/身份证/邮箱/银行卡号替换为 [类型] 标签再发给决策服务,判断语义保留',
82
82
  redactOn: '开启(含隐私数据时建议)',
83
83
  redactOff: '关闭',
84
+ allowInsecureHttp: '允许明文 HTTP 端点',
85
+ allowInsecureHttpHint: '默认拒绝向非 https、非本机的端点发请求(Authorization 里的 API Key 会明文过网)。仅在本机/内网联调且明确知险时开启',
86
+ allowInsecureOn: '允许(不推荐)',
87
+ allowInsecureOff: '拒绝(默认)',
84
88
  autoScenario: '固定场景(可选)',
85
89
  autoScenarioHint: '点右侧箭头可从 11 个内置场景里选,也可手填场景 id 或别名(如 software_dev / 开发);留空则每次自动选场景(多一次路由请求)',
86
90
  autoInject: '注入通道',
@@ -173,9 +177,13 @@ window.__ModuleLoader__.load({
173
177
  minConfidence: 'Confidence threshold',
174
178
  minConfidenceHint: 'Below this value a result is flagged for human review (0~1)',
175
179
  redact: 'Redact before send',
176
- redactHint: 'Replace phone/ID-card/email/bank-card numbers in state with [type] tags before sending to the decision service; semantics preserved',
180
+ redactHint: 'Replace phone/ID-card/email/bank-card numbers in state and questions with [type] tags before sending to the decision service; semantics preserved',
177
181
  redactOn: 'On (recommended with PII)',
178
182
  redactOff: 'Off',
183
+ allowInsecureHttp: 'Allow plain HTTP endpoints',
184
+ allowInsecureHttpHint: 'By default requests to non-https, non-loopback endpoints are refused (the API key in Authorization would travel in cleartext). Enable only for trusted local/LAN testing',
185
+ allowInsecureOn: 'Allow (not recommended)',
186
+ allowInsecureOff: 'Refuse (default)',
179
187
  autoScenario: 'Fixed scenario (optional)',
180
188
  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)',
181
189
  autoInject: 'Injection channel',
@@ -348,6 +356,8 @@ window.__ModuleLoader__.load({
348
356
  { key: 'timeoutMs', kind: 'number', min: 1000 },
349
357
  { key: 'minConfidence', kind: 'number', min: 0, max: 1 },
350
358
  { key: 'redact', kind: 'select', options: ['false', 'true'] },
359
+ // 明文端点默认拒绝(API Key 会明文过网);只有内网联调才该开
360
+ { key: 'allowInsecureHttp', kind: 'select', options: ['false', 'true'] },
351
361
  // 台账路径也是 volatile:保存后宿主重建台账(新文件从当天记录恢复累计)
352
362
  { key: 'usageLogPath', kind: 'text' },
353
363
  ]
@@ -492,6 +502,9 @@ window.__ModuleLoader__.load({
492
502
  if (key === 'redact') {
493
503
  return value === 'true' ? t('redactOn') : t('redactOff')
494
504
  }
505
+ if (key === 'allowInsecureHttp') {
506
+ return value === 'true' ? t('allowInsecureOn') : t('allowInsecureOff')
507
+ }
495
508
  return value
496
509
  }
497
510
 
@@ -577,7 +590,8 @@ window.__ModuleLoader__.load({
577
590
  if (!Number.isFinite(parsed)) continue
578
591
  next = parsed
579
592
  }
580
- if (key === 'redact') next = value === 'true'
593
+ // 布尔字段:下拉在草稿里是字符串,schema 要的是布尔
594
+ if (key === 'redact' || key === 'allowInsecureHttp') next = value === 'true'
581
595
  ops.push({ op: 'set', path: [key], value: next })
582
596
  }
583
597
  if (cardKey === 'service' && clearApiKey) ops.push({ op: 'unset', path: ['apiKey'] })
package/lib/index.d.ts CHANGED
@@ -27,8 +27,13 @@ export interface Config {
27
27
  * 主要判据是概率分布平坦度(见 `reviewAssessment` 语义),因为这类概率未标定、跨后端不可比。
28
28
  */
29
29
  minConfidence?: number
30
- /** 发送前对 state 做内置脱敏(手机号/身份证/邮箱/银行卡号替换为类型标签) */
30
+ /** 发送前对 state 与 questions 做内置脱敏(手机号/身份证/邮箱/银行卡号替换为类型标签) */
31
31
  redact?: boolean
32
+ /**
33
+ * 允许向非 https 端点发送请求(默认 false)。请求带 Bearer 凭据,明文 http
34
+ * 会让密钥过网;默认只放行 https 与本机回环地址。
35
+ */
36
+ allowInsecureHttp?: boolean
32
37
  /** 自定义场景:JSON 数组字符串 */
33
38
  customScenarios?: string
34
39
  /** 用量台账文件路径;留空则用 `$DSH_HOME/dsh-systemone/usage.jsonl` */
package/lib/index.js CHANGED
@@ -101,11 +101,19 @@ const Config = z.object({
101
101
  /** 置信度阈值:任一答案低于该值则建议人工复核 */
102
102
  minConfidence: z.number().min(0).max(1).default(0.6).volatile(),
103
103
  /**
104
- * 发送前对 state 做内置脱敏:手机号/身份证/邮箱/银行卡号替换为
105
- * `[手机号]` 等类型标签(判断语义保留)。state 会发给上游决策服务,
104
+ * 发送前对 state 与 questions 做内置脱敏:手机号/身份证/邮箱/银行卡号替换为
105
+ * `[手机号]` 等类型标签(判断语义保留)。这两部分都会发给上游决策服务,
106
106
  * 处理含隐私数据的场景(尤其 data_governance)时建议开启。
107
107
  */
108
108
  redact: z.boolean().default(false).volatile(),
109
+ /**
110
+ * 允许向非 https 端点发送请求(默认 false)。
111
+ *
112
+ * 所有请求都带 `Authorization: Bearer <key>`,走明文 http 等于把密钥发到网络上,
113
+ * 所以默认只允许 https(以及本机回环 127.0.0.1/localhost)。只有明确知道自己在
114
+ * 连内网明文网关时才应打开。
115
+ */
116
+ allowInsecureHttp: z.boolean().default(false).volatile(),
109
117
 
110
118
  /* ── 结构性字段:不在配置页里,改动需要重启 ───────────────────────────────── */
111
119
 
package/lib/provider.js CHANGED
@@ -54,10 +54,10 @@ export class HttpProvider {
54
54
  if (!ep) {
55
55
  throw new Error('SystemOne: provider=http 需要配置 endpoint 或 baseUrl')
56
56
  }
57
- return assertValidUrl(ep)
57
+ return assertValidUrl(ep, this.config)
58
58
  }
59
59
  const base = String(baseUrl || DEFAULT_BASE_URL).trim().replace(/\/+$/, '')
60
- return assertValidUrl(`${base}/systemone`)
60
+ return assertValidUrl(`${base}/systemone`, this.config)
61
61
  }
62
62
 
63
63
  /**
@@ -93,10 +93,13 @@ export class HttpProvider {
93
93
  const endpoint = this.resolveEndpoint()
94
94
  const apiKey = this.resolveApiKey()
95
95
  const timeoutMs = readNumber(this.config, 'timeoutMs', 30000) || 30000
96
+ const redact = readBoolean(this.config, 'redact', false)
96
97
  const body = JSON.stringify({
97
98
  model: model || readString(this.config, 'model', DEFAULT_MODEL) || DEFAULT_MODEL,
98
- state: redactValue(state, readBoolean(this.config, 'redact', false)),
99
- questions,
99
+ state: redactValue(state, redact),
100
+ // questions 里的 instructions 同样是自由文本(systemone_decide 的 params 也并进来),
101
+ // 只脱敏 state 会让这条路径漏掉 PII。
102
+ questions: redactQuestions(questions, redact),
100
103
  })
101
104
 
102
105
  const controller = new AbortController()
@@ -322,6 +325,35 @@ function redactValue(value, enabled) {
322
325
  return value
323
326
  }
324
327
 
328
+ /**
329
+ * 递归脱敏 questions。
330
+ *
331
+ * 只处理**自由文本**(`instructions` / `label`),**不动 qid 与 criteria 的键**——
332
+ * 键是决策结果回填到场景的索引,改了就映射不回去了。`systemone_decide` 的
333
+ * `params.instructions` 覆盖会在发请求前合并进 questions,因此也一并覆盖。
334
+ *
335
+ * @param {object} questions - 问题定义(`{ [qid]: { type, instructions, ... } }`)
336
+ * @param {boolean} enabled - 是否开启脱敏
337
+ * @returns {object} 脱敏后的副本(未开启时原样返回)
338
+ */
339
+ function redactQuestions(questions, enabled) {
340
+ if (!enabled || !questions || typeof questions !== 'object' || Array.isArray(questions)) {
341
+ return questions
342
+ }
343
+ const out = {}
344
+ for (const [qid, question] of Object.entries(questions)) {
345
+ if (!question || typeof question !== 'object' || Array.isArray(question)) {
346
+ out[qid] = question
347
+ continue
348
+ }
349
+ const next = { ...question }
350
+ if (typeof next.instructions === 'string') next.instructions = redactString(next.instructions)
351
+ if (typeof next.label === 'string') next.label = redactString(next.label)
352
+ out[qid] = next
353
+ }
354
+ return out
355
+ }
356
+
325
357
  /* ─── DSH 凭证缝读取 ───────────────────────────────────────────────────────── */
326
358
 
327
359
  /** DSH 主目录:与用量台账同源(usage.js 导出)。 */
@@ -411,14 +443,42 @@ export function redactRuleSummary() {
411
443
 
412
444
  /* ─── 通用内部工具 ─────────────────────────────────────────────────────────── */
413
445
 
414
- /** URL 合法性在发请求前拦下:漏写协议时 fetch 的报错非常隐晦,不如直接说清楚。 */
415
- function assertValidUrl(url) {
446
+ /** 本机回环地址:http 只在这里被放行(凭据不出网卡)。 */
447
+ function isLoopbackHost(hostname) {
448
+ const host = String(hostname || '').toLowerCase()
449
+ return host === 'localhost' || host === '::1' || host === '[::1]' || /^127\./.test(host)
450
+ }
451
+
452
+ /**
453
+ * 发请求前的端点体检。
454
+ *
455
+ * 除了"是不是合法 URL",还要挡住**明文传输凭据**:本插件所有请求都带
456
+ * `Authorization: Bearer <key>`,走 http 等于把密钥明文发到网络上。因此默认
457
+ * 只允许 https;http 仅对本机回环放行(本地联调 / mock 网关),其他明文端点
458
+ * 必须显式配置 `allowInsecureHttp: true`(给内网自建网关留的逃生口)。
459
+ *
460
+ * @param {string} url - 已解析出的完整端点
461
+ * @param {object} [config] - 插件配置(读取 allowInsecureHttp)
462
+ */
463
+ function assertValidUrl(url, config) {
464
+ let parsed
416
465
  try {
417
- new URL(url)
466
+ parsed = new URL(url)
418
467
  } catch {
419
468
  throw new Error(`SystemOne: 端点不是合法 URL:${url}(需以 http:// 或 https:// 开头)`)
420
469
  }
421
- return url
470
+ if (parsed.protocol === 'https:') return url
471
+ if (parsed.protocol !== 'http:') {
472
+ throw new Error(`SystemOne: 不支持的端点协议 ${parsed.protocol}:${url}(只支持 http/https)`)
473
+ }
474
+ if (isLoopbackHost(parsed.hostname)) return url
475
+ if (readBoolean(config, 'allowInsecureHttp', false)) return url
476
+ throw new Error(
477
+ `SystemOne: 拒绝向非 https 端点发送请求:${url}`
478
+ + '(Authorization 里的 API Key 会明文过网)。'
479
+ + '本机联调请用 http://127.0.0.1 或 http://localhost;'
480
+ + '确需连接明文内网网关时,请在配置里显式设置 allowInsecureHttp: true。',
481
+ )
422
482
  }
423
483
 
424
484
  /** 仅临时故障值得重试:5xx、429 限流、网络层错误(fetch 失败抛 TypeError)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@master0071/dsh-systemone",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision,可切换提供商)接入软件开发、需求预检、客服、内容审核、路由、销售线索、风控、招聘、数据治理、教育、需求变更等 11 大业务场景,为 Agent 返回带概率分布的决策与处置建议。",
5
5
  "license": "MIT",
6
6
  "type": "module",