foliko 1.1.38 → 1.1.39
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/.agent/agents/network-requester.md +44 -0
- package/.agent/agents/poster-designer.md +52 -0
- package/.agent/agents/ui-designer.md +1 -1
- package/.agent/data/default.json +23 -407
- package/.agent/data/email/processed-emails.json +1 -0
- package/.agent/data/plugins-state.json +103 -200
- package/.agent/data/web/web-config.json +5 -0
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/mcp_config.json +4 -17
- package/.agent/memory/user/mof6gk94-kneeuh.md +9 -0
- package/.agent/package.json +8 -0
- package/.agent/plugins/marknative/README.md +134 -0
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +256 -0
- package/.agent/plugins/marknative/package.json +12 -0
- package/.agent/plugins/test-plugin.py +99 -0
- package/.agent/plugins.json +11 -5
- package/.agent/python-scripts/test_sample.py +24 -0
- package/.agent/sessions/cli_default.json +2890 -18
- package/.agent/sessions/default.json +82 -0
- package/.agent/sessions/qq_c2c_D960F12877541624D7B2C836110B3D0F.json +25 -0
- package/.agent/sessions/test-clean.json +26 -0
- package/.agent/sessions/test-compress.json +1462 -0
- package/.agent/sessions/test-session.json +13 -0
- package/.agent/sessions/weixin_test.json +22 -0
- package/.agent/sessions/weixin_test_user_123.json +11 -0
- package/.agent/skills/agent-browser/SKILL.md +311 -0
- package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
- package/.agent/skills/sysinfo/SKILL.md +38 -0
- package/.agent/skills/sysinfo/system-info.sh +130 -0
- package/.agent/skills/workflow/SKILL.md +324 -0
- package/.agent/test-agent.js +35 -0
- package/.agent/weixin.json +6 -0
- package/.agent/workflows/email-digest.json +50 -0
- package/.agent/workflows/file-backup.json +21 -0
- package/.agent/workflows/get-ip-notify.json +32 -0
- package/.agent/workflows/news-aggregator.json +93 -0
- package/.agent/workflows/news-dashboard-v2.json +94 -0
- package/.agent/workflows/notification-batch.json +32 -0
- package/.claude/settings.local.json +2 -1
- package/.env.example +56 -56
- package/README.md +441 -441
- package/cli/src/utils/debounce.js +6 -3
- package/docs/qq-bot.md +976 -0
- package/package.json +2 -1
- package/plugins/qq-plugin.js +939 -0
- package/skills/find-skills/AGENTS.md +162 -162
- package/skills/find-skills/SKILL.md +133 -133
- package/temp_img.md +1 -0
- package/website_v2/styles/animations.css +7 -7
- package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
- package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
- package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
- package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
- package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
- package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
- package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
- package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
- package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
- package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
- package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
- package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
- package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
- package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
- package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
- package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
- package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
- package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
- package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
- package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
- package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
- package/.agent/ARCHITECTURE.md +0 -288
- package/.agent/data/ambient/goals.json +0 -1
- package/.agent/data/puppeteer-sessions/undefined.json +0 -6
- package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
- package/.agent/memory/feedback/mnygjgox-ualjip.md +0 -11
- package/.agent/memory/feedback/mnzugpej-esdwsr.md +0 -9
- package/.agent/memory/feedback/mo2quxke-saxs56.md +0 -9
- package/.agent/memory/feedback/mo9l6nw9-sm1ye2.md +0 -13
- package/.agent/memory/feedback/mo9uxkb3-bbxz3x.md +0 -9
- package/.agent/memory/feedback/mo9wpqy0-ftsez9.md +0 -13
- package/.agent/memory/feedback/mo9x2ps4-p7huqq.md +0 -11
- package/.agent/memory/feedback/mo9x2uay-z7ndjn.md +0 -9
- package/.agent/memory/feedback/mo9x2y7d-9wecyx.md +0 -9
- package/.agent/memory/feedback/mo9x39q3-nn7myt.md +0 -9
- package/.agent/memory/feedback/mo9xfir9-4g8a8j.md +0 -9
- package/.agent/memory/feedback/mo9xfui8-ujqrc6.md +0 -9
- package/.agent/memory/feedback/mocnx1wx-qtxxlh.md +0 -9
- package/.agent/memory/project/mnqx54u5-loqtoe.md +0 -9
- package/.agent/memory/project/mnqx84cv-mx6dmd.md +0 -9
- package/.agent/memory/project/mnsacuyr-hgtk5n.md +0 -20
- package/.agent/memory/project/mnu5hy2x-bjsg7u.md +0 -9
- package/.agent/memory/project/mny28ot4-8qe9au.md +0 -9
- package/.agent/memory/project/mnztftxj-af82rh.md +0 -9
- package/.agent/memory/project/mo0y04d0-bmaefl.md +0 -9
- package/.agent/memory/project/mo2iztxb-3c7v81.md +0 -9
- package/.agent/memory/project/mo2ssa5x-tpi1p3.md +0 -9
- package/.agent/memory/reference/mnre3cww-penbo1.md +0 -9
- package/.agent/memory/reference/mns9wn48-luerua.md +0 -14
- package/.agent/memory/reference/mns9yz5c-thc2s0.md +0 -16
- package/.agent/memory/reference/mnsfy4um-910f1o.md +0 -23
- package/.agent/memory/reference/mnsg37dp-lmfj18.md +0 -32
- package/.agent/memory/reference/mnsll60q-0j911u.md +0 -36
- package/.agent/memory/reference/mnsmlb5y-nej31u.md +0 -16
- package/.agent/memory/reference/mnssle72-yrot96.md +0 -9
- package/.agent/memory/reference/mnygj8nb-bjthmc.md +0 -20
- package/.agent/memory/reference/mnzfvs4m-ufyg9a.md +0 -12
- package/.agent/memory/user/mnsfuon6-l416q1.md +0 -21
- package/.agent/memory/user/mnsg9kut-95m7rf.md +0 -20
- package/.agent/memory/user/mnu2eo1v-yy6fhe.md +0 -9
- package/.agent/memory/user/mnu2etuo-8u8jk8.md +0 -9
- package/.agent/memory/user/mnx0rk6g-gsznjj.md +0 -9
- package/.agent/memory/user/mnyf1riz-4yo5yz.md +0 -9
- package/.agent/memory/user/mnzuh4cw-zvee8w.md +0 -13
- package/.agent/memory/user/mnzvewyj-jl67cq.md +0 -9
- package/.agent/memory/user/mnzwh9xo-43ys3f.md +0 -9
- package/.agent/memory/user/mo0ycvpn-eebsxc.md +0 -9
- package/.agent/memory/user/mo2k8c8n-132r2u.md +0 -9
- package/.agent/memory/user/mo6y2dei-5cf537.md +0 -13
- package/.agent/memory/user/mo9xsdo6-8vylww.md +0 -13
- package/.agent/plugins/puppeteer-plugin/README.md +0 -147
- package/.agent/plugins/puppeteer-plugin/index.js +0 -1422
- package/.agent/plugins/puppeteer-plugin/package.json +0 -9
- package/.agent/rules/GEMINI.md +0 -273
- package/.agent/rules/allow-rule.md +0 -77
- package/.agent/rules/log-rule.md +0 -83
- package/.agent/rules/security-rule.md +0 -93
- package/.agent/scripts/auto_preview.py +0 -148
- package/.agent/scripts/checklist.py +0 -217
- package/.agent/scripts/session_manager.py +0 -120
- package/.agent/scripts/verify_all.py +0 -327
- package/.agent/skills/doc.md +0 -177
- package/.agent/skills/fk-poster/SKILL.md +0 -1129
- package/.agent/skills/fkbuilder/SKILL.md +0 -730
- package/.agent/skills/mmx-cli/SKILL.md +0 -431
- package/.agent/workflows/brainstorm.md +0 -113
- package/.agent/workflows/create.md +0 -59
- package/.agent/workflows/debug.md +0 -103
- package/.agent/workflows/deploy.md +0 -176
- package/.agent/workflows/enhance.md +0 -63
- package/.agent/workflows/orchestrate.md +0 -237
- package/.agent/workflows/plan.md +0 -89
- package/.agent/workflows/preview.md +0 -81
- package/.agent/workflows/simple-test.md +0 -42
- package/.agent/workflows/status.md +0 -86
- package/.agent/workflows/structured-orchestrate.md +0 -180
- package/.agent/workflows/test.md +0 -144
- package/.agent/workflows/ui-ux-pro-max.md +0 -296
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ 插件
|
|
3
|
+
* 使用 @chnak/qq-bot 实现 QQ 对话
|
|
4
|
+
*
|
|
5
|
+
* 配置:
|
|
6
|
+
* - appId: QQ 开放平台应用 ID
|
|
7
|
+
* - clientSecret: QQ 开放平台应用密钥
|
|
8
|
+
* - allowedUsers: 允许交互的用户 openid 列表(空数组表示允许所有人)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
12
|
+
const { logger } = require('../src/utils/logger')
|
|
13
|
+
const log = logger.child('QQ')
|
|
14
|
+
const { cleanResponse } = require('../src/utils')
|
|
15
|
+
const { FileDownloader } = require('../src/utils/download')
|
|
16
|
+
const { z } = require('zod')
|
|
17
|
+
const { createQQBotClient, QQBotAPIClient, StreamSession } = require('@chnak/qq-bot')
|
|
18
|
+
const path = require('path')
|
|
19
|
+
const fs = require('fs')
|
|
20
|
+
|
|
21
|
+
class QQPlugin extends Plugin {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
super()
|
|
24
|
+
this.name = 'qq'
|
|
25
|
+
this.version = '1.0.0'
|
|
26
|
+
this.description = 'QQ 对话插件,使用 QQ 开放平台进行对话'
|
|
27
|
+
this.priority = 80
|
|
28
|
+
this.enabled = false
|
|
29
|
+
this.path = `.agent/data`
|
|
30
|
+
|
|
31
|
+
this.config = {
|
|
32
|
+
appId: config.appId || process.env.QQ_APP_ID,
|
|
33
|
+
clientSecret: config.clientSecret || process.env.QQ_CLIENT_SECRET,
|
|
34
|
+
allowedUsers: config.allowedUsers || [],
|
|
35
|
+
markdownSupport: true,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._framework = null
|
|
39
|
+
this._client = null
|
|
40
|
+
this._apiClient = null // 独立的 API 客户端(用于流式消息)
|
|
41
|
+
this._sessionPlugin = null
|
|
42
|
+
this._sessionDeleteHandler = null
|
|
43
|
+
this._sessionAgents = new Map() // openid -> Agent
|
|
44
|
+
this._sessionScopes = new Map() // sessionId -> sessionScope
|
|
45
|
+
this._streamSessions = new Map() // sessionId -> StreamSession
|
|
46
|
+
this._initialized = false
|
|
47
|
+
this._myUserId = null
|
|
48
|
+
this._typingUsers = new Set() // 正在发送 typing 的用户(防抖)
|
|
49
|
+
this.agent = null
|
|
50
|
+
this.sessionId = null
|
|
51
|
+
|
|
52
|
+
const saveDir = path.join(process.cwd(), '.agent', 'data', this.name)
|
|
53
|
+
this.downloader = new FileDownloader({
|
|
54
|
+
retries: 3,
|
|
55
|
+
baseDir: saveDir
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
this.systemPrompt = `你是一个 QQ 助手。
|
|
59
|
+
|
|
60
|
+
**重要:** 子Agent 匹配规则必须遵守:
|
|
61
|
+
- 根据【子 Agent 匹配表】,将任务委托给最匹配的子Agent处理
|
|
62
|
+
- 使用 subagent_call 工具并指定 agentName 来委托任务
|
|
63
|
+
- 只有当没有匹配的子Agent时,才直接调用工具
|
|
64
|
+
|
|
65
|
+
**命令执行规范:**
|
|
66
|
+
- 执行 npx skills add / npx skills remove / npx skills list 命令时,必须自动添加参数:-a openclaw -y
|
|
67
|
+
- 禁止添加 -g 参数
|
|
68
|
+
- 例如:npx skills add xxx 应该执行为 npx skills add xxx -a openclaw -y`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
install(framework) {
|
|
72
|
+
this._framework = framework
|
|
73
|
+
this._registerTools()
|
|
74
|
+
return this
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 注册 QQ 发送工具
|
|
79
|
+
*/
|
|
80
|
+
_registerTools() {
|
|
81
|
+
// 发送文本消息
|
|
82
|
+
this.registerTool({
|
|
83
|
+
name: 'qq_send_message',
|
|
84
|
+
description: '发送文本消息到 QQ(私聊)',
|
|
85
|
+
inputSchema: z.object({
|
|
86
|
+
message: z.string().describe('要发送的文本内容'),
|
|
87
|
+
openid: z.string().optional().describe('用户 openid(不填则发给机器人自己)'),
|
|
88
|
+
}),
|
|
89
|
+
execute: async (args) => {
|
|
90
|
+
return this._sendMessage(args.openid || this._myUserId, args.message)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// 发送图片/语音/视频(根据文件后缀自动判断)
|
|
95
|
+
this.registerTool({
|
|
96
|
+
name: 'qq_send_media',
|
|
97
|
+
description: '发送图片/语音/视频到 QQ(私聊),支持本地路径或 URL',
|
|
98
|
+
inputSchema: z.object({
|
|
99
|
+
filePath: z.string().optional().describe('本地文件路径(支持图片/语音/视频)'),
|
|
100
|
+
imageUrl: z.string().optional().describe('图片 URL(与 filePath 二选一)'),
|
|
101
|
+
openid: z.string().optional().describe('用户 openid(不填则发给机器人自己)'),
|
|
102
|
+
content: z.string().optional().describe('内容描述'),
|
|
103
|
+
}),
|
|
104
|
+
execute: async (args) => {
|
|
105
|
+
if (args.imageUrl) {
|
|
106
|
+
return this._sendImageByUrl(args.openid || this._myUserId, args.imageUrl, args.content)
|
|
107
|
+
} else if (args.filePath) {
|
|
108
|
+
return this._sendMedia(args.openid || this._myUserId, args.filePath, args.content)
|
|
109
|
+
}
|
|
110
|
+
return { error: '需要提供 filePath 或 imageUrl' }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// 发送文件
|
|
115
|
+
this.registerTool({
|
|
116
|
+
name: 'qq_send_file',
|
|
117
|
+
description: '发送本地文件到 QQ(私聊)',
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
filePath: z.string().describe('本地文件路径'),
|
|
120
|
+
openid: z.string().optional().describe('用户 openid(不填则发给机器人自己)'),
|
|
121
|
+
content: z.string().optional().describe('文件描述文字'),
|
|
122
|
+
}),
|
|
123
|
+
execute: async (args) => {
|
|
124
|
+
return this._sendFile(args.openid || this._myUserId, args.filePath, args.content)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 发送群聊消息(支持文本或图片)
|
|
129
|
+
this.registerTool({
|
|
130
|
+
name: 'qq_send_group_message',
|
|
131
|
+
description: '发送消息到 QQ 群(文本或本地图片)',
|
|
132
|
+
inputSchema: z.object({
|
|
133
|
+
groupOpenid: z.string().describe('群 openid'),
|
|
134
|
+
message: z.string().optional().describe('文本消息'),
|
|
135
|
+
imagePath: z.string().optional().describe('本地图片路径(与 message 二选一)'),
|
|
136
|
+
}),
|
|
137
|
+
execute: async (args) => {
|
|
138
|
+
if (args.imagePath) {
|
|
139
|
+
return this._sendGroupLocalImage(args.groupOpenid, args.imagePath, args.message)
|
|
140
|
+
} else if (args.message) {
|
|
141
|
+
return this._sendGroupMessage(args.groupOpenid, args.message)
|
|
142
|
+
}
|
|
143
|
+
return { error: '需要提供 message 或 imagePath' }
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 发送私聊文本消息
|
|
150
|
+
*/
|
|
151
|
+
async _sendMessage(openid, content, originalMsg = null) {
|
|
152
|
+
if (!this._client) {
|
|
153
|
+
return { error: 'QQ 机器人未连接' }
|
|
154
|
+
}
|
|
155
|
+
if (!openid) {
|
|
156
|
+
return { error: '未指定用户 openid' }
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
// 如果有 originalMsg,使用流式发送
|
|
160
|
+
if (originalMsg && originalMsg.id) {
|
|
161
|
+
const stream = new StreamSession(
|
|
162
|
+
this._apiClient,
|
|
163
|
+
openid,
|
|
164
|
+
originalMsg.id,
|
|
165
|
+
`stream_${Date.now()}`,
|
|
166
|
+
{
|
|
167
|
+
chunk: () => {},
|
|
168
|
+
done: () => {},
|
|
169
|
+
error: (err) => log.error('Stream error:', err.message),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
await stream.write(content)
|
|
173
|
+
await stream.done()
|
|
174
|
+
} else {
|
|
175
|
+
// 降级为普通发送
|
|
176
|
+
await this._client.sendC2CMessage({ openid, content })
|
|
177
|
+
}
|
|
178
|
+
return { success: true, message: '消息发送成功' }
|
|
179
|
+
} catch (err) {
|
|
180
|
+
log.error(`消息发送失败: ${err.message}`)
|
|
181
|
+
return { error: err.message }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 发送私聊图片(URL 方式)
|
|
187
|
+
*/
|
|
188
|
+
async _sendImageByUrl(openid, imageUrl, content) {
|
|
189
|
+
if (!this._client) {
|
|
190
|
+
return { error: 'QQ 机器人未连接' }
|
|
191
|
+
}
|
|
192
|
+
if (!openid) {
|
|
193
|
+
return { error: '未指定用户 openid' }
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await this._client.sendC2CImageMessage({
|
|
197
|
+
openid,
|
|
198
|
+
imageUrl,
|
|
199
|
+
content,
|
|
200
|
+
})
|
|
201
|
+
return { success: true, message: '图片发送成功' }
|
|
202
|
+
} catch (err) {
|
|
203
|
+
log.error(`图片发送失败: ${err.message}`)
|
|
204
|
+
return { error: err.message }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 根据文件后缀自动发送媒体文件(图片/语音/视频)
|
|
210
|
+
*/
|
|
211
|
+
async _sendMedia(openid, filePath, content) {
|
|
212
|
+
if (!this._client) {
|
|
213
|
+
return { error: 'QQ 机器人未连接' }
|
|
214
|
+
}
|
|
215
|
+
if (!openid) {
|
|
216
|
+
return { error: '未指定用户 openid' }
|
|
217
|
+
}
|
|
218
|
+
if (!fs.existsSync(filePath)) {
|
|
219
|
+
return { error: `文件不存在: ${filePath}` }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
223
|
+
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
|
224
|
+
const voiceExts = ['.silk', '.mp3', '.amr', '.wav', '.m4a']
|
|
225
|
+
const videoExts = ['.mp4', '.avi', '.mov', '.mkv', '.flv']
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
if (imageExts.includes(ext)) {
|
|
229
|
+
await this._client.sendC2CLocalImageMessage({ openid, filePath, content })
|
|
230
|
+
return { success: true, message: '图片发送成功' }
|
|
231
|
+
} else if (voiceExts.includes(ext)) {
|
|
232
|
+
await this._client.sendC2CLocalVoiceMessage({ openid, filePath })
|
|
233
|
+
return { success: true, message: '语音发送成功' }
|
|
234
|
+
} else if (videoExts.includes(ext)) {
|
|
235
|
+
await this._client.sendC2CLocalVideoMessage({ openid, filePath, content })
|
|
236
|
+
return { success: true, message: '视频发送成功' }
|
|
237
|
+
} else {
|
|
238
|
+
// 未知格式,当作文件处理
|
|
239
|
+
return this._sendFile(openid, filePath, content)
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log.error(`媒体发送失败: ${err.message}`)
|
|
243
|
+
return { error: err.message }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 发送私聊文件
|
|
249
|
+
*/
|
|
250
|
+
async _sendFile(openid, filePath, content) {
|
|
251
|
+
if (!this._client) {
|
|
252
|
+
return { error: 'QQ 机器人未连接' }
|
|
253
|
+
}
|
|
254
|
+
if (!openid) {
|
|
255
|
+
return { error: '未指定用户 openid' }
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
if (!fs.existsSync(filePath)) {
|
|
259
|
+
return { error: `文件不存在: ${filePath}` }
|
|
260
|
+
}
|
|
261
|
+
await this._client.sendC2CLocalFileMessage({
|
|
262
|
+
openid,
|
|
263
|
+
filePath,
|
|
264
|
+
content,
|
|
265
|
+
})
|
|
266
|
+
return { success: true, message: '文件发送成功' }
|
|
267
|
+
} catch (err) {
|
|
268
|
+
log.error(`文件发送失败: ${err.message}`)
|
|
269
|
+
return { error: err.message }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 发送群聊文本消息
|
|
275
|
+
*/
|
|
276
|
+
async _sendGroupMessage(groupOpenid, content) {
|
|
277
|
+
if (!this._client) {
|
|
278
|
+
return { error: 'QQ 机器人未连接' }
|
|
279
|
+
}
|
|
280
|
+
if (!groupOpenid) {
|
|
281
|
+
return { error: '未指定群 openid' }
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
await this._client.sendGroupMessage({
|
|
285
|
+
groupOpenid,
|
|
286
|
+
content,
|
|
287
|
+
})
|
|
288
|
+
return { success: true, message: '群消息发送成功' }
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.error(`群消息发送失败: ${err.message}`)
|
|
291
|
+
return { error: err.message }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 发送群本地图片
|
|
297
|
+
*/
|
|
298
|
+
async _sendGroupLocalImage(groupOpenid, imagePath, content) {
|
|
299
|
+
if (!this._client) {
|
|
300
|
+
return { error: 'QQ 机器人未连接' }
|
|
301
|
+
}
|
|
302
|
+
if (!groupOpenid) {
|
|
303
|
+
return { error: '未指定群 openid' }
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
if (!fs.existsSync(imagePath)) {
|
|
307
|
+
return { error: `文件不存在: ${imagePath}` }
|
|
308
|
+
}
|
|
309
|
+
await this._client.sendGroupLocalImageMessage({
|
|
310
|
+
groupOpenid,
|
|
311
|
+
filePath: imagePath,
|
|
312
|
+
content,
|
|
313
|
+
})
|
|
314
|
+
return { success: true, message: '群图片发送成功' }
|
|
315
|
+
} catch (err) {
|
|
316
|
+
log.error(`群图片发送失败: ${err.message}`)
|
|
317
|
+
return { error: err.message }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async start(framework) {
|
|
322
|
+
if (this._initialized) return this
|
|
323
|
+
this._initialized = true
|
|
324
|
+
|
|
325
|
+
this._framework = framework
|
|
326
|
+
this._sessionPlugin = framework.pluginManager.get('session')
|
|
327
|
+
|
|
328
|
+
// 监听 SessionPlugin 的会话删除事件
|
|
329
|
+
if (this._sessionPlugin) {
|
|
330
|
+
this._sessionDeleteHandler = (sessionId) => {
|
|
331
|
+
if (sessionId.startsWith('qq_')) {
|
|
332
|
+
// log.info(`Session deleted: ${sessionId}`)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
this._sessionPlugin.on('session:deleted', this._sessionDeleteHandler)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 监听统一通知事件
|
|
339
|
+
if (this._framework) {
|
|
340
|
+
this._framework.on('webhook:received', async (data) => {
|
|
341
|
+
await this._handleWebhookNotification(data)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
this._framework.on('notification', async (data) => {
|
|
345
|
+
await this._handleNotification(data)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 异步初始化 Bot
|
|
350
|
+
this._initBotAsync().catch(err => {
|
|
351
|
+
log.error('Failed to initialize bot:', err.message)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
return this
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async _initBotAsync() {
|
|
358
|
+
if (!this.config.appId || !this.config.clientSecret) {
|
|
359
|
+
log.error('QQ_APP_ID and QQ_CLIENT_SECRET must be configured')
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this._client = createQQBotClient({
|
|
364
|
+
appId: this.config.appId,
|
|
365
|
+
clientSecret: this.config.clientSecret,
|
|
366
|
+
markdownSupport: true, // 启用 Markdown 支持
|
|
367
|
+
log: {
|
|
368
|
+
info: (msg) => log.info('[QQ]', msg),
|
|
369
|
+
error: (msg) => log.error('[QQ]', msg),
|
|
370
|
+
warn: (msg) => log.warn('[QQ]', msg),
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// 连接后可以从事件中获取机器人信息,先直接连接
|
|
375
|
+
// 获取机器人自身的 openid(用于发送消息)
|
|
376
|
+
// try {
|
|
377
|
+
// const token = await this._client.api.getToken()
|
|
378
|
+
// } catch (err) {
|
|
379
|
+
// log.warn('Failed to get access token:', err.message)
|
|
380
|
+
// }
|
|
381
|
+
|
|
382
|
+
// 监听私聊消息
|
|
383
|
+
this._client.on('C2C_MESSAGE_CREATE', async (event) => {
|
|
384
|
+
await this._handleC2CMessage(event)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
// 监听群聊@消息
|
|
388
|
+
this._client.on('GROUP_AT_MESSAGE_CREATE', async (event) => {
|
|
389
|
+
await this._handleGroupMessage(event)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// 监听按钮交互
|
|
393
|
+
this._client.on('INTERACTION_CREATE', async (event) => {
|
|
394
|
+
await this._handleInteraction(event)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// 启动连接
|
|
398
|
+
await this._client.connect()
|
|
399
|
+
|
|
400
|
+
// 创建独立的 API 客户端(用于流式消息)
|
|
401
|
+
this._apiClient = new QQBotAPIClient({
|
|
402
|
+
appId: this.config.appId,
|
|
403
|
+
clientSecret: this.config.clientSecret,
|
|
404
|
+
markdownSupport: this.config.markdownSupport,
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
log.info('QQ Bot 已连接')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 处理私聊消息
|
|
412
|
+
*/
|
|
413
|
+
async _handleC2CMessage(event) {
|
|
414
|
+
const openid = event.author.user_openid || event.author.id
|
|
415
|
+
const content = event.content?.trim()
|
|
416
|
+
|
|
417
|
+
if (!openid || !content) return
|
|
418
|
+
|
|
419
|
+
// 权限检查
|
|
420
|
+
if (!this._checkPermission(openid)) {
|
|
421
|
+
await this._sendMessage(openid, '你没有权限使用此机器人')
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 记录机器人自己的 openid(用于工具发送)
|
|
426
|
+
if (!this._myUserId) {
|
|
427
|
+
this._myUserId = openid
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 处理命令
|
|
431
|
+
if (content.startsWith('/')) {
|
|
432
|
+
await this._handleCommand(openid, content, event)
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
await this._processChat(openid, content, event, 'c2c')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 处理群聊消息
|
|
441
|
+
*/
|
|
442
|
+
async _handleGroupMessage(event) {
|
|
443
|
+
const groupOpenid = event.group_openid
|
|
444
|
+
const content = event.content?.trim()
|
|
445
|
+
const username = event.author?.username || event.author?.id || 'unknown'
|
|
446
|
+
|
|
447
|
+
if (!groupOpenid || !content) return
|
|
448
|
+
|
|
449
|
+
// 检查是否@了机器人(content 通常包含 @机器人 的前缀)
|
|
450
|
+
// 去掉 @xxx 前缀后处理实际内容
|
|
451
|
+
const actualContent = content.replace(/^\s*@.*?\s*/, '').trim()
|
|
452
|
+
|
|
453
|
+
if (!actualContent) return
|
|
454
|
+
|
|
455
|
+
// 权限检查(可以按群或用户检查)
|
|
456
|
+
// if (!this._checkGroupPermission(groupOpenid, userId)) { ... }
|
|
457
|
+
|
|
458
|
+
// 处理命令
|
|
459
|
+
if (actualContent.startsWith('/')) {
|
|
460
|
+
await this._handleGroupCommand(groupOpenid, username, actualContent, event)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
await this._processChat(groupOpenid, actualContent, event, 'group', username)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 处理按钮交互
|
|
469
|
+
*/
|
|
470
|
+
async _handleInteraction(event) {
|
|
471
|
+
try {
|
|
472
|
+
const { button_data, button_id } = event.raw?.data?.resolved || {}
|
|
473
|
+
log.info(`Button interaction: ${button_id} - ${button_data}`)
|
|
474
|
+
|
|
475
|
+
// 确认交互
|
|
476
|
+
await this._client.acknowledgeInteraction(event.id, 0, {
|
|
477
|
+
msg: `收到按钮点击: ${button_id}`,
|
|
478
|
+
})
|
|
479
|
+
} catch (err) {
|
|
480
|
+
log.error(`Button interaction error: ${err.message}`)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 获取或创建会话 Agent
|
|
486
|
+
*/
|
|
487
|
+
_getSessionAgent(identifier, type = 'c2c') {
|
|
488
|
+
const sessionId = `qq_${type}_${identifier}`
|
|
489
|
+
this.sessionId = sessionId
|
|
490
|
+
|
|
491
|
+
// 检查缓存
|
|
492
|
+
if (this._sessionAgents.has(identifier)) {
|
|
493
|
+
const agent = this._sessionAgents.get(identifier)
|
|
494
|
+
return { agent, sessionId }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const agent = this._framework.createAgent({
|
|
498
|
+
name: `qq_${identifier}`,
|
|
499
|
+
systemPrompt: this.systemPrompt,
|
|
500
|
+
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
501
|
+
metadata: { WORK_DIR: process.cwd(), platform: 'qq', identifier }
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
this._sessionAgents.set(identifier, agent)
|
|
505
|
+
|
|
506
|
+
// 使用 SessionPlugin 管理会话历史
|
|
507
|
+
if (this._sessionPlugin) {
|
|
508
|
+
this._sessionPlugin.getOrCreateSession(sessionId, {
|
|
509
|
+
metadata: { platform: 'qq', identifier, type }
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return { agent, sessionId }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 设置 session scope 事件监听
|
|
518
|
+
*/
|
|
519
|
+
_setupSessionScopeListeners(agent, sessionId, originalMsg, identifier, msgType) {
|
|
520
|
+
const sessionScope = agent.createSessionScope(sessionId)
|
|
521
|
+
const streamSessionMap = this._streamSessions
|
|
522
|
+
|
|
523
|
+
sessionScope.on('stream:chunk', async ({ chunk, isFirst }) => {
|
|
524
|
+
if (!this._client || !originalMsg) return
|
|
525
|
+
|
|
526
|
+
const isGroup = msgType === 'group'
|
|
527
|
+
const openid = isGroup ? null : identifier
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
// 创建流式会话(仅第一次时创建)
|
|
531
|
+
if (isFirst) {
|
|
532
|
+
const msgId = originalMsg.id
|
|
533
|
+
const eventId = `stream_${Date.now()}`
|
|
534
|
+
|
|
535
|
+
let streamSession
|
|
536
|
+
if (msgType === 'group') {
|
|
537
|
+
// 群聊暂不支持流式,降级为普通发送
|
|
538
|
+
return
|
|
539
|
+
} else {
|
|
540
|
+
try {
|
|
541
|
+
streamSession = new StreamSession(
|
|
542
|
+
this._apiClient, // 使用独立的 API 客户端
|
|
543
|
+
openid,
|
|
544
|
+
msgId,
|
|
545
|
+
eventId,
|
|
546
|
+
{
|
|
547
|
+
chunk: (content) => {}, // 累积模式,不需要实时回调
|
|
548
|
+
done: () => {},
|
|
549
|
+
error: (err) => log.error('Stream error:', err.message),
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
} catch (err) {
|
|
553
|
+
log.error('Failed to create stream session:', err.message)
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
streamSessionMap.set(sessionId, streamSession)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const streamSession = streamSessionMap.get(sessionId)
|
|
562
|
+
if (streamSession) {
|
|
563
|
+
try {
|
|
564
|
+
await streamSession.write(chunk)
|
|
565
|
+
} catch (err) {
|
|
566
|
+
log.error('Stream write error:', err.message)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
sessionScope.on('message:complete', async ({ content }) => {
|
|
572
|
+
// 清理定时器
|
|
573
|
+
this.stopTypingInterval?.()
|
|
574
|
+
|
|
575
|
+
const streamSession = streamSessionMap.get(sessionId)
|
|
576
|
+
if (streamSession) {
|
|
577
|
+
try {
|
|
578
|
+
await streamSession.done()
|
|
579
|
+
} catch (err) {
|
|
580
|
+
log.error('Stream done error:', err.message)
|
|
581
|
+
}
|
|
582
|
+
streamSessionMap.delete(sessionId)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (content) {
|
|
586
|
+
if (msgType === 'group') {
|
|
587
|
+
await this._sendGroupMessage(identifier, content)
|
|
588
|
+
} else {
|
|
589
|
+
await this._sendMessage(identifier, content, originalMsg)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
sessionScope.on('queue:failed', async ({ error }) => {
|
|
595
|
+
// 清理定时器
|
|
596
|
+
this.stopTypingInterval?.()
|
|
597
|
+
|
|
598
|
+
// 清理流式会话
|
|
599
|
+
const streamSession = streamSessionMap.get(sessionId)
|
|
600
|
+
if (streamSession) {
|
|
601
|
+
streamSessionMap.delete(sessionId)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (msgType === 'group') {
|
|
605
|
+
await this._sendGroupMessage(identifier, error)
|
|
606
|
+
} else {
|
|
607
|
+
await this._sendMessage(identifier, error, originalMsg)
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
this._sessionScopes.set(sessionId, sessionScope)
|
|
612
|
+
return sessionScope
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 处理对话
|
|
617
|
+
*/
|
|
618
|
+
async _processChat(identifier, text, originalMsg, msgType = 'c2c', username = null) {
|
|
619
|
+
const sessionInfo = this._getSessionAgent(identifier, msgType)
|
|
620
|
+
if (!sessionInfo) {
|
|
621
|
+
log.error('No session agent available')
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const { agent, sessionId } = sessionInfo
|
|
626
|
+
|
|
627
|
+
// 预创建或获取 sessionScope
|
|
628
|
+
let sessionScope = this._sessionScopes.get(sessionId)
|
|
629
|
+
if (!sessionScope) {
|
|
630
|
+
sessionScope = this._setupSessionScopeListeners(agent, sessionId, originalMsg, identifier, msgType)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 添加用户名上下文(群聊时)
|
|
634
|
+
const messageWithContext = username && msgType === 'group'
|
|
635
|
+
? `[群聊消息,来自 ${username}]\n${text}`
|
|
636
|
+
: text
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
await agent.sendMessage(messageWithContext, { sessionId, priority: 1 })
|
|
640
|
+
} catch (err) {
|
|
641
|
+
log.error('Chat error:', err.message)
|
|
642
|
+
if (msgType === 'group') {
|
|
643
|
+
await this._sendGroupMessage(identifier, `发生错误:${err.message}`)
|
|
644
|
+
} else {
|
|
645
|
+
await this._sendMessage(identifier, `发生错误:${err.message}`)
|
|
646
|
+
}
|
|
647
|
+
} finally {
|
|
648
|
+
this.stopTypingInterval?.()
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 处理命令
|
|
654
|
+
*/
|
|
655
|
+
async _handleCommand(openid, text, originalMsg) {
|
|
656
|
+
const parts = text.split(' ')
|
|
657
|
+
const command = parts[0].substring(1)
|
|
658
|
+
const args = parts.slice(1).join(' ')
|
|
659
|
+
|
|
660
|
+
switch (command.toLowerCase()) {
|
|
661
|
+
case 'start':
|
|
662
|
+
case 'help':
|
|
663
|
+
await this._sendMessage(openid,
|
|
664
|
+
'👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/clean - 清除对话上下文\n/history - 查看历史消息数')
|
|
665
|
+
break
|
|
666
|
+
case 'clean':
|
|
667
|
+
case 'clear':
|
|
668
|
+
await this._clearSessionContext(openid)
|
|
669
|
+
await this._sendMessage(openid, '✅ 对话上下文已清除')
|
|
670
|
+
break
|
|
671
|
+
case 'history':
|
|
672
|
+
const sessionInfo = this._getSessionAgent(openid)
|
|
673
|
+
if (sessionInfo && this._framework) {
|
|
674
|
+
const sessionCtx = this._framework.getSessionContext(sessionInfo.sessionId)
|
|
675
|
+
const count = sessionCtx?.messageStore?.messages?.length || 0
|
|
676
|
+
const compressionCount = sessionCtx?.metadata?.compressionCount || 0
|
|
677
|
+
await this._sendMessage(openid,
|
|
678
|
+
`📊 当前状态\n\n历史消息:${count}\n压缩次数:${compressionCount}`)
|
|
679
|
+
}
|
|
680
|
+
break
|
|
681
|
+
default:
|
|
682
|
+
await this._processChat(openid, text, originalMsg, 'c2c')
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* 处理群命令
|
|
688
|
+
*/
|
|
689
|
+
async _handleGroupCommand(groupOpenid, username, text, originalMsg) {
|
|
690
|
+
const parts = text.split(' ')
|
|
691
|
+
const command = parts[0].substring(1)
|
|
692
|
+
const args = parts.slice(1).join(' ')
|
|
693
|
+
|
|
694
|
+
switch (command.toLowerCase()) {
|
|
695
|
+
case 'start':
|
|
696
|
+
case 'help':
|
|
697
|
+
await this._sendGroupMessage(groupOpenid,
|
|
698
|
+
'👋 欢迎使用 AI 助手!\n\n可用命令:\n/clean - 清除对话上下文\n/history - 查看历史消息数')
|
|
699
|
+
break
|
|
700
|
+
case 'clean':
|
|
701
|
+
case 'clear':
|
|
702
|
+
await this._clearSessionContext(groupOpenid)
|
|
703
|
+
await this._sendGroupMessage(groupOpenid, '✅ 对话上下文已清除')
|
|
704
|
+
break
|
|
705
|
+
case 'history':
|
|
706
|
+
const sessionInfo = this._getSessionAgent(groupOpenid, 'group')
|
|
707
|
+
if (sessionInfo && this._framework) {
|
|
708
|
+
const sessionCtx = this._framework.getSessionContext(sessionInfo.sessionId)
|
|
709
|
+
const count = sessionCtx?.messageStore?.messages?.length || 0
|
|
710
|
+
const compressionCount = sessionCtx?.metadata?.compressionCount || 0
|
|
711
|
+
await this._sendGroupMessage(groupOpenid,
|
|
712
|
+
`📊 当前状态\n\n历史消息:${count}\n压缩次数:${compressionCount}`)
|
|
713
|
+
}
|
|
714
|
+
break
|
|
715
|
+
default:
|
|
716
|
+
await this._processChat(groupOpenid, text, originalMsg, 'group', username)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* 清除会话上下文
|
|
722
|
+
*/
|
|
723
|
+
async _clearSessionContext(identifier) {
|
|
724
|
+
const sessionId = `qq_c2c_${identifier}`
|
|
725
|
+
|
|
726
|
+
const { agent } = this._getSessionAgent(identifier)
|
|
727
|
+
|
|
728
|
+
if (agent._chatHandler) {
|
|
729
|
+
agent._chatHandler.clearHistory(sessionId)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (this._framework) {
|
|
733
|
+
const sessionCtx = this._framework.getSessionContext(sessionId)
|
|
734
|
+
if (sessionCtx) {
|
|
735
|
+
sessionCtx.clearMessages()
|
|
736
|
+
sessionCtx.compressionState.count = 0
|
|
737
|
+
sessionCtx.metadata.compressionCount = 0
|
|
738
|
+
}
|
|
739
|
+
const cachedCtx = this._framework._sessionContexts?.get(sessionId)
|
|
740
|
+
if (cachedCtx) {
|
|
741
|
+
cachedCtx.clearMessages()
|
|
742
|
+
cachedCtx.compressionState.count = 0
|
|
743
|
+
cachedCtx.metadata.compressionCount = 0
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* 权限检查
|
|
750
|
+
*/
|
|
751
|
+
_checkPermission(openid) {
|
|
752
|
+
if (!this.config.allowedUsers || this.config.allowedUsers.length === 0) {
|
|
753
|
+
return true
|
|
754
|
+
}
|
|
755
|
+
return this.config.allowedUsers.includes(openid)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 处理 webhook 通知
|
|
760
|
+
*/
|
|
761
|
+
async _handleWebhookNotification(data) {
|
|
762
|
+
const { data: webhookData, response, sessionId } = data
|
|
763
|
+
|
|
764
|
+
if (!this._client) {
|
|
765
|
+
log.warn('Bot not ready, cannot send webhook notification')
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (!sessionId || !sessionId.startsWith('qq_')) {
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const identifier = sessionId.replace('qq_c2c_', '').replace('qq_group_', '')
|
|
774
|
+
const isGroup = sessionId.includes('group_')
|
|
775
|
+
const notificationText = `📥 [Webhook 接收]\n\n路径: ${webhookData.path}\n方法: ${webhookData.method}\n\n处理结果: ${response || '处理中...'}`
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
if (isGroup) {
|
|
779
|
+
await this._sendGroupMessage(identifier, notificationText)
|
|
780
|
+
} else {
|
|
781
|
+
await this._sendMessage(identifier, notificationText)
|
|
782
|
+
}
|
|
783
|
+
} catch (err) {
|
|
784
|
+
log.error(`Failed to send webhook notification: ${err.message}`)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* 处理统一通知
|
|
790
|
+
*/
|
|
791
|
+
async _handleNotification(data) {
|
|
792
|
+
const { title, message, source = '系统消息', level, sessionId } = data
|
|
793
|
+
|
|
794
|
+
if (!this._client) {
|
|
795
|
+
log.warn('Bot not ready, cannot send notification')
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let identifier = null
|
|
800
|
+
let isGroup = false
|
|
801
|
+
|
|
802
|
+
if (sessionId && sessionId.startsWith('qq_')) {
|
|
803
|
+
if (sessionId.includes('group_')) {
|
|
804
|
+
identifier = sessionId.replace('qq_group_', '')
|
|
805
|
+
isGroup = true
|
|
806
|
+
} else {
|
|
807
|
+
identifier = sessionId.replace('qq_c2c_', '')
|
|
808
|
+
}
|
|
809
|
+
} else if (this._sessionPlugin) {
|
|
810
|
+
const sessions = this._sessionPlugin.listSessions()
|
|
811
|
+
.filter(s => s.id.startsWith('qq_'))
|
|
812
|
+
.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
|
|
813
|
+
if (sessions.length > 0) {
|
|
814
|
+
const s = sessions[0]
|
|
815
|
+
if (s.id.includes('group_')) {
|
|
816
|
+
identifier = s.id.replace('qq_group_', '')
|
|
817
|
+
isGroup = true
|
|
818
|
+
} else {
|
|
819
|
+
identifier = s.id.replace('qq_c2c_', '')
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (!identifier) {
|
|
825
|
+
log.warn('No QQ session found for notification')
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const levelEmoji = {
|
|
830
|
+
info: 'ℹ️',
|
|
831
|
+
warning: '⚠️',
|
|
832
|
+
success: '✅',
|
|
833
|
+
error: '❌'
|
|
834
|
+
}
|
|
835
|
+
const emoji = levelEmoji[level] || 'ℹ️'
|
|
836
|
+
const notificationText = `${emoji} [${source}] ${title || '通知'}\n\n${message || ''}`
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
if (isGroup) {
|
|
840
|
+
await this._sendGroupMessage(identifier, notificationText)
|
|
841
|
+
} else {
|
|
842
|
+
await this._sendMessage(identifier, notificationText)
|
|
843
|
+
}
|
|
844
|
+
} catch (err) {
|
|
845
|
+
log.error(`Failed to send notification: ${err.message}`)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* 获取插件状态
|
|
851
|
+
*/
|
|
852
|
+
getStatus() {
|
|
853
|
+
let sessions = []
|
|
854
|
+
if (this._sessionPlugin) {
|
|
855
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
856
|
+
sessions = allSessions
|
|
857
|
+
.filter(s => s.id.startsWith('qq_'))
|
|
858
|
+
.map(s => ({
|
|
859
|
+
id: s.id,
|
|
860
|
+
historyLength: s.messageCount,
|
|
861
|
+
lastActive: s.lastActive
|
|
862
|
+
}))
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
connected: !!this._client,
|
|
867
|
+
sessionCount: sessions.length,
|
|
868
|
+
sessions,
|
|
869
|
+
config: {
|
|
870
|
+
appId: this.config.appId ? `${this.config.appId.substring(0, 8)}...` : 'not set',
|
|
871
|
+
allowedUsersCount: this.config.allowedUsers?.length || 0,
|
|
872
|
+
markdownSupport: this.config.markdownSupport,
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* 停止 Bot
|
|
879
|
+
*/
|
|
880
|
+
stopBot() {
|
|
881
|
+
if (this._client) {
|
|
882
|
+
this._client.disconnect()
|
|
883
|
+
this._client = null
|
|
884
|
+
log.info('Bot stopped')
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
reload(framework) {
|
|
889
|
+
this._framework = framework
|
|
890
|
+
this._initialized = false
|
|
891
|
+
this.stopBot()
|
|
892
|
+
this.start(framework)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
uninstall(framework) {
|
|
896
|
+
// 清理所有流式会话
|
|
897
|
+
for (const streamSession of this._streamSessions.values()) {
|
|
898
|
+
try {
|
|
899
|
+
streamSession.done?.()
|
|
900
|
+
} catch {}
|
|
901
|
+
}
|
|
902
|
+
this._streamSessions.clear()
|
|
903
|
+
|
|
904
|
+
// 销毁所有 session agents
|
|
905
|
+
for (const agent of this._sessionAgents.values()) {
|
|
906
|
+
agent.destroy()
|
|
907
|
+
}
|
|
908
|
+
this._sessionAgents.clear()
|
|
909
|
+
this._sessionScopes.clear()
|
|
910
|
+
this.stopBot()
|
|
911
|
+
|
|
912
|
+
if (this._sessionPlugin && this._sessionDeleteHandler) {
|
|
913
|
+
this._sessionPlugin.off('session:deleted', this._sessionDeleteHandler)
|
|
914
|
+
this._sessionDeleteHandler = null
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
this._sessionPlugin = null
|
|
918
|
+
this._framework = null
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// 简单的 interval 工具函数(从 cli/src/utils/debounce 引入)
|
|
923
|
+
function interval(fn, ms) {
|
|
924
|
+
let timer = null
|
|
925
|
+
const start = () => {
|
|
926
|
+
stop()
|
|
927
|
+
fn()
|
|
928
|
+
timer = setInterval(fn, ms)
|
|
929
|
+
}
|
|
930
|
+
const stop = () => {
|
|
931
|
+
if (timer) {
|
|
932
|
+
clearInterval(timer)
|
|
933
|
+
timer = null
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { start, stop }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
module.exports = QQPlugin
|