@shgroup/dsh-serenity-hooks 1.82.0 → 1.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.82.0",
3
+ "version": "1.83.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -29,6 +29,12 @@ import { type WeixinMessage } from './ilink.js';
29
29
  import { resolveInboundTarget, manualOutputFallbackNeeded, type ResolvedInboundTarget } from './inbound-event.js';
30
30
  export { manualOutputFallbackNeeded, resolveInboundTarget };
31
31
  export type { ResolvedInboundTarget };
32
+ /** 记下最近一次入站带来的 `context_token`(**只存非空**;空串会把好票覆盖成坏票) */
33
+ export declare function rememberContextToken(accountId: string, userId: string, token: string | undefined): void;
34
+ /** 取该用户最近一次的 `context_token`(没有 ⇒ `undefined`,调用方按"裸发"处理并如实回执) */
35
+ export declare function recallContextToken(accountId: string, userId: string): string | undefined;
36
+ /** 测试辅助:清空 context_token 缓存(生产零调用;与 resetWeixinTypingCache 同款) */
37
+ export declare function resetWeixinContextCache(): void;
32
38
  /** 测试辅助:清空 typing_ticket 缓存(生产零调用) */
33
39
  export declare function resetWeixinTypingCache(): void;
34
40
  /**
package/lib/index.js CHANGED
@@ -9461,15 +9461,51 @@ async function createAndBindTarget(ctx, root, dirName) {
9461
9461
  createdAsUser: true
9462
9462
  });
9463
9463
  const openingNote = deliverCarrierOpening(agent, buildCarrierOpening(parseTrajectoryCode(dirName) ?? dirName, trajectoryDescOf(dirName))) ? ", opening-as-user" : ", ⚠️ 开场白未投递";
9464
+ const titleNote = renameCarrierTitle(ctx, agent, root, dirName);
9464
9465
  const wsNote = workspaceId !== void 0 ? "" : await attachToWorkspace(ctx, root, sessionId);
9465
9466
  const viaNote = workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach";
9466
- const how = `auto-create(${sessionId}${bound.ok ? ", bound" : `, ⚠️ 绑定写入失败(${bound.code})`}${viaNote}${openingNote}${wsNote})`;
9467
+ const how = `auto-create(${sessionId}${bound.ok ? ", bound" : `, ⚠️ 绑定写入失败(${bound.code})`}${viaNote}${openingNote}${titleNote}${wsNote})`;
9467
9468
  return {
9468
9469
  agent,
9469
9470
  how
9470
9471
  };
9471
9472
  }
9472
9473
  /**
9474
+ * 把刚建的载体会话标题**钉成与 `use` 同一形态**(2026-10-21,owner 报「标题不规范」)。
9475
+ *
9476
+ * ## 为什么单独成函数
9477
+ *
9478
+ * 它是**全仓第二个**写会话标题的点(第一个是 `use`/`rebuild` 的 `renameDshSessionOnUse`)。
9479
+ * 集中在一处,将来若要改命名规则,判据仍在 `namingTitleFor` 一个地方。
9480
+ *
9481
+ * ## 三者顺序不可换
9482
+ *
9483
+ * 必须在 ③b(投开场白)**之后**调用:
9484
+ * - 开场白让 `titleInput.count` > 0,是标题得以成立的前提;
9485
+ * - 放在之后还有**降级好处**:万一 `rename` 失败,宿主自己的 fallback 标题仍在
9486
+ * (难看但存在),而不是两者皆无。
9487
+ *
9488
+ * @returns 回执片段(成功 `, titled` / 失败 `, ⚠️ 标题未写(原因)`)—— **必须出声**,
9489
+ * 否则"标题没写"会表现成"这条轨迹就是这名字"(D186:守卫可以拒绝,但不许不出声)。
9490
+ */
9491
+ function renameCarrierTitle(ctx, agent, root, dirName) {
9492
+ try {
9493
+ const titles = hostService(ctx, "sessionTitle");
9494
+ if (!titles || typeof titles.rename !== "function") return ", ⚠️ 标题未写(sessionTitle 不可用)";
9495
+ const session = agent.session;
9496
+ if (!session || typeof session !== "object") return ", ⚠️ 标题未写(无 session)";
9497
+ const title = namingTitleFor({
9498
+ sessionId: parseTrajectoryCode(dirName) ?? dirName,
9499
+ dirName,
9500
+ mdPath: join(root, "AGENT_SESSIONS", dirName, "SESSION.md")
9501
+ });
9502
+ titles.rename(session, title);
9503
+ return ", titled";
9504
+ } catch (err) {
9505
+ return `, ⚠️ 标题未写(${String(err?.message ?? err)})`;
9506
+ }
9507
+ }
9508
+ /**
9473
9509
  * 把刚建的会话挂进 **cwd 所属的那个工作区**(2026-10-17 缺陷修复)。
9474
9510
  *
9475
9511
  * 🔴 **为什么必须我们自己做(宿主源码取证,不是推断)**:
@@ -19776,7 +19812,9 @@ function installFeishuAccountImpl() {
19776
19812
  var bridge_exports = /* @__PURE__ */ __exportAll({
19777
19813
  handleIncoming: () => handleIncoming,
19778
19814
  handleMachineIncoming: () => handleMachineIncoming,
19815
+ recallContextToken: () => recallContextToken,
19779
19816
  registerWeixinBridge: () => registerWeixinBridge,
19817
+ rememberContextToken: () => rememberContextToken,
19780
19818
  resolveInboundTarget: () => resolveInboundTarget,
19781
19819
  resolveWeixinAccount: () => resolveWeixinAccount,
19782
19820
  sendProactiveText: () => sendProactiveText,
@@ -19800,6 +19838,52 @@ const MACHINE_SCOPE = "<machine>";
19800
19838
  const machineLoops = /* @__PURE__ */ new Map();
19801
19839
  /** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
19802
19840
  const typingTicketByUser = /* @__PURE__ */ new Map();
19841
+ /**
19842
+ * 🔴 **最近一次 `context_token` 缓存**(每 `(accountId, userId)`)—— 2026-10-21,owner 报
19843
+ * 「久不在微信发言,认知轨迹主动发消息就不通」。
19844
+ *
19845
+ * ## 为什么必须有它(协议规范,不是我们的发明)
19846
+ *
19847
+ * 微信 iLink 协议的 `context_token` **不是"用户 ID"**,而是
19848
+ * **「当前对话上下文的会话能力令牌」**([公开协议规范] §5.10)。规范逐字:
19849
+ *
19850
+ * - **文本回复:必填。图片/文件/视频回复:必填。**(§6.4)
19851
+ * - 「以 `(accountId, userId)` 为 key **缓存最近一次 `context_token`**」(§5.10 工程建议)
19852
+ * - 「**主动发消息能力**:公开实现普遍依赖**最近一次入站消息**带来的 `context_token`」(§12 对比表)
19853
+ * —— 这是微信相对 Telegram/Slack 的**根本差异**(那两家只要知道 chat_id 就行)。
19854
+ * - 「协议里**未观察到**单独"打开会话"的 API」(§5.10)⇒ **没有任何办法主动"开一个会话"**,
19855
+ * 只能靠人发一条消息来刷新这张票。
19856
+ *
19857
+ * ## 🔴 修前我们是什么状态
19858
+ *
19859
+ * 收到 token 后**只用一次就丢**(唯一消费点是"回复这一条消息")⇒ 那条回复发完,
19860
+ * 手上就**再没有票**了 ⇒ 之后任何**主动**发送都是裸发(不带 `context_token`)。
19861
+ * ⇒ 这与 owner 的观察完全吻合:**刚聊过就通**(票还在窗口内/刚被刷新),**隔久了就不通**。
19862
+ * ⚠️ 这**不是心跳问题** —— 规范里根本没有心跳;是**票没留住**。
19863
+ *
19864
+ * ## 边界(照规范原话)
19865
+ *
19866
+ * - **不跨用户复用、不跨 bot 账号复用**(key 含 accountId 与 userId 两段)。
19867
+ * - **不伪造 token**:只存服务端真给过的。
19868
+ * - **进程重启即丢失**(内存缓存)——规范也确认这条,因为**没有开新会话的 API**;
19869
+ * 重启后的第一条主动消息仍可能失败,这是**平台限制**,由 O3 的失败文案如实告知。
19870
+ */
19871
+ const contextTokenByUser = /* @__PURE__ */ new Map();
19872
+ /** context_token 的缓存键(与 typing 同款两段式,但**语义不同**:这里存的是会话票) */
19873
+ function contextCacheKey(accountId, userId) {
19874
+ return `${accountId}|${userId}`;
19875
+ }
19876
+ /** 记下最近一次入站带来的 `context_token`(**只存非空**;空串会把好票覆盖成坏票) */
19877
+ function rememberContextToken(accountId, userId, token) {
19878
+ if (!userId) return;
19879
+ const t = (token ?? "").trim();
19880
+ if (t === "") return;
19881
+ contextTokenByUser.set(contextCacheKey(accountId, userId), t);
19882
+ }
19883
+ /** 取该用户最近一次的 `context_token`(没有 ⇒ `undefined`,调用方按"裸发"处理并如实回执) */
19884
+ function recallContextToken(accountId, userId) {
19885
+ return contextTokenByUser.get(contextCacheKey(accountId, userId));
19886
+ }
19803
19887
  function typingCacheKey(accountId, fromUserId) {
19804
19888
  return `${accountId}|${fromUserId}`;
19805
19889
  }
@@ -19880,6 +19964,7 @@ async function sendTypingStop(cred, accountId, fromUserId) {
19880
19964
  async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
19881
19965
  const fromUserId = msg.from_user_id;
19882
19966
  if (!fromUserId) return;
19967
+ rememberContextToken(accountId, fromUserId, msg.context_token);
19883
19968
  await handleInboundEvent(ctx, root, accountId, {
19884
19969
  sendText: async ({ toUserId, text, replyRef }) => {
19885
19970
  await sendTextMessage({
@@ -20210,18 +20295,45 @@ async function sendProactiveText(input) {
20210
20295
  const { accountId, cred } = account;
20211
20296
  const trajectory = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
20212
20297
  const sessionId = weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(toUserId) || "unknown");
20298
+ /**
20299
+ * 🔴 **带上最近一次的 `context_token`**(2026-10-21,协议规范 §5.10/§6.4)。
20300
+ *
20301
+ * 修前这里是**裸发**(`sendTextMessage` 不传 `contextToken`)。而规范把 `context_token`
20302
+ * 定为「会话能力令牌」:主动发送**依赖最近一次入站消息**带来的那张票(§12 对比表逐字:
20303
+ * 这正是微信相对 Telegram/Slack 的根本差异)。
20304
+ * ⇒ 没有票时,服务端无法把消息关联到对话窗口 ⇒ 表现为"久不发言就发不出去"。
20305
+ *
20306
+ * ⚠️ **仍然允许无票发送**(不改成硬拒绝):修前就是这么发的,而且**部分时机它是能成功的**
20307
+ * (票还在服务端窗口内的那些时刻)。改成硬拒绝 = 把"有时能发"变成"永远不发"。
20308
+ * ⇒ 取**尽力而为 + 如实回执**:能带就带;没票照发,但把"没票"这件事**写进回执**,
20309
+ * 让人知道这一条是"运气发出去的"(D186:可以降级,但不许不出声)。
20310
+ */
20311
+ const contextToken = recallContextToken(accountId, toUserId);
20213
20312
  try {
20214
20313
  await sendTextMessage({
20215
20314
  baseUrl: cred.baseUrl,
20216
20315
  token: cred.token,
20217
20316
  toUserId,
20218
- text: input.text
20317
+ text: input.text,
20318
+ ...contextToken === void 0 ? {} : { contextToken }
20219
20319
  });
20220
20320
  } catch (err) {
20321
+ /**
20322
+ * 🔴 **失败必须落盘**(2026-10-21,O1)。修前失败只回给调用方、**不写任何流水**
20323
+ * (outgoing hook 只在**成功后**触发)⇒ 「失败过没有、失败成什么样」在盘上不可见
20324
+ * ⇒ 下次报障只能猜。这里补上**只有失败才会产生**的那行日志。
20325
+ *
20326
+ * ⚠️ 按规范 §11:`ret=-2` 在社区实现里被误报成"rate limited",
20327
+ * 而真因常是**缺 `context_token`**(第三方实测)⇒ 故把「有没有票」一起记进日志,
20328
+ * 这样一眼能分辨"票的问题"还是"账号/网络的问题"。
20329
+ */
20330
+ const detail = err instanceof Error ? err.message : String(err);
20331
+ console.warn(`[serenity-hooks] ✗ im 主动发送失败: account=${accountId} user=${toUserId} contextToken=${contextToken === void 0 ? "ABSENT(无票,裸发)" : "present"} err=${detail}`);
20221
20332
  return {
20222
20333
  ok: false,
20223
20334
  code: "SEND_FAILED",
20224
- error: `发送失败: ${err instanceof Error ? err.message : String(err)}`
20335
+ error: `发送失败: ${detail}`,
20336
+ ...contextToken === void 0 ? { remediation: "该用户近期未发过消息 ⇒ 手上没有 context_token(会话能力令牌)。协议没有\"主动开新会话\"的 API ⇒ 请先让用户在微信里发一条消息激活会话,再重试。" } : {}
20225
20337
  };
20226
20338
  }
20227
20339
  if (settings.hook) invokeWeixinHook(root, settings.hook, buildOutgoingHookEvent({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.82.0",
3
+ "version": "1.83.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {