@shgroup/dsh-serenity-hooks 1.28.0 → 1.28.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.28.0",
3
+ "version": "1.28.1",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -3206,44 +3206,18 @@ window.__ModuleLoader__.load({
3206
3206
  return null;
3207
3207
  }
3208
3208
  const CONFIG_PATH = "/serenity/config";
3209
- /**
3210
- * 获取已有工作区列表(需求 1:白名单从已有工作区选择而非手输)。
3211
- * 走 DSH 原生 RPC `POST /api/workspace.list`(JSON RPC 形态)。
3212
- * 信封必须是完整 ClientRequest:`{ type: 'client-request', rpcId, method, payload }`
3213
- * (api/rpc.ts wire 契约——缺 type/method → clientRequestSchema 校验失败 → bad-request)。
3214
- * 返回 { path, title }[];失败返回空数组(面板显示"暂无可选工作区")。
3215
- */
3216
- async function fetchWorkspaces() {
3217
- try {
3218
- const res = await fetch("/api/workspace.list", {
3219
- method: "POST",
3220
- headers: { "content-type": "application/json" },
3221
- body: JSON.stringify({
3222
- type: "client-request",
3223
- rpcId: `ws-${Date.now().toString(36)}`,
3224
- method: "workspace.list",
3225
- payload: {}
3226
- })
3227
- });
3228
- if (!res.ok) return [];
3229
- const items = (await res.json())?.result?.value?.items;
3230
- if (!Array.isArray(items)) return [];
3231
- return items.filter((i) => typeof i.path === "string" && i.path !== "").map((i) => ({
3232
- path: i.path,
3233
- title: typeof i.title === "string" && i.title !== "" ? i.title : i.path
3234
- }));
3235
- } catch {
3236
- return [];
3237
- }
3238
- }
3239
- /** GET 配置(含账号列表;无 hash) */
3209
+ /** GET 配置(含账号列表 + knownWorkspaces;无 hash) */
3240
3210
  async function fetchConfig() {
3241
3211
  const res = await fetch(CONFIG_PATH, { headers: {
3242
3212
  accept: "application/json",
3243
3213
  "x-serenity-ui": "1"
3244
3214
  } });
3245
3215
  if (!res.ok) return null;
3246
- return (await res.json()).config ?? null;
3216
+ const body = await res.json();
3217
+ return {
3218
+ config: body.config ?? null,
3219
+ knownWorkspaces: body.knownWorkspaces ?? []
3220
+ };
3247
3221
  }
3248
3222
  /** PUT 配置(账号 patch + 工作区白名单 + persona 彩蛋 + 开放容器白名单)→ 返回保存后的 wire */
3249
3223
  async function saveConfig(patch) {
@@ -3303,9 +3277,10 @@ window.__ModuleLoader__.load({
3303
3277
  const [saved, setSaved] = (0, react.useState)(false);
3304
3278
  (0, react.useEffect)(() => {
3305
3279
  let alive = true;
3306
- Promise.all([fetchConfig(), fetchWorkspaces()]).then(([cfg, wss]) => {
3280
+ fetchConfig().then((resp) => {
3307
3281
  if (!alive) return;
3308
- setKnownWorkspaces(wss);
3282
+ const cfg = resp?.config ?? null;
3283
+ setKnownWorkspaces(resp?.knownWorkspaces ?? []);
3309
3284
  if (cfg) {
3310
3285
  setConfig(cfg);
3311
3286
  setHost(cfg.gateway.host);
@@ -3734,8 +3709,8 @@ Instruction-following style:
3734
3709
  let alive = true;
3735
3710
  fetchConfig().then((cfg) => {
3736
3711
  if (!alive || !cfg) return;
3737
- setMode(cfg.persona?.mode ?? "");
3738
- setOverrideText(cfg.persona?.overrideText ?? "");
3712
+ setMode(cfg.config?.persona?.mode ?? "");
3713
+ setOverrideText(cfg.config?.persona?.overrideText ?? "");
3739
3714
  setLoaded(true);
3740
3715
  });
3741
3716
  return () => {
@@ -3893,7 +3868,7 @@ Instruction-following style:
3893
3868
  try {
3894
3869
  const cfg = await fetchConfig();
3895
3870
  if (!alive) return;
3896
- setAllowed(cfg?.publicAsk?.allowed ?? []);
3871
+ setAllowed(cfg?.config?.publicAsk?.allowed ?? []);
3897
3872
  } catch {}
3898
3873
  })();
3899
3874
  return () => {
@@ -150,6 +150,19 @@ export interface AdvancedSettingsWire {
150
150
  }
151
151
  /** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
152
152
  export declare function toWire(settings: AdvancedSettings): AdvancedSettingsWire;
153
+ /**
154
+ * 已知工作区投影(v1.28.0 适配 0.1.2-rc.1 A2 方案 A′):
155
+ * rc.1 workspace.list unary 删除 → AccountsEditor 白名单下拉的数据源改走 gateway 自有
156
+ * /serenity/config 的 knownWorkspaces(host workspaceRegistry.list() 投影,白名单过滤)。
157
+ * 纯函数可单测。allowPrefixes 空 = 全部放行(向后兼容默认)。
158
+ */
159
+ export declare function projectKnownWorkspaces(workspaces: Array<{
160
+ path?: string;
161
+ title?: string;
162
+ }>, allowPrefixes: readonly string[]): Array<{
163
+ path: string;
164
+ title: string;
165
+ }>;
153
166
  /**
154
167
  * wire → 持久化(面板 PUT 用):
155
168
  * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
package/lib/index.d.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  import type { Context } from 'cordis';
15
15
  import z from '@deepseek-ai/schemastery';
16
16
  export declare const name = "dsh-serenity-hooks";
17
- /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
17
+ /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
18
+ * (v1.28.0 适配 0.1.2-rc.1:+ 'settings'——B4 settings 服务由 provider 插件加载后才有) */
18
19
  export declare const inject: string[];
19
20
  /** 插件配置(cordis.yml 提供;进程级) */
20
21
  export interface Config {
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CX48YNUL.js";
3
3
  import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-BTdBHyOQ.js";
4
- import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-DIf_Uenr.js";
4
+ import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-CBU6T2F_.js";
5
5
  import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-DFkRf9ou.js";
6
- import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-DnX9TJxt.js";
6
+ import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BfVxgCZy.js";
7
7
  import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-OUCuw0Ws.js";
8
8
  import z from "@deepseek-ai/schemastery";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -2301,7 +2301,8 @@ function waitIdle(ctx, agent) {
2301
2301
  }
2302
2302
  /** 读取会话最后一个 assistant/message 文本 */
2303
2303
  function lastAssistantText$1(agent) {
2304
- const events = agent.session.events;
2304
+ const s = agent.session;
2305
+ const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
2305
2306
  for (let i = events.length - 1; i >= 0; i--) {
2306
2307
  const e = events[i];
2307
2308
  if (e && e.type === "assistant/message") {
@@ -2567,6 +2568,24 @@ const SESSION_ACTIONS = [
2567
2568
  "summary",
2568
2569
  "hook-develop-guide"
2569
2570
  ];
2571
+ /**
2572
+ * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
2573
+ * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
2574
+ * `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
2575
+ * (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
2576
+ * 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
2577
+ * 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
2578
+ * (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
2579
+ * 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
2580
+ */
2581
+ function sessionEvents(session) {
2582
+ const s = session;
2583
+ if (!s) return [];
2584
+ if (typeof s.snapshotEvents === "function") try {
2585
+ return s.snapshotEvents() ?? [];
2586
+ } catch {}
2587
+ return s.events ?? [];
2588
+ }
2570
2589
  const SESSION_MD = "SESSION.md";
2571
2590
  const ARCHIVE_DIR_NAME = "_archived";
2572
2591
  const HEALTH_STALE_DAYS = 7;
@@ -3524,6 +3543,17 @@ const DEFAULT_COMPACTION_TOOLS = [
3524
3543
  */
3525
3544
  const DEFAULT_ANCHOR_MESSAGES = ["You are the operator agent of Serenity — a cognitive container governed by the Abstract Cognitive Container (ACC) protocol.\nWe operate under the Explicit Abstraction Principle (EAP): the functional value of a thought equals its external reconstructability — every output we produce must be explicit (E↑), reconstructable (R↓), and stable (S↑).\nThe personal pronoun is us/we.\nWe anchor first, then act: the abstract layer precedes the concrete.\nPlease simply reply \"acknowledge\" — no action needed.", "Before we proceed, align on how we work:\n1. We read before we write — every decision grounds in what already exists in the container.\n2. Every output records its reasoning (R↓): decisions carry reasons and alternatives.\n3. We never jump levels — abstract layer first, then specifics.\n4. Every artifact we create is a durable cognitive anchor for the work that follows.\n5. We keep the container's state coherent (SESSION.md) as we advance.\nPlease simply reply \"acknowledge\" — no action needed."];
3526
3545
  /**
3546
+ * 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
3547
+ * resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
3548
+ *
3549
+ * 0.1.2-rc.1 适配教训:裸读 `session.events`(经类型断言)恒 undefined →
3550
+ * `!undefined` 恒 true → 有历史也永不跳过 → 任何情况发消息都重插 first-anchor。
3551
+ * 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
3552
+ */
3553
+ function hasUserMessageHistory(session) {
3554
+ return sessionEvents(session).some((event) => event.type === "user/message");
3555
+ }
3556
+ /**
3527
3557
  * 构建一个 epoch 感知晋升跟踪器(纯逻辑,可单测)。
3528
3558
  * requiredSignals:晋升所需信号数(boundary 后累计;默认 1)。
3529
3559
  * 多轮锚定(v4):两轮递进锚定时 requiredSignals = 锚定轮数——
@@ -3552,8 +3582,8 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
3552
3582
  let boundary = -1;
3553
3583
  let signalCount = 0;
3554
3584
  let rounds = 0;
3555
- const events = session?.events;
3556
- if (Array.isArray(events)) for (const event of events) {
3585
+ const events = sessionEvents(session);
3586
+ if (events.length > 0) for (const event of events) {
3557
3587
  const e = event;
3558
3588
  const seq = typeof e.seq === "number" ? e.seq : 0;
3559
3589
  if (e.type === "compaction/end") {
@@ -3695,7 +3725,7 @@ function registerBootstrap(ctx) {
3695
3725
  const sid = typeof session?.id === "string" ? session.id : void 0;
3696
3726
  if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
3697
3727
  if (depth === 0) {
3698
- if (session?.events?.some((event) => event.type === "user/message")) return;
3728
+ if (hasUserMessageHistory(session)) return;
3699
3729
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
3700
3730
  if (message?.source?.kind === "plugin") return;
3701
3731
  const inbox = agent.inbox;
@@ -3834,8 +3864,8 @@ function parseAnchorMdPath(session) {
3834
3864
  try {
3835
3865
  const nodes = [...session.surface.nodes];
3836
3866
  if (nodes.length === 0) return null;
3837
- const events = session.events;
3838
- if (!Array.isArray(events)) return null;
3867
+ const events = sessionEvents(session);
3868
+ if (events.length === 0) return null;
3839
3869
  const event = events[nodes[0]];
3840
3870
  if (!event) return null;
3841
3871
  const message = deriveEventMessage(event);
@@ -3860,8 +3890,8 @@ function sessionNameFromMdPath(mdPath) {
3860
3890
  function resolveSessionMdPath(root, scope, session) {
3861
3891
  const candidates = [];
3862
3892
  candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
3863
- const events = session.events;
3864
- if (Array.isArray(events)) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
3893
+ const events = sessionEvents(session);
3894
+ if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
3865
3895
  candidates.push(parseAnchorMdPath(session));
3866
3896
  candidates.push(findLatestActiveSessionMd(root));
3867
3897
  for (const c of candidates) {
@@ -3953,8 +3983,9 @@ function performRebuild(session, pending, meter) {
3953
3983
  if (nodes.length === 0) return false;
3954
3984
  if (meter) {
3955
3985
  let shadowedTokenCount = 0;
3986
+ const events = sessionEvents(session);
3956
3987
  for (const seq of nodes) {
3957
- const event = session.events?.[seq];
3988
+ const event = events[seq];
3958
3989
  if (!event) continue;
3959
3990
  const message = deriveEventMessage(event);
3960
3991
  if (message) shadowedTokenCount += meter.estimateMessage(message);
@@ -4765,8 +4796,7 @@ function resolveTargetAgent(ctx, mdPath) {
4765
4796
  */
4766
4797
  function readSessionTitle(session) {
4767
4798
  try {
4768
- const events = session?.events;
4769
- if (!Array.isArray(events)) return null;
4799
+ const events = sessionEvents(session);
4770
4800
  for (let i = events.length - 1; i >= 0; i--) {
4771
4801
  const e = events[i];
4772
4802
  if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
@@ -5603,6 +5633,18 @@ function toWire(settings) {
5603
5633
  };
5604
5634
  }
5605
5635
  /**
5636
+ * 已知工作区投影(v1.28.0 适配 0.1.2-rc.1 A2 方案 A′):
5637
+ * rc.1 workspace.list unary 删除 → AccountsEditor 白名单下拉的数据源改走 gateway 自有
5638
+ * /serenity/config 的 knownWorkspaces(host workspaceRegistry.list() 投影,白名单过滤)。
5639
+ * 纯函数可单测。allowPrefixes 空 = 全部放行(向后兼容默认)。
5640
+ */
5641
+ function projectKnownWorkspaces(workspaces, allowPrefixes) {
5642
+ return workspaces.filter((w) => typeof w.path === "string" && w.path !== "").filter((w) => allowPrefixes.length === 0 || allowPrefixes.some((p) => w.path.startsWith(p))).map((w) => ({
5643
+ path: w.path,
5644
+ title: typeof w.title === "string" && w.title !== "" ? w.title : w.path
5645
+ }));
5646
+ }
5647
+ /**
5606
5648
  * wire → 持久化(面板 PUT 用):
5607
5649
  * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
5608
5650
  * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
@@ -6264,8 +6306,7 @@ function shouldAutoRestore(agent) {
6264
6306
  */
6265
6307
  function shouldRestoreActive(agent) {
6266
6308
  if (!shouldAutoRestore(agent)) return false;
6267
- const events = agent.session?.events;
6268
- return Array.isArray(events) && events.length > 0;
6309
+ return sessionEvents(agent.session).length > 0;
6269
6310
  }
6270
6311
  function registerContext(ctx, opts = {}) {
6271
6312
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
@@ -6280,7 +6321,7 @@ function registerContext(ctx, opts = {}) {
6280
6321
  const scope = agentScope(agent);
6281
6322
  if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
6282
6323
  if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
6283
- const info = parseSessionContextFromEvents(agent.session.events ?? []);
6324
+ const info = parseSessionContextFromEvents(sessionEvents(agent.session));
6284
6325
  if (info) {
6285
6326
  const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
6286
6327
  if (existsSync(abs)) {
@@ -6635,7 +6676,16 @@ function registerStatusApi(ctx, opts = {}) {
6635
6676
  return;
6636
6677
  }
6637
6678
  if (req.method === "GET") {
6638
- sendJson$1(res, 200, { config: toWire(readAdvancedSettings()) });
6679
+ const settings = readAdvancedSettings();
6680
+ let known = [];
6681
+ try {
6682
+ const registry = ctx.get?.("workspaceRegistry");
6683
+ known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
6684
+ } catch {}
6685
+ sendJson$1(res, 200, {
6686
+ config: toWire(settings),
6687
+ knownWorkspaces: known
6688
+ });
6639
6689
  return;
6640
6690
  }
6641
6691
  if (req.method === "PUT") {
@@ -6668,7 +6718,7 @@ function registerStatusApi(ctx, opts = {}) {
6668
6718
  workspace: url.searchParams.get("workspace") ?? void 0
6669
6719
  });
6670
6720
  const root = findSerenityRoot(workspace) ?? "";
6671
- const { discoverCccs } = await import("./skiff-debug-DIf_Uenr.js").then((n) => n.i);
6721
+ const { discoverCccs } = await import("./skiff-debug-CBU6T2F_.js").then((n) => n.i);
6672
6722
  sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
6673
6723
  } catch (err) {
6674
6724
  sendJson$1(res, 400, { error: err.message ?? String(err) });
@@ -6698,7 +6748,7 @@ function registerStatusApi(ctx, opts = {}) {
6698
6748
  return;
6699
6749
  }
6700
6750
  const settings = readAdvancedSettings();
6701
- const { readSimpleSettings } = await import("./settings-section-DnX9TJxt.js").then((n) => n.r);
6751
+ const { readSimpleSettings } = await import("./settings-section-BfVxgCZy.js").then((n) => n.r);
6702
6752
  const simple = readSimpleSettings();
6703
6753
  const allowed = settings.publicAsk.allowed;
6704
6754
  const port = simple.acpHttpPort ?? 3100;
@@ -7381,25 +7431,6 @@ const RANDOM_UUID_POLYFILL = `<script>
7381
7431
  /** 注入标记(幂等:已注入的 HTML 不重复注入) */
7382
7432
  const POLYFILL_MARKER = "data-sp-randomuuid-polyfill";
7383
7433
  /**
7384
- * workspace.list 响应过滤(v1.22 白名单):
7385
- * DSH client→server RPC 全部走 HTTP JSON(`POST /api/workspace.list`,WS 仅下行推送)。
7386
- * 白名单(workspaces 路径前缀)非空 → 只保留匹配前缀的 items;
7387
- * 空 = 全部允许(默认,向后兼容)。
7388
- */
7389
- function filterWorkspaceList(body, allowPrefixes) {
7390
- if (allowPrefixes.length === 0) return body;
7391
- try {
7392
- const parsed = JSON.parse(body);
7393
- const value = parsed?.result?.value;
7394
- if (parsed?.result?.ok !== true || !value || !Array.isArray(value.items)) return body;
7395
- const keep = (path) => typeof path === "string" && allowPrefixes.some((p) => path.startsWith(p));
7396
- value.items = value.items.filter((item) => keep(item.path));
7397
- return JSON.stringify(parsed);
7398
- } catch {
7399
- return body;
7400
- }
7401
- }
7402
- /**
7403
7434
  * 校验 workspace.create 请求路径是否在白名单内(v1.22):
7404
7435
  * 白名单非空且路径不匹配 → 拒绝(由调用方构造 403 RPC 响应)。
7405
7436
  */
@@ -7485,8 +7516,6 @@ function startGateway(config, getAccounts) {
7485
7516
  * @param bodyOverride - workspace.create 已读 body 时的重放(白名单检查后转发)
7486
7517
  */
7487
7518
  const proxy = (req, res, bodyOverride) => {
7488
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
7489
- const method = req.method === "POST" && url.pathname.startsWith("/api/") ? url.pathname.slice(5) : null;
7490
7519
  const headers = buildProxyHeaders(req.headers, mainPort, bodyOverride);
7491
7520
  const target = request({
7492
7521
  host: "127.0.0.1",
@@ -7516,25 +7545,6 @@ function startGateway(config, getAccounts) {
7516
7545
  });
7517
7546
  return;
7518
7547
  }
7519
- if (method === "workspace.list" && status === 200 && ct.includes("application/json")) {
7520
- const chunks = [];
7521
- upstream.on("data", (c) => chunks.push(c));
7522
- upstream.on("end", () => {
7523
- const transformed = filterWorkspaceList(Buffer.concat(chunks).toString("utf-8"), allowWorkspaces);
7524
- const out = {
7525
- ...upstream.headers,
7526
- "content-length": Buffer.byteLength(transformed)
7527
- };
7528
- res.writeHead(status, out);
7529
- res.end(transformed);
7530
- });
7531
- upstream.on("error", () => {
7532
- try {
7533
- res.destroy();
7534
- } catch {}
7535
- });
7536
- return;
7537
- }
7538
7548
  res.writeHead(status, upstream.headers);
7539
7549
  upstream.pipe(res);
7540
7550
  upstream.on("error", () => {
@@ -8055,7 +8065,7 @@ function isExternalFaceSession(sessionId) {
8055
8065
  * 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
8056
8066
  * (v1.26.8 状态机,弃正则——同 v1.27.1 微信桥回复链路)。 */
8057
8067
  function lastAssistantText(agent) {
8058
- const events = agent.session.events ?? [];
8068
+ const events = sessionEvents(agent.session);
8059
8069
  for (let i = events.length - 1; i >= 0; i--) {
8060
8070
  const e = events[i];
8061
8071
  if (e && e.type === "assistant/message") {
@@ -9332,7 +9342,8 @@ function registerWeixinBridge(ctx) {
9332
9342
  //#endregion
9333
9343
  //#region src/index.ts
9334
9344
  const name = "dsh-serenity-hooks";
9335
- /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
9345
+ /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
9346
+ * (v1.28.0 适配 0.1.2-rc.1:+ 'settings'——B4 settings 服务由 provider 插件加载后才有) */
9336
9347
  const inject = [
9337
9348
  "tools",
9338
9349
  "webServer",
@@ -9342,7 +9353,8 @@ const inject = [
9342
9353
  "agentLoop",
9343
9354
  "agents",
9344
9355
  "systemPrompt",
9345
- "sessionProjections"
9356
+ "sessionProjections",
9357
+ "settings"
9346
9358
  ];
9347
9359
  const Config = z.object({
9348
9360
  serenityConfigPaths: z.array(z.string()).default([...DEFAULT_SERENITY_CONFIG_PATHS]),
@@ -50,6 +50,15 @@ export declare const DEFAULT_COMPACTION_TOOLS: string[];
50
50
  * 文本 = 原 home-serenity CCC 配置(anchorMessages),随零配置化固化进代码。
51
51
  */
52
52
  export declare const DEFAULT_ANCHOR_MESSAGES: string[];
53
+ /**
54
+ * 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
55
+ * resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
56
+ *
57
+ * 0.1.2-rc.1 适配教训:裸读 `session.events`(经类型断言)恒 undefined →
58
+ * `!undefined` 恒 true → 有历史也永不跳过 → 任何情况发消息都重插 first-anchor。
59
+ * 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
60
+ */
61
+ export declare function hasUserMessageHistory(session: unknown): boolean;
53
62
  export interface PromotionStatus {
54
63
  /** 最后一次 compaction/end 的 seq(-1 = 未压缩过) */
55
64
  boundary: number;
@@ -12,6 +12,17 @@
12
12
  import type { JsonValue } from './json.js';
13
13
  export type SessionAction = 'list' | 'show' | 'create' | 'use' | 'close' | 'health' | 'qa' | 'archive' | 'summary' | 'hook-develop-guide';
14
14
  export declare const SESSION_ACTIONS: readonly SessionAction[];
15
+ /**
16
+ * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
17
+ * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
18
+ * `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
19
+ * (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
20
+ * 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
21
+ * 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
22
+ * (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
23
+ * 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
24
+ */
25
+ export declare function sessionEvents<T = unknown>(session: unknown): readonly T[];
15
26
  export declare function sessionsRoot(root: string): string;
16
27
  interface SessionStatus {
17
28
  hasSessionMd: boolean;
@@ -0,0 +1,91 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ //#region src/settings-section.ts
4
+ var settings_section_exports = /* @__PURE__ */ __exportAll({
5
+ SERENITY_SETTINGS_NS: () => SERENITY_SETTINGS_NS,
6
+ defaultSimpleSettings: () => defaultSimpleSettings,
7
+ entryDefaults: () => entryDefaults,
8
+ readSimpleSettings: () => readSimpleSettings,
9
+ registerSettingsSection: () => registerSettingsSection,
10
+ simpleSettingsSchema: () => simpleSettingsSchema
11
+ });
12
+ /** 简单配置命名空间(dsh 设置面板的 section id / settings.yaml section) */
13
+ const SERENITY_SETTINGS_NS = "serenity-hooks";
14
+ /** schemastery schema(与 DSH 各插件 Config 同款) */
15
+ const simpleSettingsSchema = z.object({
16
+ gatewayEnabled: z.boolean().default(false),
17
+ rebuildEnabled: z.boolean().default(true),
18
+ rebuildThresholdK: z.number().min(50).max(4e3).default(400),
19
+ skiffEnabled: z.boolean().default(false),
20
+ skiffDebugPort: z.number().min(1024).max(65535).default(3099),
21
+ acpEnabled: z.boolean().default(false),
22
+ acpHttpPort: z.number().min(1024).max(65535).default(3100),
23
+ publicAskEnabled: z.boolean().default(false),
24
+ autopilotEnabled: z.boolean().default(false)
25
+ });
26
+ /** 从插件 Config 提取 entry 默认(settings base 层) */
27
+ function entryDefaults(config) {
28
+ return {
29
+ gatewayEnabled: config.gateway?.enabled ?? false,
30
+ rebuildEnabled: config.rebuild?.enabled ?? true,
31
+ rebuildThresholdK: config.rebuild?.thresholdK ?? 400,
32
+ skiffEnabled: config.skiff?.enabled ?? false,
33
+ skiffDebugPort: config.skiff?.debugPort ?? 3099,
34
+ acpEnabled: config.acp?.enabled ?? false,
35
+ acpHttpPort: config.acp?.httpPort ?? 3100,
36
+ publicAskEnabled: config.publicAsk?.enabled ?? false,
37
+ autopilotEnabled: false
38
+ };
39
+ }
40
+ /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
41
+ let simpleSource = null;
42
+ /** 进程级默认(无 settings 服务时的兜底) */
43
+ function defaultSimpleSettings() {
44
+ return {
45
+ gatewayEnabled: false,
46
+ rebuildEnabled: true,
47
+ rebuildThresholdK: 400,
48
+ skiffEnabled: false,
49
+ skiffDebugPort: 3099,
50
+ acpEnabled: false,
51
+ acpHttpPort: 3100,
52
+ publicAskEnabled: false,
53
+ autopilotEnabled: false
54
+ };
55
+ }
56
+ /**
57
+ * 读取当前简单配置(settings 解析值;无 provider/未注册 → entry 默认)。
58
+ * 各功能(gateway/rebuild/naming)启动与运行时判断开关都经此函数。
59
+ */
60
+ function readSimpleSettings() {
61
+ return simpleSource ? simpleSource() : defaultSimpleSettings();
62
+ }
63
+ /**
64
+ * 注册简单配置到 DSH settings(零改 DSH;settings.yaml 持久化 + 原生面板渲染)。
65
+ * 注:插件 Config(cordis.yml 组合层)作为 base;用户文档层叠加其上。
66
+ * 运行时读取简单配置统一经 `readSimpleSettings()`。
67
+ * v1.28.0 适配 0.1.2-rc.1(B4):`installSettingsSection`/`settingsNamespace` 便捷函数在
68
+ * rc.1 消失 → 改 `SettingsProvider.installSection(owner, ns, schema, entry, hooks)`
69
+ * (owner = consumer 插件 ctx;ns 直接字符串)。
70
+ */
71
+ function registerSettingsSection(ctx, config) {
72
+ const hooks = {
73
+ setSource: (get) => {
74
+ simpleSource = get;
75
+ },
76
+ onChange: () => {
77
+ try {
78
+ ctx.emit?.("serenity/settings-changed");
79
+ } catch {}
80
+ }
81
+ };
82
+ const settingsAny = ctx.settings;
83
+ if (settingsAny) {
84
+ settingsAny.installSection.call(settingsAny, ctx, SERENITY_SETTINGS_NS, simpleSettingsSchema, entryDefaults(config), hooks);
85
+ return;
86
+ }
87
+ hooks.setSource(() => ({}));
88
+ hooks.onChange();
89
+ }
90
+ //#endregion
91
+ export { registerSettingsSection as n, settings_section_exports as r, readSimpleSettings as t };
@@ -4,8 +4,9 @@
4
4
  * 分层决策(S142):**简单配置(开关/阈值)→ dsh 原生设置面板**;
5
5
  * **复杂配置(账号列表)→ 宁静号高级面板**(localstore + /serenity/config)。
6
6
  *
7
- * 本模块承载简单配置层:`installSettingsSection(ctx, settingsNamespace('serenity-hooks'), schema, entry)`
7
+ * 本模块承载简单配置层:`SettingsProvider.installSection(ctx, 'serenity-hooks', schema, entry)`
8
8
  * 注册 `serenity-hooks` namespace——三功能总开关 + F2 阈值。
9
+ * (v1.28.0 适配 0.1.2-rc.1:installSettingsSection/settingsNamespace 便捷函数消失 → 方法调用)
9
10
  *
10
11
  * 运行时降级守卫(版本鲁棒性):旧 RC(staging 架构)api-proxy 有
11
12
  * `WEB_SETTINGS_NAMESPACES` 静态白名单,第三方 ns 会收到 settings-not-exposed
@@ -107,5 +108,8 @@ export declare function __setSimpleSourceForTest(source: (() => SerenitySimpleSe
107
108
  * 注册简单配置到 DSH settings(零改 DSH;settings.yaml 持久化 + 原生面板渲染)。
108
109
  * 注:插件 Config(cordis.yml 组合层)作为 base;用户文档层叠加其上。
109
110
  * 运行时读取简单配置统一经 `readSimpleSettings()`。
111
+ * v1.28.0 适配 0.1.2-rc.1(B4):`installSettingsSection`/`settingsNamespace` 便捷函数在
112
+ * rc.1 消失 → 改 `SettingsProvider.installSection(owner, ns, schema, entry, hooks)`
113
+ * (owner = consumer 插件 ctx;ns 直接字符串)。
110
114
  */
111
115
  export declare function registerSettingsSection(ctx: Context, config: SimpleConfigFragment): void;
@@ -396,9 +396,20 @@ function waitIdle(ctx, agent) {
396
396
  });
397
397
  });
398
398
  }
399
+ /**
400
+ * 会话事件读取(v1.28.0 适配 0.1.2-rc.1):rc.1 Session 移除 `.events` 属性 → `snapshotEvents()` 方法。
401
+ * 兼容双形态(运行时 rc.1 snapshotEvents;测试替身 events)——同一函数两处语义,测试注入零侵入。
402
+ * 注:与 session-ops.ts 导出的共享 `sessionEvents` 同构(v1.28.1 收敛基准)。本模块保留本地副本
403
+ * 以维持独立 import 面(零 DSH 依赖测试替身形态一致);若未来重构可统一 import 共享版。
404
+ */
405
+ function sessionEvents(session) {
406
+ const s = session;
407
+ if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
408
+ return s.events ?? [];
409
+ }
399
410
  /** 读会话最后一个 assistant/message 文本(handyman 同款) */
400
411
  function lastAssistantText(agent) {
401
- const events = agent.session.events;
412
+ const events = sessionEvents(agent.session);
402
413
  for (let i = events.length - 1; i >= 0; i--) {
403
414
  const e = events[i];
404
415
  if (e && e.type === "assistant/message") {
@@ -462,7 +473,7 @@ function truncate(s, n) {
462
473
  * 公开问答页 / ACP JSON-RPC 不返回 trajectory,传 false 跳过计算;3099 调试页默认 true 保留)
463
474
  */
464
475
  async function askSkiff(ctx, agent, question, eventsStart, options) {
465
- const before = eventsStart === void 0 ? agent.session.events.length : eventsStart;
476
+ const before = eventsStart === void 0 ? sessionEvents(agent.session).length : eventsStart;
466
477
  agent.followup(createUserMessage({
467
478
  content: [{
468
479
  type: "text",
@@ -472,7 +483,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
472
483
  }));
473
484
  await waitIdle(ctx, agent);
474
485
  const answer = lastAssistantText(agent);
475
- const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(agent.session.events.slice(before));
486
+ const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
476
487
  return {
477
488
  answer,
478
489
  sessionId: String(agent.session.id ?? ""),