@ycodium-ai/plugin-im 0.2.2972 → 0.2.2974

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/dist/index.js CHANGED
@@ -126225,6 +126225,26 @@ var readAttached = (data) => {
126225
126225
  return chatKey === null || threadId === null || projectId === null || attachedAt === null ? null : { chatKey, threadId, projectId, attachedAt };
126226
126226
  };
126227
126227
 
126228
+ // src/chatAliases.ts
126229
+ var aliasKey = (rowId, alias) => `${rowId}:${alias}`;
126230
+ var chatAliasOf = (state, rowId, alias) => state.aliases?.[aliasKey(rowId, alias)];
126231
+ var nonEmpty2 = (value2) => typeof value2 === "string" && value2 !== "" ? value2 : null;
126232
+ var foldAliased = (state, data) => {
126233
+ const rowId = nonEmpty2(data["rowId"]);
126234
+ const alias = nonEmpty2(data["alias"]);
126235
+ const chatId = nonEmpty2(data["chatId"]);
126236
+ if (rowId === null || alias === null || chatId === null) return state;
126237
+ const key = aliasKey(rowId, alias);
126238
+ if (state.aliases?.[key] !== void 0) return state;
126239
+ return { ...state, aliases: { ...state.aliases, [key]: chatId } };
126240
+ };
126241
+
126242
+ // src/keepNewest.ts
126243
+ var keepNewest = (record2, limit) => {
126244
+ const entries = Object.entries(record2);
126245
+ return entries.length <= limit ? record2 : Object.fromEntries(entries.slice(entries.length - limit));
126246
+ };
126247
+
126228
126248
  // src/chatState.ts
126229
126249
  var IM_STATE_KEY = "chats";
126230
126250
  var IM_STATE_VERSION = 1;
@@ -126236,7 +126256,8 @@ var emptyState = () => ({
126236
126256
  approvals: {},
126237
126257
  turnCards: {},
126238
126258
  questions: {},
126239
- queue: {}
126259
+ queue: {},
126260
+ aliases: {}
126240
126261
  });
126241
126262
  var questionsOf = (state) => state.questions ?? {};
126242
126263
  var queueOf = (state) => state.queue ?? {};
@@ -126294,11 +126315,11 @@ var openQueueOfThread = (state, threadId) => Object.values(queueOf(state)).filte
126294
126315
  (record2) => record2.threadId === threadId && record2.settled === void 0
126295
126316
  );
126296
126317
  var isRecord = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
126297
- var nonEmpty2 = (value2) => typeof value2 === "string" && value2 !== "" ? value2 : null;
126318
+ var nonEmpty3 = (value2) => typeof value2 === "string" && value2 !== "" ? value2 : null;
126298
126319
  var readTarget = (value2) => {
126299
126320
  if (!isRecord(value2)) return null;
126300
126321
  if (value2["kind"] === "thread") return { kind: "thread" };
126301
- const teamId = nonEmpty2(value2["teamId"]);
126322
+ const teamId = nonEmpty3(value2["teamId"]);
126302
126323
  return value2["kind"] === "team" && teamId !== null ? { kind: "team", teamId } : null;
126303
126324
  };
126304
126325
  var readButtons = (value2) => {
@@ -126306,8 +126327,8 @@ var readButtons = (value2) => {
126306
126327
  const buttons = [];
126307
126328
  for (const entry of value2) {
126308
126329
  if (!isRecord(entry)) return null;
126309
- const token = nonEmpty2(entry["token"]);
126310
- const decision = nonEmpty2(entry["decision"]);
126330
+ const token = nonEmpty3(entry["token"]);
126331
+ const decision = nonEmpty3(entry["decision"]);
126311
126332
  const label = typeof entry["label"] === "string" ? entry["label"] : null;
126312
126333
  if (token === null || decision === null || label === null) return null;
126313
126334
  buttons.push({ token, label, decision });
@@ -126319,8 +126340,8 @@ var readQuestionOptions = (value2) => {
126319
126340
  const options = [];
126320
126341
  for (const entry of value2) {
126321
126342
  if (!isRecord(entry)) return null;
126322
- const token = nonEmpty2(entry["token"]);
126323
- const optionValue = nonEmpty2(entry["value"]);
126343
+ const token = nonEmpty3(entry["token"]);
126344
+ const optionValue = nonEmpty3(entry["value"]);
126324
126345
  if (token === null || optionValue === null || typeof entry["label"] !== "string") return null;
126325
126346
  const planAction = entry["planAction"];
126326
126347
  options.push({
@@ -126338,10 +126359,10 @@ var readQuestionItems = (value2) => {
126338
126359
  const items = [];
126339
126360
  for (const entry of value2) {
126340
126361
  if (!isRecord(entry)) return null;
126341
- const id = nonEmpty2(entry["id"]);
126362
+ const id = nonEmpty3(entry["id"]);
126342
126363
  const options = readQuestionOptions(entry["options"]);
126343
126364
  if (id === null || options === null || typeof entry["question"] !== "string") return null;
126344
- const submitToken = nonEmpty2(entry["submitToken"]);
126365
+ const submitToken = nonEmpty3(entry["submitToken"]);
126345
126366
  items.push({
126346
126367
  id,
126347
126368
  header: typeof entry["header"] === "string" ? entry["header"] : "",
@@ -126384,10 +126405,6 @@ var withQueueRecord = (state, intentId, patch) => {
126384
126405
  if (intentId === null || record2 === void 0) return state;
126385
126406
  return { ...state, queue: { ...queueOf(state), [intentId]: patch(record2) } };
126386
126407
  };
126387
- var keepNewest = (record2, limit) => {
126388
- const entries = Object.entries(record2);
126389
- return entries.length <= limit ? record2 : Object.fromEntries(entries.slice(entries.length - limit));
126390
- };
126391
126408
  var withChat = (state, chatKey, patch) => ({
126392
126409
  ...state,
126393
126410
  chats: { ...state.chats, [chatKey]: { ...chatOf(state, chatKey), ...patch } }
@@ -126396,19 +126413,19 @@ var foldImEvent = (state, name, data) => {
126396
126413
  if (!isRecord(data)) return state;
126397
126414
  switch (name) {
126398
126415
  case "chat-updated": {
126399
- const chatKey = nonEmpty2(data["chatKey"]);
126416
+ const chatKey = nonEmpty3(data["chatKey"]);
126400
126417
  if (chatKey === null) return state;
126401
126418
  const patch = {};
126402
126419
  if (data["projectId"] === null) patch.projectId = null;
126403
- else if (nonEmpty2(data["projectId"]) !== null) patch.projectId = nonEmpty2(data["projectId"]);
126420
+ else if (nonEmpty3(data["projectId"]) !== null) patch.projectId = nonEmpty3(data["projectId"]);
126404
126421
  if (data["threadId"] === null) patch.threadId = null;
126405
- else if (nonEmpty2(data["threadId"]) !== null) patch.threadId = nonEmpty2(data["threadId"]);
126422
+ else if (nonEmpty3(data["threadId"]) !== null) patch.threadId = nonEmpty3(data["threadId"]);
126406
126423
  const target2 = readTarget(data["target"]);
126407
126424
  if (target2 !== null) patch.target = target2;
126408
126425
  if (data["model"] === null) patch.model = null;
126409
126426
  else if (isRecord(data["model"])) {
126410
- const instanceId = nonEmpty2(data["model"]["instanceId"]);
126411
- const model = nonEmpty2(data["model"]["model"]);
126427
+ const instanceId = nonEmpty3(data["model"]["instanceId"]);
126428
+ const model = nonEmpty3(data["model"]["model"]);
126412
126429
  if (instanceId !== null && model !== null) patch.model = { instanceId, model };
126413
126430
  }
126414
126431
  if (typeof data["plan"] === "boolean") patch.plan = data["plan"];
@@ -126419,10 +126436,10 @@ var foldImEvent = (state, name, data) => {
126419
126436
  return withChat(state, chatKey, patch);
126420
126437
  }
126421
126438
  case "thread-opened": {
126422
- const chatKey = nonEmpty2(data["chatKey"]);
126423
- const threadId = nonEmpty2(data["threadId"]);
126424
- const projectId = nonEmpty2(data["projectId"]);
126425
- const openedAt = nonEmpty2(data["openedAt"]);
126439
+ const chatKey = nonEmpty3(data["chatKey"]);
126440
+ const threadId = nonEmpty3(data["threadId"]);
126441
+ const projectId = nonEmpty3(data["projectId"]);
126442
+ const openedAt = nonEmpty3(data["openedAt"]);
126426
126443
  if (chatKey === null || threadId === null || projectId === null || openedAt === null) {
126427
126444
  return state;
126428
126445
  }
@@ -126443,9 +126460,9 @@ var foldImEvent = (state, name, data) => {
126443
126460
  return { ...state, attached: keepNewest(attached, ATTACHED_THREAD_LIMIT) };
126444
126461
  }
126445
126462
  case "approval-offered": {
126446
- const chatKey = nonEmpty2(data["chatKey"]);
126447
- const threadId = nonEmpty2(data["threadId"]);
126448
- const requestId = nonEmpty2(data["requestId"]);
126463
+ const chatKey = nonEmpty3(data["chatKey"]);
126464
+ const threadId = nonEmpty3(data["threadId"]);
126465
+ const requestId = nonEmpty3(data["requestId"]);
126449
126466
  const options = readButtons(data["options"]);
126450
126467
  if (chatKey === null || threadId === null || requestId === null || options === null) {
126451
126468
  return state;
@@ -126462,7 +126479,7 @@ var foldImEvent = (state, name, data) => {
126462
126479
  };
126463
126480
  }
126464
126481
  case "approval-resolved": {
126465
- const requestId = nonEmpty2(data["requestId"]);
126482
+ const requestId = nonEmpty3(data["requestId"]);
126466
126483
  const existing = requestId === null ? void 0 : state.approvals[requestId];
126467
126484
  if (requestId === null || existing === void 0) return state;
126468
126485
  return {
@@ -126471,8 +126488,8 @@ var foldImEvent = (state, name, data) => {
126471
126488
  };
126472
126489
  }
126473
126490
  case "approval-carded": {
126474
- const requestId = nonEmpty2(data["requestId"]);
126475
- const cardId = nonEmpty2(data["cardId"]);
126491
+ const requestId = nonEmpty3(data["requestId"]);
126492
+ const cardId = nonEmpty3(data["cardId"]);
126476
126493
  const existing = requestId === null ? void 0 : state.approvals[requestId];
126477
126494
  if (requestId === null || cardId === null || existing === void 0) return state;
126478
126495
  return {
@@ -126481,9 +126498,9 @@ var foldImEvent = (state, name, data) => {
126481
126498
  };
126482
126499
  }
126483
126500
  case "question-offered": {
126484
- const chatKey = nonEmpty2(data["chatKey"]);
126485
- const threadId = nonEmpty2(data["threadId"]);
126486
- const requestId = nonEmpty2(data["requestId"]);
126501
+ const chatKey = nonEmpty3(data["chatKey"]);
126502
+ const threadId = nonEmpty3(data["threadId"]);
126503
+ const requestId = nonEmpty3(data["requestId"]);
126487
126504
  const items = readQuestionItems(data["items"]);
126488
126505
  if (chatKey === null || threadId === null || requestId === null || items === null) {
126489
126506
  return state;
@@ -126500,12 +126517,12 @@ var foldImEvent = (state, name, data) => {
126500
126517
  };
126501
126518
  }
126502
126519
  case "question-carded": {
126503
- const cardId = nonEmpty2(data["cardId"]);
126520
+ const cardId = nonEmpty3(data["cardId"]);
126504
126521
  if (cardId === null) return state;
126505
126522
  return withQuestionItem(
126506
126523
  state,
126507
- nonEmpty2(data["requestId"]),
126508
- nonEmpty2(data["questionId"]),
126524
+ nonEmpty3(data["requestId"]),
126525
+ nonEmpty3(data["questionId"]),
126509
126526
  (item) => ({ ...item, cardId })
126510
126527
  );
126511
126528
  }
@@ -126514,8 +126531,8 @@ var foldImEvent = (state, name, data) => {
126514
126531
  if (!Array.isArray(selected)) return state;
126515
126532
  return withQuestionItem(
126516
126533
  state,
126517
- nonEmpty2(data["requestId"]),
126518
- nonEmpty2(data["questionId"]),
126534
+ nonEmpty3(data["requestId"]),
126535
+ nonEmpty3(data["questionId"]),
126519
126536
  (item) => ({ ...item, selected })
126520
126537
  );
126521
126538
  }
@@ -126525,13 +126542,13 @@ var foldImEvent = (state, name, data) => {
126525
126542
  const label = typeof data["label"] === "string" ? data["label"] : "";
126526
126543
  return withQuestionItem(
126527
126544
  state,
126528
- nonEmpty2(data["requestId"]),
126529
- nonEmpty2(data["questionId"]),
126545
+ nonEmpty3(data["requestId"]),
126546
+ nonEmpty3(data["questionId"]),
126530
126547
  (item) => ({ ...item, answer, answerLabel: label })
126531
126548
  );
126532
126549
  }
126533
126550
  case "question-resolved": {
126534
- const requestId = nonEmpty2(data["requestId"]);
126551
+ const requestId = nonEmpty3(data["requestId"]);
126535
126552
  const existing = requestId === null ? void 0 : questionsOf(state)[requestId];
126536
126553
  if (requestId === null || existing === void 0) return state;
126537
126554
  const ending = data["ending"];
@@ -126548,12 +126565,12 @@ var foldImEvent = (state, name, data) => {
126548
126565
  };
126549
126566
  }
126550
126567
  case "queue-offered": {
126551
- const chatKey = nonEmpty2(data["chatKey"]);
126552
- const threadId = nonEmpty2(data["threadId"]);
126553
- const intentId = nonEmpty2(data["intentId"]);
126554
- const steerToken = nonEmpty2(data["steerToken"]);
126555
- const withdrawToken = nonEmpty2(data["withdrawToken"]);
126556
- const resumeToken = nonEmpty2(data["resumeToken"]);
126568
+ const chatKey = nonEmpty3(data["chatKey"]);
126569
+ const threadId = nonEmpty3(data["threadId"]);
126570
+ const intentId = nonEmpty3(data["intentId"]);
126571
+ const steerToken = nonEmpty3(data["steerToken"]);
126572
+ const withdrawToken = nonEmpty3(data["withdrawToken"]);
126573
+ const resumeToken = nonEmpty3(data["resumeToken"]);
126557
126574
  if (chatKey === null || threadId === null || intentId === null || steerToken === null || withdrawToken === null || resumeToken === null) {
126558
126575
  return state;
126559
126576
  }
@@ -126578,30 +126595,30 @@ var foldImEvent = (state, name, data) => {
126578
126595
  };
126579
126596
  }
126580
126597
  case "queue-carded": {
126581
- const cardId = nonEmpty2(data["cardId"]);
126598
+ const cardId = nonEmpty3(data["cardId"]);
126582
126599
  if (cardId === null) return state;
126583
- return withQueueRecord(state, nonEmpty2(data["intentId"]), (record2) => ({
126600
+ return withQueueRecord(state, nonEmpty3(data["intentId"]), (record2) => ({
126584
126601
  ...record2,
126585
126602
  cardId
126586
126603
  }));
126587
126604
  }
126588
126605
  case "queue-steering":
126589
- return withQueueRecord(state, nonEmpty2(data["intentId"]), (record2) => ({
126606
+ return withQueueRecord(state, nonEmpty3(data["intentId"]), (record2) => ({
126590
126607
  ...record2,
126591
126608
  steering: data["steering"] === true
126592
126609
  }));
126593
126610
  case "queue-settled": {
126594
126611
  const outcome = data["outcome"];
126595
126612
  if (outcome !== "steered" && outcome !== "sent" && outcome !== "removed") return state;
126596
- return withQueueRecord(state, nonEmpty2(data["intentId"]), (record2) => ({
126613
+ return withQueueRecord(state, nonEmpty3(data["intentId"]), (record2) => ({
126597
126614
  ...record2,
126598
126615
  settled: outcome
126599
126616
  }));
126600
126617
  }
126601
126618
  case "turn-card-opened": {
126602
- const cardId = nonEmpty2(data["cardId"]);
126603
- const chatKey = nonEmpty2(data["chatKey"]);
126604
- const threadId = nonEmpty2(data["threadId"]);
126619
+ const cardId = nonEmpty3(data["cardId"]);
126620
+ const chatKey = nonEmpty3(data["chatKey"]);
126621
+ const threadId = nonEmpty3(data["threadId"]);
126605
126622
  if (cardId === null || chatKey === null || threadId === null) return state;
126606
126623
  const title = typeof data["title"] === "string" ? data["title"] : "";
126607
126624
  return {
@@ -126613,11 +126630,13 @@ var foldImEvent = (state, name, data) => {
126613
126630
  };
126614
126631
  }
126615
126632
  case "turn-card-closed": {
126616
- const cardId = nonEmpty2(data["cardId"]);
126633
+ const cardId = nonEmpty3(data["cardId"]);
126617
126634
  if (cardId === null) return state;
126618
126635
  const { [cardId]: _closed, ...rest } = openTurnCardsOf(state);
126619
126636
  return { ...state, turnCards: rest };
126620
126637
  }
126638
+ case "chat-aliased":
126639
+ return foldAliased(state, data);
126621
126640
  default:
126622
126641
  return state;
126623
126642
  }
@@ -126990,6 +127009,7 @@ var readBots = (config2, adapterFor) => {
126990
127009
  };
126991
127010
  var bindingOf = (bot, chatId, state) => ({
126992
127011
  ...bot,
127012
+ reply: void 0,
126993
127013
  chatId,
126994
127014
  projectId: chatOf(state, chatKeyOf({ rowId: bot.rowId, chatId })).projectId ?? ""
126995
127015
  });
@@ -127044,7 +127064,7 @@ var numberedApprovalText = (request) => [
127044
127064
  ].join("\n");
127045
127065
 
127046
127066
  // src/approvals.ts
127047
- var makeApprovals = (core) => {
127067
+ var makeApprovals = (core, anchors) => {
127048
127068
  const { host, t, adapterFor, commit, outbound, bindingForChatKey, bindingOfThread, ready } = core;
127049
127069
  const settleApprovalCard = async (approval, settlement) => {
127050
127070
  if (approval.cardId === void 0) return;
@@ -127080,7 +127100,7 @@ var makeApprovals = (core) => {
127080
127100
  };
127081
127101
  const sendApprovalCard = async (binding, adapter, threadId, offer) => {
127082
127102
  try {
127083
- const cardId = await adapter.askApproval(binding, {
127103
+ const cardId = await adapter.askApproval(await anchors.anchored(binding, threadId, null), {
127084
127104
  title: offer.title,
127085
127105
  body: offer.body,
127086
127106
  options: offer.buttons,
@@ -127224,7 +127244,7 @@ var makeChatSetup = (core, deps) => {
127224
127244
  listedProjectsByChat.set(chatKey, listedProjectIds(rows));
127225
127245
  if (rows.length > 0 && hasPanel(binding)) {
127226
127246
  try {
127227
- await deps.openPicker(chatKey, rows);
127247
+ await deps.openPicker(chatKey, rows, binding.reply);
127228
127248
  return;
127229
127249
  } catch (error) {
127230
127250
  host.log.warn("chat could not open the project picker card", {
@@ -127574,7 +127594,7 @@ var makeCommands = (core, deps) => {
127574
127594
  case "panel":
127575
127595
  if (hasPanel(binding)) {
127576
127596
  try {
127577
- await openPanel(chatKey);
127597
+ await openPanel(chatKey, binding.reply);
127578
127598
  return true;
127579
127599
  } catch (error) {
127580
127600
  host.log.warn("chat could not open the command panel", {
@@ -127694,51 +127714,339 @@ var makeCommands = (core, deps) => {
127694
127714
  return { handleCommand, targetOf };
127695
127715
  };
127696
127716
 
127717
+ // src/keyedQueue.ts
127718
+ var makeKeyedQueue = (onError) => {
127719
+ const tails = /* @__PURE__ */ new Map();
127720
+ return (key, work) => {
127721
+ const run = (tails.get(key) ?? Promise.resolve()).then(work).catch(onError).then(() => {
127722
+ if (tails.get(key) === run) tails.delete(key);
127723
+ });
127724
+ tails.set(key, run);
127725
+ };
127726
+ };
127727
+
127697
127728
  // src/connections.ts
127729
+ var connectionKey = (adapter, bot) => JSON.stringify([
127730
+ adapter.platform,
127731
+ bot.displayName,
127732
+ // 「跟机器默认」与「不加代理」是两种选择,分开记。
127733
+ bot.proxy === void 0 ? [] : [bot.proxy],
127734
+ bot.groupMention,
127735
+ [...adapter.columns.map((column) => column.key), ...adapter.unlistedValues ?? []].map(
127736
+ (key) => bot.values[key] ?? ""
127737
+ )
127738
+ ]);
127698
127739
  var startConnections = (core, inboundHandlersFor) => {
127699
127740
  const { host, adapterFor, ready } = core;
127700
- const bots = core.bots();
127701
- const connections = [];
127702
- const started = /* @__PURE__ */ new Set();
127741
+ const live = /* @__PURE__ */ new Map();
127742
+ const enqueueForRow = makeKeyedQueue((error) => {
127743
+ host.log.warn("chat could not start an inbound connection", { error: describeError(error) });
127744
+ });
127703
127745
  let disposed = false;
127704
- for (const bot of bots) {
127746
+ const open = (bot, key) => {
127705
127747
  const adapter = adapterFor(bot.platform);
127706
- if (adapter === void 0) continue;
127707
- const fingerprint = `${bot.platform}:${adapter.columns.map((column) => bot.values[column.key] ?? "").join(" ")}`;
127708
- if (started.has(fingerprint)) {
127709
- host.log.warn("chat did not connect a bot that repeats another bot's credentials", {
127710
- bot: bot.displayName,
127711
- platform: bot.platform
127712
- });
127713
- continue;
127714
- }
127715
- started.add(fingerprint);
127716
- void ready.then(() => adapter.connect(bot, inboundHandlersFor(bot))).then((connection) => {
127717
- if (disposed) connection.close();
127718
- else connections.push(connection);
127719
- }).catch((error) => {
127720
- host.log.warn("chat could not start an inbound connection", {
127721
- bot: bot.displayName,
127722
- platform: bot.platform,
127723
- error: describeError(error)
127724
- });
127748
+ let closed = false;
127749
+ let closeConnection;
127750
+ enqueueForRow(bot.rowId, async () => {
127751
+ try {
127752
+ await ready;
127753
+ if (closed || adapter === void 0) return;
127754
+ const connection = await adapter.connect(bot, inboundHandlersFor(bot));
127755
+ if (closed) connection.close();
127756
+ else closeConnection = () => connection.close();
127757
+ } catch (error) {
127758
+ host.log.warn("chat could not start an inbound connection", {
127759
+ bot: bot.displayName,
127760
+ platform: bot.platform,
127761
+ error: describeError(error)
127762
+ });
127763
+ }
127725
127764
  });
127726
- }
127765
+ return {
127766
+ key,
127767
+ close: () => {
127768
+ closed = true;
127769
+ closeConnection?.();
127770
+ }
127771
+ };
127772
+ };
127773
+ const reconcile = () => {
127774
+ const bots = disposed ? [] : core.bots();
127775
+ const wanted = /* @__PURE__ */ new Map();
127776
+ const credentials3 = /* @__PURE__ */ new Set();
127777
+ for (const bot of bots) {
127778
+ const adapter = adapterFor(bot.platform);
127779
+ if (adapter === void 0) continue;
127780
+ const fingerprint = `${bot.platform}:${adapter.columns.map((column) => bot.values[column.key] ?? "").join(" ")}`;
127781
+ if (credentials3.has(fingerprint)) {
127782
+ host.log.warn("chat did not connect a bot that repeats another bot's credentials", {
127783
+ bot: bot.displayName,
127784
+ platform: bot.platform
127785
+ });
127786
+ continue;
127787
+ }
127788
+ credentials3.add(fingerprint);
127789
+ wanted.set(bot.rowId, { bot, key: connectionKey(adapter, bot) });
127790
+ }
127791
+ for (const [rowId, connection] of live) {
127792
+ if (wanted.get(rowId)?.key === connection.key) continue;
127793
+ connection.close();
127794
+ live.delete(rowId);
127795
+ }
127796
+ for (const [rowId, { bot, key }] of wanted) {
127797
+ if (!live.has(rowId)) live.set(rowId, open(bot, key));
127798
+ }
127799
+ return { bots: bots.length, connecting: wanted.size };
127800
+ };
127727
127801
  host.onDispose(() => {
127728
127802
  disposed = true;
127729
- for (const connection of connections) connection.close();
127803
+ reconcile();
127730
127804
  });
127731
- return { bots: bots.length, connecting: started.size };
127805
+ return { reconcile };
127732
127806
  };
127733
127807
 
127734
- // src/keyedQueue.ts
127735
- var makeKeyedQueue = (onError) => {
127736
- const tails = /* @__PURE__ */ new Map();
127737
- return (key, work) => {
127738
- const run = (tails.get(key) ?? Promise.resolve()).then(work).catch(onError).then(() => {
127739
- if (tails.get(key) === run) tails.delete(key);
127808
+ // src/deliveryHttp.ts
127809
+ import * as NodeCrypto from "node:crypto";
127810
+ import * as NodeHttp from "node:http";
127811
+
127812
+ // src/boundedBody.ts
127813
+ var readBoundedBody = (req, limit) => new Promise((resolve) => {
127814
+ const chunks = [];
127815
+ let size = 0;
127816
+ let settled = false;
127817
+ const finish = (result) => {
127818
+ if (settled) return;
127819
+ settled = true;
127820
+ resolve(result);
127821
+ };
127822
+ req.on("data", (chunk) => {
127823
+ if (settled) return;
127824
+ size += chunk.length;
127825
+ if (size > limit) {
127826
+ finish({ kind: "too-large" });
127827
+ req.destroy();
127828
+ return;
127829
+ }
127830
+ chunks.push(chunk);
127831
+ });
127832
+ req.on("end", () => finish({ kind: "ok", bytes: Buffer.concat(chunks) }));
127833
+ req.on("error", () => finish({ kind: "read-failed" }));
127834
+ req.on("close", () => finish({ kind: "read-failed" }));
127835
+ });
127836
+
127837
+ // src/deliveryContract.ts
127838
+ var DELIVERY_SEND_ERROR_CODES = [
127839
+ "bad-request",
127840
+ "unknown-bot",
127841
+ "unknown-target",
127842
+ "bot-not-connected",
127843
+ "delivery-failed"
127844
+ ];
127845
+ var isDeliverySendErrorCode = (value2) => typeof value2 === "string" && DELIVERY_SEND_ERROR_CODES.includes(value2);
127846
+ var DeliverySendError = class extends Error {
127847
+ code;
127848
+ constructor(code2, message) {
127849
+ super(message);
127850
+ this.name = "DeliverySendError";
127851
+ this.code = code2;
127852
+ }
127853
+ };
127854
+
127855
+ // src/deliveryHttp.ts
127856
+ var LOOPBACK = "127.0.0.1";
127857
+ var MAX_BODY_BYTES = 1 * 1024 * 1024;
127858
+ var PATH = "/v1/delivery/messages";
127859
+ var statusFor = (code2) => {
127860
+ switch (code2) {
127861
+ case "bad-request":
127862
+ return 400;
127863
+ case "unauthorized":
127864
+ return 401;
127865
+ case "unknown-bot":
127866
+ case "unknown-target":
127867
+ return 404;
127868
+ case "bot-not-connected":
127869
+ return 503;
127870
+ case "delivery-failed":
127871
+ return 502;
127872
+ case "method-not-allowed":
127873
+ return 405;
127874
+ case "unsupported-media-type":
127875
+ return 415;
127876
+ case "payload-too-large":
127877
+ return 413;
127878
+ }
127879
+ };
127880
+ var writeJson = (res, status, body) => {
127881
+ const payload = JSON.stringify(body);
127882
+ res.writeHead(status, {
127883
+ "content-type": "application/json; charset=utf-8",
127884
+ "content-length": Buffer.byteLength(payload),
127885
+ "cache-control": "no-store"
127886
+ });
127887
+ res.end(payload);
127888
+ };
127889
+ var writeError = (res, code2, message) => {
127890
+ writeJson(res, statusFor(code2), { error: { code: code2, message } });
127891
+ };
127892
+ var isLoopbackRemote = (remote) => {
127893
+ if (remote === void 0 || remote === "") return false;
127894
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
127895
+ };
127896
+ var timingSafeEqualString = (a, b) => {
127897
+ const left = Buffer.from(a);
127898
+ const right = Buffer.from(b);
127899
+ if (left.length !== right.length) return false;
127900
+ return NodeCrypto.timingSafeEqual(left, right);
127901
+ };
127902
+ var parseSendBody = (raw) => {
127903
+ let parsed;
127904
+ try {
127905
+ parsed = JSON.parse(raw.toString("utf8"));
127906
+ } catch {
127907
+ return "bad-request";
127908
+ }
127909
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return "bad-request";
127910
+ const record2 = parsed;
127911
+ const botId = record2["botId"];
127912
+ const targetId = record2["targetId"];
127913
+ const text3 = record2["text"];
127914
+ const format2 = record2["format"];
127915
+ if (typeof botId !== "string" || botId.trim() === "") return "bad-request";
127916
+ if (typeof targetId !== "string" || targetId.trim() === "") return "bad-request";
127917
+ if (typeof text3 !== "string" || text3.trim() === "") return "bad-request";
127918
+ if (format2 !== void 0 && format2 !== "plain" && format2 !== "markdown") return "bad-request";
127919
+ for (const key of Object.keys(record2)) {
127920
+ if (key !== "botId" && key !== "targetId" && key !== "text" && key !== "format") {
127921
+ return "bad-request";
127922
+ }
127923
+ }
127924
+ return {
127925
+ botId: botId.trim(),
127926
+ targetId: targetId.trim(),
127927
+ text: text3,
127928
+ format: format2 === "markdown" ? "markdown" : format2 === "plain" ? "plain" : void 0
127929
+ };
127930
+ };
127931
+ var startDeliveryHttpServer = async (options) => {
127932
+ const server = NodeHttp.createServer((req, res) => {
127933
+ void (async () => {
127934
+ if (!isLoopbackRemote(req.socket.remoteAddress)) {
127935
+ writeError(res, "unauthorized", "Only loopback clients are accepted.");
127936
+ return;
127937
+ }
127938
+ const url = new URL(req.url ?? "/", `http://${LOOPBACK}`);
127939
+ if (url.pathname !== PATH) {
127940
+ writeError(res, "bad-request", `Unknown path '${url.pathname}'.`);
127941
+ return;
127942
+ }
127943
+ if (req.method !== "POST") {
127944
+ res.setHeader("allow", "POST");
127945
+ writeError(res, "method-not-allowed", "Only POST is accepted.");
127946
+ return;
127947
+ }
127948
+ const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
127949
+ if (contentType !== "application/json") {
127950
+ writeError(res, "unsupported-media-type", "Content-Type must be application/json.");
127951
+ return;
127952
+ }
127953
+ if (options.token !== "") {
127954
+ const header = req.headers.authorization ?? "";
127955
+ const prefix = "Bearer ";
127956
+ if (!header.startsWith(prefix) || !timingSafeEqualString(header.slice(prefix.length), options.token)) {
127957
+ writeError(res, "unauthorized", "Bearer token required.");
127958
+ return;
127959
+ }
127960
+ }
127961
+ const read = await readBoundedBody(req, MAX_BODY_BYTES);
127962
+ if (read.kind !== "ok") {
127963
+ if (read.kind === "too-large") {
127964
+ writeError(res, "payload-too-large", "JSON body exceeds 1 MiB.");
127965
+ } else {
127966
+ writeError(res, "bad-request", "Failed to read body.");
127967
+ }
127968
+ return;
127969
+ }
127970
+ const parsed = parseSendBody(read.bytes);
127971
+ if (typeof parsed === "string") {
127972
+ writeError(res, parsed, "Invalid JSON body.");
127973
+ return;
127974
+ }
127975
+ try {
127976
+ const result = await options.send(parsed);
127977
+ writeJson(res, 200, { sent: result.sent });
127978
+ } catch (error) {
127979
+ const rawCode = error instanceof Error ? error.code : void 0;
127980
+ const code2 = isDeliverySendErrorCode(rawCode) ? rawCode : "delivery-failed";
127981
+ const message = error instanceof Error ? error.message : String(error);
127982
+ options.log?.warn("delivery http send failed", { code: code2, message });
127983
+ writeError(res, code2, message);
127984
+ }
127985
+ })().catch((error) => {
127986
+ options.log?.warn("delivery http handler crashed", {
127987
+ error: error instanceof Error ? error.message : String(error)
127988
+ });
127989
+ if (!res.headersSent) writeError(res, "delivery-failed", "Internal error.");
127740
127990
  });
127741
- tails.set(key, run);
127991
+ });
127992
+ await new Promise((resolve, reject) => {
127993
+ server.once("error", reject);
127994
+ server.listen(options.port, LOOPBACK, () => resolve());
127995
+ });
127996
+ const address = server.address();
127997
+ const port = address.port;
127998
+ options.log?.info("delivery http listening", { host: LOOPBACK, port, path: PATH });
127999
+ return {
128000
+ port,
128001
+ origin: `http://${LOOPBACK}:${String(port)}`,
128002
+ close: () => new Promise((resolve, reject) => {
128003
+ server.close((error) => error ? reject(error) : resolve());
128004
+ })
128005
+ };
128006
+ };
128007
+
128008
+ // src/deliveryHttpLifecycle.ts
128009
+ var makeDeliveryHttp = (input) => {
128010
+ const start = input.start ?? startDeliveryHttpServer;
128011
+ let running;
128012
+ let disposed = false;
128013
+ const stop = async () => {
128014
+ const current = running;
128015
+ running = void 0;
128016
+ if (current === void 0) return;
128017
+ await current.server.close().catch((error) => {
128018
+ input.log.warn("delivery http could not close", { error: describeError(error) });
128019
+ });
128020
+ };
128021
+ input.onDispose(() => {
128022
+ disposed = true;
128023
+ void stop();
128024
+ });
128025
+ return {
128026
+ apply: async (config2) => {
128027
+ const port = readDeliveryHttpPort(config2);
128028
+ const token = readDeliveryHttpToken(config2);
128029
+ const wanted = readDeliveryHttpEnabled(config2) ? `${String(port)}
128030
+ ${token}` : void 0;
128031
+ if (wanted === running?.listening) return;
128032
+ await stop();
128033
+ if (wanted === void 0 || disposed) return;
128034
+ try {
128035
+ const server = await start({
128036
+ port,
128037
+ token,
128038
+ send: input.send,
128039
+ log: {
128040
+ info: (message, fields) => input.log.info(message, fields),
128041
+ warn: (message, fields) => input.log.warn(message, fields)
128042
+ }
128043
+ });
128044
+ running = { listening: wanted, server };
128045
+ if (disposed) await stop();
128046
+ } catch (error) {
128047
+ input.log.warn("delivery http failed to listen", { port, error: describeError(error) });
128048
+ }
128049
+ }
127742
128050
  };
127743
128051
  };
127744
128052
 
@@ -127809,7 +128117,7 @@ var makeImCore = (input) => {
127809
128117
  host.log.warn("chat could not deliver", { error: describeError(error) });
127810
128118
  }
127811
128119
  };
127812
- const bots = () => readBots(input.config, adapterFor);
128120
+ const bots = () => readBots(input.config(), adapterFor);
127813
128121
  const bindingForChatKey = (chatKey) => findBindingByChatKey(bots(), state, chatKey);
127814
128122
  return {
127815
128123
  host,
@@ -128123,6 +128431,64 @@ var makeCooldown = (windowMs, now, maxKeys = Number.POSITIVE_INFINITY) => {
128123
128431
  };
128124
128432
  };
128125
128433
 
128434
+ // src/boundedMap.ts
128435
+ var makeBoundedMap = (limit) => {
128436
+ const entries = /* @__PURE__ */ new Map();
128437
+ return {
128438
+ get: (key) => entries.get(key),
128439
+ set: (key, value2) => {
128440
+ entries.delete(key);
128441
+ entries.set(key, value2);
128442
+ if (entries.size > limit) {
128443
+ const oldest = entries.keys().next().value;
128444
+ if (oldest !== void 0) entries.delete(oldest);
128445
+ }
128446
+ }
128447
+ };
128448
+ };
128449
+
128450
+ // src/replyAnchors.ts
128451
+ var THREAD_LIMIT = 256;
128452
+ var TURNS_PER_THREAD = 16;
128453
+ var anchorOfMessage = (adapter, message) => adapter?.quotesReplies === true ? { messageId: message.messageId, inThread: message.replyInThread === true } : void 0;
128454
+ var makeReplyAnchors = (host) => {
128455
+ const byThread = makeBoundedMap(THREAD_LIMIT);
128456
+ const userMessageOfTurn = async (threadId, turnId) => {
128457
+ const execution = await host.projections.getThreadExecution(threadId).catch(() => null);
128458
+ if (execution === null) return void 0;
128459
+ const latest = execution.latestTurn;
128460
+ const wanted = turnId ?? execution.session?.activeTurnId ?? latest?.turnId;
128461
+ if (wanted == null) return void 0;
128462
+ const intent = execution.turnIntents.find((candidate) => candidate.turn?.turnId === wanted);
128463
+ return intent?.messageId ?? (latest?.turnId === wanted ? latest.userMessageId : void 0);
128464
+ };
128465
+ return {
128466
+ /**
128467
+ * 开轮时记一笔:`userMessageId` 是派给宿主的那条用户消息,`binding` 是收到聊天消息的那个
128468
+ * 聊天(带着它的锚点)。没有锚点(平台不引用)就不记。
128469
+ */
128470
+ note: (threadId, userMessageId, binding) => {
128471
+ if (binding.reply === void 0) return;
128472
+ const turns = byThread.get(threadId) ?? makeBoundedMap(TURNS_PER_THREAD);
128473
+ byThread.set(threadId, turns);
128474
+ turns.set(userMessageId, { chatKey: chatKeyOf(binding), anchor: binding.reply });
128475
+ },
128476
+ /**
128477
+ * 给发往这条会话的聊天挂上 `turnId` 那一轮的锚点(null 的含义见上);取不到就原样返回。
128478
+ *
128479
+ * 锚点只配给它那条消息所在的聊天:会话可以被另一个聊天接走(`/resume`),而回复接口只按被
128480
+ * 回复的消息寻址,挂错了答复就发回原来的聊天。
128481
+ */
128482
+ anchored: async (binding, threadId, turnId) => {
128483
+ const turns = byThread.get(threadId);
128484
+ if (turns === void 0) return binding;
128485
+ const asked = await userMessageOfTurn(threadId, turnId);
128486
+ const noted = asked === void 0 ? void 0 : turns.get(asked);
128487
+ return noted?.chatKey === chatKeyOf(binding) ? { ...binding, reply: noted.anchor } : binding;
128488
+ }
128489
+ };
128490
+ };
128491
+
128126
128492
  // src/inbound.ts
128127
128493
  var SEEN_MESSAGE_LIMIT = 256;
128128
128494
  var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
@@ -128139,7 +128505,7 @@ var makeInbound = (core, deps) => {
128139
128505
  const { questionFlow, handleButtonClick } = deps.interactions;
128140
128506
  const { answerByToken } = deps.approvals;
128141
128507
  const { handlePanelClick } = deps.panelWiring;
128142
- const { startTurn, receiveAttachmentsOnly } = deps.turns;
128508
+ const { startTurn, receiveAttachmentsOnly, hasWaitingAttachments } = deps.turns;
128143
128509
  const { fetchTeams, startTeamRun } = deps.teams;
128144
128510
  const { markWorking } = deps.acknowledgements;
128145
128511
  const archivedReplyCooldown = makeCooldown(ARCHIVED_REPLY_COOLDOWN_MS, () => Date.now());
@@ -128166,6 +128532,34 @@ var makeInbound = (core, deps) => {
128166
128532
  () => deliver(target2, t("reply.notAllowedYet", { id: sender }))
128167
128533
  );
128168
128534
  };
128535
+ const currentBot = (rowId) => bots().find((candidate) => candidate.rowId === rowId);
128536
+ const inheritFromParent = (bot, chatId, parentChatId) => {
128537
+ if (parentChatId === void 0) return;
128538
+ const chatKey = chatKeyOf({ rowId: bot.rowId, chatId });
128539
+ if (chatOf(core.state, chatKey).projectId) return;
128540
+ const parent = chatOf(core.state, chatKeyOf({ rowId: bot.rowId, chatId: parentChatId }));
128541
+ if (!parent.projectId) return;
128542
+ commit("chat-updated", {
128543
+ chatKey,
128544
+ projectId: parent.projectId,
128545
+ model: parent.model,
128546
+ plan: parent.plan
128547
+ });
128548
+ };
128549
+ const noteChatAlias = (bot, alias, chatId) => {
128550
+ if (chatAliasOf(core.state, bot.rowId, alias) !== void 0) return;
128551
+ commit("chat-aliased", { rowId: bot.rowId, alias, chatId });
128552
+ };
128553
+ const settleChat = (bot, message) => {
128554
+ if (message.chatAlias === void 0) return message;
128555
+ const known = chatAliasOf(core.state, bot.rowId, message.chatAlias);
128556
+ if (known === void 0) noteChatAlias(bot, message.chatAlias, message.chatId);
128557
+ return known === void 0 || known === message.chatId ? message : { ...message, chatId: known };
128558
+ };
128559
+ const openTurn = async (speaker, message) => {
128560
+ const threadId = await startTurn(speaker, message);
128561
+ if (threadId !== null) await markWorking(speaker, threadId, message.messageId);
128562
+ };
128169
128563
  const inboundHandlersFor = (bot) => ({
128170
128564
  onError: (message) => host.log.warn("chat inbound connection failed", {
128171
128565
  platform: bot.platform,
@@ -128173,10 +128567,16 @@ var makeInbound = (core, deps) => {
128173
128567
  message
128174
128568
  }),
128175
128569
  onButton: handleButtonClick,
128176
- onPanelClick: (click) => handlePanelClick(bot, click),
128177
- onMessage: (message) => {
128178
- const current = bots().find((candidate) => candidate.rowId === bot.rowId);
128570
+ onPanelClick: (click) => {
128571
+ const current = currentBot(bot.rowId);
128572
+ return current === void 0 ? null : handlePanelClick(current, click);
128573
+ },
128574
+ noteChatAlias: (alias, chatId) => noteChatAlias(bot, alias, chatId),
128575
+ knowsChatAlias: (alias) => chatAliasOf(core.state, bot.rowId, alias) !== void 0,
128576
+ onMessage: (received) => {
128577
+ const current = currentBot(bot.rowId);
128179
128578
  if (current === void 0) return;
128579
+ const message = settleChat(current, received);
128180
128580
  const arrived = bindingOf2(current, message.chatId);
128181
128581
  if (alreadyHandled(arrived, message.messageId)) return;
128182
128582
  if (message.chatKind !== void 0 && chatOf(core.state, chatKeyOf(arrived)).chatKind !== message.chatKind) {
@@ -128206,7 +128606,11 @@ var makeInbound = (core, deps) => {
128206
128606
  return;
128207
128607
  }
128208
128608
  enqueueForChat(chatKeyOf(arrived), async () => {
128209
- const target2 = bindingOf2(current, message.chatId);
128609
+ inheritFromParent(current, message.chatId, message.parentChatId);
128610
+ const target2 = {
128611
+ ...bindingOf2(current, message.chatId),
128612
+ reply: anchorOfMessage(adapterFor(current.platform), message)
128613
+ };
128210
128614
  const chatKey = chatKeyOf(target2);
128211
128615
  const teams = await fetchTeams(target2);
128212
128616
  if (await handleCommand(target2, message.text, teams ?? [], message.senderId)) return;
@@ -128261,8 +128665,15 @@ var makeInbound = (core, deps) => {
128261
128665
  await receiveAttachmentsOnly(target2, message);
128262
128666
  return;
128263
128667
  }
128264
- const threadId = await startTurn(target2, message);
128265
- if (threadId !== null) await markWorking(target2, threadId, message.messageId);
128668
+ const branchChatId = hasWaitingAttachments(chatKey) ? void 0 : message.branchChatId;
128669
+ if (branchChatId === void 0) {
128670
+ await openTurn(target2, message);
128671
+ return;
128672
+ }
128673
+ enqueueForChat(chatKeyOf({ rowId: current.rowId, chatId: branchChatId }), async () => {
128674
+ inheritFromParent(current, branchChatId, message.chatId);
128675
+ await openTurn({ ...bindingOf2(current, branchChatId), reply: target2.reply }, message);
128676
+ });
128266
128677
  });
128267
128678
  }
128268
128679
  });
@@ -128440,7 +128851,7 @@ var makeQuestionFlow = (host) => {
128440
128851
  if (record2 === void 0 || record2.resolved || item === void 0) return;
128441
128852
  try {
128442
128853
  const cardId = await cards.open(
128443
- target2.binding,
128854
+ await host.anchored(target2.binding, record2.threadId),
128444
128855
  questionCardOf(item, record2.items.indexOf(item), record2.items.length, t)
128445
128856
  );
128446
128857
  if (cardId === null) continue;
@@ -128678,7 +129089,7 @@ var makeQueueFlow = (host, deps) => {
128678
129089
  queuePaused: execution.queuePaused,
128679
129090
  intent: execution.intents.find((intent) => intent.intentId === intentId)
128680
129091
  });
128681
- const noteQueued = async (chatKey, threadId, messageId, text3) => {
129092
+ const noteQueued = async (chatKey, threadId, messageId, text3, reply) => {
128682
129093
  const target2 = host.target(chatKey);
128683
129094
  if (target2 === void 0) return;
128684
129095
  let execution;
@@ -128723,7 +129134,10 @@ var makeQueueFlow = (host, deps) => {
128723
129134
  host.commit("queue-settled", { intentId, outcome: view.outcome });
128724
129135
  return;
128725
129136
  }
128726
- const cardId = await cards.open(target2.binding, queueCardOf(record2, view, null, t));
129137
+ const cardId = await cards.open(
129138
+ { ...target2.binding, reply },
129139
+ queueCardOf(record2, view, null, t)
129140
+ );
128727
129141
  if (cardId === null) return;
128728
129142
  host.commit("queue-carded", { intentId, cardId });
128729
129143
  shown.set(intentId, queueViewKey(view, null));
@@ -128883,6 +129297,8 @@ var makeInteractions = (core, deps) => {
128883
129297
  const adapter = binding === void 0 ? void 0 : adapterFor(binding.platform);
128884
129298
  return binding === void 0 || adapter === void 0 ? void 0 : { binding, adapter };
128885
129299
  },
129300
+ // 提问是在跑的那一轮问的。
129301
+ anchored: (binding, threadId) => deps.anchors.anchored(binding, threadId, null),
128886
129302
  allowed: (chatKey, operatorId) => {
128887
129303
  const binding = bindingForChatKey(chatKey);
128888
129304
  if (binding !== void 0 && isAllowedSender(binding, operatorId)) return true;
@@ -128961,6 +129377,9 @@ var makeInteractions = (core, deps) => {
128961
129377
  const resolved = readRequestResolved(event);
128962
129378
  if (resolved === null) return;
128963
129379
  await ready;
129380
+ if (resolved.outcome !== "cancelled" && event.threadId !== null) {
129381
+ deps.rotateTurnCard(event.threadId);
129382
+ }
128964
129383
  if (resolved.requestKind === "user-input") {
128965
129384
  questionFlow.resolved(
128966
129385
  resolved.requestId,
@@ -130779,24 +131198,50 @@ var fetchTenantToken = async (input) => {
130779
131198
  }
130780
131199
  return { token: body.tenant_access_token, expiresInSeconds: body.expire ?? 0 };
130781
131200
  };
130782
- var sendChatText = async (input) => {
131201
+ var postMessage = async (input) => {
131202
+ const headers = {
131203
+ authorization: `Bearer ${input.token}`,
131204
+ "content-type": "application/json; charset=utf-8"
131205
+ };
131206
+ const message = { msg_type: input.msgType, content: JSON.stringify(input.content) };
131207
+ const sentOf = async (response2) => (await readEnvelope(input.operation, response2)).data;
131208
+ if (input.reply !== void 0) {
131209
+ try {
131210
+ const response2 = await input.http(
131211
+ `${LARK_BASE}/im/v1/messages/${encodeURIComponent(input.reply.messageId)}/reply`,
131212
+ {
131213
+ method: "POST",
131214
+ headers,
131215
+ body: JSON.stringify({
131216
+ ...message,
131217
+ ...input.reply.inThread ? { reply_in_thread: true } : {}
131218
+ })
131219
+ }
131220
+ );
131221
+ const sent = await sentOf(response2);
131222
+ if (input.reply.inThread) input.onThreadReply?.(sent?.thread_id || void 0);
131223
+ return sent?.message_id ?? null;
131224
+ } catch (error) {
131225
+ if (!(error instanceof LarkApiError)) throw error;
131226
+ }
131227
+ }
130783
131228
  const idType = input.receiveIdType ?? "chat_id";
130784
131229
  const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=${idType}`, {
130785
131230
  method: "POST",
130786
- headers: {
130787
- authorization: `Bearer ${input.token}`,
130788
- "content-type": "application/json; charset=utf-8"
130789
- },
130790
- body: JSON.stringify({
130791
- receive_id: input.chatId,
130792
- msg_type: "text",
130793
- // content 是一个 JSON **字符串**,不是对象——飞书这条接口就是这么定的。
130794
- content: JSON.stringify({ text: input.text })
130795
- })
131231
+ headers,
131232
+ body: JSON.stringify({ receive_id: input.chatId, ...message })
130796
131233
  });
130797
- const body = await readEnvelope("send", response);
130798
- return body.data?.message_id ?? null;
131234
+ return (await sentOf(response))?.message_id ?? null;
131235
+ };
131236
+ var fetchMessageThreadId = async (input) => {
131237
+ const response = await input.http(
131238
+ `${LARK_BASE}/im/v1/messages/${encodeURIComponent(input.messageId)}`,
131239
+ { headers: { authorization: `Bearer ${input.token}` } }
131240
+ );
131241
+ const body = await readEnvelope("message.get", response);
131242
+ return body.data?.items?.[0]?.thread_id || void 0;
130799
131243
  };
131244
+ var sendChatText = async (input) => postMessage({ ...input, operation: "send", msgType: "text", content: { text: input.text } });
130800
131245
  var LARK_WORKING_REACTION = "OnIt";
130801
131246
  var addReaction = async (input) => {
130802
131247
  const response = await input.http(
@@ -130856,54 +131301,19 @@ var uploadFile = async (input) => {
130856
131301
  }
130857
131302
  return body.data.file_key;
130858
131303
  };
130859
- var sendChatFile = async (input) => {
130860
- const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
130861
- method: "POST",
130862
- headers: {
130863
- authorization: `Bearer ${input.token}`,
130864
- "content-type": "application/json; charset=utf-8"
130865
- },
130866
- body: JSON.stringify({
130867
- receive_id: input.chatId,
130868
- msg_type: "file",
130869
- content: JSON.stringify({ file_key: input.fileKey })
130870
- })
130871
- });
130872
- const body = await readEnvelope("send.file", response);
130873
- return body.data?.message_id ?? null;
130874
- };
130875
- var sendChatImage = async (input) => {
130876
- const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
130877
- method: "POST",
130878
- headers: {
130879
- authorization: `Bearer ${input.token}`,
130880
- "content-type": "application/json; charset=utf-8"
130881
- },
130882
- body: JSON.stringify({
130883
- receive_id: input.chatId,
130884
- msg_type: "image",
130885
- content: JSON.stringify({ image_key: input.imageKey })
130886
- })
130887
- });
130888
- const body = await readEnvelope("send.image", response);
130889
- return body.data?.message_id ?? null;
130890
- };
130891
- var sendCard = async (input) => {
130892
- const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
130893
- method: "POST",
130894
- headers: {
130895
- authorization: `Bearer ${input.token}`,
130896
- "content-type": "application/json; charset=utf-8"
130897
- },
130898
- body: JSON.stringify({
130899
- receive_id: input.chatId,
130900
- msg_type: "interactive",
130901
- content: JSON.stringify(input.card)
130902
- })
130903
- });
130904
- const body = await readEnvelope("send.card", response);
130905
- return body.data?.message_id ?? null;
130906
- };
131304
+ var sendChatFile = async (input) => postMessage({
131305
+ ...input,
131306
+ operation: "send.file",
131307
+ msgType: "file",
131308
+ content: { file_key: input.fileKey }
131309
+ });
131310
+ var sendChatImage = async (input) => postMessage({
131311
+ ...input,
131312
+ operation: "send.image",
131313
+ msgType: "image",
131314
+ content: { image_key: input.imageKey }
131315
+ });
131316
+ var sendCard = async (input) => postMessage({ ...input, operation: "send.card", msgType: "interactive", content: input.card });
130907
131317
  var patchCard = async (input) => {
130908
131318
  const response = await input.http(
130909
131319
  `${LARK_BASE}/im/v1/messages/${encodeURIComponent(input.messageId)}`,
@@ -136204,7 +136614,8 @@ var STATUS_TEMPLATE = {
136204
136614
  working: "wathet",
136205
136615
  done: "green",
136206
136616
  error: "red",
136207
- interrupted: "orange"
136617
+ interrupted: "orange",
136618
+ continued: "grey"
136208
136619
  };
136209
136620
  var encoder = new TextEncoder();
136210
136621
  var stripAngleBrackets = (text3) => text3.replaceAll("<", "").replaceAll(">", "");
@@ -136388,9 +136799,9 @@ var styledText = (text3, style) => {
136388
136799
  if (style?.includes("underline")) out = `<u>${out}</u>`;
136389
136800
  return out;
136390
136801
  };
136391
- var nonEmpty3 = (value2) => typeof value2 === "string" && value2 !== "";
136802
+ var nonEmpty4 = (value2) => typeof value2 === "string" && value2 !== "";
136392
136803
  var resourceOf = (element, index) => {
136393
- if (element.tag === "img" && nonEmpty3(element.image_key)) {
136804
+ if (element.tag === "img" && nonEmpty4(element.image_key)) {
136394
136805
  return {
136395
136806
  fileKey: element.image_key,
136396
136807
  kind: "image",
@@ -136398,14 +136809,14 @@ var resourceOf = (element, index) => {
136398
136809
  mimeType: "image/png"
136399
136810
  };
136400
136811
  }
136401
- if ((element.tag === "media" || element.tag === "file") && nonEmpty3(element.file_key)) {
136812
+ if ((element.tag === "media" || element.tag === "file") && nonEmpty4(element.file_key)) {
136402
136813
  const video = element.tag === "media";
136403
- const name = nonEmpty3(element.file_name) ? element.file_name : video ? `video-${String(index)}.mp4` : `file-${String(index)}`;
136814
+ const name = nonEmpty4(element.file_name) ? element.file_name : video ? `video-${String(index)}.mp4` : `file-${String(index)}`;
136404
136815
  return {
136405
136816
  fileKey: element.file_key,
136406
136817
  kind: "file",
136407
136818
  name,
136408
- mimeType: video && !nonEmpty3(element.file_name) ? "video/mp4" : mimeFromName(name)
136819
+ mimeType: video && !nonEmpty4(element.file_name) ? "video/mp4" : mimeFromName(name)
136409
136820
  };
136410
136821
  }
136411
136822
  return null;
@@ -136417,11 +136828,11 @@ var serializeElement = (element, index, botIds) => {
136417
136828
  return styledText(element.text ?? "", element.style);
136418
136829
  case "a": {
136419
136830
  const label = element.text ?? element.href ?? "";
136420
- return nonEmpty3(element.href) ? `[${label}](${element.href})` : label;
136831
+ return nonEmpty4(element.href) ? `[${label}](${element.href})` : label;
136421
136832
  }
136422
136833
  case "at": {
136423
136834
  if (element.user_id !== void 0 && botIds.has(element.user_id)) return "";
136424
- return nonEmpty3(element.user_name) ? `@${element.user_name}` : "";
136835
+ return nonEmpty4(element.user_name) ? `@${element.user_name}` : "";
136425
136836
  }
136426
136837
  case "code_block":
136427
136838
  return `\`\`\`${element.language ?? ""}
@@ -136461,7 +136872,7 @@ var serializePost = (content, botIds = /* @__PURE__ */ new Set()) => {
136461
136872
  for (const element of elements) {
136462
136873
  const index = indexOf.get(element);
136463
136874
  const key = element.tag === "img" ? element.image_key : element.file_key;
136464
- if (index !== void 0 && nonEmpty3(key)) {
136875
+ if (index !== void 0 && nonEmpty4(key)) {
136465
136876
  placeholderByKey.set(key, placeholderOf(element, index));
136466
136877
  }
136467
136878
  }
@@ -136552,7 +136963,7 @@ var readText = (content) => {
136552
136963
  const text3 = readJson(content)?.["text"];
136553
136964
  return typeof text3 === "string" ? text3 : null;
136554
136965
  };
136555
- var parseInboundMessage = (event, appId, groupMention = "always") => {
136966
+ var parseInboundMessage = (event, appId, groupMention = "always", topicOnMention = true) => {
136556
136967
  if (event.sender?.sender_type === "app") return null;
136557
136968
  const message = event.message;
136558
136969
  if (message === void 0) return null;
@@ -136562,6 +136973,11 @@ var parseInboundMessage = (event, appId, groupMention = "always") => {
136562
136973
  const messageType = message.message_type ?? "";
136563
136974
  if (!passesGroupGate(event, appId, groupMention, messageType)) return null;
136564
136975
  const senderOpenId = event.sender?.sender_id?.open_id ?? null;
136976
+ const threadId = typeof message.thread_id === "string" && message.thread_id !== "" ? message.thread_id : void 0;
136977
+ const inThread = threadId !== void 0 || topicOnMention && mentionsApp(event, appId);
136978
+ const rootGiven = typeof message.root_id === "string" && message.root_id !== "";
136979
+ const rootId = rootGiven && message.root_id !== void 0 ? message.root_id : messageId;
136980
+ const topic = chatType === "p2p" || !inThread ? {} : threadId === void 0 ? { topic: { rootMessageId: messageId } } : { topic: { rootMessageId: rootId, threadId, rootGiven } };
136565
136981
  if (UNSUPPORTED_MESSAGE_TYPES.has(messageType)) {
136566
136982
  return {
136567
136983
  messageId,
@@ -136570,6 +136986,8 @@ var parseInboundMessage = (event, appId, groupMention = "always") => {
136570
136986
  senderOpenId,
136571
136987
  text: "",
136572
136988
  resources: [],
136989
+ inThread,
136990
+ ...topic,
136573
136991
  unsupportedType: messageType
136574
136992
  };
136575
136993
  }
@@ -136580,7 +136998,7 @@ var parseInboundMessage = (event, appId, groupMention = "always") => {
136580
136998
  const raw = post !== void 0 ? post.text : messageType === "text" ? readText(message.content) : null;
136581
136999
  const text3 = mentions.reduce((value2, mention) => value2.replaceAll(mention.key, ""), raw ?? "").trim();
136582
137000
  if (text3.length === 0 && resources.length === 0) return null;
136583
- return { messageId, chatId, chatType, senderOpenId, text: text3, resources };
137001
+ return { messageId, chatId, chatType, senderOpenId, text: text3, resources, inThread, ...topic };
136584
137002
  };
136585
137003
  var fetchMessageResource = async (input) => {
136586
137004
  const url = `${LARK_BASE}/im/v1/messages/${encodeURIComponent(
@@ -136656,7 +137074,13 @@ var startLarkInbound = (input) => {
136656
137074
  const dispatcher = new import_node_sdk.EventDispatcher({}).register({
136657
137075
  "im.message.receive_v1": (event) => {
136658
137076
  const mode = input.groupMention ?? "always";
136659
- const parsed = parseInboundMessage(event, input.appId, mode);
137077
+ const parse = (groupMention) => parseInboundMessage(
137078
+ event,
137079
+ input.appId,
137080
+ groupMention,
137081
+ input.topicOnMention
137082
+ );
137083
+ const parsed = parse(mode);
136660
137084
  if (parsed !== null) {
136661
137085
  input.onMessage(parsed);
136662
137086
  return {};
@@ -136666,7 +137090,7 @@ var startLarkInbound = (input) => {
136666
137090
  const chatId = chat?.chat_id;
136667
137091
  if (typeof chatId === "string") {
136668
137092
  void input.isSoloGroup(chatId).then((solo) => {
136669
- const relaxed = solo ? parseInboundMessage(event, input.appId, "never") : null;
137093
+ const relaxed = solo ? parse("never") : null;
136670
137094
  if (relaxed !== null) input.onMessage(relaxed);
136671
137095
  });
136672
137096
  }
@@ -136733,8 +137157,27 @@ var COLUMNS = [
136733
137157
  labelKey: "column.larkAppId.label",
136734
137158
  placeholderKey: "column.larkAppId.placeholder"
136735
137159
  },
136736
- { key: "larkAppSecret", kind: "password", labelKey: "column.larkAppSecret.label" }
137160
+ { key: "larkAppSecret", kind: "password", labelKey: "column.larkAppSecret.label" },
137161
+ // 被 @ 时答复要不要开话题。只有这个适配器读它,没填过就是开(见 `topicOnMentionOf`)。
137162
+ {
137163
+ key: "larkTopicOnMention",
137164
+ kind: "select",
137165
+ labelKey: "column.larkTopicOnMention.label",
137166
+ descriptionKey: "column.larkTopicOnMention.description",
137167
+ options: [
137168
+ { value: "on", labelKey: "settings.onOff.on" },
137169
+ { value: "off", labelKey: "settings.onOff.off" }
137170
+ ]
137171
+ }
136737
137172
  ];
137173
+ var topicOnMentionOf = (binding) => binding.values["larkTopicOnMention"] !== "off";
137174
+ var TOPIC_THREAD_CACHE_LIMIT = 2e3;
137175
+ var topicChatIdOf = (chatId, rootMessageId) => `${chatId}:${rootMessageId}`;
137176
+ var splitTopicChatId = (chatId) => {
137177
+ const at = chatId.indexOf(":");
137178
+ return at < 0 ? { chatId } : { chatId: chatId.slice(0, at), rootMessageId: chatId.slice(at + 1) };
137179
+ };
137180
+ var topicAliasOf = (chatId, threadId) => `${chatId}:${threadId}`;
136738
137181
  var credentials = (binding) => ({
136739
137182
  appId: binding.values["larkAppId"] ?? "",
136740
137183
  appSecret: binding.values["larkAppSecret"] ?? ""
@@ -136749,6 +137192,60 @@ var makeLarkAdapter = (deps) => {
136749
137192
  await deps.cacheSet(key, { token: minted.token });
136750
137193
  return minted.token;
136751
137194
  };
137195
+ const aliasPorts = /* @__PURE__ */ new Map();
137196
+ const topicThreadIds = makeBoundedMap(TOPIC_THREAD_CACHE_LIMIT);
137197
+ const learning = /* @__PURE__ */ new Map();
137198
+ const learnTopic = (binding, target2, threadId) => {
137199
+ const key = chatKeyOf(binding);
137200
+ const report = (id) => {
137201
+ topicThreadIds.set(key, id);
137202
+ target2.port?.noteChatAlias?.(topicAliasOf(target2.chatId, id), binding.chatId);
137203
+ };
137204
+ const known = threadId ?? topicThreadIds.get(key);
137205
+ if (known !== void 0) {
137206
+ report(known);
137207
+ return;
137208
+ }
137209
+ const group = chatKeyOf({ rowId: binding.rowId, chatId: target2.chatId });
137210
+ const inflight = learning.get(group) ?? /* @__PURE__ */ new Map();
137211
+ if (inflight.has(key)) return;
137212
+ learning.set(group, inflight);
137213
+ const reading = fetchMessageThreadId({
137214
+ http: deps.http,
137215
+ token: target2.token,
137216
+ messageId: target2.rootMessageId
137217
+ }).then((id) => {
137218
+ if (id !== void 0) report(id);
137219
+ }).catch((error) => {
137220
+ deps.log.warn("lark could not learn the topic of a chat", { error: describeError(error) });
137221
+ }).finally(() => {
137222
+ inflight.delete(key);
137223
+ if (inflight.size === 0) learning.delete(group);
137224
+ });
137225
+ inflight.set(key, reading);
137226
+ };
137227
+ const topicUnsettled = (rowId, groupId, topic) => {
137228
+ if (topic.rootGiven === true) return null;
137229
+ const inflight = learning.get(chatKeyOf({ rowId, chatId: groupId }));
137230
+ if (inflight === void 0) return null;
137231
+ const known = aliasPorts.get(rowId)?.knowsChatAlias?.(topicAliasOf(groupId, topic.threadId));
137232
+ return known === true ? null : Promise.all(inflight.values());
137233
+ };
137234
+ const sendTarget = async (binding) => {
137235
+ const port = aliasPorts.get(binding.rowId);
137236
+ const token = await tenantToken(binding);
137237
+ const { chatId, rootMessageId } = splitTopicChatId(binding.chatId);
137238
+ if (rootMessageId === void 0) {
137239
+ return { http: deps.http, token, chatId, reply: binding.reply };
137240
+ }
137241
+ return {
137242
+ http: deps.http,
137243
+ token,
137244
+ chatId,
137245
+ reply: binding.reply ?? { messageId: rootMessageId, inThread: true },
137246
+ onThreadReply: (threadId) => learnTopic(binding, { token, chatId, rootMessageId, port }, threadId)
137247
+ };
137248
+ };
136752
137249
  const soloGroupFor = (bot) => makeSoloGroupCheck(async (chatId) => {
136753
137250
  const stats = await fetchChatStats({
136754
137251
  http: deps.http,
@@ -136762,16 +137259,28 @@ var makeLarkAdapter = (deps) => {
136762
137259
  icon: "lark",
136763
137260
  label: "Lark",
136764
137261
  columns: COLUMNS,
137262
+ quotesReplies: true,
137263
+ parentChatIdOf: (chatId) => {
137264
+ const { chatId: groupId, rootMessageId } = splitTopicChatId(chatId);
137265
+ return rootMessageId === void 0 ? void 0 : groupId;
137266
+ },
136765
137267
  isConfigured: (binding) => {
136766
137268
  const { appId, appSecret } = credentials(binding);
136767
137269
  return appId !== "" && appSecret !== "";
136768
137270
  },
136769
137271
  connect: (binding, handlers) => {
136770
137272
  const { appId, appSecret } = credentials(binding);
136771
- return startLarkInbound({
137273
+ const port = {
137274
+ noteChatAlias: handlers.noteChatAlias,
137275
+ knowsChatAlias: handlers.knowsChatAlias
137276
+ };
137277
+ aliasPorts.set(binding.rowId, port);
137278
+ let open = true;
137279
+ const inbound = startLarkInbound({
136772
137280
  appId,
136773
137281
  appSecret,
136774
137282
  groupMention: binding.groupMention,
137283
+ topicOnMention: topicOnMentionOf(binding),
136775
137284
  isSoloGroup: soloGroupFor(binding),
136776
137285
  onError: handlers.onError,
136777
137286
  onCardAction: handlers.onButton,
@@ -136783,12 +137292,23 @@ var makeLarkAdapter = (deps) => {
136783
137292
  arg: click.arg
136784
137293
  }) ?? null,
136785
137294
  onMessage: (message) => {
136786
- handlers.onMessage({
137295
+ const { topic } = message;
137296
+ const deliver = () => handlers.onMessage({
136787
137297
  messageId: message.messageId,
136788
- chatId: message.chatId,
137298
+ // 已经开出来的话题就是它自己那个聊天;将要开出来的(主窗口里 @ 机器人)先算群的,
137299
+ // 由核心决定要不要分出去。
137300
+ ...topic === void 0 ? { chatId: message.chatId } : topic.threadId === void 0 ? {
137301
+ chatId: message.chatId,
137302
+ branchChatId: topicChatIdOf(message.chatId, topic.rootMessageId)
137303
+ } : {
137304
+ chatId: topicChatIdOf(message.chatId, topic.rootMessageId),
137305
+ parentChatId: message.chatId,
137306
+ chatAlias: topicAliasOf(message.chatId, topic.threadId)
137307
+ },
136789
137308
  senderId: message.senderOpenId,
136790
137309
  text: message.text,
136791
137310
  chatKind: message.chatType === "p2p" ? "private" : "group",
137311
+ replyInThread: message.inThread,
136792
137312
  resources: message.resources.map((resource) => ({
136793
137313
  key: resource.fileKey,
136794
137314
  name: resource.name,
@@ -136797,8 +137317,21 @@ var makeLarkAdapter = (deps) => {
136797
137317
  })),
136798
137318
  ...message.unsupportedType === void 0 ? {} : { unsupportedType: message.unsupportedType }
136799
137319
  });
137320
+ const pending = topic?.threadId === void 0 ? null : topicUnsettled(binding.rowId, message.chatId, {
137321
+ threadId: topic.threadId,
137322
+ rootGiven: topic.rootGiven
137323
+ });
137324
+ if (pending === null) deliver();
137325
+ else void pending.then(() => open ? deliver() : void 0);
136800
137326
  }
136801
137327
  });
137328
+ return {
137329
+ close: () => {
137330
+ open = false;
137331
+ if (aliasPorts.get(binding.rowId) === port) aliasPorts.delete(binding.rowId);
137332
+ inbound.close();
137333
+ }
137334
+ };
136802
137335
  },
136803
137336
  createGroup: async (binding, input) => createChat({
136804
137337
  http: deps.http,
@@ -136807,13 +137340,11 @@ var makeLarkAdapter = (deps) => {
136807
137340
  memberOpenIds: input.memberIds
136808
137341
  }),
136809
137342
  sendText: async (binding, text3, options) => {
136810
- const token = await tenantToken(binding);
137343
+ const target2 = await sendTarget(binding);
136811
137344
  const receiveIdType = options?.targetKind === "user" ? "open_id" : "chat_id";
136812
137345
  for (const chunk of splitForLark(text3)) {
136813
137346
  const messageId = await sendChatText({
136814
- http: deps.http,
136815
- token,
136816
- chatId: binding.chatId,
137347
+ ...target2,
136817
137348
  receiveIdType,
136818
137349
  text: chunk
136819
137350
  });
@@ -136841,35 +137372,39 @@ var makeLarkAdapter = (deps) => {
136841
137372
  }
136842
137373
  },
136843
137374
  sendImage: async (binding, image2) => {
136844
- const token = await tenantToken(binding);
137375
+ const target2 = await sendTarget(binding);
136845
137376
  const imageKey = await uploadImage({
136846
137377
  http: deps.http,
136847
- token,
137378
+ token: target2.token,
136848
137379
  bytes: image2.bytes,
136849
137380
  name: image2.name,
136850
137381
  mimeType: image2.mimeType
136851
137382
  });
136852
- await sendChatImage({ http: deps.http, token, chatId: binding.chatId, imageKey });
137383
+ await sendChatImage({
137384
+ ...target2,
137385
+ imageKey
137386
+ });
136853
137387
  deps.log.info("lark delivered an image", { name: image2.name });
136854
137388
  },
136855
137389
  sendFile: async (binding, file) => {
136856
- const token = await tenantToken(binding);
137390
+ const target2 = await sendTarget(binding);
136857
137391
  const fileKey = await uploadFile({
136858
137392
  http: deps.http,
136859
- token,
137393
+ token: target2.token,
136860
137394
  bytes: file.bytes,
136861
137395
  name: file.name,
136862
137396
  mimeType: file.mimeType
136863
137397
  });
136864
- await sendChatFile({ http: deps.http, token, chatId: binding.chatId, fileKey });
137398
+ await sendChatFile({
137399
+ ...target2,
137400
+ fileKey
137401
+ });
136865
137402
  deps.log.info("lark delivered a file", { name: file.name });
136866
137403
  },
136867
137404
  // 图 10 MB、文件 30 MB(飞书开放平台文档:上传图片接口、上传文件接口;空文件会被拒)。
136868
137405
  outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 30 * 1024 * 1024 },
136869
137406
  askApproval: async (binding, request) => sendCard({
136870
- http: deps.http,
136871
- token: await tenantToken(binding),
136872
- chatId: binding.chatId,
137407
+ ...await sendTarget(binding),
136873
137408
  card: buildApprovalCard({
136874
137409
  title: request.title,
136875
137410
  body: request.body,
@@ -136889,10 +137424,9 @@ var makeLarkAdapter = (deps) => {
136889
137424
  },
136890
137425
  turnCards: {
136891
137426
  throttleMs: LARK_CARD_THROTTLE_MS,
137427
+ rotatesOnInteraction: true,
136892
137428
  open: async (binding, view) => sendCard({
136893
- http: deps.http,
136894
- token: await tenantToken(binding),
136895
- chatId: binding.chatId,
137429
+ ...await sendTarget(binding),
136896
137430
  card: buildTurnCard(view, deps.translate)
136897
137431
  }),
136898
137432
  update: async (binding, cardId, view) => {
@@ -136906,9 +137440,7 @@ var makeLarkAdapter = (deps) => {
136906
137440
  },
136907
137441
  panel: {
136908
137442
  open: async (binding, card) => sendCard({
136909
- http: deps.http,
136910
- token: await tenantToken(binding),
136911
- chatId: binding.chatId,
137443
+ ...await sendTarget(binding),
136912
137444
  card: buildPanelCard(card)
136913
137445
  }),
136914
137446
  update: async (binding, cardId, card) => {
@@ -136922,9 +137454,7 @@ var makeLarkAdapter = (deps) => {
136922
137454
  },
136923
137455
  choiceCards: {
136924
137456
  open: async (binding, card) => sendCard({
136925
- http: deps.http,
136926
- token: await tenantToken(binding),
136927
- chatId: binding.chatId,
137457
+ ...await sendTarget(binding),
136928
137458
  card: buildChoiceCard(card)
136929
137459
  }),
136930
137460
  update: async (binding, cardId, card) => {
@@ -136976,7 +137506,7 @@ var en_default = {
136976
137506
  bots: "Bots",
136977
137507
  language: {
136978
137508
  label: "Reply language",
136979
- description: "The language of the bot's own sentences in the chat: help, status, confirmations and notices. Chinese shortcut commands only work when this is Chinese. Saving restarts the plugin.",
137509
+ description: "The language of the bot's own sentences in the chat: help, status, confirmations and notices. Chinese shortcut commands only work when this is Chinese. A change applies from the bot's next sentence.",
136980
137510
  zhCN: "\u4E2D\u6587",
136981
137511
  en: "English"
136982
137512
  },
@@ -137108,6 +137638,10 @@ var en_default = {
137108
137638
  label: "Command panel",
137109
137639
  description: "Which commands /panel (or typing /) lists. Minimal hides low-frequency system items; typing the same command still works."
137110
137640
  },
137641
+ larkTopicOnMention: {
137642
+ label: "Open a topic when mentioned",
137643
+ description: "When someone @-mentions the bot, its answer opens a Lark topic under that message, so the question and the answer stay together. Messages already inside a topic are answered in that topic either way."
137644
+ },
137111
137645
  wecomNumberedHints: {
137112
137646
  label: "Numbered-reply hints",
137113
137647
  description: "WeCom has no buttons; approvals and questions use reply numbers. When on, include fuller usage hints."
@@ -137337,7 +137871,8 @@ var en_default = {
137337
137871
  working: "Working on it\u2026",
137338
137872
  done: "Done",
137339
137873
  error: "Something went wrong",
137340
- interrupted: "Interrupted"
137874
+ interrupted: "Interrupted",
137875
+ continued: "Continues in the card below"
137341
137876
  },
137342
137877
  step: {
137343
137878
  think: "think",
@@ -137582,7 +138117,7 @@ var zh_CN_default = {
137582
138117
  bots: "\u673A\u5668\u4EBA",
137583
138118
  language: {
137584
138119
  label: "\u56DE\u8BDD\u8BED\u8A00",
137585
- description: "\u673A\u5668\u4EBA\u5728\u804A\u5929\u91CC\u81EA\u5DF1\u8BF4\u7684\u8BDD\u7528\u4EC0\u4E48\u8BED\u8A00\uFF1A\u5E2E\u52A9\u3001\u72B6\u6001\u3001\u786E\u8BA4\u548C\u5404\u79CD\u63D0\u793A\u3002\u4E2D\u6587\u5FEB\u6377\u547D\u4EE4\u53EA\u5728\u9009\u4E2D\u6587\u65F6\u8BA4\u3002\u4FDD\u5B58\u540E\u63D2\u4EF6\u4F1A\u91CD\u542F\u3002",
138120
+ description: "\u673A\u5668\u4EBA\u5728\u804A\u5929\u91CC\u81EA\u5DF1\u8BF4\u7684\u8BDD\u7528\u4EC0\u4E48\u8BED\u8A00\uFF1A\u5E2E\u52A9\u3001\u72B6\u6001\u3001\u786E\u8BA4\u548C\u5404\u79CD\u63D0\u793A\u3002\u4E2D\u6587\u5FEB\u6377\u547D\u4EE4\u53EA\u5728\u9009\u4E2D\u6587\u65F6\u8BA4\u3002\u6539\u4E86\u4E4B\u540E\u4ECE\u673A\u5668\u4EBA\u7684\u4E0B\u4E00\u53E5\u8BDD\u8D77\u751F\u6548\u3002",
137586
138121
  zhCN: "\u4E2D\u6587",
137587
138122
  en: "English"
137588
138123
  },
@@ -137714,6 +138249,10 @@ var zh_CN_default = {
137714
138249
  label: "\u6307\u4EE4\u9762\u677F",
137715
138250
  description: "\u804A\u5929\u91CC /panel \u6216\u6253 / \u65F6\u5217\u51FA\u7684\u547D\u4EE4\u8303\u56F4\u3002\u7CBE\u7B80\u9690\u85CF\u4F4E\u9891\u7CFB\u7EDF\u9879\uFF1B\u624B\u52A8\u8F93\u5165\u540C\u4E00\u547D\u4EE4\u4ECD\u53EF\u7528\u3002"
137716
138251
  },
138252
+ larkTopicOnMention: {
138253
+ label: "\u88AB @ \u65F6\u5F00\u8BDD\u9898",
138254
+ description: "\u6709\u4EBA @ \u673A\u5668\u4EBA\u63D0\u95EE\u65F6\uFF0C\u7B54\u590D\u5728\u90A3\u6761\u6D88\u606F\u4E0B\u5F00\u4E00\u4E2A\u98DE\u4E66\u8BDD\u9898\uFF0C\u4E00\u95EE\u4E00\u7B54\u6536\u5728\u8BDD\u9898\u91CC\u3002\u5DF2\u7ECF\u5728\u8BDD\u9898\u91CC\u7684\u6D88\u606F\uFF0C\u4E0D\u7BA1\u5F00\u4E0D\u5F00\uFF0C\u7B54\u590D\u90FD\u7559\u5728\u8BE5\u8BDD\u9898\u91CC\u3002"
138255
+ },
137717
138256
  wecomNumberedHints: {
137718
138257
  label: "\u7F16\u53F7\u5E94\u7B54\u63D0\u793A",
137719
138258
  description: "\u4F01\u5FAE\u6CA1\u6709\u6309\u94AE\uFF0C\u5BA1\u6279\u4E0E\u63D0\u95EE\u9760\u56DE\u7F16\u53F7\u3002\u5F00\u542F\u65F6\u9644\u5E26\u66F4\u5B8C\u6574\u7684\u7528\u6CD5\u8BF4\u660E\u3002"
@@ -137943,7 +138482,8 @@ var zh_CN_default = {
137943
138482
  working: "\u5904\u7406\u4E2D\u2026",
137944
138483
  done: "\u5DF2\u5B8C\u6210",
137945
138484
  error: "\u51FA\u9519\u4E86",
137946
- interrupted: "\u5DF2\u4E2D\u65AD"
138485
+ interrupted: "\u5DF2\u4E2D\u65AD",
138486
+ continued: "\u540E\u7EED\u89C1\u4E0B\u65B9\u5361\u7247"
137947
138487
  },
137948
138488
  step: {
137949
138489
  think: "\u601D\u8003",
@@ -138207,6 +138747,24 @@ var makeTranslate = (language) => {
138207
138747
  { optional, language }
138208
138748
  );
138209
138749
  };
138750
+ var makeLiveTranslate = (initial) => {
138751
+ let current = makeTranslate(initial);
138752
+ const t = Object.defineProperties(
138753
+ (key, params) => current(key, params),
138754
+ {
138755
+ optional: {
138756
+ value: (key, params) => current.optional(key, params)
138757
+ },
138758
+ language: { get: () => current.language }
138759
+ }
138760
+ );
138761
+ return {
138762
+ t,
138763
+ setLanguage: (language) => {
138764
+ if (language !== current.language) current = makeTranslate(language);
138765
+ }
138766
+ };
138767
+ };
138210
138768
 
138211
138769
  // src/modelPlan.ts
138212
138770
  var makeModelPlan = (core, deps) => {
@@ -138284,6 +138842,13 @@ var makeModelPlan = (core, deps) => {
138284
138842
  return { currentModelOf, handleModel, handlePlan };
138285
138843
  };
138286
138844
 
138845
+ // src/notificationTargets.ts
138846
+ var notificationChatKeys = (state, threadId, projectId) => {
138847
+ const owner = chatOfThread(state, threadId);
138848
+ if (owner !== void 0) return [owner];
138849
+ return projectId === null ? [] : chatsOnProject(state, projectId);
138850
+ };
138851
+
138287
138852
  // src/turnOutcome.ts
138288
138853
  var outcomeOfSession = (status) => {
138289
138854
  if (status === "error") return "failed";
@@ -138298,7 +138863,7 @@ var makeNotifications = (core, deps) => {
138298
138863
  const { host, adapterFor, deliver, bindingForChatKey, ready } = core;
138299
138864
  const { clearWorking } = deps;
138300
138865
  const { turnCards } = deps.replyCards;
138301
- const { failures } = deps;
138866
+ const { failures, anchors } = deps;
138302
138867
  const projectByThread = /* @__PURE__ */ new Map();
138303
138868
  const projectOfThread = async (threadId) => {
138304
138869
  const known = projectByThread.get(threadId);
@@ -138312,7 +138877,7 @@ var makeNotifications = (core, deps) => {
138312
138877
  projectByThread.set(threadId, thread.projectId);
138313
138878
  return thread.projectId;
138314
138879
  };
138315
- const targetsForProject = (projectId) => projectId === null ? [] : chatsOnProject(core.state, projectId).flatMap((chatKey) => {
138880
+ const targetsForProject = (threadId, projectId) => notificationChatKeys(core.state, threadId, projectId).flatMap((chatKey) => {
138316
138881
  const binding = bindingForChatKey(chatKey);
138317
138882
  return binding === void 0 ? [] : [binding];
138318
138883
  });
@@ -138338,7 +138903,7 @@ var makeNotifications = (core, deps) => {
138338
138903
  if (payload.session?.activeTurnId != null) return;
138339
138904
  const said = threadsWithSomethingToReport.delete(threadId);
138340
138905
  await ready;
138341
- const targets = targetsForProject(await projectOfThread(threadId));
138906
+ const targets = targetsForProject(threadId, await projectOfThread(threadId));
138342
138907
  const reporters = said ? targets : [];
138343
138908
  if (reporters.length > 0) {
138344
138909
  const thread = await host.projections.getThread(threadId);
@@ -138348,7 +138913,12 @@ var makeNotifications = (core, deps) => {
138348
138913
  const text3 = streamsTo(binding, threadId) ? "" : `${thread.title}
138349
138914
 
138350
138915
  ${final.text}`;
138351
- await deliver(binding, text3, final.attachments);
138916
+ const owns = chatOf(core.state, chatKeyOf(binding)).threadId === threadId;
138917
+ await deliver(
138918
+ owns ? await anchors.anchored(binding, threadId, final.turnId) : binding,
138919
+ text3,
138920
+ final.attachments
138921
+ );
138352
138922
  }
138353
138923
  }
138354
138924
  }
@@ -138361,7 +138931,7 @@ ${final.text}`;
138361
138931
  if (threadId === null) return;
138362
138932
  threadsWithSomethingToReport.delete(threadId);
138363
138933
  await ready;
138364
- const targets = targetsForProject(await projectOfThread(threadId));
138934
+ const targets = targetsForProject(threadId, await projectOfThread(threadId));
138365
138935
  const textTargets = targets.filter(
138366
138936
  (binding) => !(streamsTo(binding, threadId) && turnCards.covers(threadId))
138367
138937
  );
@@ -138809,8 +139379,8 @@ ${t("panel.projects.hint")}`;
138809
139379
  };
138810
139380
  return {
138811
139381
  /** 开一张新面板卡(`/help`)。发不出就抛,调用方退回文字版。 */
138812
- open: async (chatKey) => {
138813
- const surface = deps.surface(chatKey);
139382
+ open: async (chatKey, reply) => {
139383
+ const surface = deps.surface(chatKey, reply);
138814
139384
  if (surface === null) throw new Error("this chat has no panel");
138815
139385
  const entry = { cardId: "", chatKey, stack: [{ kind: "menu" }], busy: false };
138816
139386
  const cardId = await surface.open(menuCard(chatKey));
@@ -138822,8 +139392,8 @@ ${t("panel.projects.hint")}`;
138822
139392
  * 单独发一张选项目卡(聊天还没选项目时的第一句话、`/repo`)。发不出就抛,调用方退回文字
138823
139393
  * 列表。选完不回面板菜单。
138824
139394
  */
138825
- openProjects: async (chatKey, rows) => {
138826
- const surface = deps.surface(chatKey);
139395
+ openProjects: async (chatKey, rows, reply) => {
139396
+ const surface = deps.surface(chatKey, reply);
138827
139397
  if (surface === null) throw new Error("this chat has no panel");
138828
139398
  const entry = {
138829
139399
  cardId: "",
@@ -138837,6 +139407,11 @@ ${t("panel.projects.hint")}`;
138837
139407
  entry.cardId = cardId;
138838
139408
  remember(entry);
138839
139409
  },
139410
+ /**
139411
+ * 这张卡开在哪个聊天上。没记过,或已经被淘汰,就是 undefined。
139412
+ * 只读:不改 `cards`,也不改淘汰顺序。
139413
+ */
139414
+ chatKeyOfCard: (cardId) => cards.get(cardId)?.chatKey,
138840
139415
  /**
138841
139416
  * 处理一次点击,同步回给点击者的一句提示(null 就是没话说)。取数据、跑命令、改卡都
138842
139417
  * 在回调应答之后做。
@@ -138908,12 +139483,12 @@ var makePanelWiring = (core, deps) => {
138908
139483
  const { projectRowsFor, listedProjectsByChat } = deps.rows;
138909
139484
  const panel = makePanelController({
138910
139485
  t,
138911
- surface: (chatKey) => {
139486
+ surface: (chatKey, reply) => {
138912
139487
  const binding = bindingForChatKey(chatKey);
138913
139488
  const surface = binding === void 0 ? void 0 : adapterFor(binding.platform)?.panel;
138914
139489
  if (binding === void 0 || surface === void 0) return null;
138915
139490
  return {
138916
- open: (card) => surface.open(binding, card),
139491
+ open: (card) => surface.open({ ...binding, reply }, card),
138917
139492
  update: (cardId, card) => surface.update(binding, cardId, card)
138918
139493
  };
138919
139494
  },
@@ -138965,7 +139540,10 @@ var makePanelWiring = (core, deps) => {
138965
139540
  }
138966
139541
  });
138967
139542
  const handlePanelClick = (bot, click) => {
138968
- const target2 = bindingOf2(bot, click.chatId);
139543
+ const remembered = panel.chatKeyOfCard(click.cardId);
139544
+ const opener = remembered === void 0 ? void 0 : bindingForChatKey(remembered);
139545
+ const chatId = opener !== void 0 && opener.rowId === bot.rowId && adapterFor(opener.platform)?.parentChatIdOf?.(opener.chatId) === click.chatId ? opener.chatId : click.chatId;
139546
+ const target2 = bindingOf2(bot, chatId);
138969
139547
  if (!isAllowedSender(target2, click.operatorId)) {
138970
139548
  host.log.warn("chat ignored a panel click from someone not on the allow list", {
138971
139549
  bot: target2.displayName,
@@ -139111,6 +139689,8 @@ var trailingReply = (messages) => {
139111
139689
  };
139112
139690
 
139113
139691
  // src/turnCards.ts
139692
+ var ROTATE = "rotate";
139693
+ var isDrawing = (live) => live.card !== null || live.reopen;
139114
139694
  var RECENT_ENDED_LIMIT = 256;
139115
139695
  var makeTurnCards = (deps) => {
139116
139696
  const slots = /* @__PURE__ */ new Map();
@@ -139147,12 +139727,37 @@ var makeTurnCards = (deps) => {
139147
139727
  status: end.status,
139148
139728
  errorText: end.errorText
139149
139729
  });
139730
+ const openCard = (target2, view) => makeCardStream({
139731
+ open: target2.open,
139732
+ update: target2.update,
139733
+ schedule: deps.schedule,
139734
+ throttleMs: target2.throttleMs,
139735
+ warn: deps.warn
139736
+ }).open(view);
139737
+ const seal = async (slot) => {
139738
+ await deps.ready;
139739
+ const live = slot.live;
139740
+ if (disposed || live === null || live.card === null || !live.target.rotates) return;
139741
+ const card = live.card;
139742
+ await card.finish(
139743
+ buildTurnView({
139744
+ title: live.title,
139745
+ progress: live.lastProgress,
139746
+ texts: await readTexts(live, live.lastProgress),
139747
+ status: "continued"
139748
+ })
139749
+ );
139750
+ deps.onClosed(card.cardId);
139751
+ live.card = null;
139752
+ live.reopen = true;
139753
+ };
139150
139754
  const finalize = async (slot, threadId, progress, end) => {
139151
139755
  const live = slot.live;
139152
139756
  slot.live = null;
139153
- const target2 = live?.target ?? deps.resolveTarget(threadId);
139757
+ const turnId = progress.turnId ?? live?.turnId ?? null;
139758
+ const target2 = live?.target ?? deps.resolveTarget(threadId, turnId);
139154
139759
  if (target2 === null) return;
139155
- rememberEnded(threadId, progress.turnId ?? live?.turnId ?? null);
139760
+ rememberEnded(threadId, turnId);
139156
139761
  if (end.status === "error") erroredThreads.add(threadId);
139157
139762
  const title = live?.title ?? await deps.titleOf(threadId);
139158
139763
  const view = await finalView(title, progress, live, end);
@@ -139161,15 +139766,9 @@ var makeTurnCards = (deps) => {
139161
139766
  deps.onClosed(live.card.cardId);
139162
139767
  return;
139163
139768
  }
139164
- const card = await makeCardStream({
139165
- open: target2.open,
139166
- update: target2.update,
139167
- schedule: deps.schedule,
139168
- throttleMs: target2.throttleMs,
139169
- warn: deps.warn
139170
- }).open(view);
139769
+ const card = await openCard(target2, view);
139171
139770
  if (card === null) {
139172
- await deps.fallback(threadId, target2.chatKey, view);
139771
+ await deps.fallback(threadId, turnId, target2.chatKey, view);
139173
139772
  return;
139174
139773
  }
139175
139774
  };
@@ -139188,38 +139787,33 @@ var makeTurnCards = (deps) => {
139188
139787
  }
139189
139788
  }
139190
139789
  if (item.phase === "ended") {
139191
- if (live === null && deps.resolveTarget(threadId) === null) return;
139790
+ if (live === null && deps.resolveTarget(threadId, item.turnId) === null) return;
139192
139791
  await finalize(slot, threadId, item, await deps.endOf(threadId, item.turnId));
139193
139792
  return;
139194
139793
  }
139195
139794
  erroredThreads.delete(threadId);
139196
139795
  if (live === null) {
139197
- const target2 = deps.resolveTarget(threadId);
139796
+ const target2 = deps.resolveTarget(threadId, item.turnId);
139198
139797
  if (target2 === null) return;
139199
139798
  const title = await deps.titleOf(threadId);
139200
139799
  const fresh = {
139201
139800
  chatKey: target2.chatKey,
139202
139801
  title,
139203
139802
  card: null,
139803
+ reopen: false,
139204
139804
  target: target2,
139205
139805
  turnId: item.turnId,
139206
139806
  lastSeq: item.seq,
139207
139807
  lastProgress: item,
139208
139808
  settledTexts: /* @__PURE__ */ new Map()
139209
139809
  };
139210
- const view = buildTurnView({
139810
+ const view2 = buildTurnView({
139211
139811
  title,
139212
139812
  progress: item,
139213
139813
  texts: await readTexts(fresh, item),
139214
139814
  status: "working"
139215
139815
  });
139216
- const card = await makeCardStream({
139217
- open: target2.open,
139218
- update: target2.update,
139219
- schedule: deps.schedule,
139220
- throttleMs: target2.throttleMs,
139221
- warn: deps.warn
139222
- }).open(view);
139816
+ const card = await openCard(target2, view2);
139223
139817
  slot.live = { ...fresh, card };
139224
139818
  if (card !== null) {
139225
139819
  deps.onOpened({ cardId: card.cardId, chatKey: target2.chatKey, threadId, title });
@@ -139229,15 +139823,27 @@ var makeTurnCards = (deps) => {
139229
139823
  live.turnId ??= item.turnId;
139230
139824
  live.lastSeq = item.seq;
139231
139825
  live.lastProgress = item;
139232
- if (live.card === null) return;
139233
- live.card.push(
139234
- buildTurnView({
139235
- title: live.title,
139236
- progress: item,
139237
- texts: await readTexts(live, item),
139238
- status: "working"
139239
- })
139240
- );
139826
+ if (live.card === null && !live.reopen) return;
139827
+ const view = buildTurnView({
139828
+ title: live.title,
139829
+ progress: item,
139830
+ texts: await readTexts(live, item),
139831
+ status: "working"
139832
+ });
139833
+ if (live.card !== null) {
139834
+ live.card.push(view);
139835
+ return;
139836
+ }
139837
+ live.reopen = false;
139838
+ live.card = await openCard(live.target, view);
139839
+ if (live.card !== null) {
139840
+ deps.onOpened({
139841
+ cardId: live.card.cardId,
139842
+ chatKey: live.chatKey,
139843
+ threadId,
139844
+ title: live.title
139845
+ });
139846
+ }
139241
139847
  };
139242
139848
  const pump = async (threadId, slot) => {
139243
139849
  try {
@@ -139245,7 +139851,8 @@ var makeTurnCards = (deps) => {
139245
139851
  const item = slot.queue.shift();
139246
139852
  if (item === void 0) break;
139247
139853
  try {
139248
- await handle(slot, threadId, item);
139854
+ if (item === ROTATE) await seal(slot);
139855
+ else await handle(slot, threadId, item);
139249
139856
  } catch (error) {
139250
139857
  deps.warn("could not draw a turn card", error);
139251
139858
  }
@@ -139265,7 +139872,7 @@ var makeTurnCards = (deps) => {
139265
139872
  slots.set(threadId, slot);
139266
139873
  }
139267
139874
  const last = slot.queue.at(-1);
139268
- if (last !== void 0 && last.phase === "running" && last.turnId === progress.turnId) {
139875
+ if (last !== void 0 && last !== ROTATE && last.phase === "running" && last.turnId === progress.turnId) {
139269
139876
  slot.queue[slot.queue.length - 1] = progress;
139270
139877
  } else {
139271
139878
  slot.queue.push(progress);
@@ -139275,13 +139882,29 @@ var makeTurnCards = (deps) => {
139275
139882
  void pump(threadId, slot);
139276
139883
  }
139277
139884
  },
139278
- /** 这条会话正在画卡的那一轮;没有在画的卡是 null。 */
139885
+ /**
139886
+ * 审批或提问答完了:封存这条会话开着的卡,下一拍另开一张。立刻返回;没有在画的卡、或
139887
+ * 平台不换卡,就什么都不做。
139888
+ */
139889
+ rotate: (threadId) => {
139890
+ const slot = slots.get(threadId);
139891
+ if (disposed || slot === void 0) return;
139892
+ slot.queue.push(ROTATE);
139893
+ if (!slot.running) {
139894
+ slot.running = true;
139895
+ void pump(threadId, slot);
139896
+ }
139897
+ },
139898
+ /** 这条会话正在画卡的那一轮(封存后等着另开的也算);没有在画的卡是 null。 */
139279
139899
  drawingTurn: (threadId) => {
139280
139900
  const live = slots.get(threadId)?.live;
139281
- return live?.card != null ? live.turnId : null;
139901
+ return live != null && isDrawing(live) ? live.turnId : null;
139282
139902
  },
139283
139903
  /** 这条会话现在有一张在画的卡,或者上一轮刚以失败收了卡:失败原因已经在卡上了。 */
139284
- covers: (threadId) => slots.get(threadId)?.live?.card != null || erroredThreads.has(threadId),
139904
+ covers: (threadId) => {
139905
+ const live = slots.get(threadId)?.live;
139906
+ return live != null && isDrawing(live) || erroredThreads.has(threadId);
139907
+ },
139285
139908
  /** 插件停用:放下所有在画的卡,不再发任何更新。卡会停在最后一拍,重启时被标「已中断」。 */
139286
139909
  dispose: () => {
139287
139910
  disposed = true;
@@ -139292,19 +139915,21 @@ var makeTurnCards = (deps) => {
139292
139915
  };
139293
139916
 
139294
139917
  // src/replyCards.ts
139295
- var makeReplyCards = (core, failures) => {
139918
+ var makeReplyCards = (core, failures, anchors) => {
139296
139919
  const { host, adapterFor, commit, deliver, bindingForChatKey, bindingOfThread, ready } = core;
139297
139920
  const stopRequested = /* @__PURE__ */ new Set();
139298
139921
  const turnCards = makeTurnCards({
139299
139922
  ready,
139300
- resolveTarget: (threadId) => {
139923
+ resolveTarget: (threadId, turnId) => {
139301
139924
  const binding = bindingOfThread(threadId);
139302
139925
  const cards = binding === void 0 ? void 0 : adapterFor(binding.platform)?.turnCards;
139303
139926
  if (binding === void 0 || cards === void 0) return null;
139927
+ let anchored;
139304
139928
  return {
139305
139929
  chatKey: chatKeyOf(binding),
139306
139930
  throttleMs: cards.throttleMs,
139307
- open: (view) => cards.open(binding, view),
139931
+ rotates: cards.rotatesOnInteraction === true,
139932
+ open: async (view) => cards.open(await (anchored ??= anchors.anchored(binding, threadId, turnId)), view),
139308
139933
  update: (cardId, view) => cards.update(binding, cardId, view)
139309
139934
  };
139310
139935
  },
@@ -139335,11 +139960,11 @@ var makeReplyCards = (core, failures) => {
139335
139960
  },
139336
139961
  onOpened: (card) => commit("turn-card-opened", card),
139337
139962
  onClosed: (cardId) => commit("turn-card-closed", { cardId }),
139338
- fallback: async (_threadId, chatKey, view) => {
139963
+ fallback: async (threadId, turnId, chatKey, view) => {
139339
139964
  const binding = bindingForChatKey(chatKey);
139340
139965
  if (binding === void 0) return;
139341
139966
  const text3 = [view.body, view.errorText ?? ""].filter((part) => part !== "").join("\n\n");
139342
- await deliver(binding, text3);
139967
+ await deliver(await anchors.anchored(binding, threadId, turnId), text3);
139343
139968
  },
139344
139969
  schedule: (run, delayMs) => {
139345
139970
  const timer = setTimeout(run, delayMs);
@@ -139390,24 +140015,6 @@ var ImNotConnectedError = class extends Error {
139390
140015
  }
139391
140016
  };
139392
140017
 
139393
- // src/deliveryContract.ts
139394
- var DELIVERY_SEND_ERROR_CODES = [
139395
- "bad-request",
139396
- "unknown-bot",
139397
- "unknown-target",
139398
- "bot-not-connected",
139399
- "delivery-failed"
139400
- ];
139401
- var isDeliverySendErrorCode = (value2) => typeof value2 === "string" && DELIVERY_SEND_ERROR_CODES.includes(value2);
139402
- var DeliverySendError = class extends Error {
139403
- code;
139404
- constructor(code2, message) {
139405
- super(message);
139406
- this.name = "DeliverySendError";
139407
- this.code = code2;
139408
- }
139409
- };
139410
-
139411
140018
  // src/delivery.ts
139412
140019
  var makeDeliverySender = (core, config2) => {
139413
140020
  return async (input) => {
@@ -139436,188 +140043,6 @@ var makeDeliverySender = (core, config2) => {
139436
140043
  };
139437
140044
  };
139438
140045
 
139439
- // src/deliveryHttp.ts
139440
- import * as NodeCrypto from "node:crypto";
139441
- import * as NodeHttp from "node:http";
139442
-
139443
- // src/boundedBody.ts
139444
- var readBoundedBody = (req, limit) => new Promise((resolve) => {
139445
- const chunks = [];
139446
- let size = 0;
139447
- let settled = false;
139448
- const finish = (result) => {
139449
- if (settled) return;
139450
- settled = true;
139451
- resolve(result);
139452
- };
139453
- req.on("data", (chunk) => {
139454
- if (settled) return;
139455
- size += chunk.length;
139456
- if (size > limit) {
139457
- finish({ kind: "too-large" });
139458
- req.destroy();
139459
- return;
139460
- }
139461
- chunks.push(chunk);
139462
- });
139463
- req.on("end", () => finish({ kind: "ok", bytes: Buffer.concat(chunks) }));
139464
- req.on("error", () => finish({ kind: "read-failed" }));
139465
- req.on("close", () => finish({ kind: "read-failed" }));
139466
- });
139467
-
139468
- // src/deliveryHttp.ts
139469
- var LOOPBACK = "127.0.0.1";
139470
- var MAX_BODY_BYTES = 1 * 1024 * 1024;
139471
- var PATH = "/v1/delivery/messages";
139472
- var statusFor = (code2) => {
139473
- switch (code2) {
139474
- case "bad-request":
139475
- return 400;
139476
- case "unauthorized":
139477
- return 401;
139478
- case "unknown-bot":
139479
- case "unknown-target":
139480
- return 404;
139481
- case "bot-not-connected":
139482
- return 503;
139483
- case "delivery-failed":
139484
- return 502;
139485
- case "method-not-allowed":
139486
- return 405;
139487
- case "unsupported-media-type":
139488
- return 415;
139489
- case "payload-too-large":
139490
- return 413;
139491
- }
139492
- };
139493
- var writeJson = (res, status, body) => {
139494
- const payload = JSON.stringify(body);
139495
- res.writeHead(status, {
139496
- "content-type": "application/json; charset=utf-8",
139497
- "content-length": Buffer.byteLength(payload),
139498
- "cache-control": "no-store"
139499
- });
139500
- res.end(payload);
139501
- };
139502
- var writeError = (res, code2, message) => {
139503
- writeJson(res, statusFor(code2), { error: { code: code2, message } });
139504
- };
139505
- var isLoopbackRemote = (remote) => {
139506
- if (remote === void 0 || remote === "") return false;
139507
- return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
139508
- };
139509
- var timingSafeEqualString = (a, b) => {
139510
- const left = Buffer.from(a);
139511
- const right = Buffer.from(b);
139512
- if (left.length !== right.length) return false;
139513
- return NodeCrypto.timingSafeEqual(left, right);
139514
- };
139515
- var parseSendBody = (raw) => {
139516
- let parsed;
139517
- try {
139518
- parsed = JSON.parse(raw.toString("utf8"));
139519
- } catch {
139520
- return "bad-request";
139521
- }
139522
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return "bad-request";
139523
- const record2 = parsed;
139524
- const botId = record2["botId"];
139525
- const targetId = record2["targetId"];
139526
- const text3 = record2["text"];
139527
- const format2 = record2["format"];
139528
- if (typeof botId !== "string" || botId.trim() === "") return "bad-request";
139529
- if (typeof targetId !== "string" || targetId.trim() === "") return "bad-request";
139530
- if (typeof text3 !== "string" || text3.trim() === "") return "bad-request";
139531
- if (format2 !== void 0 && format2 !== "plain" && format2 !== "markdown") return "bad-request";
139532
- for (const key of Object.keys(record2)) {
139533
- if (key !== "botId" && key !== "targetId" && key !== "text" && key !== "format") {
139534
- return "bad-request";
139535
- }
139536
- }
139537
- return {
139538
- botId: botId.trim(),
139539
- targetId: targetId.trim(),
139540
- text: text3,
139541
- format: format2 === "markdown" ? "markdown" : format2 === "plain" ? "plain" : void 0
139542
- };
139543
- };
139544
- var startDeliveryHttpServer = async (options) => {
139545
- const server = NodeHttp.createServer((req, res) => {
139546
- void (async () => {
139547
- if (!isLoopbackRemote(req.socket.remoteAddress)) {
139548
- writeError(res, "unauthorized", "Only loopback clients are accepted.");
139549
- return;
139550
- }
139551
- const url = new URL(req.url ?? "/", `http://${LOOPBACK}`);
139552
- if (url.pathname !== PATH) {
139553
- writeError(res, "bad-request", `Unknown path '${url.pathname}'.`);
139554
- return;
139555
- }
139556
- if (req.method !== "POST") {
139557
- res.setHeader("allow", "POST");
139558
- writeError(res, "method-not-allowed", "Only POST is accepted.");
139559
- return;
139560
- }
139561
- const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
139562
- if (contentType !== "application/json") {
139563
- writeError(res, "unsupported-media-type", "Content-Type must be application/json.");
139564
- return;
139565
- }
139566
- if (options.token !== "") {
139567
- const header = req.headers.authorization ?? "";
139568
- const prefix = "Bearer ";
139569
- if (!header.startsWith(prefix) || !timingSafeEqualString(header.slice(prefix.length), options.token)) {
139570
- writeError(res, "unauthorized", "Bearer token required.");
139571
- return;
139572
- }
139573
- }
139574
- const read = await readBoundedBody(req, MAX_BODY_BYTES);
139575
- if (read.kind !== "ok") {
139576
- if (read.kind === "too-large") {
139577
- writeError(res, "payload-too-large", "JSON body exceeds 1 MiB.");
139578
- } else {
139579
- writeError(res, "bad-request", "Failed to read body.");
139580
- }
139581
- return;
139582
- }
139583
- const parsed = parseSendBody(read.bytes);
139584
- if (typeof parsed === "string") {
139585
- writeError(res, parsed, "Invalid JSON body.");
139586
- return;
139587
- }
139588
- try {
139589
- const result = await options.send(parsed);
139590
- writeJson(res, 200, { sent: result.sent });
139591
- } catch (error) {
139592
- const rawCode = error instanceof Error ? error.code : void 0;
139593
- const code2 = isDeliverySendErrorCode(rawCode) ? rawCode : "delivery-failed";
139594
- const message = error instanceof Error ? error.message : String(error);
139595
- options.log?.warn("delivery http send failed", { code: code2, message });
139596
- writeError(res, code2, message);
139597
- }
139598
- })().catch((error) => {
139599
- options.log?.warn("delivery http handler crashed", {
139600
- error: error instanceof Error ? error.message : String(error)
139601
- });
139602
- if (!res.headersSent) writeError(res, "delivery-failed", "Internal error.");
139603
- });
139604
- });
139605
- await new Promise((resolve, reject) => {
139606
- server.once("error", reject);
139607
- server.listen(options.port, LOOPBACK, () => resolve());
139608
- });
139609
- const address = server.address();
139610
- const port = address.port;
139611
- options.log?.info("delivery http listening", { host: LOOPBACK, port, path: PATH });
139612
- return {
139613
- port,
139614
- origin: `http://${LOOPBACK}:${String(port)}`,
139615
- close: () => new Promise((resolve, reject) => {
139616
- server.close((error) => error ? reject(error) : resolve());
139617
- })
139618
- };
139619
- };
139620
-
139621
140046
  // src/sendFile.ts
139622
140047
  var failure = (code2, text3) => ({
139623
140048
  text: text3,
@@ -139673,7 +140098,7 @@ var makeSendFileTool = (deps) => ({
139673
140098
  handle: async (input, invocation) => {
139674
140099
  const path = typeof input["path"] === "string" ? input["path"].trim() : "";
139675
140100
  if (path === "") return failure("unreadable", "`path` is required.");
139676
- const binding = invocation.threadId === null ? void 0 : deps.boundChat(invocation.threadId);
140101
+ const binding = invocation.threadId === null ? void 0 : await deps.boundChat(invocation.threadId);
139677
140102
  if (binding === void 0) {
139678
140103
  return failure(
139679
140104
  "not-in-chat",
@@ -139992,6 +140417,7 @@ var chatIdOf = (message) => {
139992
140417
  const thread = message.message_thread_id;
139993
140418
  return message.is_topic_message === true && thread !== void 0 && thread > 0 ? `${String(message.chat.id)}:${String(thread)}` : String(message.chat.id);
139994
140419
  };
140420
+ var parentChatIdOf = (message) => chatIdOf(message) === String(message.chat.id) ? void 0 : String(message.chat.id);
139995
140421
  var splitChatId = (chatId) => {
139996
140422
  const match = /^(.+):([1-9]\d*)$/u.exec(chatId);
139997
140423
  return match === null ? { chat: chatId, thread: null } : { chat: match[1] ?? chatId, thread: Number(match[2]) };
@@ -140515,12 +140941,17 @@ var makeTelegramAdapter = (deps) => {
140515
140941
  arg: action.arg ?? null
140516
140942
  }) ?? null;
140517
140943
  };
140518
- const apiFor = async (binding) => {
140944
+ const apiFor = (binding) => {
140945
+ const token = tokenOf(binding);
140946
+ const builtFor = JSON.stringify([token, binding.proxy === void 0 ? [] : [binding.proxy]]);
140519
140947
  const existing = apis.get(binding.rowId);
140520
- if (existing !== void 0) return existing;
140521
- const http = await deps.fetchVia(binding.proxy);
140522
- const api = makeTelegramApi({ http, token: tokenOf(binding) });
140523
- apis.set(binding.rowId, api);
140948
+ if (existing?.builtFor === builtFor) return existing.api;
140949
+ const api = deps.fetchVia(binding.proxy).then((http) => makeTelegramApi({ http, token }));
140950
+ const entry = { builtFor, api };
140951
+ apis.set(binding.rowId, entry);
140952
+ api.catch(() => {
140953
+ if (apis.get(binding.rowId) === entry) apis.delete(binding.rowId);
140954
+ });
140524
140955
  return api;
140525
140956
  };
140526
140957
  const makeWriter = (api, chatId, resume) => makeTurnWriter({
@@ -140618,9 +141049,11 @@ var makeTelegramAdapter = (deps) => {
140618
141049
  });
140619
141050
  continue;
140620
141051
  }
141052
+ const parentChatId = parentChatIdOf(message);
140621
141053
  handlers.onMessage({
140622
141054
  messageId: String(message.message_id),
140623
141055
  chatId: chatIdOf(message),
141056
+ ...parentChatId === void 0 ? {} : { parentChatId },
140624
141057
  senderId: message.from === void 0 ? null : String(message.from.id),
140625
141058
  text: text3,
140626
141059
  chatKind: message.chat.type === "private" ? "private" : "group",
@@ -140846,6 +141279,10 @@ var makeTurns = (core, deps) => {
140846
141279
  return true;
140847
141280
  }
140848
141281
  };
141282
+ const waitingFor = (chatKey, threadId) => {
141283
+ const waiting = pendingByChat.get(chatKey);
141284
+ return waiting !== void 0 && waiting.threadId === threadId ? waiting : void 0;
141285
+ };
140849
141286
  const interactionModeOf = (chatKey) => chatOf(core.state, chatKey).plan ? "plan" : "default";
140850
141287
  const ensureThread = async (binding, titleSeed) => {
140851
141288
  const chatKey = chatKeyOf(binding);
@@ -140905,9 +141342,8 @@ var makeTurns = (core, deps) => {
140905
141342
  const text3 = message.text;
140906
141343
  const threadId = await ensureThread(binding, text3 || message.resources[0]?.name || "Chat");
140907
141344
  if (threadId === null) return null;
140908
- const waiting = pendingByChat.get(chatKey);
141345
+ const carried = waitingFor(chatKey, threadId);
140909
141346
  pendingByChat.delete(chatKey);
140910
- const carried = waiting?.threadId === threadId ? waiting : void 0;
140911
141347
  const own = await collectResources(
140912
141348
  binding,
140913
141349
  threadId,
@@ -140918,6 +141354,7 @@ var makeTurns = (core, deps) => {
140918
141354
  const attachments = [...carried?.attachments ?? [], ...own.attachments];
140919
141355
  const notes = [...carried?.notes ?? [], ...own.notes];
140920
141356
  const userMessageId = crypto.randomUUID();
141357
+ deps.anchors.note(threadId, userMessageId, binding);
140921
141358
  let intentAction;
140922
141359
  if (binding.queueMode === "steer") {
140923
141360
  try {
@@ -140965,7 +141402,8 @@ var makeTurns = (core, deps) => {
140965
141402
  chatKey,
140966
141403
  threadId,
140967
141404
  userMessageId,
140968
- text3.trim() || attachments[0]?.name || message.resources[0]?.name || ""
141405
+ text3.trim() || attachments[0]?.name || message.resources[0]?.name || "",
141406
+ binding.reply
140969
141407
  );
140970
141408
  return threadId;
140971
141409
  };
@@ -140991,7 +141429,14 @@ var makeTurns = (core, deps) => {
140991
141429
  })
140992
141430
  );
140993
141431
  };
140994
- return { threadStillUsable, ensureThread, startTurn, receiveAttachmentsOnly };
141432
+ const hasWaitingAttachments = (chatKey) => (waitingFor(chatKey, chatOf(core.state, chatKey).threadId)?.attachments.length ?? 0) > 0;
141433
+ return {
141434
+ threadStillUsable,
141435
+ ensureThread,
141436
+ startTurn,
141437
+ receiveAttachmentsOnly,
141438
+ hasWaitingAttachments
141439
+ };
140995
141440
  };
140996
141441
 
140997
141442
  // src/webSync.ts
@@ -141814,6 +142259,7 @@ ${kept}`;
141814
142259
  icon: "wecom",
141815
142260
  label: "WeCom",
141816
142261
  columns: COLUMNS4,
142262
+ unlistedValues: ["wecomEndpoint"],
141817
142263
  isConfigured: (binding) => {
141818
142264
  const { botId, botSecret } = credentials2(binding);
141819
142265
  return botId !== "" && botSecret !== "";
@@ -142920,7 +143366,9 @@ var imPlugin = definePlugin({
142920
143366
  id: "im",
142921
143367
  apiVersion: 3,
142922
143368
  activate: async (ctx) => {
142923
- const t = makeTranslate(parseReplyLanguage(ctx.config[REPLY_LANGUAGE_FIELD]));
143369
+ const { t, setLanguage } = makeLiveTranslate(
143370
+ parseReplyLanguage(ctx.config[REPLY_LANGUAGE_FIELD])
143371
+ );
142924
143372
  const adapters = [
142925
143373
  makeLarkAdapter({
142926
143374
  http: ctx.http,
@@ -142989,29 +143437,36 @@ var imPlugin = definePlugin({
142989
143437
  onDispose: ctx.onDispose
142990
143438
  },
142991
143439
  pluginId: ctx.id,
142992
- config: ctx.config,
143440
+ config: () => ctx.config,
142993
143441
  derivedState: ctx.derivedState,
142994
143442
  t,
142995
143443
  adapters
142996
143444
  });
142997
143445
  const acknowledgements = makeAcknowledgements(core);
142998
143446
  const failures = makeFailureNotices(core);
142999
- const replyCards = makeReplyCards(core, failures);
143447
+ const replyAnchors = makeReplyAnchors(core.host);
143448
+ const replyCards = makeReplyCards(core, failures, replyAnchors);
143000
143449
  const teams = makeTeamRuns(core, { clearWorking: acknowledgements.clearWorking });
143001
143450
  const notifications = makeNotifications(core, {
143002
143451
  clearWorking: acknowledgements.clearWorking,
143003
143452
  replyCards,
143004
- failures
143453
+ failures,
143454
+ anchors: replyAnchors
143005
143455
  });
143006
143456
  const webSync = makeWebSync(core);
143007
- const approvals = makeApprovals(core);
143008
- const interactions = makeInteractions(core, approvals);
143457
+ const approvals = makeApprovals(core, replyAnchors);
143458
+ const interactions = makeInteractions(core, {
143459
+ ...approvals,
143460
+ anchors: replyAnchors,
143461
+ rotateTurnCard: replyCards.turnCards.rotate
143462
+ });
143009
143463
  const inboundAttachments = makeInboundAttachments(core);
143010
143464
  const turns = makeTurns(core, {
143011
143465
  attachments: inboundAttachments,
143012
143466
  queueFlow: interactions.queueFlow,
143013
143467
  rememberThreadProject: notifications.rememberThreadProject,
143014
- noteInboundUserMessage: webSync.noteInboundUserMessage
143468
+ noteInboundUserMessage: webSync.noteInboundUserMessage,
143469
+ anchors: replyAnchors
143015
143470
  });
143016
143471
  const sessions = makeSessions(core);
143017
143472
  const modelPlan = makeModelPlan(core, turns);
@@ -143027,7 +143482,7 @@ var imPlugin = definePlugin({
143027
143482
  });
143028
143483
  const chatSetup = makeChatSetup(core, {
143029
143484
  rows: projectRows,
143030
- openPicker: (chatKey, rows) => panelWiring.panel.openProjects(chatKey, rows)
143485
+ openPicker: (chatKey, rows, reply) => panelWiring.panel.openProjects(chatKey, rows, reply)
143031
143486
  });
143032
143487
  const commands = makeCommands(core, {
143033
143488
  clearWorking: acknowledgements.clearWorking,
@@ -143035,7 +143490,7 @@ var imPlugin = definePlugin({
143035
143490
  sessions,
143036
143491
  modelPlan,
143037
143492
  chatSetup,
143038
- openPanel: (chatKey) => panelWiring.panel.open(chatKey)
143493
+ openPanel: (chatKey, reply) => panelWiring.panel.open(chatKey, reply)
143039
143494
  });
143040
143495
  const inbound = makeInbound(core, {
143041
143496
  enqueueForChat: chatQueue.enqueueForChat,
@@ -143053,7 +143508,10 @@ var imPlugin = definePlugin({
143053
143508
  name: "im",
143054
143509
  tools: [
143055
143510
  makeSendFileTool({
143056
- boundChat: (threadId) => core.bindingOfThread(threadId),
143511
+ boundChat: async (threadId) => {
143512
+ const binding = core.bindingOfThread(threadId);
143513
+ return binding === void 0 ? void 0 : replyAnchors.anchored(binding, threadId, null);
143514
+ },
143057
143515
  adapterFor: (binding) => core.adapterFor(binding.platform),
143058
143516
  translate: t,
143059
143517
  log: {
@@ -143063,34 +143521,21 @@ var imPlugin = definePlugin({
143063
143521
  })
143064
143522
  ]
143065
143523
  });
143066
- const { bots, connecting } = startConnections(core, inbound.inboundHandlersFor);
143067
- if (readDeliveryHttpEnabled(ctx.config)) {
143068
- const port = readDeliveryHttpPort(ctx.config);
143069
- const token = readDeliveryHttpToken(ctx.config);
143070
- const send = makeDeliverySender(core, ctx.config);
143071
- try {
143072
- const server = await startDeliveryHttpServer({
143073
- port,
143074
- token,
143075
- send,
143076
- log: {
143077
- info: (message, fields) => ctx.log.info(message, fields),
143078
- warn: (message, fields) => ctx.log.warn(message, fields)
143079
- }
143080
- });
143081
- ctx.onDispose(() => {
143082
- void server.close().catch((error) => {
143083
- ctx.log.warn("delivery http could not close", {
143084
- error: error instanceof Error ? error.message : String(error)
143085
- });
143086
- });
143087
- });
143088
- } catch (error) {
143089
- ctx.log.warn("delivery http failed to listen", {
143090
- port,
143091
- error: error instanceof Error ? error.message : String(error)
143092
- });
143093
- }
143524
+ const connections = startConnections(core, inbound.inboundHandlersFor);
143525
+ const { bots, connecting } = connections.reconcile();
143526
+ const deliveryHttp = makeDeliveryHttp({
143527
+ // 每次请求按此刻的设置找目标:投递目标表改了不用重开监听。
143528
+ send: (input) => makeDeliverySender(core, ctx.config)(input),
143529
+ log: ctx.log,
143530
+ onDispose: ctx.onDispose
143531
+ });
143532
+ await deliveryHttp.apply(ctx.config);
143533
+ if (typeof ctx.settings.onChange === "function") {
143534
+ ctx.settings.onChange(async (config2) => {
143535
+ setLanguage(parseReplyLanguage(config2[REPLY_LANGUAGE_FIELD]));
143536
+ connections.reconcile();
143537
+ await deliveryHttp.apply(config2);
143538
+ });
143094
143539
  }
143095
143540
  ctx.log.info("chat plugin active", { bots, connecting });
143096
143541
  }