@shgroup/dsh-serenity-hooks 1.46.0 → 1.47.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.
@@ -918,4 +918,10 @@ function appendBound(session, action, rec) {
918
918
  }
919
919
  }
920
920
  //#endregion
921
- export { useSession as C, localIdStamp as D, localHuman as E, summarize as S, localFileStamp as T, resolveSessionByTitle as _, resolveSessionTrajectoryLabel as a, setActiveSessionInfo as b, TRAJECTORY_ACTIONS as c, extractSessionMdPathFromText as d, findSession as f, readActiveSessionMd as g, parseSessionContextFromEvents as h, readLastBound as i, clearActiveSessionInfo as l, listSessions as m, listBoundSessionIds as n, setBindingStore as o, getActiveSessionInfo as p, pruneMissingBindings as r, DEFAULT_SESSION_SCOPE as s, appendBound as t, createSession as u, sessionEvents as v, isoLocal as w, showSession as x, sessionsRoot as y };
921
+ //#region src/message-source.ts
922
+ /** ACC 注入消息的 kind —— 唤醒 / 打回 / keeper 提醒 / 锚定轮 / rebuild 交接等一律同值。 */
923
+ const ACC_MESSAGE_KIND = "plugin:dsh-serenity-hooks";
924
+ /** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
925
+ const PLUGIN_SOURCE = { kind: ACC_MESSAGE_KIND };
926
+ //#endregion
927
+ export { showSession as C, localFileStamp as D, isoLocal as E, localHuman as O, setActiveSessionInfo as S, useSession as T, parseSessionContextFromEvents as _, pruneMissingBindings as a, sessionEvents as b, setBindingStore as c, clearActiveSessionInfo as d, createSession as f, listSessions as g, getActiveSessionInfo as h, listBoundSessionIds as i, localIdStamp as k, DEFAULT_SESSION_SCOPE as l, findSession as m, PLUGIN_SOURCE as n, readLastBound as o, extractSessionMdPathFromText as p, appendBound as r, resolveSessionTrajectoryLabel as s, ACC_MESSAGE_KIND as t, TRAJECTORY_ACTIONS as u, readActiveSessionMd as v, summarize as w, sessionsRoot as x, resolveSessionByTitle as y };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * message-source.ts — ACC 注入消息的来源标识(**单一真相源**,v1.47.0 / DSH 0.1.7-rc.1 适配)
3
+ *
4
+ * **为什么有这个文件**:DSH 0.1.7-rc.1(session format v3→v4)把消息来源从
5
+ * 「`kind: 'plugin'` + `plugin` 字段」改成**每个生产者声明自己的 kind** ——
6
+ * `@deepseek-ai/dsh-llm` 的 `MessageSourceMap` 逐字写着:
7
+ * *"each producer declares its own `kind` in its own module; there is no shared
8
+ * catch-all `plugin` kind"*。⇒ 旧写法 `{ kind: 'plugin', plugin: 'dsh-serenity-hooks' }`
9
+ * **在类型层不再合法**(合并和类型里没有 `plugin` 这一支),必须用**模块声明合并**
10
+ * 补上自己的 kind(宿主 `dsh-session-title-llm` 与 `dsh-compaction` 都用此法)。
11
+ *
12
+ * **为什么取字面量 `plugin:dsh-serenity-hooks`**:v3→v4 迁移的 `producerKind()`
13
+ * (`session-format-v3-to-v4/src/sources.ts`)把**第一方之外**的旧生产者一律写为
14
+ * `plugin:${旧 plugin 字段}` ⇒ **存量会话升级后就是这个字面量**。新代码沿用同一字面量,
15
+ * 使同一生产者在旧数据与新数据里**只有一个 kind**(否则下游与我方读取面都要处理两种)。
16
+ */
17
+ import type { ContextFormed, MessageSource } from '@deepseek-ai/dsh-llm';
18
+ declare module '@deepseek-ai/dsh-llm' {
19
+ interface MessageSourceMap {
20
+ 'plugin:dsh-serenity-hooks': {
21
+ kind: 'plugin:dsh-serenity-hooks';
22
+ } & ContextFormed;
23
+ }
24
+ }
25
+ /** ACC 注入消息的 kind —— 唤醒 / 打回 / keeper 提醒 / 锚定轮 / rebuild 交接等一律同值。 */
26
+ export declare const ACC_MESSAGE_KIND: "plugin:dsh-serenity-hooks";
27
+ /** ACC 注入消息的统一来源值(各缝共用;需要 `form` 时按 `ContextFormed` 另加字段)。 */
28
+ export declare const PLUGIN_SOURCE: MessageSource;
@@ -11,6 +11,15 @@
11
11
  * (dsh-serenity-hooks/container_trajectory rebuild/mech-registry),打回会瘫痪自身工作。
12
12
  */
13
13
  import type { Context } from 'cordis';
14
+ import type { Agent } from '@deepseek-ai/dsh-agent';
15
+ /** 读会话最后一个 assistant/message 文本(skiff-core 同款)
16
+ * **2026-09-23(S142 D77)起导出**:`unattended-seam.ts` 复用同一读法(本容器纪律:
17
+ * 「同一读法只留一份」——两份 `lastAssistantText` 会在宿主改事件形状时**只修一处**)。
18
+ * **v1.27.7:剥离 think 块再检测**(用户:"不检查think内容(因为不输出)")——
19
+ * ` think…` 是模型思考过程,不进入用户可见输出;敏感词检测只针对**最终呈现文本**,
20
+ * 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
21
+ * (v1.26.8 状态机,弃正则——同 v1.27.1 微信桥回复链路)。 */
22
+ export declare function lastAssistantText(agent: Agent): string;
14
23
  /**
15
24
  * 注册输出守卫(index.ts apply 调用):
16
25
  * turn-stopping 时检测最终输出,命中敏感词 → steer 打回重生成(官方机制,零改 DSH)。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * context.ts — 拦截缝:ACC 上下文注入(agent/session-start + agent/prompt-submit)
2
+ * context.ts — 拦截缝:ACC 上下文注入(agent/created + agent/prompt-submit)
3
3
  *
4
4
  * 对应 opencode-serenity-plugin 的 system.transform(ACC 身份/约束注入)+ Phase 2 访谈提示。
5
5
  *
@@ -1,6 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as hostSettings } from "./access-fiehjxV6.js";
3
- import z from "@deepseek-ai/schemastery";
4
3
  //#region src/ports.ts
5
4
  /**
6
5
  * 集中端口表。
@@ -62,43 +61,11 @@ const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id
62
61
  var settings_section_exports = /* @__PURE__ */ __exportAll({
63
62
  SERENITY_SETTINGS_NS: () => SERENITY_SETTINGS_NS,
64
63
  defaultSimpleSettings: () => defaultSimpleSettings,
65
- entryDefaults: () => entryDefaults,
66
64
  readSimpleSettings: () => readSimpleSettings,
67
- registerSettingsSection: () => registerSettingsSection
65
+ registerSettingsSection: () => registerSettingsSection,
66
+ simpleSettingsFromConfig: () => simpleSettingsFromConfig
68
67
  });
69
- /** 简单配置命名空间(dsh 设置面板的 section id / settings.yaml section) */
70
- const SERENITY_SETTINGS_NS = "serenity-hooks";
71
- /** schemastery schema(与 DSH 各插件 Config 同款) */
72
- const simpleSettingsSchema = z.object({
73
- gatewayEnabled: z.boolean().default(false),
74
- rebuildEnabled: z.boolean().default(true),
75
- rebuildThresholdK: z.number().min(50).max(4e3).default(400),
76
- skiffEnabled: z.boolean().default(false),
77
- skiffDebugPort: z.number().min(1024).max(65535).default(SKIFF_DEBUG_PORT),
78
- acpEnabled: z.boolean().default(false),
79
- acpHttpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT),
80
- publicAskEnabled: z.boolean().default(false),
81
- croEnabled: z.boolean().default(true)
82
- });
83
- /** 从插件 Config 提取 entry 默认(settings base 层) */
84
- function entryDefaults(config) {
85
- return {
86
- gatewayEnabled: config.gateway?.enabled ?? false,
87
- rebuildEnabled: config.rebuild?.enabled ?? true,
88
- rebuildThresholdK: config.rebuild?.thresholdK ?? 400,
89
- skiffEnabled: config.skiff?.enabled ?? false,
90
- skiffDebugPort: config.skiff?.debugPort ?? SKIFF_DEBUG_PORT,
91
- acpEnabled: config.acp?.enabled ?? false,
92
- acpHttpPort: config.acp?.httpPort ?? ACP_HTTP_PORT,
93
- publicAskEnabled: config.publicAsk?.enabled ?? false,
94
- croEnabled: true
95
- };
96
- }
97
- /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
98
- let simpleSource = null;
99
- /** 缺 settings provider 的告警去重(进程级一次;降级路径可能被多次注册调用) */
100
- let warnedNoSettingsProvider = false;
101
- /** 进程级默认(无 settings 服务时的兜底) */
68
+ /** 进程级默认(无 Config 可读时的兜底;与 Config 各字段的缺省一致) */
102
69
  function defaultSimpleSettings() {
103
70
  return {
104
71
  gatewayEnabled: false,
@@ -109,46 +76,65 @@ function defaultSimpleSettings() {
109
76
  acpEnabled: false,
110
77
  acpHttpPort: ACP_HTTP_PORT,
111
78
  publicAskEnabled: false,
112
- croEnabled: true
79
+ croEnabled: true,
80
+ unattendedEnabled: false
81
+ };
82
+ }
83
+ /**
84
+ * Config(宿主 Loader 校验后的解析值)→ 扁平简单配置。**本文件唯一的读取语义**。
85
+ *
86
+ * 优先级:**面板层(扁平键)> 部署层(嵌套段)> 内建缺省**。
87
+ * @param config - 插件 Config(`fiber.config` 或 apply 收到的 config)
88
+ * @returns 扁平开关集合
89
+ */
90
+ function simpleSettingsFromConfig(config) {
91
+ const d = defaultSimpleSettings();
92
+ return {
93
+ gatewayEnabled: config.gatewayEnabled ?? config.gateway?.enabled ?? d.gatewayEnabled,
94
+ rebuildEnabled: config.rebuildEnabled ?? config.rebuild?.enabled ?? d.rebuildEnabled,
95
+ rebuildThresholdK: config.rebuildThresholdK ?? config.rebuild?.thresholdK ?? d.rebuildThresholdK,
96
+ skiffEnabled: config.skiffEnabled ?? config.skiff?.enabled ?? d.skiffEnabled,
97
+ skiffDebugPort: config.skiffDebugPort ?? config.skiff?.debugPort ?? d.skiffDebugPort,
98
+ acpEnabled: config.acpEnabled ?? config.acp?.enabled ?? d.acpEnabled,
99
+ acpHttpPort: config.acpHttpPort ?? config.acp?.httpPort ?? d.acpHttpPort,
100
+ publicAskEnabled: config.publicAskEnabled ?? d.publicAskEnabled,
101
+ croEnabled: config.croEnabled ?? d.croEnabled,
102
+ unattendedEnabled: config.unattendedEnabled ?? d.unattendedEnabled
113
103
  };
114
104
  }
105
+ /** 运行时源(`registerSettingsSection` 装配:读本插件 fiber 的**当前** Config) */
106
+ let simpleSource = null;
115
107
  /**
116
- * 读取当前简单配置(settings 解析值;无 provider/未注册 → entry 默认)。
117
- * 各功能(gateway/rebuild/naming)启动与运行时判断开关都经此函数。
108
+ * 读取当前简单配置(各功能 gateway/rebuild/skiff/… 的启动与运行时判断都经此)。
109
+ *
110
+ * 源 = 本插件 fiber 的 `config`(宿主在配置变更时走 `fiber.update()` ⇒ `config` 始终最新,
111
+ * **不必**依赖 apply 重跑,也不必缓存)。
112
+ * @returns 扁平开关集合
118
113
  */
119
114
  function readSimpleSettings() {
120
115
  return simpleSource ? simpleSource() : defaultSimpleSettings();
121
116
  }
122
117
  /**
123
- * 注册简单配置到 DSH settings(零改 DSH;settings.yaml 持久化 + 原生面板渲染)。
124
- * 注:插件 Config(cordis.yml 组合层)作为 base;用户文档层叠加其上。
125
- * 运行时读取简单配置统一经 `readSimpleSettings()`。
126
- * v1.28.0 适配 0.1.2-rc.1(B4):`installSettingsSection`/`settingsNamespace` 便捷函数在
127
- * rc.1 消失 → 改 `SettingsProvider.installSection(owner, ns, schema, entry, hooks)`
128
- * (owner = consumer 插件 ctx;ns 直接字符串)。
118
+ * 装配简单配置读取面(v1.21 分层;**v0.1.7 起不再注册任何 section**)。
119
+ *
120
+ * 两件事:
121
+ * 1. 把"本插件自己的 Config"接成简单配置源 —— 读 `ctx.fiber.config`(宿主 `fiber.update()`
122
+ * 保证最新),拿不到 fiber 时退回 apply 收到的 `config`。
123
+ * 2. 关掉宿主按 Config schema **自动生成**的通用页(`settings.configure({ auto: false })`)——
124
+ * 门面是我们的自定义页 `client/SettingsSection.tsx`(同一个命名空间,同一份数据)。
125
+ *
126
+ * ⚠️ 两个失败都不阻断插件装载(面板是附属能力,绝不能成为启动单点)。
127
+ * @param ctx - 插件上下文
128
+ * @param config - apply 收到的 Config(fiber.config 不可用时的兜底)
129
129
  */
130
130
  function registerSettingsSection(ctx, config) {
131
- const hooks = {
132
- setSource: (get) => {
133
- simpleSource = get;
134
- },
135
- onChange: () => {
136
- try {
137
- ctx.emit?.("serenity/settings-changed");
138
- } catch {}
139
- }
140
- };
141
- const settingsAny = hostSettings(ctx);
142
- if (settingsAny) {
143
- settingsAny.installSection.call(settingsAny, ctx, SERENITY_SETTINGS_NS, simpleSettingsSchema, entryDefaults(config), hooks);
144
- return;
145
- }
146
- if (!warnedNoSettingsProvider) {
147
- warnedNoSettingsProvider = true;
148
- console.warn("[serenity-hooks] ✗ settings 服务不可用 → 简单配置降级为 entry 默认值(面板开关/阈值改动不生效;检查宿主 settings provider 是否装载)");
149
- }
150
- hooks.setSource(() => ({}));
151
- hooks.onChange();
131
+ const fiber = ctx.fiber;
132
+ const readConfig = () => fiber?.config ?? config;
133
+ simpleSource = () => simpleSettingsFromConfig(readConfig());
134
+ const settings = hostSettings(ctx);
135
+ try {
136
+ settings?.configure?.({ auto: false }, fiber);
137
+ } catch {}
152
138
  }
153
139
  //#endregion
154
140
  export { FACE_PORTS as a, MECHANISM_PORTS as c, ACP_HTTP_PORT as i, SKIFF_DEBUG_PORT as l, registerSettingsSection as n, GATEWAY_PORT as o, settings_section_exports as r, MAIN_WEB_PORT as s, readSimpleSettings as t, WEIXIN_SEND_PORT as u };
@@ -1,22 +1,40 @@
1
1
  /**
2
- * settings-section.ts — 简单配置注册(dsh 原生设置面板,v1.21 分层)
2
+ * settings-section.ts — 简单配置读取(dsh 原生设置面板;v1.21 分层,**v1.47 A 案移植到 0.1.7 表单模型**)
3
3
  *
4
4
  * 分层决策(S142):**简单配置(开关/阈值)→ dsh 原生设置面板**;
5
5
  * **复杂配置(账号列表)→ 宁静号高级面板**(localstore + /serenity/config)。
6
6
  *
7
- * 本模块承载简单配置层:`SettingsProvider.installSection(ctx, 'serenity-hooks', schema, entry)`
8
- * 注册 `serenity-hooks` namespace——三功能总开关 + F2 阈值。
9
- * (v1.28.0 适配 0.1.2-rc.1:installSettingsSection/settingsNamespace 便捷函数消失 → 方法调用)
7
+ * ## 🔴 v0.1.7 起(A 案):**"安装 section"这件事整段退场**
10
8
  *
11
- * 运行时降级守卫(版本鲁棒性):旧 RC(staging 架构)api-proxy 有
12
- * `WEB_SETTINGS_NAMESPACES` 静态白名单,第三方 ns 会收到 settings-not-exposed
13
- * ——client 面板读不到。新 RC(官方 master)已删除白名单(全量 describe)。
14
- * 本模块不依赖白名单状态:register 总是执行(host 侧零成本);
15
- * **client 侧**(SettingsSection 组件)以 describe 结果为据——ns 不在描述列表
16
- * 则显示降级提示(引导去宁静号面板),在则渲染 schema 表单。零配置自动适配。
9
+ * 宿主把 `SettingsProvider.installSection` 换成 `SettingsForms`:设置页不再由插件
10
+ * "注册一个 namespace + schema + entry + hooks",而是**由插件自己的 cordis `Config`
11
+ * schema 投影成表单**(`packages/settings/settings` 的 `describe()` 遍历 profile 条目)。
12
+ * ⇒ 命名空间**就是 profile 条目 id** —— 本插件 = `serenity-hooks`
13
+ * (见 `cordis.patch.yml` 的 `- insert: - id: serenity-hooks`)。
14
+ *
15
+ * ## 为什么旧扁平键**原样保留**(这是本文件最容易被后人改错的地方)
16
+ *
17
+ * 旧模型把简单配置存成 `~/.dsh/settings.yaml` 的 `serenity-hooks:` 段,字段名是**扁平**的
18
+ * (`gatewayEnabled` / `skiffEnabled` / …)。新模型的命名空间**逐字相同**
19
+ * (`serenity-hooks`),而宿主的 `SettingsForms.importLegacyDocument()` 会把该段
20
+ * **按同名条目导入**;schemastery 的 object 解析在非 strict 模式下 `merge(result, data)`
21
+ * ⇒ **未声明的键会被保留而非报错**。⇒ 只要这些扁平键**仍在本插件 Config 里声明**,
22
+ * **旧值就自动随宿主导入搬过来,零迁移代码**(这正是宿主的迁移惯例:`LEGACY_SECTION_ENTRIES`
23
+ * 只重映射**段名**,从不改写字段名)。
24
+ *
25
+ * ## 两层拼写(**有意保留,不是重复真相源**)
26
+ *
27
+ * | 层 | 拼写 | 谁写 | 语义 |
28
+ * |---|---|---|---|
29
+ * | **面板层**(用户层) | 扁平键 `gatewayEnabled` | 设置面板 / 宿主 legacy 导入 | 用户在界面上改的那个 |
30
+ * | **部署层** | 嵌套段 `gateway.enabled` | `cordis.yml` / bundle patch | 部署时的缺省 |
31
+ *
32
+ * 读取优先级 = **面板层 > 部署层 > 内建缺省**(`simpleSettingsFromConfig`)。
33
+ * 🔴 面板层的键**故意不带 schemastery 默认值** —— 有默认值就永远非 `undefined`,
34
+ * 部署层会被无声压死("未设置"必须可观测)。
17
35
  */
18
36
  import type { Context } from 'cordis';
19
- /** 插件 Config 的简单配置片段(index.ts Config 组合;settings entry base) */
37
+ /** 插件 Config 的简单配置片段(index.ts 的 Config 子集;面板层 + 部署层两种拼写) */
20
38
  interface SimpleConfigFragment {
21
39
  gateway?: {
22
40
  enabled?: boolean;
@@ -35,68 +53,92 @@ interface SimpleConfigFragment {
35
53
  enabled?: boolean;
36
54
  httpPort?: number;
37
55
  };
38
- /** F4d 建议问答页(实验性):按认知容器暴露问答页供他人验证(全局开关;与 ACP 共用端口) */
39
- publicAsk?: {
40
- enabled?: boolean;
41
- };
56
+ gatewayEnabled?: boolean;
57
+ rebuildEnabled?: boolean;
58
+ rebuildThresholdK?: number;
59
+ skiffEnabled?: boolean;
60
+ skiffDebugPort?: number;
61
+ acpEnabled?: boolean;
62
+ acpHttpPort?: number;
63
+ publicAskEnabled?: boolean;
64
+ croEnabled?: boolean;
65
+ unattendedEnabled?: boolean;
42
66
  }
43
- /** 简单配置命名空间(dsh 设置面板的 section id / settings.yaml section) */
44
- export declare const SERENITY_SETTINGS_NS = "serenity-hooks";
45
67
  /**
46
- * 简单配置 schema(schemastery):三功能总开关 + F2 阈值 + F4 Skiff 启停。
47
- * 与 DSH settings 的 schema 语义一致(z.object 布尔/数字)。
68
+ * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
48
69
  */
70
+ export declare const SERENITY_SETTINGS_NS = "serenity-hooks";
71
+ /** 简单配置的扁平形态(运行时读取的统一形状;各功能门控只看它) */
49
72
  interface SerenitySimpleSettings {
50
73
  /** F1 双端口网关总开关 */
51
74
  gatewayEnabled: boolean;
52
75
  /** F2 超限重建总开关 */
53
76
  rebuildEnabled: boolean;
54
- /** F2 触发阈值(需求① S142 用户拍板:百分比比例 → K 数值;projectedTokens ≥ thresholdK*1000 触发,纯绝对无窗口比例保护) */
77
+ /** F2 触发阈值(需求① S142 用户拍板:百分比比例 → K 数值;projectedTokens ≥ thresholdK*1000 触发) */
55
78
  rebuildThresholdK: number;
56
79
  /** F4 Skiff 调试服务总开关(实验性;默认关——不随插件加载自动启动,人工开启) */
57
80
  skiffEnabled: boolean;
58
81
  /** F4 Skiff 调试端口(默认 3099,仅 127.0.0.1) */
59
82
  skiffDebugPort: number;
60
- /** F4c ACP HTTP JSON-RPC 端点总开关(实验性;默认关——人工开启) */
83
+ /** F4c ACP HTTP JSON-RPC 端点总开关(实验性;默认关) */
61
84
  acpEnabled: boolean;
62
85
  /** F4c ACP HTTP 端口(默认 3100,仅 127.0.0.1) */
63
86
  acpHttpPort: number;
64
87
  /** F4d 建议问答页总开关(实验性;默认关——按认知容器暴露问答页,key 认证) */
65
88
  publicAskEnabled: boolean;
66
- /** **CRO(轨迹自编程唤起)总开关** —— 2026-09-21 所有者令新增。
89
+ /** **CRO(轨迹自编程唤起)总开关**(2026-09-21 所有者令新增;**缺省开**)。
67
90
  *
68
91
  * CRO = 轨迹目录下放一个 `continuous-re-occurrence.ts`,由 ACC 的 5min tick spawn,
69
92
  * 由**程序**决定"要不要唤、何时唤、唤起的提示词是什么"。
70
- *
71
- * **缺省开**:① 它已在生产上稳定运行(S151 / S185 各有程序在跑);② 它的**默认存在感为零**
72
- * ——没有程序文件的轨迹本来就不参与;③ 缺省关会让"已上线的程序突然不跑"(等于静默回退)。
73
- * **只关 CRO 阶段**:`send-later` / `send-now` / 唤醒表投递**照常**(见 `cro.ts` 头注的承诺)。
74
- *
75
- * ⚠️ 与它**同时废止**的键:`wakeSchedulerEnabled`(所有者 2026-09-21 令「send-later 的开关
76
- * 不再重要了,砍掉」)——唤醒调度器**不再有任何闸**(恒开);旧配置里的该键**静默忽略**。 */
93
+ * 缺省开的理由:① 已在生产稳定运行(S151 / S185);② 默认存在感为零(没有程序文件的轨迹
94
+ * 本来就不参与);③ 缺省关会让"已上线的程序突然不跑"。
95
+ * **只关 CRO 阶段**:`send-later` / `send-now` / 唤醒表投递**照常**。
96
+ * ⚠️ 与它**同时废止**的键:`wakeSchedulerEnabled`(唤醒调度器现恒开、无闸;旧键静默忽略)。 */
77
97
  croEnabled: boolean;
98
+ /** **无人值守代理回复(unattended proxy)总开关**(2026-09-23 所有者令新增;**缺省关**)。
99
+ *
100
+ * 开启后:CCC 会话**被 LLM 主动停止**且无人应答时,ACC 注入一次**代理用户的结构化回复**,
101
+ * 以**验证码(nonce)**作「合法收束」判据 —— **未回码 ⇒ 继续,回码 ⇒ 放过**。
102
+ * **缺省关**:它把**人**从回路里拿掉(失败模式是静默的)。
103
+ * **排除面**:仅有 CRO 程序的轨迹不适用(所有者 2026-09-23 裁决:口径由"宽"改"窄"
104
+ * —— 原"任何自排唤醒都排除"会让本机制永远够不着靠续接绳活着的维护会话;改窄后
105
+ * **自排绳不再排除**,绳退化为**长周期保险丝**)。设计全文见 S142 的 **D77**。 */
106
+ unattendedEnabled: boolean;
78
107
  }
79
- /** 从插件 Config 提取 entry 默认(settings base 层) */
80
- export declare function entryDefaults(config: SimpleConfigFragment): SerenitySimpleSettings;
81
- /** 进程级默认(无 settings 服务时的兜底) */
108
+ /** 进程级默认(无 Config 可读时的兜底;与 Config 各字段的缺省一致) */
82
109
  export declare function defaultSimpleSettings(): SerenitySimpleSettings;
83
110
  /**
84
- * 读取当前简单配置(settings 解析值;无 provider/未注册 → entry 默认)。
85
- * 各功能(gateway/rebuild/naming)启动与运行时判断开关都经此函数。
111
+ * Config(宿主 Loader 校验后的解析值)→ 扁平简单配置。**本文件唯一的读取语义**。
112
+ *
113
+ * 优先级:**面板层(扁平键)> 部署层(嵌套段)> 内建缺省**。
114
+ * @param config - 插件 Config(`fiber.config` 或 apply 收到的 config)
115
+ * @returns 扁平开关集合
116
+ */
117
+ export declare function simpleSettingsFromConfig(config: SimpleConfigFragment): SerenitySimpleSettings;
118
+ /**
119
+ * 读取当前简单配置(各功能 gateway/rebuild/skiff/… 的启动与运行时判断都经此)。
120
+ *
121
+ * 源 = 本插件 fiber 的 `config`(宿主在配置变更时走 `fiber.update()` ⇒ `config` 始终最新,
122
+ * **不必**依赖 apply 重跑,也不必缓存)。
123
+ * @returns 扁平开关集合
86
124
  */
87
125
  export declare function readSimpleSettings(): SerenitySimpleSettings;
88
126
  /**
89
- * 测试注入钩子(生产零调用):替换/恢复运行时源,便于单测各功能门控
90
- * (registerSettingsSection 的 setSource 回调在 mock dsh-settings 时不会触发)。
127
+ * 测试注入钩子(生产零调用):替换/恢复运行时源,便于单测各功能门控。
91
128
  */
92
129
  export declare function __setSimpleSourceForTest(source: (() => SerenitySimpleSettings) | null): void;
93
130
  /**
94
- * 注册简单配置到 DSH settings(零改 DSH;settings.yaml 持久化 + 原生面板渲染)。
95
- * 注:插件 Config(cordis.yml 组合层)作为 base;用户文档层叠加其上。
96
- * 运行时读取简单配置统一经 `readSimpleSettings()`。
97
- * v1.28.0 适配 0.1.2-rc.1(B4):`installSettingsSection`/`settingsNamespace` 便捷函数在
98
- * rc.1 消失 → 改 `SettingsProvider.installSection(owner, ns, schema, entry, hooks)`
99
- * (owner = consumer 插件 ctx;ns 直接字符串)。
131
+ * 装配简单配置读取面(v1.21 分层;**v0.1.7 起不再注册任何 section**)。
132
+ *
133
+ * 两件事:
134
+ * 1. 把"本插件自己的 Config"接成简单配置源 —— 读 `ctx.fiber.config`(宿主 `fiber.update()`
135
+ * 保证最新),拿不到 fiber 时退回 apply 收到的 `config`。
136
+ * 2. 关掉宿主按 Config schema **自动生成**的通用页(`settings.configure({ auto: false })`)——
137
+ * 门面是我们的自定义页 `client/SettingsSection.tsx`(同一个命名空间,同一份数据)。
138
+ *
139
+ * ⚠️ 两个失败都不阻断插件装载(面板是附属能力,绝不能成为启动单点)。
140
+ * @param ctx - 插件上下文
141
+ * @param config - apply 收到的 Config(fiber.config 不可用时的兜底)
100
142
  */
101
143
  export declare function registerSettingsSection(ctx: Context, config: SimpleConfigFragment): void;
102
144
  export {};
@@ -0,0 +1,247 @@
1
+ /**
2
+ * unattended-ops.ts — 无人值守代理回复(unattended proxy)机制层(S142 **D77**,owner 2026-09-23 提案)
3
+ *
4
+ * ## 这东西用来干什么(白话)
5
+ *
6
+ * 用户发个任务就走开。现在模型干完一段就停下来等回话,用户不在 ⇒ **会话挂在那儿不动**。
7
+ * 本机制 = 用户走开后,系统**代替用户说一句"继续"**:让它自己跑完;干完了用一个**验证码**报告;
8
+ * 卡住了先自己想办法;真不能定的**写下来等用户回来**。
9
+ *
10
+ * ## 归属(D23:ACC 管机制与数据,CCC 管措辞与纪律)
11
+ *
12
+ * | 面 | 归谁 | 内容 |
13
+ * |---|---|---|
14
+ * | **机制** | 🔴 **ACC(本模块)** | 触发判定 / 验证码 / 四选一结构 / 轮上限 / 流水 |
15
+ * | **不可代批的*具体内容*** | **CCC 纪律** | ACC 只给**通用下限**({@link UNATTENDED_BLOCKED_FLOOR}),CCC 可加严 |
16
+ *
17
+ * 🔵 下限之所以落在 ACC(而不是"完全交给 CCC"):**没有这张清单,"自行解决"=把所有把关点自动放行**。
18
+ * 最终内容仍待 owner 一句(设计 §13-5)——本版先给**保守下限**,CCC 只能在其上加严、不得放宽。
19
+ *
20
+ * ## 🔴 核心原则:单一续驱者(single-driver)
21
+ *
22
+ * > **一个会话同一时刻只应有一个"续驱者":用户 / CRO / 自排绳 / 无人值守代理。**
23
+ *
24
+ * 口径(owner 2026-09-23 19:1x 裁决)= **窄口径:只排除「有 CRO 程序的轨迹」**。
25
+ * 原「宽口径」(任何自排唤醒都排除)**作废** —— 它会让本机制**永远够不着靠续接绳活着的会话**,
26
+ * 等于**放弃"代替旧机制"**(绳因此退化为**长周期保险丝**:6–12h 一条)。
27
+ *
28
+ * ## 🔴 铁律(与 `cro.ts` / `cro-log.ts` 同族,勿破)
29
+ *
30
+ * **本模块的每个导出函数都不抛错**:写盘 / 解析 / 裁剪失败一律返回结构化失败。
31
+ * ⇒ 代理机制的任何问题**不影响 turn 收尾**、不影响既有链路(唤醒投递 / 输出守卫 / rebuild)。
32
+ *
33
+ * ## 🔴 为什么不构成第二真相源(勿删此论证)
34
+ *
35
+ * | 载体 | 谁写 | 主语 | 回答 |
36
+ * |---|---|---|---|
37
+ * | `<轨迹目录>/unattended-log.json`(**本模块**) | **ACC** | ACC | 「**我注入了什么、判了什么、放过没有**」 |
38
+ * | `<轨迹目录>/SESSION.md` | **LLM(CCC)** | CCC | 「**这轮发生了什么、为什么这么定**」 |
39
+ *
40
+ * **内容不重叠**:本模块只记**机械事实**(轮次 / 验证码 / 判定理由码 / 是否放过),**不写散文**;
41
+ * LLM 侧的散文与理由归 SESSION.md。完整审计 = 两者合看(本模块 = 注入面|SESSION.md = 判断面)。
42
+ *
43
+ * ## 🔴 验证码形态(禁用 4 位字母码)
44
+ *
45
+ * 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
46
+ * ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
47
+ * 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
48
+ * 先例 = `handyman` background 的 `SERENITY_HANDYMAN_DONE_<16hex>`(同族实现)。
49
+ */
50
+ /** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
51
+ export declare const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
52
+ /** 完成码包装后缀 */
53
+ export declare const UNATTENDED_DONE_SUFFIX = "]]";
54
+ /** 注入消息的**首行标记**(幂等可辨:模型与事后审计都能一眼看出这是代理消息,不是用户消息) */
55
+ export declare const UNATTENDED_PROXY_MARK = "[[UNATTENDED-PROXY v1]]";
56
+ /** 🔴 合成消息**固定身份句**(owner 已裁:「形如用户消息,但首句自报身份与权限边界」) */
57
+ export declare const UNATTENDED_IDENTITY_LINE = "\u65E0\u4EBA\u503C\u5B88\u4EE3\u7406 \u00B7 \u975E\u672C\u4EBA";
58
+ /** nonce 字节数 ⇒ 16 个 hex 字符(与 handyman 同量级;够长到不可能被复述猜中) */
59
+ export declare const UNATTENDED_NONCE_BYTES = 8;
60
+ /** 生成一枚新 nonce(**每轮一枚**;旧码作废由调用方在文本里声明) */
61
+ export declare function newNonce(): string;
62
+ /** 完成码全文(注入文本展示用;校验走 {@link hasDoneToken}) */
63
+ export declare function doneToken(nonce: string): string;
64
+ /**
65
+ * 出现过的完成码 nonce 列表(按出现顺序、去重;**小写归一**)。
66
+ * 容忍 8–64 位 hex(模型可能手抄多/少几个字符 ⇒ 那就不匹配,属预期:只认本轮那枚)。
67
+ */
68
+ export declare function parseDoneNonces(text: string): string[];
69
+ /** 本轮收尾文本里是否出现过**当前轮**那枚码(唯一合法收束判据) */
70
+ export declare function hasDoneToken(text: string, nonce: string): boolean;
71
+ /** 单个会话在一个"代理段"内的**轮次上限**(设计建议 10–20;取 15——远小于 handyman 的 100) */
72
+ export declare const UNATTENDED_MAX_ROUNDS = 15;
73
+ /** 一个"代理段"的**墙钟上限**(自第一次注入起算;跨轮累计,防"轮次没到但烧了一天") */
74
+ export declare const UNATTENDED_MAX_WALL_MS: number;
75
+ /** 触及即停(写待办/交接,**不循环**)——**CCC 的纪律更严则从严** */
76
+ export declare const UNATTENDED_BLOCKED_FLOOR: readonly string[];
77
+ /** 注入消息装配输入 */
78
+ export interface ProxyMessageInput {
79
+ /** 本轮轮次号(1 起) */
80
+ round: number;
81
+ /** 轮上限(写进文本让模型知道还剩几轮) */
82
+ maxRounds: number;
83
+ /** 本轮 nonce */
84
+ nonce: string;
85
+ /** 不可代批清单(缺省 = {@link UNATTENDED_BLOCKED_FLOOR}) */
86
+ blocked?: readonly string[];
87
+ }
88
+ /** 组装一次代理注入消息(**形如用户消息,但首句自报身份与权限边界**) */
89
+ export declare function buildProxyMessage(input: ProxyMessageInput): string;
90
+ /**
91
+ * 组装一次**假完成打回**消息(设计 §7 的机械核验分支:声称已通知用户,但本轮无发送记录)。
92
+ *
93
+ * ⚠️ 同样带**新 nonce**:它是一次"再来一轮",不是"放过"。
94
+ */
95
+ export declare function buildFalseNotifyRebuke(input: ProxyMessageInput): string;
96
+ /** 收尾文本是否**声称**已联系用户(见 {@link NOTIFY_CLAIM_PATTERNS} 的误判方向说明) */
97
+ export declare function claimsUserNotification(text: string): boolean;
98
+ /**
99
+ * **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
100
+ * `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
101
+ * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `handyman-`(自主 worker,已有自己的完成码循环)。
102
+ *
103
+ * ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
104
+ * **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
105
+ */
106
+ export declare function isProxyExcludedSessionId(sessionId: string): boolean;
107
+ /** 判定所需事实(**全部由 seam 取好**,本函数不做任何 I/O、不读时钟) */
108
+ export interface UnattendedFacts {
109
+ /** 总闸(settings `unattendedEnabled`,缺省关) */
110
+ enabled: boolean;
111
+ /** 本会话所属 CCC 根;null = 不在任何 CCC 内 */
112
+ root: string | null;
113
+ /** 是 CCC 内**主会话**(`delegationDepth === 0`;子代理/无头运行不算) */
114
+ isMainSession: boolean;
115
+ /** 会话 id 在排除面({@link isProxyExcludedSessionId}) */
116
+ sessionIdExcluded: boolean;
117
+ /** **本轮由真实用户消息发起**(`agent/inbox/claimed` + source.kind === 'user') */
118
+ userInitiated: boolean;
119
+ /** 该轨迹**有 CRO 程序**(窄口径的唯一排除项) */
120
+ hasCroProgram: boolean;
121
+ /** 该会话当前的代理段状态;null = 尚未开始 */
122
+ run: ProxyRun | null;
123
+ /** 当前时刻(epoch ms,由调用方注入 ⇒ 用例可穷举时间边界) */
124
+ nowMs: number;
125
+ /** 收尾文本里出现的当前轮候选码({@link parseDoneNonces} 的首个;无 ⇒ null) */
126
+ doneNonce: string | null;
127
+ /** 收尾文本**声称**已通知用户({@link claimsUserNotification}) */
128
+ claimsNotify: boolean;
129
+ /** 本轮窗口内 ACC 侧**确实**发出过消息({@link hasOutboundSendSince}) */
130
+ outboundSeen: boolean;
131
+ }
132
+ /** 判定结论(`reason` 是**机器可读理由码**:写进流水,便于事后 grep 归因) */
133
+ export interface UnattendedVerdict {
134
+ action: 'skip' | 'accept' | 'inject' | 'cap';
135
+ reason: string;
136
+ /** 轮次号:`inject` = **即将注入**的那一轮;`accept` / `cap` = 当前轮;`skip` = 0 */
137
+ round: number;
138
+ }
139
+ /**
140
+ * 一次判定的**唯一入口**(纯函数)。
141
+ *
142
+ * 顺序即语义(R↓,勿随意调换):
143
+ * 1. **闸/空间/会话资格**(关着、不在 CCC、非主会话、排除面)⇒ skip;
144
+ * 2. **单一续驱者**:用户刚发起 ⇒ skip(人就在场);该轨迹有 CRO ⇒ skip(程序在驱动);
145
+ * 3. **合法收束优先于轮上限** —— 最后一轮交的码必须算数(否则等于永远收不了尾);
146
+ * 4. 交码 + 声称已通知 + **无发送记录** ⇒ 假完成,走下一轮(reason=`false-notify`);
147
+ * 5. 墙钟 / 轮上限 ⇒ cap(**停下不动**,退回改造前的行为,不产生新风险);
148
+ * 6. 其余 ⇒ inject 下一轮。
149
+ */
150
+ export declare function decideUnattendedAction(f: UnattendedFacts): UnattendedVerdict;
151
+ /** 一个会话的"代理段"状态(自第一次注入起算,直到 放过 / 上限 / 用户回来) */
152
+ export interface ProxyRun {
153
+ /** 已注入到第几轮 */
154
+ round: number;
155
+ /** **当前轮**的 nonce(校验只认它) */
156
+ nonce: string;
157
+ /** 第一次注入的时刻(墙钟上限的起算点,**跨轮累计**) */
158
+ startedAt: number;
159
+ }
160
+ /** 取该会话的代理段(无 ⇒ null) */
161
+ export declare function getProxyRun(sessionId: string): ProxyRun | null;
162
+ /** 开始 / 推进一段代理(`startedAt` 由调用方给出 ⇒ 跨轮保留首次值) */
163
+ export declare function setProxyRun(sessionId: string, run: ProxyRun): void;
164
+ /** 结束该会话的代理段(放过 / 上限 / 用户回来 / 载体销毁) */
165
+ export declare function clearProxyRun(sessionId: string): void;
166
+ /** 当前在代理的会话 id(诊断 / 测试用) */
167
+ export declare function proxyRunSessionIds(): string[];
168
+ /** 当前代理中、已注入轮数的合计(`acc-diag` 观测面用) */
169
+ export declare function activeProxyRoundTotal(): number;
170
+ /** 记下"这个载体的这一轮是真实用户发起的" */
171
+ export declare function noteUserTurn(sessionId: string, turn: number): void;
172
+ /** 该载体的这一轮是否由真实用户发起(判据 = `noteUserTurn` 写入的最新一条) */
173
+ export declare function isUserTurn(sessionId: string, turn: number | null | undefined): boolean;
174
+ /** 清掉该载体的用户轮次记录(载体销毁时) */
175
+ export declare function clearUserTurn(sessionId: string): void;
176
+ /** 清空全部进程内运行态(disposer;**不碰磁盘**) */
177
+ export declare function clearUnattendedState(): void;
178
+ /** 测试辅助:复位(生产零调用) */
179
+ export declare function __resetUnattendedForTest(): void;
180
+ /** 每个 CCC 保留的最近发送条数(环形裁剪 ⇒ 表大小有界) */
181
+ export declare const UNATTENDED_OUTBOUND_KEEP = 20;
182
+ /** 发送记录保留窗(超出即丢:它只服务"这一轮到底发没发"这个瞬时问题) */
183
+ export declare const UNATTENDED_OUTBOUND_TTL_MS: number;
184
+ /**
185
+ * 记一次**成功的**出站发送(`im-bridge` 工具面在通道返回成功后调用)。
186
+ *
187
+ * ⚠️ **覆盖边界(诚实标注)**:只覆盖 **ACC 自己这条通道**(`im-bridge` 的 send / send-file)。
188
+ * CCC 自有的发送通道(如 `mail-tool` MSM)**ACC 看不见** ⇒ 那类"已通知"落到
189
+ * {@link hasOutboundSendSince} = false ⇒ 会被判成假完成。⇒ 本版口径:**代理模式下"联系用户"
190
+ * 只认 ACC 通道**;这与设计 §7 的原话(im-bridge 记日志)一致,边界写在此处以免误读。
191
+ */
192
+ export declare function noteOutboundSend(root: string, atMs?: number): void;
193
+ /** 自 `sinceMs` 起该 CCC 是否有过成功出站发送(**只读,不清表**——清理归写入侧的裁剪) */
194
+ export declare function hasOutboundSendSince(root: string, sinceMs: number, nowMs?: number): boolean;
195
+ /** 发送记录表大小(诊断 / 测试用) */
196
+ export declare function outboundSendRoots(): string[];
197
+ /** 固定文件名(与 `cro-wake-log.json` 并排、主语分明:那份记"我叫了什么",这份记"我代说了什么") */
198
+ export declare const UNATTENDED_LOG_FILENAME = "unattended-log.json";
199
+ /** 保留窗 = 2 日(与 CRO 流水同值;**常量** ⇒ 无需人工清理动作) */
200
+ export declare const UNATTENDED_LOG_RETENTION_MS: number;
201
+ /** 一条代理流水(**成功与失败都记**,与 CRO 流水同规格) */
202
+ export interface UnattendedLogEntry {
203
+ /** 时刻(当地 RFC3339 带偏移,遵 D67) */
204
+ at: string;
205
+ /** 事件:inject(注入了一轮)/ accept(交码放过)/ cap(撞上限停下)/ yield(用户回来、让位) */
206
+ event: 'inject' | 'accept' | 'cap' | 'yield';
207
+ /** 轮次号 */
208
+ round: number;
209
+ /** 该轮 nonce(yield / cap 时 = 停下时的当前码;无 ⇒ null,**不留空白**) */
210
+ nonce: string | null;
211
+ /** 机器可读理由码(判定函数的 `reason`) */
212
+ reason: string;
213
+ /** 载体 dsh 会话 id(多载体轨迹可分辨) */
214
+ sessionId: string;
215
+ }
216
+ /** 文件形态(与 `wake-registry.json` / `cro-wake-log.json` 同款:`{version, entries}`) */
217
+ export interface UnattendedLog {
218
+ version: number;
219
+ entries: UnattendedLogEntry[];
220
+ }
221
+ /** 日志绝对路径 */
222
+ export declare function unattendedLogPath(root: string, dirName: string): string;
223
+ /** 读取流水(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本) */
224
+ export declare function loadUnattendedLog(root: string, dirName: string): {
225
+ log: UnattendedLog;
226
+ error: string | null;
227
+ };
228
+ /**
229
+ * 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
230
+ * 🔴 `at` 不可解析者**一律丢弃**并在返回值里报数(`dropped`)——**不静默吞掉**(同 `cro-log.ts` 纪律)。
231
+ */
232
+ export declare function pruneUnattendedLogEntries(entries: UnattendedLogEntry[], nowMs: number, windowMs?: number): {
233
+ kept: UnattendedLogEntry[];
234
+ pruned: number;
235
+ dropped: number;
236
+ };
237
+ /**
238
+ * 追加一条流水并按窗裁剪(**本模块的落盘入口;永不抛**)。
239
+ *
240
+ * 顺序 = 读 → 追加 → 裁剪 → **原子写**(tmp + rename)。任一步失败 ⇒ `{ok:false, error}`,**不抛**
241
+ * ——调用方只记一行 console 日志 ⇒ **代理机制的问题绝不影响 turn 收尾**。
242
+ */
243
+ export declare function appendUnattendedLog(root: string, dirName: string, entry: Omit<UnattendedLogEntry, 'at'>, nowMs?: number): {
244
+ ok: boolean;
245
+ error: string | null;
246
+ kept: number;
247
+ };