@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.
- package/clawdbot.plugin.json +1 -1
- package/dist/index.js +2 -0
- package/dist/src/admin-resolver.d.ts +27 -0
- package/dist/src/admin-resolver.js +122 -0
- package/dist/src/channel.js +3 -0
- package/dist/src/gateway.js +101 -1515
- package/dist/src/inbound-attachments.d.ts +58 -0
- package/dist/src/inbound-attachments.js +234 -0
- package/dist/src/message-queue.d.ts +50 -0
- package/dist/src/message-queue.js +115 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +462 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/startup-greeting.d.ts +28 -0
- package/dist/src/startup-greeting.js +70 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +247 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/utils/file-utils.d.ts +9 -0
- package/dist/src/utils/file-utils.js +43 -0
- package/dist/src/utils/platform.d.ts +10 -0
- package/dist/src/utils/platform.js +16 -0
- package/dist/src/utils/text-parsing.d.ts +32 -0
- package/dist/src/utils/text-parsing.js +78 -0
- package/index.ts +2 -0
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/{qqbot-cron → qqbot-remind}/SKILL.md +40 -20
- package/src/admin-resolver.ts +140 -0
- package/src/channel.ts +3 -0
- package/src/gateway.ts +124 -1589
- package/src/inbound-attachments.ts +304 -0
- package/src/message-queue.ts +169 -0
- package/src/outbound-deliver.ts +552 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/startup-greeting.ts +88 -0
- package/src/stt.ts +86 -0
- package/src/tools/remind.ts +296 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/utils/file-utils.ts +45 -0
- package/src/utils/platform.ts +17 -0
- package/src/utils/text-parsing.ts +80 -0
package/clawdbot.plugin.json
CHANGED
|
@@ -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-
|
|
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
|
+
}
|
package/dist/src/channel.js
CHANGED
|
@@ -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"}`);
|