Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

382 lines
8.0 KiB
TypeScript

/**
* Turns neutral command specs into Discord REST slash-command payloads with
* button-based argument menus. Consumed by the slash-command registration path
* when syncing commands to the Discord application.
*/
import {
type APIApplicationCommandOption,
ApplicationCommandOptionType,
ButtonStyle,
type RESTPostAPIChatInputApplicationCommandsJSONBody,
} from "discord-api-types/v10";
/**
* Command argument definition
*/
export interface CommandArgDefinition {
name: string;
description: string;
type: "string" | "number" | "boolean";
required?: boolean;
choices?: Array<{ label: string; value: string }>;
}
/**
* Native command specification
*/
export interface NativeCommandSpec {
name: string;
description: string;
acceptsArgs?: boolean;
args?: CommandArgDefinition[];
ephemeralDefault?: boolean;
}
/**
* Command argument values
*/
export type CommandArgValues = Record<string, string | number | boolean>;
/**
* Parsed command arguments
*/
export interface CommandArgs {
values: CommandArgValues;
raw?: string;
}
/**
* Result of building command options
*/
export interface BuiltCommandOption {
name: string;
description: string;
type: ApplicationCommandOptionType;
required?: boolean;
choices?: Array<{ name: string; value: string | number }>;
}
/**
* Button specification for argument menus
*/
export interface CommandArgButton {
label: string;
customId: string;
style: ButtonStyle;
}
/**
* Row of buttons for argument menus
*/
export interface CommandArgButtonRow {
buttons: CommandArgButton[];
}
/**
* Argument menu specification
*/
export interface CommandArgMenu {
content: string;
rows: CommandArgButtonRow[];
}
/**
* Key for command argument custom IDs
*/
export const COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg";
/**
* Builds Discord command options from argument definitions
*/
export function buildDiscordCommandOptions(
args?: CommandArgDefinition[],
): BuiltCommandOption[] | undefined {
if (!args || args.length === 0) {
return undefined;
}
return args.map((arg) => {
const required = arg.required ?? false;
if (arg.type === "number") {
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.Number,
required,
};
}
if (arg.type === "boolean") {
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.Boolean,
required,
};
}
const choices =
arg.choices && arg.choices.length > 0 && arg.choices.length <= 25
? arg.choices.map((choice) => ({
name: choice.label,
value: choice.value,
}))
: undefined;
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.String,
required,
choices,
};
});
}
/**
* Builds a complete Discord slash command JSON body
*/
export function buildDiscordSlashCommand(
spec: NativeCommandSpec,
): RESTPostAPIChatInputApplicationCommandsJSONBody {
const options = buildDiscordCommandOptions(spec.args);
const commandOptions: APIApplicationCommandOption[] | undefined =
options?.map(
(opt) =>
({
name: opt.name,
description: opt.description,
type: opt.type,
required: opt.required,
choices: opt.choices,
}) as APIApplicationCommandOption,
) ??
(spec.acceptsArgs
? [
{
name: "input",
description: "Command input",
type: ApplicationCommandOptionType.String,
required: false,
} as APIApplicationCommandOption,
]
: undefined);
return {
name: spec.name,
description: spec.description,
options: commandOptions,
};
}
/**
* Encodes a command argument value for use in custom IDs
*/
export function encodeCommandArgValue(value: string): string {
return encodeURIComponent(value);
}
/**
* Decodes a command argument value from a custom ID
*/
export function decodeCommandArgValue(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
/**
* Builds a custom ID for a command argument button
*/
export function buildCommandArgCustomId(params: {
command: string;
arg: string;
value: string;
userId: string;
}): string {
return [
`${COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeCommandArgValue(params.command)}`,
`arg=${encodeCommandArgValue(params.arg)}`,
`value=${encodeCommandArgValue(params.value)}`,
`user=${encodeCommandArgValue(params.userId)}`,
].join(";");
}
/**
* Parses command argument data from a custom ID
*/
export function parseCommandArgCustomId(
customId: string,
): { command: string; arg: string; value: string; userId: string } | null {
if (!customId.startsWith(COMMAND_ARG_CUSTOM_ID_KEY)) {
return null;
}
const parts = customId.split(";");
const data: Record<string, string> = {};
for (const part of parts) {
const [key, value] = part.split("=");
if (key && value) {
// Handle the first part which has the prefix
const cleanKey = key.replace(`${COMMAND_ARG_CUSTOM_ID_KEY}:`, "");
data[cleanKey] = decodeCommandArgValue(value);
}
}
if (!data.command || !data.arg || !data.value || !data.user) {
return null;
}
return {
command: data.command,
arg: data.arg,
value: data.value,
userId: data.user,
};
}
/**
* Chunks an array into smaller arrays of a specified size
*/
function chunkArray<T>(items: T[], size: number): T[][] {
if (size <= 0) {
return [items];
}
const rows: T[][] = [];
for (let i = 0; i < items.length; i += size) {
rows.push(items.slice(i, i + size));
}
return rows;
}
/**
* Builds an argument menu with buttons for command selection
*/
export function buildCommandArgMenu(params: {
commandName: string;
arg: CommandArgDefinition;
choices: Array<{ value: string; label: string }>;
userId: string;
title?: string;
buttonsPerRow?: number;
}): CommandArgMenu {
const {
commandName,
arg,
choices,
userId,
title,
buttonsPerRow = 4,
} = params;
const rows = chunkArray(choices.slice(0, 20), buttonsPerRow).map(
(rowChoices) => ({
buttons: rowChoices.map((choice) => ({
label: choice.label.slice(0, 80),
customId: buildCommandArgCustomId({
command: commandName,
arg: arg.name,
value: choice.value,
userId,
}),
style: ButtonStyle.Secondary,
})),
}),
);
const content =
title ?? `Choose ${arg.description || arg.name} for /${commandName}.`;
return { content, rows };
}
/**
* Checks if an error is a Discord "Unknown Interaction" error
*/
export function isUnknownInteractionError(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
const err = error as {
code?: number;
status?: number;
message?: string;
rawError?: { code?: number; message?: string };
};
// Discord error code 10062 = Unknown Interaction
if (err.code === 10062 || err.rawError?.code === 10062) {
return true;
}
if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) {
return true;
}
if (/Unknown interaction/i.test(err.rawError?.message ?? "")) {
return true;
}
return false;
}
/**
* Safely executes a Discord interaction call, catching expired interaction errors
*/
export async function safeInteractionCall<T>(
fn: () => Promise<T>,
onExpired?: () => void,
): Promise<T | null> {
try {
return await fn();
} catch (error) {
if (isUnknownInteractionError(error)) {
onExpired?.();
return null;
}
throw error;
}
}
/**
* Creates command arguments from a single value
*/
export function createCommandArgs(
argName: string,
value: string | number | boolean,
): CommandArgs {
return {
values: { [argName]: value },
};
}
/**
* Serializes command arguments to a string
*/
export function serializeCommandArgs(args?: CommandArgs): string {
if (!args?.values) {
return "";
}
return Object.entries(args.values)
.map(([key, value]) => `${key}=${String(value)}`)
.join(" ");
}
/**
* Builds the full command text from arguments
*/
export function buildCommandText(
commandName: string,
args?: CommandArgs,
): string {
const argsText = serializeCommandArgs(args);
return argsText ? `/${commandName} ${argsText}` : `/${commandName}`;
}