@tencent-connect/openclaw-qqbot 1.7.0 → 1.7.1

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/src/gateway.ts CHANGED
@@ -9,6 +9,7 @@ import { recordKnownUser, flushKnownUsers } from "./known-users.js";
9
9
  import { getQQBotRuntime } from "./runtime.js";
10
10
  import { isGroupAllowed, resolveGroupName, resolveGroupPrompt, resolveHistoryLimit, resolveGroupPolicy, resolveGroupConfig, resolveIgnoreOtherMentions, resolveMentionPatterns } from "./config.js";
11
11
  import { qqbotPlugin, stripMentionText, detectWasMentioned } from "./channel.js";
12
+ import { QQBotApprovalHandler, registerApprovalHandler, unregisterApprovalHandler, getApprovalHandler } from "./approval-handler.js";
12
13
  import {
13
14
  recordPendingHistoryEntry,
14
15
  buildPendingHistoryContext,
@@ -21,7 +22,7 @@ import {
21
22
  } from "./group-history.js";
22
23
 
23
24
  import { setRefIndex, getRefIndex, formatRefEntryForAgent, formatMessageReferenceForAgent, flushRefIndex, type RefAttachmentSummary } from "./ref-index-store.js";
24
- import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion, type SlashCommandContext, type SlashCommandFileResult } from "./slash-commands.js";
25
+ import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion, type SlashCommandContext, type SlashCommandFileResult, type SlashCommandDelegateResult } from "./slash-commands.js";
25
26
  import { createMessageQueue, type QueuedMessage } from "./message-queue.js";
26
27
  import { triggerUpdateCheck } from "./update-checker.js";
27
28
  import { startImageServer, isImageServerRunning, type ImageServerConfig } from "./image-server.js";
@@ -174,9 +175,27 @@ async function handleInteractionCreate(params: {
174
175
  await acknowledgeInteraction(token, event.id, 0, { claw_cfg: ackClawCfg });
175
176
  log?.info(`[qqbot:${account.accountId}] Interaction ACK (type=${INTERACTION_TYPE_CONFIG_UPDATE}) sent: ${event.id}, claw_cfg=${JSON.stringify(ackClawCfg)}`);
176
177
  } else {
177
- // 其他类型:普通 ACK
178
+ // 普通按钮交互:先 ACK
178
179
  await acknowledgeInteraction(token, event.id);
179
180
  log?.debug?.(`[qqbot:${account.accountId}] Interaction ACK sent: ${event.id}`);
181
+
182
+ // Inline Keyboard 审批按钮(type=1 Callback)
183
+ // button_data 格式:approve:<approvalId>:<decision>
184
+ // approvalId 可能是 "exec:uuid" / "plugin:uuid"(带前缀)或纯 "uuid"(无前缀)
185
+ const buttonData = event.data?.resolved?.button_data ?? "";
186
+ const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
187
+ if (m) {
188
+ const approvalId = m[1]!;
189
+ const decision = m[2] as "allow-once" | "allow-always" | "deny";
190
+ const userId = event.group_member_openid || event.user_openid || event.data?.resolved?.user_id || "unknown";
191
+ log?.info(`[qqbot:${account.accountId}] Approval button clicked: approvalId=${approvalId}, decision=${decision}, user=${userId}, buttonData=${buttonData}`);
192
+ const handler = getApprovalHandler(account.accountId);
193
+ if (handler) {
194
+ void handler.resolveApproval(approvalId, decision);
195
+ } else {
196
+ log?.error(`[qqbot:${account.accountId}] Approval button: no handler found for accountId=${account.accountId}`);
197
+ }
198
+ }
180
199
  }
181
200
  }
182
201
 
@@ -517,6 +536,17 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
517
536
  log?.info(`[qqbot:${account.accountId}] Restored session from storage: sessionId=${sessionId}, lastSeq=${lastSeq}`);
518
537
  }
519
538
 
539
+ // ============ 审批 Handler ============
540
+ const approvalHandler = new QQBotApprovalHandler({
541
+ accountId: account.accountId,
542
+ appId: account.appId,
543
+ clientSecret: account.clientSecret,
544
+ cfg: cfg as any,
545
+ log,
546
+ });
547
+ registerApprovalHandler(account.accountId, approvalHandler);
548
+ void approvalHandler.start();
549
+
520
550
  // ============ 消息队列(复用 createMessageQueue,内置群消息合并/淘汰策略) ============
521
551
  const msgQueue = createMessageQueue({
522
552
  accountId: account.accountId,
@@ -526,7 +556,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
526
556
 
527
557
  // 斜杠指令拦截:在入队前匹配插件级指令,命中则直接回复,不入队
528
558
  // 紧急命令列表:这些命令会立即执行,不进入斜杠匹配流程
529
- const URGENT_COMMANDS = ["/stop"];
559
+ // /stop — 停止当前 agent run,清空队列
560
+ // /approve — 审批决策,必须在 agent 等待审批时立即执行,否则死锁
561
+ const URGENT_COMMANDS = ["/stop", "/approve"];
530
562
 
531
563
  const trySlashCommandOrEnqueue = async (msg: QueuedMessage): Promise<void> => {
532
564
  const content = (msg.content ?? "").trim();
@@ -577,6 +609,16 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
577
609
  return;
578
610
  }
579
611
 
612
+ // 委托给 AI 模型:用加工后的 prompt 替换原始消息入队
613
+ const isDelegateResult = typeof reply === "object" && reply !== null && "delegatePrompt" in reply;
614
+ if (isDelegateResult) {
615
+ const delegatePrompt = (reply as SlashCommandDelegateResult).delegatePrompt;
616
+ log?.info(`[qqbot:${account.accountId}] Slash command delegated to AI: ${content.slice(0, 40)}`);
617
+ msg.content = delegatePrompt;
618
+ msgQueue.enqueue(msg);
619
+ return;
620
+ }
621
+
580
622
  // 命中插件级指令,直接回复
581
623
  log?.info(`[qqbot:${account.accountId}] Slash command matched: ${content}, replying directly`);
582
624
  const token = await getAccessToken(account.appId, account.clientSecret);
@@ -635,6 +677,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
635
677
  flushKnownUsers();
636
678
  // P1-4: 保存引用索引数据
637
679
  flushRefIndex();
680
+ // 停止审批 handler
681
+ void approvalHandler.stop();
682
+ unregisterApprovalHandler(account.accountId);
638
683
  });
639
684
 
640
685
  const cleanup = () => {
@@ -1457,7 +1502,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1457
1502
  });
1458
1503
  }
1459
1504
 
1460
-
1461
1505
  const dispatchPromise = pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1462
1506
  ctx: ctxPayload,
1463
1507
  cfg,
@@ -369,19 +369,34 @@ declare module "openclaw/plugin-sdk" {
369
369
  /** 群消息策略适配器(resolveRequireMention / resolveToolPolicy / resolveGroupIntroHint) */
370
370
  export interface ChannelGroupAdapter {
371
371
  /** 是否需要 @机器人才响应 */
372
- resolveRequireMention?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => boolean;
372
+ resolveRequireMention?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => boolean | undefined;
373
373
  /** 群聊 AI 工具使用范围 */
374
- resolveToolPolicy?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string; senderId?: string }) => "full" | "restricted" | "none";
374
+ resolveToolPolicy?: (
375
+ ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string; senderId?: string }
376
+ ) =>
377
+ | "full"
378
+ | "restricted"
379
+ | "none"
380
+ | GroupToolPolicyConfig
381
+ | undefined;
375
382
  /** 平台特有的群聊行为提示 */
376
383
  resolveGroupIntroHint?: (ctx: { cfg: OpenClawConfig; accountId?: string; groupId: string }) => string | undefined;
377
384
  /** 其他适配器方法 */
378
385
  [key: string]: unknown;
379
386
  }
380
387
 
388
+ /** 工具策略配置(用于把 none/restricted/full 映射为 allow/deny) */
389
+ export type GroupToolPolicyConfig = {
390
+ allow: string[];
391
+ deny?: string[];
392
+ };
393
+
381
394
  /** @mention 检测与清理适配器(stripMentionText / detectWasMentioned) */
382
395
  export interface ChannelMentionAdapter {
383
396
  /** 清理 @mention 文本:平台格式→可读格式,去除 @机器人自身 */
384
397
  stripMentionText?: (text: string, mentions?: Array<{ member_openid?: string; nickname?: string; is_you?: boolean }>) => string;
398
+ /** stripMentions:框架回调型(一次性拿到 text + ctx) */
399
+ stripMentions?: (params: { text: string; ctx: unknown }) => string;
385
400
  /** 检测当前消息是否 @了机器人 */
386
401
  detectWasMentioned?: (ctx: {
387
402
  eventType?: string;
@@ -393,6 +408,14 @@ declare module "openclaw/plugin-sdk" {
393
408
  [key: string]: unknown;
394
409
  }
395
410
 
411
+ /** 状态摘要构建(仅覆盖当前项目用到的字段) */
412
+ export interface ChannelPluginStatus {
413
+ defaultRuntime: Record<string, unknown>;
414
+ buildChannelSummary?: (ctx: { snapshot: any }) => Record<string, unknown>;
415
+ buildAccountSnapshot?: (ctx: { account: any; runtime: any }) => Record<string, unknown>;
416
+ [key: string]: unknown;
417
+ }
418
+
396
419
  /**
397
420
  * 频道插件接口(泛型)
398
421
  */
@@ -423,6 +446,8 @@ declare module "openclaw/plugin-sdk" {
423
446
  groups?: ChannelGroupAdapter;
424
447
  /** @mention 检测与清理适配器 */
425
448
  mentions?: ChannelMentionAdapter;
449
+ /** 状态摘要构建(可选) */
450
+ status?: ChannelPluginStatus;
426
451
  /** 启动函数 */
427
452
  start?: (runtime: PluginRuntime) => void | Promise<void>;
428
453
  /** 停止函数 */
@@ -579,6 +604,54 @@ declare module "openclaw/plugin-sdk" {
579
604
  hasMatchInput?: boolean;
580
605
  }): MatchedGroupAccessDecision;
581
606
 
607
+ // ============ 审批运行时类型(minimal) ============
608
+ // 这些类型只覆盖本项目真实使用到的字段,
609
+ // 用来消除 strict/noImplicitAny 下的连锁报错。
610
+
611
+ export interface ExecApprovalRequest {
612
+ id: string;
613
+ expiresAtMs: number;
614
+ request: {
615
+ commandPreview?: string;
616
+ command?: string;
617
+ cwd?: string;
618
+ agentId?: string;
619
+ turnSourceAccountId?: string;
620
+ sessionKey?: string;
621
+ turnSourceTo?: string;
622
+ [key: string]: unknown;
623
+ };
624
+ }
625
+
626
+ export interface ExecApprovalResolved {
627
+ id: string;
628
+ decision: string;
629
+ [key: string]: unknown;
630
+ }
631
+
632
+ export interface PluginApprovalRequest {
633
+ id: string;
634
+ request: {
635
+ timeoutMs?: number;
636
+ severity?: "critical" | "info" | string;
637
+ title: string;
638
+ description?: string;
639
+ toolName?: string;
640
+ pluginId?: string;
641
+ agentId?: string;
642
+ turnSourceAccountId?: string;
643
+ sessionKey?: string;
644
+ turnSourceTo?: string;
645
+ [key: string]: unknown;
646
+ };
647
+ }
648
+
649
+ export interface PluginApprovalResolved {
650
+ id: string;
651
+ decision: string;
652
+ [key: string]: unknown;
653
+ }
654
+
582
655
  // ============ 其他导出 ============
583
656
 
584
657
  /** 默认账户 ID 常量 */
@@ -587,3 +660,55 @@ declare module "openclaw/plugin-sdk" {
587
660
  /** 规范化账户 ID */
588
661
  export function normalizeAccountId(accountId: string | undefined | null): string;
589
662
  }
663
+
664
+ declare module "openclaw/plugin-sdk/approval-runtime" {
665
+ export interface ExecApprovalReplyMetadata {
666
+ approvalId: string;
667
+ approvalSlug: string;
668
+ allowedDecisions?: string[];
669
+ }
670
+ export type ExecApprovalRequest = import("openclaw/plugin-sdk").ExecApprovalRequest;
671
+ export type ExecApprovalResolved = import("openclaw/plugin-sdk").ExecApprovalResolved;
672
+ export type PluginApprovalRequest = import("openclaw/plugin-sdk").PluginApprovalRequest;
673
+ export type PluginApprovalResolved = import("openclaw/plugin-sdk").PluginApprovalResolved;
674
+ export function getExecApprovalReplyMetadata(payload: { channelData?: unknown }): ExecApprovalReplyMetadata | null;
675
+ }
676
+
677
+ declare module "openclaw/plugin-sdk/core" {
678
+ export type OpenClawConfig = import("openclaw/plugin-sdk").OpenClawConfig;
679
+ export type ChannelPlugin<TAccount = unknown> =
680
+ import("openclaw/plugin-sdk").ChannelPlugin<TAccount>;
681
+
682
+ export const applyAccountNameToChannelSection: typeof import("openclaw/plugin-sdk")
683
+ .applyAccountNameToChannelSection;
684
+ export const deleteAccountFromConfigSection: typeof import("openclaw/plugin-sdk")
685
+ .deleteAccountFromConfigSection;
686
+ export const setAccountEnabledInConfigSection: typeof import("openclaw/plugin-sdk")
687
+ .setAccountEnabledInConfigSection;
688
+
689
+ // 允许其它 core 导出被逐步补齐(避免引入大量严格类型时频繁改代码)
690
+ export * from "openclaw/plugin-sdk";
691
+ }
692
+
693
+ declare module "openclaw/plugin-sdk/gateway-runtime" {
694
+ export interface EventFrame {
695
+ event: string;
696
+ payload: unknown;
697
+ }
698
+
699
+ export interface GatewayClient {
700
+ start: () => void | Promise<void>;
701
+ stop: () => void | Promise<void>;
702
+ request: (method: string, params: unknown) => Promise<unknown>;
703
+ }
704
+
705
+ export function createOperatorApprovalsGatewayClient(options: {
706
+ config: import("openclaw/plugin-sdk").OpenClawConfig;
707
+ gatewayUrl?: string;
708
+ clientDisplayName?: string;
709
+ onEvent: (evt: EventFrame) => void;
710
+ onHelloOk?: () => void;
711
+ onConnectError?: (err: { message: string }) => void;
712
+ onClose?: (code: number, reason: string) => void;
713
+ }): Promise<GatewayClient>;
714
+ }
@@ -22,6 +22,7 @@ import { saveCredentialBackup } from "./credential-backup.js";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { getPackageVersion } from "./utils/pkg-version.js";
24
24
  import { getQQBotRuntime } from "./runtime.js";
25
+ import { isApprovalFeatureAvailable } from "./approval-handler.js";
25
26
  const require = createRequire(import.meta.url);
26
27
 
27
28
  let PLUGIN_VERSION = getPackageVersion(import.meta.url);
@@ -216,8 +217,8 @@ export interface QueueSnapshot {
216
217
  senderPending: number;
217
218
  }
218
219
 
219
- /** 斜杠指令返回值:文本、带文件的结果、或 null(不处理) */
220
- export type SlashCommandResult = string | SlashCommandFileResult | null;
220
+ /** 斜杠指令返回值:文本、带文件的结果、委托给模型、或 null(不处理) */
221
+ export type SlashCommandResult = string | SlashCommandFileResult | SlashCommandDelegateResult | null;
221
222
 
222
223
  /** 带文件的指令结果(先回复文本,再发送文件) */
223
224
  export interface SlashCommandFileResult {
@@ -226,6 +227,12 @@ export interface SlashCommandFileResult {
226
227
  filePath: string;
227
228
  }
228
229
 
230
+ /** 委托给 AI 模型处理:用加工后的 prompt 替换原始消息入队 */
231
+ export interface SlashCommandDelegateResult {
232
+ /** 替换原始消息内容的 prompt,交给 AI 模型执行 */
233
+ delegatePrompt: string;
234
+ }
235
+
229
236
  /** 斜杠指令定义 */
230
237
  interface SlashCommand {
231
238
  /** 指令名(不含 /) */
@@ -2008,6 +2015,250 @@ registerCommand({
2008
2015
  },
2009
2016
  });
2010
2017
 
2018
+ // ============ /bot-approve 审批配置管理 ============
2019
+
2020
+ /**
2021
+ * /bot-approve — 管理命令执行审批配置
2022
+ *
2023
+ * 修改 openclaw.json 中 tools.exec.security / tools.exec.ask 字段。
2024
+ *
2025
+ * security: deny | allowlist | full
2026
+ * ask: off | on-miss | always
2027
+ */
2028
+ registerCommand({
2029
+ name: "bot-approve",
2030
+ description: "管理命令执行审批配置",
2031
+ usage: [
2032
+ `/bot-approve 查看操作指引`,
2033
+ `/bot-approve on 开启审批(白名单模式,推荐)`,
2034
+ `/bot-approve off 关闭审批,命令直接执行`,
2035
+ `/bot-approve always 始终审批,每次执行都需审批`,
2036
+ `/bot-approve reset 恢复框架默认值`,
2037
+ `/bot-approve status 查看当前审批配置`,
2038
+ ].join("\n"),
2039
+ handler: async (ctx) => {
2040
+ const arg = ctx.args.trim().toLowerCase();
2041
+
2042
+ // 审批功能需要 openclaw >= 3.22(gateway-runtime 模块)
2043
+ if (!isApprovalFeatureAvailable()) {
2044
+ return `❌ 当前 OpenClaw 版本不支持审批功能,请升级至最新版本后重试。`;
2045
+ }
2046
+
2047
+ let runtime;
2048
+ try {
2049
+ runtime = getQQBotRuntime();
2050
+ } catch {
2051
+ // runtime 不可用,构造 prompt 委托给 AI 模型通过 CLI 完成
2052
+ const presetMap: Record<string, { security: string; ask: string; desc: string }> = {
2053
+ on: { security: "allowlist", ask: "on-miss", desc: "开启审批(白名单模式)" },
2054
+ off: { security: "full", ask: "off", desc: "关闭审批" },
2055
+ always: { security: "allowlist", ask: "always", desc: "严格模式(每次都审批)" },
2056
+ strict: { security: "allowlist", ask: "always", desc: "严格模式(每次都审批)" },
2057
+ };
2058
+ const preset = presetMap[arg];
2059
+ if (preset) {
2060
+ return {
2061
+ delegatePrompt: [
2062
+ `请帮我${preset.desc}。执行以下单条命令(不要拆开):`,
2063
+ `openclaw config set tools.exec.security ${preset.security} && openclaw config set tools.exec.ask ${preset.ask}`,
2064
+ `执行完成后告诉用户审批配置已更新为 security=${preset.security}, ask=${preset.ask}。`,
2065
+ ].join("\n"),
2066
+ };
2067
+ }
2068
+ if (arg === "reset") {
2069
+ return {
2070
+ delegatePrompt: [
2071
+ `请帮我重置审批配置。执行以下单条命令(不要拆开):`,
2072
+ `openclaw config unset tools.exec.security && openclaw config unset tools.exec.ask`,
2073
+ `执行完成后告诉用户审批配置已重置为框架默认值。`,
2074
+ ].join("\n"),
2075
+ };
2076
+ }
2077
+ if (arg === "status") {
2078
+ return {
2079
+ delegatePrompt: [
2080
+ `请帮我查看当前命令执行审批配置。执行以下单条命令(不要拆开):`,
2081
+ `echo "security=$(openclaw config get tools.exec.security)" && echo "ask=$(openclaw config get tools.exec.ask)"`,
2082
+ `然后告诉用户当前 security 和 ask 的值,以及可用的操作选项:`,
2083
+ `- /bot-approve on 开启审批(白名单模式)`,
2084
+ `- /bot-approve off 关闭审批`,
2085
+ `- /bot-approve always 严格模式`,
2086
+ `- /bot-approve reset 恢复默认`,
2087
+ ].join("\n"),
2088
+ };
2089
+ }
2090
+ // 无参数或未知参数:直接返回操作指引
2091
+ return [
2092
+ `🔐 命令执行审批配置`,
2093
+ ``,
2094
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
2095
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
2096
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
2097
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
2098
+ `<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`,
2099
+ ].join("\n");
2100
+ }
2101
+ const configApi = runtime.config as {
2102
+ loadConfig: () => Record<string, unknown>;
2103
+ writeConfigFile: (cfg: unknown) => Promise<void>;
2104
+ };
2105
+
2106
+ const loadExecConfig = () => {
2107
+ const cfg = configApi.loadConfig() as Record<string, unknown>;
2108
+ const tools = (cfg.tools ?? {}) as Record<string, unknown>;
2109
+ const exec = (tools.exec ?? {}) as Record<string, unknown>;
2110
+ return {
2111
+ security: String(exec.security ?? "deny"),
2112
+ ask: String(exec.ask ?? "on-miss"),
2113
+ };
2114
+ };
2115
+
2116
+ const writeExecConfig = async (security: string, ask: string) => {
2117
+ const cfg = structuredClone(configApi.loadConfig()) as Record<string, unknown>;
2118
+ const tools = ((cfg.tools ?? {}) as Record<string, unknown>);
2119
+ const exec = ((tools.exec ?? {}) as Record<string, unknown>);
2120
+ exec.security = security;
2121
+ exec.ask = ask;
2122
+ tools.exec = exec;
2123
+ cfg.tools = tools;
2124
+ await configApi.writeConfigFile(cfg);
2125
+ };
2126
+
2127
+ const formatStatus = (security: string, ask: string) => {
2128
+ const secIcon = security === "full" ? "🟢" : security === "allowlist" ? "🟡" : "🔴";
2129
+ const askIcon = ask === "off" ? "🟢" : ask === "always" ? "🔴" : "🟡";
2130
+ return [
2131
+ `🔐 当前审批配置`,
2132
+ ``,
2133
+ `${secIcon} 安全模式 (security): **${security}**`,
2134
+ `${askIcon} 审批模式 (ask): **${ask}**`,
2135
+ ``,
2136
+ security === "deny" ? `⚠️ 当前为 deny 模式,所有命令执行被拒绝` :
2137
+ security === "full" && ask === "off" ? `✅ 所有命令无需审批直接执行` :
2138
+ security === "allowlist" && ask === "on-miss" ? `🛡️ 白名单命令直接执行,其余需审批` :
2139
+ ask === "always" ? `🔒 每次命令执行都需要人工审批` :
2140
+ `ℹ️ security=${security}, ask=${ask}`,
2141
+ ].join("\n");
2142
+ };
2143
+
2144
+ // 无参数:操作指引
2145
+ if (!arg) {
2146
+ return [
2147
+ `🔐 命令执行审批配置`,
2148
+ ``,
2149
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
2150
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
2151
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
2152
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
2153
+ `<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`,
2154
+ ].join("\n");
2155
+ }
2156
+
2157
+ // status: 查看当前配置
2158
+ if (arg === "status") {
2159
+ const { security, ask } = loadExecConfig();
2160
+ return [
2161
+ formatStatus(security, ask),
2162
+ ``,
2163
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批`,
2164
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
2165
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
2166
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
2167
+ ].join("\n");
2168
+ }
2169
+
2170
+ // on: 开启审批(白名单 + 未命中审批)
2171
+ if (arg === "on") {
2172
+ try {
2173
+ await writeExecConfig("allowlist", "on-miss");
2174
+ return [
2175
+ `✅ 审批已开启`,
2176
+ ``,
2177
+ `• security = allowlist(白名单模式)`,
2178
+ `• ask = on-miss(未命中白名单时需审批)`,
2179
+ ``,
2180
+ `已批准的命令自动加入白名单,下次直接执行。`,
2181
+ ].join("\n");
2182
+ } catch (err) {
2183
+ return `❌ 配置更新失败: ${err}`;
2184
+ }
2185
+ }
2186
+
2187
+ // off: 关闭审批
2188
+ if (arg === "off") {
2189
+ try {
2190
+ await writeExecConfig("full", "off");
2191
+ return [
2192
+ `✅ 审批已关闭`,
2193
+ ``,
2194
+ `• security = full(允许所有命令)`,
2195
+ `• ask = off(不需要审批)`,
2196
+ ``,
2197
+ `⚠️ 所有命令将直接执行,不会弹出审批确认。`,
2198
+ ].join("\n");
2199
+ } catch (err) {
2200
+ return `❌ 配置更新失败: ${err}`;
2201
+ }
2202
+ }
2203
+
2204
+ // always: 始终审批(每次都审批)
2205
+ if (arg === "always") {
2206
+ try {
2207
+ await writeExecConfig("allowlist", "always");
2208
+ return [
2209
+ `✅ 已切换为严格审批模式`,
2210
+ ``,
2211
+ `• security = allowlist`,
2212
+ `• ask = always(每次执行都需审批)`,
2213
+ ``,
2214
+ `每个命令都会弹出审批按钮,需手动确认。`,
2215
+ ].join("\n");
2216
+ } catch (err) {
2217
+ return `❌ 配置更新失败: ${err}`;
2218
+ }
2219
+ }
2220
+
2221
+ // reset: 删除配置,恢复框架默认值
2222
+ if (arg === "reset") {
2223
+ try {
2224
+ const cfg = structuredClone(configApi.loadConfig()) as Record<string, unknown>;
2225
+ const tools = (cfg.tools ?? {}) as Record<string, unknown>;
2226
+ const exec = (tools.exec ?? {}) as Record<string, unknown>;
2227
+ delete exec.security;
2228
+ delete exec.ask;
2229
+ if (Object.keys(exec).length === 0) {
2230
+ delete tools.exec;
2231
+ } else {
2232
+ tools.exec = exec;
2233
+ }
2234
+ if (Object.keys(tools).length === 0) {
2235
+ delete cfg.tools;
2236
+ } else {
2237
+ cfg.tools = tools;
2238
+ }
2239
+ await configApi.writeConfigFile(cfg);
2240
+ return [
2241
+ `✅ 审批配置已重置`,
2242
+ ``,
2243
+ `已移除 tools.exec.security 和 tools.exec.ask`,
2244
+ `框架将使用默认值(security=deny, ask=on-miss)`,
2245
+ ``,
2246
+ `如需开启命令执行,请使用 /bot-approve on`,
2247
+ ].join("\n");
2248
+ } catch (err) {
2249
+ return `❌ 配置更新失败: ${err}`;
2250
+ }
2251
+ }
2252
+
2253
+ return [
2254
+ `❌ 未知参数: ${arg}`,
2255
+ ``,
2256
+ `可用选项: on | off | always | reset`,
2257
+ `输入 /bot-approve ? 查看详细用法`,
2258
+ ].join("\n");
2259
+ },
2260
+ });
2261
+
2011
2262
  // ============ 匹配入口 ============
2012
2263
 
2013
2264
  /**
package/src/types.ts CHANGED
@@ -339,6 +339,84 @@ export interface InteractionEvent {
339
339
  };
340
340
  }
341
341
 
342
+ // ---- Keyboard 类型 ----
343
+
344
+ /**
345
+ * 按钮 Action 类型
346
+ * 0=跳转链接 1=回调型(INTERACTION_CREATE) 2=指令型(直接发文本) 3=mqqapi
347
+ */
348
+ export type KeyboardActionType = 0 | 1 | 2 | 3;
349
+
350
+ /** 按钮权限 */
351
+ export interface KeyboardPermission {
352
+ /** 0=全体 1=管理员 2=按钮指定 3=身份组 */
353
+ type: 0 | 1 | 2 | 3;
354
+ specify_role_ids?: string[];
355
+ specify_user_ids?: string[];
356
+ }
357
+
358
+ /** 二次确认弹窗 */
359
+ export interface KeyboardModal {
360
+ content: string;
361
+ confirm_text?: string;
362
+ cancel_text?: string;
363
+ }
364
+
365
+ /** 按钮 Action */
366
+ export interface KeyboardAction {
367
+ type: KeyboardActionType;
368
+ data?: string;
369
+ /** true = 点击后直接发出(Enter)*/
370
+ enter?: boolean;
371
+ /** 仅指令型(type=2):是否把指令发到输入框(reply=true)还是静默发出 */
372
+ reply?: boolean;
373
+ permission?: KeyboardPermission;
374
+ click_limit?: number;
375
+ unsupport_tips?: string;
376
+ modal?: KeyboardModal;
377
+ }
378
+
379
+ /** 按钮渲染数据 */
380
+ export interface KeyboardRenderData {
381
+ label: string;
382
+ visited_label?: string;
383
+ /** 0=灰色线框 1=蓝色线框 2=推荐回复专用 3=红色字体 4=蓝色背景 */
384
+ style?: 0 | 1 | 2 | 3 | 4;
385
+ }
386
+
387
+ /** 单个按钮 */
388
+ export interface KeyboardButton {
389
+ id: string;
390
+ render_data?: KeyboardRenderData;
391
+ action?: KeyboardAction;
392
+ group_id?: string;
393
+ }
394
+
395
+ /** 一行按钮 */
396
+ export interface KeyboardRow {
397
+ buttons: KeyboardButton[];
398
+ }
399
+
400
+ /** CustomKeyboard(自定义按钮内容) */
401
+ export interface CustomKeyboard {
402
+ rows: KeyboardRow[];
403
+ }
404
+
405
+ /** MessageKeyboard(keyboard / prompt_keyboard.keyboard 共用) */
406
+ export interface MessageKeyboard {
407
+ /** 模板 ID(与 content 二选一) */
408
+ id?: string;
409
+ /** 自定义内容 */
410
+ content?: CustomKeyboard;
411
+ }
412
+
413
+ /**
414
+ * Inline Keyboard(消息内嵌按钮,需平台审核)
415
+ * 发送字段:keyboard
416
+ * JSON: { "keyboard": { "id": "...", "content": { "rows": [...] } } }
417
+ */
418
+ export type InlineKeyboard = MessageKeyboard;
419
+
342
420
  /**
343
421
  * WebSocket 事件负载
344
422
  */