@jerryliang122/openclaw-qqbot 1.0.0

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 (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +967 -0
  3. package/README.zh.md +790 -0
  4. package/dist/index.cjs +18072 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +1222 -0
  7. package/index.ts +92 -0
  8. package/openclaw.plugin.json +38 -0
  9. package/package.json +69 -0
  10. package/preload.cjs +19 -0
  11. package/scripts/link-sdk-core.cjs +268 -0
  12. package/scripts/proactive-api-server.ts +369 -0
  13. package/scripts/send-proactive.ts +293 -0
  14. package/scripts/test-sendmedia.ts +116 -0
  15. package/skills/qqbot-channel/SKILL.md +285 -0
  16. package/skills/qqbot-channel/references/api_references.md +521 -0
  17. package/skills/qqbot-remind/SKILL.md +159 -0
  18. package/skills/qqbot-upgrade/SKILL.md +56 -0
  19. package/src/adapter/contract.ts +63 -0
  20. package/src/adapter/lint.ts +144 -0
  21. package/src/adapter/media.ts +40 -0
  22. package/src/adapter/pairing.ts +95 -0
  23. package/src/adapter/resolve.ts +255 -0
  24. package/src/adapter/setup.ts +13 -0
  25. package/src/adapter/webhook.ts +248 -0
  26. package/src/adapter/workspace.ts +21 -0
  27. package/src/agent-prompt-adapter.ts +26 -0
  28. package/src/bot-instance.ts +60 -0
  29. package/src/channel.ts +230 -0
  30. package/src/commands/bot-approve.ts +143 -0
  31. package/src/commands/bot-clear-storage.ts +114 -0
  32. package/src/commands/bot-group-always.ts +62 -0
  33. package/src/commands/bot-group-info.ts +48 -0
  34. package/src/commands/bot-help.ts +40 -0
  35. package/src/commands/bot-logs.ts +248 -0
  36. package/src/commands/bot-me.ts +18 -0
  37. package/src/commands/bot-pairing.ts +50 -0
  38. package/src/commands/bot-ping.ts +33 -0
  39. package/src/commands/bot-streaming.ts +55 -0
  40. package/src/commands/bot-upgrade.ts +56 -0
  41. package/src/commands/bot-version.ts +41 -0
  42. package/src/commands/config-util.ts +96 -0
  43. package/src/commands/index.ts +51 -0
  44. package/src/config.ts +403 -0
  45. package/src/constants.ts +6 -0
  46. package/src/dispatch/body-assembler.ts +308 -0
  47. package/src/dispatch/ctx-builder.ts +127 -0
  48. package/src/dispatch/dispatch.ts +667 -0
  49. package/src/dispatch/envelope-builder.ts +112 -0
  50. package/src/dispatch/index.ts +2 -0
  51. package/src/features/approval-capability.ts +302 -0
  52. package/src/features/approval-helpers.ts +271 -0
  53. package/src/features/approval-utils.ts +21 -0
  54. package/src/features/command-panel.ts +301 -0
  55. package/src/features/credential-backup.ts +74 -0
  56. package/src/features/group-mode-store.ts +79 -0
  57. package/src/features/history-store.ts +75 -0
  58. package/src/features/msgid-cache.ts +55 -0
  59. package/src/features/outbound-echo-store.ts +46 -0
  60. package/src/features/proactive-budget.ts +57 -0
  61. package/src/features/proactive.ts +549 -0
  62. package/src/features/question-helpers.ts +771 -0
  63. package/src/features/quota-manager.ts +173 -0
  64. package/src/features/ref-index-store.ts +289 -0
  65. package/src/features/secret-input-store.ts +118 -0
  66. package/src/features/secret-store-cli.ts +324 -0
  67. package/src/features/typing-refresh.ts +51 -0
  68. package/src/features/update-checker.ts +166 -0
  69. package/src/gateway/event-handlers.ts +456 -0
  70. package/src/gateway/index.ts +3 -0
  71. package/src/gateway/lifecycle.ts +236 -0
  72. package/src/gateway/middleware-setup.ts +173 -0
  73. package/src/gateway/qqbot-gateway.ts +458 -0
  74. package/src/gateway-adapter.ts +44 -0
  75. package/src/heartbeat-adapter.ts +57 -0
  76. package/src/message-adapter.ts +40 -0
  77. package/src/messaging-adapter.ts +78 -0
  78. package/src/middleware/access-control.ts +125 -0
  79. package/src/middleware/attachment.ts +373 -0
  80. package/src/middleware/inbound-guard.ts +102 -0
  81. package/src/middleware/policy-injector.ts +71 -0
  82. package/src/middleware/secret-capture.ts +161 -0
  83. package/src/middleware/typing.ts +110 -0
  84. package/src/openclaw-plugin-sdk.d.ts +543 -0
  85. package/src/outbound/chunker.ts +80 -0
  86. package/src/outbound/debounce.ts +102 -0
  87. package/src/outbound/deliver-pipeline.ts +235 -0
  88. package/src/outbound/index.ts +3 -0
  89. package/src/outbound/local-file-router.ts +145 -0
  90. package/src/outbound/media-send.ts +408 -0
  91. package/src/outbound/outbound-service.ts +298 -0
  92. package/src/outbound/reply-limiter.ts +139 -0
  93. package/src/outbound/sanitize.ts +32 -0
  94. package/src/outbound/streaming-controller.ts +332 -0
  95. package/src/outbound/target.ts +109 -0
  96. package/src/outbound-adapter.ts +323 -0
  97. package/src/plugin-base.ts +42 -0
  98. package/src/request-context.ts +50 -0
  99. package/src/runtime.ts +42 -0
  100. package/src/setup/account-key.ts +41 -0
  101. package/src/setup/finalize.ts +110 -0
  102. package/src/setup/login.ts +197 -0
  103. package/src/setup/surface.ts +40 -0
  104. package/src/status-adapter.ts +56 -0
  105. package/src/tools/platform.ts +149 -0
  106. package/src/tools/remind.ts +308 -0
  107. package/src/tools/secret-input.ts +185 -0
  108. package/src/types-augment.d.ts +54 -0
  109. package/src/types-plugin.ts +82 -0
  110. package/src/types.ts +620 -0
  111. package/src/typing-lifecycle.ts +182 -0
  112. package/src/utils/mention.ts +52 -0
  113. package/src/utils/pkg-version.ts +23 -0
  114. package/src/utils/platform.ts +459 -0
  115. package/src/utils/plugin-logger.ts +104 -0
  116. package/src/utils/ssrf-guard.ts +132 -0
  117. package/src/utils/stt.ts +150 -0
  118. package/src/utils/voice-text.ts +61 -0
  119. package/tsconfig.json +17 -0
  120. package/tsup.config.ts +64 -0
@@ -0,0 +1,308 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { getRequestTarget, getRequestAccountId } from "../request-context.js";
3
+
4
+ // ========== 类型定义 ==========
5
+
6
+ interface RemindParams {
7
+ action: "add" | "list" | "remove";
8
+ /** 提醒内容(action=add 时必填) */
9
+ content?: string;
10
+ /**
11
+ * 投递目标地址(可选,系统会自动从当前会话上下文获取)。
12
+ * 仅在需要手动指定时填写。
13
+ */
14
+ to?: string;
15
+ /**
16
+ * 时间描述(action=add 时必填)
17
+ * - 一次性:相对时间如 "5m"、"1h30m"、"2h",或绝对毫秒时间戳
18
+ * - 周期性:cron 表达式如 "0 8 * * *"
19
+ */
20
+ time?: string;
21
+ /** 时区(周期提醒时使用,默认 Asia/Shanghai) */
22
+ timezone?: string;
23
+ /** 提醒名称(可选,默认自动生成) */
24
+ name?: string;
25
+ /** jobId(action=remove 时必填) */
26
+ jobId?: string;
27
+ }
28
+
29
+ // ========== JSON Schema ==========
30
+
31
+ const RemindSchema = {
32
+ type: "object",
33
+ properties: {
34
+ action: {
35
+ type: "string",
36
+ description:
37
+ "操作类型。add=创建提醒, list=查看已有提醒, remove=删除提醒",
38
+ enum: ["add", "list", "remove"],
39
+ },
40
+ content: {
41
+ type: "string",
42
+ description: '提醒内容,如"喝水"、"开会"。action=add 时必填。',
43
+ },
44
+ to: {
45
+ type: "string",
46
+ description:
47
+ "投递目标地址(可选)。系统会自动从当前会话获取,通常无需手动填写。" +
48
+ "私聊格式:qqbot:c2c:user_openid,群聊格式:qqbot:group:group_openid。",
49
+ },
50
+ time: {
51
+ type: "string",
52
+ description:
53
+ "时间描述。支持两种格式:\n" +
54
+ "1. 相对时间:如 \"5m\"(5分钟后)、\"1h\"(1小时后)、\"1h30m\"(1.5小时后)、\"2d\"(2天后)\n" +
55
+ "2. cron 表达式:如 \"0 8 * * *\"(每天8点)、\"0 9 * * 1-5\"(工作日9点)\n" +
56
+ "系统会自动判断:包含空格的视为 cron 表达式(周期提醒),否则视为相对时间(一次性提醒)。\n" +
57
+ "action=add 时必填。",
58
+ },
59
+ timezone: {
60
+ type: "string",
61
+ description:
62
+ "时区,仅周期提醒(cron)时需要。默认 \"Asia/Shanghai\"。",
63
+ },
64
+ name: {
65
+ type: "string",
66
+ description: "提醒任务名称(可选)。默认自动从 content 截取前 20 字。",
67
+ },
68
+ jobId: {
69
+ type: "string",
70
+ description: "要删除的任务 ID。action=remove 时必填,先用 list 获取。",
71
+ },
72
+ },
73
+ required: ["action"],
74
+ } as const;
75
+
76
+ // ========== 工具函数 ==========
77
+
78
+ function json(data: unknown) {
79
+ return {
80
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
81
+ details: data,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * 解析相对时间字符串为毫秒数
87
+ * 支持格式:5m, 1h, 1h30m, 2d, 30s, 1d2h30m 等
88
+ */
89
+ function parseRelativeTime(timeStr: string): number | null {
90
+ const s = timeStr.trim().toLowerCase();
91
+
92
+ // 纯数字 → 视为分钟
93
+ if (/^\d+$/.test(s)) {
94
+ return parseInt(s, 10) * 60_000;
95
+ }
96
+
97
+ let totalMs = 0;
98
+ let matched = false;
99
+ const regex = /(\d+(?:\.\d+)?)\s*(d|h|m|s)/g;
100
+ let match: RegExpExecArray | null;
101
+
102
+ while ((match = regex.exec(s)) !== null) {
103
+ matched = true;
104
+ const value = parseFloat(match[1]);
105
+ const unit = match[2];
106
+ switch (unit) {
107
+ case "d": totalMs += value * 86_400_000; break;
108
+ case "h": totalMs += value * 3_600_000; break;
109
+ case "m": totalMs += value * 60_000; break;
110
+ case "s": totalMs += value * 1_000; break;
111
+ }
112
+ }
113
+
114
+ return matched ? Math.round(totalMs) : null;
115
+ }
116
+
117
+ /**
118
+ * 判断是否为 cron 表达式(包含空格且有 3~6 段)
119
+ */
120
+ function isCronExpression(timeStr: string): boolean {
121
+ const parts = timeStr.trim().split(/\s+/);
122
+ return parts.length >= 3 && parts.length <= 6;
123
+ }
124
+
125
+ /**
126
+ * 自动生成任务名称
127
+ */
128
+ function generateJobName(content: string): string {
129
+ const trimmed = content.trim();
130
+ const short = trimmed.length > 20 ? trimmed.slice(0, 20) + "…" : trimmed;
131
+ return `提醒: ${short}`;
132
+ }
133
+
134
+ /**
135
+ * 构建一次性提醒的 cron 工具参数
136
+ */
137
+ function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
138
+ const atMs = Date.now() + delayMs;
139
+ const content = params.content!;
140
+ const name = params.name || generateJobName(content);
141
+
142
+ return {
143
+ action: "add",
144
+ job: {
145
+ name,
146
+ schedule: { kind: "at", atMs },
147
+ sessionTarget: "isolated",
148
+ wakeMode: "now",
149
+ deleteAfterRun: true,
150
+ payload: {
151
+ kind: "agentTurn",
152
+ message: buildReminderPrompt(content),
153
+ },
154
+ delivery: {
155
+ mode: "announce",
156
+ channel: "qqbot",
157
+ to,
158
+ accountId,
159
+ },
160
+ },
161
+ };
162
+ }
163
+
164
+ /**
165
+ * 构建周期提醒的 cron 工具参数
166
+ */
167
+ function buildCronJob(params: RemindParams, to: string, accountId: string) {
168
+ const content = params.content!;
169
+ const name = params.name || generateJobName(content);
170
+ const tz = params.timezone || "Asia/Shanghai";
171
+
172
+ return {
173
+ action: "add",
174
+ job: {
175
+ name,
176
+ schedule: { kind: "cron", expr: params.time!.trim(), tz },
177
+ sessionTarget: "isolated",
178
+ wakeMode: "now",
179
+ payload: {
180
+ kind: "agentTurn",
181
+ message: buildReminderPrompt(content),
182
+ },
183
+ delivery: {
184
+ mode: "announce",
185
+ channel: "qqbot",
186
+ to,
187
+ accountId,
188
+ },
189
+ },
190
+ };
191
+ }
192
+
193
+ /**
194
+ * 构建提醒 payload 中的 AI prompt
195
+ */
196
+ function buildReminderPrompt(content: string): string {
197
+ return (
198
+ `你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:${content}。` +
199
+ `要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 ` +
200
+ `(3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 ` +
201
+ `(5) 控制在2-3句话以内 (6) 用emoji点缀`
202
+ );
203
+ }
204
+
205
+ /**
206
+ * 格式化延迟时间为人类可读文本
207
+ */
208
+ function formatDelay(ms: number): string {
209
+ const totalSeconds = Math.round(ms / 1000);
210
+ if (totalSeconds < 60) return `${totalSeconds}秒`;
211
+ const totalMinutes = Math.round(ms / 60_000);
212
+ if (totalMinutes < 60) return `${totalMinutes}分钟`;
213
+ const hours = Math.floor(totalMinutes / 60);
214
+ const minutes = totalMinutes % 60;
215
+ if (minutes === 0) return `${hours}小时`;
216
+ return `${hours}小时${minutes}分钟`;
217
+ }
218
+
219
+ // ========== 注册入口 ==========
220
+
221
+ export function registerRemindTool(api: OpenClawPluginApi): void {
222
+ api.registerTool(
223
+ {
224
+ name: "qqbot_remind",
225
+ label: "QQBot 定时提醒",
226
+ description:
227
+ "创建、查询、删除 QQ 定时提醒。" +
228
+ "使用简单参数即可,无需手动构造 cron JSON。\n" +
229
+ "创建提醒:action=add, content=提醒内容, to=目标地址, time=时间\n" +
230
+ "查看提醒:action=list\n" +
231
+ "删除提醒:action=remove, jobId=任务ID(先 list 获取)\n" +
232
+ "时间格式示例:\"5m\"(5分钟后) \"1h\"(1小时后) \"0 8 * * *\"(每天8点)",
233
+ parameters: RemindSchema,
234
+ execute(_toolCallId, params) {
235
+ const p = params as RemindParams;
236
+
237
+ // ===== list =====
238
+ if (p.action === "list") {
239
+ return json({
240
+ _instruction: "请立即使用 cron 工具执行以下参数",
241
+ cronParams: { action: "list" },
242
+ });
243
+ }
244
+
245
+ // ===== remove =====
246
+ if (p.action === "remove") {
247
+ if (!p.jobId) {
248
+ return json({ error: "action=remove 时 jobId 为必填参数。请先用 action=list 获取 jobId。" });
249
+ }
250
+ return json({
251
+ _instruction: "请立即使用 cron 工具执行以下参数",
252
+ cronParams: { action: "remove", jobId: p.jobId },
253
+ });
254
+ }
255
+
256
+ // ===== add =====
257
+ if (!p.content) {
258
+ return json({ error: "action=add 时 content(提醒内容)为必填参数" });
259
+ }
260
+ // 优先使用 AI 传入的 to,否则自动从请求级上下文获取(AsyncLocalStorage)
261
+ const resolvedTo = p.to || getRequestTarget();
262
+ if (!resolvedTo) {
263
+ return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
264
+ }
265
+ // 从请求上下文获取当前账户 ID(多账户场景),fallback 到 "default"
266
+ const resolvedAccountId = getRequestAccountId() || "default";
267
+ if (!p.time) {
268
+ return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
269
+ }
270
+
271
+ // 判断是 cron 表达式还是相对时间
272
+ if (isCronExpression(p.time)) {
273
+ // 周期提醒
274
+ const cronJob = buildCronJob(p, resolvedTo, resolvedAccountId);
275
+ return json({
276
+ _instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
277
+ cronParams: cronJob,
278
+ summary: `⏰ 周期提醒: "${p.content}" (${p.time}, tz=${p.timezone || "Asia/Shanghai"})`,
279
+ });
280
+ }
281
+
282
+ // 一次性提醒
283
+ const delayMs = parseRelativeTime(p.time);
284
+ if (!delayMs || delayMs <= 0) {
285
+ return json({
286
+ error: `无法解析时间 "${p.time}"。支持格式:` +
287
+ `相对时间如 "5m"、"1h"、"1h30m"、"2d";` +
288
+ `cron 表达式如 "0 8 * * *"(每天8点)`,
289
+ });
290
+ }
291
+
292
+ if (delayMs < 30_000) {
293
+ return json({ error: "提醒时间不能少于 30 秒" });
294
+ }
295
+
296
+ const onceJob = buildOnceJob(p, delayMs, resolvedTo, resolvedAccountId);
297
+ return json({
298
+ _instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
299
+ cronParams: onceJob,
300
+ summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
301
+ });
302
+ },
303
+ },
304
+ { name: "qqbot_remind" },
305
+ );
306
+
307
+ }
308
+
@@ -0,0 +1,185 @@
1
+ /**
2
+ * qqbot_secret_input 工具 — 聊天内密钥输入卡片
3
+ *
4
+ * 用户想交给 AI 一个新密钥/凭据时,host AI 调用本工具(而非让用户把
5
+ * 密钥粘贴进对话)。工具给当前 c2c 用户发一张输入卡片并登记 pending,
6
+ * 用户下一条消息由 secretCapture 中间件拦截,插件就地执行
7
+ * `openclaw secrets store set <NAME> --kind env --value ...` 并直接回执。
8
+ *
9
+ * kind 恒为 env(代理可读环境变量):聊天卡片收来的值本就落在 QQ 聊天
10
+ * 记录里,且 AI 随后要读取使用;secret kind 是「受保护的机密」
11
+ * (write-only,代理不可读),存进去 AI 反而用不了(2026-08-30 用户反馈)。
12
+ *
13
+ * 非阻塞:工具发卡后立即返回,AI 应结束本轮;执行与回执完全由插件自治。
14
+ */
15
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
16
+ import { tryGetBotForAccount } from '../bot-instance.js';
17
+ import { isValidSecretName } from '../features/secret-store-cli.js';
18
+ import {
19
+ DEFAULT_SECRET_INPUT_TTL_MS,
20
+ cancelPendingSecretInput,
21
+ registerPendingSecretInput,
22
+ } from '../features/secret-input-store.js';
23
+ import { getRequestAccountId, getRequestTarget } from '../request-context.js';
24
+ import { parseTarget } from '../outbound/target.js';
25
+ import type { InlineKeyboard, KeyboardButton } from '../types.js';
26
+
27
+ // ========== JSON Schema ==========
28
+
29
+ const SecretInputSchema = {
30
+ type: 'object',
31
+ properties: {
32
+ name: {
33
+ type: 'string',
34
+ description:
35
+ '环境变量名。命名规则:以大写字母开头,仅含大写字母/数字/下划线,' +
36
+ '最长 128 字符。推荐语义化命名,如 GITHUB_TOKEN、OPENWEATHER_API_KEY、' +
37
+ 'DEEPSEEK_API_KEY。值将以代理可读环境变量(kind=env)保存,AI 之后可直接读取使用。',
38
+ },
39
+ description: {
40
+ type: 'string',
41
+ description: '该密钥的用途说明(可选),会展示在输入卡片上,帮助用户确认。',
42
+ },
43
+ },
44
+ required: ['name'],
45
+ } as const;
46
+
47
+ // ========== 卡片构建 ==========
48
+
49
+ function buildSecretInputCard(
50
+ name: string,
51
+ description: string | undefined,
52
+ ttlMinutes: number,
53
+ ): string {
54
+ const lines = [`**🔒 密钥输入 · ${name}**`];
55
+ if (description) lines.push('', `用途:${description}`);
56
+ lines.push(
57
+ '',
58
+ `请在下方输入框直接粘贴并发送 **${name}** 的值:`,
59
+ '',
60
+ '- 将保存为**代理可读环境变量**(AI 之后可直接读取使用),' +
61
+ `写入方式:\`openclaw secrets store set ${name} --kind env\``,
62
+ '- 你发送的消息会被插件直接拦截执行,**不会进入 AI 对话**',
63
+ '- 密钥会留存在你的聊天记录中,请留意所在环境安全',
64
+ `- ${ttlMinutes} 分钟内有效;发送「取消」可退出`,
65
+ );
66
+ return lines.join('\n');
67
+ }
68
+
69
+ /** 「取消」指令按钮:action.type=2 + enter=true,客户端点击后以真实用户消息发出「取消」 */
70
+ function buildSecretInputKeyboard(): InlineKeyboard {
71
+ const buttons: KeyboardButton[] = [
72
+ {
73
+ id: 'secret_input_cancel',
74
+ render_data: {
75
+ label: '❌ 取消',
76
+ visited_label: '已取消',
77
+ style: 1 as const,
78
+ },
79
+ action: {
80
+ type: 2 as const,
81
+ data: '取消',
82
+ enter: true,
83
+ permission: { type: 2 as const },
84
+ unsupport_tips: '当前客户端不支持,请手动发送 取消',
85
+ },
86
+ },
87
+ ];
88
+ return { content: { rows: [{ buttons }] } };
89
+ }
90
+
91
+ // ========== 工具函数 ==========
92
+
93
+ function json(data: unknown) {
94
+ return {
95
+ content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }],
96
+ details: data,
97
+ };
98
+ }
99
+
100
+ // ========== 注册入口 ==========
101
+
102
+ export function registerSecretInputTool(api: OpenClawPluginApi): void {
103
+ api.registerTool(
104
+ {
105
+ name: 'qqbot_secret_input',
106
+ label: 'QQBot Secret Input Card',
107
+ description:
108
+ '当用户要交付一个新密钥/凭据(API Key、Token 等)给系统保存时调用:' +
109
+ '向用户私聊发送一张密钥输入卡片,用户把密钥粘贴在输入框发送后,' +
110
+ '插件会自动拦截该消息并执行 openclaw secrets store set 完成保存,' +
111
+ '结果由插件直接回复用户。密钥内容不会进入对话。' +
112
+ '必须由用户提供 name 参数(环境变量名)。',
113
+ parameters: SecretInputSchema,
114
+ async execute(_toolCallId, params) {
115
+ const p = params as { name?: string; description?: string };
116
+ const name = (p.name ?? '').trim();
117
+ if (!name) return json({ error: 'name 为必填参数(环境变量名)' });
118
+ if (!isValidSecretName(name)) {
119
+ return json({
120
+ error:
121
+ '环境变量名不合法:必须以大写字母开头,仅含大写字母/数字/下划线,最长 128 字符' +
122
+ `(收到 "${name}")。示例:OPENWEATHER_API_KEY。请修正后重试。`,
123
+ });
124
+ }
125
+ // 恒为 env:见文件头注释(卡片值 = 代理可读环境,secret 是 write-only 代理不可读)
126
+ const kind = 'env' as const;
127
+
128
+ const target = getRequestTarget();
129
+ const accountId = getRequestAccountId() ?? 'default';
130
+ if (!target) {
131
+ return json({ error: '无法获取当前会话目标,此工具仅支持在 QQ Bot 消息会话中使用' });
132
+ }
133
+ const parsed = parseTarget(target);
134
+ if (parsed.scope !== 'c2c' || !parsed.targetId) {
135
+ return json({
136
+ error:
137
+ `密钥输入卡片仅支持私聊(c2c)会话,当前目标 ${target} 不适用` +
138
+ '——密钥不应出现在群聊中。请引导用户在私聊中发起。',
139
+ });
140
+ }
141
+
142
+ const bot = tryGetBotForAccount(accountId);
143
+ if (!bot) {
144
+ return json({ error: `账户 ${accountId} 的 gateway 尚未启动,无法发送密钥输入卡片` });
145
+ }
146
+
147
+ const description = p.description?.trim() || undefined;
148
+ // 先登记后发卡(避免「先发后登」竞态,同 multi-question 约定)
149
+ registerPendingSecretInput({
150
+ accountId,
151
+ senderOpenid: parsed.targetId,
152
+ name,
153
+ kind,
154
+ description,
155
+ createdAt: Date.now(),
156
+ });
157
+ try {
158
+ await bot.sendTextWithKeyboard(
159
+ { scope: 'c2c', targetId: parsed.targetId },
160
+ buildSecretInputCard(name, description, DEFAULT_SECRET_INPUT_TTL_MS / 60_000),
161
+ buildSecretInputKeyboard() as never,
162
+ );
163
+ } catch (err) {
164
+ cancelPendingSecretInput(accountId, parsed.targetId);
165
+ return json({
166
+ error: `密钥输入卡片发送失败:${err instanceof Error ? err.message : String(err)}`,
167
+ });
168
+ }
169
+
170
+ return json({
171
+ ok: true,
172
+ name,
173
+ kind,
174
+ command: `openclaw secrets store set ${name} --kind env --value <用户输入>`,
175
+ instruction:
176
+ '卡片已发送给用户。用户下一条私聊消息将被插件自动拦截(不会进入对话),' +
177
+ `执行上述命令保存密钥,成功/失败由插件直接回复用户。` +
178
+ '请直接结束本轮(可告知用户按卡片提示发送密钥),不要等待用户输入,' +
179
+ '也不要让用户把密钥内容发到对话里。',
180
+ });
181
+ },
182
+ },
183
+ { name: 'qqbot_secret_input' },
184
+ );
185
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SDK MiddlewareState 类型扩展
3
+ *
4
+ * 通过 TypeScript module augmentation 为 SDK 的 MiddlewareState 添加
5
+ * 中间件填充的 well-known keys 类型声明。
6
+ *
7
+ * 这些字段由 SDK 内置中间件 / 项目自定义中间件填充:
8
+ * - envelope: envelopeFormatter 中间件 → 组装后的 agentBody(字符串)
9
+ * - assembledBody: envelopeFormatter format 注入函数 → 完整 AssembledBody 缓存
10
+ * - history: historyBuffer 中间件 → 群历史消息列表
11
+ * - quote: quoteRef 中间件 → 引用消息信息
12
+ * - mention: mentionGate 中间件 → @bot 判定结果
13
+ * - command: slashCommand 中间件 → 解析的命令
14
+ * - processedAttachments: attachmentProcessor 中间件 → 语音 STT、图片 URL
15
+ */
16
+ import '@tencent-connect/qqbot-nodejs';
17
+ import type { AssembledBody } from './dispatch/body-assembler.js';
18
+ import type { ProcessedAttachments } from './middleware/attachment.js';
19
+
20
+ declare module '@tencent-connect/qqbot-nodejs' {
21
+ interface MiddlewareState {
22
+ /** envelopeFormatter 输出的字符串(默认 = AssembledBody.agentBody) */
23
+ envelope?: string;
24
+ /** 项目自定义 format 注入的完整组装结果(dispatch 阶段直接消费) */
25
+ assembledBody?: AssembledBody;
26
+ /** historyBuffer 填充的群历史 */
27
+ history?: Array<{
28
+ role: string;
29
+ content: string;
30
+ senderId?: string;
31
+ senderName?: string;
32
+ timestamp?: number;
33
+ }>;
34
+ /** quoteRef 解析的引用信息 */
35
+ quote?: {
36
+ content: string;
37
+ senderId: string;
38
+ attachments?: unknown[];
39
+ messageId?: string;
40
+ };
41
+ /** mentionGate 判定结果 */
42
+ mention?: {
43
+ wasMentioned: boolean;
44
+ stripped?: string;
45
+ };
46
+ /** slashCommand 解析的命令(命令被匹配时存在) */
47
+ command?: {
48
+ name: string;
49
+ args: string;
50
+ };
51
+ /** attachmentProcessor 处理的附件结果 */
52
+ processedAttachments?: ProcessedAttachments;
53
+ }
54
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * QQBot Channel Plugin 类型定义
3
+ */
4
+
5
+ import type { ResolvedQQBotAccount } from './types.js';
6
+
7
+ /**
8
+ * QQBot 探测结果
9
+ */
10
+ export interface QQBotProbe {
11
+ ok: boolean;
12
+ bot?: {
13
+ id?: string;
14
+ username?: string;
15
+ };
16
+ status?: number;
17
+ }
18
+
19
+ /**
20
+ * QQBot 审计结果
21
+ */
22
+ export interface QQBotAudit {
23
+ ok: boolean;
24
+ checkedGroups: number;
25
+ unresolvedGroups: number;
26
+ }
27
+
28
+ /**
29
+ * QQBot 会话路由
30
+ */
31
+ export interface QQBotSessionRoute {
32
+ to: string;
33
+ threadId?: string | number;
34
+ chatType: 'direct' | 'group';
35
+ sessionKey: string;
36
+ baseSessionKey: string;
37
+ }
38
+
39
+ /**
40
+ * 配额状态
41
+ */
42
+ export interface QuotaState {
43
+ count: number;
44
+ expiresAt: number;
45
+ }
46
+
47
+ /**
48
+ * 配额管理参数
49
+ */
50
+ export interface QuotaCheckParams {
51
+ accountId: string;
52
+ msgId?: string;
53
+ scope: 'c2c' | 'group';
54
+ }
55
+
56
+ export interface QuotaConsumeParams extends QuotaCheckParams {
57
+ msgId: string;
58
+ log?: {
59
+ debug?: (message: string) => void;
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Typing 续期参数
65
+ */
66
+ export interface TypingParams {
67
+ accountId: string;
68
+ to: string;
69
+ replyToId: string;
70
+ log?: {
71
+ debug?: (message: string) => void;
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Typing 状态
77
+ */
78
+ export interface TypingState {
79
+ timer: NodeJS.Timeout;
80
+ startedAt: number;
81
+ renewalCount: number;
82
+ }