@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.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/CHANGELOG.md +42 -0
- package/LICENSE +661 -0
- package/README.en.md +110 -0
- package/README.md +102 -2
- package/dsh-memory-local/FORK-BASE.json +24 -0
- package/dsh-memory-local/README.md +113 -0
- package/dsh-memory-local/build.mjs +101 -0
- package/dsh-memory-local/cordis.patch.yml +20 -0
- package/dsh-memory-local/deploy.mjs +306 -0
- package/dsh-memory-local/entity-status.mjs +75 -0
- package/dsh-memory-local/lib/bridge.mjs +108 -0
- package/dsh-memory-local/lib/host-adapter.js +1171 -0
- package/dsh-memory-local/lib/index.d.ts +128 -0
- package/dsh-memory-local/lib/index.js +944 -0
- package/dsh-memory-local/lib/injector.mjs +400 -0
- package/dsh-memory-local/lib/memory-read.mjs +116 -0
- package/dsh-memory-local/lib/module-gate.mjs +68 -0
- package/dsh-memory-local/lib/pointer.mjs +152 -0
- package/dsh-memory-local/lib/prod-gate.mjs +121 -0
- package/dsh-memory-local/lib/recall-log.mjs +118 -0
- package/dsh-memory-local/lib/recall.mjs +192 -0
- package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
- package/dsh-memory-local/lib/user-message.mjs +32 -0
- package/dsh-memory-local/lib/vectors-read.mjs +110 -0
- package/dsh-memory-local/package.json +51 -0
- package/dsh-memory-local/restore.mjs +259 -0
- package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
- package/dsh-memory-local/src/bridge.mjs +108 -0
- package/dsh-memory-local/src/host-adapter.js +1171 -0
- package/dsh-memory-local/src/index.d.ts +128 -0
- package/dsh-memory-local/src/index.js +944 -0
- package/dsh-memory-local/src/injector.mjs +400 -0
- package/dsh-memory-local/src/memory-read.mjs +116 -0
- package/dsh-memory-local/src/module-gate.mjs +68 -0
- package/dsh-memory-local/src/pointer.mjs +152 -0
- package/dsh-memory-local/src/recall-log.mjs +118 -0
- package/dsh-memory-local/src/recall.mjs +192 -0
- package/dsh-memory-local/src/schema-migrate.mjs +365 -0
- package/dsh-memory-local/src/user-message.mjs +32 -0
- package/dsh-memory-local/src/vectors-read.mjs +110 -0
- package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
- package/dsh-memory-local/test/probe-entity.mjs +170 -0
- package/dsh-memory-local/test/regression.mjs +270 -0
- package/dsh-memory-local/test/s2b.test.mjs +568 -0
- package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
- package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
- package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
- package/dsh-memory-local/verify-after-restart.mjs +181 -0
- package/lib/deploy-guard.mjs +391 -0
- package/lib/guard-http.mjs +93 -0
- package/lib/pitfall-form-map.mjs +130 -0
- package/lib/pitfall-score.mjs +185 -0
- package/lib/session-file-name.mjs +160 -0
- package/lib/zstd-frames.mjs +78 -0
- package/memory-db-tools/README.md +68 -0
- package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
- package/memory-db-tools/audit-analyze.mjs +352 -0
- package/memory-db-tools/audit-dump-ids.mjs +245 -0
- package/memory-db-tools/audit-export.mjs +270 -0
- package/memory-db-tools/audit-hit-probe.mjs +227 -0
- package/memory-db-tools/audit-ledger-probe.mjs +236 -0
- package/memory-db-tools/audit-persona-overlap.mjs +277 -0
- package/memory-db-tools/audit-struct-probe.mjs +213 -0
- package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
- package/memory-db-tools/check-memory-db.mjs +8 -0
- package/memory-db-tools/dump-memory-db.mjs +12 -0
- package/memory-db-tools/lib/memory-scope.mjs +446 -0
- package/memory-db-tools/list-memory-summary.mjs +477 -0
- package/memory-db-tools/memory-identity-check.mjs +101 -0
- package/memory-db-tools/memory-schema-migrate.mjs +290 -0
- package/memory-db-tools/memory-scope-fixture.mjs +197 -0
- package/memory-db-tools/read-memories.mjs +26 -0
- package/memory-db-tools/read-memory-db.mjs +20 -0
- package/memory-db-tools/refine-memories.mjs +247 -0
- package/memory-db-tools/show-mem-times.mjs +22 -0
- package/memory-engine/README.md +70 -0
- package/memory-engine/bench/bench.mjs +221 -0
- package/memory-engine/cli/distill-rollback.mjs +86 -0
- package/memory-engine/cli/distill.mjs +156 -0
- package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
- package/memory-engine/cli/doc-index.mjs +48 -0
- package/memory-engine/cli/domain-tags.mjs +251 -0
- package/memory-engine/cli/pitfall-hook.mjs +91 -0
- package/memory-engine/cli/recall.mjs +132 -0
- package/memory-engine/cli/selfcheck.mjs +57 -0
- package/memory-engine/cli/serve-start.mjs +247 -0
- package/memory-engine/cli/sweep.mjs +58 -0
- package/memory-engine/cli/sync.mjs +69 -0
- package/memory-engine/lib/cli-util.mjs +197 -0
- package/memory-engine/lib/config.mjs +135 -0
- package/memory-engine/lib/dirty-ledger.mjs +174 -0
- package/memory-engine/lib/distill-rollback.mjs +178 -0
- package/memory-engine/lib/distill.mjs +898 -0
- package/memory-engine/lib/doc-index-store.mjs +325 -0
- package/memory-engine/lib/doc-index.mjs +529 -0
- package/memory-engine/lib/doc-scan.mjs +271 -0
- package/memory-engine/lib/doc-summarize.mjs +305 -0
- package/memory-engine/lib/domain-tags.mjs +429 -0
- package/memory-engine/lib/embedder.mjs +242 -0
- package/memory-engine/lib/hashes.mjs +12 -0
- package/memory-engine/lib/memory-source.mjs +127 -0
- package/memory-engine/lib/pitfall-source.mjs +170 -0
- package/memory-engine/lib/policy-source.mjs +371 -0
- package/memory-engine/lib/recall.mjs +331 -0
- package/memory-engine/lib/selfcheck.mjs +131 -0
- package/memory-engine/lib/sweep.mjs +159 -0
- package/memory-engine/lib/sync.mjs +180 -0
- package/memory-engine/lib/tick-hook.mjs +218 -0
- package/memory-engine/lib/vectors-store.mjs +292 -0
- package/memory-engine/package-lock.json +1086 -0
- package/memory-engine/package.json +22 -0
- package/memory-engine/serve.mjs +171 -0
- package/memory-engine/test/01-store.test.mjs +127 -0
- package/memory-engine/test/02-sync.test.mjs +196 -0
- package/memory-engine/test/03-sweep.test.mjs +145 -0
- package/memory-engine/test/04-recall.test.mjs +214 -0
- package/memory-engine/test/05-selfcheck.test.mjs +175 -0
- package/memory-engine/test/06-doc-index.test.mjs +928 -0
- package/memory-engine/test/07-domain-tags.test.mjs +298 -0
- package/memory-engine/test/08-distill.test.mjs +686 -0
- package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
- package/memory-engine/test/10-tick-hook.test.mjs +252 -0
- package/memory-engine/test/11-policy-source.test.mjs +224 -0
- package/memory-engine/test/_tickhook-worker.mjs +56 -0
- package/memory-engine/test/fixtures.mjs +205 -0
- package/memory-engine/test/harness.mjs +73 -0
- package/memory-engine/test/run.mjs +35 -0
- package/memory-engine/tools/bridge-smoke.mjs +80 -0
- package/memory-engine/tools/fetch-model.mjs +128 -0
- package/memory-engine/tools/smoke-recall.mjs +76 -0
- package/package.json +53 -4
- package/persona-memory-audit.mjs +217 -0
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
// ══ GENERATED FILE — DO NOT EDIT ══
|
|
2
|
+
// 本文件由 tools/sync-host-adapter.mjs 生成 —— 手工修改会在下一次同步时被覆盖,
|
|
3
|
+
// 且会被 tools/check-decoupling.mjs 的 R5 判为违规(副本 sha ≠ 唯一源)。改接口请改唯一源。
|
|
4
|
+
// source: tools/lib/host-adapter.mjs
|
|
5
|
+
// sourceSha256: e69900195cfd3a58efc449f9b37c75aea0d7bf76ba7c7517ab019a600393628c
|
|
6
|
+
// sourceVersion: 0.3.1
|
|
7
|
+
// format: esm
|
|
8
|
+
// target: memory-local-lib-esm
|
|
9
|
+
// ══ END HEADER ══
|
|
10
|
+
// tools/lib/host-adapter.mjs —— 宿主适配层(**唯一源 / single source of truth**)
|
|
11
|
+
//
|
|
12
|
+
// 方案:docs\大管家adapter能力-方案-20260914.md §3.2(接口契约)/ §3.7(唯一源 + 派生副本)
|
|
13
|
+
// 批次:P0 · 档 A(**等价替换,零语义变化**)
|
|
14
|
+
// P1 · 收敛扩面(2026-09-15):补 `SVC` 服务名常量表 + 补齐各包用到的 `EV` 条目 + `eventLabel()`
|
|
15
|
+
// —— 依据方案 §10.2「解法 A(真收敛)」:服务名/事件名字面量收敛到本唯一源,
|
|
16
|
+
// 各包只写 `inject = [SVC.webServer]` / `ctx.inject([SVC.settings], cb)` / 常量比较。
|
|
17
|
+
// 仍守档 A:**零语义变化**(只把字面量换成同值常量,不改任何判定口径)。
|
|
18
|
+
// P2-提前批 · 服务面收敛 + 能力档(2026-09-15):
|
|
19
|
+
// ① 新增 `settingsApiOf()` / `installSectionCompat()` —— settings 首装面的双线分支承载;
|
|
20
|
+
// ② 新增 `serviceFace()` / `hostCapabilities()` / `capabilityProfile()` —— **只输出事实,不含策略**;
|
|
21
|
+
// ③ `EV` 补 15 条 0.1.5 新增事件常量(**仅常量**,不进任何判定表、不改任何判定);
|
|
22
|
+
// ④ 新增 `CLIENT_PKG` / `clientInjectDecl()` —— 客户端 inject 声明解析(§3.4 静默跳过口径)。
|
|
23
|
+
// ⚠️ **零判定变更**:`classifyTail` / `isTurnInterior` / `isSessionLevel` / `isTerminalTurnEnd` /
|
|
24
|
+
// `UNKNOWN_FAMILY_POLICY` 本批**一行未改**(档 B 仍未切,见方案 §10.2 / 待拍板事项 ②)。
|
|
25
|
+
// ⚠️ 订正注(2026-09-28 · 只标注不改原文):上句「档 B 仍未切」**已过期** —— 主人 2026-09-28 11:38
|
|
26
|
+
// 拍板「切」,档 B 已上生产,`UNKNOWN_FAMILY_POLICY` 现为 `'open'`(见本文件该常量定义处)。
|
|
27
|
+
// 依据 = `docs\交付报告-adapter档B切与不切差异-20260928.md`(1554 真实会话差异 0 条 + 构造样本正向验证)。
|
|
28
|
+
//
|
|
29
|
+
// 为什么存在(一句话):主包(@deepseek-ai/dsh*)的事件名 / 服务名 / 事件粒度会随版本变
|
|
30
|
+
// (0.1.5 删 `assistant/chunk`、`tool/code-dispatch*` 改名 `tool/ptc-dispatch*`、
|
|
31
|
+
// `assistant/attempt` 由"增量流"改"结算式整段流"),把**字面量散落在 7 个自研包**里,
|
|
32
|
+
// 升级时改点会随功能增长而增长。本文件是**全生态唯一允许出现主包事件名/服务名字面量**的地方,
|
|
33
|
+
// 其余代码只调这里的语义化接口(族判定 / 语义取数 / 探测 / 能力档)。
|
|
34
|
+
//
|
|
35
|
+
// 硬约束(违反即失去存在意义):
|
|
36
|
+
// 1. **零依赖、纯函数、零状态** —— 不 import 任何模块(含 node: 内置),不读盘、不读环境变量、
|
|
37
|
+
// 不 import `@deepseek-ai/*` 运行时符号(连 `import type` 都不需要:本文件是纯 JS 判定)。
|
|
38
|
+
// 2. **本文件不承担"策略"** —— 只给"结构 + 事实 + 族";"被打断会话要不要报""未闭合算不算活跃"
|
|
39
|
+
// 这类策略仍归调用方(面板宁少报不误报 / P2 宁多报不放过,语义不同,禁止在这里统一)。
|
|
40
|
+
// 3. **不得用墙钟做判据**(方案 §9.3 R3)—— 一律 `now - 事件时间` 的单调差值,
|
|
41
|
+
// 窗口常量集中在本文件,测试覆盖"未来时间 / 半截帧"两个既有反例。
|
|
42
|
+
// 4. **P0 档 A ≠ 档 B**:本文件现阶段的族判定**必须在现有样本上与旧的显式白名单逐条等价**
|
|
43
|
+
// (回归差异必须为 0)。"未知族归 open"(更安全但换尺子)属**档 B**,等裸跑收尾 + 主人单独批准,
|
|
44
|
+
// 在本文件里以 `UNKNOWN_FAMILY_POLICY` 一处集中表达,**P0 一律 'unknown'**。
|
|
45
|
+
// ⚠️ 订正注(2026-09-28 · 只标注不改原文):上句「**P0 一律 'unknown'**」**已过期** —— 前置条件(裸跑收尾 +
|
|
46
|
+
// 差异量化)已满足,主人 2026-09-28 11:38 单独批准切档 B ⇒ 现行值 = `'open'`(档 B)。
|
|
47
|
+
// **P1/P2 语义不变**:本文件仍只给「结构 + 事实 + 族」,策略仍归调用方(:27 第 2 条不变)。
|
|
48
|
+
//
|
|
49
|
+
// 副本策略(§3.7):插件实体可能被单独拷到别的机器 ⇒ 不能"一个文件被 7 包共同 import"。
|
|
50
|
+
// 唯一源 = 本文件;派生副本 = `tools/sync-host-adapter.mjs` 幂等生成到各包 `lib/host-adapter.mjs`(含源 sha 头);
|
|
51
|
+
// 守门 = `tools/check-decoupling.mjs` R5 校验副本 sha == 唯一源。
|
|
52
|
+
//
|
|
53
|
+
// 【设备:ROG】|P0 落地 2026-09-14 | P2-提前批 2026-09-15 | +`SVC.clientModules`(生态面板 boot 解析修复)2026-09-23
|
|
54
|
+
// | 2026-09-28 档 B 切(`UNKNOWN_FAMILY_POLICY`: 'unknown' → 'open',主人单独批准)
|
|
55
|
+
// | 2026-10-10 空会话误判治本(`classifyTail` 加「整段零回合内事件 ⇒ closed」前置判定;
|
|
56
|
+
// 修「新建空会话静止 10 分钟被判卡死」;真源 `docs\交付报告-卡死自愈空会话误判治本-20261010.md`)
|
|
57
|
+
// | 2026-10-16 `eventUsage()` 补 `assistant/message` 分支(0.2.x 现行日志的 usage 落点;此前只认
|
|
58
|
+
// 0.1.1 `assistant/chunk` / 0.1.5 `assistant/attempt` ⇒ 现行日志命中 0、账本按天数据自 9-22 冻结);
|
|
59
|
+
// ⚠️ `HOST_ADAPTER_VERSION` **保持 0.3.1 不变** —— 组件版本登记在 `lib/version.mjs` 的 PLUGIN_COMPONENTS,
|
|
60
|
+
// bump 属独立变更(须主人拍板)⇒ 本次变更以 **sha256 + 本变更史行** 为准;
|
|
61
|
+
// 真源 `docs\交付报告-harness成本与路由-拍板落地A组-读数修复与轻建议-20261016.md`
|
|
62
|
+
|
|
63
|
+
export const HOST_ADAPTER_VERSION = '0.3.1';
|
|
64
|
+
/** 唯一源标识(副本头 / R5 校验 / 报告用)。 */
|
|
65
|
+
export const HOST_ADAPTER_SOURCE = 'tools/lib/host-adapter.mjs';
|
|
66
|
+
|
|
67
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// 词表 —— 主包事件名的唯一落点
|
|
69
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
/** 已取证的主包事件名(0.1.1-rc.2 现网线 / 0.1.5-rc.x 金丝雀线)。 */
|
|
72
|
+
export const EV = Object.freeze({
|
|
73
|
+
// —— 回合边界(两线同名)
|
|
74
|
+
TURN_START: 'turn/start',
|
|
75
|
+
TURN_END: 'turn/end',
|
|
76
|
+
// —— 会话终结标记(重启/停机时主包给当时活跃会话补写)
|
|
77
|
+
SESSION_END_SEED: 'session/end-seed',
|
|
78
|
+
SESSION_TITLE: 'session/title',
|
|
79
|
+
// —— 模型产出(★ 两线**粒度不同**,不是简单改名)
|
|
80
|
+
LEGACY_ASSISTANT_CHUNK: 'assistant/chunk', // 0.1.1:增量流(chunk 序列)
|
|
81
|
+
ASSISTANT_ATTEMPT: 'assistant/attempt', // 0.1.5:结算式(一条带完整 stream[] + usage + interrupted)
|
|
82
|
+
ASSISTANT_MESSAGE: 'assistant/message', // 两线共有:完整消息
|
|
83
|
+
// —— 代码派发(★ 0.1.5 改名)
|
|
84
|
+
LEGACY_TOOL_CODE_DISPATCH: 'tool/code-dispatch',
|
|
85
|
+
LEGACY_TOOL_CODE_DISPATCH_START: 'tool/code-dispatch-start',
|
|
86
|
+
TOOL_PTC_DISPATCH: 'tool/ptc-dispatch',
|
|
87
|
+
TOOL_PTC_DISPATCH_START: 'tool/ptc-dispatch-start',
|
|
88
|
+
// —— 常用工具事件
|
|
89
|
+
TOOL_CALL: 'tool/call',
|
|
90
|
+
TOOL_RESULT: 'tool/result',
|
|
91
|
+
USER_MESSAGE: 'user/message',
|
|
92
|
+
// —— P1 收敛补充(原先散在各包的裸字面量;两线同名)
|
|
93
|
+
SESSION_EVENT: 'session/event', // 会话事件总线(订阅点,非写入事件)
|
|
94
|
+
SESSION_TITLE_LLM_REQUEST: 'session/title-llm-request',
|
|
95
|
+
CONNECTION_RESET: 'connection/reset', // 客户端侧连接重置
|
|
96
|
+
STEP_START: 'step/start',
|
|
97
|
+
REQUEST_HEADER: 'request/header',
|
|
98
|
+
REQUEST_CONTEXT: 'request/context',
|
|
99
|
+
PERMISSION_PRESET: 'permission/preset',
|
|
100
|
+
SANDBOX_MODE: 'sandbox/mode',
|
|
101
|
+
APPROVAL_POLICY: 'approval/policy',
|
|
102
|
+
// —— P5 写入守门人事件(butler-panel / audit-guard 消费)
|
|
103
|
+
FS_WRITE_INTENT: 'fs/write-intent',
|
|
104
|
+
FS_EDIT_INTENT: 'fs/edit-intent',
|
|
105
|
+
FS_OBSERVED: 'fs/observed',
|
|
106
|
+
TOOLS_PRE_EXECUTE: 'tools/pre-execute',
|
|
107
|
+
// —— P2:0.1.5 新增事件族(取证口径 = 两线 `lib/types/known-event-types.js` 逐行 diff,
|
|
108
|
+
// 见 docs\长期库大扫除-20260914\adapter-认知缺口取证-20260915.md)。
|
|
109
|
+
// ⚠️ **只登记常量,不进任何判定表**:这些名字属"新线上可能出现的事件",
|
|
110
|
+
// 是否需要据此改判定属档 B(换尺子),本批不做。
|
|
111
|
+
DELIVERABLES_PRESENTED: 'deliverables/presented',
|
|
112
|
+
FEEDBACK_MESSAGE_PUT: 'feedback/message-put',
|
|
113
|
+
FEEDBACK_MESSAGE_DELETE: 'feedback/message-delete',
|
|
114
|
+
MODEL_SELECTION: 'model/selection',
|
|
115
|
+
SESSION_LOG_DELIVERY_ACCEPTED: 'session-log-deepseek/delivery-accepted',
|
|
116
|
+
SUBAGENT_CATALOG: 'subagent/catalog',
|
|
117
|
+
SUBAGENT_MODEL_SELECTION_POLICY: 'subagent/model-selection-policy',
|
|
118
|
+
SYSTEM_MESSAGE: 'system/message',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* ctx 服务名(**唯一落点**)—— 各包的静态 `inject` 声明与运行时探测一律从这里取。
|
|
123
|
+
*
|
|
124
|
+
* 为什么必须有这张表(方案 §10.2 解法 A · 真收敛):
|
|
125
|
+
* `inject` 是宿主契约声明,名字本身无法用"语义调用"消除;写在各包里就是 R4 违规
|
|
126
|
+
* (裸服务名字面量),收敛到唯一源才是真收敛 —— `inject = [SVC.webServer]` 与
|
|
127
|
+
* `ctx.inject([SVC.settings], cb)` 都不再在消费包里留字面量。
|
|
128
|
+
*
|
|
129
|
+
* ⚠️ **双线形态**:0.1.1-rc.2(现网生产)与 0.1.5-rc.x(金丝雀)的服务名**逐条同名**
|
|
130
|
+
* (取证:两线主包 `provide('<name>')` / `reflect.provide('<name>', …)` 对照),
|
|
131
|
+
* 故本表**没有 legacyName 映射**;将来某线真改名时,只需在这里补一个 `*_LEGACY` 键 +
|
|
132
|
+
* `pick(ctx, [SVC.x, SVC.x_LEGACY])`,调用方仍零改动。
|
|
133
|
+
* 服务名清单口径 = `docs\自研插件运行时API登记表.md` §1.2(与 check-decoupling R4 词表同源)。
|
|
134
|
+
*
|
|
135
|
+
* ⚠️ **客户端侧引用不到本表**:浏览器 bundle 是 `window.__ModuleLoader__` 的 CJS 形态,
|
|
136
|
+
* 由宿主按 `exports["./client"]` 单文件 serve(`/plugins/<id>/client.js`),无法 import 副本
|
|
137
|
+
* ⇒ 客户端 bundle 里的服务名/事件名字面量在当前口径下不可收敛(P1 余项,见交付报告)。
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ **`apiProxy` 已在 0.1.5 线整体移除**(P2 取证 2026-09-15,判据 = 两线主包闭包 grep):
|
|
140
|
+
* 0.1.1 全闭包 `apiProxy` 命中 **93 处**、0.1.5 命中 **0 处**;0.1.5 的服务面换成
|
|
141
|
+
* `sessionController` / `settingsController` / `workspaceController` / `sessionFeedback` …(见 `serviceFace()`)。
|
|
142
|
+
* ⇒ 探测口径:`pick(ctx, [SVC.apiProxy]) === null` 即"该服务在本线不存在",
|
|
143
|
+
* **不得**把它留在静态 `inject` 声明里(会让 entry 永久 pending = boot 失败,方案 §3.4 / §9.2 I3)。
|
|
144
|
+
*/
|
|
145
|
+
export const SVC = Object.freeze({
|
|
146
|
+
webServer: 'webServer',
|
|
147
|
+
settings: 'settings',
|
|
148
|
+
settingsScope: 'settingsScope',
|
|
149
|
+
commands: 'commands',
|
|
150
|
+
tools: 'tools',
|
|
151
|
+
skills: 'skills',
|
|
152
|
+
sessionTitle: 'sessionTitle',
|
|
153
|
+
llm: 'llm',
|
|
154
|
+
sessions: 'sessions',
|
|
155
|
+
systemPrompt: 'systemPrompt',
|
|
156
|
+
/** ⚠️ **仅 0.1.1/0.1.2 存在;0.1.5 已整体移除**(探测走 `pick()`,勿进静态 inject)。 */
|
|
157
|
+
apiProxy: 'apiProxy',
|
|
158
|
+
agents: 'agents',
|
|
159
|
+
logger: 'logger',
|
|
160
|
+
locale: 'locale',
|
|
161
|
+
slots: 'slots',
|
|
162
|
+
connection: 'connection',
|
|
163
|
+
remote: 'remote',
|
|
164
|
+
workspaces: 'workspaces',
|
|
165
|
+
// —— P2 新增:0.1.5 线独有的服务名(登记以便探测候选;0.1.1 上不存在 ⇒ 探测返回缺)
|
|
166
|
+
sessionController: 'sessionController',
|
|
167
|
+
settingsController: 'settingsController',
|
|
168
|
+
workspaceController: 'workspaceController',
|
|
169
|
+
sessionFeedback: 'sessionFeedback',
|
|
170
|
+
workspaceFiles: 'workspaceFiles',
|
|
171
|
+
documentPreviews: 'documentPreviews',
|
|
172
|
+
fileUpload: 'fileUpload',
|
|
173
|
+
resources: 'resources',
|
|
174
|
+
webhookRuntime: 'webhookRuntime',
|
|
175
|
+
/**
|
|
176
|
+
* 客户端启动图服务(0.1.x 两线同名):`ctx.clientModules.graph()` 返回的**正是服务进
|
|
177
|
+
* `window.__DSH_BOOT__` 的那份图**(`{ entries, batches }`,元素含 `id`)——
|
|
178
|
+
* 权威、进程内、不经 HTTP、不受 0.1.5 首页 token 门影响。
|
|
179
|
+
* ⚠️ 消费方**必须动态探测**(`ctx.inject([SVC.clientModules], cb)`),**不得**写进静态
|
|
180
|
+
* `inject`:该线若无此服务 → entry 永久 pending = boot 崩(方案 §3.4 / §9.2 I3)。
|
|
181
|
+
* ★2026-09-23 新增(生态面板 boot 解析修复):取代"爬首页 HTML 猜 `__DSH_BOOT__` 字面形态"
|
|
182
|
+
* —— 那条路在 0.1.5 上永远抓不到(注入形态变更 + token 门),害得面板把活着的插件
|
|
183
|
+
* 报成"未加载"。真源 `docs\交付报告-缺陷B复评-cliententry-20260923.md` §5.1。
|
|
184
|
+
*/
|
|
185
|
+
clientModules: 'clientModules',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 主包服务面在两条线上的**已知差异**(事实层;仅供 `serviceFace()` 与文档使用,**不含策略**)。
|
|
190
|
+
* 取证:2026-09-15 两线主包闭包 `super(ctx, "<name>")` 全量扫描(76 个 vs 101 个服务名)。
|
|
191
|
+
*/
|
|
192
|
+
export const SVC_GONE_ON_015 = Object.freeze(['apiProxy', 'conversationEvents', 'conversationViews']);
|
|
193
|
+
/** 0.1.5 新增的服务名(0.1.1 无)。 */
|
|
194
|
+
export const SVC_NEW_ON_015 = Object.freeze([
|
|
195
|
+
'credentialsController', 'directoryPickerController', 'documentPreviews', 'fileUpload', 'fileUploads',
|
|
196
|
+
'resources', 'sessionController', 'sessionFeedback', 'sessionFileReferences', 'sessionSkillCatalog',
|
|
197
|
+
'settingsController', 'sidebarRight', 'sidebarRightTabs', 'subagentModelSelection', 'uiConversation',
|
|
198
|
+
'uiSession', 'uiWorkspace', 'webhookRuntime', 'workspaceController', 'workspaceFiles',
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 回合边界事件(`turn/start` ↔ `turn/end` 配对折叠)。
|
|
203
|
+
* ⚠️ 仍以 Set 形式导出:`restart-coordinator.mjs` 等处用 `.has()` 消费(保持既有公共 API 不变)。
|
|
204
|
+
*/
|
|
205
|
+
export const TURN_BOUNDARY_TYPES = new Set([EV.TURN_START, EV.TURN_END]);
|
|
206
|
+
|
|
207
|
+
/** 种子/会话终结标记(主包 dsh-session:seed 会话末尾追加)。 */
|
|
208
|
+
export const SESSION_END_TYPES = new Set([EV.SESSION_END_SEED]);
|
|
209
|
+
|
|
210
|
+
/** `turn/end` 的「正常收尾」reason.kind;其余(interrupted/aborted/error/…)一律当未正常收尾。 */
|
|
211
|
+
export const NORMAL_TURN_END_KINDS = new Set(['completed', 'stop']);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 回合中间事件 —— **族前缀**(0.1.5 的教训:枚举事件名必死,新族会持续出现)。
|
|
215
|
+
* 命中即"回合内部正在发生的事"。
|
|
216
|
+
*/
|
|
217
|
+
const TURN_INTERIOR_PREFIXES = Object.freeze([
|
|
218
|
+
'assistant/', // 含 0.1.1 的 chunk/message 与 0.1.5 的 attempt
|
|
219
|
+
'user/',
|
|
220
|
+
'tool/', // ⚠️ `tool-workflow/…` 不落此处(第 5 字符是 '-' 不是 '/'),单独列前缀
|
|
221
|
+
'step/',
|
|
222
|
+
'llm/',
|
|
223
|
+
'request/',
|
|
224
|
+
'todo/',
|
|
225
|
+
'hook/',
|
|
226
|
+
'tool-workflow/',
|
|
227
|
+
'team/',
|
|
228
|
+
'subagent/',
|
|
229
|
+
'compaction/',
|
|
230
|
+
'feedback/',
|
|
231
|
+
'web/',
|
|
232
|
+
'agent/', // 含 `agent/inbox/spliced`;`agent-preset/selected` 不匹配(前缀不同)
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
/** 族前缀盖不到的回合中间事件(逐个列名)。 */
|
|
236
|
+
const TURN_INTERIOR_EXACT = new Set([
|
|
237
|
+
'session/title-llm-request', // 前缀是 session/ 但语义属回合内(标题生成请求)
|
|
238
|
+
// ⚠️ `approval/` **不能整族纳入**:asked/decided 是回合内动作,而 `approval/policy`
|
|
239
|
+
// 是会话级配置事件(在 OUT_OF_TURN_TYPES 里)⇒ 只能逐项列。
|
|
240
|
+
'approval/asked',
|
|
241
|
+
'approval/decided',
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 回合中间事件显式表(0.1.1-rc.2 词表口径,含 0.1.5 改名后的对应项)。
|
|
246
|
+
* ⚠️ **判定不再用它**(改走 `isTurnInterior` 族判定);保留为:①兼容导出(旧引用)
|
|
247
|
+
* ②族判定的"金标准"对照(单测断言 族 ⊇ 本表,防止族前缀写漏)。
|
|
248
|
+
*/
|
|
249
|
+
export const MID_TURN_TYPES = new Set([
|
|
250
|
+
EV.LEGACY_ASSISTANT_CHUNK, EV.ASSISTANT_ATTEMPT, EV.ASSISTANT_MESSAGE, 'user/message',
|
|
251
|
+
'step/start', 'step/end',
|
|
252
|
+
'tool/call', 'tool/result', EV.LEGACY_TOOL_CODE_DISPATCH, EV.LEGACY_TOOL_CODE_DISPATCH_START,
|
|
253
|
+
EV.TOOL_PTC_DISPATCH, EV.TOOL_PTC_DISPATCH_START,
|
|
254
|
+
'llm/retry', 'llm/retry-started',
|
|
255
|
+
'approval/asked', 'approval/decided',
|
|
256
|
+
'agent/inbox/spliced',
|
|
257
|
+
'request/header', 'request/context',
|
|
258
|
+
'todo/write',
|
|
259
|
+
'hook/invoked', 'hook/result',
|
|
260
|
+
'tool-workflow/agent-start', 'tool-workflow/agent-end',
|
|
261
|
+
'tool-workflow/run-start', 'tool-workflow/run-end',
|
|
262
|
+
'team/member', 'team/message/queued', 'team/message/delivered', 'team/task',
|
|
263
|
+
'subagent/descriptor',
|
|
264
|
+
'compaction/start', 'compaction/summary', 'compaction/end', 'compaction/prune',
|
|
265
|
+
'feedback/record',
|
|
266
|
+
'web/deepseek-search-llm-request', 'session/title-llm-request',
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
/** 会话级 / 回合外事件(出现在 turn/end 之后属正常,不表示回合进行中)。 */
|
|
270
|
+
export const OUT_OF_TURN_TYPES = new Set([
|
|
271
|
+
EV.TURN_END, EV.SESSION_END_SEED,
|
|
272
|
+
'session/title', 'goal/change', 'schedule/change',
|
|
273
|
+
'plan/mode', 'permission/preset', 'sandbox/mode', 'approval/policy',
|
|
274
|
+
'agent-preset/selected', 'command/run', 'command/done',
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 未知族的归属策略(§3.3 档 A / 档 B 的唯一开关点)。
|
|
279
|
+
* - `'unknown'` = **档 A(P0 现状)**:不认识的事件不当"回合进行中",落 unknown 由 caller 决定。
|
|
280
|
+
* - `'open'` = **档 B(换尺子)**:未知族归 open(安全侧)。
|
|
281
|
+
* ⚠️ P0 阶段**禁止改成 'open'** —— 属换尺子,必须等裸跑收尾 + 主人单独批准(卡红线)。
|
|
282
|
+
* 订正注(2026-09-28 · 只标注不改原文):该禁令**已解除** —— 前置条件(裸跑收尾 + 差异量化)已满足,
|
|
283
|
+
* 主人 2026-09-28 11:38 单独批准切档 B 并上生产 ⇒ 本行现行值 = `'open'`。上句保留为历史注记。
|
|
284
|
+
* 依据 = `docs\交付报告-adapter档B切与不切差异-20260928.md`(1554 真实会话差异 0 条 + 构造样本正向验证)。
|
|
285
|
+
* 回滚 = 本行改回 `'unknown'` → 重跑 sync + 各包 deploy + P0 闸门重启;
|
|
286
|
+
* 逐字节回滚器 = `_archive\adapter-policyB-20260928-1141\restore.mjs`(默认 dry-run,`--apply` 真回写,逐文件核 sha256)。
|
|
287
|
+
*/
|
|
288
|
+
export const UNKNOWN_FAMILY_POLICY = 'open';
|
|
289
|
+
|
|
290
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
291
|
+
// events 组 —— 族判定与结构判定
|
|
292
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
293
|
+
|
|
294
|
+
const isStr = (v) => typeof v === 'string' && v.length > 0;
|
|
295
|
+
|
|
296
|
+
/** `turn/start`。 */
|
|
297
|
+
export function isTurnStart(type) {
|
|
298
|
+
return type === EV.TURN_START;
|
|
299
|
+
}
|
|
300
|
+
/** `turn/end`。 */
|
|
301
|
+
export function isTurnEnd(type) {
|
|
302
|
+
return type === EV.TURN_END;
|
|
303
|
+
}
|
|
304
|
+
/** 回合边界(start 或 end)。 */
|
|
305
|
+
export function isTurnBoundary(type) {
|
|
306
|
+
return type === EV.TURN_START || type === EV.TURN_END;
|
|
307
|
+
}
|
|
308
|
+
/** 会话终结标记(`session/end-seed`)。 */
|
|
309
|
+
export function isSessionEnd(type) {
|
|
310
|
+
return type === EV.SESSION_END_SEED;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 回合中间事件(族判定;替代旧的事件名枚举)。 */
|
|
314
|
+
export function isTurnInterior(type) {
|
|
315
|
+
if (!isStr(type)) return false;
|
|
316
|
+
if (TURN_INTERIOR_EXACT.has(type)) return true;
|
|
317
|
+
for (const p of TURN_INTERIOR_PREFIXES) if (type.startsWith(p)) return true;
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 会话级 / 回合外事件(出现在回合收尾之后不表示"在跑")。 */
|
|
322
|
+
export function isSessionLevel(type) {
|
|
323
|
+
return isStr(type) && OUT_OF_TURN_TYPES.has(type);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 是否「正常收尾」的 `turn/end`。
|
|
328
|
+
* 口径与旧实现逐字一致:缺 kind / 空 kind 视为正常(老日志没有 kind),
|
|
329
|
+
* kind 非空且不在 NORMAL_TURN_END_KINDS 内 → 未正常收尾(interrupted)。
|
|
330
|
+
*/
|
|
331
|
+
export function isTerminalTurnEnd(ev) {
|
|
332
|
+
if (!ev || !isTurnEnd(ev.type)) return false;
|
|
333
|
+
const kind = ev?.data?.reason?.kind;
|
|
334
|
+
if (typeof kind !== 'string' || kind === '') return true;
|
|
335
|
+
return NORMAL_TURN_END_KINDS.has(kind);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** `turn/end` 的非正常 kind(无/空/正常 → null)。 */
|
|
339
|
+
export function turnEndAbnormalKind(ev) {
|
|
340
|
+
const kind = ev?.data?.reason?.kind;
|
|
341
|
+
if (typeof kind !== 'string' || kind === '') return null;
|
|
342
|
+
return NORMAL_TURN_END_KINDS.has(kind) ? null : kind;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 折叠尾部事件的回合开闭状态(结构判定;**策略仍归 caller**)。
|
|
347
|
+
*
|
|
348
|
+
* 迁移自 `dsh-butler/lib/restart.mjs` 的 `classifyTail`(2026-09-11 v0.1.1 语义,逐字保留):
|
|
349
|
+
* ① `turn/end` 的 `data.reason.kind` 不在 NORMAL_TURN_END_KINDS → `state='interrupted'`
|
|
350
|
+
* (回合结束了,但是被打断的);`interrupted` 对"活跃判定"等价 closed,
|
|
351
|
+
* 对"被打断清单 / P2 会盯人"是一条真信号。
|
|
352
|
+
* ② `session/end-seed` 透明化 —— 它是重启/停机补写的收尾标记,常落在被打断会话最后一条、
|
|
353
|
+
* 把真尾挡住 ⇒ 记下 `seedSeen` 后**继续往前找**真边界;窗口内无任何边界 → closed(种子会话已终结)。
|
|
354
|
+
* ③ 窗口内无边界 → 按最后一条事件归类:seedSeen → closed;回合中间族 → open;否则按档 B。
|
|
355
|
+
* ⚠️ 订正注(2026-10-10 · 只标注不改原文):原文末句写「否则 → unknown」——`UNKNOWN_FAMILY_POLICY`
|
|
356
|
+
* 自 2026-09-28 起为 `'open'`(档 B);且该「否则」分支现在**还受 ④ 收窄**。
|
|
357
|
+
* ④ **2026-10-10 治本**(卡「卡死自愈空会话误判」):窗口内无边界时,若整段 `events`
|
|
358
|
+
* **一个"回合内事件"都没有**(`isTurnInterior` 对全部事件均为 false)⇒ 判 `closed` —— 该会话
|
|
359
|
+
* 从未进入过回合(新建 / 空壳:尾巴只有会话级种子事件,如 `permission/preset`、`sandbox/mode`、
|
|
360
|
+
* `approval/policy`,甚至只有首帧 `session`)。真卡死会话的尾巴里**必然**含回合内事件
|
|
361
|
+
* (`turn/start` / `step/*` / `assistant/*` / `tool/*` / `agent/*` / `request/*` …),
|
|
362
|
+
* 尾部只读窗口 256KB 不会把它们全切掉 ⇒「零回合内事件」判 open 没有依据。
|
|
363
|
+
* **有任一回合内事件时下面逐字不变**(mid-turn → open;否则按 `UNKNOWN_FAMILY_POLICY`)。
|
|
364
|
+
*
|
|
365
|
+
* @param {Array<{type:string,seq?:number,time?:number,data?:any}>} events 尾部事件(seq 升序)
|
|
366
|
+
* @returns {{state:'empty'|'closed'|'open'|'interrupted'|'unknown', boundary:object|null, last:object|null, reason:string}}
|
|
367
|
+
*/
|
|
368
|
+
export function classifyTail(events) {
|
|
369
|
+
const list = Array.isArray(events) ? events.filter((e) => e && isStr(e.type)) : [];
|
|
370
|
+
if (!list.length) return { state: 'empty', boundary: null, last: null, reason: 'no-events' };
|
|
371
|
+
const last = list[list.length - 1];
|
|
372
|
+
let seedSeen = false;
|
|
373
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
374
|
+
const e = list[i];
|
|
375
|
+
if (isTurnEnd(e.type)) {
|
|
376
|
+
const abnormal = turnEndAbnormalKind(e);
|
|
377
|
+
const suffix = seedSeen ? '(在 session/end-seed 之前)' : '';
|
|
378
|
+
return {
|
|
379
|
+
state: abnormal ? 'interrupted' : 'closed',
|
|
380
|
+
boundary: e,
|
|
381
|
+
last,
|
|
382
|
+
reason: abnormal ? `boundary=turn/end(kind=${abnormal})${suffix}` : `boundary=turn/end${suffix}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (isTurnStart(e.type)) {
|
|
386
|
+
return {
|
|
387
|
+
state: 'open',
|
|
388
|
+
boundary: e,
|
|
389
|
+
last,
|
|
390
|
+
reason: seedSeen ? 'boundary=turn/start(在 session/end-seed 之前,回合未闭合)' : 'boundary=turn/start',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// end-seed 不当边界直接判 closed,而是记下"已终结"后继续往前找真尾
|
|
394
|
+
if (isSessionEnd(e.type)) { seedSeen = true; continue; }
|
|
395
|
+
}
|
|
396
|
+
// 窗口内无回合边界:按最后一条事件归类(fail-safe 由调用方 + mtime 窗口兜底)
|
|
397
|
+
if (seedSeen) return { state: 'closed', boundary: null, last, reason: 'session/end-seed(会话已终结)' };
|
|
398
|
+
// ★ 2026-10-10 治本(卡「卡死自愈空会话误判」):**前置判定** —— 整段里一个"回合内事件"都没有
|
|
399
|
+
// ⇒ 这个会话从未进入过回合(新建 / 空壳)。此时判 open 没有依据,改判 closed。
|
|
400
|
+
// 为什么只在这一档收窄:真卡死会话的尾巴里必然含回合内事件(turn/start、step/*、assistant/*、
|
|
401
|
+
// tool/*、agent/*、request/* …),尾部只读窗口 256KB 不会把它们全切掉(turn/start 之后的事件都在窗口内);
|
|
402
|
+
// 而"整段零回合内事件"= 只有会话级种子事件(permission/preset、sandbox/mode、approval/policy)
|
|
403
|
+
// 甚至只有首帧 session。原判 open 的后果(2026-10-10 生产实况):空会话静止 10 分钟被卡死自愈链
|
|
404
|
+
// 判「回合未闭合」→ 发提醒(提醒本身把空会话唤醒)、整夜模式下还会给 P0 闸门塞白重启事由。
|
|
405
|
+
// ⚠️ 只收窄"整段零回合内事件"这一种情形:只要**有一个**回合内事件,下面逐字不变(档 B 意图保持)。
|
|
406
|
+
if (!list.some((e) => isTurnInterior(e.type))) {
|
|
407
|
+
return { state: 'closed', boundary: null, last, reason: `no-boundary no-turn-interior tail=${last.type}(纯会话级尾巴 ⇒ closed)` };
|
|
408
|
+
}
|
|
409
|
+
if (isTurnInterior(last.type)) {
|
|
410
|
+
return { state: 'open', boundary: null, last, reason: `no-boundary mid-turn tail=${last.type}` };
|
|
411
|
+
}
|
|
412
|
+
if (UNKNOWN_FAMILY_POLICY === 'open') {
|
|
413
|
+
return { state: 'open', boundary: null, last, reason: `no-boundary unknown-family tail=${last.type}(档 B:未知族归 open)` };
|
|
414
|
+
}
|
|
415
|
+
return { state: 'unknown', boundary: null, last, reason: `no-boundary tail=${last.type}` };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 人类可读的"尾巴停在 X"(打断清单 / 通知用)。
|
|
420
|
+
* 文案逐字保留旧实现(含"正在流式输出 / 新回合即将开始"两处特判)——文案属"人类可读映射",
|
|
421
|
+
* 是本文件(唯一源)的职责,各包自带映射表视作 R4 违规。
|
|
422
|
+
*/
|
|
423
|
+
export function describeTail(event) {
|
|
424
|
+
if (!event || !isStr(event.type)) return '尾巴停在未知事件';
|
|
425
|
+
const d = event.data ?? {};
|
|
426
|
+
const where = [];
|
|
427
|
+
if (Number.isFinite(d.turn)) where.push(`turn ${d.turn}`);
|
|
428
|
+
if (Number.isFinite(d.step)) where.push(`step ${d.step}`);
|
|
429
|
+
let what = event.type;
|
|
430
|
+
if (event.type === EV.TOOL_CALL && d.name) what = `tool/call ${d.name}`;
|
|
431
|
+
if (event.type === EV.TOOL_RESULT && d.message) what = 'tool/result';
|
|
432
|
+
if (event.type === EV.LEGACY_ASSISTANT_CHUNK) what = 'assistant/chunk(正在流式输出)';
|
|
433
|
+
if (event.type === EV.ASSISTANT_ATTEMPT) what = 'assistant/attempt(本轮产出已结算)';
|
|
434
|
+
if (event.type === EV.USER_MESSAGE) what = 'user/message(新回合即将开始)';
|
|
435
|
+
const when = Number.isFinite(event.time) ? new Date(event.time).toISOString() : null;
|
|
436
|
+
return `尾巴停在 ${what}${where.length ? `(${where.join(' · ')})` : ''}${when ? ` @${when}` : ''}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 事件类型 → 人话标签(**人类可读映射**,本文件职责;各包自带映射表视作 R4 违规,见方案 §9.2 I5)。
|
|
441
|
+
*
|
|
442
|
+
* 用途:向主人说明「这份日志里到底有什么」(归档会话的空壳预览等)。
|
|
443
|
+
* 未登记的类型**回落到事件名本身**(与旧实现 `LABELS[key] ?? key` 逐字等价)。
|
|
444
|
+
* ⚠️ 客户端 bundle 引用不到本文件 ⇒ 由 host 半调用本函数、把标签随 API 结果下发,
|
|
445
|
+
* client 只负责渲染(P1 收敛:archive-sessions 的 EVENT_LABELS 即按此迁移)。
|
|
446
|
+
*/
|
|
447
|
+
export function eventLabel(type) {
|
|
448
|
+
switch (type) {
|
|
449
|
+
case 'session': return '会话头';
|
|
450
|
+
case EV.USER_MESSAGE: return '主人消息';
|
|
451
|
+
case EV.ASSISTANT_MESSAGE: return '本鱼回复';
|
|
452
|
+
case EV.TOOL_CALL: return '工具调用';
|
|
453
|
+
case EV.TOOL_RESULT: return '工具结果';
|
|
454
|
+
case EV.PERMISSION_PRESET: return '权限预设';
|
|
455
|
+
case EV.SANDBOX_MODE: return '沙箱模式';
|
|
456
|
+
case EV.APPROVAL_POLICY: return '审批策略';
|
|
457
|
+
case EV.SESSION_END_SEED: return '会话结束标记';
|
|
458
|
+
case EV.TURN_END: return '回合结束';
|
|
459
|
+
case EV.LEGACY_ASSISTANT_CHUNK: return '流式片段';
|
|
460
|
+
// —— 以下为 P1 新增(原调用方表里没有;命中即"新曾出现过的事件",未命中仍回落事件名)
|
|
461
|
+
case EV.TURN_START: return '回合开始';
|
|
462
|
+
case EV.STEP_START: return '步骤开始';
|
|
463
|
+
case EV.SESSION_TITLE: return '会话标题';
|
|
464
|
+
default: return isStr(type) ? type : '未知事件';
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 本地兼容表:旧名 ↔ 新名(**只用于人类可读文案 / 迁移对照,不承担判定职责**)。
|
|
470
|
+
* @returns {{legacy:string, current:string, line:'0.1.1'|'0.1.5', note:string}|null}
|
|
471
|
+
*/
|
|
472
|
+
export function legacyName(type) {
|
|
473
|
+
switch (type) {
|
|
474
|
+
case EV.LEGACY_ASSISTANT_CHUNK:
|
|
475
|
+
return { legacy: 'assistant/chunk', current: 'assistant/attempt', line: '0.1.1', note: '增量流 → 结算式整段流(粒度变了,不只是改名)' };
|
|
476
|
+
case EV.ASSISTANT_ATTEMPT:
|
|
477
|
+
return { legacy: 'assistant/chunk', current: 'assistant/attempt', line: '0.1.5', note: '增量流 → 结算式整段流(粒度变了,不只是改名)' };
|
|
478
|
+
case EV.LEGACY_TOOL_CODE_DISPATCH:
|
|
479
|
+
return { legacy: 'tool/code-dispatch', current: 'tool/ptc-dispatch', line: '0.1.1', note: '改名' };
|
|
480
|
+
case EV.LEGACY_TOOL_CODE_DISPATCH_START:
|
|
481
|
+
return { legacy: 'tool/code-dispatch-start', current: 'tool/ptc-dispatch-start', line: '0.1.1', note: '改名' };
|
|
482
|
+
case EV.TOOL_PTC_DISPATCH:
|
|
483
|
+
return { legacy: 'tool/code-dispatch', current: 'tool/ptc-dispatch', line: '0.1.5', note: '改名' };
|
|
484
|
+
case EV.TOOL_PTC_DISPATCH_START:
|
|
485
|
+
return { legacy: 'tool/code-dispatch-start', current: 'tool/ptc-dispatch-start', line: '0.1.5', note: '改名' };
|
|
486
|
+
default:
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
492
|
+
// activity 组 —— 语义取数(§2.1.1:粒度变了 ⇒ 光有族判定只能"不炸",保证不了"不哑")
|
|
493
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
/** 流式静默窗口(单点定义;判据只用 `now - 事件时间` 的单调差值,禁墙钟)。 */
|
|
496
|
+
export const DEFAULT_STREAM_WINDOW_MS = 120_000;
|
|
497
|
+
|
|
498
|
+
const numOrNull = (v) => (Number.isFinite(Number(v)) ? Number(v) : null);
|
|
499
|
+
|
|
500
|
+
/** 是否"模型产出"事件(0.1.1 的 chunk/message;0.1.5 的 attempt)——族判定。 */
|
|
501
|
+
export function isAssistantProduction(type) {
|
|
502
|
+
return isStr(type) && type.startsWith('assistant/');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** 文本抽取(尽力而为,缺字段返回 '';绝不抛错)。 */
|
|
506
|
+
function textOfContent(content) {
|
|
507
|
+
if (typeof content === 'string') return content;
|
|
508
|
+
if (!Array.isArray(content)) return '';
|
|
509
|
+
let out = '';
|
|
510
|
+
for (const part of content) {
|
|
511
|
+
if (!part) continue;
|
|
512
|
+
if (typeof part === 'string') { out += part; continue; }
|
|
513
|
+
if (typeof part.text === 'string') out += part.text;
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** 0.1.5 `assistant/attempt.stream` 的文本抽取(结构未完全取证 ⇒ 递归收集 text 字段,缺则空串)。 */
|
|
519
|
+
function textOfStream(stream) {
|
|
520
|
+
if (typeof stream === 'string') return stream;
|
|
521
|
+
if (!Array.isArray(stream)) return '';
|
|
522
|
+
let out = '';
|
|
523
|
+
for (const rec of stream) {
|
|
524
|
+
if (!rec || typeof rec !== 'object') continue;
|
|
525
|
+
if (typeof rec.text === 'string') { out += rec.text; continue; }
|
|
526
|
+
if (rec.chunk && typeof rec.chunk.text === 'string') { out += rec.chunk.text; continue; }
|
|
527
|
+
if (rec.delta && typeof rec.delta.text === 'string') out += rec.delta.text;
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 单条事件的产出/用量(**逐事件**取数;token 统计等增量消费者用它)。
|
|
534
|
+
*
|
|
535
|
+
* **三线**形态判别(不靠版本号,靠事件形态 —— 版本读不到也不会跑偏):
|
|
536
|
+
* - 0.1.1:`assistant/chunk` + `data.chunk.type === 'usage'` → `data.chunk.usage`(旧行为逐字节不变)
|
|
537
|
+
* - 0.1.5:`assistant/attempt` + `data.usage` → `data.usage`(⚠️ 本机全库实测 usage 命中 0 条 = 纸面兼容)
|
|
538
|
+
* - 0.2.x:`assistant/message` + `data.usage` / `data.message.usage` → 现行日志的 usage 落点
|
|
539
|
+
* (★ 2026-10-16 补:此前只认前两线 ⇒ 现行日志命中 0、生产账本按天数据自 2026-09-22 静默冻结)
|
|
540
|
+
*
|
|
541
|
+
* 🔴 **双计陷阱**:0.1.1 世代**同一笔 usage 在 `assistant/chunk` 与 `assistant/message` 各出现一次**
|
|
542
|
+
* (2026-10-16 全库实测:1037 个 legacy 文件全部 `chunkUsage == messageUsage`)⇒ 两者都算会翻倍。
|
|
543
|
+
* 故 message 分支可经 `opts.message === false` 关闭 —— 「整文件世代判别」的消费者
|
|
544
|
+
* (`token-usage.mjs#extractUsage`)据此防双计;**逐事件消费者要么显式传参、要么确认单世代日志**。
|
|
545
|
+
*
|
|
546
|
+
* @param {object} ev 会话日志事件
|
|
547
|
+
* @param {{message?:boolean}} [opts] `message:false` ⇒ 不取 `assistant/message`(防 0.1.1 双计)
|
|
548
|
+
* @returns {{usage:object, turn:any, step:any, time:number, source:'chunk'|'attempt'|'message'}|null}
|
|
549
|
+
*/
|
|
550
|
+
export function eventUsage(ev, opts = {}) {
|
|
551
|
+
if (!ev || !isStr(ev.type)) return null;
|
|
552
|
+
const d = ev.data ?? {};
|
|
553
|
+
if (ev.type === EV.LEGACY_ASSISTANT_CHUNK) {
|
|
554
|
+
const chunk = d.chunk ?? null;
|
|
555
|
+
if (!chunk || chunk.type !== 'usage' || !chunk.usage) return null;
|
|
556
|
+
return { usage: chunk.usage, turn: d.turn ?? '?', step: d.step ?? '?', time: Number(ev.time ?? 0), source: 'chunk' };
|
|
557
|
+
}
|
|
558
|
+
if (ev.type === EV.ASSISTANT_ATTEMPT) {
|
|
559
|
+
if (!d.usage) return null;
|
|
560
|
+
return { usage: d.usage, turn: d.turn ?? '?', step: d.step ?? '?', time: Number(ev.time ?? 0), source: 'attempt' };
|
|
561
|
+
}
|
|
562
|
+
// ★ 2026-10-16:0.2.x 现行日志(usage 只在 assistant/message)—— 字段口径与 productionOf() 对齐
|
|
563
|
+
if (ev.type === EV.ASSISTANT_MESSAGE && opts.message !== false) {
|
|
564
|
+
const u = d.usage ?? d.message?.usage ?? null;
|
|
565
|
+
if (!u || typeof u !== 'object') return null;
|
|
566
|
+
return { usage: u, turn: d.turn ?? '?', step: d.step ?? '?', time: Number(ev.time ?? 0), source: 'message' };
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** 单条产出事件的归一记录(内部用)。 */
|
|
572
|
+
function productionOf(ev) {
|
|
573
|
+
const d = ev?.data ?? {};
|
|
574
|
+
if (ev.type === EV.ASSISTANT_ATTEMPT) {
|
|
575
|
+
return {
|
|
576
|
+
source: 'attempt',
|
|
577
|
+
text: textOfStream(d.stream),
|
|
578
|
+
usage: d.usage ?? null,
|
|
579
|
+
interrupted: d.interrupted === true,
|
|
580
|
+
turn: d.turn ?? null,
|
|
581
|
+
step: d.step ?? null,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (ev.type === EV.ASSISTANT_MESSAGE) {
|
|
585
|
+
return {
|
|
586
|
+
source: 'message',
|
|
587
|
+
text: textOfContent(d.message?.content ?? d.content),
|
|
588
|
+
usage: d.usage ?? d.message?.usage ?? null,
|
|
589
|
+
interrupted: false,
|
|
590
|
+
turn: d.turn ?? null,
|
|
591
|
+
step: d.step ?? null,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (ev.type === EV.LEGACY_ASSISTANT_CHUNK) {
|
|
595
|
+
const chunk = d.chunk ?? null;
|
|
596
|
+
const isUsage = chunk?.type === 'usage';
|
|
597
|
+
return {
|
|
598
|
+
source: 'chunk',
|
|
599
|
+
text: isUsage ? '' : (typeof chunk?.text === 'string' ? chunk.text : ''),
|
|
600
|
+
usage: isUsage ? (chunk.usage ?? null) : null,
|
|
601
|
+
interrupted: false,
|
|
602
|
+
turn: d.turn ?? null,
|
|
603
|
+
step: d.step ?? null,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 最后一次模型产出(时间 / 文本 / 用量)。
|
|
611
|
+
* 0.1.5 从 `assistant/attempt`(结算式)取;0.1.1 从 `assistant/message` / `assistant/chunk` 取。
|
|
612
|
+
* @returns {{at:number|null, text:string, usage:object|null, source:string, turn:any, step:any, index:number}|null}
|
|
613
|
+
*/
|
|
614
|
+
export function lastProduction(events) {
|
|
615
|
+
const list = Array.isArray(events) ? events : [];
|
|
616
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
617
|
+
const ev = list[i];
|
|
618
|
+
if (!ev || !isAssistantProduction(ev.type)) continue;
|
|
619
|
+
const rec = productionOf(ev);
|
|
620
|
+
if (!rec) continue;
|
|
621
|
+
return { at: numOrNull(ev.time), ...rec, index: i };
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* attempt 级状态(0.1.5 原生;0.1.1 由 chunk 序列推断)。
|
|
628
|
+
* @returns {{open:boolean, interrupted:boolean, usage:object|null, hasAttempt:boolean, lastSource:string|null}}
|
|
629
|
+
*/
|
|
630
|
+
export function attemptState(events) {
|
|
631
|
+
const list = Array.isArray(events) ? events : [];
|
|
632
|
+
let usage = null;
|
|
633
|
+
let interrupted = false;
|
|
634
|
+
let hasAttempt = false;
|
|
635
|
+
let lastSource = null;
|
|
636
|
+
for (const ev of list) {
|
|
637
|
+
if (!ev || !isAssistantProduction(ev.type)) continue;
|
|
638
|
+
if (ev.type === EV.ASSISTANT_ATTEMPT) {
|
|
639
|
+
hasAttempt = true;
|
|
640
|
+
lastSource = 'attempt';
|
|
641
|
+
if (ev.data?.usage) usage = ev.data.usage;
|
|
642
|
+
if (ev.data?.interrupted === true) interrupted = true;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const rec = eventUsage(ev);
|
|
646
|
+
if (rec) { usage = rec.usage; lastSource = rec.source; }
|
|
647
|
+
else if (ev.type === EV.ASSISTANT_MESSAGE) lastSource = 'message';
|
|
648
|
+
}
|
|
649
|
+
const cls = classifyTail(list);
|
|
650
|
+
return { open: cls.state === 'open', interrupted, usage, hasAttempt, lastSource };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 此刻是否在流式输出 / 在跑(语义判定)。
|
|
655
|
+
* 口径:结构上回合未闭合(`classifyTail().state === 'open'`)**且**最后一条事件在静默窗口内。
|
|
656
|
+
* - 0.1.1:chunk 增量持续写 → 总是新鲜,等价于旧"回合未闭合"直觉;
|
|
657
|
+
* - 0.1.5:attempt 结算式(流式期间可能无事件)→ 本函数会转 false,**这正是升级后要换的判据**
|
|
658
|
+
* (§2.1.1 空窗期误报),P2 批按主包线给判定档。
|
|
659
|
+
* @param {Array} events
|
|
660
|
+
* @param {number} now 判定时刻(单调差值口径:只用 now - 事件时间)
|
|
661
|
+
*/
|
|
662
|
+
export function isStreamingNow(events, now, opts = {}) {
|
|
663
|
+
const windowMs = Number(opts.windowMs ?? DEFAULT_STREAM_WINDOW_MS);
|
|
664
|
+
const cls = classifyTail(events);
|
|
665
|
+
if (cls.state !== 'open') return false;
|
|
666
|
+
const at = numOrNull(cls.last?.time);
|
|
667
|
+
if (at === null) return true; // 无时间戳 → 只以结构为准
|
|
668
|
+
return (Number(now) - at) <= windowMs;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
672
|
+
// services 组 —— 存在性探测(**不存在返回 null/false,绝不抛错**)
|
|
673
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 服务是否存在。
|
|
677
|
+
* cordis 铁律:读**未 inject** 的服务属性会抛错(`cannot get property X without inject`),
|
|
678
|
+
* 不是返回 undefined ⇒ 这里的任何取法都必须包 try/catch,失败即 false(优雅降级)。
|
|
679
|
+
* @param {{get?:Function}} ctx cordis 上下文(或任何有 get 的对象)
|
|
680
|
+
* @param {string} name 服务名
|
|
681
|
+
*/
|
|
682
|
+
export function has(ctx, name) {
|
|
683
|
+
if (!ctx || !isStr(name)) return false;
|
|
684
|
+
try {
|
|
685
|
+
if (typeof ctx.get === 'function') return Boolean(ctx.get(name));
|
|
686
|
+
return Boolean(ctx[name]);
|
|
687
|
+
} catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 按优先级挑第一个存在的服务名;都不存在 → null(**不抛错**)。
|
|
694
|
+
* @param {{get?:Function}} ctx
|
|
695
|
+
* @param {string[]} names 候选(按优先级)
|
|
696
|
+
*/
|
|
697
|
+
export function pick(ctx, names) {
|
|
698
|
+
const list = Array.isArray(names) ? names : [];
|
|
699
|
+
for (const n of list) if (has(ctx, n)) return n;
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* 动态获取服务(拿不到返回 null)。
|
|
705
|
+
* ⚠️ 调用方**不得**把服务名留在静态 `inject` 声明里 —— 声明包含不存在的服务会让 entry
|
|
706
|
+
* 永久 pending(= boot 失败)。正确姿势:用 `ctx.inject([...], cb)` 动态探测。
|
|
707
|
+
*/
|
|
708
|
+
export function service(ctx, name) {
|
|
709
|
+
if (!ctx || !isStr(name)) return null;
|
|
710
|
+
try {
|
|
711
|
+
if (typeof ctx.get === 'function') return ctx.get(name) ?? null;
|
|
712
|
+
const v = ctx[name];
|
|
713
|
+
return v ?? null;
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── P2:服务面收敛(settings 首装面双线分支 · 客户端 inject 声明解析) ──────────────
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* settings 首装面("给 settings 注册一个 namespace")**两代 API**——本函数只**判事实**:
|
|
723
|
+
* 运行中的宿主到底提供哪一代承载体。
|
|
724
|
+
*
|
|
725
|
+
* 取证(2026-09-15,两线主包实读):
|
|
726
|
+
* - **0.1.1-rc.2**:`@deepseek-ai/dsh-settings` 包级导出 `installSettingsSection(ctx, ns, schema, entry, hooks)`
|
|
727
|
+
* 与 `settingsNamespace(value)`;`SettingsProvider`(`settings` 服务)类上**只有 `register(ns, schema, options)`**,
|
|
728
|
+
* **没有 `installSection`**(实读方法清单确认:register/describe/get/write/section/resolve… 无 installSection)。
|
|
729
|
+
* - **0.1.5-rc.2**:两个包级导出**被移除**(实读导出面 = `SettingsConflictError` / `SettingsProvider` / `redactSecrets`),
|
|
730
|
+
* 改为 `settings` 服务上的实例方法 `installSection(owner, ns, schema, entry, hooks)`。
|
|
731
|
+
* ⇒ 判据 = `installSection` 方法的有无(**新线专有**),不是包级导出的有无
|
|
732
|
+
* (调用方拿不到包级导入面,且静态 named import 会直接让 loader entry 加载失败——这是 P2 最贵的坑)。
|
|
733
|
+
*
|
|
734
|
+
* @param {object} ctx 任意 cordis 上下文(或任何有 get 的对象)
|
|
735
|
+
* @returns {{api:'installSection'|'register'|'legacy-package'|'none', via:'ctx.settings.installSection'|'ctx.settings.register'|'@deepseek-ai/dsh-settings'|null, requires:string, degraded:boolean}}
|
|
736
|
+
* · `api='installSection'` → 用 `installSectionCompat()` 走服务方法(**0.1.2+ 首选**)
|
|
737
|
+
* · `api='register'` → 服务在但无 installSection ⇒ 适配层用 `register()` 自行组装(fallback)
|
|
738
|
+
* · `api='legacy-package'`→ 服务探测不到 ⇒ 需包级 `installSettingsSection`(**由 caller 以动态 import 提供**,
|
|
739
|
+
* 适配层不 import 主包符号,见硬约束 1)
|
|
740
|
+
* · `api='none'` → 完全拿不到 ⇒ **降级路径**:调用方保持"只读组合 entry",绝不抛错
|
|
741
|
+
*/
|
|
742
|
+
export function settingsApiOf(ctx) {
|
|
743
|
+
const svc = service(ctx, SVC.settings);
|
|
744
|
+
const hasInstallSection = Boolean(svc && typeof svc.installSection === 'function');
|
|
745
|
+
const hasRegister = Boolean(svc && typeof svc.register === 'function');
|
|
746
|
+
if (hasInstallSection) {
|
|
747
|
+
return { api: 'installSection', via: 'ctx.settings.installSection', requires: SVC.settings, degraded: false };
|
|
748
|
+
}
|
|
749
|
+
if (hasRegister) {
|
|
750
|
+
return { api: 'register', via: 'ctx.settings.register', requires: SVC.settings, degraded: true };
|
|
751
|
+
}
|
|
752
|
+
return { api: 'none', via: null, requires: SVC.settings, degraded: true };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* 在任意一条线上装载"settings 段落"(**承载体选择 + 降级**;本函数不 import 主包符号)。
|
|
757
|
+
*
|
|
758
|
+
* 行为:
|
|
759
|
+
* · 新线(`settings.installSection` 在位)→ 原样调用,等价于 0.1.1 的包级 helper
|
|
760
|
+
* (官方实现即"包级 helper 减去它自己的 `ctx.inject(['settings'])` 包装",已实读两线源码对照);
|
|
761
|
+
* · 中间线(只有 `register`)→ 适配层**自行组装**同一套 hooks 语义
|
|
762
|
+
* (`setSource` 绑到 scope.get()、卸载时回落到入口、`onChange` 初唤 + 变更时唤);
|
|
763
|
+
* · 都不可得 → 返回 `{ok:false}`(**降级路径**),调用方保持"只读组合 entry",**不抛错**。
|
|
764
|
+
*
|
|
765
|
+
* @param {object} ctx cordis 上下文(owner:注销时用它判 isUnloading)
|
|
766
|
+
* @param {{ns:string, schema:any, entry:any, hooks:{setSource:Function,onChange:Function}, isUnloading?:Function}} opts
|
|
767
|
+
* @returns {{ok:boolean, api:string, scope:object|null, reason:string}}
|
|
768
|
+
*/
|
|
769
|
+
export function installSectionCompat(ctx, opts = {}) {
|
|
770
|
+
const probe = settingsApiOf(ctx);
|
|
771
|
+
const svc = service(ctx, SVC.settings);
|
|
772
|
+
const { ns, schema, entry, hooks } = opts;
|
|
773
|
+
if (!ns || !hooks || typeof hooks.setSource !== 'function' || typeof hooks.onChange !== 'function') {
|
|
774
|
+
return { ok: false, api: probe.api, scope: null, reason: 'invalid-arguments' };
|
|
775
|
+
}
|
|
776
|
+
if (probe.api === 'installSection') {
|
|
777
|
+
try {
|
|
778
|
+
svc.installSection(ctx, ns, schema, entry, hooks);
|
|
779
|
+
return { ok: true, api: 'installSection', scope: null, reason: 'host-installSection' };
|
|
780
|
+
} catch (e) {
|
|
781
|
+
return { ok: false, api: 'installSection', scope: null, reason: `installSection-threw:${String(e?.message ?? e)}` };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (probe.api === 'register') {
|
|
785
|
+
// 适配层自行组装(与两线 installSection 的 hooks 语义逐条一致)
|
|
786
|
+
try {
|
|
787
|
+
const scope = svc.register(ns, schema, { base: entry, ...(hooks.validate === undefined ? {} : { validate: hooks.validate }) });
|
|
788
|
+
hooks.setSource(() => scope.get());
|
|
789
|
+
try { ctx.effect(() => () => { if (typeof opts.isUnloading === 'function' && opts.isUnloading(ctx)) return; hooks.setSource(() => entry); hooks.onChange(); }); } catch { /* effect 不可用 → 不注册回落副作用 */ }
|
|
790
|
+
hooks.onChange();
|
|
791
|
+
if (typeof scope.watch === 'function') {
|
|
792
|
+
scope.watch(() => { if (typeof opts.isUnloading === 'function' && opts.isUnloading(ctx)) return; hooks.onChange(); });
|
|
793
|
+
}
|
|
794
|
+
return { ok: true, api: 'register', scope, reason: 'adapter-assembled-register' };
|
|
795
|
+
} catch (e) {
|
|
796
|
+
return { ok: false, api: 'register', scope: null, reason: `register-threw:${String(e?.message ?? e)}` };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// 降级:拿不到任何承载体 —— 调用方保持只读组合 entry
|
|
800
|
+
return { ok: false, api: 'none', scope: null, reason: 'settings-service-absent' };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* 主包线**自带**的包级符号(供生态面板 / 升级体检对照;**只声明事实,不在本文件 import**)。
|
|
805
|
+
* 取值来源:0.1.5-rc.2 实读——`@deepseek-ai/dsh-settings` 的导出面 = `SettingsConflictError` /
|
|
806
|
+
* `SettingsProvider` / `redactSecrets`(`installSettingsSection`、`settingsNamespace` 已移除)。
|
|
807
|
+
*/
|
|
808
|
+
export const SETTINGS_PKG_EXPORTS = Object.freeze({
|
|
809
|
+
'0.1.1': Object.freeze(['SettingsConflictError', 'SettingsProvider', 'deepEqualJson', 'installSettingsSection', 'redactSecrets', 'settingsNamespace']),
|
|
810
|
+
'0.1.5': Object.freeze(['SettingsConflictError', 'SettingsProvider', 'redactSecrets']),
|
|
811
|
+
});
|
|
812
|
+
/** 0.1.2+ 移除的 settings 包级导出(静态 named import 会让 loader entry 直接加载失败)。 */
|
|
813
|
+
export const SETTINGS_PKG_REMOVED_SINCE_012 = Object.freeze(['installSettingsSection', 'settingsNamespace']);
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 客户端插件包名(`dsh.client.inject` 里声明、由宿主在浏览器侧组合;P1 取证 15 处客户端字面量的收敛面相)。
|
|
817
|
+
* ⚠️ 客户端包**不存在 → 静默跳过**(界面在、功能没了,方案 §3.4 第二行)——本表只为 caller 提供
|
|
818
|
+
* "该声明是否落在已知客户端包面"这一事实,**是否告警、告警给谁,属 caller 策略**。
|
|
819
|
+
*/
|
|
820
|
+
export const CLIENT_PKG = Object.freeze({
|
|
821
|
+
runtime: 'dsh-client-runtime',
|
|
822
|
+
locale: 'dsh-client-locale',
|
|
823
|
+
modules: 'dsh-client-modules',
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* 解析一段客户端 `inject` 声明(字符串或数组)→ 结构事实。
|
|
828
|
+
* @param {string[]|string} decl
|
|
829
|
+
* @param {(name:string)=>boolean} [pkgPresent] 可选探针:包在当前闭包内是否存在(由 caller 注入)
|
|
830
|
+
* @returns {{services:string[], unknownClientPackages:string[], skipped:boolean}}
|
|
831
|
+
* · `skipped=true` ⇔ 声明里存在**探针判定为不存在**的客户端包 ⇒ 该客户端模块会被宿主静默跳过,
|
|
832
|
+
* 调用方**应当**把它变成一条显式告警(方案 §3.4:不静默)。
|
|
833
|
+
*/
|
|
834
|
+
export function clientInjectDecl(decl, pkgPresent) {
|
|
835
|
+
const list = Array.isArray(decl) ? decl : (isStr(decl) ? [decl] : []);
|
|
836
|
+
const services = [];
|
|
837
|
+
const unknownClientPackages = [];
|
|
838
|
+
for (const n of list) {
|
|
839
|
+
if (!isStr(n)) continue;
|
|
840
|
+
services.push(n);
|
|
841
|
+
if (typeof pkgPresent !== 'function') continue;
|
|
842
|
+
if (n.startsWith('dsh-client-') || Object.values(CLIENT_PKG).includes(n)) {
|
|
843
|
+
let present = false;
|
|
844
|
+
try { present = pkgPresent(n) === true; } catch { present = false; }
|
|
845
|
+
if (!present) unknownClientPackages.push(n);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { services, unknownClientPackages, skipped: unknownClientPackages.length > 0 };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* 服务面事实快照(**只输出事实,不含策略**)。
|
|
853
|
+
* 调用方(P2 / P5 / 面板)据此自行决定"这条线上该用什么判定"。
|
|
854
|
+
*
|
|
855
|
+
* @param {object} ctx cordis 上下文
|
|
856
|
+
* @param {string[]} [candidates] 关心的服务名(缺省 = 关注清单 + SVC_GONE_ON_015)
|
|
857
|
+
* @returns {{present:object, missing:string[], goneOn015:string[], settingsApi:string, settingsDegraded:boolean}}
|
|
858
|
+
*/
|
|
859
|
+
export function serviceFace(ctx, candidates) {
|
|
860
|
+
const watch = Array.isArray(candidates) && candidates.length
|
|
861
|
+
? candidates
|
|
862
|
+
: [SVC.webServer, SVC.settings, SVC.settingsScope, SVC.sessions, SVC.agents, SVC.llm, SVC.sessionTitle,
|
|
863
|
+
SVC.apiProxy, SVC.sessionController, SVC.settingsController, SVC.workspaceController, SVC.locale, SVC.slots]
|
|
864
|
+
.concat(SVC_GONE_ON_015);
|
|
865
|
+
const present = {};
|
|
866
|
+
const missing = [];
|
|
867
|
+
for (const n of watch) {
|
|
868
|
+
const ok = has(ctx, n);
|
|
869
|
+
present[n] = ok;
|
|
870
|
+
if (!ok) missing.push(n);
|
|
871
|
+
}
|
|
872
|
+
const api = settingsApiOf(ctx);
|
|
873
|
+
return {
|
|
874
|
+
present,
|
|
875
|
+
missing,
|
|
876
|
+
// 注意:**独立探测**(不依赖 candidates 是否含这些名字)
|
|
877
|
+
goneOn015: SVC_GONE_ON_015.filter((n) => !has(ctx, n)),
|
|
878
|
+
settingsApi: api.api,
|
|
879
|
+
settingsDegraded: api.degraded,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
884
|
+
// version 组 —— 能力档(读不到版本 → 'unknown',**永不抛错**)
|
|
885
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
886
|
+
|
|
887
|
+
/** 已知主包线(0.1.1 = 现网生产;0.1.2 = 拆包线;0.1.5 = 当前官方线)。 */
|
|
888
|
+
export const HOST_LINES = Object.freeze(['0.1.1', '0.1.2', '0.1.5']);
|
|
889
|
+
|
|
890
|
+
/** 能力 → 支持它的主包线。 */
|
|
891
|
+
const CAPABILITY_LINES = Object.freeze({
|
|
892
|
+
'assistant-chunk-stream': new Set(['0.1.1', '0.1.2']), // 增量 chunk 流
|
|
893
|
+
'assistant-attempt-settlement': new Set(['0.1.5']), // 结算式 attempt(含 usage/interrupted)
|
|
894
|
+
'tool-code-dispatch': new Set(['0.1.1', '0.1.2']),
|
|
895
|
+
'tool-ptc-dispatch': new Set(['0.1.5']),
|
|
896
|
+
'session-end-seed': new Set(['0.1.1', '0.1.2', '0.1.5']),
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* P2 能力档扩展面(**只登记"这条线上有什么"的事实**:承载体 / 失效症状 / 降级出口)。
|
|
901
|
+
* ⚠️ 本表**不含策略** —— 不写"该怎么办",只写"是什么、会怎样坏、哪个出口可用";
|
|
902
|
+
* `fallback` 给的是**出口名**(调用方自行决定用不用),不是自动行为。
|
|
903
|
+
* 取证来源:`docs\长期库大扫除-20260914\adapter-认知缺口取证-20260915.md` +
|
|
904
|
+
* 本批 2026-09-15 两线主包闭包实读(服务名扫描 / settings 导出面 / known-event-types diff)。
|
|
905
|
+
*/
|
|
906
|
+
const CAPABILITY_DETAIL = Object.freeze({
|
|
907
|
+
'assistant-chunk-stream': Object.freeze({
|
|
908
|
+
carrier: 'assistant/chunk(增量 chunk,逐 chunk 落盘)', fallback: 'lastProduction()/eventUsage() 的 chunk 分支',
|
|
909
|
+
}),
|
|
910
|
+
'assistant-attempt-settlement': Object.freeze({
|
|
911
|
+
carrier: 'assistant/attempt(结算式整段 stream[] + usage + interrupted)',
|
|
912
|
+
symptom: '流式期间无事件落盘(长 attempt 静默)⇒ 以"回合未闭合 + 长时间无新事件"为判据的检测会误报',
|
|
913
|
+
fallback: 'isStreamingNow() 的 attempt 分支(结构配对口径,禁 mtime/墙钟)',
|
|
914
|
+
}),
|
|
915
|
+
'tool-code-dispatch': Object.freeze({ carrier: 'tool/code-dispatch*(字段含 subCallId=deterministic)' }),
|
|
916
|
+
'tool-ptc-dispatch': Object.freeze({ carrier: 'tool/ptc-dispatch*(0.1.5 改名,语义同构已取证)' }),
|
|
917
|
+
'session-end-seed': Object.freeze({ carrier: 'session/end-seed(重启/停机补写的收尾标记)' }),
|
|
918
|
+
'settings-install-section-method': Object.freeze({
|
|
919
|
+
lines: new Set(['0.1.2', '0.1.5']),
|
|
920
|
+
carrier: 'ctx.settings.installSection(owner, ns, schema, entry, hooks)(0.1.2+ 引入)',
|
|
921
|
+
fallback: 'settingsApiOf() → register 组装 / caller 动态 import 包级 helper',
|
|
922
|
+
}),
|
|
923
|
+
'settings-pkg-installSettingsSection': Object.freeze({
|
|
924
|
+
lines: new Set(['0.1.1']),
|
|
925
|
+
carrier: '@deepseek-ai/dsh-settings 的包级 installSettingsSection(**0.1.2+ 已移除**)',
|
|
926
|
+
symptom: '静态 named import 会让 loader entry 直接加载失败(整树不激活、不到 HTTP 200)',
|
|
927
|
+
fallback: '改走 ctx.inject([SVC.settings]) + settings.installSection / register',
|
|
928
|
+
}),
|
|
929
|
+
'api-proxy-service': Object.freeze({
|
|
930
|
+
lines: new Set(['0.1.1', '0.1.2']),
|
|
931
|
+
carrier: 'ctx.apiProxy',
|
|
932
|
+
symptom: '0.1.5 该服务整体移除 ⇒ 留在静态 inject 声明里会让 entry 永久 pending(boot 失败)',
|
|
933
|
+
fallback: 'pick(ctx, [SVC.apiProxy]) 动态探测 + 降级路径',
|
|
934
|
+
}),
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
/** 能力 → 主包线(含 P2 扩展面)。 */
|
|
938
|
+
function linesOf(cap) {
|
|
939
|
+
const d = CAPABILITY_DETAIL[cap];
|
|
940
|
+
if (d?.lines) return d.lines;
|
|
941
|
+
return CAPABILITY_LINES[cap] ?? null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** 由事件形态推断主包线(版本字符串不可得时的兜底)。 */
|
|
945
|
+
export function detectLineFromEvents(events) {
|
|
946
|
+
const list = Array.isArray(events) ? events : [];
|
|
947
|
+
let sawNew = false;
|
|
948
|
+
let sawOld = false;
|
|
949
|
+
for (const ev of list) {
|
|
950
|
+
const t = ev?.type;
|
|
951
|
+
if (!isStr(t)) continue;
|
|
952
|
+
if (t === EV.ASSISTANT_ATTEMPT || t === EV.TOOL_PTC_DISPATCH || t === EV.TOOL_PTC_DISPATCH_START) sawNew = true;
|
|
953
|
+
else if (t === EV.LEGACY_ASSISTANT_CHUNK || t === EV.LEGACY_TOOL_CODE_DISPATCH || t === EV.LEGACY_TOOL_CODE_DISPATCH_START) sawOld = true;
|
|
954
|
+
}
|
|
955
|
+
if (sawNew && !sawOld) return '0.1.5';
|
|
956
|
+
if (sawOld && !sawNew) return '0.1.1';
|
|
957
|
+
return 'unknown';
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* 主包线:'0.1.1' | '0.1.2' | '0.1.5' | 'unknown'(永不抛错)。
|
|
962
|
+
* @param {string|{version?:string,events?:Array}|undefined} input 版本串 / 含版本的对象 / 事件数组
|
|
963
|
+
*/
|
|
964
|
+
export function hostLine(input) {
|
|
965
|
+
try {
|
|
966
|
+
if (typeof input === 'string') {
|
|
967
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(input.trim());
|
|
968
|
+
if (!m) return 'unknown';
|
|
969
|
+
// 主包行号用**三段**(0.1.1 / 0.1.2 / 0.1.5 —— 差别在 patch 位,不能只取 major.minor)
|
|
970
|
+
const short = `${m[1]}.${m[2]}.${m[3]}`;
|
|
971
|
+
return HOST_LINES.includes(short) ? short : 'unknown';
|
|
972
|
+
}
|
|
973
|
+
if (input && typeof input === 'object') {
|
|
974
|
+
if (typeof input.version === 'string') return hostLine(input.version);
|
|
975
|
+
if (Array.isArray(input.events)) return detectLineFromEvents(input.events);
|
|
976
|
+
}
|
|
977
|
+
return 'unknown';
|
|
978
|
+
} catch {
|
|
979
|
+
return 'unknown';
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* 该主包线是否支持某能力。
|
|
985
|
+
* @param {string} capability 见 CAPABILITY_LINES
|
|
986
|
+
* @param {string|object} [line] 缺省 = hostLine()(读不到即 'unknown' → false,**不会抛错**)
|
|
987
|
+
*/
|
|
988
|
+
export function supports(capability, line = 'unknown') {
|
|
989
|
+
const lines = linesOf(capability);
|
|
990
|
+
if (!lines) return false;
|
|
991
|
+
const l = typeof line === 'string' ? line : hostLine(line);
|
|
992
|
+
return lines.has(l);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* P2 · 按主包线派生**能力档**:`{capability: boolean}`(**只给事实**)。
|
|
997
|
+
* 供调用方(P2 / P5 / 面板)自行判断"这条线上该用什么判定"——本函数**不返回任何策略**。
|
|
998
|
+
* @param {string|{version?:string,events?:Array}|undefined} line 主包线(或版本串 / 含 events 的对象)
|
|
999
|
+
*/
|
|
1000
|
+
export function hostCapabilities(line = 'unknown') {
|
|
1001
|
+
const l = typeof line === 'string' ? line : hostLine(line);
|
|
1002
|
+
const out = {};
|
|
1003
|
+
for (const cap of Object.keys(CAPABILITY_LINES)) out[cap] = linesOf(cap).has(l);
|
|
1004
|
+
for (const cap of Object.keys(CAPABILITY_DETAIL)) {
|
|
1005
|
+
if (cap in out) continue;
|
|
1006
|
+
out[cap] = linesOf(cap).has(l);
|
|
1007
|
+
}
|
|
1008
|
+
return out;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** 能力档的**静态说明**(承载体 / 症状 / 出口名——仍是事实层,不含"该怎么办")。 */
|
|
1012
|
+
export function capabilityDetail(capability) {
|
|
1013
|
+
return CAPABILITY_DETAIL[capability] ?? null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* P2 · **能力剖面**(版本 + 能力档 + 可选运行时服务面事实)。
|
|
1018
|
+
*
|
|
1019
|
+
* 与 `hostLine()` 的关系:`hostLine()` 保持原样(只回一个线名),本函数是**新增的第二入口**,
|
|
1020
|
+
* 不改变任何既有调用方的行为。
|
|
1021
|
+
*
|
|
1022
|
+
* ⚠️ **只提供事实**:本函数**不**回答"该不该报""算不算活跃"——
|
|
1023
|
+
* 面板"被打断会话"(宁少报不误报)与 P2"回合未闭合"(宁多报不放过)语义不同,
|
|
1024
|
+
* 策略仍归 caller(方案 §3.2 判定职责分层)。
|
|
1025
|
+
*
|
|
1026
|
+
* @param {{version?:string, events?:Array, ctx?:object, services?:string[]}|string|undefined} input
|
|
1027
|
+
* @returns {{line:string, capabilities:object, serviceFace:object|null}}
|
|
1028
|
+
*/
|
|
1029
|
+
export function capabilityProfile(input) {
|
|
1030
|
+
let line = 'unknown';
|
|
1031
|
+
let face = null;
|
|
1032
|
+
try {
|
|
1033
|
+
if (typeof input === 'string') line = hostLine(input);
|
|
1034
|
+
else if (input && typeof input === 'object') {
|
|
1035
|
+
if (typeof input.version === 'string') line = hostLine(input.version);
|
|
1036
|
+
else if (Array.isArray(input.events)) line = detectLineFromEvents(input.events);
|
|
1037
|
+
if (input.ctx) face = serviceFace(input.ctx, input.services);
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
line = 'unknown';
|
|
1041
|
+
}
|
|
1042
|
+
return { line, capabilities: hostCapabilities(line), serviceFace: face };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* 能力清单(文档 / 面板 / 登记表自动化生成用)。
|
|
1047
|
+
* ⚠️ **向后兼容**:无参调用仍返回原 5 能力 × 3 线 = 15 行(既有消费方 / 单测不变);
|
|
1048
|
+
* P2 扩展面(含"承载体 / 症状 / 出口"那 3 条)需显式 `{extended:true}` 才会并入 → 8 × 3 = 24 行。
|
|
1049
|
+
* @param {{extended?:boolean}} [opts]
|
|
1050
|
+
*/
|
|
1051
|
+
export function capabilityMatrix(opts = {}) {
|
|
1052
|
+
const out = [];
|
|
1053
|
+
const caps = opts.extended === true
|
|
1054
|
+
? [...new Set([...Object.keys(CAPABILITY_LINES), ...Object.keys(CAPABILITY_DETAIL)])]
|
|
1055
|
+
: Object.keys(CAPABILITY_LINES);
|
|
1056
|
+
for (const cap of caps) {
|
|
1057
|
+
for (const l of HOST_LINES) out.push({ capability: cap, line: l, supported: linesOf(cap).has(l) });
|
|
1058
|
+
}
|
|
1059
|
+
return out;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1063
|
+
// 自检(零依赖;`node tools/lib/host-adapter.mjs --self-test`)
|
|
1064
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1065
|
+
|
|
1066
|
+
/** 极简自检:族判定 / 边界态 / 双线分支。返回 {pass, fail, failures}。 */
|
|
1067
|
+
export function selfTest() {
|
|
1068
|
+
const failures = [];
|
|
1069
|
+
const eq = (name, got, want) => {
|
|
1070
|
+
const a = JSON.stringify(got);
|
|
1071
|
+
const b = JSON.stringify(want);
|
|
1072
|
+
if (a !== b) failures.push(`${name}: got ${a} want ${b}`);
|
|
1073
|
+
};
|
|
1074
|
+
eq('isTurnInterior(assistant/chunk)', isTurnInterior(EV.LEGACY_ASSISTANT_CHUNK), true);
|
|
1075
|
+
eq('isTurnInterior(assistant/attempt)', isTurnInterior(EV.ASSISTANT_ATTEMPT), true);
|
|
1076
|
+
eq('isTurnInterior(tool/ptc-dispatch)', isTurnInterior(EV.TOOL_PTC_DISPATCH), true);
|
|
1077
|
+
eq('isTurnInterior(session/title)', isTurnInterior(EV.SESSION_TITLE), false);
|
|
1078
|
+
eq('isTurnInterior(agent-preset/selected)', isTurnInterior('agent-preset/selected'), false);
|
|
1079
|
+
eq('isTurnInterior(tool-workflow/agent-start)', isTurnInterior('tool-workflow/agent-start'), true);
|
|
1080
|
+
eq('classifyTail(empty)', classifyTail([]).state, 'empty');
|
|
1081
|
+
eq('classifyTail(open)', classifyTail([{ type: EV.TURN_START }]).state, 'open');
|
|
1082
|
+
eq('classifyTail(closed)', classifyTail([{ type: EV.TURN_END, data: { reason: { kind: 'completed' } } }]).state, 'closed');
|
|
1083
|
+
eq('classifyTail(interrupted)', classifyTail([{ type: EV.TURN_END, data: { reason: { kind: 'aborted' } } }]).state, 'interrupted');
|
|
1084
|
+
// ★ 2026-10-10 空会话误判治本:整段零回合内事件(只有会话级种子事件)⇒ closed
|
|
1085
|
+
eq('classifyTail(纯会话级种子尾巴⇒closed)', classifyTail([{ type: EV.PERMISSION_PRESET }, { type: EV.SANDBOX_MODE }, { type: 'approval/policy' }]).state, 'closed');
|
|
1086
|
+
eq('classifyTail(有回合内事件+未知族尾⇒open)', classifyTail([{ type: EV.TOOL_RESULT }, { type: 'some/new-family' }]).state, 'open');
|
|
1087
|
+
eq('eventUsage(chunk)', eventUsage({ type: EV.LEGACY_ASSISTANT_CHUNK, time: 1, data: { chunk: { type: 'usage', usage: { inputTokens: 5 } } } })?.usage?.inputTokens, 5);
|
|
1088
|
+
eq('eventUsage(attempt)', eventUsage({ type: EV.ASSISTANT_ATTEMPT, time: 1, data: { usage: { inputTokens: 7 } } })?.usage?.inputTokens, 7);
|
|
1089
|
+
eq('eventUsage(none)', eventUsage({ type: EV.TOOL_CALL }), null);
|
|
1090
|
+
eq('eventUsage(message)', eventUsage({ type: EV.ASSISTANT_MESSAGE, time: 1, data: { usage: { inputTokens: 9 } } })?.usage?.inputTokens, 9);
|
|
1091
|
+
eq('eventUsage(message.message.usage)', eventUsage({ type: EV.ASSISTANT_MESSAGE, time: 1, data: { message: { usage: { inputTokens: 11 } } } })?.usage?.inputTokens, 11);
|
|
1092
|
+
eq('eventUsage(message, opts.message=false)', eventUsage({ type: EV.ASSISTANT_MESSAGE, time: 1, data: { usage: { inputTokens: 9 } } }, { message: false }), null);
|
|
1093
|
+
eq('eventUsage(message 无 usage)', eventUsage({ type: EV.ASSISTANT_MESSAGE, time: 1, data: {} }), null);
|
|
1094
|
+
eq('hostLine(0.1.5-rc.2)', hostLine('0.1.5-rc.2'), '0.1.5');
|
|
1095
|
+
eq('hostLine(garbage)', hostLine('nope'), 'unknown');
|
|
1096
|
+
eq('supports(attempt,0.1.5)', supports('assistant-attempt-settlement', '0.1.5'), true);
|
|
1097
|
+
eq('supports(attempt,0.1.1)', supports('assistant-attempt-settlement', '0.1.1'), false);
|
|
1098
|
+
eq('has(null ctx)', has(null, 'apiProxy'), false);
|
|
1099
|
+
// —— P1:服务名常量表 / 新增事件常量 / 人话标签
|
|
1100
|
+
eq('SVC.webServer', SVC.webServer, 'webServer');
|
|
1101
|
+
eq('SVC 键=值自洽', Object.entries(SVC).filter(([k, v]) => k !== v).map(([k]) => k), []);
|
|
1102
|
+
eq('SVC frozen', Object.isFrozen(SVC), true);
|
|
1103
|
+
eq('EV.SESSION_EVENT', EV.SESSION_EVENT, 'session/event');
|
|
1104
|
+
eq('EV.TOOLS_PRE_EXECUTE', EV.TOOLS_PRE_EXECUTE, 'tools/pre-execute');
|
|
1105
|
+
eq('eventLabel(user/message)', eventLabel(EV.USER_MESSAGE), '主人消息');
|
|
1106
|
+
eq('eventLabel 未知回落事件名', eventLabel('weird/thing'), 'weird/thing');
|
|
1107
|
+
eq('isTurnInterior(fs/write-intent)=false', isTurnInterior(EV.FS_WRITE_INTENT), false);
|
|
1108
|
+
// —— P2:服务面探测 / 降级 / 能力档(**只验事实与降级路径,不涉策略**)
|
|
1109
|
+
const fakeNew = { get: (n) => (n === SVC.settings ? { installSection() {} } : { stub: n }) };
|
|
1110
|
+
const fakeMid = { get: (n) => (n === SVC.settings ? { register() {} } : { stub: n }) };
|
|
1111
|
+
const fakeNone = { get: () => null };
|
|
1112
|
+
const fakeThrow = { get: () => { throw new Error('cannot get property "settings" without inject'); } };
|
|
1113
|
+
eq('settingsApiOf(new) 判 installSection', settingsApiOf(fakeNew).api, 'installSection');
|
|
1114
|
+
eq('settingsApiOf(mid) 判 register', settingsApiOf(fakeMid).api, 'register');
|
|
1115
|
+
eq('settingsApiOf(none) 判 none', settingsApiOf(fakeNone).api, 'none');
|
|
1116
|
+
eq('settingsApiOf(抛错 ctx) 不抛且判 none', settingsApiOf(fakeThrow).api, 'none');
|
|
1117
|
+
eq('settingsApiOf(null) 不抛且判 none', settingsApiOf(null).api, 'none');
|
|
1118
|
+
eq('settingsApiOf(new).degraded=false', settingsApiOf(fakeNew).degraded, false);
|
|
1119
|
+
eq('settingsApiOf(mid).degraded=true', settingsApiOf(fakeMid).degraded, true);
|
|
1120
|
+
eq('installSectionCompat(新线) ok', installSectionCompat(fakeNew, { ns: 'x', schema: null, entry: {}, hooks: { setSource() {}, onChange() {} } }).api, 'installSection');
|
|
1121
|
+
eq('installSectionCompat(无线) 降级不抛', installSectionCompat(fakeNone, { ns: 'x', schema: null, entry: {}, hooks: { setSource() {}, onChange() {} } }).ok, false);
|
|
1122
|
+
eq('installSectionCompat(无线) reason', installSectionCompat(fakeNone, { ns: 'x', schema: null, entry: {}, hooks: { setSource() {}, onChange() {} } }).reason, 'settings-service-absent');
|
|
1123
|
+
eq('installSectionCompat(参数非法) 降级', installSectionCompat(fakeNew, {}).reason, 'invalid-arguments');
|
|
1124
|
+
eq('serviceFace(goneOn015) 含 apiProxy', serviceFace(fakeNone).goneOn015.includes(SVC.apiProxy), true);
|
|
1125
|
+
eq('serviceFace(全在) apiProxy 不缺', serviceFace(fakeNew).goneOn015.includes(SVC.apiProxy), false);
|
|
1126
|
+
eq('serviceFace(missing) 列出 settings', serviceFace(fakeNone, [SVC.settings]).missing, [SVC.settings]);
|
|
1127
|
+
eq('SVC_GONE_ON_015 冻结', Object.isFrozen(SVC_GONE_ON_015), true);
|
|
1128
|
+
eq('SETTINGS_PKG_REMOVED_SINCE_012', SETTINGS_PKG_REMOVED_SINCE_012, ['installSettingsSection', 'settingsNamespace']);
|
|
1129
|
+
eq('SETTINGS_PKG_EXPORTS.0.1.5 无 installSettingsSection', SETTINGS_PKG_EXPORTS['0.1.5'].includes('installSettingsSection'), false);
|
|
1130
|
+
eq('clientInjectDecl(无探针) skipped=false', clientInjectDecl(['slots', 'locale']).skipped, false);
|
|
1131
|
+
eq('clientInjectDecl(包缺) skipped=true', clientInjectDecl(['slots', 'dsh-client-runtime'], () => false).skipped, true);
|
|
1132
|
+
eq('clientInjectDecl(包在) skipped=false', clientInjectDecl(['dsh-client-locale'], () => true).skipped, false);
|
|
1133
|
+
eq('clientInjectDecl(抛错探针) 不抛', clientInjectDecl(['dsh-client-locale'], () => { throw new Error('x'); }).unknownClientPackages, ['dsh-client-locale']);
|
|
1134
|
+
eq('hostCapabilities(0.1.5).attempt', hostCapabilities('0.1.5')['assistant-attempt-settlement'], true);
|
|
1135
|
+
eq('hostCapabilities(0.1.1).attempt', hostCapabilities('0.1.1')['assistant-attempt-settlement'], false);
|
|
1136
|
+
eq('hostCapabilities(0.1.5).api-proxy=false', hostCapabilities('0.1.5')['api-proxy-service'], false);
|
|
1137
|
+
eq('hostCapabilities(0.1.1).api-proxy=true', hostCapabilities('0.1.1')['api-proxy-service'], true);
|
|
1138
|
+
eq('hostCapabilities(0.1.5).settingsSeam=true', hostCapabilities('0.1.5')['settings-install-section-method'], true);
|
|
1139
|
+
eq('hostCapabilities(0.1.1).settingsSeam=false', hostCapabilities('0.1.1')['settings-install-section-method'], false);
|
|
1140
|
+
eq('hostCapabilities(0.1.2).settingsSeam=true', hostCapabilities('0.1.2')['settings-install-section-method'], true);
|
|
1141
|
+
eq('hostCapabilities(0.1.1).pkgHelper=true', hostCapabilities('0.1.1')['settings-pkg-installSettingsSection'], true);
|
|
1142
|
+
eq('hostCapabilities(0.1.2).pkgHelper=false', hostCapabilities('0.1.2')['settings-pkg-installSettingsSection'], false);
|
|
1143
|
+
eq('hostCapabilities(unknown) 全 false', Object.values(hostCapabilities('unknown')).some(Boolean), false);
|
|
1144
|
+
eq('capabilityDetail 含 symptom', Boolean(capabilityDetail('api-proxy-service').symptom), true);
|
|
1145
|
+
eq('capabilityDetail(未知)=null', capabilityDetail('nope'), null);
|
|
1146
|
+
eq('capabilityProfile(str)', capabilityProfile('0.1.5-rc.2').line, '0.1.5');
|
|
1147
|
+
eq('capabilityProfile(events)', capabilityProfile({ events: [{ type: EV.ASSISTANT_ATTEMPT }] }).line, '0.1.5');
|
|
1148
|
+
eq('capabilityProfile(ctx) 带服务面', capabilityProfile({ version: '0.1.1-rc.2', ctx: fakeNone }).serviceFace.missing.includes(SVC.settings), true);
|
|
1149
|
+
eq('capabilityProfile(null) 不抛', capabilityProfile(null).line, 'unknown');
|
|
1150
|
+
eq('capabilityMatrix 基线 15 行(向后兼容)', capabilityMatrix().length, 15);
|
|
1151
|
+
eq('capabilityMatrix(extended) 覆盖 P2 新能力', capabilityMatrix({ extended: true }).filter((r) => r.capability === 'api-proxy-service').length, 3);
|
|
1152
|
+
eq('EV.MODEL_SELECTION', EV.MODEL_SELECTION, 'model/selection');
|
|
1153
|
+
eq('EV.SUBAGENT_CATALOG', EV.SUBAGENT_CATALOG, 'subagent/catalog');
|
|
1154
|
+
return { pass: failures.length === 0, checks: 67, failures };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const invokedDirectly = (() => {
|
|
1158
|
+
try {
|
|
1159
|
+
const argv1 = process.argv?.[1] ?? '';
|
|
1160
|
+
return typeof argv1 === 'string' && argv1.replace(/\\/g, '/').endsWith('tools/lib/host-adapter.mjs');
|
|
1161
|
+
} catch {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
})();
|
|
1165
|
+
|
|
1166
|
+
if (invokedDirectly && process.argv.includes('--self-test')) {
|
|
1167
|
+
const r = selfTest();
|
|
1168
|
+
for (const f of r.failures) process.stdout.write(`FAIL ${f}\n`);
|
|
1169
|
+
process.stdout.write(`${r.pass ? 'PASS' : 'FAIL'} host-adapter self-test ${r.checks - r.failures.length}/${r.checks}\n`);
|
|
1170
|
+
process.exitCode = r.pass ? 0 : 1;
|
|
1171
|
+
}
|