@tencent-connect/openclaw-qqbot 1.6.4-alpha.14 → 1.6.4-alpha.16

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 (47) hide show
  1. package/clawdbot.plugin.json +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/src/admin-resolver.d.ts +27 -0
  4. package/dist/src/admin-resolver.js +122 -0
  5. package/dist/src/channel.js +3 -0
  6. package/dist/src/gateway.js +101 -1515
  7. package/dist/src/inbound-attachments.d.ts +58 -0
  8. package/dist/src/inbound-attachments.js +234 -0
  9. package/dist/src/message-queue.d.ts +50 -0
  10. package/dist/src/message-queue.js +115 -0
  11. package/dist/src/outbound-deliver.d.ts +48 -0
  12. package/dist/src/outbound-deliver.js +462 -0
  13. package/dist/src/reply-dispatcher.d.ts +35 -0
  14. package/dist/src/reply-dispatcher.js +311 -0
  15. package/dist/src/startup-greeting.d.ts +28 -0
  16. package/dist/src/startup-greeting.js +70 -0
  17. package/dist/src/stt.d.ts +21 -0
  18. package/dist/src/stt.js +70 -0
  19. package/dist/src/tools/remind.d.ts +2 -0
  20. package/dist/src/tools/remind.js +247 -0
  21. package/dist/src/typing-keepalive.d.ts +27 -0
  22. package/dist/src/typing-keepalive.js +64 -0
  23. package/dist/src/utils/file-utils.d.ts +9 -0
  24. package/dist/src/utils/file-utils.js +43 -0
  25. package/dist/src/utils/platform.d.ts +10 -0
  26. package/dist/src/utils/platform.js +16 -0
  27. package/dist/src/utils/text-parsing.d.ts +32 -0
  28. package/dist/src/utils/text-parsing.js +78 -0
  29. package/index.ts +2 -0
  30. package/moltbot.plugin.json +1 -1
  31. package/openclaw.plugin.json +1 -1
  32. package/package.json +1 -1
  33. package/skills/{qqbot-cron → qqbot-remind}/SKILL.md +40 -20
  34. package/src/admin-resolver.ts +140 -0
  35. package/src/channel.ts +3 -0
  36. package/src/gateway.ts +124 -1589
  37. package/src/inbound-attachments.ts +304 -0
  38. package/src/message-queue.ts +169 -0
  39. package/src/outbound-deliver.ts +552 -0
  40. package/src/reply-dispatcher.ts +334 -0
  41. package/src/startup-greeting.ts +88 -0
  42. package/src/stt.ts +86 -0
  43. package/src/tools/remind.ts +296 -0
  44. package/src/typing-keepalive.ts +59 -0
  45. package/src/utils/file-utils.ts +45 -0
  46. package/src/utils/platform.ts +17 -0
  47. package/src/utils/text-parsing.ts +80 -0
@@ -3,7 +3,7 @@
3
3
  "name": "OpenClaw QQ Bot",
4
4
  "description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
5
5
  "channels": ["qqbot"],
6
- "skills": ["skills/qqbot-channel", "skills/qqbot-cron", "skills/qqbot-media"],
6
+ "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
7
7
  "capabilities": {
8
8
  "proactiveMessaging": true,
9
9
  "cronJobs": true
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
2
  import { qqbotPlugin } from "./src/channel.js";
3
3
  import { setQQBotRuntime } from "./src/runtime.js";
4
4
  import { registerChannelTool } from "./src/tools/channel.js";
5
+ import { registerRemindTool } from "./src/tools/remind.js";
5
6
  const plugin = {
6
7
  id: "openclaw-qqbot",
7
8
  name: "QQ Bot",
@@ -11,6 +12,7 @@ const plugin = {
11
12
  setQQBotRuntime(api.runtime);
12
13
  api.registerChannel({ plugin: qqbotPlugin });
13
14
  registerChannelTool(api);
15
+ registerRemindTool(api);
14
16
  },
15
17
  };
16
18
  export default plugin;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 管理员解析器模块
3
+ * - 管理员 openid 持久化读写
4
+ * - 升级问候目标读写
5
+ * - 启动问候语发送
6
+ */
7
+ export interface AdminResolverContext {
8
+ accountId: string;
9
+ appId: string;
10
+ clientSecret: string;
11
+ log?: {
12
+ info: (msg: string) => void;
13
+ error: (msg: string) => void;
14
+ };
15
+ }
16
+ export declare function loadAdminOpenId(accountId: string): string | undefined;
17
+ export declare function saveAdminOpenId(accountId: string, openid: string): void;
18
+ export declare function loadUpgradeGreetingTargetOpenId(accountId: string, appId: string): string | undefined;
19
+ export declare function clearUpgradeGreetingTargetOpenId(accountId: string, appId: string): void;
20
+ /**
21
+ * 解析管理员 openid:
22
+ * 1. 优先读持久化文件(稳定)
23
+ * 2. fallback 取第一个私聊用户,并写入文件锁定
24
+ */
25
+ export declare function resolveAdminOpenId(ctx: Pick<AdminResolverContext, "accountId" | "log">): string | undefined;
26
+ /** 异步发送启动问候语(仅发给管理员) */
27
+ export declare function sendStartupGreetings(ctx: AdminResolverContext, trigger: "READY" | "RESUMED"): void;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 管理员解析器模块
3
+ * - 管理员 openid 持久化读写
4
+ * - 升级问候目标读写
5
+ * - 启动问候语发送
6
+ */
7
+ import path from "node:path";
8
+ import * as fs from "node:fs";
9
+ import { getQQBotDataDir } from "./utils/platform.js";
10
+ import { listKnownUsers } from "./known-users.js";
11
+ import { getAccessToken, sendProactiveC2CMessage } from "./api.js";
12
+ import { getStartupGreetingPlan, markStartupGreetingSent, markStartupGreetingFailed } from "./startup-greeting.js";
13
+ // ---- 文件路径 ----
14
+ function getAdminMarkerFile(accountId) {
15
+ return path.join(getQQBotDataDir("data"), `admin-${accountId}.json`);
16
+ }
17
+ function getUpgradeGreetingTargetFile(accountId, appId) {
18
+ const safeAccountId = accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
19
+ const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
20
+ return path.join(getQQBotDataDir("data"), `upgrade-greeting-target-${safeAccountId}-${safeAppId}.json`);
21
+ }
22
+ // ---- 管理员 openid 持久化 ----
23
+ export function loadAdminOpenId(accountId) {
24
+ try {
25
+ const file = getAdminMarkerFile(accountId);
26
+ if (fs.existsSync(file)) {
27
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
28
+ if (data.openid)
29
+ return data.openid;
30
+ }
31
+ }
32
+ catch { /* 文件损坏视为无 */ }
33
+ return undefined;
34
+ }
35
+ export function saveAdminOpenId(accountId, openid) {
36
+ try {
37
+ fs.writeFileSync(getAdminMarkerFile(accountId), JSON.stringify({ openid, savedAt: new Date().toISOString() }));
38
+ }
39
+ catch { /* ignore */ }
40
+ }
41
+ // ---- 升级问候目标 ----
42
+ export function loadUpgradeGreetingTargetOpenId(accountId, appId) {
43
+ try {
44
+ const file = getUpgradeGreetingTargetFile(accountId, appId);
45
+ if (fs.existsSync(file)) {
46
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
47
+ if (!data.openid)
48
+ return undefined;
49
+ if (data.appId && data.appId !== appId)
50
+ return undefined;
51
+ if (data.accountId && data.accountId !== accountId)
52
+ return undefined;
53
+ return data.openid;
54
+ }
55
+ }
56
+ catch { /* 文件损坏视为无 */ }
57
+ return undefined;
58
+ }
59
+ export function clearUpgradeGreetingTargetOpenId(accountId, appId) {
60
+ try {
61
+ const file = getUpgradeGreetingTargetFile(accountId, appId);
62
+ if (fs.existsSync(file)) {
63
+ fs.unlinkSync(file);
64
+ }
65
+ }
66
+ catch { /* ignore */ }
67
+ }
68
+ // ---- 解析管理员 ----
69
+ /**
70
+ * 解析管理员 openid:
71
+ * 1. 优先读持久化文件(稳定)
72
+ * 2. fallback 取第一个私聊用户,并写入文件锁定
73
+ */
74
+ export function resolveAdminOpenId(ctx) {
75
+ const saved = loadAdminOpenId(ctx.accountId);
76
+ if (saved)
77
+ return saved;
78
+ const first = listKnownUsers({ accountId: ctx.accountId, type: "c2c", sortBy: "firstSeenAt", sortOrder: "asc", limit: 1 })[0]?.openid;
79
+ if (first) {
80
+ saveAdminOpenId(ctx.accountId, first);
81
+ ctx.log?.info(`[qqbot:${ctx.accountId}] Auto-detected admin openid: ${first} (persisted)`);
82
+ }
83
+ return first;
84
+ }
85
+ // ---- 启动问候语 ----
86
+ /** 异步发送启动问候语(仅发给管理员) */
87
+ export function sendStartupGreetings(ctx, trigger) {
88
+ (async () => {
89
+ const plan = getStartupGreetingPlan();
90
+ if (!plan.shouldSend || !plan.greeting) {
91
+ ctx.log?.info(`[qqbot:${ctx.accountId}] Skipping startup greeting (${plan.reason ?? "debounced"}, trigger=${trigger})`);
92
+ return;
93
+ }
94
+ const upgradeTargetOpenId = loadUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
95
+ const targetOpenId = upgradeTargetOpenId || resolveAdminOpenId(ctx);
96
+ if (!targetOpenId) {
97
+ markStartupGreetingFailed(plan.version, "no-admin");
98
+ ctx.log?.info(`[qqbot:${ctx.accountId}] Skipping startup greeting (no admin or known user)`);
99
+ return;
100
+ }
101
+ try {
102
+ const receiverType = upgradeTargetOpenId ? "upgrade-requester" : "admin";
103
+ ctx.log?.info(`[qqbot:${ctx.accountId}] Sending startup greeting to ${receiverType} (trigger=${trigger}): "${plan.greeting}"`);
104
+ const token = await getAccessToken(ctx.appId, ctx.clientSecret);
105
+ const GREETING_TIMEOUT_MS = 10_000;
106
+ await Promise.race([
107
+ sendProactiveC2CMessage(token, targetOpenId, plan.greeting),
108
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
109
+ ]);
110
+ markStartupGreetingSent(plan.version);
111
+ if (upgradeTargetOpenId) {
112
+ clearUpgradeGreetingTargetOpenId(ctx.accountId, ctx.appId);
113
+ }
114
+ ctx.log?.info(`[qqbot:${ctx.accountId}] Sent startup greeting to ${receiverType}: ${targetOpenId}`);
115
+ }
116
+ catch (err) {
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ markStartupGreetingFailed(plan.version, message);
119
+ ctx.log?.error(`[qqbot:${ctx.accountId}] Failed to send startup greeting: ${message}`);
120
+ }
121
+ })();
122
+ }
@@ -5,6 +5,7 @@ import { startGateway } from "./gateway.js";
5
5
  import { qqbotOnboardingAdapter } from "./onboarding.js";
6
6
  import { getQQBotRuntime } from "./runtime.js";
7
7
  import { saveCredentialBackup, loadCredentialBackup } from "./credential-backup.js";
8
+ import { initApiConfig } from "./api.js";
8
9
  /** QQ Bot 单条消息文本长度上限 */
9
10
  export const TEXT_CHUNK_LIMIT = 5000;
10
11
  /**
@@ -201,6 +202,7 @@ export const qqbotPlugin = {
201
202
  console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
202
203
  console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
203
204
  const account = resolveQQBotAccount(cfg, accountId);
205
+ initApiConfig({ markdownSupport: account.markdownSupport });
204
206
  console.log(`[qqbot:channel] sendText resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
205
207
  const result = await sendText({ to, text, accountId, replyToId, account });
206
208
  console.log(`[qqbot:channel] sendText result: messageId=${result.messageId}, error=${result.error ?? "none"}`);
@@ -213,6 +215,7 @@ export const qqbotPlugin = {
213
215
  sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
214
216
  console.log(`[qqbot:channel] sendMedia called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, mediaUrl=${mediaUrl?.slice(0, 80)}, text.length=${text?.length ?? 0}`);
215
217
  const account = resolveQQBotAccount(cfg, accountId);
218
+ initApiConfig({ markdownSupport: account.markdownSupport });
216
219
  console.log(`[qqbot:channel] sendMedia resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
217
220
  const result = await sendMedia({ to, text: text ?? "", mediaUrl: mediaUrl ?? "", accountId, replyToId, account });
218
221
  console.log(`[qqbot:channel] sendMedia result: messageId=${result.messageId}, error=${result.error ?? "none"}`);