@clawos-dev/clawd 0.2.488 → 0.2.490

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.
@@ -21317,10 +21317,22 @@ var EnvSecretSchema = external_exports.object({
21317
21317
  var EnvAssetSchema = external_exports.object({
21318
21318
  name: external_exports.string().min(1)
21319
21319
  });
21320
+ var ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
21321
+ var EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
21320
21322
  var MANIFEST_BODY = {
21321
21323
  clis: external_exports.array(EnvCliSchema),
21322
21324
  secrets: external_exports.array(EnvSecretSchema),
21323
- assets: external_exports.array(EnvAssetSchema)
21325
+ assets: external_exports.array(EnvAssetSchema),
21326
+ /**
21327
+ * 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
21328
+ * 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
21329
+ *
21330
+ * 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
21331
+ * 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
21332
+ *
21333
+ * **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
21334
+ */
21335
+ connectors: external_exports.array(ConnectorNameSchema).default([])
21324
21336
  };
21325
21337
  var EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
21326
21338
  var EnvironmentManifestSchema = external_exports.object({
@@ -21453,8 +21465,9 @@ var DeployRerunJobArgsSchema = external_exports.object({
21453
21465
  }).strict();
21454
21466
  var DeployReportFindingArgsSchema = external_exports.object({
21455
21467
  personaId: external_exports.string().min(1),
21456
- kind: external_exports.enum(["cli", "secret", "asset"]),
21468
+ kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
21457
21469
  // 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
21470
+ // connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
21458
21471
  item: external_exports.record(external_exports.string(), external_exports.unknown()),
21459
21472
  // 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
21460
21473
  evidence: external_exports.object({
@@ -21750,6 +21763,16 @@ var DispatchRecordSchema = external_exports.object({
21750
21763
  sourceSessionId: external_exports.string().min(1),
21751
21764
  sourcePersonaId: external_exports.string().min(1).optional(),
21752
21765
  targetPersonaId: external_exports.string().min(1),
21766
+ /**
21767
+ * 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
21768
+ *
21769
+ * **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
21770
+ * 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
21771
+ *
21772
+ * B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
21773
+ * 那条行记的是 B 视角的本地 dispatch。
21774
+ */
21775
+ targetDeviceId: external_exports.string().min(1).optional(),
21753
21776
  /** B 的 sessionId;registerBSession 后回填 */
21754
21777
  workerSessionId: external_exports.string().min(1).optional(),
21755
21778
  /** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
@@ -21776,7 +21799,17 @@ var DispatchRecordSchema = external_exports.object({
21776
21799
  deliveredAt: external_exports.string().min(1).optional()
21777
21800
  });
21778
21801
  var DispatchListArgsSchema = external_exports.object({
21779
- sourceSessionId: external_exports.string().min(1).optional()
21802
+ sourceSessionId: external_exports.string().min(1).optional(),
21803
+ /**
21804
+ * 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
21805
+ *
21806
+ * 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
21807
+ * 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
21808
+ * dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
21809
+ *
21810
+ * 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
21811
+ */
21812
+ relatedSessionId: external_exports.string().min(1).optional()
21780
21813
  });
21781
21814
  var DispatchListResultSchema = external_exports.object({
21782
21815
  records: external_exports.array(DispatchRecordSchema)
package/dist/cli.cjs CHANGED
@@ -5951,7 +5951,7 @@ var init_contact_handshake = __esm({
5951
5951
  });
5952
5952
 
5953
5953
  // ../protocol/src/environment-schemas.ts
5954
- var EnvCliSchema, EnvSecretSchema, EnvAssetSchema, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
5954
+ var EnvCliSchema, EnvSecretSchema, EnvAssetSchema, ConnectorNameSchema, EnvConnectorSchema, MANIFEST_BODY, EnvManifestBodySchema, EnvironmentManifestSchema, EnvEvidenceSchema;
5955
5955
  var init_environment_schemas = __esm({
5956
5956
  "../protocol/src/environment-schemas.ts"() {
5957
5957
  "use strict";
@@ -5993,10 +5993,22 @@ var init_environment_schemas = __esm({
5993
5993
  EnvAssetSchema = external_exports.object({
5994
5994
  name: external_exports.string().min(1)
5995
5995
  });
5996
+ ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
5997
+ EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
5996
5998
  MANIFEST_BODY = {
5997
5999
  clis: external_exports.array(EnvCliSchema),
5998
6000
  secrets: external_exports.array(EnvSecretSchema),
5999
- assets: external_exports.array(EnvAssetSchema)
6001
+ assets: external_exports.array(EnvAssetSchema),
6002
+ /**
6003
+ * 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
6004
+ * 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
6005
+ *
6006
+ * 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
6007
+ * 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
6008
+ *
6009
+ * **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
6010
+ */
6011
+ connectors: external_exports.array(ConnectorNameSchema).default([])
6000
6012
  };
6001
6013
  EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
6002
6014
  EnvironmentManifestSchema = external_exports.object({
@@ -6148,8 +6160,9 @@ var init_deploy_schemas = __esm({
6148
6160
  }).strict();
6149
6161
  DeployReportFindingArgsSchema = external_exports.object({
6150
6162
  personaId: external_exports.string().min(1),
6151
- kind: external_exports.enum(["cli", "secret", "asset"]),
6163
+ kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
6152
6164
  // 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
6165
+ // connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
6153
6166
  item: external_exports.record(external_exports.string(), external_exports.unknown()),
6154
6167
  // 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
6155
6168
  evidence: external_exports.object({
@@ -6494,6 +6507,16 @@ var init_dispatch = __esm({
6494
6507
  sourceSessionId: external_exports.string().min(1),
6495
6508
  sourcePersonaId: external_exports.string().min(1).optional(),
6496
6509
  targetPersonaId: external_exports.string().min(1),
6510
+ /**
6511
+ * 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
6512
+ *
6513
+ * **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
6514
+ * 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
6515
+ *
6516
+ * B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
6517
+ * 那条行记的是 B 视角的本地 dispatch。
6518
+ */
6519
+ targetDeviceId: external_exports.string().min(1).optional(),
6497
6520
  /** B 的 sessionId;registerBSession 后回填 */
6498
6521
  workerSessionId: external_exports.string().min(1).optional(),
6499
6522
  /** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
@@ -6520,7 +6543,17 @@ var init_dispatch = __esm({
6520
6543
  deliveredAt: external_exports.string().min(1).optional()
6521
6544
  });
6522
6545
  DispatchListArgsSchema = external_exports.object({
6523
- sourceSessionId: external_exports.string().min(1).optional()
6546
+ sourceSessionId: external_exports.string().min(1).optional(),
6547
+ /**
6548
+ * 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
6549
+ *
6550
+ * 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
6551
+ * 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
6552
+ * dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
6553
+ *
6554
+ * 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
6555
+ */
6556
+ relatedSessionId: external_exports.string().min(1).optional()
6524
6557
  });
6525
6558
  DispatchListResultSchema = external_exports.object({
6526
6559
  records: external_exports.array(DispatchRecordSchema)
@@ -48974,11 +49007,14 @@ var SessionManager = class {
48974
49007
  * 该群的专属 guest session,turn-idle 后从 reducer buffer 提取回复文本。
48975
49008
  *
48976
49009
  * 跟 createShiftSession 的差异:
48977
- * - **find-or-create**:按 capId `lark-<chatId>` 找现存 session(一群一条,D3),
48978
- * 没有才建;daemon `create` 的无 dedupe 语义不变——dedupe 是本方法私有实现
49010
+ * - **find-or-create**:按 capId `lark-<chatId>` 找现存 session,没有才建;daemon
49011
+ * `create` 的无 dedupe 语义不变——dedupe 是本方法私有实现。
49012
+ * 粒度由云端给的 chatId 决定,daemon 只当它是不透明 key:群消息是虚拟三段式
49013
+ * `<真实 chat_id>:<thread_id>:<open_id>`(spec 2026-09-10,一话题一人一条),
49014
+ * p2p 是裸 chat_id,文档评论是 `doc:<fileToken>`
48979
49015
  * - guest scope + 沙箱:群成员 query 不带 owner 权限;10min guest idle-kill + resume
48980
- * - turn-idle 后**不 kill**(session 常驻,记忆即价值);延迟 settleDelayMs 等尾随
48981
- * text 落盘(schemas.ts turn_end 注释:CC 写 turn_end 早于尾段正文)再读 buffer
49016
+ * - turn-idle 后**不 kill**(session 常驻,记忆即价值);TUI 下延迟 settleDelayMs 等
49017
+ * 尾随 text 落盘(schemas.ts turn_end 注释 ②)再读 buffer,SDK / codex 同步读
48982
49018
  * - 回复提取:extractLarkReplyText(最后一条 user_text 之后的 text 事件拼接)
48983
49019
  */
48984
49020
  runLarkChatTurn(args) {
@@ -49045,21 +49081,23 @@ var SessionManager = class {
49045
49081
  if (!r) return;
49046
49082
  r.input({ kind: "command", command: { kind: "stop" } });
49047
49083
  };
49048
- const settleDelayMs = args.settleDelayMs ?? 500;
49084
+ const settleDelayMs = args.settleDelayMs ?? (this.deps.mode === "tui" ? 500 : 0);
49085
+ const extractAndSettle = () => {
49086
+ try {
49087
+ const events = this.readHistoryEventsForScope(sessionId, scope);
49088
+ const replyText = extractLarkReplyText(events);
49089
+ settle(replyText ? { ok: true, replyText } : { error: "empty reply" });
49090
+ } catch (err) {
49091
+ settle({ error: err.message });
49092
+ }
49093
+ };
49049
49094
  unsubscribe = this.subscribe(
49050
49095
  sessionId,
49051
49096
  scope,
49052
49097
  makeEndFrameHandler(sessionId, (info) => {
49053
49098
  if (info.kind === "idle") {
49054
- setTimeout(() => {
49055
- try {
49056
- const events = this.readHistoryEventsForScope(sessionId, scope);
49057
- const replyText = extractLarkReplyText(events);
49058
- settle(replyText ? { ok: true, replyText } : { error: "empty reply" });
49059
- } catch (err) {
49060
- settle({ error: err.message });
49061
- }
49062
- }, settleDelayMs);
49099
+ if (settleDelayMs === 0) extractAndSettle();
49100
+ else setTimeout(extractAndSettle, settleDelayMs);
49063
49101
  } else if (info.kind === "stopped") {
49064
49102
  settle({ error: "session stopped before reply" });
49065
49103
  } else {
@@ -53662,6 +53700,7 @@ var PersonaDispatchManager = class {
53662
53700
  sourceSessionId: args.sourceSessionId,
53663
53701
  ...args.sourcePersonaId ? { sourcePersonaId: args.sourcePersonaId } : {},
53664
53702
  targetPersonaId: args.targetPersona,
53703
+ ...args.targetDeviceId ? { targetDeviceId: args.targetDeviceId } : {},
53665
53704
  taskText: args.taskText,
53666
53705
  ...args.taskBrief ? { taskBrief: args.taskBrief } : {},
53667
53706
  ...args.meta ? { meta: args.meta } : {}
@@ -53779,6 +53818,7 @@ function createDispatchStore(deps) {
53779
53818
  sourceSessionId: input.sourceSessionId,
53780
53819
  ...input.sourcePersonaId ? { sourcePersonaId: input.sourcePersonaId } : {},
53781
53820
  targetPersonaId: input.targetPersonaId,
53821
+ ...input.targetDeviceId ? { targetDeviceId: input.targetDeviceId } : {},
53782
53822
  taskText: input.taskText.slice(0, TASK_TEXT_MAX),
53783
53823
  ...input.taskBrief ? { taskBrief: input.taskBrief } : {},
53784
53824
  // 新开 dispatch = 第 1 轮;续派在 handler 里 +1
@@ -53800,7 +53840,9 @@ function createDispatchStore(deps) {
53800
53840
  return merged;
53801
53841
  }
53802
53842
  function list(filter) {
53803
- return records.filter((r) => !filter?.sourceSessionId || r.sourceSessionId === filter.sourceSessionId).slice().sort((a, b2) => a.createdAt < b2.createdAt ? 1 : a.createdAt > b2.createdAt ? -1 : 0);
53843
+ return records.filter((r) => !filter?.sourceSessionId || r.sourceSessionId === filter.sourceSessionId).filter(
53844
+ (r) => !filter?.relatedSessionId || r.sourceSessionId === filter.relatedSessionId || r.workerSessionId === filter.relatedSessionId
53845
+ ).slice().sort((a, b2) => a.createdAt < b2.createdAt ? 1 : a.createdAt > b2.createdAt ? -1 : 0);
53804
53846
  }
53805
53847
  function markInterruptedAsFailed(nowIso3) {
53806
53848
  let touched = false;
@@ -54988,11 +55030,16 @@ async function writeScan(dir, scan) {
54988
55030
  await writeJson(import_node_path31.default.join(dir, SCAN_FILE), DeployScanSchema.parse(scan));
54989
55031
  }
54990
55032
  function emptyDeployFile() {
54991
- return { version: 1, manifest: { version: 1, clis: [], secrets: [], assets: [] } };
55033
+ return { version: 1, manifest: { version: 1, clis: [], secrets: [], assets: [], connectors: [] } };
54992
55034
  }
55035
+ var FINDING_SCHEMAS = {
55036
+ cli: EnvCliSchema,
55037
+ secret: EnvSecretSchema,
55038
+ asset: EnvAssetSchema,
55039
+ connector: EnvConnectorSchema
55040
+ };
54993
55041
  function parseFinding(kind, item) {
54994
- const schema = kind === "cli" ? EnvCliSchema : kind === "secret" ? EnvSecretSchema : EnvAssetSchema;
54995
- const parsed = schema.safeParse(item);
55042
+ const parsed = FINDING_SCHEMAS[kind].safeParse(item);
54996
55043
  if (!parsed.success) {
54997
55044
  const first = parsed.error.issues[0];
54998
55045
  throw new Error(`${kind} \u6761\u76EE\u4E0D\u5408\u6CD5\uFF1A${first ? `${first.path.join(".")} ${first.message}` : ""}`);
@@ -55000,6 +55047,11 @@ function parseFinding(kind, item) {
55000
55047
  return parsed.data;
55001
55048
  }
55002
55049
  function upsertFinding(file, kind, item) {
55050
+ if (kind === "connector") {
55051
+ const have = file.manifest.connectors;
55052
+ const connectors = have.includes(item.name) ? have : [...have, item.name];
55053
+ return { ...file, manifest: { ...file.manifest, connectors } };
55054
+ }
55003
55055
  const key = kind === "cli" ? "clis" : kind === "secret" ? "secrets" : "assets";
55004
55056
  const list = [...file.manifest[key]];
55005
55057
  const idx = list.findIndex((x) => x.name === item.name);
@@ -55086,6 +55138,10 @@ var L = [
55086
55138
  // smoke 与 deploy 都要:smoke 得用正式那份模型探活,否则验的不是将要上线的东西。
55087
55139
  " model: ${{ steps.plan.outputs.model }}",
55088
55140
  " effort: ${{ steps.plan.outputs.effort }}",
55141
+ // 装了哪些连接器(JSON 数组)。gateway 按它决定要不要为任务签工牌、把授权链接拼进 prompt
55142
+ //(design 2026-09-09-cloud-connector-credentials)。名字形状在 deploy-file.mjs 卡死成 [a-z0-9-],
55143
+ // 所以下面单引号包住直接拼进命令行是安全的。
55144
+ " connectors: ${{ steps.plan.outputs.connectors }}",
55089
55145
  " steps:",
55090
55146
  " - name: Checkout persona",
55091
55147
  " uses: actions/checkout@v4",
@@ -55208,6 +55264,7 @@ var L = [
55208
55264
  " --persona-id '${{ needs.build.outputs.persona_id }}' \\",
55209
55265
  " --model '${{ needs.build.outputs.model }}' \\",
55210
55266
  " --effort '${{ needs.build.outputs.effort }}' \\",
55267
+ " --connectors '${{ needs.build.outputs.connectors }}' \\",
55211
55268
  " --run-number '${{ github.run_number }}'",
55212
55269
  "",
55213
55270
  " deploy:",
@@ -55237,7 +55294,8 @@ var L = [
55237
55294
  " --image '${{ needs.build.outputs.image }}' \\",
55238
55295
  " --persona-id '${{ needs.build.outputs.persona_id }}' \\",
55239
55296
  " --model '${{ needs.build.outputs.model }}' \\",
55240
- " --effort '${{ needs.build.outputs.effort }}'",
55297
+ " --effort '${{ needs.build.outputs.effort }}' \\",
55298
+ " --connectors '${{ needs.build.outputs.connectors }}'",
55241
55299
  ""
55242
55300
  ];
55243
55301
  var WORKFLOW_TEMPLATE = L.join("\n");
@@ -56413,6 +56471,18 @@ init_protocol();
56413
56471
  var ERROR_REPLY_TEXT = "\u5904\u7406\u51FA\u9519\u4E86\uFF0C\u8BF7\u91CD\u8BD5";
56414
56472
  var LOCAL_TURN_RETRIES = 2;
56415
56473
  var STOPPED_ERROR = "session stopped before reply";
56474
+ var isChatOf = (key, realChatId) => key === realChatId || key.startsWith(`${realChatId}:`);
56475
+ var THREAD_TITLE_MAX = 20;
56476
+ var threadTitleOf = (env) => {
56477
+ const root = env.threadContext?.[0]?.text?.trim();
56478
+ if (!root) return void 0;
56479
+ return root.length > THREAD_TITLE_MAX ? `${root.slice(0, THREAD_TITLE_MAX)}\u2026` : root;
56480
+ };
56481
+ var sessionNameOf = (env) => {
56482
+ if (env.type === "doc_comment" || (env.chatType ?? "group") !== "group") return env.chatName;
56483
+ const speaker = env.senderName || env.senderOpenId;
56484
+ return [env.chatName, threadTitleOf(env), speaker].filter(Boolean).join("\xB7") || void 0;
56485
+ };
56416
56486
  var DEBOUNCE_MS = 1e3;
56417
56487
  var MAX_IMAGE_BYTES = 8 * 1024 * 1024;
56418
56488
  var EXT_BY_MIME = {
@@ -56517,9 +56587,23 @@ function createLarkChatRouter(deps) {
56517
56587
  if (lines.length === 0) lines.push("[\u6709\u4EBA\u5728\u98DE\u4E66\u6587\u6863\u91CC @ \u4E86\u4F60\uFF0C\u4F46\u8BC4\u8BBA\u5185\u5BB9\u62C9\u53D6\u5931\u8D25]");
56518
56588
  return lines.join("\n");
56519
56589
  };
56590
+ const buildThreadContextSection = async (env, batchMessageIds) => {
56591
+ const rows = (env.threadContext ?? []).filter((r) => !batchMessageIds.has(r.messageId));
56592
+ if (rows.length === 0) return void 0;
56593
+ const head = env.threadContextTruncated ? `[\u8BDD\u9898\u4E0A\u4E0B\u6587 \xB7 \u5171 ${rows.length} \u6761\uFF0C\u66F4\u65E9\u7684\u5DF2\u7565]` : `[\u8BDD\u9898\u4E0A\u4E0B\u6587 \xB7 \u5171 ${rows.length} \u6761]`;
56594
+ const lines = [head];
56595
+ for (const row of rows) {
56596
+ lines.push(`${row.senderName}\uFF1A${row.text?.trim() ?? ""}`);
56597
+ lines.push(...await imageLinesFor(env, row.messageId, row.imageKeys ?? []));
56598
+ lines.push(...await fileLinesFor(env, row.messageId, row.files ?? []));
56599
+ }
56600
+ lines.push("---");
56601
+ return lines.join("\n");
56602
+ };
56520
56603
  const buildSection = async (env) => {
56521
56604
  if (env.type === "doc_comment") return buildDocCommentSection(env);
56522
56605
  const parentLines = env.parentMessageId ? [
56606
+ ...env.parentText?.trim() ? [`[\u88AB\u5F15\u7528\u7684\u6D88\u606F] ${env.parentText.trim()}`] : [],
56523
56607
  ...await imageLinesFor(env, env.parentMessageId, env.parentImageKeys ?? []),
56524
56608
  ...await fileLinesFor(env, env.parentMessageId, env.parentFiles ?? [])
56525
56609
  ] : [];
@@ -56560,19 +56644,21 @@ ${body}
56560
56644
  const section = [quoteAnnotation(env, i, idxByMessageId), sectionBody].filter(Boolean).join("\n");
56561
56645
  if (section) sections.push(section);
56562
56646
  }
56563
- const body = sections.join("\n") || "[\u975E\u6587\u672C\u6D88\u606F]";
56647
+ const threadSection = await buildThreadContextSection(envs[0], new Set(idxByMessageId.keys()));
56648
+ const body = [threadSection, ...sections].filter(Boolean).join("\n") || "[\u975E\u6587\u672C\u6D88\u606F]";
56564
56649
  const runTurn = (text) => {
56565
56650
  const turn = deps.manager.runLarkChatTurn({
56566
56651
  personaId: last.personaId,
56567
56652
  chatId: last.chatId,
56568
- chatName: last.chatName,
56653
+ chatName: sessionNameOf(last),
56569
56654
  chatType: last.chatType ?? "group",
56570
56655
  senderOpenId: last.senderOpenId ?? "",
56571
56656
  senderName: last.senderName ?? last.senderOpenId ?? "\u672A\u77E5\u6210\u5458",
56572
56657
  text
56573
56658
  });
56574
- activity.set(last.chatId, {
56575
- chatId: last.chatId,
56659
+ const activityChatId = last.realChatId ?? last.chatId;
56660
+ activity.set(activityChatId, {
56661
+ chatId: activityChatId,
56576
56662
  chatName: last.chatName,
56577
56663
  lastActiveAt: Date.now(),
56578
56664
  personaId: last.personaId
@@ -56607,7 +56693,8 @@ ${formatLarkSpeakerReminder(speaker)}`
56607
56693
  try {
56608
56694
  await deps.cloud.reply({
56609
56695
  appId: last.appId,
56610
- chatId: last.chatId,
56696
+ // 回复走真身:chatId 自 spec 2026-09-10 起是虚拟三段式,只作会话 key 用
56697
+ chatId: last.realChatId ?? last.chatId,
56611
56698
  // envs 已过滤保证锚点非空
56612
56699
  replyToMessageId: replyAnchorOf(last),
56613
56700
  text: replyText,
@@ -56650,11 +56737,13 @@ ${formatLarkSpeakerReminder(speaker)}`
56650
56737
  if (env.type === "bot_removed") {
56651
56738
  deps.store.addRemovedChat(env.personaId, env.chatId);
56652
56739
  activity.delete(env.chatId);
56653
- queues.delete(env.chatId);
56654
- const p2 = pending.get(env.chatId);
56655
- if (p2) {
56740
+ for (const key2 of [...queues.keys()]) {
56741
+ if (isChatOf(key2, env.chatId)) queues.delete(key2);
56742
+ }
56743
+ for (const [key2, p2] of [...pending.entries()]) {
56744
+ if (!isChatOf(key2, env.chatId)) continue;
56656
56745
  clearTimeout(p2.timer);
56657
- pending.delete(env.chatId);
56746
+ pending.delete(key2);
56658
56747
  }
56659
56748
  deps.logger?.info(`larkBot removed from chat ${env.chatId} (persona ${env.personaId})`);
56660
56749
  return;
@@ -56812,9 +56901,10 @@ function canAccessPersona(grants, personaId2, action) {
56812
56901
 
56813
56902
  // src/handlers/persona-dispatch.ts
56814
56903
  init_claude();
56815
- function buildFollowUpTaskPack(dispatchId, prompt, round) {
56904
+ function buildFollowUpTaskPack(dispatchId, prompt, round, from) {
56816
56905
  return formatDispatchTaskEnvelope({
56817
56906
  dispatchId,
56907
+ ...from ? { from } : {},
56818
56908
  round,
56819
56909
  detail: [
56820
56910
  `[Follow-up instruction for dispatch ${dispatchId}]`,
@@ -56840,6 +56930,7 @@ function buildPersonaDispatchHandlers(deps) {
56840
56930
  });
56841
56931
  }
56842
56932
  const args = DispatchRunArgsSchema.parse(rest);
56933
+ const sourcePersonaId = deps.getSessionPersonaId?.(sourceSessionId);
56843
56934
  if (args.tool !== void 0 && ctx?.principal.kind === "guest") {
56844
56935
  throw new ClawdError(
56845
56936
  ERROR_CODES.UNAUTHORIZED,
@@ -56895,7 +56986,14 @@ function buildPersonaDispatchHandlers(deps) {
56895
56986
  });
56896
56987
  const ok = deps.sendFollowUp(
56897
56988
  rec4.workerSessionId,
56898
- buildFollowUpTaskPack(args.dispatchId, args.prompt, nextRound),
56989
+ buildFollowUpTaskPack(
56990
+ args.dispatchId,
56991
+ args.prompt,
56992
+ nextRound,
56993
+ // 台账行里有就用台账的(本机派活一律有),没有再现查一次:老台账行是在
56994
+ // sourcePersonaId 有写入点之前落的,那些行只能靠现查补上。
56995
+ rec4.sourcePersonaId ?? deps.getSessionPersonaId?.(sourceSessionId)
56996
+ ),
56899
56997
  // 来源分型(L2):续派并进 dispatch-task;realtime 侧靠这个 meta 打标,
56900
56998
  // history 侧靠 classifyUserMessage 从信封解出同值。
56901
56999
  "dispatch-task"
@@ -56961,6 +57059,10 @@ function buildPersonaDispatchHandlers(deps) {
56961
57059
  const { dispatchId: dispatchId2 } = mgr.start({
56962
57060
  sourceSessionId,
56963
57061
  targetPersona,
57062
+ ...sourcePersonaId ? { sourcePersonaId } : {},
57063
+ // A 端台账记「我派给了谁的机器」:worker 会话在对端,本机点不进去,卡片的归属带
57064
+ // 靠这个字段说出「在哪台机器上」而不是干瞪眼(design §4 跨设备那一行)。
57065
+ targetDeviceId: args.targetDeviceId,
56964
57066
  taskText: args.prompt,
56965
57067
  ...args.brief ? { taskBrief: args.brief } : {},
56966
57068
  ...args.meta ? { meta: args.meta } : {}
@@ -57034,6 +57136,9 @@ function buildPersonaDispatchHandlers(deps) {
57034
57136
  const { dispatchId } = mgr.start({
57035
57137
  sourceSessionId,
57036
57138
  targetPersona,
57139
+ // 「这活谁派的」的真源。schema 里早就有这个字段,本次之前没有任何写入点——
57140
+ // 于是 worker 会话的 dispatch 卡片永远查不到派活方(归属带只能退回信封)。
57141
+ ...sourcePersonaId ? { sourcePersonaId } : {},
57037
57142
  taskText: args.prompt,
57038
57143
  ...args.brief ? { taskBrief: args.brief } : {},
57039
57144
  ...args.meta ? { meta: args.meta } : {}
@@ -57100,9 +57205,10 @@ function buildPersonaDispatchHandlers(deps) {
57100
57205
  const list = async (frame) => {
57101
57206
  const { type: _t, requestId: _r, ...rest } = frame;
57102
57207
  const args = DispatchListArgsSchema.parse(rest);
57103
- const records = deps.store.list(
57104
- args.sourceSessionId ? { sourceSessionId: args.sourceSessionId } : void 0
57105
- );
57208
+ const records = deps.store.list({
57209
+ ...args.sourceSessionId ? { sourceSessionId: args.sourceSessionId } : {},
57210
+ ...args.relatedSessionId ? { relatedSessionId: args.relatedSessionId } : {}
57211
+ });
57106
57212
  return {
57107
57213
  response: { type: "personaDispatch:list:ok", records }
57108
57214
  };
@@ -65428,7 +65534,7 @@ function computeMethodAccess(args) {
65428
65534
  }
65429
65535
 
65430
65536
  // src/version.ts
65431
- var version = "0.2.488".length > 0 ? "0.2.488" : "dev";
65537
+ var version = "0.2.490".length > 0 ? "0.2.490" : "dev";
65432
65538
 
65433
65539
  // src/cli-probe/probe.ts
65434
65540
  var fs72 = __toESM(require("fs"), 1);
@@ -70546,6 +70652,9 @@ async function startDaemon(config) {
70546
70652
  },
70547
70653
  // 续派(spec 2026-07-21):任务包注入 worker session,复用投递同款注入路径。
70548
70654
  sendFollowUp: (workerSessionId, text, meta) => manager.injectIntoSession(workerSessionId, text, meta),
70655
+ // 「这活是谁派的」:台账 sourcePersonaId + 续派包信封 from 都吃它。与首发任务包在
70656
+ // SessionManager 那侧的反查(buildDispatchTaskPack 的 from)是同一个字段、同一个查法。
70657
+ getSessionPersonaId: (sessionId) => manager.findOwnedSession(sessionId)?.ownerPersonaId,
70549
70658
  logger,
70550
70659
  spawnB: async (args) => {
70551
70660
  logger.info("dispatch.spawnB.start", {
@@ -21310,10 +21310,22 @@ var EnvSecretSchema = external_exports.object({
21310
21310
  var EnvAssetSchema = external_exports.object({
21311
21311
  name: external_exports.string().min(1)
21312
21312
  });
21313
+ var ConnectorNameSchema = external_exports.string().regex(/^[a-z0-9-]+$/, "\u8FDE\u63A5\u5668\u540D\u53EA\u80FD\u662F\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u77ED\u6A2A\u7EBF");
21314
+ var EnvConnectorSchema = external_exports.object({ name: ConnectorNameSchema });
21313
21315
  var MANIFEST_BODY = {
21314
21316
  clis: external_exports.array(EnvCliSchema),
21315
21317
  secrets: external_exports.array(EnvSecretSchema),
21316
- assets: external_exports.array(EnvAssetSchema)
21318
+ assets: external_exports.array(EnvAssetSchema),
21319
+ /**
21320
+ * 装了哪些连接器(design 2026-09-09-cloud-connector-credentials §7.1)。**字符串数组**,
21321
+ * 是四段里唯一不是 `{name}` 对象数组的一段:连接器没有任何附加属性,包一层对象只是多一层壳。
21322
+ *
21323
+ * 不复用 secrets:那段说的是「需要一份值,由部署者填」,这里没有值要填;
21324
+ * 不从 clis 推断:lark-cli 在底座里,每个 persona 都有它,推不出任何东西。
21325
+ *
21326
+ * **缺省空而不是必填**:本机上已有的单据不用为此重扫一遍;没声明就是没装,扫描器照样会报。
21327
+ */
21328
+ connectors: external_exports.array(ConnectorNameSchema).default([])
21317
21329
  };
21318
21330
  var EnvManifestBodySchema = external_exports.object(MANIFEST_BODY);
21319
21331
  var EnvironmentManifestSchema = external_exports.object({
@@ -21446,8 +21458,9 @@ var DeployRerunJobArgsSchema = external_exports.object({
21446
21458
  }).strict();
21447
21459
  var DeployReportFindingArgsSchema = external_exports.object({
21448
21460
  personaId: external_exports.string().min(1),
21449
- kind: external_exports.enum(["cli", "secret", "asset"]),
21461
+ kind: external_exports.enum(["cli", "secret", "asset", "connector"]),
21450
21462
  // 该条目在 manifest 对应数组里的内容(形状随 kind 变,handler 按 kind 收窄校验)
21463
+ // connector 的 item 只有 name,落进清单时只留名字(manifest.connectors 是字符串数组)
21451
21464
  item: external_exports.record(external_exports.string(), external_exports.unknown()),
21452
21465
  // 这条发现的来历(一句人话 + 原文出处),形状同 EnvEvidence,target 由 handler 从 kind+name 派生
21453
21466
  evidence: external_exports.object({
@@ -21743,6 +21756,16 @@ var DispatchRecordSchema = external_exports.object({
21743
21756
  sourceSessionId: external_exports.string().min(1),
21744
21757
  sourcePersonaId: external_exports.string().min(1).optional(),
21745
21758
  targetPersonaId: external_exports.string().min(1),
21759
+ /**
21760
+ * 跨设备派活时 B 所在的设备(`DispatchRunArgs.targetDeviceId`),A 端台账记「我派给了谁的机器」。
21761
+ *
21762
+ * **缺省即本机**,与入参侧用 `undefined` 表示本机是同一条口径——不引第二种表示法。
21763
+ * 存量台账行没有这个字段,消费方(dispatch 卡片的归属带)据此只显示 persona、不显示设备。
21764
+ *
21765
+ * B 端自己的台账行**不会**有它:peer-forward 转发时故意 strip 掉(B 据此判定本地执行),
21766
+ * 那条行记的是 B 视角的本地 dispatch。
21767
+ */
21768
+ targetDeviceId: external_exports.string().min(1).optional(),
21746
21769
  /** B 的 sessionId;registerBSession 后回填 */
21747
21770
  workerSessionId: external_exports.string().min(1).optional(),
21748
21771
  /** 任务摘要;截断(≤500 字符)在 daemon store.add 处保证 */
@@ -21769,7 +21792,17 @@ var DispatchRecordSchema = external_exports.object({
21769
21792
  deliveredAt: external_exports.string().min(1).optional()
21770
21793
  });
21771
21794
  var DispatchListArgsSchema = external_exports.object({
21772
- sourceSessionId: external_exports.string().min(1).optional()
21795
+ sourceSessionId: external_exports.string().min(1).optional(),
21796
+ /**
21797
+ * 「**这条会话卷入的**所有台账行」:`sourceSessionId === it || workerSessionId === it`。
21798
+ *
21799
+ * 为什么不复用 `sourceSessionId`:一条会话既可能是派活方(在自己这儿看回报卡),也可能是
21800
+ * 干活方(在 worker 会话里看任务包),后者用 sourceSessionId 过滤一条也查不到。UI 的
21801
+ * dispatch 卡片要的正是「跟我这条会话有关的全部 dispatch」,一次调用答完,不必拉全量再本地筛。
21802
+ *
21803
+ * 与 `sourceSessionId` 同时给时两个条件都要满足(AND),但调用方没有这么用的场景。
21804
+ */
21805
+ relatedSessionId: external_exports.string().min(1).optional()
21773
21806
  });
21774
21807
  var DispatchListResultSchema = external_exports.object({
21775
21808
  records: external_exports.array(DispatchRecordSchema)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scan-persona-for-deploy
3
- description: 老板要把某个 persona 发布到云上时触发——把这个 persona 读一遍,弄清它挪到一台干净的云上机器要预备什么(命令行工具、凭据、随行资产),边查边填进它的部署文件。收到形如「扫描 persona-xxx 的部署环境,personaId=persona-xxx」的任务时使用。只负责扫,不负责部署。
3
+ description: 老板要把某个 persona 发布到云上时触发——把这个 persona 读一遍,弄清它挪到一台干净的云上机器要预备什么(命令行工具、凭据、随行资产、连接器),边查边填进它的部署文件。收到形如「扫描 persona-xxx 的部署环境,personaId=persona-xxx」的任务时使用。只负责扫,不负责部署。
4
4
  ---
5
5
 
6
6
  # 扫 persona 的上云环境
@@ -35,7 +35,7 @@ description: 老板要把某个 persona 发布到云上时触发——把这个
35
35
  ```
36
36
  call({ method: "deploy:reportFinding", args: {
37
37
  personaId: "<任务里给的>",
38
- kind: "cli" | "secret" | "asset",
38
+ kind: "cli" | "secret" | "asset" | "connector",
39
39
  item: { ... }, // 见下面各类的字段
40
40
  evidence: { reason: "...", file: "...", quote: "..." }
41
41
  }})
@@ -99,6 +99,18 @@ call({ method: "deploy:reportFinding", args: {
99
99
  - 判据往往落在「这个动作在云上由谁提供」。persona 在本机走 daemon 提供的 MCP 干的活,
100
100
  云上容器里没有 daemon,靠的就是那份随行资产。
101
101
 
102
+ ### kind: "connector" —— 以用户本人身份操作的第三方
103
+
104
+ `item` 字段只有 `name`(小写字母、数字、短横线)。目前能声明的只有 `feishu`。
105
+
106
+ 判据:**persona 里出现「以用户本人身份操作某个第三方」的动作**就报——查他的日程、发他的消息、
107
+ 读他的文档,这类活在云上要先拿到他本人的授权,授权由云上的连接器完成,不是部署者填一份凭据就能代替的。
108
+
109
+ - 跟 `secret` 的区别:`secret` 是「部署者出一份值,全体使用者共用」;`connector` 是「每个使用者各自授权一次」。
110
+ 一个 persona 用 `lark-cli … --as user` 操作飞书,报的是 `connector: feishu`,**不要**再给它报一条飞书的 `secret`。
111
+ - 跟 `cli` 的区别:`lark-cli` 这个工具本身在底座镜像里,不用报 `cli`;报 `connector` 说的是「它要用谁的身份」。
112
+ - `evidence.quote` 引 skill 里带 `--as user` 的那类命令行就够。
113
+
102
114
  ### evidence —— 每条的来历
103
115
 
104
116
  - `reason`:**一句人话**,说清「为什么需要它 + 你从哪看出来的」,写给不熟悉这个 persona 的人看。