@ycodium-ai/plugin-im 0.2.2488 → 0.2.2526

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 +6 -0
  2. package/dist/index.js +331 -145
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -10,6 +10,12 @@ Each settings-card row is one group:
10
10
 
11
11
  - Pick a project and that group can start work there, and only hears that project.
12
12
  - Leave the project empty and the row only reports, on every project.
13
+ - Archived projects are read-only. The project picker hides them unless a row is
14
+ already bound to one (shown as "(archived)"), and a message to a group bound
15
+ to an archived project gets a one-line reply that the project is archived
16
+ instead of starting work, at most once every ten minutes per group. Chat
17
+ commands such as help and status still work.
18
+ Restore the project to resume.
13
19
  - Secrets (bot tokens, app credentials) stay in the environment secret store.
14
20
  The patch file keeps a reference, not the value.
15
21
  - Who may command the bot is an allow-list of that platform's user ids. Empty
package/dist/index.js CHANGED
@@ -125980,9 +125980,49 @@ var describeTargets = (target, teams) => {
125980
125980
  return [current, "", list, "", "\u7528 /team <\u961F\u540D> \u5207\u6362\uFF0C/thread \u5207\u56DE\u666E\u901A\u4F1A\u8BDD\u3002"].join("\n");
125981
125981
  };
125982
125982
 
125983
+ // src/cooldown.ts
125984
+ var makeCooldown = (windowMs, now) => {
125985
+ const lastAt = /* @__PURE__ */ new Map();
125986
+ return {
125987
+ tryAcquire: (key) => {
125988
+ const current = now();
125989
+ const previous = lastAt.get(key);
125990
+ if (previous !== void 0 && current - previous < windowMs) return false;
125991
+ lastAt.set(key, current);
125992
+ return true;
125993
+ }
125994
+ };
125995
+ };
125996
+
125997
+ // src/splitMessage.ts
125998
+ var splitMessage = (text, limit) => {
125999
+ if (text.length <= limit) return [text];
126000
+ const chunks = [];
126001
+ let rest = text;
126002
+ while (rest.length > limit) {
126003
+ const window2 = rest.slice(0, limit);
126004
+ const paragraph = window2.lastIndexOf("\n\n");
126005
+ const line = window2.lastIndexOf("\n");
126006
+ const half = limit / 2;
126007
+ let cut;
126008
+ if (paragraph >= half) cut = paragraph + 2;
126009
+ else if (line >= half) cut = line + 1;
126010
+ else if (paragraph > 0 || line > 0) cut = Math.max(paragraph > 0 ? paragraph + 2 : 0, line + 1);
126011
+ else {
126012
+ cut = limit;
126013
+ const last = window2.charCodeAt(limit - 1);
126014
+ if (last >= 55296 && last <= 56319) cut -= 1;
126015
+ }
126016
+ chunks.push(rest.slice(0, cut));
126017
+ rest = rest.slice(cut);
126018
+ }
126019
+ if (rest.length > 0) chunks.push(rest);
126020
+ return chunks;
126021
+ };
126022
+
125983
126023
  // src/larkApi.ts
125984
126024
  var LARK_BASE = "https://open.feishu.cn/open-apis";
125985
- var LARK_TEXT_MAX_CHARS = 1500;
126025
+ var LARK_TEXT_MAX_CHARS = 3500;
125986
126026
  var LarkApiError = class extends Error {
125987
126027
  operation;
125988
126028
  code;
@@ -126122,7 +126162,7 @@ var sendChatCard = async (input) => {
126122
126162
  const body = await readEnvelope("send.card", response);
126123
126163
  return body.data?.message_id ?? null;
126124
126164
  };
126125
- var clampText = (text) => text.length <= LARK_TEXT_MAX_CHARS ? text : `${text.slice(0, LARK_TEXT_MAX_CHARS)}\u2026`;
126165
+ var splitForLark = (text) => splitMessage(text, LARK_TEXT_MAX_CHARS);
126126
126166
 
126127
126167
  // src/larkInbound.ts
126128
126168
  var import_node_sdk = __toESM(require_lib2(), 1);
@@ -126258,6 +126298,16 @@ var cardActionToken = (event) => {
126258
126298
  }
126259
126299
  return null;
126260
126300
  };
126301
+ var cardActionOperator = (event) => {
126302
+ const roots = [event, event?.event];
126303
+ for (const root of roots) {
126304
+ const record = root;
126305
+ for (const candidate of [record?.operator?.open_id, record?.open_id]) {
126306
+ if (typeof candidate === "string" && candidate.length > 0) return candidate;
126307
+ }
126308
+ }
126309
+ return null;
126310
+ };
126261
126311
  var startLarkInbound = (input) => {
126262
126312
  const dispatcher = new import_node_sdk.EventDispatcher({}).register({
126263
126313
  "im.message.receive_v1": (event) => {
@@ -126265,12 +126315,13 @@ var startLarkInbound = (input) => {
126265
126315
  if (parsed !== null) input.onMessage(parsed);
126266
126316
  return {};
126267
126317
  },
126268
- // 卡片点击走的也是 event 帧(宿主的飞书桥实测过),所以在同一个 dispatcher 上注册
126269
- // 就行,不用开 webhook。这条不需要 @:点按钮直接推给应用。
126318
+ // 卡片点击走的也是 event 帧(实测过),所以在同一个 dispatcher 上注册就行,不用开
126319
+ // webhook。这条不需要 @:点按钮直接推给应用。
126270
126320
  "card.action.trigger": (event) => {
126271
126321
  const token = cardActionToken(event);
126272
- if (token !== null) input.onCardAction(token);
126273
- return {};
126322
+ if (token === null) return {};
126323
+ const notice = input.onCardAction(token, cardActionOperator(event));
126324
+ return notice === null ? {} : { toast: { type: "info", content: notice } };
126274
126325
  }
126275
126326
  });
126276
126327
  const client = new import_node_sdk.WSClient({
@@ -126337,13 +126388,16 @@ var makeLarkAdapter = (deps) => {
126337
126388
  });
126338
126389
  },
126339
126390
  sendText: async (binding, text) => {
126340
- const messageId = await sendChatText({
126341
- http: deps.http,
126342
- token: await tenantToken(binding),
126343
- chatId: binding.chatId,
126344
- text: clampText(text)
126345
- });
126346
- deps.log.info("lark delivered a message", { messageId, group: binding.displayName });
126391
+ const token = await tenantToken(binding);
126392
+ for (const chunk of splitForLark(text)) {
126393
+ const messageId = await sendChatText({
126394
+ http: deps.http,
126395
+ token,
126396
+ chatId: binding.chatId,
126397
+ text: chunk
126398
+ });
126399
+ deps.log.info("lark delivered a message", { messageId, group: binding.displayName });
126400
+ }
126347
126401
  },
126348
126402
  sendImage: async (binding, image) => {
126349
126403
  const token = await tenantToken(binding);
@@ -126401,6 +126455,14 @@ var makeLarkAdapter = (deps) => {
126401
126455
  };
126402
126456
  };
126403
126457
 
126458
+ // src/projectArchive.ts
126459
+ var imProjectOptions = (projects, boundIds) => projects.flatMap((project) => {
126460
+ if (project.archivedAt == null) return [{ value: project.projectId, label: project.title }];
126461
+ return boundIds.has(project.projectId) ? [{ value: project.projectId, label: `${project.title} (archived)` }] : [];
126462
+ });
126463
+ var archivedProjectReply = (title) => `\u9879\u76EE\u300C${title}\u300D\u5DF2\u5F52\u6863\uFF0C\u6062\u590D\u540E\u518D\u53D1\u6D88\u606F\u3002`;
126464
+ var missingProjectReply = (group) => `\u300C${group}\u300D\u7ED1\u7684\u9879\u76EE\u5DF2\u7ECF\u4E0D\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u4E86\uFF08\u53EF\u80FD\u88AB\u5220\u4E86\uFF09\uFF0C\u5F00\u4E0D\u4E86\u4F1A\u8BDD\u3002\u5230 Ycodium \u7684 IM \u8BBE\u7F6E\u91CC\u7ED9\u8FD9\u4E2A\u7FA4\u91CD\u65B0\u9009\u4E00\u4E2A\u9879\u76EE\u3002`;
126465
+
126404
126466
  // src/telegramApi.ts
126405
126467
  var API_BASE = "https://api.telegram.org";
126406
126468
  var POLL_TIMEOUT_SECONDS = 45;
@@ -126451,15 +126513,17 @@ var resourceOf = (message) => {
126451
126513
  }
126452
126514
  return null;
126453
126515
  };
126454
- var splitForTelegram = (text, limit = TELEGRAM_TEXT_MAX_CHARS) => {
126455
- if (text.length <= limit) return [text];
126456
- const chunks = [];
126457
- for (let index = 0; index < text.length; index += limit) {
126458
- chunks.push(text.slice(index, index + limit));
126516
+ var splitForTelegram = (text, limit = TELEGRAM_TEXT_MAX_CHARS) => splitMessage(text, limit);
126517
+ var nextOffset = (updates, current) => updates.reduce((offset, update) => Math.max(offset, update.update_id + 1), current);
126518
+ var commandForBot = (text, botUsername) => {
126519
+ const match = text.match(/^(\/[A-Za-z0-9_]+)@([A-Za-z0-9_]+)(?=\s|$)/);
126520
+ if (match === null) return text;
126521
+ const [whole, command = "", addressee = ""] = match;
126522
+ if (botUsername !== null && addressee.toLowerCase() !== botUsername.toLowerCase()) {
126523
+ return null;
126459
126524
  }
126460
- return chunks;
126525
+ return command + text.slice(whole.length);
126461
126526
  };
126462
- var nextOffset = (updates, current) => updates.reduce((offset, update) => Math.max(offset, update.update_id + 1), current);
126463
126527
  var makeTelegramApi = (input) => {
126464
126528
  const call = async (method, body) => {
126465
126529
  const response = await input.http(`${API_BASE}/bot${input.token}/${method}`, {
@@ -126474,6 +126538,8 @@ var makeTelegramApi = (input) => {
126474
126538
  return envelope.result;
126475
126539
  };
126476
126540
  return {
126541
+ /** 机器人自己是谁;群里的命令靠它的 `username` 认出是不是发给自己的。 */
126542
+ getMe: () => call("getMe", {}),
126477
126543
  /**
126478
126544
  * 拉一批更新。挂在服务端最多 `POLL_TIMEOUT_SECONDS` 秒。
126479
126545
  *
@@ -126500,8 +126566,11 @@ var makeTelegramApi = (input) => {
126500
126566
  ])
126501
126567
  }
126502
126568
  }),
126503
- /** 答完点击,不然客户端上那个按钮一直转圈。 */
126504
- answerCallback: (callbackQueryId) => call("answerCallbackQuery", { callback_query_id: callbackQueryId }),
126569
+ /** 答完点击,不然客户端上那个按钮一直转圈。带 `text` 时点击者会看到一句提示。 */
126570
+ answerCallback: (callbackQueryId, text) => call("answerCallbackQuery", {
126571
+ callback_query_id: callbackQueryId,
126572
+ ...text === void 0 ? {} : { text }
126573
+ }),
126505
126574
  sendPhoto: async (chatId, photo) => {
126506
126575
  const form = new FormData();
126507
126576
  form.set("chat_id", chatId);
@@ -126544,6 +126613,7 @@ var COLUMNS2 = [
126544
126613
  var BACKOFF_START_MS = 1e3;
126545
126614
  var BACKOFF_MAX_MS = 6e4;
126546
126615
  var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
126616
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
126547
126617
  var makeTelegramAdapter = (deps) => {
126548
126618
  const apis = /* @__PURE__ */ new Map();
126549
126619
  const apiFor = async (binding) => {
@@ -126561,32 +126631,41 @@ var makeTelegramAdapter = (deps) => {
126561
126631
  columns: COLUMNS2,
126562
126632
  isConfigured: (binding) => tokenOf(binding) !== "",
126563
126633
  connect: async (binding, handlers) => {
126564
- const api = await apiFor(binding);
126565
126634
  let stop = () => {
126566
126635
  };
126567
126636
  const stopped = new Promise((resolve) => stop = resolve);
126568
126637
  let running = true;
126569
126638
  let offset = 0;
126570
126639
  let backoff = BACKOFF_START_MS;
126640
+ let botUsername;
126571
126641
  const poll = async () => {
126572
126642
  for (; ; ) {
126573
126643
  if (!running) return;
126574
126644
  try {
126645
+ const api = await apiFor(binding);
126646
+ botUsername ??= (await api.getMe()).username ?? null;
126575
126647
  const updates = await api.getUpdates(offset);
126648
+ if (!running) return;
126576
126649
  backoff = BACKOFF_START_MS;
126577
126650
  offset = nextOffset(updates, offset);
126578
126651
  for (const update of updates) {
126579
126652
  const callback = update.callback_query;
126580
126653
  if (callback?.data !== void 0) {
126581
- handlers.onApproval(callback.data);
126582
- await api.answerCallback(callback.id).catch(() => void 0);
126654
+ const notice = handlers.onApproval(
126655
+ callback.data,
126656
+ callback.from === void 0 ? null : String(callback.from.id)
126657
+ );
126658
+ await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
126583
126659
  continue;
126584
126660
  }
126585
126661
  const message = update.message;
126586
126662
  if (message === void 0) continue;
126587
126663
  const resource = resourceOf(message);
126588
- const text = (message.text ?? message.caption ?? "").trim();
126589
- if (text === "" && resource === null) continue;
126664
+ const text = commandForBot(
126665
+ (message.text ?? message.caption ?? "").trim(),
126666
+ botUsername
126667
+ );
126668
+ if (text === null || text === "" && resource === null) continue;
126590
126669
  handlers.onMessage({
126591
126670
  messageId: String(message.message_id),
126592
126671
  chatId: String(message.chat.id),
@@ -126603,10 +126682,7 @@ var makeTelegramAdapter = (deps) => {
126603
126682
  } catch (error) {
126604
126683
  if (!running) return;
126605
126684
  handlers.onError(error instanceof Error ? error.message : String(error));
126606
- await Promise.race([
126607
- new Promise((resolve) => setTimeout(resolve, backoff)),
126608
- stopped
126609
- ]);
126685
+ await Promise.race([(deps.sleep ?? defaultSleep)(backoff), stopped]);
126610
126686
  backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
126611
126687
  }
126612
126688
  }
@@ -126670,14 +126746,7 @@ var parseWecomMessage = (frame, botId) => {
126670
126746
  chatType: mentioned ? "single" : chatType
126671
126747
  };
126672
126748
  };
126673
- var splitForWecom = (text, limit = WECOM_TEXT_MAX_CHARS) => {
126674
- if (text.length <= limit) return [text];
126675
- const chunks = [];
126676
- for (let index = 0; index < text.length; index += limit) {
126677
- chunks.push(text.slice(index, index + limit));
126678
- }
126679
- return chunks;
126680
- };
126749
+ var splitForWecom = (text, limit = WECOM_TEXT_MAX_CHARS) => splitMessage(text, limit);
126681
126750
  var decoder = new TextDecoder();
126682
126751
  var runWecomClient = (input) => {
126683
126752
  let sequence = 0;
@@ -126687,6 +126756,8 @@ var runWecomClient = (input) => {
126687
126756
  const subscribeId = nextRequestId("aibot_subscribe");
126688
126757
  let missedPong = 0;
126689
126758
  let closed = false;
126759
+ let finished = false;
126760
+ const ended = Promise.withResolvers();
126690
126761
  const send = (frame) => input.socket.send(JSON.stringify(frame));
126691
126762
  const settleAck = (requestId, error) => {
126692
126763
  const pending = pendingAcks.get(requestId);
@@ -126718,6 +126789,16 @@ var runWecomClient = (input) => {
126718
126789
  const message = parseWecomMessage(frame, input.botId);
126719
126790
  if (message !== null) input.handlers.onMessage(message);
126720
126791
  };
126792
+ const finish = () => {
126793
+ if (finished) return;
126794
+ finished = true;
126795
+ clearInterval(heartbeat);
126796
+ void input.socket.close().catch(() => void 0);
126797
+ for (const requestId of pendingAcks.keys()) {
126798
+ settleAck(requestId, new Error("WeCom connection closed"));
126799
+ }
126800
+ ended.resolve();
126801
+ };
126721
126802
  const readLoop = async () => {
126722
126803
  try {
126723
126804
  for await (const chunk of input.socket.messages) {
@@ -126728,12 +126809,13 @@ var runWecomClient = (input) => {
126728
126809
  input.handlers.onError(error instanceof Error ? error.message : String(error));
126729
126810
  }
126730
126811
  }
126812
+ finish();
126731
126813
  };
126732
126814
  const heartbeat = setInterval(() => {
126733
- if (closed) return;
126815
+ if (finished) return;
126734
126816
  if (missedPong >= MAX_MISSED_PONGS) {
126735
126817
  input.handlers.onError("socket hang up: missed pong");
126736
- void input.socket.close();
126818
+ finish();
126737
126819
  return;
126738
126820
  }
126739
126821
  missedPong += 1;
@@ -126753,9 +126835,14 @@ var runWecomClient = (input) => {
126753
126835
  }).catch((error) => {
126754
126836
  settleAck(subscribeId, error instanceof Error ? error : new Error(String(error)));
126755
126837
  });
126756
- void subscribed.catch((error) => {
126757
- input.handlers.onError(error instanceof Error ? error.message : String(error));
126758
- });
126838
+ const ready = subscribed.then(
126839
+ () => true,
126840
+ (error) => {
126841
+ if (!closed) input.handlers.onError(error instanceof Error ? error.message : String(error));
126842
+ finish();
126843
+ return false;
126844
+ }
126845
+ );
126759
126846
  const sendText = async (chatId, text) => {
126760
126847
  for (const [index, content] of splitForWecom(text).entries()) {
126761
126848
  const inboundRequestId = index === 0 ? pendingRequests.get(chatId) : void 0;
@@ -126800,18 +126887,22 @@ var runWecomClient = (input) => {
126800
126887
  },
126801
126888
  close: () => {
126802
126889
  closed = true;
126803
- clearInterval(heartbeat);
126804
- void input.socket.close();
126890
+ finish();
126805
126891
  },
126892
+ ended: ended.promise,
126893
+ ready,
126806
126894
  pendingAckCountForTests: () => pendingAcks.size
126807
126895
  };
126808
126896
  };
126809
126897
 
126810
126898
  // src/wecomAdapter.ts
126899
+ var BACKOFF_START_MS2 = 1e3;
126900
+ var BACKOFF_MAX_MS2 = 6e4;
126811
126901
  var COLUMNS3 = [
126812
126902
  { key: "wecomBotId", kind: "text", label: "WeCom bot ID" },
126813
126903
  { key: "wecomBotSecret", kind: "password", label: "WeCom bot secret" }
126814
126904
  ];
126905
+ var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
126815
126906
  var credentials2 = (binding) => ({
126816
126907
  botId: binding.values["wecomBotId"] ?? "",
126817
126908
  botSecret: binding.values["wecomBotSecret"] ?? "",
@@ -126834,39 +126925,73 @@ var makeWecomAdapter = (deps) => {
126834
126925
  const { botId, botSecret } = credentials2(binding);
126835
126926
  return botId !== "" && botSecret !== "";
126836
126927
  },
126837
- connect: async (binding, handlers) => {
126928
+ connect: (binding, handlers) => {
126838
126929
  const { botId, botSecret, endpoint } = credentials2(binding);
126839
- const socket = await deps.connectSocket(endpoint);
126840
- const client = runWecomClient({
126841
- socket: {
126842
- send: (data) => socket.send(data),
126843
- messages: socket.messages,
126844
- close: () => socket.close()
126845
- },
126846
- botId,
126847
- botSecret,
126848
- handlers: {
126849
- onError: handlers.onError,
126850
- onMessage: (message) => {
126851
- if (message.chatType !== "single") return;
126852
- client.rememberRequest(message.chatId, message.requestId);
126853
- handlers.onMessage({
126854
- messageId: message.messageId,
126855
- chatId: message.chatId,
126856
- senderId: message.senderUserId,
126857
- text: message.text,
126858
- // 企微这一版不收附件:宿主那份也没做,先不假装支持。
126859
- resource: null
126860
- });
126930
+ let stopped = false;
126931
+ let current = null;
126932
+ const stopSignal = Promise.withResolvers();
126933
+ const runOnce = async () => {
126934
+ const socket = await deps.connectSocket(endpoint);
126935
+ if (stopped) {
126936
+ await socket.close();
126937
+ return false;
126938
+ }
126939
+ const client = runWecomClient({
126940
+ socket: {
126941
+ send: (data) => socket.send(data),
126942
+ messages: socket.messages,
126943
+ close: () => socket.close()
126944
+ },
126945
+ botId,
126946
+ botSecret,
126947
+ handlers: {
126948
+ onError: handlers.onError,
126949
+ onMessage: (message) => {
126950
+ if (message.chatType !== "single") return;
126951
+ client.rememberRequest(message.chatId, message.requestId);
126952
+ handlers.onMessage({
126953
+ messageId: message.messageId,
126954
+ chatId: message.chatId,
126955
+ senderId: message.senderUserId,
126956
+ text: message.text,
126957
+ // 企微这一版不收附件,先不假装支持。
126958
+ resource: null
126959
+ });
126960
+ }
126861
126961
  }
126962
+ });
126963
+ current = client;
126964
+ clients.set(binding.rowId, client);
126965
+ const healthy = client.ready.then((ok) => {
126966
+ if (ok) deps.log.info("wecom connected", { group: binding.displayName });
126967
+ return ok;
126968
+ });
126969
+ await client.ended;
126970
+ if (clients.get(binding.rowId) === client) clients.delete(binding.rowId);
126971
+ current = null;
126972
+ return healthy;
126973
+ };
126974
+ const supervise = async () => {
126975
+ let backoff = BACKOFF_START_MS2;
126976
+ for (; ; ) {
126977
+ if (stopped) return;
126978
+ try {
126979
+ if (await runOnce()) backoff = BACKOFF_START_MS2;
126980
+ } catch (error) {
126981
+ handlers.onError(error instanceof Error ? error.message : String(error));
126982
+ }
126983
+ if (stopped) return;
126984
+ deps.log.warn("wecom reconnecting", { group: binding.displayName, inMs: backoff });
126985
+ await Promise.race([(deps.sleep ?? defaultSleep2)(backoff), stopSignal.promise]);
126986
+ backoff = Math.min(backoff * 2, BACKOFF_MAX_MS2);
126862
126987
  }
126863
- });
126864
- clients.set(binding.rowId, client);
126865
- deps.log.info("wecom connected", { group: binding.displayName });
126988
+ };
126989
+ void supervise();
126866
126990
  return {
126867
126991
  close: () => {
126868
- clients.delete(binding.rowId);
126869
- client.close();
126992
+ stopped = true;
126993
+ stopSignal.resolve();
126994
+ current?.close();
126870
126995
  }
126871
126996
  };
126872
126997
  },
@@ -126901,6 +127026,9 @@ var SEEN_MESSAGE_LIMIT = 256;
126901
127026
  var THREAD_PROJECT_CACHE_LIMIT = 256;
126902
127027
  var TITLE_MAX_CHARS = 72;
126903
127028
  var DEFAULT_PLATFORM = "lark";
127029
+ var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
127030
+ var NOT_ALLOWED_NOTICE = "\u4F60\u4E0D\u5728\u8FD9\u4E2A\u7FA4\u7684\u53EF\u6307\u6325\u540D\u5355\u91CC\uFF0C\u8FD9\u6B21\u70B9\u51FB\u6CA1\u6709\u751F\u6548\u3002";
127031
+ var noDefaultModelReply = (title) => `\u9879\u76EE\u300C${title}\u300D\u8FD8\u6CA1\u6709\u9ED8\u8BA4\u6A21\u578B\uFF0C\u5F00\u4E0D\u4E86\u4F1A\u8BDD\u3002\u5148\u5728 Ycodium \u91CC\u7ED9\u5B83\u9009\u4E00\u4E2A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u518D\u56DE\u6765\u53D1\u6D88\u606F\u3002`;
126904
127032
  var INBOUND_RUNTIME_MODES = /* @__PURE__ */ new Set([
126905
127033
  "approval-required",
126906
127034
  "auto-accept-edits",
@@ -126908,6 +127036,7 @@ var INBOUND_RUNTIME_MODES = /* @__PURE__ */ new Set([
126908
127036
  "full-access"
126909
127037
  ]);
126910
127038
  var DEFAULT_INBOUND_RUNTIME_MODE = "auto-accept-edits";
127039
+ var chatKeyOf = (binding) => `${binding.platform}:${binding.chatId}`;
126911
127040
  var readString = (row, key) => {
126912
127041
  const value = row[key];
126913
127042
  return typeof value === "string" ? value.trim() : "";
@@ -126951,6 +127080,15 @@ var imPlugin = definePlugin({
126951
127080
  ];
126952
127081
  const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
126953
127082
  const projects = await ctx.projections.listProjects().catch(() => []);
127083
+ const boundProjectIds = /* @__PURE__ */ new Set();
127084
+ const configuredRows = ctx.config[GROUPS_FIELD];
127085
+ if (typeof configuredRows === "object" && configuredRows !== null) {
127086
+ for (const row of Object.values(configuredRows)) {
127087
+ if (typeof row !== "object" || row === null) continue;
127088
+ const projectId = readString(row, "projectId");
127089
+ if (projectId !== "") boundProjectIds.add(projectId);
127090
+ }
127091
+ }
126954
127092
  const platformColumn = {
126955
127093
  key: "platform",
126956
127094
  kind: "select",
@@ -127006,7 +127144,7 @@ var imPlugin = definePlugin({
127006
127144
  description: "Where a message from this group starts its thread. Empty means the row only reports, and reports every project.",
127007
127145
  options: [
127008
127146
  { value: "", label: "Report only, all projects" },
127009
- ...projects.map((project) => ({ value: project.projectId, label: project.title }))
127147
+ ...imProjectOptions(projects, boundProjectIds)
127010
127148
  ]
127011
127149
  },
127012
127150
  {
@@ -127119,7 +127257,7 @@ var imPlugin = definePlugin({
127119
127257
  const targetsForProject = (projectId) => readBindings().filter(
127120
127258
  (binding) => binding.projectId === "" || binding.projectId === projectId
127121
127259
  );
127122
- const bindingForChat = (chatId) => readBindings().find((binding) => binding.chatId === chatId);
127260
+ const bindingForChat = (platform, chatId) => readBindings().find((binding) => binding.platform === platform && binding.chatId === chatId);
127123
127261
  const threadsWithSomethingToReport = /* @__PURE__ */ new Set();
127124
127262
  const acknowledgements = /* @__PURE__ */ new Map();
127125
127263
  const markWorking = async (binding, threadId, messageId) => {
@@ -127202,7 +127340,7 @@ ${final.text}`, final.attachments);
127202
127340
  const binding = chatByRun.get(runId);
127203
127341
  if (binding === void 0) return;
127204
127342
  chatByRun.delete(runId);
127205
- await clearWorking(binding, binding.chatId);
127343
+ await clearWorking(binding, chatKeyOf(binding));
127206
127344
  await deliver(binding, event.summary);
127207
127345
  });
127208
127346
  ctx.events.on("thread.failed", async (event) => {
@@ -127225,8 +127363,8 @@ ${final.text}`, final.attachments);
127225
127363
  for (const [key, value] of answersByToken) {
127226
127364
  if (value.requestId === answer.requestId) answersByToken.delete(key);
127227
127365
  }
127228
- for (const [chatId, options] of openOptionsByChat) {
127229
- if (options.some((option) => option.token === token)) openOptionsByChat.delete(chatId);
127366
+ for (const [chatKey, options] of openOptionsByChat) {
127367
+ if (options.some((option) => option.token === token)) openOptionsByChat.delete(chatKey);
127230
127368
  }
127231
127369
  await ctx.commands.dispatch({
127232
127370
  type: "thread.approval.respond",
@@ -127245,7 +127383,7 @@ ${final.text}`, final.attachments);
127245
127383
  const threadId = event.threadId;
127246
127384
  if (threadId === null) return;
127247
127385
  const binding = readBindings().find(
127248
- (candidate) => threadByChat.get(candidate.chatId) === threadId
127386
+ (candidate) => threadByChat.get(chatKeyOf(candidate)) === threadId
127249
127387
  );
127250
127388
  if (binding === void 0) return;
127251
127389
  const adapter = adapterFor(binding.platform);
@@ -127262,11 +127400,12 @@ ${final.text}`, final.attachments);
127262
127400
  answersByToken.set(token, {
127263
127401
  threadId,
127264
127402
  requestId: approval.requestId,
127265
- decision: option.decision
127403
+ decision: option.decision,
127404
+ chatKey: chatKeyOf(binding)
127266
127405
  });
127267
127406
  return { label: option.label, token };
127268
127407
  });
127269
- openOptionsByChat.set(binding.chatId, buttons);
127408
+ openOptionsByChat.set(chatKeyOf(binding), buttons);
127270
127409
  try {
127271
127410
  await adapter.askApproval(binding, {
127272
127411
  title: approval.appName ?? "Ycodium",
@@ -127278,10 +127417,29 @@ ${final.text}`, final.attachments);
127278
127417
  ctx.log.warn("chat could not ask for the approval", { error: describeError(error) });
127279
127418
  }
127280
127419
  });
127281
- const alreadyHandled = (messageId) => {
127282
- if (seen.has(messageId)) return true;
127283
- seen.add(messageId);
127284
- seenOrder.push(messageId);
127420
+ const isAllowedSender = (binding, sender) => binding.allowedSenders.size === 0 || sender !== null && binding.allowedSenders.has(sender);
127421
+ const handleApprovalClick = (token, operatorId) => {
127422
+ const answer = answersByToken.get(token);
127423
+ if (answer === void 0) return null;
127424
+ const target = readBindings().find((binding) => chatKeyOf(binding) === answer.chatKey);
127425
+ if (target === void 0 || !isAllowedSender(target, operatorId)) {
127426
+ ctx.log.warn("chat ignored an approval click from someone not on the allow list", {
127427
+ group: target?.displayName ?? answer.chatKey,
127428
+ operator: operatorId
127429
+ });
127430
+ return NOT_ALLOWED_NOTICE;
127431
+ }
127432
+ void answerByToken(token).catch((error) => {
127433
+ ctx.log.warn("chat could not answer the approval", { error: describeError(error) });
127434
+ });
127435
+ return null;
127436
+ };
127437
+ const archivedReplyCooldown = makeCooldown(ARCHIVED_REPLY_COOLDOWN_MS, () => Date.now());
127438
+ const alreadyHandled = (target, messageId) => {
127439
+ const key = `${chatKeyOf(target)}:${messageId}`;
127440
+ if (seen.has(key)) return true;
127441
+ seen.add(key);
127442
+ seenOrder.push(key);
127285
127443
  if (seenOrder.length > SEEN_MESSAGE_LIMIT) {
127286
127444
  const oldest = seenOrder.shift();
127287
127445
  if (oldest !== void 0) seen.delete(oldest);
@@ -127329,7 +127487,7 @@ ${final.text}`, final.attachments);
127329
127487
  ) : []
127330
127488
  ).catch(() => []);
127331
127489
  const command = parseChatCommand({ text: message.text, teams, chinese: true });
127332
- const chatId = binding.chatId;
127490
+ const chatKey = chatKeyOf(binding);
127333
127491
  if (command.kind !== "message") {
127334
127492
  ctx.log.info("chat handled a command", {
127335
127493
  group: binding.displayName,
@@ -127340,19 +127498,19 @@ ${final.text}`, final.attachments);
127340
127498
  case "message":
127341
127499
  return false;
127342
127500
  case "selectTeam":
127343
- targetByChat.set(chatId, { kind: "team", teamId: command.team.teamId });
127344
- threadByChat.delete(chatId);
127501
+ targetByChat.set(chatKey, { kind: "team", teamId: command.team.teamId });
127502
+ threadByChat.delete(chatKey);
127345
127503
  await deliver(binding, `\u597D\uFF0C\u63A5\u4E0B\u6765\u6D3E\u7ED9\u300C${command.team.title}\u300D\u3002`);
127346
127504
  return true;
127347
127505
  case "selectThread":
127348
- targetByChat.set(chatId, { kind: "thread" });
127349
- threadByChat.delete(chatId);
127506
+ targetByChat.set(chatKey, { kind: "thread" });
127507
+ threadByChat.delete(chatKey);
127350
127508
  await deliver(binding, "\u597D\uFF0C\u63A5\u4E0B\u6765\u5F00\u666E\u901A\u4F1A\u8BDD\u3002");
127351
127509
  return true;
127352
127510
  case "showTargets":
127353
127511
  await deliver(
127354
127512
  binding,
127355
- describeTargets(targetByChat.get(chatId) ?? { kind: "thread" }, teams)
127513
+ describeTargets(targetByChat.get(chatKey) ?? { kind: "thread" }, teams)
127356
127514
  );
127357
127515
  return true;
127358
127516
  case "teamNotFound":
@@ -127361,7 +127519,7 @@ ${final.text}`, final.attachments);
127361
127519
  [
127362
127520
  `\u6CA1\u627E\u5230\u53EB\u300C${command.query}\u300D\u7684\u961F\uFF0C\u4E5F\u53EF\u80FD\u662F\u4E0D\u6B62\u4E00\u652F\u5BF9\u5F97\u4E0A\u3002`,
127363
127521
  "",
127364
- describeTargets(targetByChat.get(chatId) ?? { kind: "thread" }, teams)
127522
+ describeTargets(targetByChat.get(chatKey) ?? { kind: "thread" }, teams)
127365
127523
  ].join("\n")
127366
127524
  );
127367
127525
  return true;
@@ -127372,12 +127530,12 @@ ${final.text}`, final.attachments);
127372
127530
  await deliver(binding, helpText());
127373
127531
  return true;
127374
127532
  case "new":
127375
- threadByChat.delete(chatId);
127533
+ threadByChat.delete(chatKey);
127376
127534
  await deliver(binding, "\u597D\uFF0C\u4E0B\u4E00\u53E5\u8BDD\u5F00\u4E00\u6761\u65B0\u4F1A\u8BDD\u3002");
127377
127535
  return true;
127378
127536
  case "status": {
127379
- const threadId = threadByChat.get(chatId);
127380
- const routed = targetByChat.get(chatId) ?? { kind: "thread" };
127537
+ const threadId = threadByChat.get(chatKey);
127538
+ const routed = targetByChat.get(chatKey) ?? { kind: "thread" };
127381
127539
  const where = routed.kind === "team" ? `\u6D3E\u7ED9\uFF1A${teams.find((team) => team.teamId === routed.teamId)?.title ?? routed.teamId}` : "\u666E\u901A\u4F1A\u8BDD";
127382
127540
  if (threadId === void 0) {
127383
127541
  await deliver(binding, `${where}\u3002\u73B0\u5728\u6CA1\u6709\u5728\u8DD1\u7684\u6D3B\u513F\u3002`);
@@ -127396,7 +127554,7 @@ ${final.text}`, final.attachments);
127396
127554
  return true;
127397
127555
  }
127398
127556
  case "stop": {
127399
- const threadId = threadByChat.get(chatId);
127557
+ const threadId = threadByChat.get(chatKey);
127400
127558
  if (threadId === void 0) {
127401
127559
  await deliver(binding, "\u8FD9\u4E2A\u7FA4\u73B0\u5728\u6CA1\u6709\u5728\u8DD1\u7684\u6D3B\u513F\u3002");
127402
127560
  return true;
@@ -127442,9 +127600,9 @@ ${final.text}`, final.attachments);
127442
127600
  ctx.log.info("chat started a team run", { group: binding.displayName, teamId, runId });
127443
127601
  };
127444
127602
  const startTurn = async (binding, message) => {
127445
- const chatId = binding.chatId;
127603
+ const chatKey = chatKeyOf(binding);
127446
127604
  const text = message.text;
127447
- let threadId = threadByChat.get(chatId);
127605
+ let threadId = threadByChat.get(chatKey);
127448
127606
  if (threadId === void 0) {
127449
127607
  const project = await ctx.projections.getProject(binding.projectId);
127450
127608
  if (project === null) {
@@ -127454,6 +127612,14 @@ ${final.text}`, final.attachments);
127454
127612
  });
127455
127613
  return null;
127456
127614
  }
127615
+ if (project.defaultModelSelection === null) {
127616
+ ctx.log.warn("chat cannot start a thread: the project has no default model", {
127617
+ group: binding.displayName,
127618
+ projectId: binding.projectId
127619
+ });
127620
+ await deliver(binding, noDefaultModelReply(project.title));
127621
+ return null;
127622
+ }
127457
127623
  threadId = crypto.randomUUID();
127458
127624
  await ctx.commands.dispatch({
127459
127625
  type: "thread.create",
@@ -127461,14 +127627,14 @@ ${final.text}`, final.attachments);
127461
127627
  threadId,
127462
127628
  projectId: binding.projectId,
127463
127629
  title: titleFrom(text || message.resource?.name || "Chat"),
127464
- ...project.defaultModelSelection === null ? {} : { modelSelection: project.defaultModelSelection },
127630
+ modelSelection: project.defaultModelSelection,
127465
127631
  runtimeMode: binding.runtimeMode,
127466
127632
  interactionMode: "default",
127467
127633
  branch: null,
127468
127634
  worktreePath: null,
127469
127635
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
127470
127636
  });
127471
- threadByChat.set(chatId, threadId);
127637
+ threadByChat.set(chatKey, threadId);
127472
127638
  projectByThread.set(threadId, binding.projectId);
127473
127639
  }
127474
127640
  const attachments = await collectAttachment(binding, threadId, message);
@@ -127489,7 +127655,7 @@ ${final.text}`, final.attachments);
127489
127655
  });
127490
127656
  ctx.log.info("chat started a turn from a message", {
127491
127657
  group: binding.displayName,
127492
- chatId,
127658
+ chatId: binding.chatId,
127493
127659
  threadId
127494
127660
  });
127495
127661
  return threadId;
@@ -127497,65 +127663,85 @@ ${final.text}`, final.attachments);
127497
127663
  const receiving = readBindings().filter((binding) => binding.projectId !== "");
127498
127664
  const connections = [];
127499
127665
  const started = /* @__PURE__ */ new Set();
127500
- for (const binding of receiving) {
127501
- const adapter = adapterFor(binding.platform);
127502
- if (adapter === void 0) continue;
127503
- const fingerprint = `${binding.platform}:${adapter.columns.map((column) => binding.values[column.key] ?? "").join(" ")}`;
127504
- if (started.has(fingerprint)) continue;
127505
- started.add(fingerprint);
127506
- const connection = await adapter.connect(binding, {
127507
- onError: (message) => ctx.log.warn("chat inbound connection failed", { message }),
127508
- onApproval: (token) => {
127509
- void answerByToken(token).catch((error) => {
127666
+ let disposed = false;
127667
+ const inboundHandlersFor = (platform) => ({
127668
+ onError: (message) => ctx.log.warn("chat inbound connection failed", { platform, message }),
127669
+ onApproval: handleApprovalClick,
127670
+ onMessage: (message) => {
127671
+ const target = bindingForChat(platform, message.chatId);
127672
+ if (target === void 0 || target.projectId === "") return;
127673
+ if (alreadyHandled(target, message.messageId)) return;
127674
+ if (!isAllowedSender(target, message.senderId)) {
127675
+ ctx.log.warn("chat ignored a message from someone not on the allow list", {
127676
+ group: target.displayName,
127677
+ sender: message.senderId
127678
+ });
127679
+ return;
127680
+ }
127681
+ const open = openOptionsByChat.get(chatKeyOf(target));
127682
+ const answerToken = open === void 0 ? null : adapterFor(target.platform)?.answerByText?.(target, message.text, open) ?? null;
127683
+ if (answerToken !== null) {
127684
+ void answerByToken(answerToken).catch((error) => {
127510
127685
  ctx.log.warn("chat could not answer the approval", { error: describeError(error) });
127511
127686
  });
127512
- },
127513
- onMessage: (message) => {
127514
- const target = bindingForChat(message.chatId);
127515
- if (target === void 0 || target.projectId === "") return;
127516
- if (alreadyHandled(message.messageId)) return;
127517
- if (target.allowedSenders.size > 0) {
127518
- const sender = message.senderId;
127519
- if (sender === null || !target.allowedSenders.has(sender)) {
127520
- ctx.log.warn("chat ignored a message from someone not on the allow list", {
127687
+ return;
127688
+ }
127689
+ void (async () => {
127690
+ if (await handleCommand(target, message)) return;
127691
+ const project = await ctx.projections.getProject(target.projectId);
127692
+ if (project === null || project.archivedAt != null) {
127693
+ if (project === null) {
127694
+ ctx.log.warn("chat cannot start work: the configured project is gone", {
127521
127695
  group: target.displayName,
127522
- sender
127696
+ projectId: target.projectId
127523
127697
  });
127524
- return;
127525
127698
  }
127699
+ if (archivedReplyCooldown.tryAcquire(chatKeyOf(target))) {
127700
+ await deliver(
127701
+ target,
127702
+ project === null ? missingProjectReply(target.displayName) : archivedProjectReply(project.title)
127703
+ );
127704
+ }
127705
+ return;
127526
127706
  }
127527
- const open = openOptionsByChat.get(message.chatId);
127528
- const answerToken = open === void 0 ? null : adapterFor(target.platform)?.answerByText?.(target, message.text, open) ?? null;
127529
- if (answerToken !== null) {
127530
- void answerByToken(answerToken).catch((error) => {
127531
- ctx.log.warn("chat could not answer the approval", { error: describeError(error) });
127532
- });
127707
+ const routed = targetByChat.get(chatKeyOf(target)) ?? { kind: "thread" };
127708
+ if (routed.kind === "team") {
127709
+ await startTeamRun(target, routed.teamId, message.text);
127710
+ await markWorking(target, chatKeyOf(target), message.messageId);
127533
127711
  return;
127534
127712
  }
127535
- void (async () => {
127536
- if (await handleCommand(target, message)) return;
127537
- const routed = targetByChat.get(message.chatId) ?? { kind: "thread" };
127538
- if (routed.kind === "team") {
127539
- await startTeamRun(target, routed.teamId, message.text);
127540
- await markWorking(target, message.chatId, message.messageId);
127541
- return;
127542
- }
127543
- const threadId = await startTurn(target, message);
127544
- if (threadId !== null) await markWorking(target, threadId, message.messageId);
127545
- })().catch((error) => {
127546
- ctx.log.warn("chat could not handle the message", { error: describeError(error) });
127547
- });
127548
- }
127713
+ const threadId = await startTurn(target, message);
127714
+ if (threadId !== null) await markWorking(target, threadId, message.messageId);
127715
+ })().catch((error) => {
127716
+ ctx.log.warn("chat could not handle the message", { error: describeError(error) });
127717
+ });
127718
+ }
127719
+ });
127720
+ for (const binding of receiving) {
127721
+ const adapter = adapterFor(binding.platform);
127722
+ if (adapter === void 0) continue;
127723
+ const fingerprint = `${binding.platform}:${adapter.columns.map((column) => binding.values[column.key] ?? "").join(" ")}`;
127724
+ if (started.has(fingerprint)) continue;
127725
+ started.add(fingerprint);
127726
+ void Promise.resolve().then(() => adapter.connect(binding, inboundHandlersFor(binding.platform))).then((connection) => {
127727
+ if (disposed) connection.close();
127728
+ else connections.push(connection);
127729
+ }).catch((error) => {
127730
+ ctx.log.warn("chat could not start an inbound connection", {
127731
+ group: binding.displayName,
127732
+ platform: binding.platform,
127733
+ error: describeError(error)
127734
+ });
127549
127735
  });
127550
- connections.push(connection);
127551
127736
  }
127552
127737
  ctx.onDispose(() => {
127738
+ disposed = true;
127553
127739
  for (const connection of connections) connection.close();
127554
127740
  });
127555
127741
  ctx.log.info("chat plugin active", {
127556
127742
  groups: readBindings().length,
127557
127743
  receiving: receiving.length,
127558
- connections: connections.length
127744
+ connecting: started.size
127559
127745
  });
127560
127746
  }
127561
127747
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-im",
3
- "version": "0.2.2488",
3
+ "version": "0.2.2526",
4
4
  "description": "Talk to your agents from Telegram, WeCom or Lark.",
5
5
  "keywords": [
6
6
  "im",
@@ -24,7 +24,7 @@
24
24
  "effect": "4.0.0-beta.103",
25
25
  "esbuild": "0.28.1",
26
26
  "vite-plus": "0.2.2",
27
- "@ycodium-ai/plugin-api": "0.2.2488"
27
+ "@ycodium-ai/plugin-api": "0.2.2526"
28
28
  },
29
29
  "ycodium": {
30
30
  "plugin": "./dist/index.js",