@tencent-connect/openclaw-qqbot 1.7.0 → 1.7.2

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.
Files changed (45) hide show
  1. package/README.md +216 -49
  2. package/README.zh.md +216 -4
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/api.d.ts +6 -0
  6. package/dist/src/api.js +33 -4
  7. package/dist/src/approval-handler.d.ts +47 -0
  8. package/dist/src/approval-handler.js +372 -0
  9. package/dist/src/channel.js +72 -0
  10. package/dist/src/config.d.ts +5 -1
  11. package/dist/src/config.js +12 -2
  12. package/dist/src/gateway.js +175 -170
  13. package/dist/src/slash-commands.d.ts +7 -2
  14. package/dist/src/slash-commands.js +354 -3
  15. package/dist/src/tools/channel.js +1 -4
  16. package/dist/src/tools/remind.js +0 -1
  17. package/dist/src/transport/index.d.ts +10 -0
  18. package/dist/src/transport/index.js +9 -0
  19. package/dist/src/transport/webhook-transport.d.ts +67 -0
  20. package/dist/src/transport/webhook-transport.js +245 -0
  21. package/dist/src/transport/webhook-verify.d.ts +48 -0
  22. package/dist/src/transport/webhook-verify.js +98 -0
  23. package/dist/src/types.d.ts +85 -0
  24. package/dist/src/utils/audio-convert.js +37 -9
  25. package/index.ts +1 -0
  26. package/package.json +1 -1
  27. package/scripts/postinstall-link-sdk.js +44 -0
  28. package/scripts/upgrade-via-npm.sh +358 -62
  29. package/scripts/upgrade-via-source.sh +122 -85
  30. package/src/api.ts +50 -5
  31. package/src/approval-handler.ts +505 -0
  32. package/src/channel.ts +76 -0
  33. package/src/config.ts +15 -2
  34. package/src/gateway.ts +181 -169
  35. package/src/onboarding.ts +8 -0
  36. package/src/openclaw-plugin-sdk.d.ts +127 -2
  37. package/src/slash-commands.ts +390 -5
  38. package/src/tools/channel.ts +1 -7
  39. package/src/tools/remind.ts +0 -2
  40. package/src/transport/index.ts +11 -0
  41. package/src/transport/webhook-transport.ts +332 -0
  42. package/src/transport/webhook-verify.ts +119 -0
  43. package/src/types.ts +100 -1
  44. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  45. package/src/utils/audio-convert.ts +37 -9
@@ -20,6 +20,7 @@ import { saveCredentialBackup } from "./credential-backup.js";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { getPackageVersion } from "./utils/pkg-version.js";
22
22
  import { getQQBotRuntime } from "./runtime.js";
23
+ import { isApprovalFeatureAvailable } from "./approval-handler.js";
23
24
  const require = createRequire(import.meta.url);
24
25
  let PLUGIN_VERSION = getPackageVersion(import.meta.url);
25
26
  // 获取 openclaw 框架版本(不缓存,每次实时获取)
@@ -29,11 +30,16 @@ export function getFrameworkVersion() {
29
30
  // Windows 上 npm 安装的 CLI 通常是 .cmd wrapper,execFileSync 需要 shell:true 才能执行
30
31
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
31
32
  try {
32
- const out = execFileSync(cli, ["--version"], {
33
+ const rawOut = execFileSync(cli, ["--version"], {
33
34
  timeout: 3000, encoding: "utf8",
34
35
  ...(isWindows() ? { shell: true } : {}),
35
36
  }).trim();
36
37
  // 输出格式: "OpenClaw 2026.3.13 (61d171a)"
38
+ // CLI 启动时可能输出 proxy 等初始化日志到 stdout,需过滤出真正的版本行
39
+ const out = rawOut
40
+ .split("\n")
41
+ .find((line) => /^(OpenClaw|clawdbot|moltbot)\s/i.test(line))
42
+ ?.trim() ?? rawOut;
37
43
  if (out) {
38
44
  return out;
39
45
  }
@@ -239,7 +245,7 @@ registerCommand({
239
245
  ].join("\n"),
240
246
  handler: (ctx) => {
241
247
  // 群聊场景排除仅限私聊的指令
242
- const GROUP_EXCLUDED_COMMANDS = new Set(["bot-upgrade", "bot-clear-storage"]);
248
+ const GROUP_EXCLUDED_COMMANDS = new Set(["bot-upgrade", "bot-clear-storage", "bot-logs", "bot-approve", "bot-group-allways", "bot-streaming"]);
243
249
  const isGroup = ctx.type === "group";
244
250
  const lines = [`### QQBot插件内置调试指令`, ``];
245
251
  for (const [name, cmd] of commands) {
@@ -1504,7 +1510,11 @@ registerCommand({
1504
1510
  `导出最近的 OpenClaw 日志文件(最多 4 个)。`,
1505
1511
  `每个文件最多保留最后 1000 行,以文件形式返回。`,
1506
1512
  ].join("\n"),
1507
- handler: () => {
1513
+ handler: (ctx) => {
1514
+ // 日志导出仅在私聊中可用
1515
+ if (ctx.type !== "c2c") {
1516
+ return `💡 请在私聊中使用此指令`;
1517
+ }
1508
1518
  const logDirs = collectCandidateLogDirs();
1509
1519
  const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
1510
1520
  if (recentFiles.length === 0) {
@@ -1848,6 +1858,347 @@ registerCommand({
1848
1858
  }
1849
1859
  },
1850
1860
  });
1861
+ // ============ /bot-approve 审批配置管理 ============
1862
+ /**
1863
+ * /bot-approve — 管理命令执行审批配置
1864
+ *
1865
+ * 修改 openclaw.json 中 tools.exec.security / tools.exec.ask 字段。
1866
+ *
1867
+ * security: deny | allowlist | full
1868
+ * ask: off | on-miss | always
1869
+ */
1870
+ registerCommand({
1871
+ name: "bot-approve",
1872
+ description: "管理命令执行审批配置",
1873
+ usage: [
1874
+ `/bot-approve 查看操作指引`,
1875
+ `/bot-approve on 开启审批(白名单模式,推荐)`,
1876
+ `/bot-approve off 关闭审批,命令直接执行`,
1877
+ `/bot-approve always 始终审批,每次执行都需审批`,
1878
+ `/bot-approve reset 恢复框架默认值`,
1879
+ `/bot-approve status 查看当前审批配置`,
1880
+ ].join("\n"),
1881
+ handler: async (ctx) => {
1882
+ // 审批管理仅在私聊中可用
1883
+ if (ctx.type !== "c2c") {
1884
+ return `💡 请在私聊中使用此指令`;
1885
+ }
1886
+ const arg = ctx.args.trim().toLowerCase();
1887
+ // 审批功能需要 openclaw >= 3.22(gateway-runtime 模块)
1888
+ if (!isApprovalFeatureAvailable()) {
1889
+ return `❌ 当前 OpenClaw 版本不支持审批功能,请升级至最新版本后重试。`;
1890
+ }
1891
+ let runtime;
1892
+ try {
1893
+ runtime = getQQBotRuntime();
1894
+ }
1895
+ catch {
1896
+ // runtime 不可用,构造 prompt 委托给 AI 模型通过 CLI 完成
1897
+ const presetMap = {
1898
+ on: { security: "allowlist", ask: "on-miss", desc: "开启审批(白名单模式)" },
1899
+ off: { security: "full", ask: "off", desc: "关闭审批" },
1900
+ always: { security: "allowlist", ask: "always", desc: "严格模式(每次都审批)" },
1901
+ strict: { security: "allowlist", ask: "always", desc: "严格模式(每次都审批)" },
1902
+ };
1903
+ const preset = presetMap[arg];
1904
+ if (preset) {
1905
+ return {
1906
+ delegatePrompt: [
1907
+ `请帮我${preset.desc}。执行以下单条命令(不要拆开):`,
1908
+ `openclaw config set tools.exec.security ${preset.security} && openclaw config set tools.exec.ask ${preset.ask}`,
1909
+ `执行完成后告诉用户审批配置已更新为 security=${preset.security}, ask=${preset.ask}。`,
1910
+ ].join("\n"),
1911
+ };
1912
+ }
1913
+ if (arg === "reset") {
1914
+ return {
1915
+ delegatePrompt: [
1916
+ `请帮我重置审批配置。执行以下单条命令(不要拆开):`,
1917
+ `openclaw config unset tools.exec.security && openclaw config unset tools.exec.ask`,
1918
+ `执行完成后告诉用户审批配置已重置为框架默认值。`,
1919
+ ].join("\n"),
1920
+ };
1921
+ }
1922
+ if (arg === "status") {
1923
+ return {
1924
+ delegatePrompt: [
1925
+ `请帮我查看当前命令执行审批配置。执行以下单条命令(不要拆开):`,
1926
+ `echo "security=$(openclaw config get tools.exec.security)" && echo "ask=$(openclaw config get tools.exec.ask)"`,
1927
+ `然后告诉用户当前 security 和 ask 的值,以及可用的操作选项:`,
1928
+ `- /bot-approve on 开启审批(白名单模式)`,
1929
+ `- /bot-approve off 关闭审批`,
1930
+ `- /bot-approve always 严格模式`,
1931
+ `- /bot-approve reset 恢复默认`,
1932
+ ].join("\n"),
1933
+ };
1934
+ }
1935
+ // 无参数或未知参数:直接返回操作指引
1936
+ return [
1937
+ `🔐 命令执行审批配置`,
1938
+ ``,
1939
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
1940
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
1941
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
1942
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
1943
+ `<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`,
1944
+ ].join("\n");
1945
+ }
1946
+ const configApi = runtime.config;
1947
+ const loadExecConfig = () => {
1948
+ const cfg = configApi.loadConfig();
1949
+ const tools = (cfg.tools ?? {});
1950
+ const exec = (tools.exec ?? {});
1951
+ return {
1952
+ security: String(exec.security ?? "deny"),
1953
+ ask: String(exec.ask ?? "on-miss"),
1954
+ };
1955
+ };
1956
+ const writeExecConfig = async (security, ask) => {
1957
+ const cfg = structuredClone(configApi.loadConfig());
1958
+ const tools = (cfg.tools ?? {});
1959
+ const exec = (tools.exec ?? {});
1960
+ exec.security = security;
1961
+ exec.ask = ask;
1962
+ tools.exec = exec;
1963
+ cfg.tools = tools;
1964
+ await configApi.writeConfigFile(cfg);
1965
+ };
1966
+ const formatStatus = (security, ask) => {
1967
+ const secIcon = security === "full" ? "🟢" : security === "allowlist" ? "🟡" : "🔴";
1968
+ const askIcon = ask === "off" ? "🟢" : ask === "always" ? "🔴" : "🟡";
1969
+ return [
1970
+ `🔐 当前审批配置`,
1971
+ ``,
1972
+ `${secIcon} 安全模式 (security): **${security}**`,
1973
+ `${askIcon} 审批模式 (ask): **${ask}**`,
1974
+ ``,
1975
+ security === "deny" ? `⚠️ 当前为 deny 模式,所有命令执行被拒绝` :
1976
+ security === "full" && ask === "off" ? `✅ 所有命令无需审批直接执行` :
1977
+ security === "allowlist" && ask === "on-miss" ? `🛡️ 白名单命令直接执行,其余需审批` :
1978
+ ask === "always" ? `🔒 每次命令执行都需要人工审批` :
1979
+ `ℹ️ security=${security}, ask=${ask}`,
1980
+ ].join("\n");
1981
+ };
1982
+ // 无参数:操作指引
1983
+ if (!arg) {
1984
+ return [
1985
+ `🔐 命令执行审批配置`,
1986
+ ``,
1987
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
1988
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
1989
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
1990
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
1991
+ `<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`,
1992
+ ].join("\n");
1993
+ }
1994
+ // status: 查看当前配置
1995
+ if (arg === "status") {
1996
+ const { security, ask } = loadExecConfig();
1997
+ return [
1998
+ formatStatus(security, ask),
1999
+ ``,
2000
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批`,
2001
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
2002
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
2003
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
2004
+ ].join("\n");
2005
+ }
2006
+ // on: 开启审批(白名单 + 未命中审批)
2007
+ if (arg === "on") {
2008
+ try {
2009
+ await writeExecConfig("allowlist", "on-miss");
2010
+ return [
2011
+ `✅ 审批已开启`,
2012
+ ``,
2013
+ `• security = allowlist(白名单模式)`,
2014
+ `• ask = on-miss(未命中白名单时需审批)`,
2015
+ ``,
2016
+ `已批准的命令自动加入白名单,下次直接执行。`,
2017
+ ].join("\n");
2018
+ }
2019
+ catch (err) {
2020
+ return `❌ 配置更新失败: ${err}`;
2021
+ }
2022
+ }
2023
+ // off: 关闭审批
2024
+ if (arg === "off") {
2025
+ try {
2026
+ await writeExecConfig("full", "off");
2027
+ return [
2028
+ `✅ 审批已关闭`,
2029
+ ``,
2030
+ `• security = full(允许所有命令)`,
2031
+ `• ask = off(不需要审批)`,
2032
+ ``,
2033
+ `⚠️ 所有命令将直接执行,不会弹出审批确认。`,
2034
+ ].join("\n");
2035
+ }
2036
+ catch (err) {
2037
+ return `❌ 配置更新失败: ${err}`;
2038
+ }
2039
+ }
2040
+ // always: 始终审批(每次都审批)
2041
+ if (arg === "always") {
2042
+ try {
2043
+ await writeExecConfig("allowlist", "always");
2044
+ return [
2045
+ `✅ 已切换为严格审批模式`,
2046
+ ``,
2047
+ `• security = allowlist`,
2048
+ `• ask = always(每次执行都需审批)`,
2049
+ ``,
2050
+ `每个命令都会弹出审批按钮,需手动确认。`,
2051
+ ].join("\n");
2052
+ }
2053
+ catch (err) {
2054
+ return `❌ 配置更新失败: ${err}`;
2055
+ }
2056
+ }
2057
+ // reset: 删除配置,恢复框架默认值
2058
+ if (arg === "reset") {
2059
+ try {
2060
+ const cfg = structuredClone(configApi.loadConfig());
2061
+ const tools = (cfg.tools ?? {});
2062
+ const exec = (tools.exec ?? {});
2063
+ delete exec.security;
2064
+ delete exec.ask;
2065
+ if (Object.keys(exec).length === 0) {
2066
+ delete tools.exec;
2067
+ }
2068
+ else {
2069
+ tools.exec = exec;
2070
+ }
2071
+ if (Object.keys(tools).length === 0) {
2072
+ delete cfg.tools;
2073
+ }
2074
+ else {
2075
+ cfg.tools = tools;
2076
+ }
2077
+ await configApi.writeConfigFile(cfg);
2078
+ return [
2079
+ `✅ 审批配置已重置`,
2080
+ ``,
2081
+ `已移除 tools.exec.security 和 tools.exec.ask`,
2082
+ `框架将使用默认值(security=deny, ask=on-miss)`,
2083
+ ``,
2084
+ `如需开启命令执行,请使用 /bot-approve on`,
2085
+ ].join("\n");
2086
+ }
2087
+ catch (err) {
2088
+ return `❌ 配置更新失败: ${err}`;
2089
+ }
2090
+ }
2091
+ return [
2092
+ `❌ 未知参数: ${arg}`,
2093
+ ``,
2094
+ `可用选项: on | off | always | reset`,
2095
+ `输入 /bot-approve ? 查看详细用法`,
2096
+ ].join("\n");
2097
+ },
2098
+ });
2099
+ // ============ /bot-group-allways ============
2100
+ /**
2101
+ * /bot-group-allways on|off — 修改群消息默认响应模式
2102
+ *
2103
+ * 直接修改当前账户的 defaultRequireMention 配置项并持久化到 openclaw.json。
2104
+ * 修改后即时生效(下一条消息起按新配置处理)。
2105
+ *
2106
+ * on = AI 自主判断何时发言(无需 @),off = 仅被 @ 时回复
2107
+ */
2108
+ registerCommand({
2109
+ name: "bot-group-allways",
2110
+ description: "修改群消息默认响应模式",
2111
+ usage: [
2112
+ `/bot-group-allways on AI 自主判断何时发言(无需 @)`,
2113
+ `/bot-group-allways off 仅在被 @ 时回复`,
2114
+ `/bot-group-allways 查看当前设置`,
2115
+ ``,
2116
+ `设为 on 后,AI 会自主判断每条消息是否需要回复(无需 @)。`,
2117
+ `仍可通过 groups.{groupId}.requireMention 对单个群覆盖。`,
2118
+ ``,
2119
+ `优先级:具体群配置 > 通配符 "*" > defaultRequireMention(本指令)> 默认 true`,
2120
+ ].join("\n"),
2121
+ handler: async (ctx) => {
2122
+ // 群响应模式设置仅在私聊中可用
2123
+ if (ctx.type !== "c2c") {
2124
+ return `💡 请在私聊中使用此指令`;
2125
+ }
2126
+ const arg = ctx.args.trim().toLowerCase();
2127
+ // 读取当前 defaultRequireMention 状态
2128
+ const currentVal = ctx.accountConfig?.defaultRequireMention;
2129
+ const currentRequireMention = currentVal ?? true; // 未设置时硬编码默认为 true
2130
+ // 无参数:查看当前状态
2131
+ if (!arg) {
2132
+ return [
2133
+ `🤖 群自主发言状态:${currentRequireMention ? "❌ 仅被 @ 时回复" : "✅ 自主判断何时发言"}`,
2134
+ `使用 <qqbot-cmd-input text="/bot-group-allways on" show="/bot-group-allways on"/> 设为自主发言`,
2135
+ `使用 <qqbot-cmd-input text="/bot-group-allways off" show="/bot-group-allways off"/> 设为仅被 @ 时回复`,
2136
+ ].join("\n");
2137
+ }
2138
+ if (arg !== "on" && arg !== "off") {
2139
+ return `❌ 参数错误,请使用 on 或 off\n\n示例:/bot-group-allways on`;
2140
+ }
2141
+ const newRequireMention = arg === "off"; // on=自主发言(requireMention=false), off=仅被@时回复(requireMention=true)
2142
+ // 如果状态没变,直接返回
2143
+ if (newRequireMention === currentRequireMention) {
2144
+ return `🤖 群自主发言已经是"${arg}"状态,无需操作`;
2145
+ }
2146
+ // 更新配置
2147
+ try {
2148
+ const runtime = getQQBotRuntime();
2149
+ const configApi = runtime.config;
2150
+ const currentCfg = structuredClone(configApi.loadConfig());
2151
+ const qqbot = (currentCfg.channels ?? {}).qqbot;
2152
+ if (!qqbot) {
2153
+ return `❌ 配置文件中未找到 qqbot 通道配置`;
2154
+ }
2155
+ const accountId = ctx.accountId;
2156
+ const isNamedAccount = accountId !== "default" && qqbot.accounts?.[accountId];
2157
+ if (isNamedAccount) {
2158
+ // 命名账户:更新 accounts.{accountId}.defaultRequireMention
2159
+ const accounts = qqbot.accounts;
2160
+ const acct = accounts[accountId] ?? {};
2161
+ acct.defaultRequireMention = newRequireMention;
2162
+ accounts[accountId] = acct;
2163
+ qqbot.accounts = accounts;
2164
+ }
2165
+ else {
2166
+ // 默认账户:更新 qqbot.defaultRequireMention
2167
+ qqbot.defaultRequireMention = newRequireMention;
2168
+ }
2169
+ await configApi.writeConfigFile(currentCfg);
2170
+ return [
2171
+ `✅ 群自主发言已设置为 ${newRequireMention ? "**off**(仅被 @ 时回复)" : "**on**(AI 自主判断何时发言)"}`,
2172
+ ``,
2173
+ newRequireMention
2174
+ ? `仅在被 @ 机器人才会回复。`
2175
+ : `AI 将自主判断群消息是否需要回复,无需被 @ 即可发言。`,
2176
+ ``,
2177
+ ].join("\n");
2178
+ }
2179
+ catch (err) {
2180
+ const fwVer = getFrameworkVersion();
2181
+ return [
2182
+ `❌ 当前版本不支持该指令`,
2183
+ ``,
2184
+ `🦞框架版本:${fwVer}`,
2185
+ `🤖QQBot 插件版本:v${PLUGIN_VERSION}`,
2186
+ ``,
2187
+ `可通过以下命令手动设置:`,
2188
+ ``,
2189
+ `\`\`\`shell`,
2190
+ `# 设为 AI 自主判断何时发言(defaultRequireMention=false)`,
2191
+ `openclaw config set channels.qqbot.defaultRequireMention false`,
2192
+ `# 或设为仅被 @ 时回复(defaultRequireMention=true)`,
2193
+ `openclaw config set channels.qqbot.defaultRequireMention true`,
2194
+ ``,
2195
+ `# 重启网关使配置生效`,
2196
+ `openclaw gateway restart`,
2197
+ `\`\`\``,
2198
+ ].join("\n");
2199
+ }
2200
+ },
2201
+ });
1851
2202
  // ============ 匹配入口 ============
1852
2203
  /**
1853
2204
  * 尝试匹配并执行插件级斜杠指令
@@ -1,8 +1,6 @@
1
1
  import { resolveQQBotAccount } from "../config.js";
2
2
  import { listQQBotAccountIds } from "../config.js";
3
- import { getAccessToken } from "../api.js";
4
- // ========== 常量 ==========
5
- const API_BASE = "https://api.sgroup.qq.com";
3
+ import { getAccessToken, API_BASE } from "../api.js";
6
4
  const DEFAULT_TIMEOUT_MS = 30000;
7
5
  // ========== JSON Schema ==========
8
6
  const ChannelApiSchema = {
@@ -230,5 +228,4 @@ export function registerChannelTool(api) {
230
228
  }
231
229
  },
232
230
  }, { name: "qqbot_channel_api" });
233
- console.log("[qqbot-channel-api] Registered QQ channel API proxy tool");
234
231
  }
@@ -252,5 +252,4 @@ export function registerRemindTool(api) {
252
252
  });
253
253
  },
254
254
  }, { name: "qqbot_remind" });
255
- console.log("[qqbot-remind] Registered QQBot remind tool");
256
255
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Transport module — QQ Bot event receiving mechanisms.
3
+ *
4
+ * Supports two transport modes:
5
+ * - **WebSocket** (default): long-lived WS connection with heartbeat, RESUME, etc.
6
+ * - **Webhook** (HTTP callback): QQ platform POSTs events to registered path.
7
+ */
8
+ export type { WebhookInboundEvent, WebhookTransportOptions, QQBotWebhookTarget } from "./webhook-transport.js";
9
+ export { startWebhookTransport, resolveWebhookPath } from "./webhook-transport.js";
10
+ export { verifyWebhookSignature, signValidationResponse, ed25519Sign } from "./webhook-verify.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Transport module — QQ Bot event receiving mechanisms.
3
+ *
4
+ * Supports two transport modes:
5
+ * - **WebSocket** (default): long-lived WS connection with heartbeat, RESUME, etc.
6
+ * - **Webhook** (HTTP callback): QQ platform POSTs events to registered path.
7
+ */
8
+ export { startWebhookTransport, resolveWebhookPath } from "./webhook-transport.js";
9
+ export { verifyWebhookSignature, signValidationResponse, ed25519Sign } from "./webhook-verify.js";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Webhook Transport — receive QQ Bot events via HTTP POST callbacks.
3
+ *
4
+ * Uses OpenClaw plugin-sdk's `registerWebhookTargetWithPluginRoute` (模式 C)
5
+ * to register webhook HTTP routes through the framework's gateway HTTP server,
6
+ * sharing the same port and benefiting from built-in rate limiting & in-flight guards.
7
+ *
8
+ * Architecture:
9
+ * 1. On gateway startAccount, register a webhook target via plugin-sdk
10
+ * 2. Framework routes POST requests to our handler
11
+ * 3. Handler verifies Ed25519 signatures and dispatches events
12
+ * 4. Returns op:12 ACK immediately, processes events asynchronously
13
+ * 5. On account stop (abortSignal), unregister the target
14
+ *
15
+ * Configuration (openclaw.yaml):
16
+ * ```yaml
17
+ * channels:
18
+ * qqbot:
19
+ * appId: "xxx"
20
+ * clientSecret: "xxx"
21
+ * transport: webhook
22
+ * webhook:
23
+ * path: /qqbot/webhook
24
+ * ```
25
+ */
26
+ import type { ResolvedQQBotAccount } from "../types.js";
27
+ /** Webhook target registered per account */
28
+ export interface QQBotWebhookTarget {
29
+ path: string;
30
+ accountId: string;
31
+ appId: string;
32
+ clientSecret: string;
33
+ }
34
+ /** Webhook event dispatched to the consumer */
35
+ export interface WebhookInboundEvent {
36
+ eventType: string;
37
+ data: unknown;
38
+ seq?: number;
39
+ }
40
+ /** Options for starting webhook transport */
41
+ export interface WebhookTransportOptions {
42
+ account: ResolvedQQBotAccount;
43
+ abortSignal: AbortSignal;
44
+ onEvent: (event: WebhookInboundEvent) => void | Promise<void>;
45
+ onReady?: () => void;
46
+ onError?: (error: Error) => void;
47
+ log?: {
48
+ info: (msg: string) => void;
49
+ warn?: (msg: string) => void;
50
+ error: (msg: string) => void;
51
+ debug?: (msg: string) => void;
52
+ };
53
+ }
54
+ /**
55
+ * Start the webhook transport for a given account.
56
+ *
57
+ * Registers a webhook target on the framework's plugin HTTP route system.
58
+ * The handler verifies Ed25519 signatures, ACKs immediately, and dispatches
59
+ * events asynchronously via the provided `onEvent` callback.
60
+ *
61
+ * Returns when the abortSignal is triggered (account stopped).
62
+ */
63
+ export declare function startWebhookTransport(opts: WebhookTransportOptions): Promise<void>;
64
+ /**
65
+ * Resolve the webhook path for a given account (for external configuration / setWebhook calls).
66
+ */
67
+ export declare function resolveWebhookPath(account: ResolvedQQBotAccount): string;