mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 11:43:17 +08:00
feat(feishu): register native Slash Command panel on startup
The Feishu bot now calls the app_slash_commands OpenAPI at startup to register its common commands (menu, new, help, status, compact, sessionlist, workspacelist, watch, unwatch, watchlist, archived) as native Slash Commands, so typing / in a Feishu direct-message input box pops the command panel and tapping a command triggers it. The command list is owned and pushed by dsh-im; it does not depend on the dsh/Harness backend. Registration is best-effort and runs asynchronously, so it never blocks the long-connection startup or message delivery when the app lacks the application:app_slash_command:read/write scopes. Adds src/channels/feishu/slash-command-registry.mjs (manifest, list/create/delete, idempotent sync), wires it into feishu-runtime.mjs (slashCommands config flag + status fields), and covers it with tests.
This commit is contained in:
parent
7f968ac1d8
commit
64ba1463a8
6 changed files with 383 additions and 0 deletions
|
|
@ -3,6 +3,10 @@ import { FeishuHarnessBridge } from './bridge.mjs';
|
|||
import { cardActionProbeCard } from './feishu-cards.mjs';
|
||||
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
||||
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
||||
import {
|
||||
registerSlashCommands,
|
||||
SLASH_COMMAND_MANIFEST,
|
||||
} from './slash-command-registry.mjs';
|
||||
import {
|
||||
connectionTestTargetUnavailable,
|
||||
sendRememberedConnectionTest,
|
||||
|
|
@ -84,6 +88,11 @@ export function createBridgeStatus({ allowedSenderCount = 1 } = {}) {
|
|||
agentPreset: 'standard',
|
||||
authorizationMode: 'sender-open-id-allowlist',
|
||||
allowedSenderCount,
|
||||
slashCommandRegistration: 'idle',
|
||||
slashCommandsRegistered: 0,
|
||||
slashCommandsExisting: 0,
|
||||
slashCommandsFailed: 0,
|
||||
slashCommandsError: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +128,7 @@ export class FeishuRuntime {
|
|||
#abortController = null;
|
||||
#pendingCardActionProbes = new Map();
|
||||
#status;
|
||||
#slashCommands = true;
|
||||
|
||||
constructor({
|
||||
lark,
|
||||
|
|
@ -137,6 +147,7 @@ export class FeishuRuntime {
|
|||
replyTimeoutMs = 600000,
|
||||
connectTimeoutMs = 15000,
|
||||
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
slashCommands = true,
|
||||
wsAgent,
|
||||
logger = console,
|
||||
}) {
|
||||
|
|
@ -169,6 +180,7 @@ export class FeishuRuntime {
|
|||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
this.#connectTimeoutMs = connectTimeoutMs;
|
||||
this.#requestTimeoutMs = requestTimeoutMs;
|
||||
this.#slashCommands = Boolean(slashCommands);
|
||||
this.#wsAgent = wsAgent;
|
||||
this.#logger = logger;
|
||||
this.#status = createBridgeStatus({ allowedSenderCount: normalizedOwners.length });
|
||||
|
|
@ -368,6 +380,12 @@ export class FeishuRuntime {
|
|||
});
|
||||
await Promise.all([wsStarted, ready]);
|
||||
assertCurrentStart();
|
||||
// Register the native Slash Command panel best-effort and asynchronously
|
||||
// so it never blocks the long-connection startup. The panel is only a
|
||||
// client-side convenience; failure here must not take the bot down.
|
||||
if (this.#slashCommands && httpInstance) {
|
||||
void this.#registerSlashCommands(httpInstance, isCurrentStart);
|
||||
}
|
||||
return this.status;
|
||||
} catch (error) {
|
||||
// stop() owns the terminal idle state for an explicitly aborted start.
|
||||
|
|
@ -598,6 +616,43 @@ export class FeishuRuntime {
|
|||
return { sent: true };
|
||||
}
|
||||
|
||||
async #registerSlashCommands(httpInstance, isCurrentStart) {
|
||||
this.#status.slashCommandRegistration = 'registering';
|
||||
this.#status.slashCommandsError = null;
|
||||
try {
|
||||
const result = await registerSlashCommands({
|
||||
appId: this.#appId,
|
||||
appSecret: this.#appSecret,
|
||||
domain: this.#domain,
|
||||
httpInstance,
|
||||
manifest: SLASH_COMMAND_MANIFEST,
|
||||
});
|
||||
if (!isCurrentStart()) return;
|
||||
this.#status.slashCommandRegistration = 'done';
|
||||
this.#status.slashCommandsRegistered = result.created.length;
|
||||
this.#status.slashCommandsExisting = result.existing.length;
|
||||
this.#status.slashCommandsFailed = result.failed.length;
|
||||
this.#status.slashCommandsError = result.failed.length > 0
|
||||
? result.failed.map((f) => `/${f.command}: ${f.error?.message ?? String(f.error)}`).join('; ')
|
||||
: null;
|
||||
if (result.created.length > 0) {
|
||||
this.#logger.info?.(`[dsh-feishu] registered ${result.created.length} slash command(s)`);
|
||||
}
|
||||
if (result.failed.length > 0) {
|
||||
this.#logger.warn?.(
|
||||
`[dsh-feishu] ${result.failed.length} slash command(s) failed to register: ${this.#status.slashCommandsError}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCurrentStart()) return;
|
||||
this.#status.slashCommandRegistration = 'failed';
|
||||
this.#status.slashCommandsError = error?.message ?? String(error);
|
||||
this.#logger.warn?.(
|
||||
`[dsh-feishu] slash command registration skipped: ${this.#status.slashCommandsError}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
stop(options = {}) {
|
||||
if (this.#stopping) return this.#stopping;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue