@ycodium-ai/plugin-im 0.2.2566 → 0.2.2578

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
@@ -126208,6 +126208,7 @@ var ZH_ALIASES = {
126208
126208
  \u72B6\u6001: "status",
126209
126209
  \u65B0\u5BF9\u8BDD: "new",
126210
126210
  \u5E2E\u52A9: "help",
126211
+ \u9762\u677F: "panel",
126211
126212
  \u4F1A\u8BDD\u5217\u8868: "sessions"
126212
126213
  };
126213
126214
  var resolveTeam = (query, teams) => {
@@ -126225,6 +126226,7 @@ var parseChatCommand = (input) => {
126225
126226
  if (/^\/status$/i.test(trimmed)) return { kind: "status" };
126226
126227
  if (/^\/new$/i.test(trimmed)) return { kind: "new" };
126227
126228
  if (/^\/help$/i.test(trimmed)) return { kind: "help" };
126229
+ if (/^\/panel$/i.test(trimmed)) return { kind: "panel" };
126228
126230
  if (/^\/sessions$/i.test(trimmed)) return { kind: "sessions" };
126229
126231
  const resume = resumeArgument(input.text, input.chinese);
126230
126232
  if (resume !== void 0) {
@@ -126249,7 +126251,7 @@ var parseChatCommand = (input) => {
126249
126251
  }
126250
126252
  return { kind: "message" };
126251
126253
  };
126252
- var helpText = (t) => [
126254
+ var helpText = (t, options = {}) => [
126253
126255
  t("help.title"),
126254
126256
  t("help.stop"),
126255
126257
  t("help.status"),
@@ -126260,7 +126262,8 @@ var helpText = (t) => [
126260
126262
  t("help.plan"),
126261
126263
  t("help.team"),
126262
126264
  t("help.thread"),
126263
- t("help.aliases")
126265
+ t("help.aliases"),
126266
+ ...options.panel === true ? ["", t("help.panel")] : []
126264
126267
  ].join("\n");
126265
126268
  var describeTargets = (target, teams, t) => {
126266
126269
  const current = target.kind === "thread" ? t("targets.currentThread") : t("targets.currentTeam", {
@@ -131972,6 +131975,40 @@ var buildApprovalSettledCard = (settlement, t) => ({
131972
131975
  }
131973
131976
  ]
131974
131977
  });
131978
+ var PANEL_TEMPLATE = {
131979
+ menu: "blue",
131980
+ busy: "grey",
131981
+ notice: "orange"
131982
+ };
131983
+ var panelButton = (button) => ({
131984
+ tag: "button",
131985
+ text: { tag: "plain_text", content: button.label },
131986
+ type: button.danger === true ? "danger" : "default",
131987
+ value: button.action.arg === void 0 ? { pn: button.action.op } : { pn: button.action.op, a: button.action.arg }
131988
+ });
131989
+ var buildPanelCard = (card) => {
131990
+ const elements = [];
131991
+ if (card.text !== "") elements.push({ tag: "markdown", content: stripAngleBrackets(card.text) });
131992
+ for (const group of card.groups) {
131993
+ if (group.buttons.length === 0) continue;
131994
+ if (group.heading !== null) {
131995
+ elements.push({ tag: "markdown", content: `**${stripAngleBrackets(group.heading)}**` });
131996
+ }
131997
+ elements.push({ tag: "action", actions: group.buttons.map(panelButton) });
131998
+ }
131999
+ if (card.nav.length > 0) {
132000
+ elements.push({ tag: "hr" }, { tag: "action", actions: card.nav.map(panelButton) });
132001
+ }
132002
+ if (elements.length === 0) elements.push({ tag: "markdown", content: " " });
132003
+ return {
132004
+ config: { wide_screen_mode: true },
132005
+ header: {
132006
+ title: { tag: "plain_text", content: card.title },
132007
+ template: PANEL_TEMPLATE[card.tone]
132008
+ },
132009
+ elements
132010
+ };
132011
+ };
131975
132012
 
131976
132013
  // src/larkInbound.ts
131977
132014
  var import_node_sdk = __toESM(require_lib2(), 1);
@@ -132087,26 +132124,44 @@ var fetchMessageResource = async (input) => {
132087
132124
  }
132088
132125
  return new Uint8Array(await response.arrayBuffer());
132089
132126
  };
132090
- var cardActionToken = (event) => {
132127
+ var cardActionValue = (event) => {
132091
132128
  const roots = [event, event?.event];
132092
132129
  for (const root of roots) {
132093
132130
  const value = root?.action?.value;
132094
132131
  if (value === void 0 || value === null) continue;
132095
- let record;
132096
- if (typeof value === "string") {
132097
- try {
132098
- record = JSON.parse(value);
132099
- } catch {
132100
- record = void 0;
132101
- }
132102
- } else {
132103
- record = value;
132132
+ if (typeof value !== "string") return value;
132133
+ try {
132134
+ return JSON.parse(value);
132135
+ } catch {
132136
+ continue;
132104
132137
  }
132105
- const token = record?.["tk"];
132106
- if (typeof token === "string" && token.length > 0) return token;
132107
132138
  }
132108
132139
  return null;
132109
132140
  };
132141
+ var cardActionToken = (event) => {
132142
+ const token = cardActionValue(event)?.["tk"];
132143
+ return typeof token === "string" && token.length > 0 ? token : null;
132144
+ };
132145
+ var cardActionPanel = (event) => {
132146
+ const value = cardActionValue(event);
132147
+ const op = value?.["pn"];
132148
+ if (typeof op !== "string" || op === "") return null;
132149
+ const arg = value?.["a"];
132150
+ const roots = [event, event?.event];
132151
+ const pick = (read) => {
132152
+ for (const root of roots) {
132153
+ if (typeof root !== "object" || root === null) continue;
132154
+ const candidate = read(root);
132155
+ if (typeof candidate === "string" && candidate !== "") return candidate;
132156
+ }
132157
+ return null;
132158
+ };
132159
+ const context = (root) => typeof root["context"] === "object" && root["context"] !== null ? root["context"] : void 0;
132160
+ const cardId = pick((root) => context(root)?.["open_message_id"] ?? root["open_message_id"]);
132161
+ const chatId = pick((root) => context(root)?.["open_chat_id"] ?? root["open_chat_id"]);
132162
+ if (cardId === null || chatId === null) return null;
132163
+ return { op, arg: typeof arg === "string" ? arg : null, cardId, chatId };
132164
+ };
132110
132165
  var cardActionOperator = (event) => {
132111
132166
  const roots = [event, event?.event];
132112
132167
  for (const root of roots) {
@@ -132127,6 +132182,11 @@ var startLarkInbound = (input) => {
132127
132182
  // 卡片点击走的也是 event 帧(实测过),所以在同一个 dispatcher 上注册就行,不用开
132128
132183
  // webhook。这条不需要 @:点按钮直接推给应用。
132129
132184
  "card.action.trigger": (event) => {
132185
+ const panel = cardActionPanel(event);
132186
+ if (panel !== null) {
132187
+ const notice2 = input.onPanelClick?.(panel, cardActionOperator(event)) ?? null;
132188
+ return notice2 === null ? {} : { toast: { type: "info", content: notice2 } };
132189
+ }
132130
132190
  const token = cardActionToken(event);
132131
132191
  if (token === null) return {};
132132
132192
  const notice = input.onCardAction(token, cardActionOperator(event));
@@ -132185,6 +132245,13 @@ var makeLarkAdapter = (deps) => {
132185
132245
  appSecret,
132186
132246
  onError: handlers.onError,
132187
132247
  onCardAction: handlers.onApproval,
132248
+ onPanelClick: (click, operatorOpenId) => handlers.onPanelClick?.({
132249
+ chatId: click.chatId,
132250
+ cardId: click.cardId,
132251
+ operatorId: operatorOpenId,
132252
+ op: click.op,
132253
+ arg: click.arg
132254
+ }) ?? null,
132188
132255
  onMessage: (message) => {
132189
132256
  handlers.onMessage({
132190
132257
  messageId: message.messageId,
@@ -132263,6 +132330,22 @@ var makeLarkAdapter = (deps) => {
132263
132330
  });
132264
132331
  }
132265
132332
  },
132333
+ panel: {
132334
+ open: async (binding, card) => sendCard({
132335
+ http: deps.http,
132336
+ token: await tenantToken(binding),
132337
+ chatId: binding.chatId,
132338
+ card: buildPanelCard(card)
132339
+ }),
132340
+ update: async (binding, cardId, card) => {
132341
+ await patchCard({
132342
+ http: deps.http,
132343
+ token: await tenantToken(binding),
132344
+ messageId: cardId,
132345
+ card: buildPanelCard(card)
132346
+ });
132347
+ }
132348
+ },
132266
132349
  acknowledge: async (binding, messageId) => addReaction({
132267
132350
  http: deps.http,
132268
132351
  token: await tenantToken(binding),
@@ -132425,7 +132508,8 @@ var en_default = {
132425
132508
  sessions: "\xB7 /sessions \u2014 list the sessions this bot opened for this group's project",
132426
132509
  resume: "\xB7 /resume number \u2014 switch to one of them (a number from /sessions, or the start of its id)",
132427
132510
  model: "\xB7 /model \u2014 see the model this group uses; /model number or /model instance/model switches it",
132428
- plan: "\xB7 /plan \u2014 toggle plan mode (the agent proposes a plan instead of acting); /plan on, /plan off set it"
132511
+ plan: "\xB7 /plan \u2014 toggle plan mode (the agent proposes a plan instead of acting); /plan on, /plan off set it",
132512
+ panel: "Send /panel to open the command panel with a button for every command."
132429
132513
  },
132430
132514
  sessions: {
132431
132515
  title: "Sessions this bot opened for this group's project ({{count}}):",
@@ -132498,6 +132582,48 @@ var en_default = {
132498
132582
  chosen: "Chosen: {{label}}",
132499
132583
  elsewhere: "Already handled elsewhere (the web app or another client)."
132500
132584
  }
132585
+ },
132586
+ panel: {
132587
+ title: "Command panel",
132588
+ hint: "A button does the same as typing its command. You can still type.",
132589
+ category: {
132590
+ session: "Session",
132591
+ target: "Send to",
132592
+ system: "Settings and status"
132593
+ },
132594
+ cmd: {
132595
+ new: "New session",
132596
+ stop: "Stop this turn",
132597
+ sessions: "Sessions",
132598
+ team: "Send to a team",
132599
+ thread: "Normal session",
132600
+ model: "Model",
132601
+ planOn: "Plan mode: on",
132602
+ planOff: "Plan mode: off",
132603
+ status: "Status",
132604
+ help: "Text help"
132605
+ },
132606
+ back: "Back",
132607
+ prev: "Previous",
132608
+ next: "Next",
132609
+ page: "Page {{page}} of {{total}}",
132610
+ loading: "Loading\u2026",
132611
+ working: "Working on it\u2026",
132612
+ expired: "This panel has expired. Send /panel again.",
132613
+ busy: "The last tap is still being handled. Give it a moment.",
132614
+ loadFailed: "Could not read that just now. Try again in a moment.",
132615
+ sessions: {
132616
+ title: "Pick a session",
132617
+ hint: "Tap one to switch to it."
132618
+ },
132619
+ teams: {
132620
+ title: "Pick a team",
132621
+ hint: "Tap a team and the next messages go to it.",
132622
+ unavailable: "Could not read the team list."
132623
+ },
132624
+ models: {
132625
+ title: "Pick a model"
132626
+ }
132501
132627
  }
132502
132628
  };
132503
132629
 
@@ -132632,7 +132758,8 @@ var zh_CN_default = {
132632
132758
  sessions: "\xB7 /sessions \u2014\u2014 \u5217\u51FA\u672C\u673A\u5668\u4EBA\u4E3A\u8FD9\u4E2A\u7FA4\u7684\u9879\u76EE\u5F00\u8FC7\u7684\u4F1A\u8BDD",
132633
132759
  resume: "\xB7 /resume \u7F16\u53F7 \u2014\u2014 \u5207\u5230\u5176\u4E2D\u4E00\u6761\uFF08/sessions \u91CC\u7684\u7F16\u53F7\uFF0C\u6216\u4F1A\u8BDD id \u7684\u5F00\u5934\uFF09",
132634
132760
  model: "\xB7 /model \u2014\u2014 \u770B\u8FD9\u4E2A\u7FA4\u7528\u7684\u6A21\u578B\uFF1B/model \u7F16\u53F7 \u6216 /model \u5B9E\u4F8B/\u6A21\u578B \u5207\u6362",
132635
- plan: "\xB7 /plan \u2014\u2014 \u5F00\u5173\u8BA1\u5212\u6A21\u5F0F\uFF08agent \u5148\u51FA\u8BA1\u5212\u3001\u4E0D\u52A8\u624B\uFF09\uFF1B/plan on\u3001/plan off \u660E\u786E\u8BBE\u5B9A"
132761
+ plan: "\xB7 /plan \u2014\u2014 \u5F00\u5173\u8BA1\u5212\u6A21\u5F0F\uFF08agent \u5148\u51FA\u8BA1\u5212\u3001\u4E0D\u52A8\u624B\uFF09\uFF1B/plan on\u3001/plan off \u660E\u786E\u8BBE\u5B9A",
132762
+ panel: "\u53D1 /panel \u6253\u5F00\u53EF\u70B9\u7684\u547D\u4EE4\u9762\u677F\u3002"
132636
132763
  },
132637
132764
  sessions: {
132638
132765
  title: "\u672C\u673A\u5668\u4EBA\u4E3A\u8FD9\u4E2A\u7FA4\u7684\u9879\u76EE\u5F00\u8FC7\u7684\u4F1A\u8BDD\uFF08{{count}} \u6761\uFF09\uFF1A",
@@ -132705,6 +132832,48 @@ var zh_CN_default = {
132705
132832
  chosen: "\u5DF2\u9009\uFF1A{{label}}",
132706
132833
  elsewhere: "\u5DF2\u5728\u522B\u5904\u5904\u7406\uFF08\u7F51\u9875\u6216\u5176\u4ED6\u5BA2\u6237\u7AEF\uFF09\u3002"
132707
132834
  }
132835
+ },
132836
+ panel: {
132837
+ title: "\u547D\u4EE4\u9762\u677F",
132838
+ hint: "\u70B9\u6309\u94AE\u7B49\u4E8E\u6253\u51FA\u5BF9\u5E94\u7684\u547D\u4EE4\uFF0C\u4E5F\u53EF\u4EE5\u76F4\u63A5\u6253\u5B57\u3002",
132839
+ category: {
132840
+ session: "\u4F1A\u8BDD",
132841
+ target: "\u6D3E\u7ED9\u8C01",
132842
+ system: "\u8BBE\u7F6E\u4E0E\u72B6\u6001"
132843
+ },
132844
+ cmd: {
132845
+ new: "\u65B0\u4F1A\u8BDD",
132846
+ stop: "\u505C\u6B62\u672C\u8F6E",
132847
+ sessions: "\u4F1A\u8BDD\u5217\u8868",
132848
+ team: "\u6D3E\u7ED9\u56E2\u961F",
132849
+ thread: "\u666E\u901A\u4F1A\u8BDD",
132850
+ model: "\u6A21\u578B",
132851
+ planOn: "\u8BA1\u5212\u6A21\u5F0F\uFF1A\u5F00",
132852
+ planOff: "\u8BA1\u5212\u6A21\u5F0F\uFF1A\u5173",
132853
+ status: "\u72B6\u6001",
132854
+ help: "\u6587\u5B57\u8BF4\u660E"
132855
+ },
132856
+ back: "\u8FD4\u56DE",
132857
+ prev: "\u4E0A\u4E00\u9875",
132858
+ next: "\u4E0B\u4E00\u9875",
132859
+ page: "\u7B2C {{page}} / {{total}} \u9875",
132860
+ loading: "\u52A0\u8F7D\u4E2D\u2026",
132861
+ working: "\u5904\u7406\u4E2D\u2026",
132862
+ expired: "\u9762\u677F\u5DF2\u8FC7\u671F\uFF0C\u518D\u53D1\u4E00\u6B21 /panel\u3002",
132863
+ busy: "\u4E0A\u4E00\u6B65\u8FD8\u5728\u5904\u7406\uFF0C\u7A0D\u7B49\u4E00\u4E0B\u3002",
132864
+ loadFailed: "\u521A\u624D\u6CA1\u8BFB\u51FA\u6765\uFF0C\u7A0D\u540E\u518D\u8BD5\u3002",
132865
+ sessions: {
132866
+ title: "\u9009\u4E00\u6761\u4F1A\u8BDD",
132867
+ hint: "\u70B9\u4E00\u6761\u5207\u8FC7\u53BB\u3002"
132868
+ },
132869
+ teams: {
132870
+ title: "\u9009\u4E00\u652F\u961F",
132871
+ hint: "\u70B9\u4E00\u652F\u961F\uFF0C\u63A5\u4E0B\u6765\u7684\u6D3B\u6D3E\u7ED9\u5B83\u3002",
132872
+ unavailable: "\u8BFB\u4E0D\u5230\u56E2\u961F\u5217\u8868\u3002"
132873
+ },
132874
+ models: {
132875
+ title: "\u9009\u6A21\u578B"
132876
+ }
132708
132877
  }
132709
132878
  };
132710
132879
 
@@ -132845,18 +133014,401 @@ var pickSession = (arg, listed, rows) => {
132845
133014
  return matches.length === 1 ? matches[0]?.threadId ?? null : null;
132846
133015
  };
132847
133016
 
133017
+ // src/panelCommands.ts
133018
+ var PANEL_CATEGORIES = ["session", "target", "system"];
133019
+ var PANEL_COMMANDS = [
133020
+ {
133021
+ id: "new",
133022
+ category: "session",
133023
+ line: "/new",
133024
+ kind: "new",
133025
+ labelKey: "panel.cmd.new",
133026
+ action: { op: "run", arg: "new" }
133027
+ },
133028
+ {
133029
+ id: "stop",
133030
+ category: "session",
133031
+ line: "/stop",
133032
+ kind: "stop",
133033
+ labelKey: "panel.cmd.stop",
133034
+ action: { op: "run", arg: "stop" }
133035
+ },
133036
+ {
133037
+ id: "sessions",
133038
+ category: "session",
133039
+ line: "/sessions",
133040
+ kind: "sessions",
133041
+ labelKey: "panel.cmd.sessions",
133042
+ action: { op: "open", arg: "sessions" }
133043
+ },
133044
+ {
133045
+ id: "team",
133046
+ category: "target",
133047
+ line: "/team",
133048
+ kind: "showTargets",
133049
+ labelKey: "panel.cmd.team",
133050
+ action: { op: "open", arg: "teams" }
133051
+ },
133052
+ {
133053
+ id: "thread",
133054
+ category: "target",
133055
+ line: "/thread",
133056
+ kind: "selectThread",
133057
+ labelKey: "panel.cmd.thread",
133058
+ action: { op: "run", arg: "thread" }
133059
+ },
133060
+ {
133061
+ id: "model",
133062
+ category: "system",
133063
+ line: "/model",
133064
+ kind: "model",
133065
+ labelKey: "panel.cmd.model",
133066
+ action: { op: "open", arg: "models" }
133067
+ },
133068
+ {
133069
+ id: "plan",
133070
+ category: "system",
133071
+ line: "/plan",
133072
+ kind: "plan",
133073
+ // 实际标签随当前状态在「开 / 关」间翻,见控制器;这里放关着时的那个。
133074
+ labelKey: "panel.cmd.planOff",
133075
+ action: { op: "run", arg: "plan" }
133076
+ },
133077
+ {
133078
+ id: "status",
133079
+ category: "system",
133080
+ line: "/status",
133081
+ kind: "status",
133082
+ labelKey: "panel.cmd.status",
133083
+ action: { op: "run", arg: "status" }
133084
+ },
133085
+ {
133086
+ id: "help",
133087
+ category: "system",
133088
+ // 这个按钮发的是文字版命令列表,不是又开一张面板。
133089
+ line: "/help",
133090
+ kind: "help",
133091
+ labelKey: "panel.cmd.help",
133092
+ action: { op: "run", arg: "help" }
133093
+ }
133094
+ ];
133095
+ var PANEL_RUN_LINES = Object.fromEntries(
133096
+ PANEL_COMMANDS.filter((spec) => spec.action.op === "run" && spec.id !== "help").map((spec) => [
133097
+ spec.id,
133098
+ spec.line
133099
+ ])
133100
+ );
133101
+
133102
+ // src/panelController.ts
133103
+ var PANEL_PAGE_SIZE = 8;
133104
+ var PANEL_CARD_LIMIT = 200;
133105
+ var LABEL_MAX_CHARS = 60;
133106
+ var PICK_KINDS = /* @__PURE__ */ new Set(["sessions", "teams", "models"]);
133107
+ var clip = (label) => {
133108
+ const chars = Array.from(label);
133109
+ return chars.length <= LABEL_MAX_CHARS ? label : `${chars.slice(0, LABEL_MAX_CHARS - 1).join("")}\u2026`;
133110
+ };
133111
+ var pageCount = (total) => Math.max(1, Math.ceil(total / PANEL_PAGE_SIZE));
133112
+ var makePanelController = (deps) => {
133113
+ const { t } = deps;
133114
+ const cards = /* @__PURE__ */ new Map();
133115
+ const tails = /* @__PURE__ */ new Map();
133116
+ const post = (cardId, job) => {
133117
+ const run = (tails.get(cardId) ?? Promise.resolve()).then(job).catch((error) => deps.warn("panel could not update a card", error)).then(() => {
133118
+ if (tails.get(cardId) === run) tails.delete(cardId);
133119
+ });
133120
+ tails.set(cardId, run);
133121
+ return run;
133122
+ };
133123
+ const remember = (entry) => {
133124
+ cards.delete(entry.cardId);
133125
+ cards.set(entry.cardId, entry);
133126
+ if (cards.size > PANEL_CARD_LIMIT) {
133127
+ const oldest = cards.keys().next().value;
133128
+ if (oldest !== void 0) cards.delete(oldest);
133129
+ }
133130
+ };
133131
+ const button = (label, op, arg, danger) => ({
133132
+ label: clip(label),
133133
+ action: { op, arg },
133134
+ ...danger === true ? { danger } : {}
133135
+ });
133136
+ const busyCard = (title, text2) => ({
133137
+ title,
133138
+ tone: "busy",
133139
+ text: text2,
133140
+ groups: [],
133141
+ nav: []
133142
+ });
133143
+ const viewTitle = (kind) => t(`panel.${kind}.title`);
133144
+ const menuCard = (chatKey) => ({
133145
+ title: t("panel.title"),
133146
+ tone: "menu",
133147
+ text: t("panel.hint"),
133148
+ groups: PANEL_CATEGORIES.map(
133149
+ (category) => ({
133150
+ heading: t(`panel.category.${category}`),
133151
+ buttons: PANEL_COMMANDS.filter((spec) => spec.category === category).map(
133152
+ (spec) => button(
133153
+ // 计划模式的按钮把当前状态写在标签上,点了之后标签跟着翻。
133154
+ spec.id === "plan" ? t(deps.planOn(chatKey) ? "panel.cmd.planOn" : "panel.cmd.planOff") : t(spec.labelKey),
133155
+ spec.action.op,
133156
+ spec.action.arg,
133157
+ spec.id === "stop"
133158
+ )
133159
+ )
133160
+ })
133161
+ ),
133162
+ nav: []
133163
+ });
133164
+ const pickerCard = (input) => {
133165
+ const total = pageCount(input.items.length);
133166
+ const page = Math.min(Math.max(input.page, 0), total - 1);
133167
+ const shown = input.items.slice(page * PANEL_PAGE_SIZE, (page + 1) * PANEL_PAGE_SIZE);
133168
+ const nav = [];
133169
+ if (input.depth > 1) nav.push(button(t("panel.back"), "back"));
133170
+ if (page > 0) nav.push(button(t("panel.prev"), "page", String(page - 1)));
133171
+ if (page < total - 1) nav.push(button(t("panel.next"), "page", String(page + 1)));
133172
+ const pageNote = total > 1 ? t("panel.page", { page: page + 1, total }) : "";
133173
+ return {
133174
+ title: viewTitle(input.kind),
133175
+ tone: "menu",
133176
+ text: [input.text, pageNote].filter((part) => part !== "").join("\n"),
133177
+ groups: shown.map((item) => ({ heading: null, buttons: [item] })),
133178
+ nav
133179
+ };
133180
+ };
133181
+ const render = (entry) => {
133182
+ const view = entry.stack.at(-1) ?? { kind: "menu" };
133183
+ const depth = entry.stack.length;
133184
+ switch (view.kind) {
133185
+ case "menu":
133186
+ return menuCard(entry.chatKey);
133187
+ case "notice":
133188
+ return {
133189
+ title: view.title,
133190
+ tone: "notice",
133191
+ text: view.text,
133192
+ groups: [],
133193
+ nav: depth > 1 ? [button(t("panel.back"), "back")] : []
133194
+ };
133195
+ case "sessions": {
133196
+ const now = deps.now();
133197
+ return pickerCard({
133198
+ kind: "sessions",
133199
+ page: view.page,
133200
+ depth,
133201
+ text: view.rows.length === 0 ? t("sessions.empty") : t("panel.sessions.hint"),
133202
+ items: view.rows.map(
133203
+ (row, index) => button(sessionLine(row, index + 1, now, t), "pick", `sessions:${row.threadId}`)
133204
+ )
133205
+ });
133206
+ }
133207
+ case "teams":
133208
+ return pickerCard({
133209
+ kind: "teams",
133210
+ page: view.page,
133211
+ depth,
133212
+ text: view.teams === null ? t("panel.teams.unavailable") : view.teams.length === 0 ? t("command.noTeams") : t("panel.teams.hint"),
133213
+ items: (view.teams ?? []).map(
133214
+ (team) => button(team.title, "pick", `teams:${team.teamId}`)
133215
+ )
133216
+ });
133217
+ case "models": {
133218
+ const { models } = view;
133219
+ const current = models?.current ?? null;
133220
+ const isCurrent = (choice) => current !== null && choice.instanceId === current.instanceId && choice.model === current.model;
133221
+ return pickerCard({
133222
+ kind: "models",
133223
+ page: view.page,
133224
+ depth,
133225
+ text: models === null || models.choices.length === 0 ? t("model.unavailable") : current === null ? t("model.currentUnknown") : t("model.current", {
133226
+ model: modelLabel(
133227
+ models.choices.find(isCurrent) ?? {
133228
+ instanceName: current.instanceId,
133229
+ modelName: current.model
133230
+ }
133231
+ )
133232
+ }),
133233
+ items: (models?.choices ?? []).map(
133234
+ (choice) => button(
133235
+ `${isCurrent(choice) ? "\u2605 " : ""}${modelLabel(choice)}`,
133236
+ "pick",
133237
+ `models:${choice.instanceId}/${choice.model}`
133238
+ )
133239
+ )
133240
+ });
133241
+ }
133242
+ }
133243
+ };
133244
+ const show = (entry) => post(entry.cardId, async () => {
133245
+ const surface = deps.surface(entry.chatKey);
133246
+ if (surface === null) return;
133247
+ const card = render(entry);
133248
+ try {
133249
+ await surface.update(entry.cardId, card);
133250
+ } catch (error) {
133251
+ deps.warn("panel could not update a card, posting a new one", error);
133252
+ const replacement = await surface.open(card);
133253
+ if (replacement !== null) {
133254
+ cards.delete(entry.cardId);
133255
+ entry.cardId = replacement;
133256
+ remember(entry);
133257
+ }
133258
+ }
133259
+ });
133260
+ const placeholder = (entry, title, text2) => {
133261
+ void post(entry.cardId, async () => {
133262
+ await deps.surface(entry.chatKey)?.update(entry.cardId, busyCard(title, text2));
133263
+ });
133264
+ };
133265
+ const loadView = async (chatKey, kind) => {
133266
+ switch (kind) {
133267
+ case "sessions":
133268
+ return { kind, page: 0, rows: await deps.loadSessions(chatKey) };
133269
+ case "teams":
133270
+ return { kind, page: 0, teams: await deps.loadTeams(chatKey) };
133271
+ case "models":
133272
+ return { kind, page: 0, models: await deps.loadModels(chatKey) };
133273
+ }
133274
+ };
133275
+ const backToMenu = (entry) => {
133276
+ entry.stack = [{ kind: "menu" }];
133277
+ return show(entry);
133278
+ };
133279
+ const guardBusy = (entry, work) => {
133280
+ entry.busy = true;
133281
+ return work().finally(() => {
133282
+ entry.busy = false;
133283
+ });
133284
+ };
133285
+ const operate = (entry, job) => {
133286
+ entry.busy = true;
133287
+ deps.defer(() => {
133288
+ placeholder(entry, t("panel.title"), t("panel.working"));
133289
+ deps.enqueue(
133290
+ entry.chatKey,
133291
+ () => guardBusy(entry, async () => {
133292
+ try {
133293
+ await job();
133294
+ } catch (error) {
133295
+ deps.warn("panel could not run a command", error);
133296
+ }
133297
+ await backToMenu(entry);
133298
+ })
133299
+ );
133300
+ });
133301
+ };
133302
+ const enter = (entry, kind) => {
133303
+ entry.busy = true;
133304
+ deps.defer(() => {
133305
+ placeholder(entry, viewTitle(kind), t("panel.loading"));
133306
+ deps.enqueue(
133307
+ entry.chatKey,
133308
+ () => guardBusy(entry, async () => {
133309
+ try {
133310
+ entry.stack.push(await loadView(entry.chatKey, kind));
133311
+ } catch (error) {
133312
+ deps.warn("panel could not load a view", error);
133313
+ entry.stack.push({
133314
+ kind: "notice",
133315
+ title: viewTitle(kind),
133316
+ text: t("panel.loadFailed")
133317
+ });
133318
+ }
133319
+ await show(entry);
133320
+ })
133321
+ );
133322
+ });
133323
+ };
133324
+ const navigate = (entry, change) => {
133325
+ change();
133326
+ deps.defer(() => void show(entry));
133327
+ };
133328
+ return {
133329
+ /** 开一张新面板卡(`/help`)。发不出就抛,调用方退回文字版。 */
133330
+ open: async (chatKey) => {
133331
+ const surface = deps.surface(chatKey);
133332
+ if (surface === null) throw new Error("this chat has no panel");
133333
+ const entry = { cardId: "", chatKey, stack: [{ kind: "menu" }], busy: false };
133334
+ const cardId = await surface.open(menuCard(chatKey));
133335
+ if (cardId === null) throw new Error("the panel card was sent without an id");
133336
+ entry.cardId = cardId;
133337
+ remember(entry);
133338
+ },
133339
+ /**
133340
+ * 处理一次点击,同步回给点击者的一句提示(null 就是没话说)。取数据、跑命令、改卡都
133341
+ * 在回调应答之后做。
133342
+ */
133343
+ click: (click) => {
133344
+ const entry = cards.get(click.cardId);
133345
+ if (entry === void 0 || entry.chatKey !== click.chatKey) return t("panel.expired");
133346
+ if (entry.busy) return t("panel.busy");
133347
+ const top = entry.stack.at(-1) ?? { kind: "menu" };
133348
+ const arg = click.arg ?? "";
133349
+ switch (click.op) {
133350
+ case "back":
133351
+ if (entry.stack.length > 1) navigate(entry, () => void entry.stack.pop());
133352
+ return null;
133353
+ case "page": {
133354
+ const page = Number(arg);
133355
+ if ((top.kind === "sessions" || top.kind === "teams" || top.kind === "models") && Number.isInteger(page) && page >= 0) {
133356
+ navigate(entry, () => {
133357
+ entry.stack[entry.stack.length - 1] = { ...top, page };
133358
+ });
133359
+ }
133360
+ return null;
133361
+ }
133362
+ case "open":
133363
+ if (top.kind === "menu" && PICK_KINDS.has(arg)) enter(entry, arg);
133364
+ return null;
133365
+ case "run": {
133366
+ if (top.kind !== "menu") return null;
133367
+ if (arg === "help") {
133368
+ operate(entry, () => deps.sendHelpText(entry.chatKey));
133369
+ return null;
133370
+ }
133371
+ const line = PANEL_RUN_LINES[arg];
133372
+ if (line !== void 0) operate(entry, () => deps.runLine(entry.chatKey, line));
133373
+ return null;
133374
+ }
133375
+ case "pick": {
133376
+ const split = arg.indexOf(":");
133377
+ const key = split < 0 ? "" : arg.slice(split + 1);
133378
+ const line = top.kind === "sessions" && arg.startsWith("sessions:") ? top.rows.find((row) => row.threadId === key) === void 0 ? null : `/resume ${key}` : top.kind === "teams" && arg.startsWith("teams:") ? top.teams?.find((team) => team.teamId === key) === void 0 ? null : `/team ${key}` : top.kind === "models" && arg.startsWith("models:") ? top.models?.choices.find(
133379
+ (choice) => `${choice.instanceId}/${choice.model}` === key
133380
+ ) === void 0 ? null : `/model ${key}` : null;
133381
+ if (line === null) {
133382
+ deps.warn("panel ignored a pick that is not in the current view");
133383
+ return null;
133384
+ }
133385
+ operate(entry, () => deps.runLine(entry.chatKey, line));
133386
+ return null;
133387
+ }
133388
+ default:
133389
+ deps.warn(`panel ignored an unknown action: ${click.op}`);
133390
+ return null;
133391
+ }
133392
+ }
133393
+ };
133394
+ };
133395
+
132848
133396
  // src/telegramApi.ts
132849
133397
  var API_BASE = "https://api.telegram.org";
132850
133398
  var POLL_TIMEOUT_SECONDS = 45;
132851
133399
  var TELEGRAM_TEXT_MAX_CHARS = 4e3;
132852
133400
  var TelegramApiError = class extends Error {
132853
133401
  code;
132854
- constructor(method, code2, description) {
133402
+ /** 限流(429)时平台给的等待秒数;别的错误没有。 */
133403
+ retryAfterSeconds;
133404
+ constructor(method, code2, description, retryAfterSeconds) {
132855
133405
  super(`telegram ${method} failed (${String(code2 ?? "?")}): ${description}`);
132856
133406
  this.name = "TelegramApiError";
132857
133407
  this.code = code2;
133408
+ this.retryAfterSeconds = retryAfterSeconds;
132858
133409
  }
132859
133410
  };
133411
+ var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
132860
133412
  var PHOTO_NAME = "photo.jpg";
132861
133413
  var PHOTO_MIME = "image/jpeg";
132862
133414
  var mimeFromName2 = (name) => {
@@ -132915,10 +133467,20 @@ var makeTelegramApi = (input) => {
132915
133467
  });
132916
133468
  const envelope = await response.json();
132917
133469
  if (envelope.ok !== true) {
132918
- 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
+ );
132919
133476
  }
132920
133477
  return envelope.result;
132921
133478
  };
133479
+ const keyboard = (rows) => ({
133480
+ inline_keyboard: rows.map(
133481
+ (row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
133482
+ )
133483
+ });
132922
133484
  return {
132923
133485
  /** 机器人自己是谁;群里的命令靠它的 `username` 认出是不是发给自己的。 */
132924
133486
  getMe: () => call("getMe", {}),
@@ -132938,16 +133500,45 @@ var makeTelegramApi = (input) => {
132938
133500
  await call("sendMessage", { chat_id: chatId, text: chunk });
132939
133501
  }
132940
133502
  },
132941
- /** 带按钮的消息。一行一个按钮,审批选项通常不多。 */
132942
- sendButtons: (chatId, text2, buttons) => call("sendMessage", {
132943
- chat_id: chatId,
132944
- text: text2,
132945
- reply_markup: {
132946
- inline_keyboard: buttons.map((button) => [
132947
- { text: button.label, callback_data: button.token }
132948
- ])
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;
132949
133529
  }
132950
- }),
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
+ },
132951
133542
  /** 答完点击,不然客户端上那个按钮一直转圈。带 `text` 时点击者会看到一句提示。 */
132952
133543
  answerCallback: (callbackQueryId, text2) => call("answerCallbackQuery", {
132953
133544
  callback_query_id: callbackQueryId,
@@ -132988,16 +133579,168 @@ var makeTelegramApi = (input) => {
132988
133579
  };
132989
133580
  };
132990
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
+
132991
133685
  // src/telegramAdapter.ts
132992
133686
  var COLUMNS2 = [
132993
133687
  { key: "telegramToken", kind: "password", labelKey: "column.telegramToken.label" }
132994
133688
  ];
132995
133689
  var BACKOFF_START_MS = 1e3;
132996
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;
132997
133694
  var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
132998
133695
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
132999
133696
  var makeTelegramAdapter = (deps) => {
133000
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
+ };
133001
133744
  const apiFor = async (binding) => {
133002
133745
  const existing = apis.get(binding.rowId);
133003
133746
  if (existing !== void 0) return existing;
@@ -133006,6 +133749,15 @@ var makeTelegramAdapter = (deps) => {
133006
133749
  apis.set(binding.rowId, api);
133007
133750
  return api;
133008
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
+ });
133009
133761
  return {
133010
133762
  platform: "telegram",
133011
133763
  icon: "telegram",
@@ -133033,10 +133785,8 @@ var makeTelegramAdapter = (deps) => {
133033
133785
  for (const update of updates) {
133034
133786
  const callback = update.callback_query;
133035
133787
  if (callback?.data !== void 0) {
133036
- const notice = handlers.onApproval(
133037
- callback.data,
133038
- callback.from === void 0 ? null : String(callback.from.id)
133039
- );
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);
133040
133790
  await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
133041
133791
  continue;
133042
133792
  }
@@ -133089,8 +133839,72 @@ var makeTelegramAdapter = (deps) => {
133089
133839
  },
133090
133840
  askApproval: async (binding, request) => {
133091
133841
  const api = await apiFor(binding);
133092
- await api.sendButtons(binding.chatId, request.body, request.options);
133093
- 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
+ }
133094
133908
  },
133095
133909
  fetchResource: async (binding, input) => {
133096
133910
  const api = await apiFor(binding);
@@ -133360,6 +134174,7 @@ var runWecomClient = (input) => {
133360
134174
  const nextRequestId = (kind) => `${kind}_${++sequence}`;
133361
134175
  const pendingAcks = /* @__PURE__ */ new Map();
133362
134176
  const pendingRequests = /* @__PURE__ */ new Map();
134177
+ const openStreams = /* @__PURE__ */ new Map();
133363
134178
  const subscribeId = nextRequestId("aibot_subscribe");
133364
134179
  let missedPong = 0;
133365
134180
  let closed = false;
@@ -133389,6 +134204,8 @@ var runWecomClient = (input) => {
133389
134204
  return;
133390
134205
  }
133391
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;
133392
134209
  settleAck(requestId, error);
133393
134210
  return;
133394
134211
  }
@@ -133400,6 +134217,7 @@ var runWecomClient = (input) => {
133400
134217
  if (finished) return;
133401
134218
  finished = true;
133402
134219
  clearInterval(heartbeat);
134220
+ openStreams.clear();
133403
134221
  void input.socket.close().catch(() => void 0);
133404
134222
  for (const requestId of pendingAcks.keys()) {
133405
134223
  settleAck(requestId, new Error("WeCom connection closed"));
@@ -133487,8 +134305,38 @@ var runWecomClient = (input) => {
133487
134305
  }
133488
134306
  }
133489
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
+ };
133490
134337
  return {
133491
134338
  sendText,
134339
+ openStream,
133492
134340
  rememberRequest: (chatId, requestId) => {
133493
134341
  if (requestId !== null) pendingRequests.set(chatId, requestId);
133494
134342
  },
@@ -133503,6 +134351,8 @@ var runWecomClient = (input) => {
133503
134351
  };
133504
134352
 
133505
134353
  // src/wecomAdapter.ts
134354
+ var WECOM_CARD_THROTTLE_MS = 400;
134355
+ var LIVE_STREAM_LIMIT = 200;
133506
134356
  var BACKOFF_START_MS2 = 1e3;
133507
134357
  var BACKOFF_MAX_MS2 = 6e4;
133508
134358
  var COLUMNS3 = [
@@ -133523,6 +134373,36 @@ var optionIndexFrom = (text2, count) => {
133523
134373
  };
133524
134374
  var makeWecomAdapter = (deps) => {
133525
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
+ };
133526
134406
  return {
133527
134407
  platform: "wecom",
133528
134408
  icon: "wecom",
@@ -133621,6 +134501,25 @@ var makeWecomAdapter = (deps) => {
133621
134501
  await (clients.get(binding.rowId)?.sendText(binding.chatId, lines.join("\n")) ?? Promise.resolve());
133622
134502
  return null;
133623
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
+ },
133624
134523
  answerByText: (_binding, text2, options) => {
133625
134524
  const index = optionIndexFrom(text2, options.length);
133626
134525
  return index === null ? null : options[index - 1]?.token ?? null;
@@ -133674,6 +134573,7 @@ var imPlugin = definePlugin({
133674
134573
  // Telegram 在不少网络里要绕代理,而绕不绕、绕哪个是按行配的 —— 这是插件面上
133675
134574
  // `ctx.proxies` 的第一个真实消费者。
133676
134575
  fetchVia: (proxy) => ctx.proxies.fetch(proxy),
134576
+ translate: t,
133677
134577
  log: {
133678
134578
  info: (message, fields) => ctx.log.info(message, fields),
133679
134579
  warn: (message, fields) => ctx.log.warn(message, fields)
@@ -133682,6 +134582,7 @@ var imPlugin = definePlugin({
133682
134582
  makeWecomAdapter({
133683
134583
  // 企微不需要厂商 SDK:全程 JSON over WebSocket,走插件面那条出网长连接。
133684
134584
  connectSocket: (url) => ctx.socket.connect({ url }),
134585
+ translate: t,
133685
134586
  log: {
133686
134587
  info: (message, fields) => ctx.log.info(message, fields),
133687
134588
  warn: (message, fields) => ctx.log.warn(message, fields)
@@ -134405,9 +135306,10 @@ ${final.text}`;
134405
135306
  commit("chat-updated", { chatKey, plan: next });
134406
135307
  await deliver(binding, t(next ? "plan.on" : "plan.off"));
134407
135308
  };
134408
- const handleCommand = async (binding, message, teams) => {
135309
+ const hasPanel = (binding) => adapterFor(binding.platform)?.panel !== void 0;
135310
+ const handleCommand = async (binding, text2, teams) => {
134409
135311
  const command = parseChatCommand({
134410
- text: message.text,
135312
+ text: text2,
134411
135313
  teams,
134412
135314
  // 中文别名只在回话语言是中文时认,免得英文用户打一个「停」就被当成命令。
134413
135315
  chinese: t.language === "zh-CN"
@@ -134451,7 +135353,20 @@ ${final.text}`;
134451
135353
  await deliver(binding, t("command.noTeams"));
134452
135354
  return true;
134453
135355
  case "help":
134454
- await deliver(binding, helpText(t));
135356
+ await deliver(binding, helpText(t, { panel: hasPanel(binding) }));
135357
+ return true;
135358
+ case "panel":
135359
+ if (hasPanel(binding)) {
135360
+ try {
135361
+ await panel.open(chatKey);
135362
+ return true;
135363
+ } catch (error) {
135364
+ ctx.log.warn("chat could not open the command panel", {
135365
+ error: describeError(error)
135366
+ });
135367
+ }
135368
+ }
135369
+ await deliver(binding, helpText(t, { panel: false }));
134455
135370
  return true;
134456
135371
  case "sessions":
134457
135372
  await showSessions(binding);
@@ -134640,9 +135555,75 @@ ${final.text}`;
134640
135555
  });
134641
135556
  chatQueues.set(chatKey, run);
134642
135557
  };
135558
+ const panel = makePanelController({
135559
+ t,
135560
+ surface: (chatKey) => {
135561
+ const binding = bindingOfChat(chatKey);
135562
+ const surface = binding === void 0 ? void 0 : adapterFor(binding.platform)?.panel;
135563
+ if (binding === void 0 || surface === void 0) return null;
135564
+ return {
135565
+ open: (card) => surface.open(binding, card),
135566
+ update: (cardId, card) => surface.update(binding, cardId, card)
135567
+ };
135568
+ },
135569
+ planOn: (chatKey) => chatOf(state, chatKey).plan,
135570
+ loadSessions: async (chatKey) => {
135571
+ const binding = bindingOfChat(chatKey);
135572
+ if (binding === void 0) return [];
135573
+ const rows = await sessionRowsFor(binding);
135574
+ listedByChat.set(chatKey, listedThreadIds(rows));
135575
+ return rows;
135576
+ },
135577
+ loadTeams: async (chatKey) => {
135578
+ const binding = bindingOfChat(chatKey);
135579
+ return binding === void 0 ? null : fetchTeams(binding);
135580
+ },
135581
+ loadModels: async (chatKey) => {
135582
+ const binding = bindingOfChat(chatKey);
135583
+ const instances = await ctx.providers.listInstances().catch(() => null);
135584
+ if (binding === void 0 || instances === null) return null;
135585
+ return { current: await currentModelOf(binding), choices: modelChoices(instances) };
135586
+ },
135587
+ runLine: async (chatKey, line) => {
135588
+ const binding = bindingOfChat(chatKey);
135589
+ if (binding === void 0) return;
135590
+ await handleCommand(binding, line, await fetchTeams(binding) ?? []);
135591
+ },
135592
+ sendHelpText: async (chatKey) => {
135593
+ const binding = bindingOfChat(chatKey);
135594
+ if (binding !== void 0) await deliver(binding, helpText(t));
135595
+ },
135596
+ enqueue: enqueueForChat,
135597
+ // 改卡必须排在回调应答之后:飞书在应答不带改卡时会把卡恢复成点击前的样子。
135598
+ defer: (run) => {
135599
+ setTimeout(run, 0);
135600
+ },
135601
+ now: () => Date.now(),
135602
+ warn: (message, error) => {
135603
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError(error) });
135604
+ }
135605
+ });
135606
+ const handlePanelClick = (platform, click) => {
135607
+ const target = bindingForChat(platform, click.chatId);
135608
+ if (target === void 0 || target.projectId === "") return null;
135609
+ if (!isAllowedSender(target, click.operatorId)) {
135610
+ ctx.log.warn("chat ignored a panel click from someone not on the allow list", {
135611
+ group: target.displayName,
135612
+ operator: click.operatorId
135613
+ });
135614
+ return t("reply.notAllowed");
135615
+ }
135616
+ return panel.click({
135617
+ chatKey: chatKeyOf(target),
135618
+ cardId: click.cardId,
135619
+ op: click.op,
135620
+ arg: click.arg
135621
+ });
135622
+ };
134643
135623
  const inboundHandlersFor = (platform) => ({
134644
135624
  onError: (message) => ctx.log.warn("chat inbound connection failed", { platform, message }),
134645
135625
  onApproval: handleApprovalClick,
135626
+ onPanelClick: (click) => handlePanelClick(platform, click),
134646
135627
  onMessage: (message) => {
134647
135628
  const target = bindingForChat(platform, message.chatId);
134648
135629
  if (target === void 0 || target.projectId === "") return;
@@ -134668,7 +135649,7 @@ ${final.text}`;
134668
135649
  }
134669
135650
  enqueueForChat(chatKeyOf(target), async () => {
134670
135651
  const teams = await fetchTeams(target);
134671
- if (await handleCommand(target, message, teams ?? [])) return;
135652
+ if (await handleCommand(target, message.text, teams ?? [])) return;
134672
135653
  const project = await ctx.projections.getProject(target.projectId);
134673
135654
  if (project === null || project.archivedAt != null) {
134674
135655
  if (project === null) {