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.
Files changed (165) hide show
  1. package/.agent/agents/network-requester.md +44 -0
  2. package/.agent/agents/poster-designer.md +52 -0
  3. package/.agent/agents/ui-designer.md +1 -1
  4. package/.agent/data/default.json +23 -407
  5. package/.agent/data/email/processed-emails.json +1 -0
  6. package/.agent/data/plugins-state.json +103 -200
  7. package/.agent/data/web/web-config.json +5 -0
  8. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  9. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  10. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  11. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  12. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  13. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  14. package/.agent/mcp_config.json +4 -17
  15. package/.agent/memory/user/mof6gk94-kneeuh.md +9 -0
  16. package/.agent/package.json +8 -0
  17. package/.agent/plugins/marknative/README.md +134 -0
  18. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  19. package/.agent/plugins/marknative/fonts.zip +0 -0
  20. package/.agent/plugins/marknative/index.js +256 -0
  21. package/.agent/plugins/marknative/package.json +12 -0
  22. package/.agent/plugins/test-plugin.py +99 -0
  23. package/.agent/plugins.json +11 -5
  24. package/.agent/python-scripts/test_sample.py +24 -0
  25. package/.agent/sessions/cli_default.json +2890 -18
  26. package/.agent/sessions/default.json +82 -0
  27. package/.agent/sessions/qq_c2c_D960F12877541624D7B2C836110B3D0F.json +25 -0
  28. package/.agent/sessions/test-clean.json +26 -0
  29. package/.agent/sessions/test-compress.json +1462 -0
  30. package/.agent/sessions/test-session.json +13 -0
  31. package/.agent/sessions/weixin_test.json +22 -0
  32. package/.agent/sessions/weixin_test_user_123.json +11 -0
  33. package/.agent/skills/agent-browser/SKILL.md +311 -0
  34. package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
  35. package/.agent/skills/sysinfo/SKILL.md +38 -0
  36. package/.agent/skills/sysinfo/system-info.sh +130 -0
  37. package/.agent/skills/workflow/SKILL.md +324 -0
  38. package/.agent/test-agent.js +35 -0
  39. package/.agent/weixin.json +6 -0
  40. package/.agent/workflows/email-digest.json +50 -0
  41. package/.agent/workflows/file-backup.json +21 -0
  42. package/.agent/workflows/get-ip-notify.json +32 -0
  43. package/.agent/workflows/news-aggregator.json +93 -0
  44. package/.agent/workflows/news-dashboard-v2.json +94 -0
  45. package/.agent/workflows/notification-batch.json +32 -0
  46. package/.claude/settings.local.json +2 -1
  47. package/.env.example +56 -56
  48. package/README.md +441 -441
  49. package/cli/src/utils/debounce.js +6 -3
  50. package/docs/qq-bot.md +976 -0
  51. package/package.json +2 -1
  52. package/plugins/qq-plugin.js +939 -0
  53. package/skills/find-skills/AGENTS.md +162 -162
  54. package/skills/find-skills/SKILL.md +133 -133
  55. package/temp_img.md +1 -0
  56. package/website_v2/styles/animations.css +7 -7
  57. package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
  58. package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
  59. package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
  60. package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
  61. package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
  62. package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
  63. package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
  64. package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  65. package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  66. package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
  67. package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  68. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  69. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  70. package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  71. package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
  72. package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
  73. package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  74. package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  75. package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  76. package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
  77. package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
  78. package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
  79. package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  80. package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
  81. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
  82. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
  83. package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
  84. package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
  85. package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
  86. package/.agent/ARCHITECTURE.md +0 -288
  87. package/.agent/data/ambient/goals.json +0 -1
  88. package/.agent/data/puppeteer-sessions/undefined.json +0 -6
  89. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  90. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  91. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  92. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  93. package/.agent/memory/feedback/mnygjgox-ualjip.md +0 -11
  94. package/.agent/memory/feedback/mnzugpej-esdwsr.md +0 -9
  95. package/.agent/memory/feedback/mo2quxke-saxs56.md +0 -9
  96. package/.agent/memory/feedback/mo9l6nw9-sm1ye2.md +0 -13
  97. package/.agent/memory/feedback/mo9uxkb3-bbxz3x.md +0 -9
  98. package/.agent/memory/feedback/mo9wpqy0-ftsez9.md +0 -13
  99. package/.agent/memory/feedback/mo9x2ps4-p7huqq.md +0 -11
  100. package/.agent/memory/feedback/mo9x2uay-z7ndjn.md +0 -9
  101. package/.agent/memory/feedback/mo9x2y7d-9wecyx.md +0 -9
  102. package/.agent/memory/feedback/mo9x39q3-nn7myt.md +0 -9
  103. package/.agent/memory/feedback/mo9xfir9-4g8a8j.md +0 -9
  104. package/.agent/memory/feedback/mo9xfui8-ujqrc6.md +0 -9
  105. package/.agent/memory/feedback/mocnx1wx-qtxxlh.md +0 -9
  106. package/.agent/memory/project/mnqx54u5-loqtoe.md +0 -9
  107. package/.agent/memory/project/mnqx84cv-mx6dmd.md +0 -9
  108. package/.agent/memory/project/mnsacuyr-hgtk5n.md +0 -20
  109. package/.agent/memory/project/mnu5hy2x-bjsg7u.md +0 -9
  110. package/.agent/memory/project/mny28ot4-8qe9au.md +0 -9
  111. package/.agent/memory/project/mnztftxj-af82rh.md +0 -9
  112. package/.agent/memory/project/mo0y04d0-bmaefl.md +0 -9
  113. package/.agent/memory/project/mo2iztxb-3c7v81.md +0 -9
  114. package/.agent/memory/project/mo2ssa5x-tpi1p3.md +0 -9
  115. package/.agent/memory/reference/mnre3cww-penbo1.md +0 -9
  116. package/.agent/memory/reference/mns9wn48-luerua.md +0 -14
  117. package/.agent/memory/reference/mns9yz5c-thc2s0.md +0 -16
  118. package/.agent/memory/reference/mnsfy4um-910f1o.md +0 -23
  119. package/.agent/memory/reference/mnsg37dp-lmfj18.md +0 -32
  120. package/.agent/memory/reference/mnsll60q-0j911u.md +0 -36
  121. package/.agent/memory/reference/mnsmlb5y-nej31u.md +0 -16
  122. package/.agent/memory/reference/mnssle72-yrot96.md +0 -9
  123. package/.agent/memory/reference/mnygj8nb-bjthmc.md +0 -20
  124. package/.agent/memory/reference/mnzfvs4m-ufyg9a.md +0 -12
  125. package/.agent/memory/user/mnsfuon6-l416q1.md +0 -21
  126. package/.agent/memory/user/mnsg9kut-95m7rf.md +0 -20
  127. package/.agent/memory/user/mnu2eo1v-yy6fhe.md +0 -9
  128. package/.agent/memory/user/mnu2etuo-8u8jk8.md +0 -9
  129. package/.agent/memory/user/mnx0rk6g-gsznjj.md +0 -9
  130. package/.agent/memory/user/mnyf1riz-4yo5yz.md +0 -9
  131. package/.agent/memory/user/mnzuh4cw-zvee8w.md +0 -13
  132. package/.agent/memory/user/mnzvewyj-jl67cq.md +0 -9
  133. package/.agent/memory/user/mnzwh9xo-43ys3f.md +0 -9
  134. package/.agent/memory/user/mo0ycvpn-eebsxc.md +0 -9
  135. package/.agent/memory/user/mo2k8c8n-132r2u.md +0 -9
  136. package/.agent/memory/user/mo6y2dei-5cf537.md +0 -13
  137. package/.agent/memory/user/mo9xsdo6-8vylww.md +0 -13
  138. package/.agent/plugins/puppeteer-plugin/README.md +0 -147
  139. package/.agent/plugins/puppeteer-plugin/index.js +0 -1422
  140. package/.agent/plugins/puppeteer-plugin/package.json +0 -9
  141. package/.agent/rules/GEMINI.md +0 -273
  142. package/.agent/rules/allow-rule.md +0 -77
  143. package/.agent/rules/log-rule.md +0 -83
  144. package/.agent/rules/security-rule.md +0 -93
  145. package/.agent/scripts/auto_preview.py +0 -148
  146. package/.agent/scripts/checklist.py +0 -217
  147. package/.agent/scripts/session_manager.py +0 -120
  148. package/.agent/scripts/verify_all.py +0 -327
  149. package/.agent/skills/doc.md +0 -177
  150. package/.agent/skills/fk-poster/SKILL.md +0 -1129
  151. package/.agent/skills/fkbuilder/SKILL.md +0 -730
  152. package/.agent/skills/mmx-cli/SKILL.md +0 -431
  153. package/.agent/workflows/brainstorm.md +0 -113
  154. package/.agent/workflows/create.md +0 -59
  155. package/.agent/workflows/debug.md +0 -103
  156. package/.agent/workflows/deploy.md +0 -176
  157. package/.agent/workflows/enhance.md +0 -63
  158. package/.agent/workflows/orchestrate.md +0 -237
  159. package/.agent/workflows/plan.md +0 -89
  160. package/.agent/workflows/preview.md +0 -81
  161. package/.agent/workflows/simple-test.md +0 -42
  162. package/.agent/workflows/status.md +0 -86
  163. package/.agent/workflows/structured-orchestrate.md +0 -180
  164. package/.agent/workflows/test.md +0 -144
  165. 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