@shgroup/dsh-serenity-hooks 1.28.0 → 1.28.2

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/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";
@@ -18,6 +18,7 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
18
18
  import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
19
19
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
20
20
  import { createServer, request } from "node:http";
21
+ import * as zlib from "node:zlib";
21
22
  //#region src/fs-ops.ts
22
23
  /**
23
24
  * fs-ops.ts — cc_fs 纯操作层(零 DSH 依赖,可独立单测)
@@ -2301,7 +2302,8 @@ function waitIdle(ctx, agent) {
2301
2302
  }
2302
2303
  /** 读取会话最后一个 assistant/message 文本 */
2303
2304
  function lastAssistantText$1(agent) {
2304
- const events = agent.session.events;
2305
+ const s = agent.session;
2306
+ const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
2305
2307
  for (let i = events.length - 1; i >= 0; i--) {
2306
2308
  const e = events[i];
2307
2309
  if (e && e.type === "assistant/message") {
@@ -2567,6 +2569,24 @@ const SESSION_ACTIONS = [
2567
2569
  "summary",
2568
2570
  "hook-develop-guide"
2569
2571
  ];
2572
+ /**
2573
+ * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
2574
+ * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
2575
+ * `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
2576
+ * (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
2577
+ * 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
2578
+ * 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
2579
+ * (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
2580
+ * 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
2581
+ */
2582
+ function sessionEvents(session) {
2583
+ const s = session;
2584
+ if (!s) return [];
2585
+ if (typeof s.snapshotEvents === "function") try {
2586
+ return s.snapshotEvents() ?? [];
2587
+ } catch {}
2588
+ return s.events ?? [];
2589
+ }
2570
2590
  const SESSION_MD = "SESSION.md";
2571
2591
  const ARCHIVE_DIR_NAME = "_archived";
2572
2592
  const HEALTH_STALE_DAYS = 7;
@@ -3524,6 +3544,17 @@ const DEFAULT_COMPACTION_TOOLS = [
3524
3544
  */
3525
3545
  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
3546
  /**
3547
+ * 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
3548
+ * resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
3549
+ *
3550
+ * 0.1.2-rc.1 适配教训:裸读 `session.events`(经类型断言)恒 undefined →
3551
+ * `!undefined` 恒 true → 有历史也永不跳过 → 任何情况发消息都重插 first-anchor。
3552
+ * 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
3553
+ */
3554
+ function hasUserMessageHistory(session) {
3555
+ return sessionEvents(session).some((event) => event.type === "user/message");
3556
+ }
3557
+ /**
3527
3558
  * 构建一个 epoch 感知晋升跟踪器(纯逻辑,可单测)。
3528
3559
  * requiredSignals:晋升所需信号数(boundary 后累计;默认 1)。
3529
3560
  * 多轮锚定(v4):两轮递进锚定时 requiredSignals = 锚定轮数——
@@ -3552,8 +3583,8 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
3552
3583
  let boundary = -1;
3553
3584
  let signalCount = 0;
3554
3585
  let rounds = 0;
3555
- const events = session?.events;
3556
- if (Array.isArray(events)) for (const event of events) {
3586
+ const events = sessionEvents(session);
3587
+ if (events.length > 0) for (const event of events) {
3557
3588
  const e = event;
3558
3589
  const seq = typeof e.seq === "number" ? e.seq : 0;
3559
3590
  if (e.type === "compaction/end") {
@@ -3695,7 +3726,7 @@ function registerBootstrap(ctx) {
3695
3726
  const sid = typeof session?.id === "string" ? session.id : void 0;
3696
3727
  if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
3697
3728
  if (depth === 0) {
3698
- if (session?.events?.some((event) => event.type === "user/message")) return;
3729
+ if (hasUserMessageHistory(session)) return;
3699
3730
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
3700
3731
  if (message?.source?.kind === "plugin") return;
3701
3732
  const inbox = agent.inbox;
@@ -3834,8 +3865,8 @@ function parseAnchorMdPath(session) {
3834
3865
  try {
3835
3866
  const nodes = [...session.surface.nodes];
3836
3867
  if (nodes.length === 0) return null;
3837
- const events = session.events;
3838
- if (!Array.isArray(events)) return null;
3868
+ const events = sessionEvents(session);
3869
+ if (events.length === 0) return null;
3839
3870
  const event = events[nodes[0]];
3840
3871
  if (!event) return null;
3841
3872
  const message = deriveEventMessage(event);
@@ -3860,8 +3891,8 @@ function sessionNameFromMdPath(mdPath) {
3860
3891
  function resolveSessionMdPath(root, scope, session) {
3861
3892
  const candidates = [];
3862
3893
  candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
3863
- const events = session.events;
3864
- if (Array.isArray(events)) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
3894
+ const events = sessionEvents(session);
3895
+ if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
3865
3896
  candidates.push(parseAnchorMdPath(session));
3866
3897
  candidates.push(findLatestActiveSessionMd(root));
3867
3898
  for (const c of candidates) {
@@ -3953,8 +3984,9 @@ function performRebuild(session, pending, meter) {
3953
3984
  if (nodes.length === 0) return false;
3954
3985
  if (meter) {
3955
3986
  let shadowedTokenCount = 0;
3987
+ const events = sessionEvents(session);
3956
3988
  for (const seq of nodes) {
3957
- const event = session.events?.[seq];
3989
+ const event = events[seq];
3958
3990
  if (!event) continue;
3959
3991
  const message = deriveEventMessage(event);
3960
3992
  if (message) shadowedTokenCount += meter.estimateMessage(message);
@@ -4765,8 +4797,7 @@ function resolveTargetAgent(ctx, mdPath) {
4765
4797
  */
4766
4798
  function readSessionTitle(session) {
4767
4799
  try {
4768
- const events = session?.events;
4769
- if (!Array.isArray(events)) return null;
4800
+ const events = sessionEvents(session);
4770
4801
  for (let i = events.length - 1; i >= 0; i--) {
4771
4802
  const e = events[i];
4772
4803
  if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
@@ -5603,6 +5634,18 @@ function toWire(settings) {
5603
5634
  };
5604
5635
  }
5605
5636
  /**
5637
+ * 已知工作区投影(v1.28.0 适配 0.1.2-rc.1 A2 方案 A′):
5638
+ * rc.1 workspace.list unary 删除 → AccountsEditor 白名单下拉的数据源改走 gateway 自有
5639
+ * /serenity/config 的 knownWorkspaces(host workspaceRegistry.list() 投影,白名单过滤)。
5640
+ * 纯函数可单测。allowPrefixes 空 = 全部放行(向后兼容默认)。
5641
+ */
5642
+ function projectKnownWorkspaces(workspaces, allowPrefixes) {
5643
+ return workspaces.filter((w) => typeof w.path === "string" && w.path !== "").filter((w) => allowPrefixes.length === 0 || allowPrefixes.some((p) => w.path.startsWith(p))).map((w) => ({
5644
+ path: w.path,
5645
+ title: typeof w.title === "string" && w.title !== "" ? w.title : w.path
5646
+ }));
5647
+ }
5648
+ /**
5606
5649
  * wire → 持久化(面板 PUT 用):
5607
5650
  * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
5608
5651
  * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
@@ -6264,8 +6307,7 @@ function shouldAutoRestore(agent) {
6264
6307
  */
6265
6308
  function shouldRestoreActive(agent) {
6266
6309
  if (!shouldAutoRestore(agent)) return false;
6267
- const events = agent.session?.events;
6268
- return Array.isArray(events) && events.length > 0;
6310
+ return sessionEvents(agent.session).length > 0;
6269
6311
  }
6270
6312
  function registerContext(ctx, opts = {}) {
6271
6313
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
@@ -6280,7 +6322,7 @@ function registerContext(ctx, opts = {}) {
6280
6322
  const scope = agentScope(agent);
6281
6323
  if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
6282
6324
  if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
6283
- const info = parseSessionContextFromEvents(agent.session.events ?? []);
6325
+ const info = parseSessionContextFromEvents(sessionEvents(agent.session));
6284
6326
  if (info) {
6285
6327
  const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
6286
6328
  if (existsSync(abs)) {
@@ -6635,7 +6677,16 @@ function registerStatusApi(ctx, opts = {}) {
6635
6677
  return;
6636
6678
  }
6637
6679
  if (req.method === "GET") {
6638
- sendJson$1(res, 200, { config: toWire(readAdvancedSettings()) });
6680
+ const settings = readAdvancedSettings();
6681
+ let known = [];
6682
+ try {
6683
+ const registry = ctx.get?.("workspaceRegistry");
6684
+ known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
6685
+ } catch {}
6686
+ sendJson$1(res, 200, {
6687
+ config: toWire(settings),
6688
+ knownWorkspaces: known
6689
+ });
6639
6690
  return;
6640
6691
  }
6641
6692
  if (req.method === "PUT") {
@@ -6668,7 +6719,7 @@ function registerStatusApi(ctx, opts = {}) {
6668
6719
  workspace: url.searchParams.get("workspace") ?? void 0
6669
6720
  });
6670
6721
  const root = findSerenityRoot(workspace) ?? "";
6671
- const { discoverCccs } = await import("./skiff-debug-DIf_Uenr.js").then((n) => n.i);
6722
+ const { discoverCccs } = await import("./skiff-debug-CBU6T2F_.js").then((n) => n.i);
6672
6723
  sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
6673
6724
  } catch (err) {
6674
6725
  sendJson$1(res, 400, { error: err.message ?? String(err) });
@@ -6698,7 +6749,7 @@ function registerStatusApi(ctx, opts = {}) {
6698
6749
  return;
6699
6750
  }
6700
6751
  const settings = readAdvancedSettings();
6701
- const { readSimpleSettings } = await import("./settings-section-DnX9TJxt.js").then((n) => n.r);
6752
+ const { readSimpleSettings } = await import("./settings-section-BfVxgCZy.js").then((n) => n.r);
6702
6753
  const simple = readSimpleSettings();
6703
6754
  const allowed = settings.publicAsk.allowed;
6704
6755
  const port = simple.acpHttpPort ?? 3100;
@@ -7381,25 +7432,6 @@ const RANDOM_UUID_POLYFILL = `<script>
7381
7432
  /** 注入标记(幂等:已注入的 HTML 不重复注入) */
7382
7433
  const POLYFILL_MARKER = "data-sp-randomuuid-polyfill";
7383
7434
  /**
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
7435
  * 校验 workspace.create 请求路径是否在白名单内(v1.22):
7404
7436
  * 白名单非空且路径不匹配 → 拒绝(由调用方构造 403 RPC 响应)。
7405
7437
  */
@@ -7422,7 +7454,9 @@ function workspaceDenyResponse(rpcId) {
7422
7454
  }
7423
7455
  });
7424
7456
  }
7425
- /** 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过) */
7457
+ /**
7458
+ * 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过)
7459
+ */
7426
7460
  function injectPolyfillHtml(html) {
7427
7461
  if (html.includes(POLYFILL_MARKER)) return html;
7428
7462
  const head = RANDOM_UUID_POLYFILL.replace("<script>", `<script ${POLYFILL_MARKER}="1">`);
@@ -7430,6 +7464,37 @@ function injectPolyfillHtml(html) {
7430
7464
  return `${head}\n${html}`;
7431
7465
  }
7432
7466
  /**
7467
+ * 反代 HTML 响应注入变换(v1.28.2 修复,S142 白屏根因):
7468
+ * 上游按 Accept-Encoding 可能返回 gzip/br/deflate 压缩的 HTML——注入必须在**明文**上进行,
7469
+ * 把压缩字节 toString 当文本注入会破坏压缩流(找不到 </head> → polyfill 前置 → HTML 损坏
7470
+ * → 浏览器白屏,用户实测 https://dsh.notfoundhome.cc 登录后白屏)。
7471
+ *
7472
+ * @param raw - 上游响应完整 body(可能压缩)
7473
+ * @param upstreamHeaders - 上游响应头(node IncomingHttpHeaders 形态)
7474
+ * @returns { body, headers } 注入后的明文 body + 修正后的响应头(去 content-encoding/transfer-encoding、
7475
+ * 重算 content-length);解压失败返回 null(调用方应原样透传避免破坏)
7476
+ */
7477
+ function transformHtmlForProxy(raw, upstreamHeaders) {
7478
+ let body = raw;
7479
+ const enc = String(upstreamHeaders["content-encoding"] ?? "").toLowerCase().trim();
7480
+ const outHeaders = { ...upstreamHeaders };
7481
+ if (enc === "gzip" || enc === "x-gzip" || enc === "deflate" || enc === "br") try {
7482
+ if (enc === "br") body = zlib.brotliDecompressSync(body);
7483
+ else if (enc === "deflate") body = zlib.inflateSync(body);
7484
+ else body = zlib.gunzipSync(body);
7485
+ delete outHeaders["content-encoding"];
7486
+ } catch {
7487
+ return null;
7488
+ }
7489
+ const transformed = injectPolyfillHtml(body.toString("utf-8"));
7490
+ outHeaders["content-length"] = Buffer.byteLength(transformed);
7491
+ delete outHeaders["transfer-encoding"];
7492
+ return {
7493
+ body: transformed,
7494
+ headers: outHeaders
7495
+ };
7496
+ }
7497
+ /**
7433
7498
  * 反代请求头构造(v1.22.1 信任栅栏修复,纯逻辑可测):
7434
7499
  * DSH isTrustedApiRequest 要求 Origin.host === Host.host——Host 改写为 loopback 后
7435
7500
  * Origin 必须同步改写(浏览器 POST 必带 Origin,透传外部地址 → 403)。
@@ -7444,6 +7509,69 @@ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
7444
7509
  return headers;
7445
7510
  }
7446
7511
  //#endregion
7512
+ //#region src/gateway-dsh-auth.ts
7513
+ /** 从一组 set-cookie 头提取 `dsh-auth-*` cookie(browser-auth sessionCookie 首段) */
7514
+ function pickDshCookie(setCookieHeader) {
7515
+ const entries = Array.isArray(setCookieHeader) ? setCookieHeader : setCookieHeader === void 0 ? [] : [setCookieHeader];
7516
+ for (const entry of entries) {
7517
+ const first = entry.split(";", 1)[0].trim();
7518
+ if (first.startsWith("dsh-auth-") && first.includes("=")) return first;
7519
+ }
7520
+ }
7521
+ /**
7522
+ * 把 dsh browser cookie 合并进现有 Cookie 头(保持外部既有 cookie——serenity_session 等)。
7523
+ * 现有头可为 string / string[](node 头形态);无现有 → 只返回 dsh cookie。
7524
+ */
7525
+ function mergeCookieHeader(existing, dshCookie) {
7526
+ const existingStr = Array.isArray(existing) ? existing.join("; ") : typeof existing === "string" && existing !== "" ? existing : void 0;
7527
+ return existingStr !== void 0 ? `${existingStr}; ${dshCookie}` : dshCookie;
7528
+ }
7529
+ /**
7530
+ * 内存换取 authority 绑定的 dsh browser cookie(S142 拍板方案核心)。
7531
+ * 用官方通道在进程内完成 token→cookie 交换:构造一个对 `http://<authority>/?token=…`
7532
+ * 的 index 请求 → connection.authorizeIndex 校验 token → 写 set-cookie → 提取 cookie。
7533
+ * @returns cookie 头值(如 `dsh-auth-xxx=v1.…`);authority/connection 异常 → undefined
7534
+ */
7535
+ function exchangeDshCookie(connection, authority) {
7536
+ try {
7537
+ const url = new URL(connection.authenticatedUrl(`http://${authority}`));
7538
+ if (url.searchParams.get("token") === null) return void 0;
7539
+ const headers = { host: authority };
7540
+ const request = {
7541
+ url: `${url.pathname}${url.search}`,
7542
+ method: "GET",
7543
+ headers
7544
+ };
7545
+ let captured;
7546
+ const response = {
7547
+ writeHead(_status, headers) {
7548
+ if (headers !== void 0) captured = headers["set-cookie"];
7549
+ return response;
7550
+ },
7551
+ end: () => response
7552
+ };
7553
+ connection.authorizeIndex(request, response);
7554
+ return pickDshCookie(captured);
7555
+ } catch {
7556
+ return;
7557
+ }
7558
+ }
7559
+ /**
7560
+ * 构建一个带内存缓存的 dsh cookie 提供者(S142 拍板:内存缓存,不落盘)。
7561
+ * 惰性换取:首次调用才向 connection 换;失败返回 undefined(不缓存失败——下次重试)。
7562
+ * 调用方可周期性失效缓存重换(cookie 30 天有效;进程重启 token 仍在 → 换一次长期可用)。
7563
+ */
7564
+ function createDshCookieProvider(connection, authority) {
7565
+ let cached;
7566
+ return () => {
7567
+ if (connection === void 0) return void 0;
7568
+ if (cached !== void 0) return cached;
7569
+ const cookie = exchangeDshCookie(connection, authority);
7570
+ if (cookie !== void 0) cached = cookie;
7571
+ return cookie;
7572
+ };
7573
+ }
7574
+ //#endregion
7447
7575
  //#region src/gateway.ts
7448
7576
  /** 读取请求体(≤ maxBytes;超限 reject) */
7449
7577
  function readBody$1(req, maxBytes) {
@@ -7466,7 +7594,7 @@ function readBody$1(req, maxBytes) {
7466
7594
  * @param config - 监听/代理配置。
7467
7595
  * @param getAccounts - 运行时读取账号列表(plugin 全局文件;gateway.enabled 开关在调用方判)。
7468
7596
  */
7469
- function startGateway(config, getAccounts) {
7597
+ function startGateway(config, getAccounts, getDshCookie) {
7470
7598
  const { host, port, cookieName, mainPort, loginDelayMs } = config;
7471
7599
  const allowWorkspaces = config.allowWorkspaces ?? [];
7472
7600
  const cookieSecure = config.cookieSecure === true;
@@ -7475,6 +7603,18 @@ function startGateway(config, getAccounts) {
7475
7603
  /** 校验请求 Cookie 是否含有效 token(v1.22.4:滑动过期 + 返回会话供审计) */
7476
7604
  const authed = (req) => validateToken(cookieValue(req.headers.cookie, cookieName));
7477
7605
  /**
7606
+ * 构造上游请求头(buildProxyHeaders 基准 + dsh browser cookie 注入)。
7607
+ * v1.28.2(0.1.2-rc.1 BrowserAuth 适配):主端口 /api + index 需 dsh browser cookie。
7608
+ * 反代 Host 已改写 loopback(过信任栅栏)→ 只缺 cookie → 注入内存换取 cookie
7609
+ *(createDshCookieProvider 官方通道,零落盘)。无 cookie(旧 dsh/换取失败)→ 不注入向后兼容。
7610
+ */
7611
+ const upstreamHeaders = (req, bodyOverride) => {
7612
+ const headers = buildProxyHeaders(req.headers, mainPort, bodyOverride);
7613
+ const dshCookie = getDshCookie?.();
7614
+ if (dshCookie !== void 0) headers.cookie = mergeCookieHeader(headers.cookie, dshCookie);
7615
+ return headers;
7616
+ };
7617
+ /**
7478
7618
  * 反向代理:改写 Host + Origin 头 → 主端口(loopback 过信任栅栏)。
7479
7619
  * v1.22.1 修复:DSH 信任栅栏(api-request-trust.ts isTrustedApiRequest)要求
7480
7620
  * **Origin.host === Host.host**——仅改写 Host(127.0.0.1:3080)而 Origin 透传
@@ -7485,9 +7625,7 @@ function startGateway(config, getAccounts) {
7485
7625
  * @param bodyOverride - workspace.create 已读 body 时的重放(白名单检查后转发)
7486
7626
  */
7487
7627
  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
- const headers = buildProxyHeaders(req.headers, mainPort, bodyOverride);
7628
+ const headers = upstreamHeaders(req, bodyOverride);
7491
7629
  const target = request({
7492
7630
  host: "127.0.0.1",
7493
7631
  port: mainPort,
@@ -7501,32 +7639,15 @@ function startGateway(config, getAccounts) {
7501
7639
  const chunks = [];
7502
7640
  upstream.on("data", (c) => chunks.push(c));
7503
7641
  upstream.on("end", () => {
7504
- const transformed = injectPolyfillHtml(Buffer.concat(chunks).toString("utf-8"));
7505
- const out = {
7506
- ...upstream.headers,
7507
- "content-length": Buffer.byteLength(transformed)
7508
- };
7509
- res.writeHead(status, out);
7510
- res.end(transformed);
7511
- });
7512
- upstream.on("error", () => {
7513
- try {
7514
- res.destroy();
7515
- } catch {}
7516
- });
7517
- return;
7518
- }
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);
7642
+ let raw = Buffer.concat(chunks);
7643
+ const transformedRes = transformHtmlForProxy(raw, upstream.headers);
7644
+ if (transformedRes === null) {
7645
+ res.writeHead(status, upstream.headers);
7646
+ res.end(raw);
7647
+ return;
7648
+ }
7649
+ res.writeHead(status, transformedRes.headers);
7650
+ res.end(transformedRes.body);
7530
7651
  });
7531
7652
  upstream.on("error", () => {
7532
7653
  try {
@@ -7757,7 +7878,7 @@ function startGateway(config, getAccounts) {
7757
7878
  port: mainPort,
7758
7879
  path: req.url ?? "/",
7759
7880
  method: req.method,
7760
- headers: buildProxyHeaders(req.headers, mainPort)
7881
+ headers: upstreamHeaders(req)
7761
7882
  });
7762
7883
  upstream.on("upgrade", (ures, usock, uhead) => {
7763
7884
  usocket = usock;
@@ -7831,6 +7952,42 @@ function startGateway(config, getAccounts) {
7831
7952
  }
7832
7953
  };
7833
7954
  }
7955
+ /**
7956
+ * 构造 dsh browser cookie 提供者(v1.28.2 0.1.2-rc.1 BrowserAuth 适配):
7957
+ * 经 ctx.connection(HostConnectionHandle)官方通道在内存换取 authority=127.0.0.1:<mainPort>
7958
+ * 的 cookie 并缓存(createDshCookieProvider 惰性换取)。connection 缺失(旧 dsh/非 web 装配)
7959
+ * → provider 返回 undefined → 反代不注入(旧形态无 BrowserAuth,天然兼容)。
7960
+ *
7961
+ * v1.28.2 修复:**connection 每次调用现取**(不绑定 sync 时点)——gateway sync 在 apply
7962
+ * 早期跑,此时 connection 服务可能尚未装配(web-app 的 URL 打印在其后);首次反代请求
7963
+ * 到达时 connection 必已就绪。cachedProvider 缓存换取结果(同一 connection 实例)。
7964
+ */
7965
+ function buildDshCookieProvider(ctx, mainPort) {
7966
+ const authority = `127.0.0.1:${mainPort}`;
7967
+ let cachedProvider;
7968
+ let diagnosed = false;
7969
+ return () => {
7970
+ try {
7971
+ const connection = ctx.get?.("connection");
7972
+ if (!connection || typeof connection.authenticatedUrl !== "function" || typeof connection.authorizeIndex !== "function") {
7973
+ if (!diagnosed) {
7974
+ diagnosed = true;
7975
+ console.log("[serenity-hooks] dsh browser-auth 适配: connection ✗ 不可取(不注入 dsh cookie——旧 dsh/非 web 装配兼容)");
7976
+ }
7977
+ return;
7978
+ }
7979
+ if (cachedProvider === void 0) cachedProvider = createDshCookieProvider(connection, authority);
7980
+ const cookie = cachedProvider();
7981
+ if (!diagnosed) {
7982
+ diagnosed = true;
7983
+ console.log(`[serenity-hooks] dsh browser-auth 适配: connection ✓ 可取 + cookie ${cookie === void 0 ? "✗ 换取失败(上游 401 时检查)" : "✓ 已内存换取(注入反代)"}`);
7984
+ }
7985
+ return cookie;
7986
+ } catch {
7987
+ return;
7988
+ }
7989
+ };
7990
+ }
7834
7991
  /** 注册 gateway(index.ts apply 调用)。
7835
7992
  * 归属原则(v1.22):gateway 是 plugin 全局能力——enabled 开关读 DSH settings
7836
7993
  * (readSimpleSettings().gatewayEnabled),host/port/accounts 读 plugin 全局文件
@@ -7868,7 +8025,7 @@ function registerGateway(ctx) {
7868
8025
  cookieSecure: settings.gateway.cookieSecure === true,
7869
8026
  allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate !== false,
7870
8027
  totpEnabled: settings.gateway.totpEnabled === true
7871
- }, () => readAdvancedSettings().gateway.accounts);
8028
+ }, () => readAdvancedSettings().gateway.accounts, buildDshCookieProvider(ctx, webServer.port));
7872
8029
  const accounts = settings.gateway.accounts.length;
7873
8030
  const wsNote = settings.gateway.workspaces.length === 0 ? "" : `;工作区白名单 ${settings.gateway.workspaces.length} 条`;
7874
8031
  console.log(`[serenity-hooks] gateway 已启动: http://${settings.gateway.host}:${settings.gateway.port} → 127.0.0.1:${webServer.port}` + (accounts === 0 ? "(⚠️ 未配置账号,登录页将提示)" : `(${accounts} 个账号)`) + wsNote);
@@ -8055,7 +8212,7 @@ function isExternalFaceSession(sessionId) {
8055
8212
  * 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
8056
8213
  * (v1.26.8 状态机,弃正则——同 v1.27.1 微信桥回复链路)。 */
8057
8214
  function lastAssistantText(agent) {
8058
- const events = agent.session.events ?? [];
8215
+ const events = sessionEvents(agent.session);
8059
8216
  for (let i = events.length - 1; i >= 0; i--) {
8060
8217
  const e = events[i];
8061
8218
  if (e && e.type === "assistant/message") {
@@ -9332,7 +9489,8 @@ function registerWeixinBridge(ctx) {
9332
9489
  //#endregion
9333
9490
  //#region src/index.ts
9334
9491
  const name = "dsh-serenity-hooks";
9335
- /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
9492
+ /** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
9493
+ * (v1.28.0 适配 0.1.2-rc.1:+ 'settings'——B4 settings 服务由 provider 插件加载后才有) */
9336
9494
  const inject = [
9337
9495
  "tools",
9338
9496
  "webServer",
@@ -9342,7 +9500,8 @@ const inject = [
9342
9500
  "agentLoop",
9343
9501
  "agents",
9344
9502
  "systemPrompt",
9345
- "sessionProjections"
9503
+ "sessionProjections",
9504
+ "settings"
9346
9505
  ];
9347
9506
  const Config = z.object({
9348
9507
  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;