feat(qqbot): 定时提醒技能与主动消息系统
**新增提醒技能** - 新增 skills/qqbot-cron/SKILL.md 定时提醒技能定义 - 支持一次性提醒(--at)和周期性提醒(--cron) - 支持设置、查询、取消提醒操作 **主动消息系统** - 新增 src/proactive.ts 主动消息发送模块 - 新增 src/known-users.ts 已知用户管理 - 新增 src/session-store.ts 会话存储 - 支持主动向用户/群组发送消息 **工具脚本** - 新增 scripts/proactive-api-server.ts 主动消息API服务
This commit is contained in:
273
scripts/send-proactive.ts
Normal file
273
scripts/send-proactive.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
/**
|
||||
* QQBot 主动消息 CLI 工具
|
||||
*
|
||||
* 使用示例:
|
||||
* # 发送私聊消息
|
||||
* npx ts-node scripts/send-proactive.ts --to "用户openid" --text "你好!"
|
||||
*
|
||||
* # 发送群聊消息
|
||||
* npx ts-node scripts/send-proactive.ts --to "群组openid" --type group --text "群公告"
|
||||
*
|
||||
* # 列出已知用户
|
||||
* npx ts-node scripts/send-proactive.ts --list
|
||||
*
|
||||
* # 列出群聊用户
|
||||
* npx ts-node scripts/send-proactive.ts --list --type group
|
||||
*
|
||||
* # 广播消息
|
||||
* npx ts-node scripts/send-proactive.ts --broadcast --text "系统公告" --type c2c --limit 10
|
||||
*/
|
||||
|
||||
import {
|
||||
sendProactiveMessageDirect,
|
||||
listKnownUsers,
|
||||
getKnownUsersStats,
|
||||
broadcastMessage,
|
||||
} from "../src/proactive.js";
|
||||
import type { ResolvedQQBotAccount } from "../src/types.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs(): Record<string, string | boolean> {
|
||||
const args: Record<string, string | boolean> = {};
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg.startsWith("--")) {
|
||||
const key = arg.slice(2);
|
||||
const nextArg = argv[i + 1];
|
||||
if (nextArg && !nextArg.startsWith("--")) {
|
||||
args[key] = nextArg;
|
||||
i++;
|
||||
} else {
|
||||
args[key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// 从配置文件加载账户信息
|
||||
function loadAccount(accountId = "default"): ResolvedQQBotAccount | null {
|
||||
const configPath = path.join(process.env.HOME || "/home/ubuntu", "clawd", "config.json");
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
// 尝试从环境变量获取
|
||||
const appId = process.env.QQBOT_APP_ID;
|
||||
const clientSecret = process.env.QQBOT_CLIENT_SECRET;
|
||||
|
||||
if (appId && clientSecret) {
|
||||
return {
|
||||
accountId,
|
||||
appId,
|
||||
clientSecret,
|
||||
enabled: true,
|
||||
secretSource: "env",
|
||||
};
|
||||
}
|
||||
|
||||
console.error("配置文件不存在且环境变量未设置");
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
const qqbot = config.channels?.qqbot;
|
||||
|
||||
if (!qqbot) {
|
||||
console.error("配置中没有 qqbot 配置");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析账户配置
|
||||
if (accountId === "default") {
|
||||
return {
|
||||
accountId: "default",
|
||||
appId: qqbot.appId || process.env.QQBOT_APP_ID,
|
||||
clientSecret: qqbot.clientSecret || process.env.QQBOT_CLIENT_SECRET,
|
||||
enabled: qqbot.enabled ?? true,
|
||||
secretSource: qqbot.clientSecret ? "config" : "env",
|
||||
};
|
||||
}
|
||||
|
||||
const accountConfig = qqbot.accounts?.[accountId];
|
||||
if (accountConfig) {
|
||||
return {
|
||||
accountId,
|
||||
appId: accountConfig.appId || qqbot.appId || process.env.QQBOT_APP_ID,
|
||||
clientSecret: accountConfig.clientSecret || qqbot.clientSecret || process.env.QQBOT_CLIENT_SECRET,
|
||||
enabled: accountConfig.enabled ?? true,
|
||||
secretSource: accountConfig.clientSecret ? "config" : "env",
|
||||
};
|
||||
}
|
||||
|
||||
console.error(`账户 ${accountId} 不存在`);
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error(`加载配置失败: ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
// 显示帮助
|
||||
if (args.help || args.h) {
|
||||
console.log(`
|
||||
QQBot 主动消息 CLI 工具
|
||||
|
||||
用法:
|
||||
npx ts-node scripts/send-proactive.ts [选项]
|
||||
|
||||
选项:
|
||||
--to <openid> 目标用户或群组的 openid
|
||||
--text <message> 要发送的消息内容
|
||||
--type <type> 消息类型: c2c (私聊) 或 group (群聊),默认 c2c
|
||||
--account <id> 账户 ID,默认 default
|
||||
|
||||
--list 列出已知用户
|
||||
--stats 显示用户统计
|
||||
--broadcast 广播消息给所有已知用户
|
||||
--limit <n> 限制数量
|
||||
|
||||
--help, -h 显示帮助
|
||||
|
||||
示例:
|
||||
# 发送私聊消息
|
||||
npx ts-node scripts/send-proactive.ts --to "0Eda5EA7-xxx" --text "你好!"
|
||||
|
||||
# 发送群聊消息
|
||||
npx ts-node scripts/send-proactive.ts --to "A1B2C3D4" --type group --text "群公告"
|
||||
|
||||
# 列出最近 10 个私聊用户
|
||||
npx ts-node scripts/send-proactive.ts --list --type c2c --limit 10
|
||||
|
||||
# 广播消息
|
||||
npx ts-node scripts/send-proactive.ts --broadcast --text "系统公告" --limit 5
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const accountId = (args.account as string) || "default";
|
||||
const type = (args.type as "c2c" | "group") || "c2c";
|
||||
const limit = args.limit ? parseInt(args.limit as string, 10) : undefined;
|
||||
|
||||
// 列出已知用户
|
||||
if (args.list) {
|
||||
const users = listKnownUsers({
|
||||
type: args.type as "c2c" | "group" | "channel" | undefined,
|
||||
accountId: args.account as string | undefined,
|
||||
limit,
|
||||
});
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log("没有已知用户");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n已知用户列表 (共 ${users.length} 个):\n`);
|
||||
console.log("类型\t\tOpenID\t\t\t\t\t\t昵称\t\t最后交互时间");
|
||||
console.log("─".repeat(100));
|
||||
|
||||
for (const user of users) {
|
||||
const lastTime = new Date(user.lastInteractionAt).toLocaleString();
|
||||
console.log(`${user.type}\t\t${user.openid.slice(0, 20)}...\t${user.nickname || "-"}\t\t${lastTime}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示统计
|
||||
if (args.stats) {
|
||||
const stats = getKnownUsersStats(args.account as string | undefined);
|
||||
console.log(`\n用户统计:`);
|
||||
console.log(` 总计: ${stats.total}`);
|
||||
console.log(` 私聊: ${stats.c2c}`);
|
||||
console.log(` 群聊: ${stats.group}`);
|
||||
console.log(` 频道: ${stats.channel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 广播消息
|
||||
if (args.broadcast) {
|
||||
if (!args.text) {
|
||||
console.error("请指定消息内容 (--text)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 加载配置用于广播
|
||||
const configPath = path.join(process.env.HOME || "/home/ubuntu", "clawd", "config.json");
|
||||
let cfg: Record<string, unknown> = {};
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
console.log(`\n开始广播消息...\n`);
|
||||
const result = await broadcastMessage(args.text as string, cfg as any, {
|
||||
type,
|
||||
accountId,
|
||||
limit,
|
||||
});
|
||||
|
||||
console.log(`\n广播完成:`);
|
||||
console.log(` 发送总数: ${result.total}`);
|
||||
console.log(` 成功: ${result.success}`);
|
||||
console.log(` 失败: ${result.failed}`);
|
||||
|
||||
if (result.failed > 0) {
|
||||
console.log(`\n失败详情:`);
|
||||
for (const r of result.results) {
|
||||
if (!r.result.success) {
|
||||
console.log(` ${r.to}: ${r.result.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送单条消息
|
||||
if (args.to && args.text) {
|
||||
const account = loadAccount(accountId);
|
||||
if (!account) {
|
||||
console.error("无法加载账户配置");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n发送消息...`);
|
||||
console.log(` 目标: ${args.to}`);
|
||||
console.log(` 类型: ${type}`);
|
||||
console.log(` 内容: ${args.text}`);
|
||||
|
||||
const result = await sendProactiveMessageDirect(
|
||||
account,
|
||||
args.to as string,
|
||||
args.text as string,
|
||||
type
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`\n✅ 发送成功!`);
|
||||
console.log(` 消息ID: ${result.messageId}`);
|
||||
console.log(` 时间戳: ${result.timestamp}`);
|
||||
} else {
|
||||
console.log(`\n❌ 发送失败: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有有效参数
|
||||
console.error("请指定操作。使用 --help 查看帮助。");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`执行失败: ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user