@ycodium-ai/plugin-im 0.2.2554 → 0.2.2570

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/README.md CHANGED
@@ -51,14 +51,22 @@ written as plugin facts (`ctx.events.append`) and folded back by a pure function
51
51
  archived or moved to another project is dropped on the next message.
52
52
 
53
53
  Chat commands: `/stop`, `/status`, `/new`, `/sessions`, `/resume`, `/model`,
54
- `/plan`, `/team`, `/thread`, `/help`. `/sessions` and `/resume` only list and
54
+ `/plan`, `/team`, `/thread`, `/help`, `/panel`. `/sessions` and `/resume` only list and
55
55
  switch threads this plugin opened, since the host lets a plugin send only into
56
56
  its own threads.
57
57
 
58
+ On Lark, `/panel` sends a command panel card (`/help` stays text and ends with a pointer to it; other platforms' `/help` does not mention it): every command above
59
+ is a button, and the ones that take an argument (a thread, a team, a model) open
60
+ a second view on the same card with a Back button. Each card keeps its own view
61
+ stack in memory (`src/panelController.ts`), so after a restart an old card only
62
+ answers that it has expired. A tap runs the same handler as the typed command and
63
+ waits in the same per-group queue as typed messages. Another platform opts in by
64
+ implementing the optional `panel` surface on its adapter (`src/adapter.ts`).
65
+
58
66
  The **Reply language** setting (Chinese by default, or English) picks the
59
- language of everything the bot says itself, and of the settings card. The copy
60
- lives in `locales/en.json` and `locales/zh-CN.json`; Chinese shortcut commands
61
- only work in Chinese.
67
+ language of everything the bot says itself. The settings card follows the
68
+ interface language instead. The copy lives in `locales/en.json` and
69
+ `locales/zh-CN.json`; Chinese shortcut commands only work in Chinese.
62
70
 
63
71
  ## Requirements
64
72
 
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));
@@ -132147,8 +132207,13 @@ var startLarkInbound = (input) => {
132147
132207
  // src/larkAdapter.ts
132148
132208
  var TOKEN_CACHE_MAX_AGE_MS = 30 * 6e4;
132149
132209
  var COLUMNS = [
132150
- { key: "larkAppId", kind: "text", label: "Lark app ID", placeholder: "cli_\u2026" },
132151
- { key: "larkAppSecret", kind: "password", label: "Lark app secret" }
132210
+ {
132211
+ key: "larkAppId",
132212
+ kind: "text",
132213
+ labelKey: "column.larkAppId.label",
132214
+ placeholderKey: "column.larkAppId.placeholder"
132215
+ },
132216
+ { key: "larkAppSecret", kind: "password", labelKey: "column.larkAppSecret.label" }
132152
132217
  ];
132153
132218
  var credentials = (binding) => ({
132154
132219
  appId: binding.values["larkAppId"] ?? "",
@@ -132180,6 +132245,13 @@ var makeLarkAdapter = (deps) => {
132180
132245
  appSecret,
132181
132246
  onError: handlers.onError,
132182
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,
132183
132255
  onMessage: (message) => {
132184
132256
  handlers.onMessage({
132185
132257
  messageId: message.messageId,
@@ -132258,6 +132330,22 @@ var makeLarkAdapter = (deps) => {
132258
132330
  });
132259
132331
  }
132260
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
+ },
132261
132349
  acknowledge: async (binding, messageId) => addReaction({
132262
132350
  http: deps.http,
132263
132351
  token: await tenantToken(binding),
@@ -132328,7 +132416,8 @@ var en_default = {
132328
132416
  label: "Platform"
132329
132417
  },
132330
132418
  larkAppId: {
132331
- label: "Lark app ID"
132419
+ label: "Lark app ID",
132420
+ placeholder: "cli_\u2026"
132332
132421
  },
132333
132422
  larkAppSecret: {
132334
132423
  label: "Lark app secret"
@@ -132419,7 +132508,8 @@ var en_default = {
132419
132508
  sessions: "\xB7 /sessions \u2014 list the sessions this bot opened for this group's project",
132420
132509
  resume: "\xB7 /resume number \u2014 switch to one of them (a number from /sessions, or the start of its id)",
132421
132510
  model: "\xB7 /model \u2014 see the model this group uses; /model number or /model instance/model switches it",
132422
- 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."
132423
132513
  },
132424
132514
  sessions: {
132425
132515
  title: "Sessions this bot opened for this group's project ({{count}}):",
@@ -132492,6 +132582,48 @@ var en_default = {
132492
132582
  chosen: "Chosen: {{label}}",
132493
132583
  elsewhere: "Already handled elsewhere (the web app or another client)."
132494
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
+ }
132495
132627
  }
132496
132628
  };
132497
132629
 
@@ -132534,7 +132666,8 @@ var zh_CN_default = {
132534
132666
  label: "\u5E73\u53F0"
132535
132667
  },
132536
132668
  larkAppId: {
132537
- label: "\u98DE\u4E66\u5E94\u7528 ID"
132669
+ label: "\u98DE\u4E66\u5E94\u7528 ID",
132670
+ placeholder: "cli_\u2026"
132538
132671
  },
132539
132672
  larkAppSecret: {
132540
132673
  label: "\u98DE\u4E66\u5E94\u7528\u5BC6\u94A5"
@@ -132625,7 +132758,8 @@ var zh_CN_default = {
132625
132758
  sessions: "\xB7 /sessions \u2014\u2014 \u5217\u51FA\u672C\u673A\u5668\u4EBA\u4E3A\u8FD9\u4E2A\u7FA4\u7684\u9879\u76EE\u5F00\u8FC7\u7684\u4F1A\u8BDD",
132626
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",
132627
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",
132628
- 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"
132629
132763
  },
132630
132764
  sessions: {
132631
132765
  title: "\u672C\u673A\u5668\u4EBA\u4E3A\u8FD9\u4E2A\u7FA4\u7684\u9879\u76EE\u5F00\u8FC7\u7684\u4F1A\u8BDD\uFF08{{count}} \u6761\uFF09\uFF1A",
@@ -132698,6 +132832,48 @@ var zh_CN_default = {
132698
132832
  chosen: "\u5DF2\u9009\uFF1A{{label}}",
132699
132833
  elsewhere: "\u5DF2\u5728\u522B\u5904\u5904\u7406\uFF08\u7F51\u9875\u6216\u5176\u4ED6\u5BA2\u6237\u7AEF\uFF09\u3002"
132700
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
+ }
132701
132877
  }
132702
132878
  };
132703
132879
 
@@ -132838,6 +133014,385 @@ var pickSession = (arg, listed, rows) => {
132838
133014
  return matches.length === 1 ? matches[0]?.threadId ?? null : null;
132839
133015
  };
132840
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
+
132841
133396
  // src/telegramApi.ts
132842
133397
  var API_BASE = "https://api.telegram.org";
132843
133398
  var POLL_TIMEOUT_SECONDS = 45;
@@ -132983,7 +133538,7 @@ var makeTelegramApi = (input) => {
132983
133538
 
132984
133539
  // src/telegramAdapter.ts
132985
133540
  var COLUMNS2 = [
132986
- { key: "telegramToken", kind: "password", label: "Telegram bot token" }
133541
+ { key: "telegramToken", kind: "password", labelKey: "column.telegramToken.label" }
132987
133542
  ];
132988
133543
  var BACKOFF_START_MS = 1e3;
132989
133544
  var BACKOFF_MAX_MS = 6e4;
@@ -133499,8 +134054,8 @@ var runWecomClient = (input) => {
133499
134054
  var BACKOFF_START_MS2 = 1e3;
133500
134055
  var BACKOFF_MAX_MS2 = 6e4;
133501
134056
  var COLUMNS3 = [
133502
- { key: "wecomBotId", kind: "text", label: "WeCom bot ID" },
133503
- { key: "wecomBotSecret", kind: "password", label: "WeCom bot secret" }
134057
+ { key: "wecomBotId", kind: "text", labelKey: "column.wecomBotId.label" },
134058
+ { key: "wecomBotSecret", kind: "password", labelKey: "column.wecomBotSecret.label" }
133504
134059
  ];
133505
134060
  var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133506
134061
  var credentials2 = (binding) => ({
@@ -133695,99 +134250,99 @@ var imPlugin = definePlugin({
133695
134250
  const platformColumn = {
133696
134251
  key: "platform",
133697
134252
  kind: "select",
133698
- label: t("column.platform.label"),
134253
+ labelKey: "column.platform.label",
133699
134254
  options: adapters.map((adapter) => ({
133700
134255
  value: adapter.platform,
133701
- // 平台名在目录里有本地叫法(飞书、企业微信);没有就用适配器自己的名字。
133702
- label: t.optional(`platform.${adapter.platform}`) ?? adapter.label,
134256
+ // 平台名在目录里有本地叫法(飞书、企业微信)。
134257
+ labelKey: `platform.${adapter.platform}`,
133703
134258
  icon: adapter.icon
133704
134259
  }))
133705
134260
  };
133706
134261
  const credentialColumns = adapters.flatMap(
133707
134262
  (adapter) => adapter.columns.map((column) => ({
133708
134263
  ...column,
133709
- // 凭据列的字也走目录;适配器自己写的英文只是目录里没有这个键时的回落。
133710
- label: t.optional(`column.${column.key}.label`) ?? column.label,
133711
134264
  showWhen: { key: platformColumn.key, equals: [adapter.platform] }
133712
134265
  }))
133713
134266
  );
133714
134267
  ctx.settingsCard({
133715
- title: t("settings.title"),
133716
- description: t("settings.description"),
134268
+ titleKey: "settings.title",
134269
+ descriptionKey: "settings.description",
133717
134270
  fields: [
133718
134271
  {
133719
134272
  key: REPLY_LANGUAGE_FIELD,
133720
134273
  kind: "select",
133721
- label: t("settings.language.label"),
133722
- description: t("settings.language.description"),
134274
+ labelKey: "settings.language.label",
134275
+ descriptionKey: "settings.language.description",
133723
134276
  options: [
133724
- { value: "zh-CN", label: t("settings.language.zhCN") },
133725
- { value: "en", label: t("settings.language.en") }
134277
+ { value: "zh-CN", labelKey: "settings.language.zhCN" },
134278
+ { value: "en", labelKey: "settings.language.en" }
133726
134279
  ]
133727
134280
  },
133728
134281
  {
133729
134282
  key: GROUPS_FIELD,
133730
134283
  kind: "table",
133731
- label: t("settings.groups"),
134284
+ labelKey: "settings.groups",
133732
134285
  columns: [
133733
134286
  {
133734
134287
  key: "displayName",
133735
134288
  kind: "text",
133736
- label: t("column.displayName.label"),
133737
- placeholder: t("column.displayName.placeholder")
134289
+ labelKey: "column.displayName.label",
134290
+ placeholderKey: "column.displayName.placeholder"
133738
134291
  },
133739
134292
  platformColumn,
133740
134293
  ...credentialColumns,
133741
134294
  {
133742
134295
  key: "chatId",
133743
134296
  kind: "text",
133744
- label: t("column.chatId.label"),
133745
- description: t("column.chatId.description")
134297
+ labelKey: "column.chatId.label",
134298
+ descriptionKey: "column.chatId.description"
133746
134299
  },
133747
134300
  {
133748
134301
  key: "allowedSenders",
133749
134302
  kind: "text",
133750
- label: t("column.allowedSenders.label"),
133751
- description: t("column.allowedSenders.description")
134303
+ labelKey: "column.allowedSenders.label",
134304
+ descriptionKey: "column.allowedSenders.description"
133752
134305
  },
133753
134306
  {
133754
134307
  // 选项由宿主填,连三态一起——插件只声明「这个格子装的是代理」,宿主因此
133755
134308
  // 也就认得出这一行引用了哪个代理,删代理时拦得住(路线图 #64)。
133756
134309
  key: "proxy",
133757
134310
  kind: "proxy",
133758
- label: t("column.proxy.label"),
133759
- description: t("column.proxy.description")
134311
+ labelKey: "column.proxy.label",
134312
+ descriptionKey: "column.proxy.description"
133760
134313
  },
133761
134314
  {
133762
134315
  key: "projectId",
133763
134316
  kind: "select",
133764
- label: t("column.projectId.label"),
133765
- description: t("column.projectId.description"),
134317
+ labelKey: "column.projectId.label",
134318
+ descriptionKey: "column.projectId.description",
133766
134319
  options: [
133767
- { value: "", label: t("settings.reportOnly") },
134320
+ { value: "", labelKey: "settings.reportOnly" },
134321
+ // 项目名是数据,只能写成字面量;已归档的那段后缀宿主翻不了(选项没有带参数的
134322
+ // 键),所以仍按回话语言取。
133768
134323
  ...imProjectOptions(projects, boundProjectIds, t("settings.archivedSuffix"))
133769
134324
  ]
133770
134325
  },
133771
134326
  {
133772
134327
  key: "runtimeMode",
133773
134328
  kind: "select",
133774
- label: t("column.runtimeMode.label"),
133775
- description: t("column.runtimeMode.description"),
134329
+ labelKey: "column.runtimeMode.label",
134330
+ descriptionKey: "column.runtimeMode.description",
133776
134331
  options: [
133777
- { value: "approval-required", label: t("settings.runtimeMode.approvalRequired") },
133778
- { value: "auto-accept-edits", label: t("settings.runtimeMode.autoAcceptEdits") },
133779
- { value: "auto", label: t("settings.runtimeMode.auto") },
133780
- { value: "full-access", label: t("settings.runtimeMode.fullAccess") }
134332
+ { value: "approval-required", labelKey: "settings.runtimeMode.approvalRequired" },
134333
+ { value: "auto-accept-edits", labelKey: "settings.runtimeMode.autoAcceptEdits" },
134334
+ { value: "auto", labelKey: "settings.runtimeMode.auto" },
134335
+ { value: "full-access", labelKey: "settings.runtimeMode.fullAccess" }
133781
134336
  ]
133782
134337
  },
133783
134338
  {
133784
134339
  key: "mode",
133785
134340
  kind: "select",
133786
- label: t("column.mode.label"),
133787
- description: t("column.mode.description"),
134341
+ labelKey: "column.mode.label",
134342
+ descriptionKey: "column.mode.description",
133788
134343
  options: [
133789
- { value: "turn", label: t("settings.notifyMode.turn") },
133790
- { value: "message", label: t("settings.notifyMode.message") }
134344
+ { value: "turn", labelKey: "settings.notifyMode.turn" },
134345
+ { value: "message", labelKey: "settings.notifyMode.message" }
133791
134346
  ]
133792
134347
  }
133793
134348
  ]
@@ -134398,9 +134953,10 @@ ${final.text}`;
134398
134953
  commit("chat-updated", { chatKey, plan: next });
134399
134954
  await deliver(binding, t(next ? "plan.on" : "plan.off"));
134400
134955
  };
134401
- const handleCommand = async (binding, message, teams) => {
134956
+ const hasPanel = (binding) => adapterFor(binding.platform)?.panel !== void 0;
134957
+ const handleCommand = async (binding, text2, teams) => {
134402
134958
  const command = parseChatCommand({
134403
- text: message.text,
134959
+ text: text2,
134404
134960
  teams,
134405
134961
  // 中文别名只在回话语言是中文时认,免得英文用户打一个「停」就被当成命令。
134406
134962
  chinese: t.language === "zh-CN"
@@ -134444,7 +135000,20 @@ ${final.text}`;
134444
135000
  await deliver(binding, t("command.noTeams"));
134445
135001
  return true;
134446
135002
  case "help":
134447
- await deliver(binding, helpText(t));
135003
+ await deliver(binding, helpText(t, { panel: hasPanel(binding) }));
135004
+ return true;
135005
+ case "panel":
135006
+ if (hasPanel(binding)) {
135007
+ try {
135008
+ await panel.open(chatKey);
135009
+ return true;
135010
+ } catch (error) {
135011
+ ctx.log.warn("chat could not open the command panel", {
135012
+ error: describeError(error)
135013
+ });
135014
+ }
135015
+ }
135016
+ await deliver(binding, helpText(t, { panel: false }));
134448
135017
  return true;
134449
135018
  case "sessions":
134450
135019
  await showSessions(binding);
@@ -134633,9 +135202,75 @@ ${final.text}`;
134633
135202
  });
134634
135203
  chatQueues.set(chatKey, run);
134635
135204
  };
135205
+ const panel = makePanelController({
135206
+ t,
135207
+ surface: (chatKey) => {
135208
+ const binding = bindingOfChat(chatKey);
135209
+ const surface = binding === void 0 ? void 0 : adapterFor(binding.platform)?.panel;
135210
+ if (binding === void 0 || surface === void 0) return null;
135211
+ return {
135212
+ open: (card) => surface.open(binding, card),
135213
+ update: (cardId, card) => surface.update(binding, cardId, card)
135214
+ };
135215
+ },
135216
+ planOn: (chatKey) => chatOf(state, chatKey).plan,
135217
+ loadSessions: async (chatKey) => {
135218
+ const binding = bindingOfChat(chatKey);
135219
+ if (binding === void 0) return [];
135220
+ const rows = await sessionRowsFor(binding);
135221
+ listedByChat.set(chatKey, listedThreadIds(rows));
135222
+ return rows;
135223
+ },
135224
+ loadTeams: async (chatKey) => {
135225
+ const binding = bindingOfChat(chatKey);
135226
+ return binding === void 0 ? null : fetchTeams(binding);
135227
+ },
135228
+ loadModels: async (chatKey) => {
135229
+ const binding = bindingOfChat(chatKey);
135230
+ const instances = await ctx.providers.listInstances().catch(() => null);
135231
+ if (binding === void 0 || instances === null) return null;
135232
+ return { current: await currentModelOf(binding), choices: modelChoices(instances) };
135233
+ },
135234
+ runLine: async (chatKey, line) => {
135235
+ const binding = bindingOfChat(chatKey);
135236
+ if (binding === void 0) return;
135237
+ await handleCommand(binding, line, await fetchTeams(binding) ?? []);
135238
+ },
135239
+ sendHelpText: async (chatKey) => {
135240
+ const binding = bindingOfChat(chatKey);
135241
+ if (binding !== void 0) await deliver(binding, helpText(t));
135242
+ },
135243
+ enqueue: enqueueForChat,
135244
+ // 改卡必须排在回调应答之后:飞书在应答不带改卡时会把卡恢复成点击前的样子。
135245
+ defer: (run) => {
135246
+ setTimeout(run, 0);
135247
+ },
135248
+ now: () => Date.now(),
135249
+ warn: (message, error) => {
135250
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError(error) });
135251
+ }
135252
+ });
135253
+ const handlePanelClick = (platform, click) => {
135254
+ const target = bindingForChat(platform, click.chatId);
135255
+ if (target === void 0 || target.projectId === "") return null;
135256
+ if (!isAllowedSender(target, click.operatorId)) {
135257
+ ctx.log.warn("chat ignored a panel click from someone not on the allow list", {
135258
+ group: target.displayName,
135259
+ operator: click.operatorId
135260
+ });
135261
+ return t("reply.notAllowed");
135262
+ }
135263
+ return panel.click({
135264
+ chatKey: chatKeyOf(target),
135265
+ cardId: click.cardId,
135266
+ op: click.op,
135267
+ arg: click.arg
135268
+ });
135269
+ };
134636
135270
  const inboundHandlersFor = (platform) => ({
134637
135271
  onError: (message) => ctx.log.warn("chat inbound connection failed", { platform, message }),
134638
135272
  onApproval: handleApprovalClick,
135273
+ onPanelClick: (click) => handlePanelClick(platform, click),
134639
135274
  onMessage: (message) => {
134640
135275
  const target = bindingForChat(platform, message.chatId);
134641
135276
  if (target === void 0 || target.projectId === "") return;
@@ -134661,7 +135296,7 @@ ${final.text}`;
134661
135296
  }
134662
135297
  enqueueForChat(chatKeyOf(target), async () => {
134663
135298
  const teams = await fetchTeams(target);
134664
- if (await handleCommand(target, message, teams ?? [])) return;
135299
+ if (await handleCommand(target, message.text, teams ?? [])) return;
134665
135300
  const project = await ctx.projections.getProject(target.projectId);
134666
135301
  if (project === null || project.archivedAt != null) {
134667
135302
  if (project === null) {
package/locales/en.json CHANGED
@@ -36,7 +36,8 @@
36
36
  "label": "Platform"
37
37
  },
38
38
  "larkAppId": {
39
- "label": "Lark app ID"
39
+ "label": "Lark app ID",
40
+ "placeholder": "cli_…"
40
41
  },
41
42
  "larkAppSecret": {
42
43
  "label": "Lark app secret"
@@ -127,7 +128,8 @@
127
128
  "sessions": "· /sessions — list the sessions this bot opened for this group's project",
128
129
  "resume": "· /resume number — switch to one of them (a number from /sessions, or the start of its id)",
129
130
  "model": "· /model — see the model this group uses; /model number or /model instance/model switches it",
130
- "plan": "· /plan — toggle plan mode (the agent proposes a plan instead of acting); /plan on, /plan off set it"
131
+ "plan": "· /plan — toggle plan mode (the agent proposes a plan instead of acting); /plan on, /plan off set it",
132
+ "panel": "Send /panel to open the command panel with a button for every command."
131
133
  },
132
134
  "sessions": {
133
135
  "title": "Sessions this bot opened for this group's project ({{count}}):",
@@ -200,5 +202,47 @@
200
202
  "chosen": "Chosen: {{label}}",
201
203
  "elsewhere": "Already handled elsewhere (the web app or another client)."
202
204
  }
205
+ },
206
+ "panel": {
207
+ "title": "Command panel",
208
+ "hint": "A button does the same as typing its command. You can still type.",
209
+ "category": {
210
+ "session": "Session",
211
+ "target": "Send to",
212
+ "system": "Settings and status"
213
+ },
214
+ "cmd": {
215
+ "new": "New session",
216
+ "stop": "Stop this turn",
217
+ "sessions": "Sessions",
218
+ "team": "Send to a team",
219
+ "thread": "Normal session",
220
+ "model": "Model",
221
+ "planOn": "Plan mode: on",
222
+ "planOff": "Plan mode: off",
223
+ "status": "Status",
224
+ "help": "Text help"
225
+ },
226
+ "back": "Back",
227
+ "prev": "Previous",
228
+ "next": "Next",
229
+ "page": "Page {{page}} of {{total}}",
230
+ "loading": "Loading…",
231
+ "working": "Working on it…",
232
+ "expired": "This panel has expired. Send /panel again.",
233
+ "busy": "The last tap is still being handled. Give it a moment.",
234
+ "loadFailed": "Could not read that just now. Try again in a moment.",
235
+ "sessions": {
236
+ "title": "Pick a session",
237
+ "hint": "Tap one to switch to it."
238
+ },
239
+ "teams": {
240
+ "title": "Pick a team",
241
+ "hint": "Tap a team and the next messages go to it.",
242
+ "unavailable": "Could not read the team list."
243
+ },
244
+ "models": {
245
+ "title": "Pick a model"
246
+ }
203
247
  }
204
248
  }
@@ -36,7 +36,8 @@
36
36
  "label": "平台"
37
37
  },
38
38
  "larkAppId": {
39
- "label": "飞书应用 ID"
39
+ "label": "飞书应用 ID",
40
+ "placeholder": "cli_…"
40
41
  },
41
42
  "larkAppSecret": {
42
43
  "label": "飞书应用密钥"
@@ -127,7 +128,8 @@
127
128
  "sessions": "· /sessions —— 列出本机器人为这个群的项目开过的会话",
128
129
  "resume": "· /resume 编号 —— 切到其中一条(/sessions 里的编号,或会话 id 的开头)",
129
130
  "model": "· /model —— 看这个群用的模型;/model 编号 或 /model 实例/模型 切换",
130
- "plan": "· /plan —— 开关计划模式(agent 先出计划、不动手);/plan on、/plan off 明确设定"
131
+ "plan": "· /plan —— 开关计划模式(agent 先出计划、不动手);/plan on、/plan off 明确设定",
132
+ "panel": "发 /panel 打开可点的命令面板。"
131
133
  },
132
134
  "sessions": {
133
135
  "title": "本机器人为这个群的项目开过的会话({{count}} 条):",
@@ -200,5 +202,47 @@
200
202
  "chosen": "已选:{{label}}",
201
203
  "elsewhere": "已在别处处理(网页或其他客户端)。"
202
204
  }
205
+ },
206
+ "panel": {
207
+ "title": "命令面板",
208
+ "hint": "点按钮等于打出对应的命令,也可以直接打字。",
209
+ "category": {
210
+ "session": "会话",
211
+ "target": "派给谁",
212
+ "system": "设置与状态"
213
+ },
214
+ "cmd": {
215
+ "new": "新会话",
216
+ "stop": "停止本轮",
217
+ "sessions": "会话列表",
218
+ "team": "派给团队",
219
+ "thread": "普通会话",
220
+ "model": "模型",
221
+ "planOn": "计划模式:开",
222
+ "planOff": "计划模式:关",
223
+ "status": "状态",
224
+ "help": "文字说明"
225
+ },
226
+ "back": "返回",
227
+ "prev": "上一页",
228
+ "next": "下一页",
229
+ "page": "第 {{page}} / {{total}} 页",
230
+ "loading": "加载中…",
231
+ "working": "处理中…",
232
+ "expired": "面板已过期,再发一次 /panel。",
233
+ "busy": "上一步还在处理,稍等一下。",
234
+ "loadFailed": "刚才没读出来,稍后再试。",
235
+ "sessions": {
236
+ "title": "选一条会话",
237
+ "hint": "点一条切过去。"
238
+ },
239
+ "teams": {
240
+ "title": "选一支队",
241
+ "hint": "点一支队,接下来的活派给它。",
242
+ "unavailable": "读不到团队列表。"
243
+ },
244
+ "models": {
245
+ "title": "选模型"
246
+ }
203
247
  }
204
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-im",
3
- "version": "0.2.2554",
3
+ "version": "0.2.2570",
4
4
  "description": "Talk to your agents from Telegram, WeCom or Lark.",
5
5
  "keywords": [
6
6
  "im",
@@ -27,7 +27,7 @@
27
27
  "esbuild": "0.28.1",
28
28
  "markdown-it": "^15.0.2",
29
29
  "vite-plus": "0.2.2",
30
- "@ycodium-ai/plugin-api": "0.2.2554"
30
+ "@ycodium-ai/plugin-api": "0.2.2570"
31
31
  },
32
32
  "ycodium": {
33
33
  "plugin": "./dist/index.js",