Files
qqbot/src/types.ts
Mingkuan e74be7a3f8 Release/1.7.1 (#247)
* feat: 添加审批功能(Inline Keyboard 按钮交互)

- 新增 approval-handler.ts:监听 Gateway 审批事件,发送带 Inline Keyboard 的审批消息
- api.ts:添加 sendC2CMessageWithInlineKeyboard / sendGroupMessageWithInlineKeyboard
- gateway.ts:处理 Inline Keyboard 按钮回调,注册/注销 approval-handler
- channel.ts:配置 execApprovals(3.28)和 approvals(3.31+)抑制框架 Forwarder 重复通知
- types.ts:添加 InlineKeyboard / KeyboardButton 等 Keyboard 类型定义
- openclaw-plugin-sdk.d.ts:补充 approval-runtime 模块声明

* feat: 增加outbound过滤

* feat: 新增审批相关配置

1. 斜杠指令当遇到报错的时候,支持将文本代理给模型来回复用户

* feat: 新增gateway审批路由注册

* feat: 新增gateway审批路由注册

* feat: 修复低版本发现的问题

1. 3.11版本兼容性处理,动态加载gateway依赖
2. 兼容3.28版本,声明审批功能可用
3. 恢复默认配置的文案修改

* feat: 修复gateway依赖加载失败问题

* feat: update 1.7.1 release
2026-04-03 03:12:30 +08:00

482 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── QQ 消息类型常量message_type 枚举值) ──
/** 普通文本消息 */
export const MSG_TYPE_TEXT = 0;
/** 引用(回复)消息 */
export const MSG_TYPE_QUOTE = 103;
/**
* QQ Bot 配置类型
*/
export interface QQBotConfig {
appId: string;
clientSecret?: string;
clientSecretFile?: string;
}
/**
* 解析后的 QQ Bot 账户
*/
export interface ResolvedQQBotAccount {
accountId: string;
name?: string;
enabled: boolean;
appId: string;
clientSecret: string;
secretSource: "config" | "file" | "env" | "none";
/** 系统提示词 */
systemPrompt?: string;
/** 图床服务器公网地址 */
imageServerBaseUrl?: string;
/** 是否支持 markdown 消息(默认 true */
markdownSupport: boolean;
config: QQBotAccountConfig;
}
/** 群消息策略open=全响应 | allowlist=白名单 | disabled=不响应 */
export type GroupPolicy = "open" | "allowlist" | "disabled";
/** 工具策略full=全部 | restricted=限制敏感工具 | none=禁止 */
export type ToolPolicy = "full" | "restricted" | "none";
/** 单个群的配置 */
export interface GroupConfig {
/** 是否需要 @机器人才响应(默认 true */
requireMention?: boolean;
/**
* 是否忽略 @了其他用户但没有 @机器人的消息(默认 false
* 开启后,消息中 @了其他人但未 @bot 时直接丢弃(不记录历史、不触发 AI
*/
ignoreOtherMentions?: boolean;
/** 群聊中 AI 可使用的工具范围(默认 restricted */
toolPolicy?: ToolPolicy;
/** 群名称 */
name?: string;
/** 群消息行为 PE未配置时使用内置默认值 */
prompt?: string;
/** 群历史消息缓存条数0 禁用,默认 20 */
historyLimit?: number;
}
/**
* QQ Bot 账户配置
*/
export interface QQBotAccountConfig {
enabled?: boolean;
name?: string;
appId?: string;
clientSecret?: string;
clientSecretFile?: string;
dmPolicy?: "open" | "pairing" | "allowlist";
allowFrom?: string[];
/** 群消息策略(默认 allowlist */
groupPolicy?: GroupPolicy;
/** 群白名单groupPolicy 为 allowlist 时生效) */
groupAllowFrom?: string[];
/** 群配置映射(按 groupOpenid 索引,"*" 为默认) */
groups?: Record<string, GroupConfig>;
/** 系统提示词,会添加在用户消息前面 */
systemPrompt?: string;
/** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */
imageServerBaseUrl?: string;
/** 是否支持 markdown 消息(默认 true设为 false 可禁用) */
markdownSupport?: boolean;
/**
* @deprecated 请使用 audioFormatPolicy.uploadDirectFormats
* 可直接上传的音频格式(不转换为 SILK向后兼容
*/
voiceDirectUploadFormats?: string[];
/**
* 音频格式策略配置
* 统一管理入站STT和出站上传的音频格式转换行为
*/
audioFormatPolicy?: AudioFormatPolicy;
/**
* 是否启用公网 URL 直传 QQ 平台(默认 true
* 启用时:公网 URL 先直传给 QQ 开放平台的富媒体 API平台自行拉取失败后自动 fallback 到插件下载再 Base64 上传
* 禁用时:公网 URL 始终由插件先下载到本地,再以 Base64 上传(适用于 QQ 平台无法访问目标 URL 的场景)
*/
urlDirectUpload?: boolean;
/**
* /bot-upgrade 指令返回的升级指引网址
* 默认: https://doc.weixin.qq.com/doc/w3_AKEAGQaeACgCNHrh1CbHzTAKtT2gB?scode=AJEAIQdfAAozxFEnLZAKEAGQaeACg
*/
upgradeUrl?: string;
/**
* /bot-upgrade 指令的行为模式
* - "doc":展示升级文档链接(安全模式)
* - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
*/
upgradeMode?: "doc" | "hot-reload";
/**
* /bot-upgrade 热更新时使用的 npm 包名
* 支持 "scope/name"(自动补 @)或 "@scope/name" 格式
* 默认: "@tencent-connect/openclaw-qqbot"
* 示例: "ryantest/openclaw-qqbot"
*/
upgradePkg?: string;
/**
* 出站消息合并回复debounce配置
* 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
*/
deliverDebounce?: DeliverDebounceConfig;
/**
* 是否启用流式消息(默认 false
* 启用后AI 的回复会以流式形式逐步显示在 QQ 聊天中,
* 用户可以看到文字逐字出现的打字机效果。
* 设置为 true 可开启流式消息。
*
* 注意:仅 C2C私聊支持流式消息 API。
*/
streaming?: boolean;
}
/**
* 出站消息合并回复配置
*/
export interface DeliverDebounceConfig {
/**
* 是否启用合并回复(默认 true
*/
enabled?: boolean;
/**
* 合并窗口时长(毫秒),在此时间内的连续 deliver 会被合并
* 默认 1500ms
*/
windowMs?: number;
/**
* 最大等待时长(毫秒),从第一条 deliver 开始计算,超过此时间强制发送
* 防止持续有新 deliver 导致一直不发送
* 默认 8000ms
*/
maxWaitMs?: number;
/**
* 合并文本之间的分隔符
* 默认 "\n\n---\n\n"
*/
separator?: string;
}
/**
* 音频格式策略:控制哪些格式可跳过转换
*/
export interface AudioFormatPolicy {
/**
* STT 模型直接支持的音频格式(入站:跳过 SILK→WAV 转换)
* 如果 STT 服务支持直接处理某些格式(如 silk/amr可将其加入此列表
* 例如: [".silk", ".amr", ".wav", ".mp3", ".ogg"]
* 默认为空(所有语音都先转换为 WAV 再送 STT
*/
sttDirectFormats?: string[];
/**
* QQ 平台支持直传的音频格式出站跳过→SILK 转换)
* 默认为 [".wav", ".mp3", ".silk"]QQ Bot API 原生支持的三种格式)
* 仅当需要覆盖默认值时才配置此项
*/
uploadDirectFormats?: string[];
/**
* 是否启用语音转码(默认 true
* 设为 false 可在环境无 ffmpeg 时跳过转码,直接以文件形式发送
* 当禁用时,非原生格式的音频会 fallback 到 sendDocument文件发送
*/
transcodeEnabled?: boolean;
}
/**
* 富媒体附件
*/
export interface MessageAttachment {
content_type: string; // 如 "image/png"
filename?: string;
height?: number;
width?: number;
size?: number;
url: string;
voice_wav_url?: string; // QQ 提供的 WAV 格式语音直链,有值时优先使用以避免 SILK→WAV 转换
asr_refer_text?: string; // QQ 事件内置 ASR 语音识别文本
}
/**
* C2C 消息事件
*/
export interface C2CMessageEvent {
author: {
id: string;
union_openid: string;
user_openid: string;
};
content: string;
id: string;
timestamp: string;
message_scene?: {
source: string;
/** ext 数组,可能包含 ref_msg_idx=REFIDX_xxx引用的消息和 msg_idx=REFIDX_xxx自身索引 */
ext?: string[];
};
attachments?: MessageAttachment[];
/** 消息类型,参见 MSG_TYPE_* */
message_type?: number;
/** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
msg_elements?: MsgElement[];
}
/**
* 频道 AT 消息事件
*/
export interface GuildMessageEvent {
id: string;
channel_id: string;
guild_id: string;
content: string;
timestamp: string;
author: {
id: string;
username?: string;
bot?: boolean;
};
member?: {
nick?: string;
joined_at?: string;
};
attachments?: MessageAttachment[];
}
/** 消息元素结点,引用消息时 msg_elements[0] 为被引用的原始消息 */
export interface MsgElement {
/** 消息索引标识 */
msg_idx?: string;
/** 消息类型,参见 MSG_TYPE_* 常量 */
message_type?: number;
/** 文本内容 */
content?: string;
/** 附件列表 */
attachments?: MessageAttachment[];
/** 嵌套消息元素(引用消息场景下可能存在) */
msg_elements?: MsgElement[];
}
/**
* 群聊 AT 消息事件
*/
export interface GroupMessageEvent {
author: {
id: string;
member_openid: string;
username?: string;
bot?: boolean;
};
content: string;
id: string;
timestamp: string;
group_id: string;
group_openid: string;
message_scene?: {
source: string;
ext?: string[];
};
attachments?: MessageAttachment[];
/** @提及列表 */
mentions?: Array<{
scope?: "all" | "single";
id?: string;
user_openid?: string;
member_openid?: string;
nickname?: string;
bot?: boolean;
/** 是否 @机器人自身 */
is_you?: boolean;
}>;
/** 消息类型,参见 MSG_TYPE_* */
message_type?: number;
/** 消息元素列表,引用消息时 [0] 为被引用的原始消息 */
msg_elements?: MsgElement[];
}
/**
* 按钮交互事件INTERACTION_CREATE
*/
export interface InteractionEvent {
/** 事件 ID用于回应交互PUT /interactions/{id} */
id: string;
/** 事件类型11=消息按钮 12=单聊快捷菜单 */
type: number;
/** 场景c2c / group / guild */
scene?: string;
/** 场景类型0=频道 1=群聊 2=单聊 */
chat_type?: number;
/** 触发时间 RFC3339 */
timestamp?: string;
/** 频道 openid仅频道场景 */
guild_id?: string;
/** 子频道 openid仅频道场景 */
channel_id?: string;
/** 单聊用户 openid仅 c2c 场景) */
user_openid?: string;
/** 群 openid仅群聊场景 */
group_openid?: string;
/** 群内触发用户 openid仅群聊场景 */
group_member_openid?: string;
version: number;
data: {
type: number;
resolved: {
/** 按钮 action.data 值 */
button_data?: string;
/** 按钮 id */
button_id?: string;
/** 操作用户 userid仅频道场景 */
user_id?: string;
/** 自定义菜单 id仅菜单场景 */
feature_id?: string;
/** 操作的消息 id仅频道场景 */
message_id?: string;
/** 配置更新:群消息模式 "mention"=@机器人时激活 "always"=总是激活 */
require_mention?: string;
/** 配置更新:群消息策略 */
group_policy?: GroupPolicy;
/** 配置更新:@文本的名称提及BOT名多个使用,分隔 */
mention_patterns?: string;
};
};
}
// ---- Keyboard 类型 ----
/**
* 按钮 Action 类型
* 0=跳转链接 1=回调型(INTERACTION_CREATE) 2=指令型(直接发文本) 3=mqqapi
*/
export type KeyboardActionType = 0 | 1 | 2 | 3;
/** 按钮权限 */
export interface KeyboardPermission {
/** 0=全体 1=管理员 2=按钮指定 3=身份组 */
type: 0 | 1 | 2 | 3;
specify_role_ids?: string[];
specify_user_ids?: string[];
}
/** 二次确认弹窗 */
export interface KeyboardModal {
content: string;
confirm_text?: string;
cancel_text?: string;
}
/** 按钮 Action */
export interface KeyboardAction {
type: KeyboardActionType;
data?: string;
/** true = 点击后直接发出Enter*/
enter?: boolean;
/** 仅指令型type=2是否把指令发到输入框reply=true还是静默发出 */
reply?: boolean;
permission?: KeyboardPermission;
click_limit?: number;
unsupport_tips?: string;
modal?: KeyboardModal;
}
/** 按钮渲染数据 */
export interface KeyboardRenderData {
label: string;
visited_label?: string;
/** 0=灰色线框 1=蓝色线框 2=推荐回复专用 3=红色字体 4=蓝色背景 */
style?: 0 | 1 | 2 | 3 | 4;
}
/** 单个按钮 */
export interface KeyboardButton {
id: string;
render_data?: KeyboardRenderData;
action?: KeyboardAction;
group_id?: string;
}
/** 一行按钮 */
export interface KeyboardRow {
buttons: KeyboardButton[];
}
/** CustomKeyboard自定义按钮内容 */
export interface CustomKeyboard {
rows: KeyboardRow[];
}
/** MessageKeyboardkeyboard / prompt_keyboard.keyboard 共用) */
export interface MessageKeyboard {
/** 模板 ID与 content 二选一) */
id?: string;
/** 自定义内容 */
content?: CustomKeyboard;
}
/**
* Inline Keyboard消息内嵌按钮需平台审核
* 发送字段keyboard
* JSON: { "keyboard": { "id": "...", "content": { "rows": [...] } } }
*/
export type InlineKeyboard = MessageKeyboard;
/**
* WebSocket 事件负载
*/
export interface WSPayload {
op: number;
d?: unknown;
s?: number;
t?: string;
}
// ---- 流式消息常量 ----
/** 流式消息输入模式 */
export const StreamInputMode = {
/** 每次发送的 content_raw 替换整条消息内容 */
REPLACE: "replace",
} as const;
export type StreamInputMode = (typeof StreamInputMode)[keyof typeof StreamInputMode];
/** 流式消息输入状态 */
export const StreamInputState = {
/** 正文生成中 */
GENERATING: 1,
/** 正文生成结束(终结状态) */
DONE: 10,
} as const;
export type StreamInputState = (typeof StreamInputState)[keyof typeof StreamInputState];
/** 流式消息内容类型 */
export const StreamContentType = {
MARKDOWN: "markdown",
} as const;
export type StreamContentType = (typeof StreamContentType)[keyof typeof StreamContentType];
/**
* 流式消息请求体
* 对应 StreamReq proto
*/
export interface StreamMessageRequest {
/** 输入模式 */
input_mode: StreamInputMode;
/** 输入状态 */
input_state: StreamInputState;
/** 内容类型 */
content_type: StreamContentType;
/** markdown 内容 */
content_raw: string;
/** 事件 ID */
event_id: string;
/** 原始消息 ID */
msg_id: string;
/** 流式消息 ID首次发送后返回后续分片需携带 */
stream_msg_id?: string;
/** 递增序号 */
msg_seq: number;
/** 同一条流式会话内的发送索引,从 0 开始,每次发送前递增;新流式会话重新从 0 开始 */
index: number;
}