@ycodium-ai/plugin-im 0.2.2570 → 0.2.2588

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.
Files changed (3) hide show
  1. package/README.md +31 -15
  2. package/dist/index.js +370 -17
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,18 +30,32 @@ is HTTP long-polling.
30
30
  The default is one message per finished turn. A single turn can produce dozens
31
31
  of assistant messages, mostly commentary between tool calls.
32
32
 
33
- On Lark, the reply to a thread this plugin opened is one card that is patched
34
- as the agent works (`thread.turn-progress` ticks, reply text read with
35
- `getThreadMessage`) and settled when the turn ends; threads opened elsewhere
36
- still get the per-turn or per-message text. Platforms that cannot edit a sent
37
- message keep sending text. Approval cards are posted for every pending request
38
- and replaced with a settled notice when the request is answered, here or
39
- elsewhere (`thread.request-resolved`). Cards that were still open when the
40
- plugin restarted are marked interrupted. The card pieces are pure and
41
- separate: `turnView.ts` (ticks to a view model), `larkCards.ts` (view model
42
- to card JSON), `larkMarkdown.ts`, `larkCardStream.ts` (throttle, one patch in
43
- flight, newest snapshot wins), `turnCards.ts` (per-thread queue and card
44
- lifecycle).
33
+ The reply to a thread this plugin opened is written as the agent works
34
+ (`thread.turn-progress` ticks, reply text read with `getThreadMessage`) and
35
+ settled when the turn ends; threads opened elsewhere still get the per-turn or
36
+ per-message text. How depends on the platform:
37
+
38
+ - **Lark** patches one card.
39
+ - **Telegram** edits one message (`editMessageText`, plain text) and, when the
40
+ reply outgrows a single message, freezes it and continues in the next one.
41
+ A `message is not modified` answer counts as success; a 429 waits the
42
+ `retry_after` it names, once.
43
+ - **WeCom** keeps sending frames of one `stream.id` (`finish: false`) on the
44
+ inbound message's `req_id` and closes with `finish: true`. With no inbound
45
+ message to reply to, or after the platform rejects the stream, the settled text
46
+ goes out as ordinary messages instead.
47
+
48
+ An adapter opts in with the optional `turnCards` surface (`src/adapter.ts`).
49
+ Approval messages are posted for every pending request on platforms with
50
+ buttons and replaced with a settled notice when the request is answered, here or
51
+ elsewhere (`thread.request-resolved`); WeCom, which answers by number, posts only
52
+ the first and just stops accepting its number. Cards that were still open when the
53
+ plugin restarted are marked interrupted. The pieces are pure and separate:
54
+ `turnView.ts` (ticks to a view model), `larkCards.ts` (view model to card JSON),
55
+ `larkMarkdown.ts`, `larkCardStream.ts` (throttle, one update in flight, newest
56
+ snapshot wins), `turnCards.ts` (per-thread queue and lifecycle),
57
+ `plainTurnText.ts` (view model to plain text), `telegramTurn.ts` (message
58
+ rollover).
45
59
 
46
60
  A group keeps its place across restarts. Which thread a group is talking in, the
47
61
  team it is pointed at, the model and plan mode set from the group, the threads
@@ -55,9 +69,11 @@ Chat commands: `/stop`, `/status`, `/new`, `/sessions`, `/resume`, `/model`,
55
69
  switch threads this plugin opened, since the host lets a plugin send only into
56
70
  its own threads.
57
71
 
58
- On Lark, `/panel` sends a command panel card (`/help` stays text and ends with a pointer to it; other platforms' `/help` does not mention it): every command above
59
- is a button, and the ones that take an argument (a thread, a team, a model) open
60
- a second view on the same card with a Back button. Each card keeps its own view
72
+ On Lark and Telegram, `/panel` sends a command panel (`/help` stays text and ends with a pointer to it; on WeCom, which has no panel, it does not): every command above
73
+ is a button (a card on Lark, an inline keyboard on Telegram), and the ones that take an argument (a thread, a team, a model) open
74
+ a second view on the same message with a Back button. Telegram buttons carry only
75
+ a short `pn:<version>:<index>` token (`callback_data` is capped at 64 bytes);
76
+ the action stays in memory and a tap on an older keyboard version counts as expired. Each card keeps its own view
61
77
  stack in memory (`src/panelController.ts`), so after a restart an old card only
62
78
  answers that it has expired. A tap runs the same handler as the typed command and
63
79
  waits in the same per-group queue as typed messages. Another platform opts in by
package/dist/index.js CHANGED
@@ -133399,12 +133399,16 @@ var POLL_TIMEOUT_SECONDS = 45;
133399
133399
  var TELEGRAM_TEXT_MAX_CHARS = 4e3;
133400
133400
  var TelegramApiError = class extends Error {
133401
133401
  code;
133402
- constructor(method, code2, description) {
133402
+ /** 限流(429)时平台给的等待秒数;别的错误没有。 */
133403
+ retryAfterSeconds;
133404
+ constructor(method, code2, description, retryAfterSeconds) {
133403
133405
  super(`telegram ${method} failed (${String(code2 ?? "?")}): ${description}`);
133404
133406
  this.name = "TelegramApiError";
133405
133407
  this.code = code2;
133408
+ this.retryAfterSeconds = retryAfterSeconds;
133406
133409
  }
133407
133410
  };
133411
+ var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
133408
133412
  var PHOTO_NAME = "photo.jpg";
133409
133413
  var PHOTO_MIME = "image/jpeg";
133410
133414
  var mimeFromName2 = (name) => {
@@ -133463,10 +133467,20 @@ var makeTelegramApi = (input) => {
133463
133467
  });
133464
133468
  const envelope = await response.json();
133465
133469
  if (envelope.ok !== true) {
133466
- throw new TelegramApiError(method, envelope.error_code, envelope.description ?? "no reason");
133470
+ throw new TelegramApiError(
133471
+ method,
133472
+ envelope.error_code,
133473
+ envelope.description ?? "no reason",
133474
+ envelope.parameters?.retry_after
133475
+ );
133467
133476
  }
133468
133477
  return envelope.result;
133469
133478
  };
133479
+ const keyboard = (rows) => ({
133480
+ inline_keyboard: rows.map(
133481
+ (row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
133482
+ )
133483
+ });
133470
133484
  return {
133471
133485
  /** 机器人自己是谁;群里的命令靠它的 `username` 认出是不是发给自己的。 */
133472
133486
  getMe: () => call("getMe", {}),
@@ -133486,16 +133500,45 @@ var makeTelegramApi = (input) => {
133486
133500
  await call("sendMessage", { chat_id: chatId, text: chunk });
133487
133501
  }
133488
133502
  },
133489
- /** 带按钮的消息。一行一个按钮,审批选项通常不多。 */
133490
- sendButtons: (chatId, text2, buttons) => call("sendMessage", {
133491
- chat_id: chatId,
133492
- text: text2,
133493
- reply_markup: {
133494
- inline_keyboard: buttons.map((button) => [
133495
- { text: button.label, callback_data: button.token }
133496
- ])
133503
+ /**
133504
+ * 发一条消息,回它的 id。调用方保证 `text` 不超过单条上限(边写边更新的回复要自己
133505
+ * 在上限处翻篇,这里不替它切)。带 `rows` 就挂一块内联键盘。
133506
+ */
133507
+ sendOne: async (chatId, text2, rows) => {
133508
+ const sent = await call("sendMessage", {
133509
+ chat_id: chatId,
133510
+ text: text2,
133511
+ ...rows === void 0 ? {} : { reply_markup: keyboard(rows) }
133512
+ });
133513
+ return sent?.message_id === void 0 ? null : String(sent.message_id);
133514
+ },
133515
+ /**
133516
+ * 改一条已发出的消息。内容没变(`message is not modified`)当成功。`rows` 不给就是
133517
+ * 去掉按钮:改了文字又要保留按钮的调用方得自己再传一次。
133518
+ */
133519
+ editMessage: async (chatId, messageId, text2, rows = []) => {
133520
+ try {
133521
+ await call("editMessageText", {
133522
+ chat_id: chatId,
133523
+ message_id: Number(messageId),
133524
+ text: text2,
133525
+ reply_markup: keyboard(rows)
133526
+ });
133527
+ } catch (error) {
133528
+ if (!isNotModified(error)) throw error;
133497
133529
  }
133498
- }),
133530
+ },
133531
+ /** 带按钮的审批消息,一行一个按钮;回消息 id,之后才改得了它(平台没给就是 null)。 */
133532
+ sendButtons: async (chatId, text2, buttons) => {
133533
+ const sent = await call("sendMessage", {
133534
+ chat_id: chatId,
133535
+ text: text2,
133536
+ reply_markup: keyboard(
133537
+ buttons.map((button) => [{ text: button.label, data: button.token }])
133538
+ )
133539
+ });
133540
+ return sent?.message_id === void 0 ? null : String(sent.message_id);
133541
+ },
133499
133542
  /** 答完点击,不然客户端上那个按钮一直转圈。带 `text` 时点击者会看到一句提示。 */
133500
133543
  answerCallback: (callbackQueryId, text2) => call("answerCallbackQuery", {
133501
133544
  callback_query_id: callbackQueryId,
@@ -133536,16 +133579,168 @@ var makeTelegramApi = (input) => {
133536
133579
  };
133537
133580
  };
133538
133581
 
133582
+ // src/telegramPanel.ts
133583
+ var PANEL_DATA_PREFIX = "pn:";
133584
+ var renderPanel = (card, version) => {
133585
+ const actions = [];
133586
+ const rowOf = (buttons) => buttons.map((button) => {
133587
+ actions.push(button.action);
133588
+ return {
133589
+ text: button.label,
133590
+ data: `${PANEL_DATA_PREFIX}${String(version)}:${String(actions.length - 1)}`
133591
+ };
133592
+ });
133593
+ const rows = [];
133594
+ for (const group of card.groups) {
133595
+ if (group.buttons.length > 0) rows.push(rowOf(group.buttons));
133596
+ }
133597
+ if (card.nav.length > 0) rows.push(rowOf(card.nav));
133598
+ return {
133599
+ text: [card.title, card.text].filter((part) => part !== "").join("\n\n"),
133600
+ rows,
133601
+ actions
133602
+ };
133603
+ };
133604
+ var parsePanelData = (data) => {
133605
+ const match = /^pn:(\d+):(\d+)$/.exec(data);
133606
+ if (match === null) return null;
133607
+ return { version: Number(match[1]), index: Number(match[2]) };
133608
+ };
133609
+
133610
+ // src/plainTurnText.ts
133611
+ var RUNNING_LABEL_MAX_CHARS = 200;
133612
+ var clip2 = (text2, max) => {
133613
+ const chars = Array.from(text2);
133614
+ return chars.length <= max ? text2 : `${chars.slice(0, max - 1).join("")}\u2026`;
133615
+ };
133616
+ var turnTextParts = (view, t) => {
133617
+ const names = view.steps.map((kind) => t(`card.step.${kind}`));
133618
+ if (view.omitted > 0) names.unshift(t("card.step.omitted", { count: view.omitted }));
133619
+ const headLines = [view.title === "" ? t("card.untitled") : view.title];
133620
+ if (names.length > 0) headLines.push(names.join(" \u2192 "));
133621
+ if (view.running !== null) headLines.push(`\u{1F527} ${clip2(view.running, RUNNING_LABEL_MAX_CHARS)}`);
133622
+ const body = view.body.trim();
133623
+ const footLines = [];
133624
+ if (view.status === "working") {
133625
+ if (body === "" && names.length === 0) footLines.push(t("card.status.working"));
133626
+ } else {
133627
+ footLines.push(t(`card.status.${view.status}`));
133628
+ if (view.errorText !== null && view.errorText !== "") footLines.push(view.errorText);
133629
+ }
133630
+ return { head: headLines.join("\n"), body, foot: footLines.join("\n") };
133631
+ };
133632
+ var joinParts = (...parts) => parts.filter((part) => part !== "").join("\n\n");
133633
+
133634
+ // src/telegramTurn.ts
133635
+ var makeTurnWriter = (deps) => {
133636
+ let first = deps.resume ?? null;
133637
+ let live = deps.resume ?? null;
133638
+ let liveText = null;
133639
+ let frozen = 0;
133640
+ let consumed = 0;
133641
+ const put = async (text2) => {
133642
+ const bounded = text2.length <= deps.limit ? text2 : text2.slice(0, deps.limit);
133643
+ if (live === null) {
133644
+ const id = await deps.messages.send(bounded);
133645
+ if (id === null) throw new Error("telegram sent a message without an id");
133646
+ live = id;
133647
+ first ??= id;
133648
+ } else if (bounded === liveText) {
133649
+ return;
133650
+ } else {
133651
+ await deps.messages.edit(live, bounded);
133652
+ }
133653
+ liveText = bounded;
133654
+ };
133655
+ return {
133656
+ firstId: () => first,
133657
+ write: async (view) => {
133658
+ const { head, body, foot } = turnTextParts(view, deps.t);
133659
+ consumed = Math.min(consumed, body.length);
133660
+ for (; ; ) {
133661
+ const rest = body.slice(consumed);
133662
+ const prefix = frozen === 0 ? head.slice(0, Math.floor(deps.limit / 2)) : "";
133663
+ const whole = joinParts(prefix, rest, foot);
133664
+ if (whole.length <= deps.limit) {
133665
+ if (whole !== "") await put(whole);
133666
+ return;
133667
+ }
133668
+ const room = deps.limit - prefix.length - 2;
133669
+ const chunk = splitMessage(rest, room)[0] ?? "";
133670
+ if (chunk === "") {
133671
+ await put(whole);
133672
+ return;
133673
+ }
133674
+ await put(joinParts(prefix, chunk));
133675
+ frozen += 1;
133676
+ consumed += chunk.length;
133677
+ consumed += body.slice(consumed).length - body.slice(consumed).trimStart().length;
133678
+ live = null;
133679
+ liveText = null;
133680
+ }
133681
+ }
133682
+ };
133683
+ };
133684
+
133539
133685
  // src/telegramAdapter.ts
133540
133686
  var COLUMNS2 = [
133541
133687
  { key: "telegramToken", kind: "password", labelKey: "column.telegramToken.label" }
133542
133688
  ];
133543
133689
  var BACKOFF_START_MS = 1e3;
133544
133690
  var BACKOFF_MAX_MS = 6e4;
133691
+ var TELEGRAM_CARD_THROTTLE_MS = 1200;
133692
+ var FLOOD_WAIT_MAX_SECONDS = 30;
133693
+ var REMEMBERED_CARD_LIMIT = 200;
133545
133694
  var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
133546
133695
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133547
133696
  var makeTelegramAdapter = (deps) => {
133548
133697
  const apis = /* @__PURE__ */ new Map();
133698
+ const sleep = deps.sleep ?? defaultSleep;
133699
+ const t = deps.translate;
133700
+ const withFloodWait = async (run) => {
133701
+ try {
133702
+ return await run();
133703
+ } catch (error) {
133704
+ if (error instanceof TelegramApiError && error.code === 429 && error.retryAfterSeconds !== void 0 && error.retryAfterSeconds <= FLOOD_WAIT_MAX_SECONDS) {
133705
+ await sleep(error.retryAfterSeconds * 1e3);
133706
+ return run();
133707
+ }
133708
+ throw error;
133709
+ }
133710
+ };
133711
+ const remembering = () => {
133712
+ const entries = /* @__PURE__ */ new Map();
133713
+ return {
133714
+ get: (key) => entries.get(key),
133715
+ set: (key, value) => {
133716
+ entries.delete(key);
133717
+ entries.set(key, value);
133718
+ if (entries.size > REMEMBERED_CARD_LIMIT) {
133719
+ const oldest = entries.keys().next().value;
133720
+ if (oldest !== void 0) entries.delete(oldest);
133721
+ }
133722
+ },
133723
+ delete: (key) => entries.delete(key)
133724
+ };
133725
+ };
133726
+ const turnWriters = remembering();
133727
+ const panelKeyboards = remembering();
133728
+ const panelClick = (callback, operatorId, handlers) => {
133729
+ const messageId = callback.message?.message_id;
133730
+ const chatId = callback.message?.chat.id;
133731
+ if (messageId === void 0 || chatId === void 0) return t("panel.expired");
133732
+ const keyboard = panelKeyboards.get(`${String(chatId)}:${String(messageId)}`);
133733
+ const parsed = parsePanelData(callback.data ?? "");
133734
+ const action = keyboard === void 0 || parsed === null || parsed.version !== keyboard.version ? void 0 : keyboard.actions[parsed.index];
133735
+ if (action === void 0) return t("panel.expired");
133736
+ return handlers.onPanelClick?.({
133737
+ chatId: String(chatId),
133738
+ cardId: String(messageId),
133739
+ operatorId,
133740
+ op: action.op,
133741
+ arg: action.arg ?? null
133742
+ }) ?? null;
133743
+ };
133549
133744
  const apiFor = async (binding) => {
133550
133745
  const existing = apis.get(binding.rowId);
133551
133746
  if (existing !== void 0) return existing;
@@ -133554,6 +133749,15 @@ var makeTelegramAdapter = (deps) => {
133554
133749
  apis.set(binding.rowId, api);
133555
133750
  return api;
133556
133751
  };
133752
+ const makeWriter = (api, chatId, resume) => makeTurnWriter({
133753
+ t,
133754
+ limit: TELEGRAM_TEXT_MAX_CHARS,
133755
+ ...resume === void 0 ? {} : { resume },
133756
+ messages: {
133757
+ send: (text2) => withFloodWait(() => api.sendOne(chatId, text2)),
133758
+ edit: (messageId, text2) => withFloodWait(() => api.editMessage(chatId, messageId, text2))
133759
+ }
133760
+ });
133557
133761
  return {
133558
133762
  platform: "telegram",
133559
133763
  icon: "telegram",
@@ -133581,10 +133785,8 @@ var makeTelegramAdapter = (deps) => {
133581
133785
  for (const update of updates) {
133582
133786
  const callback = update.callback_query;
133583
133787
  if (callback?.data !== void 0) {
133584
- const notice = handlers.onApproval(
133585
- callback.data,
133586
- callback.from === void 0 ? null : String(callback.from.id)
133587
- );
133788
+ const operatorId = callback.from === void 0 ? null : String(callback.from.id);
133789
+ const notice = callback.data.startsWith(PANEL_DATA_PREFIX) ? panelClick(callback, operatorId, handlers) : handlers.onApproval(callback.data, operatorId);
133588
133790
  await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
133589
133791
  continue;
133590
133792
  }
@@ -133637,8 +133839,72 @@ var makeTelegramAdapter = (deps) => {
133637
133839
  },
133638
133840
  askApproval: async (binding, request) => {
133639
133841
  const api = await apiFor(binding);
133640
- await api.sendButtons(binding.chatId, request.body, request.options);
133641
- return null;
133842
+ return api.sendButtons(binding.chatId, request.body, request.options);
133843
+ },
133844
+ settleApproval: async (binding, cardId, settlement) => {
133845
+ const api = await apiFor(binding);
133846
+ const line = settlement.kind === "chosen" ? t("card.approval.chosen", { label: settlement.label }) : t("card.approval.elsewhere");
133847
+ await withFloodWait(
133848
+ () => api.editMessage(
133849
+ binding.chatId,
133850
+ cardId,
133851
+ `${t("card.approval.doneTitle")}
133852
+ ${line}`
133853
+ )
133854
+ );
133855
+ },
133856
+ turnCards: {
133857
+ throttleMs: TELEGRAM_CARD_THROTTLE_MS,
133858
+ open: async (binding, view) => {
133859
+ const api = await apiFor(binding);
133860
+ const writer = makeWriter(api, binding.chatId);
133861
+ try {
133862
+ await writer.write(view);
133863
+ } catch (error) {
133864
+ if (writer.firstId() === null) throw error;
133865
+ deps.log.warn("telegram could not finish the first card write", {
133866
+ error: error instanceof Error ? error.message : String(error)
133867
+ });
133868
+ }
133869
+ const cardId = writer.firstId();
133870
+ if (cardId !== null && view.status === "working") {
133871
+ turnWriters.set(`${binding.chatId}:${cardId}`, writer);
133872
+ }
133873
+ return cardId;
133874
+ },
133875
+ update: async (binding, cardId, view) => {
133876
+ const key = `${binding.chatId}:${cardId}`;
133877
+ const writer = turnWriters.get(key) ?? makeWriter(await apiFor(binding), binding.chatId, cardId);
133878
+ await writer.write(view);
133879
+ if (view.status !== "working") turnWriters.delete(key);
133880
+ else turnWriters.set(key, writer);
133881
+ }
133882
+ },
133883
+ panel: {
133884
+ open: async (binding, card) => {
133885
+ const api = await apiFor(binding);
133886
+ const rendered = renderPanel(card, 1);
133887
+ const cardId = await withFloodWait(
133888
+ () => api.sendOne(binding.chatId, rendered.text, rendered.rows)
133889
+ );
133890
+ if (cardId !== null) {
133891
+ panelKeyboards.set(`${binding.chatId}:${cardId}`, {
133892
+ version: 1,
133893
+ actions: rendered.actions
133894
+ });
133895
+ }
133896
+ return cardId;
133897
+ },
133898
+ update: async (binding, cardId, card) => {
133899
+ const api = await apiFor(binding);
133900
+ const key = `${binding.chatId}:${cardId}`;
133901
+ const version = (panelKeyboards.get(key)?.version ?? 0) + 1;
133902
+ const rendered = renderPanel(card, version);
133903
+ await withFloodWait(
133904
+ () => api.editMessage(binding.chatId, cardId, rendered.text, rendered.rows)
133905
+ );
133906
+ panelKeyboards.set(key, { version, actions: rendered.actions });
133907
+ }
133642
133908
  },
133643
133909
  fetchResource: async (binding, input) => {
133644
133910
  const api = await apiFor(binding);
@@ -133908,6 +134174,7 @@ var runWecomClient = (input) => {
133908
134174
  const nextRequestId = (kind) => `${kind}_${++sequence}`;
133909
134175
  const pendingAcks = /* @__PURE__ */ new Map();
133910
134176
  const pendingRequests = /* @__PURE__ */ new Map();
134177
+ const openStreams = /* @__PURE__ */ new Map();
133911
134178
  const subscribeId = nextRequestId("aibot_subscribe");
133912
134179
  let missedPong = 0;
133913
134180
  let closed = false;
@@ -133937,6 +134204,8 @@ var runWecomClient = (input) => {
133937
134204
  return;
133938
134205
  }
133939
134206
  const error = typeof frame.errcode === "number" && frame.errcode !== 0 ? new Error(`WeCom ack failed: ${String(frame.errcode)} ${frame.errmsg ?? ""}`) : null;
134207
+ const stream = openStreams.get(requestId);
134208
+ if (stream !== void 0 && error !== null) stream.failed = true;
133940
134209
  settleAck(requestId, error);
133941
134210
  return;
133942
134211
  }
@@ -133948,6 +134217,7 @@ var runWecomClient = (input) => {
133948
134217
  if (finished) return;
133949
134218
  finished = true;
133950
134219
  clearInterval(heartbeat);
134220
+ openStreams.clear();
133951
134221
  void input.socket.close().catch(() => void 0);
133952
134222
  for (const requestId of pendingAcks.keys()) {
133953
134223
  settleAck(requestId, new Error("WeCom connection closed"));
@@ -134035,8 +134305,38 @@ var runWecomClient = (input) => {
134035
134305
  }
134036
134306
  }
134037
134307
  };
134308
+ const openStream = (chatId) => {
134309
+ const requestId = pendingRequests.get(chatId);
134310
+ if (requestId === void 0) return null;
134311
+ pendingRequests.delete(chatId);
134312
+ const id = nextRequestId("stream");
134313
+ const state = { failed: false, finished: false };
134314
+ openStreams.set(requestId, state);
134315
+ return {
134316
+ id,
134317
+ send: async (content, finish2) => {
134318
+ if (state.failed) return false;
134319
+ if (state.finished) return true;
134320
+ if (finish2) state.finished = true;
134321
+ try {
134322
+ await send({
134323
+ cmd: "aibot_respond_msg",
134324
+ headers: { req_id: requestId },
134325
+ body: { msgtype: "stream", stream: { id, finish: finish2, content } }
134326
+ });
134327
+ } catch (error) {
134328
+ state.failed = true;
134329
+ openStreams.delete(requestId);
134330
+ throw error;
134331
+ }
134332
+ if (finish2) openStreams.delete(requestId);
134333
+ return true;
134334
+ }
134335
+ };
134336
+ };
134038
134337
  return {
134039
134338
  sendText,
134339
+ openStream,
134040
134340
  rememberRequest: (chatId, requestId) => {
134041
134341
  if (requestId !== null) pendingRequests.set(chatId, requestId);
134042
134342
  },
@@ -134051,6 +134351,8 @@ var runWecomClient = (input) => {
134051
134351
  };
134052
134352
 
134053
134353
  // src/wecomAdapter.ts
134354
+ var WECOM_CARD_THROTTLE_MS = 400;
134355
+ var LIVE_STREAM_LIMIT = 200;
134054
134356
  var BACKOFF_START_MS2 = 1e3;
134055
134357
  var BACKOFF_MAX_MS2 = 6e4;
134056
134358
  var COLUMNS3 = [
@@ -134071,6 +134373,36 @@ var optionIndexFrom = (text2, count) => {
134071
134373
  };
134072
134374
  var makeWecomAdapter = (deps) => {
134073
134375
  const clients = /* @__PURE__ */ new Map();
134376
+ const t = deps.translate;
134377
+ const liveStreams = /* @__PURE__ */ new Map();
134378
+ const rememberStream = (key, entry) => {
134379
+ liveStreams.set(key, entry);
134380
+ if (liveStreams.size > LIVE_STREAM_LIMIT) {
134381
+ const oldest = liveStreams.keys().next().value;
134382
+ if (oldest !== void 0) liveStreams.delete(oldest);
134383
+ }
134384
+ };
134385
+ const emit = async (entry, view) => {
134386
+ const { head, body, foot } = turnTextParts(view, t);
134387
+ if (view.status === "working") {
134388
+ let shown = body;
134389
+ const whole = joinParts(head, body, foot);
134390
+ if (whole.length > WECOM_TEXT_MAX_CHARS) {
134391
+ const marker = t("card.truncated", { count: 9999999 });
134392
+ const room = WECOM_TEXT_MAX_CHARS - head.length - foot.length - marker.length - 6;
134393
+ const kept = room > 0 ? body.slice(Math.max(0, body.length - room)) : "";
134394
+ shown = `${t("card.truncated", { count: body.length - kept.length })}
134395
+ ${kept}`;
134396
+ }
134397
+ await entry.stream.send(joinParts(head, shown, foot), false);
134398
+ return;
134399
+ }
134400
+ const chunks = splitForWecom(joinParts(head, body, foot));
134401
+ const delivered = await entry.stream.send(chunks[0] ?? "", true);
134402
+ for (const chunk of delivered ? chunks.slice(1) : chunks) {
134403
+ await entry.client.sendText(entry.chatId, chunk);
134404
+ }
134405
+ };
134074
134406
  return {
134075
134407
  platform: "wecom",
134076
134408
  icon: "wecom",
@@ -134169,6 +134501,25 @@ var makeWecomAdapter = (deps) => {
134169
134501
  await (clients.get(binding.rowId)?.sendText(binding.chatId, lines.join("\n")) ?? Promise.resolve());
134170
134502
  return null;
134171
134503
  },
134504
+ turnCards: {
134505
+ throttleMs: WECOM_CARD_THROTTLE_MS,
134506
+ open: async (binding, view) => {
134507
+ const client = clients.get(binding.rowId);
134508
+ const stream = client?.openStream(binding.chatId) ?? null;
134509
+ if (client === void 0 || stream === null) return null;
134510
+ const entry = { stream, client, chatId: binding.chatId };
134511
+ await emit(entry, view);
134512
+ if (view.status === "working") rememberStream(`${binding.rowId}:${stream.id}`, entry);
134513
+ return stream.id;
134514
+ },
134515
+ update: async (binding, cardId, view) => {
134516
+ const key = `${binding.rowId}:${cardId}`;
134517
+ const entry = liveStreams.get(key);
134518
+ if (entry === void 0) return;
134519
+ await emit(entry, view);
134520
+ if (view.status !== "working") liveStreams.delete(key);
134521
+ }
134522
+ },
134172
134523
  answerByText: (_binding, text2, options) => {
134173
134524
  const index = optionIndexFrom(text2, options.length);
134174
134525
  return index === null ? null : options[index - 1]?.token ?? null;
@@ -134222,6 +134573,7 @@ var imPlugin = definePlugin({
134222
134573
  // Telegram 在不少网络里要绕代理,而绕不绕、绕哪个是按行配的 —— 这是插件面上
134223
134574
  // `ctx.proxies` 的第一个真实消费者。
134224
134575
  fetchVia: (proxy) => ctx.proxies.fetch(proxy),
134576
+ translate: t,
134225
134577
  log: {
134226
134578
  info: (message, fields) => ctx.log.info(message, fields),
134227
134579
  warn: (message, fields) => ctx.log.warn(message, fields)
@@ -134230,6 +134582,7 @@ var imPlugin = definePlugin({
134230
134582
  makeWecomAdapter({
134231
134583
  // 企微不需要厂商 SDK:全程 JSON over WebSocket,走插件面那条出网长连接。
134232
134584
  connectSocket: (url) => ctx.socket.connect({ url }),
134585
+ translate: t,
134233
134586
  log: {
134234
134587
  info: (message, fields) => ctx.log.info(message, fields),
134235
134588
  warn: (message, fields) => ctx.log.warn(message, fields)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-im",
3
- "version": "0.2.2570",
3
+ "version": "0.2.2588",
4
4
  "description": "Talk to your agents from Telegram, WeCom or Lark.",
5
5
  "keywords": [
6
6
  "im",
@@ -27,7 +27,7 @@
27
27
  "esbuild": "0.28.1",
28
28
  "markdown-it": "^15.0.2",
29
29
  "vite-plus": "0.2.2",
30
- "@ycodium-ai/plugin-api": "0.2.2570"
30
+ "@ycodium-ai/plugin-api": "0.2.2588"
31
31
  },
32
32
  "ycodium": {
33
33
  "plugin": "./dist/index.js",