@songsid/agend 2.1.10-beta.3 → 2.1.10-beta.5
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/channel/adapters/discord.d.ts +20 -1
- package/dist/channel/adapters/discord.js +111 -49
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +18 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/factory.d.ts +2 -0
- package/dist/channel/factory.js.map +1 -1
- package/dist/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/fleet-context.d.ts +3 -18
- package/dist/fleet-label.d.ts +5 -0
- package/dist/fleet-label.js +19 -0
- package/dist/fleet-label.js.map +1 -0
- package/dist/fleet-manager.d.ts +59 -32
- package/dist/fleet-manager.js +296 -244
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -1
- package/dist/locale.js +36 -30
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.js +9 -2
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.js +1 -1
- package/dist/login-flows.js.map +1 -1
- package/dist/login-window-lock.js +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/topic-commands.d.ts +0 -2
- package/dist/topic-commands.js +22 -47
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +18 -6
- package/dist/types.js +8 -2
- package/dist/types.js.map +1 -1
- package/dist/wake-coordinator.js +2 -2
- package/dist/web-terminal.d.ts +1 -1
- package/dist/web-terminal.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ graph LR
|
|
|
124
124
|
| Grok Build | `curl -fsSL https://x.ai/cli/install.sh \| bash` | `grok` (x.ai OAuth device flow). Needs CLI 1.0.13 or later; run `grok update` if the server refuses an older one |
|
|
125
125
|
| Meta Muse Code | `mkdir -p "$HOME/.local/bin" && curl -fsSL https://api.meta.ai/muse-launcher.sh -o "$HOME/.local/bin/muse" && chmod +x "$HOME/.local/bin/muse" && MUSE_LAUNCHER_INSTALL=1 "$HOME/.local/bin/muse"` (the launcher keeps its binary beside itself, so it is saved first) | `muse login` |
|
|
126
126
|
|
|
127
|
-
**Or install from chat with `/
|
|
127
|
+
**Or install from chat with `/login <backend>`** (beta, fleet admins only), without SSH-ing into the host: when the CLI is not installed yet, `/login` runs the command above in a fleet window, finds the CLI where its installer put it, adds that directory to the fleet's PATH, and goes on to sign in. A bare `/login` offers every backend above except Gemini CLI (`/login gemini-cli` still installs it).
|
|
128
128
|
|
|
129
129
|
**Tested CLI versions (AgEnD 2.1.9).** Codex 0.155 to 0.159. Claude Code 2.1.286 (first-run, trust and resume screens captured from the real CLI). Kiro CLI 2.21 to 2.27 run live; older 2.x versions still start, with a warning to update, and anything newer than 2.27 is launched only after its own `--help` confirms the flags AgEnD pins (the legacy UI on kiro's v1 engine, the terminal UI on v2). A kiro-cli that can no longer run an instance that way is refused, not started on another engine (#1109). `kiro_ui: v3` is not supported yet (#849). Grok CLI 1.0.13 or later. Muse 1.3.0. Antigravity and OpenCode have no pinned version. Other versions usually work, but a new CLI release can change its screens, so AgEnD only claims a version after testing against it.
|
|
130
130
|
|
package/README.zh-TW.md
CHANGED
|
@@ -109,7 +109,7 @@ graph LR
|
|
|
109
109
|
| Grok Build | `curl -fsSL https://x.ai/cli/install.sh \| bash` | `grok`(x.ai OAuth device flow)。CLI 需 1.0.13 以上,舊版會被伺服器拒絕,請執行 `grok update` |
|
|
110
110
|
| Meta Muse Code | `mkdir -p "$HOME/.local/bin" && curl -fsSL https://api.meta.ai/muse-launcher.sh -o "$HOME/.local/bin/muse" && chmod +x "$HOME/.local/bin/muse" && MUSE_LAUNCHER_INSTALL=1 "$HOME/.local/bin/muse"`(launcher 會把 binary 放在自己旁邊,所以要先存檔) | `muse login` |
|
|
111
111
|
|
|
112
|
-
**也可以在聊天室用 `/
|
|
112
|
+
**也可以在聊天室用 `/login <backend>` 安裝**(beta、僅限 fleet 管理員),不必 SSH 進主機:CLI 還沒安裝時,`/login` 會在 fleet 視窗執行上表的指令,到安裝程式實際放置的位置找到 CLI,把那個目錄加進 fleet 的 PATH,然後接著登入。單獨輸入 `/login` 會列出上表除了 Gemini CLI 以外的所有 backend(`/login gemini-cli` 仍可安裝它)。
|
|
113
113
|
|
|
114
114
|
**已測試的 CLI 版本(AgEnD 2.1.9)**:Codex 0.155 到 0.159;Claude Code 2.1.286(首次啟動、trust、resume 畫面都從真實 CLI 擷取);Kiro CLI 2.21 到 2.27 實際跑過;更舊的 2.x 仍可啟動,但會提醒更新;比 2.27 更新的版本,要先用它自己的 `--help` 確認 AgEnD 鎖定的參數(legacy UI 搭 kiro 的 v1 engine、terminal UI 搭 v2)還在才會啟動。若 kiro-cli 已無法這樣執行某個 instance,AgEnD 會拒絕啟動,不會換成別的 engine 執行(#1109)。`kiro_ui: v3` 尚未支援(#849);Grok CLI 1.0.13 以上;Muse 1.3.0。Antigravity 與 OpenCode 沒有釘定版本。其他版本通常也能用,但 CLI 新版可能改動畫面,所以 AgEnD 只在實測過後才宣稱支援某個版本。
|
|
115
115
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { Client } from "discord.js";
|
|
2
|
+
import { Client, ActionRowBuilder, ButtonBuilder } from "discord.js";
|
|
3
3
|
import type { ChannelAdapter, ApprovalHandle, SendOpts, SentMessage, PermissionPrompt, Choice, AlertData, AdapterHealthSnapshot, TopicPresence } from "../types.js";
|
|
4
4
|
import type { AccessManager } from "../access-manager.js";
|
|
5
5
|
/** Curated ClassicBot backends for Discord's native slash-option dropdown. */
|
|
@@ -34,6 +34,8 @@ export interface DiscordAdapterOptions {
|
|
|
34
34
|
categoryName?: string;
|
|
35
35
|
generalChannelId?: string;
|
|
36
36
|
registerCommands?: boolean;
|
|
37
|
+
/** This fleet's label, appended to the `/login` description. */
|
|
38
|
+
fleetLabel?: string;
|
|
37
39
|
/** Test seams; production callers leave these unset. */
|
|
38
40
|
clientFactory?: () => Client;
|
|
39
41
|
now?: () => number;
|
|
@@ -41,6 +43,22 @@ export interface DiscordAdapterOptions {
|
|
|
41
43
|
staleThresholdMs?: number;
|
|
42
44
|
reconnectBaseDelayMs?: number;
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Buttons laid out in rows of five (#1133). One row used to hold them all, and
|
|
48
|
+
* Discord rejects a row of more than five, so a chooser of seven backends
|
|
49
|
+
* could never be posted. More than 25 cannot be shown at all — an
|
|
50
|
+
* error, never a silent truncation.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buttonRows(choices: ReadonlyArray<{
|
|
53
|
+
id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
}>): ActionRowBuilder<ButtonBuilder>[];
|
|
56
|
+
/**
|
|
57
|
+
* A slash command description that names its fleet: every AgEnD bot in a
|
|
58
|
+
* guild registers `/login`, and the menu shows them side by side. Discord
|
|
59
|
+
* caps descriptions at 100 characters.
|
|
60
|
+
*/
|
|
61
|
+
export declare function withFleetLabel(description: string, label: string | undefined): string;
|
|
44
62
|
export declare class DiscordAdapter extends EventEmitter implements ChannelAdapter {
|
|
45
63
|
readonly type = "discord";
|
|
46
64
|
readonly topology: "channels";
|
|
@@ -75,6 +93,7 @@ export declare class DiscordAdapter extends EventEmitter implements ChannelAdapt
|
|
|
75
93
|
private categoryName;
|
|
76
94
|
private generalChannelId?;
|
|
77
95
|
private registerCommands;
|
|
96
|
+
private fleetLabel;
|
|
78
97
|
private queue;
|
|
79
98
|
private lastChatId;
|
|
80
99
|
private attachmentUrls;
|
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { createWriteStream } from "node:fs";
|
|
9
|
-
import { Client, GatewayIntentBits, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ApplicationCommandOptionType, ChannelType, PermissionFlagsBits, MessageFlags, Status, ActivityType, } from "discord.js";
|
|
9
|
+
import { Client, GatewayIntentBits, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ApplicationCommandOptionType, ChannelType, PermissionFlagsBits, MessageFlags, Events, Status, ActivityType, } from "discord.js";
|
|
10
10
|
import { MessageQueue } from "../message-queue.js";
|
|
11
11
|
import { splitTextFenceAware, truncatePreview, fenceBlock } from "../markdown-chunk.js";
|
|
12
12
|
const DISCORD_MAX_LENGTH = 2000;
|
|
@@ -25,6 +25,62 @@ export const DISCORD_START_BACKEND_CHOICES = [
|
|
|
25
25
|
{ name: "Meta Muse Code", value: "muse" },
|
|
26
26
|
];
|
|
27
27
|
const CLASSIC_START_BACKEND_SELECT_ID = "classic-start-backend";
|
|
28
|
+
/** Discord's limits: 5 buttons to an action row, 5 rows to a message. */
|
|
29
|
+
const BUTTONS_PER_ROW = 5;
|
|
30
|
+
const MAX_BUTTON_ROWS = 5;
|
|
31
|
+
/**
|
|
32
|
+
* Buttons laid out in rows of five (#1133). One row used to hold them all, and
|
|
33
|
+
* Discord rejects a row of more than five, so a chooser of seven backends
|
|
34
|
+
* could never be posted. More than 25 cannot be shown at all — an
|
|
35
|
+
* error, never a silent truncation.
|
|
36
|
+
*/
|
|
37
|
+
export function buttonRows(choices) {
|
|
38
|
+
if (choices.length > BUTTONS_PER_ROW * MAX_BUTTON_ROWS) {
|
|
39
|
+
throw new Error(`Discord shows at most ${BUTTONS_PER_ROW * MAX_BUTTON_ROWS} buttons on one message (${choices.length} requested)`);
|
|
40
|
+
}
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (let i = 0; i < choices.length; i += BUTTONS_PER_ROW) {
|
|
43
|
+
rows.push(new ActionRowBuilder().addComponents(choices.slice(i, i + BUTTONS_PER_ROW).map(choice => new ButtonBuilder()
|
|
44
|
+
.setCustomId(choice.id)
|
|
45
|
+
.setLabel(choice.label.slice(0, 80))
|
|
46
|
+
.setStyle(ButtonStyle.Secondary))));
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The click's acknowledgement (#1133). The click itself was acknowledged on
|
|
52
|
+
* arrival (deferUpdate, inside Discord's 3 s window); a notice — why nothing
|
|
53
|
+
* happened — goes to the clicker alone as an ephemeral follow-up.
|
|
54
|
+
*/
|
|
55
|
+
function privateNotice(interaction, adapterId) {
|
|
56
|
+
let done = false;
|
|
57
|
+
return (notice) => {
|
|
58
|
+
if (done)
|
|
59
|
+
return;
|
|
60
|
+
done = true;
|
|
61
|
+
if (!notice)
|
|
62
|
+
return;
|
|
63
|
+
// Never throws into the caller: the fleet's decision is already made.
|
|
64
|
+
Promise.resolve().then(() => interaction.followUp({ content: notice, flags: MessageFlags.Ephemeral }))
|
|
65
|
+
.catch(err => console.warn(`[discord:${adapterId}] could not send a private button notice (${err.message})`));
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A nonce-armed prompt button (`postNonceButtonPrompt`): `<prefix>:<32 hex>:<action>`.
|
|
70
|
+
* Fleet-manager validates these against the prompt that created them.
|
|
71
|
+
*/
|
|
72
|
+
const NONCE_BUTTON_ID = /^[a-z][a-z-]*:[0-9a-f]{32}:[a-z0-9-]+$/;
|
|
73
|
+
/**
|
|
74
|
+
* A slash command description that names its fleet: every AgEnD bot in a
|
|
75
|
+
* guild registers `/login`, and the menu shows them side by side. Discord
|
|
76
|
+
* caps descriptions at 100 characters.
|
|
77
|
+
*/
|
|
78
|
+
export function withFleetLabel(description, label) {
|
|
79
|
+
if (!label)
|
|
80
|
+
return description;
|
|
81
|
+
const suffix = ` · ${label}`;
|
|
82
|
+
return description.length + suffix.length <= 100 ? description + suffix : `${description.slice(0, 100 - suffix.length - 1)}…${suffix}`;
|
|
83
|
+
}
|
|
28
84
|
export class DiscordAdapter extends EventEmitter {
|
|
29
85
|
type = "discord";
|
|
30
86
|
topology = "channels";
|
|
@@ -59,6 +115,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
59
115
|
categoryName;
|
|
60
116
|
generalChannelId;
|
|
61
117
|
registerCommands;
|
|
118
|
+
fleetLabel;
|
|
62
119
|
queue;
|
|
63
120
|
lastChatId = null;
|
|
64
121
|
attachmentUrls = new Map();
|
|
@@ -73,6 +130,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
73
130
|
this.categoryName = opts.categoryName ?? "AgEnD Agents";
|
|
74
131
|
this.generalChannelId = opts.generalChannelId;
|
|
75
132
|
this.registerCommands = opts.registerCommands !== false;
|
|
133
|
+
this.fleetLabel = opts.fleetLabel;
|
|
76
134
|
mkdirSync(this.inboxDir, { recursive: true });
|
|
77
135
|
this.clientFactory = opts.clientFactory ?? (() => this.buildClient());
|
|
78
136
|
this.now = opts.now ?? Date.now;
|
|
@@ -203,7 +261,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
203
261
|
if (packet.t)
|
|
204
262
|
this.lastDispatchAt = this.now();
|
|
205
263
|
});
|
|
206
|
-
client.once(
|
|
264
|
+
client.once(Events.ClientReady, () => void this.handleClientReady(client, generation));
|
|
207
265
|
// Reactions on the bot's messages, as inbound events (#408). Both add and remove
|
|
208
266
|
// are reported so an agent can see an approval being withdrawn.
|
|
209
267
|
const onReaction = async (reaction, user, action) => {
|
|
@@ -461,8 +519,12 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
461
519
|
// interaction expires (>3s). Catch to prevent crashing the entire daemon.
|
|
462
520
|
client.on("interactionCreate", async (interaction) => {
|
|
463
521
|
try {
|
|
464
|
-
if (!this.isCurrentClient(client, generation))
|
|
522
|
+
if (!this.isCurrentClient(client, generation)) {
|
|
523
|
+
// A replaced client is destroyed, so this should never fire; if it
|
|
524
|
+
// does, the click is not lost without a trace (#1133).
|
|
525
|
+
console.info(`[discord:${this.id}] ignored an interaction on a replaced gateway client`);
|
|
465
526
|
return;
|
|
527
|
+
}
|
|
466
528
|
// Buttons: acknowledge IMMEDIATELY, before any guild/channel filtering.
|
|
467
529
|
// A button has a 3s ack window; any early return (unknown guild/channel)
|
|
468
530
|
// or a downstream no-op (e.g. the cancel button was already cleared) would
|
|
@@ -474,9 +536,14 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
474
536
|
await interaction.deferUpdate();
|
|
475
537
|
}
|
|
476
538
|
catch { /* already acknowledged / unknown interaction */ }
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
539
|
+
// Act on buttons from the primary guild or a known open channel, and
|
|
540
|
+
// on nonce-armed prompts from anywhere (#1131): a slash command is
|
|
541
|
+
// accepted from other guilds, so the prompt it posts there must be
|
|
542
|
+
// clickable. Fleet-manager checks those fail-closed — nonce, chat,
|
|
543
|
+
// message and fleet admin. Anything else is dropped, with a log line.
|
|
544
|
+
if (interaction.guildId !== this.guildId && !this.openChannels.has(interaction.channelId ?? "")
|
|
545
|
+
&& !NONCE_BUTTON_ID.test(interaction.customId)) {
|
|
546
|
+
console.info(`[discord:${this.id}] ignored a button outside the primary guild (guild ${interaction.guildId ?? "none"}, channel ${interaction.channelId ?? "none"})`);
|
|
480
547
|
return;
|
|
481
548
|
}
|
|
482
549
|
this.emitFromClient(client, generation, "callback_query", {
|
|
@@ -485,6 +552,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
485
552
|
threadId: interaction.channelId,
|
|
486
553
|
messageId: interaction.message.id,
|
|
487
554
|
userId: interaction.user.id,
|
|
555
|
+
ack: privateNotice(interaction, this.id),
|
|
488
556
|
});
|
|
489
557
|
return;
|
|
490
558
|
}
|
|
@@ -513,6 +581,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
513
581
|
threadId: interaction.channelId,
|
|
514
582
|
messageId: interaction.message.id,
|
|
515
583
|
userId: interaction.user.id,
|
|
584
|
+
ack: privateNotice(interaction, this.id),
|
|
516
585
|
});
|
|
517
586
|
return;
|
|
518
587
|
}
|
|
@@ -550,7 +619,8 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
550
619
|
// Discord ephemeral interaction replies cannot.
|
|
551
620
|
const fullRestart = interaction.commandName === "restart"
|
|
552
621
|
&& interaction.options.getString("mode") === "full";
|
|
553
|
-
|
|
622
|
+
const ephemeral = interaction.commandName !== "update" && !fullRestart;
|
|
623
|
+
await interaction.deferReply(ephemeral ? { flags: MessageFlags.Ephemeral } : {});
|
|
554
624
|
// Extract options as key-value pairs for fleet-manager
|
|
555
625
|
const options = {};
|
|
556
626
|
for (const opt of interaction.options.data) {
|
|
@@ -894,7 +964,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
894
964
|
// a guild with the primary — only the primary owns the guild's commands).
|
|
895
965
|
if (this.registerCommands)
|
|
896
966
|
try {
|
|
897
|
-
await client.application?.commands.set([
|
|
967
|
+
const registered = await client.application?.commands.set([
|
|
898
968
|
{
|
|
899
969
|
name: "start", description: t("slash.start"),
|
|
900
970
|
options: [{
|
|
@@ -963,7 +1033,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
963
1033
|
}],
|
|
964
1034
|
},
|
|
965
1035
|
{
|
|
966
|
-
name: "login", description: "🔒 " + t("slash.login"),
|
|
1036
|
+
name: "login", description: withFleetLabel("🔒 " + t("slash.login"), this.fleetLabel),
|
|
967
1037
|
options: [
|
|
968
1038
|
{
|
|
969
1039
|
name: "backend", description: t("slash.option.login_backend"),
|
|
@@ -975,29 +1045,12 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
975
1045
|
{ name: "grok", value: "grok" },
|
|
976
1046
|
{ name: "muse", value: "muse" },
|
|
977
1047
|
{ name: "antigravity", value: "antigravity" },
|
|
1048
|
+
{ name: "opencode", value: "opencode" },
|
|
978
1049
|
],
|
|
979
1050
|
},
|
|
980
1051
|
{ name: "code", description: t("slash.option.login_code"), type: ApplicationCommandOptionType.String, required: false },
|
|
981
1052
|
{ name: "cancel", description: t("slash.option.login_cancel"), type: ApplicationCommandOptionType.Boolean, required: false },
|
|
982
|
-
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
name: "install-cli", description: "🔒 " + t("slash.install_cli"),
|
|
986
|
-
options: [
|
|
987
|
-
{
|
|
988
|
-
name: "backend", description: t("slash.option.install_backend"),
|
|
989
|
-
type: ApplicationCommandOptionType.String, required: false,
|
|
990
|
-
choices: [
|
|
991
|
-
{ name: "claude-code", value: "claude-code" },
|
|
992
|
-
{ name: "codex", value: "codex" },
|
|
993
|
-
{ name: "kiro-cli", value: "kiro-cli" },
|
|
994
|
-
{ name: "grok", value: "grok" },
|
|
995
|
-
{ name: "muse", value: "muse" },
|
|
996
|
-
{ name: "antigravity", value: "antigravity" },
|
|
997
|
-
{ name: "opencode", value: "opencode" },
|
|
998
|
-
],
|
|
999
|
-
},
|
|
1000
|
-
{ name: "cancel", description: t("slash.option.install_cancel"), type: ApplicationCommandOptionType.Boolean, required: false },
|
|
1053
|
+
{ name: "reinstall", description: t("slash.option.login_reinstall"), type: ApplicationCommandOptionType.Boolean, required: false },
|
|
1001
1054
|
],
|
|
1002
1055
|
},
|
|
1003
1056
|
{ name: "clear", description: "🔒 " + t("slash.clear") },
|
|
@@ -1030,9 +1083,19 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1030
1083
|
},
|
|
1031
1084
|
{ name: "cancel", description: t("slash.cancel") },
|
|
1032
1085
|
]);
|
|
1086
|
+
this.emitFromClient(client, generation, "slash_registration", { ok: true, count: registered?.size });
|
|
1033
1087
|
}
|
|
1034
1088
|
catch (err) {
|
|
1035
|
-
// Non-fatal
|
|
1089
|
+
// Non-fatal, and no retry here (the next ready registers again) — but
|
|
1090
|
+
// never silent (#1131): Discord keeps the previous command list when a
|
|
1091
|
+
// registration is rejected, so a new command just never appears.
|
|
1092
|
+
const e = err;
|
|
1093
|
+
this.emitFromClient(client, generation, "slash_registration", {
|
|
1094
|
+
ok: false,
|
|
1095
|
+
code: e?.code,
|
|
1096
|
+
status: e?.status,
|
|
1097
|
+
message: String(e?.message ?? err),
|
|
1098
|
+
});
|
|
1036
1099
|
}
|
|
1037
1100
|
if (!this.isCurrentClient(client, generation))
|
|
1038
1101
|
return;
|
|
@@ -1089,7 +1152,7 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1089
1152
|
const chunks = splitTextFenceAware(reply, EMBED_MAX);
|
|
1090
1153
|
const message = await interaction.editReply({ content: "", embeds: [{ description: chunks[0] }], components: [] });
|
|
1091
1154
|
for (let i = 1; i < chunks.length; i++) {
|
|
1092
|
-
await interaction.followUp({
|
|
1155
|
+
await interaction.followUp({ flags: MessageFlags.Ephemeral, embeds: [{ description: chunks[i] }] });
|
|
1093
1156
|
}
|
|
1094
1157
|
return message.id;
|
|
1095
1158
|
}
|
|
@@ -1178,13 +1241,16 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1178
1241
|
async editMessageRemoveButtons(chatId, messageId, text, threadId) {
|
|
1179
1242
|
// Prefer the exact channel (handles forum-topic threads, which a GuildText
|
|
1180
1243
|
// scan misses); fall back to scanning top-level text channels.
|
|
1244
|
+
let exactError;
|
|
1181
1245
|
try {
|
|
1182
1246
|
const channel = await this._fetchTextChannel(threadId ?? chatId);
|
|
1183
1247
|
const msg = await channel.messages.fetch(messageId);
|
|
1184
1248
|
await msg.edit({ content: truncatePreview(text, DISCORD_MAX_LENGTH), components: [] });
|
|
1185
1249
|
return;
|
|
1186
1250
|
}
|
|
1187
|
-
catch {
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
exactError = err; /* not in that channel — fall through to scan */
|
|
1253
|
+
}
|
|
1188
1254
|
try {
|
|
1189
1255
|
const guild = await (await this.readyClient()).guilds.fetch(this.guildId);
|
|
1190
1256
|
const channels = guild.channels.cache.filter((c) => c.type === ChannelType.GuildText);
|
|
@@ -1200,7 +1266,11 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1200
1266
|
}
|
|
1201
1267
|
}
|
|
1202
1268
|
}
|
|
1203
|
-
catch { /*
|
|
1269
|
+
catch { /* no guild to scan — the edit failed */ }
|
|
1270
|
+
// Nothing was edited: say so (#1133). The text may be the only place an
|
|
1271
|
+
// outcome is shown (a cancel, a resend's result), and a caller that
|
|
1272
|
+
// believes it landed has no reason to post it another way.
|
|
1273
|
+
throw exactError instanceof Error ? exactError : new Error("the message could not be edited");
|
|
1204
1274
|
}
|
|
1205
1275
|
async deleteMessage(chatId, messageId, threadId) {
|
|
1206
1276
|
// Prefer the exact channel (handles forum-topic threads, which a GuildText
|
|
@@ -1451,15 +1521,14 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1451
1521
|
if (alert.choices && alert.choices.length > 0) {
|
|
1452
1522
|
const channelId = opts?.threadId ?? chatId;
|
|
1453
1523
|
const channel = await this._fetchTextChannel(channelId);
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
return { messageId: msg.id, chatId, threadId: opts?.threadId };
|
|
1524
|
+
const msg = await channel.send({ content: alert.message, components: buttonRows(alert.choices) });
|
|
1525
|
+
// The address this adapter's own callbacks report for these buttons
|
|
1526
|
+
// (guild + channel as thread), not the caller's spelling of it (#1131).
|
|
1527
|
+
// A native slash command addresses its channel as the chat; the button
|
|
1528
|
+
// click then arrived with chatId = guild and every nonce check rejected it.
|
|
1529
|
+
// Exactly the callback's value, even when no group_id is configured
|
|
1530
|
+
// (guildId is then "" in both places).
|
|
1531
|
+
return { messageId: msg.id, chatId: this.guildId, threadId: channelId };
|
|
1463
1532
|
}
|
|
1464
1533
|
return this.sendText(chatId, alert.message, opts);
|
|
1465
1534
|
}
|
|
@@ -1480,16 +1549,9 @@ export class DiscordAdapter extends EventEmitter {
|
|
|
1480
1549
|
async editAlert(chatId, messageId, alert, opts) {
|
|
1481
1550
|
const channel = await this._fetchTextChannel(opts?.threadId ?? chatId);
|
|
1482
1551
|
const msg = await channel.messages.fetch(messageId);
|
|
1483
|
-
const row = new ActionRowBuilder();
|
|
1484
|
-
for (const choice of alert.choices ?? []) {
|
|
1485
|
-
row.addComponents(new ButtonBuilder()
|
|
1486
|
-
.setCustomId(choice.id)
|
|
1487
|
-
.setLabel(choice.label.slice(0, 80))
|
|
1488
|
-
.setStyle(ButtonStyle.Secondary));
|
|
1489
|
-
}
|
|
1490
1552
|
await msg.edit({
|
|
1491
1553
|
content: truncatePreview(alert.message, DISCORD_MAX_LENGTH),
|
|
1492
|
-
components:
|
|
1554
|
+
components: buttonRows(alert.choices ?? []),
|
|
1493
1555
|
});
|
|
1494
1556
|
}
|
|
1495
1557
|
// ── Topology: create channel ────────────────────────────────────────────
|