feat: add slash command help

This commit is contained in:
2026-07-09 19:43:13 +08:00
parent f260195439
commit f1d29176d3
4 changed files with 432 additions and 43 deletions
+26 -43
View File
@@ -38,6 +38,7 @@ import { MessageBatcher, messageBatchKey, type MessageBatcherOptions } from "./m
import { ApprovalManager } from "./approval.js";
import { SenderNameCache } from "./senderCache.js";
import { TriggerQueue, triggerQueue as defaultTriggerQueue, type QueuedTrigger } from "./triggerQueue.js";
import { createSlashCommandRegistry, formatSlashHelpTarget, parseSlashHelpSubcommand, parseSlashInvocation } from "./slashCommands.js";
export { ApprovalManager } from "./approval.js";
export type { ApprovalResult, PendingApproval } from "./approval.js";
@@ -87,6 +88,12 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
const runAgent = deps.runAgent ?? defaultRunAgent;
const approvalManager = new ApprovalManager();
const triggerQueue = deps.triggerQueue ?? defaultTriggerQueue;
const slashCommands = createSlashCommandRegistry({
prisma: deps.prisma,
settings: deps.settings,
logger: deps.logger,
triggerQueue,
});
const batchContexts = new Map<string, TriggerRunContext>();
// runId → AbortController for live runs. Registered when a run starts,
// used by the interrupt card action to abort the SDK query, cleared in the
@@ -676,48 +683,23 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
// batcher. Session commands bypass the lock because they don't create runs;
// role/unknown slash prompts still use the normal run path and queue if locked.
if (cleanPrompt.startsWith("/")) {
const cmd = cleanPrompt.split(/\s+/)[0];
switch (cmd) {
case "/new": {
await deps.prisma.agentSession.updateMany({
where: { projectId, archivedAt: null },
data: { archivedAt: new Date() },
});
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。");
return;
}
case "/resume": {
const latest = await deps.prisma.agentSession.findFirst({
where: { projectId, archivedAt: { not: null } },
orderBy: { archivedAt: "desc" },
select: { id: true },
});
if (latest === null) {
await sendText(rt, chatId, "没有可恢复的会话。");
return;
}
await deps.prisma.agentSession.update({
where: { id: latest.id },
data: { archivedAt: null },
});
await sendText(rt, chatId, "已恢复上一个会话。");
return;
}
case "/reset": {
await deps.prisma.agentSession.updateMany({
where: { projectId, archivedAt: null },
data: { archivedAt: new Date() },
});
const cleared = triggerQueue.clear(projectId);
if (cleared > 0) {
deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
}
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。");
return;
}
default:
break;
const invocation = parseSlashInvocation(cleanPrompt);
const helpSubcommandTarget = invocation === null ? null : parseSlashHelpSubcommand(invocation);
if (helpSubcommandTarget !== null) {
// Help is generated on demand because role/tool configuration is runtime data.
const models = await deps.settings.modelRegistry({ projectId });
await sendText(rt, chatId, formatSlashHelpTarget(helpSubcommandTarget, models, slashCommands));
return;
}
if (invocation !== null) {
const slashCommand = slashCommands.get(invocation.name);
if (slashCommand !== undefined) {
await slashCommand.run({ invocation, projectId, chatId, rt });
return;
}
}
await startAgentRun(runContext, cleanPrompt);
return;
}
@@ -1037,11 +1019,12 @@ async function downloadPostMessageFiles(args: {
await downloadMessageFile(args.rt, args.msg.message_id, fileKey, savePath);
}
}
/**
* Parse a leading `/<role>` command from a prompt (e.g. `/draft 帮我写教案`).
* Returns the role id and the remaining prompt with the command stripped.
* Control commands already handled upstream (`/new`, `/resume`, `/reset`) are
* ignored here — they never reach this function.
* Control/help commands already handled upstream are ignored here — they never
* reach this function.
*
* Unknown `/foo` that isn't a registered role: returns `null` role and the
* original prompt unchanged (the slash is treated as literal text). Role