@ycodium-ai/plugin-im 0.2.2566 → 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,10 +51,18 @@ 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
67
  language of everything the bot says itself. The settings card follows the
60
68
  interface language instead. The copy lives in `locales/en.json` and
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,6 +133014,385 @@ 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;
@@ -134405,9 +134953,10 @@ ${final.text}`;
134405
134953
  commit("chat-updated", { chatKey, plan: next });
134406
134954
  await deliver(binding, t(next ? "plan.on" : "plan.off"));
134407
134955
  };
134408
- const handleCommand = async (binding, message, teams) => {
134956
+ const hasPanel = (binding) => adapterFor(binding.platform)?.panel !== void 0;
134957
+ const handleCommand = async (binding, text2, teams) => {
134409
134958
  const command = parseChatCommand({
134410
- text: message.text,
134959
+ text: text2,
134411
134960
  teams,
134412
134961
  // 中文别名只在回话语言是中文时认,免得英文用户打一个「停」就被当成命令。
134413
134962
  chinese: t.language === "zh-CN"
@@ -134451,7 +135000,20 @@ ${final.text}`;
134451
135000
  await deliver(binding, t("command.noTeams"));
134452
135001
  return true;
134453
135002
  case "help":
134454
- 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 }));
134455
135017
  return true;
134456
135018
  case "sessions":
134457
135019
  await showSessions(binding);
@@ -134640,9 +135202,75 @@ ${final.text}`;
134640
135202
  });
134641
135203
  chatQueues.set(chatKey, run);
134642
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
+ };
134643
135270
  const inboundHandlersFor = (platform) => ({
134644
135271
  onError: (message) => ctx.log.warn("chat inbound connection failed", { platform, message }),
134645
135272
  onApproval: handleApprovalClick,
135273
+ onPanelClick: (click) => handlePanelClick(platform, click),
134646
135274
  onMessage: (message) => {
134647
135275
  const target = bindingForChat(platform, message.chatId);
134648
135276
  if (target === void 0 || target.projectId === "") return;
@@ -134668,7 +135296,7 @@ ${final.text}`;
134668
135296
  }
134669
135297
  enqueueForChat(chatKeyOf(target), async () => {
134670
135298
  const teams = await fetchTeams(target);
134671
- if (await handleCommand(target, message, teams ?? [])) return;
135299
+ if (await handleCommand(target, message.text, teams ?? [])) return;
134672
135300
  const project = await ctx.projections.getProject(target.projectId);
134673
135301
  if (project === null || project.archivedAt != null) {
134674
135302
  if (project === null) {
package/locales/en.json CHANGED
@@ -128,7 +128,8 @@
128
128
  "sessions": "· /sessions — list the sessions this bot opened for this group's project",
129
129
  "resume": "· /resume number — switch to one of them (a number from /sessions, or the start of its id)",
130
130
  "model": "· /model — see the model this group uses; /model number or /model instance/model switches it",
131
- "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."
132
133
  },
133
134
  "sessions": {
134
135
  "title": "Sessions this bot opened for this group's project ({{count}}):",
@@ -201,5 +202,47 @@
201
202
  "chosen": "Chosen: {{label}}",
202
203
  "elsewhere": "Already handled elsewhere (the web app or another client)."
203
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
+ }
204
247
  }
205
248
  }
@@ -128,7 +128,8 @@
128
128
  "sessions": "· /sessions —— 列出本机器人为这个群的项目开过的会话",
129
129
  "resume": "· /resume 编号 —— 切到其中一条(/sessions 里的编号,或会话 id 的开头)",
130
130
  "model": "· /model —— 看这个群用的模型;/model 编号 或 /model 实例/模型 切换",
131
- "plan": "· /plan —— 开关计划模式(agent 先出计划、不动手);/plan on、/plan off 明确设定"
131
+ "plan": "· /plan —— 开关计划模式(agent 先出计划、不动手);/plan on、/plan off 明确设定",
132
+ "panel": "发 /panel 打开可点的命令面板。"
132
133
  },
133
134
  "sessions": {
134
135
  "title": "本机器人为这个群的项目开过的会话({{count}} 条):",
@@ -201,5 +202,47 @@
201
202
  "chosen": "已选:{{label}}",
202
203
  "elsewhere": "已在别处处理(网页或其他客户端)。"
203
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
+ }
204
247
  }
205
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-im",
3
- "version": "0.2.2566",
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.2566"
30
+ "@ycodium-ai/plugin-api": "0.2.2570"
31
31
  },
32
32
  "ycodium": {
33
33
  "plugin": "./dist/index.js",