foliko 1.0.14 → 1.0.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/.claude/settings.local.json +19 -1
- package/cli/src/ui/chat-ui.js +30 -2
- package/package.json +4 -1
- package/plugins/default-plugins.js +40 -3
- package/plugins/telegram-plugin.js +88 -56
- package/plugins/weixin-plugin.js +326 -0
|
@@ -37,7 +37,25 @@
|
|
|
37
37
|
"Bash(node -c plugins/default-plugins.js && echo \"Syntax OK\")",
|
|
38
38
|
"Bash(node -c plugins/scheduler-plugin.js && echo \"Syntax OK\")",
|
|
39
39
|
"Bash(node -c plugins/telegram-plugin.js && node -c plugins/scheduler-plugin.js && echo \"Syntax OK\")",
|
|
40
|
-
"Bash(node -c plugins/telegram-plugin.js && echo \"Syntax OK\")"
|
|
40
|
+
"Bash(node -c plugins/telegram-plugin.js && echo \"Syntax OK\")",
|
|
41
|
+
"WebSearch",
|
|
42
|
+
"Bash(npm ls:*)",
|
|
43
|
+
"Bash(cd node_modules/@pinixai/weixin-bot && npm run build 2>&1)",
|
|
44
|
+
"Bash(cd node_modules/@pinixai/weixin-bot && npx tsc 2>&1)",
|
|
45
|
+
"Bash(npm install:*)",
|
|
46
|
+
"Bash(cd node_modules/@pinixai/weixin-bot && npx typescript --version && npx tsc 2>&1)",
|
|
47
|
+
"Bash(npx tsc:*)",
|
|
48
|
+
"Bash(node --check /d/Code/vb-agent/plugins/weixin-plugin.js 2>&1)",
|
|
49
|
+
"Bash(node --check /d/Code/vb-agent/plugins/telegram-plugin.js && node --check /d/Code/vb-agent/plugins/weixin-plugin.js && echo \"OK\")",
|
|
50
|
+
"Bash(node --check /d/Code/vb-agent/cli/src/ui/chat-ui.js && echo \"OK\")",
|
|
51
|
+
"Bash(npm uninstall:*)",
|
|
52
|
+
"Bash(rm -rf /tmp/weixin-bot && git clone https://github.com/chnak/weixin-bot.git /tmp/weixin-bot 2>&1)",
|
|
53
|
+
"Bash(mkdir -p node_modules/@pinixai/weixin-bot && cp -r /tmp/weixin-bot/nodejs/* node_modules/@pinixai/weixin-bot/ && cd node_modules/@pinixai/weixin-bot && npm install 2>&1)",
|
|
54
|
+
"Bash(node --check /d/Code/vb-agent/plugins/weixin-plugin.js && echo \"OK\")",
|
|
55
|
+
"Bash(node -e \"import\\('@pinixai/weixin-bot'\\).then\\(m => console.log\\('OK:', Object.keys\\(m\\)\\)\\).catch\\(e => console.error\\('Error:', e.message\\)\\)\")",
|
|
56
|
+
"Bash(npm run:*)",
|
|
57
|
+
"Bash(node -e \"const { WeixinBot } = require\\('@pinixai/weixin-bot'\\); console.log\\(typeof WeixinBot\\)\" 2>&1)",
|
|
58
|
+
"Bash(node -e \"import\\('@pinixai/weixin-bot'\\).then\\(m => console.log\\('OK:', typeof m.WeixinBot\\)\\).catch\\(e => console.error\\('Error:', e.message\\)\\)\" 2>&1)"
|
|
41
59
|
]
|
|
42
60
|
}
|
|
43
61
|
}
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -8,10 +8,25 @@ const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils
|
|
|
8
8
|
const { renderLine } = require('../utils/markdown')
|
|
9
9
|
|
|
10
10
|
class ChatUI {
|
|
11
|
-
constructor(agent) {
|
|
11
|
+
constructor(agent, options = {}) {
|
|
12
12
|
this.agent = agent
|
|
13
13
|
this.rl = null
|
|
14
14
|
this.lines = [] // 多行输入的累积
|
|
15
|
+
|
|
16
|
+
// 会话管理
|
|
17
|
+
this.sessionId = options.sessionId || 'cli_default'
|
|
18
|
+
this.sessionPlugin = null
|
|
19
|
+
|
|
20
|
+
// 如果 agent 有 framework,获取 SessionPlugin
|
|
21
|
+
if (agent.framework) {
|
|
22
|
+
this.sessionPlugin = agent.framework.pluginManager.get('session')
|
|
23
|
+
// 确保会话存在
|
|
24
|
+
if (this.sessionPlugin) {
|
|
25
|
+
this.sessionPlugin.getOrCreateSession(this.sessionId, {
|
|
26
|
+
metadata: { platform: 'cli' }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
/**
|
|
@@ -106,6 +121,11 @@ class ChatUI {
|
|
|
106
121
|
async sendMessage(message) {
|
|
107
122
|
console.log()
|
|
108
123
|
|
|
124
|
+
// 添加用户消息到会话历史
|
|
125
|
+
if (this.sessionPlugin) {
|
|
126
|
+
this.sessionPlugin.addMessage(this.sessionId, { role: 'user', content: message })
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
// 用于打断的标志
|
|
110
130
|
let interrupted = false
|
|
111
131
|
|
|
@@ -120,6 +140,8 @@ class ChatUI {
|
|
|
120
140
|
// 监听 Ctrl+C
|
|
121
141
|
process.on('SIGINT', interruptHandler)
|
|
122
142
|
|
|
143
|
+
let fullResponse = ''
|
|
144
|
+
|
|
123
145
|
try {
|
|
124
146
|
let lineBuffer = ''
|
|
125
147
|
const renderState = { inThink: false, inCodeBlock: false }
|
|
@@ -127,10 +149,11 @@ class ChatUI {
|
|
|
127
149
|
console.log(colored('Agent:', GREEN))
|
|
128
150
|
console.log()
|
|
129
151
|
|
|
130
|
-
for await (const chunk of this.agent.chatStream(message)) {
|
|
152
|
+
for await (const chunk of this.agent.chatStream(message, { sessionId: this.sessionId })) {
|
|
131
153
|
if (interrupted) break
|
|
132
154
|
|
|
133
155
|
if (chunk.type === 'text') {
|
|
156
|
+
fullResponse += chunk.text
|
|
134
157
|
lineBuffer += chunk.text
|
|
135
158
|
|
|
136
159
|
while (lineBuffer.includes('\n')) {
|
|
@@ -159,6 +182,11 @@ class ChatUI {
|
|
|
159
182
|
}
|
|
160
183
|
} finally {
|
|
161
184
|
process.removeListener('SIGINT', interruptHandler)
|
|
185
|
+
|
|
186
|
+
// 添加助手回复到会话历史
|
|
187
|
+
if (this.sessionPlugin && fullResponse) {
|
|
188
|
+
this.sessionPlugin.addMessage(this.sessionId, { role: 'assistant', content: fullResponse })
|
|
189
|
+
}
|
|
162
190
|
}
|
|
163
191
|
}
|
|
164
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foliko",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "简约的插件化 Agent 框架",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"@ai-sdk/openai-compatible": "^2.0.35",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
|
+
"@pinixai/weixin-bot": "github:chnak/weixin-bot",
|
|
29
30
|
"ai": "^6.0.116",
|
|
30
31
|
"dotenv": "^17.3.1",
|
|
31
32
|
"imap": "^0.8.19",
|
|
@@ -35,6 +36,8 @@
|
|
|
35
36
|
"node-cron": "^4.2.1",
|
|
36
37
|
"node-telegram-bot-api": "^0.67.0",
|
|
37
38
|
"nodemailer": "^6.10.0",
|
|
39
|
+
"qrcode-terminal": "^0.12.0",
|
|
40
|
+
"weixin-bot": "github:chnak/weixin-bot",
|
|
38
41
|
"zod": "^3.24.0"
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -71,17 +71,36 @@ function loadAgentConfig(agentDir = '.agent') {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// 加载 plugins.json
|
|
74
|
+
// 加载 plugins.json (支持 telegram, weixin 等插件配置)
|
|
75
75
|
const pluginsFile = path.join(resolvedDir, 'plugins.json')
|
|
76
76
|
if (fs.existsSync(pluginsFile)) {
|
|
77
77
|
try {
|
|
78
78
|
const content = fs.readFileSync(pluginsFile, 'utf-8')
|
|
79
|
-
|
|
79
|
+
const pluginsConfig = JSON.parse(content)
|
|
80
|
+
// telegram 配置
|
|
81
|
+
if (pluginsConfig.telegram) {
|
|
82
|
+
config.telegram = pluginsConfig.telegram
|
|
83
|
+
}
|
|
84
|
+
// weixin 配置
|
|
85
|
+
if (pluginsConfig.weixin) {
|
|
86
|
+
config.weixin = pluginsConfig.weixin
|
|
87
|
+
}
|
|
80
88
|
} catch (err) {
|
|
81
89
|
console.error(`[AgentConfig] Failed to load plugins.json:`, err.message)
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
// 加载 weixin.json (如果存在)
|
|
94
|
+
const weixinFile = path.join(resolvedDir, 'weixin.json')
|
|
95
|
+
if (fs.existsSync(weixinFile)) {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(weixinFile, 'utf-8')
|
|
98
|
+
config.weixin = { ...config.weixin, ...JSON.parse(content) }
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`[AgentConfig] Failed to load weixin.json:`, err.message)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
// 加载 mcp_config.json
|
|
86
105
|
const mcpFile = path.join(resolvedDir, 'mcp_config.json')
|
|
87
106
|
if (fs.existsSync(mcpFile)) {
|
|
@@ -187,7 +206,7 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
187
206
|
'install', 'ai', 'storage', 'tools', 'workflow', 'skill-manager',
|
|
188
207
|
'mcp-executor', 'shell-executor', 'python-executor', 'session',
|
|
189
208
|
'audit', 'rules', 'scheduler', 'file-system', 'think',
|
|
190
|
-
'python-plugin-loader', 'telegram'
|
|
209
|
+
'python-plugin-loader', 'telegram', 'weixin'
|
|
191
210
|
])
|
|
192
211
|
|
|
193
212
|
// 辅助函数:检查插件是否应该加载(核心插件不能禁用)
|
|
@@ -375,6 +394,24 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
375
394
|
}
|
|
376
395
|
}
|
|
377
396
|
|
|
397
|
+
// 12.7 WeChat 插件(始终加载,由配置控制是否扫码)
|
|
398
|
+
if (shouldLoad('weixin')) {
|
|
399
|
+
try {
|
|
400
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
401
|
+
const createWeixinPlugin = require('./weixin-plugin')
|
|
402
|
+
const WeixinPlugin = createWeixinPlugin(Plugin)
|
|
403
|
+
const weixinConfig = {
|
|
404
|
+
forceLogin: agentConfig.weixin?.forceLogin || false,
|
|
405
|
+
qrcodeTerminal: agentConfig.weixin?.qrcodeTerminal !== false,
|
|
406
|
+
allowedUsers: agentConfig.weixin?.allowedUsers || []
|
|
407
|
+
}
|
|
408
|
+
await framework.loadPlugin(new WeixinPlugin(weixinConfig))
|
|
409
|
+
console.log('[Bootstrap] WeChat Plugin loaded')
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.warn('[Bootstrap] WeChat Plugin not available:', err.message)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
378
415
|
// 13. 加载自定义插件
|
|
379
416
|
await loadCustomPlugins(framework, agentConfig)
|
|
380
417
|
|
|
@@ -38,8 +38,8 @@ module.exports = function(Plugin) {
|
|
|
38
38
|
|
|
39
39
|
this._framework = null
|
|
40
40
|
this._bot = null
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
41
|
+
this._sessionPlugin = null
|
|
42
|
+
this._agentsCache = new Map() // chatId -> agent (保留 agent 引用)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
install(framework) {
|
|
@@ -53,6 +53,9 @@ module.exports = function(Plugin) {
|
|
|
53
53
|
return this
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// 获取 SessionPlugin 引用
|
|
57
|
+
this._sessionPlugin = framework.pluginManager.get('session')
|
|
58
|
+
|
|
56
59
|
this._initBot()
|
|
57
60
|
return this
|
|
58
61
|
}
|
|
@@ -140,21 +143,22 @@ module.exports = function(Plugin) {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// 其他情况(包括 null 或其他 sessionId),发送到最近的 Telegram 会话
|
|
143
|
-
if (this.
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
if (this._sessionPlugin) {
|
|
147
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
148
|
+
const telegramSessions = allSessions
|
|
149
|
+
.filter(s => s.id.startsWith('telegram_'))
|
|
150
|
+
.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
|
|
151
|
+
|
|
152
|
+
if (telegramSessions.length > 0) {
|
|
153
|
+
const chatId = telegramSessions[0].id.replace('telegram_', '')
|
|
154
|
+
try {
|
|
155
|
+
await this._bot.sendMessage(chatId, reminderText)
|
|
156
|
+
console.log(`[Telegram] Reminder sent to chat ${chatId}`)
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(`[Telegram] Failed to send reminder to ${chatId}:`, err.message)
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log('[Telegram] No active Telegram sessions to send reminder')
|
|
158
162
|
}
|
|
159
163
|
} else {
|
|
160
164
|
console.log('[Telegram] No active Telegram sessions to send reminder')
|
|
@@ -181,30 +185,35 @@ module.exports = function(Plugin) {
|
|
|
181
185
|
|
|
182
186
|
/**
|
|
183
187
|
* 获取或创建会话Agent
|
|
188
|
+
* 使用 SessionPlugin 统一管理会话历史
|
|
184
189
|
*/
|
|
185
190
|
_getSessionAgent(chatId) {
|
|
186
191
|
console.log('[Telegram] _getSessionAgent called for chatId:', chatId)
|
|
187
|
-
let session = this._sessions.get(chatId)
|
|
188
192
|
|
|
189
|
-
|
|
193
|
+
// 检查缓存的 agent 引用
|
|
194
|
+
let agent = this._agentsCache.get(chatId)
|
|
195
|
+
if (!agent) {
|
|
190
196
|
const mainAgent = this._getMainAgent()
|
|
191
197
|
console.log('[Telegram] mainAgent:', !!mainAgent, mainAgent?.name)
|
|
192
198
|
if (!mainAgent) {
|
|
193
199
|
console.log('[Telegram] ERROR: mainAgent is null!')
|
|
194
200
|
return null
|
|
195
201
|
}
|
|
202
|
+
agent = mainAgent
|
|
203
|
+
this._agentsCache.set(chatId, agent)
|
|
204
|
+
}
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
console.log(`[Telegram] New session created for: ${chatId}`)
|
|
206
|
+
// 使用 SessionPlugin 管理会话历史
|
|
207
|
+
if (this._sessionPlugin) {
|
|
208
|
+
const sessionId = `telegram_${chatId}`
|
|
209
|
+
// 确保会话存在(不会重复创建)
|
|
210
|
+
this._sessionPlugin.getOrCreateSession(sessionId, {
|
|
211
|
+
metadata: { platform: 'telegram', chatId }
|
|
212
|
+
})
|
|
205
213
|
}
|
|
206
214
|
|
|
207
|
-
|
|
215
|
+
console.log(`[Telegram] Session ready for chatId: ${chatId}`)
|
|
216
|
+
return { agent, sessionId: `telegram_${chatId}` }
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
/**
|
|
@@ -280,12 +289,19 @@ module.exports = function(Plugin) {
|
|
|
280
289
|
break
|
|
281
290
|
|
|
282
291
|
case 'history':
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
if (this._sessionPlugin) {
|
|
293
|
+
const sessionId = `telegram_${chatId}`
|
|
294
|
+
const session = this._sessionPlugin.getSession(sessionId)
|
|
295
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
296
|
+
const telegramSessionCount = allSessions.filter(s => s.id.startsWith('telegram_')).length
|
|
297
|
+
await this._sendMessage(chatId,
|
|
298
|
+
`📊 当前状态\n\n会话数:${telegramSessionCount}\n` +
|
|
299
|
+
`历史消息:${session?.messages.length || 0}\n` +
|
|
300
|
+
`最后活跃:${session?.lastActive?.toLocaleString() || '无'}`,
|
|
301
|
+
msg.message_id)
|
|
302
|
+
} else {
|
|
303
|
+
await this._sendMessage(chatId, '❌ 会话服务不可用', msg.message_id)
|
|
304
|
+
}
|
|
289
305
|
break
|
|
290
306
|
|
|
291
307
|
default:
|
|
@@ -299,16 +315,19 @@ module.exports = function(Plugin) {
|
|
|
299
315
|
*/
|
|
300
316
|
async _processChat(chatId, text, replyToMessageId) {
|
|
301
317
|
console.log('[Telegram] _processChat called, chatId:', chatId, 'text:', text.substring(0, 50))
|
|
302
|
-
const
|
|
303
|
-
console.log('[Telegram] session:', !!
|
|
304
|
-
if (!
|
|
318
|
+
const sessionInfo = this._getSessionAgent(chatId)
|
|
319
|
+
console.log('[Telegram] session:', !!sessionInfo)
|
|
320
|
+
if (!sessionInfo) {
|
|
305
321
|
await this._sendMessage(chatId, '❌ AI 服务未初始化', replyToMessageId)
|
|
306
322
|
return
|
|
307
323
|
}
|
|
308
324
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
const { agent, sessionId } = sessionInfo
|
|
326
|
+
|
|
327
|
+
// 使用 SessionPlugin 添加用户消息到历史
|
|
328
|
+
if (this._sessionPlugin) {
|
|
329
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
|
|
330
|
+
}
|
|
312
331
|
|
|
313
332
|
// 发送思考中消息
|
|
314
333
|
let thinkingMsg
|
|
@@ -323,11 +342,10 @@ module.exports = function(Plugin) {
|
|
|
323
342
|
|
|
324
343
|
try {
|
|
325
344
|
let fullResponse = ''
|
|
326
|
-
const chatIdStr = `telegram_${chatId}`
|
|
327
345
|
|
|
328
346
|
// 使用流式响应
|
|
329
|
-
for await (const chunk of
|
|
330
|
-
sessionId:
|
|
347
|
+
for await (const chunk of agent.chatStream(text, {
|
|
348
|
+
sessionId: sessionId
|
|
331
349
|
})) {
|
|
332
350
|
// chatStream 返回 { type: 'text', text } 或 { type: 'tool-call', toolName, args }
|
|
333
351
|
if (chunk.type === 'text' && chunk.text) {
|
|
@@ -347,8 +365,10 @@ module.exports = function(Plugin) {
|
|
|
347
365
|
}
|
|
348
366
|
}
|
|
349
367
|
|
|
350
|
-
//
|
|
351
|
-
|
|
368
|
+
// 保存助手回复到历史(使用 SessionPlugin)
|
|
369
|
+
if (this._sessionPlugin) {
|
|
370
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
|
|
371
|
+
}
|
|
352
372
|
|
|
353
373
|
// 发送最终回复(转义 Markdown 特殊字符)
|
|
354
374
|
const safeResponse = escapeMarkdown(fullResponse) || '抱歉,我没有收到有效的回复。'
|
|
@@ -501,11 +521,13 @@ module.exports = function(Plugin) {
|
|
|
501
521
|
* 清除会话
|
|
502
522
|
*/
|
|
503
523
|
_clearSession(chatId) {
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
524
|
+
// 清除 SessionPlugin 中的会话
|
|
525
|
+
if (this._sessionPlugin) {
|
|
526
|
+
const sessionId = `telegram_${chatId}`
|
|
527
|
+
this._sessionPlugin.deleteSession(sessionId)
|
|
507
528
|
}
|
|
508
|
-
|
|
529
|
+
// 清除 agent 缓存
|
|
530
|
+
this._agentsCache.delete(chatId)
|
|
509
531
|
}
|
|
510
532
|
|
|
511
533
|
/**
|
|
@@ -522,14 +544,23 @@ module.exports = function(Plugin) {
|
|
|
522
544
|
* 获取插件状态
|
|
523
545
|
*/
|
|
524
546
|
getStatus() {
|
|
547
|
+
// 从 SessionPlugin 获取 Telegram 相关会话
|
|
548
|
+
let sessions = []
|
|
549
|
+
if (this._sessionPlugin) {
|
|
550
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
551
|
+
sessions = allSessions
|
|
552
|
+
.filter(s => s.id.startsWith('telegram_'))
|
|
553
|
+
.map(s => ({
|
|
554
|
+
chatId: s.id.replace('telegram_', ''),
|
|
555
|
+
historyLength: s.messageCount,
|
|
556
|
+
lastActive: s.lastActive
|
|
557
|
+
}))
|
|
558
|
+
}
|
|
559
|
+
|
|
525
560
|
return {
|
|
526
561
|
connected: !!this._bot,
|
|
527
|
-
sessionCount:
|
|
528
|
-
sessions
|
|
529
|
-
chatId,
|
|
530
|
-
historyLength: session.history.length,
|
|
531
|
-
lastActive: session.lastActive
|
|
532
|
-
})),
|
|
562
|
+
sessionCount: sessions.length,
|
|
563
|
+
sessions,
|
|
533
564
|
config: {
|
|
534
565
|
groupMode: this.config.groupMode,
|
|
535
566
|
prefix: this.config.prefix,
|
|
@@ -568,7 +599,8 @@ module.exports = function(Plugin) {
|
|
|
568
599
|
|
|
569
600
|
uninstall(framework) {
|
|
570
601
|
this.stopBot()
|
|
571
|
-
this.
|
|
602
|
+
this._agentsCache.clear()
|
|
603
|
+
this._sessionPlugin = null
|
|
572
604
|
this._framework = null
|
|
573
605
|
}
|
|
574
606
|
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat 插件
|
|
3
|
+
* 使用 @pinixai/weixin-bot 实现微信对话
|
|
4
|
+
*
|
|
5
|
+
* 配置:
|
|
6
|
+
* - forceLogin: 是否强制重新扫码登录
|
|
7
|
+
* - qrcodeTerminal: 是否在终端渲染二维码 (默认 true)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
11
|
+
const { z } = require('zod')
|
|
12
|
+
|
|
13
|
+
module.exports = function(Plugin) {
|
|
14
|
+
return class WeixinPlugin extends Plugin {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super()
|
|
17
|
+
this.name = 'weixin'
|
|
18
|
+
this.version = '1.0.0'
|
|
19
|
+
this.description = '微信对话插件,使用微信网页账号进行对话'
|
|
20
|
+
this.priority = 80
|
|
21
|
+
|
|
22
|
+
this.config = {
|
|
23
|
+
forceLogin: config.forceLogin || process.env.WEIXIN_FORCE_LOGIN === 'true',
|
|
24
|
+
qrcodeTerminal: config.qrcodeTerminal !== false && process.env.WEIXIN_QRCODE_TERMINAL !== 'false',
|
|
25
|
+
systemPrompt: config.systemPrompt || '你是一个有帮助的AI助手。',
|
|
26
|
+
allowedUsers: config.allowedUsers || []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this._framework = null
|
|
30
|
+
this._bot = null
|
|
31
|
+
this._sessionPlugin = null
|
|
32
|
+
this._agentsCache = new Map() // userId -> agent
|
|
33
|
+
this._qrcodeTerminal = null
|
|
34
|
+
this._origStderrWrite = null
|
|
35
|
+
this._initialized = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
install(framework) {
|
|
39
|
+
this._framework = framework
|
|
40
|
+
return this
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start(framework) {
|
|
44
|
+
// 防止重复初始化
|
|
45
|
+
if (this._initialized) return this
|
|
46
|
+
this._initialized = true
|
|
47
|
+
|
|
48
|
+
// 获取 SessionPlugin 引用
|
|
49
|
+
this._sessionPlugin = framework.pluginManager.get('session')
|
|
50
|
+
|
|
51
|
+
// 异步初始化 Bot
|
|
52
|
+
this._initBotAsync().catch(err => {
|
|
53
|
+
console.error('[WeChat] Failed to initialize bot:', err.message)
|
|
54
|
+
})
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _initBotAsync() {
|
|
59
|
+
|
|
60
|
+
let WeixinBot
|
|
61
|
+
try {
|
|
62
|
+
const module = await import('@pinixai/weixin-bot')
|
|
63
|
+
WeixinBot = module.WeixinBot
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn('[WeChat] Failed to load @pinixai/weixin-bot:', err.message)
|
|
66
|
+
console.warn('[WeChat] Make sure @pinixai/weixin-bot is installed: npm install @pinixai/weixin-bot qrcode-terminal')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 如果启用终端二维码渲染
|
|
71
|
+
if (this.config.qrcodeTerminal) {
|
|
72
|
+
try {
|
|
73
|
+
this._qrcodeTerminal = require('qrcode-terminal')
|
|
74
|
+
this._interceptQRCode()
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn('[WeChat] qrcode-terminal not installed:', err.message)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this._bot = new WeixinBot({
|
|
81
|
+
onError: (err) => {
|
|
82
|
+
console.error('[WeChat] Error:', err instanceof Error ? err.stack ?? err.message : String(err))
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const loginOptions = { force: this.config.forceLogin }
|
|
87
|
+
console.log('[WeChat]', this.config.forceLogin ? '强制重新扫码登录...' : '正在登录(已有凭证则自动跳过扫码)...')
|
|
88
|
+
|
|
89
|
+
const creds = await this._bot.login(loginOptions)
|
|
90
|
+
console.log('[WeChat] 登录成功 — Bot ID:', creds.accountId)
|
|
91
|
+
console.log('[WeChat] 关联用户:', creds.userId)
|
|
92
|
+
console.log('[WeChat] API 地址:', creds.baseUrl)
|
|
93
|
+
|
|
94
|
+
// 注册消息处理
|
|
95
|
+
this._bot.onMessage(async (msg) => {
|
|
96
|
+
await this._handleMessage(msg)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// 启动Bot
|
|
100
|
+
await this._bot.run()
|
|
101
|
+
console.log('[WeChat] 开始接收微信消息')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 拦截 SDK 的 stderr 输出,渲染二维码到终端
|
|
106
|
+
*/
|
|
107
|
+
_interceptQRCode() {
|
|
108
|
+
if (!this._qrcodeTerminal) return
|
|
109
|
+
|
|
110
|
+
this._origStderrWrite = process.stderr.write.bind(process.stderr)
|
|
111
|
+
process.stderr.write = ((chunk, ...args) => {
|
|
112
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString()
|
|
113
|
+
// 检测到登录 URL,渲染二维码
|
|
114
|
+
if (str.startsWith('https://') && str.includes('qrcode=')) {
|
|
115
|
+
const url = str.trim()
|
|
116
|
+
this._qrcodeTerminal.generate(url, { small: true }, (qr) => {
|
|
117
|
+
this._origStderrWrite(qr + '\n')
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
return this._origStderrWrite(chunk, ...args)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取主Agent
|
|
126
|
+
*/
|
|
127
|
+
_getMainAgent() {
|
|
128
|
+
if (this._framework._mainAgent) {
|
|
129
|
+
return this._framework._mainAgent
|
|
130
|
+
}
|
|
131
|
+
const agents = this._framework._agents || []
|
|
132
|
+
return agents.length > 0 ? agents[agents.length - 1] : null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取或创建会话Agent
|
|
137
|
+
* 使用 SessionPlugin 统一管理会话历史
|
|
138
|
+
*/
|
|
139
|
+
_getSessionAgent(userId) {
|
|
140
|
+
// 检查缓存的 agent 引用
|
|
141
|
+
let agent = this._agentsCache.get(userId)
|
|
142
|
+
if (!agent) {
|
|
143
|
+
const mainAgent = this._getMainAgent()
|
|
144
|
+
if (!mainAgent) {
|
|
145
|
+
console.log('[WeChat] ERROR: mainAgent is null!')
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
agent = mainAgent
|
|
149
|
+
this._agentsCache.set(userId, agent)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 使用 SessionPlugin 管理会话历史
|
|
153
|
+
if (this._sessionPlugin) {
|
|
154
|
+
const sessionId = `weixin_${userId}`
|
|
155
|
+
this._sessionPlugin.getOrCreateSession(sessionId, {
|
|
156
|
+
metadata: { platform: 'weixin', userId }
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`[WeChat] Session ready for user: ${userId}`)
|
|
161
|
+
return { agent, sessionId: `weixin_${userId}` }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 处理消息
|
|
166
|
+
*/
|
|
167
|
+
async _handleMessage(msg) {
|
|
168
|
+
if (!msg || !msg.userId) return
|
|
169
|
+
|
|
170
|
+
const userId = msg.userId
|
|
171
|
+
// 从 SessionPlugin 获取历史消息数量
|
|
172
|
+
let messageCount = 0
|
|
173
|
+
if (this._sessionPlugin) {
|
|
174
|
+
const session = this._sessionPlugin.getSession(`weixin_${userId}`)
|
|
175
|
+
messageCount = session?.messages?.length || 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`[WeChat] #${messageCount + 1} | 类型: ${msg.type} | 用户: ${userId}`)
|
|
179
|
+
console.log(`[WeChat] 内容: ${msg.text}`)
|
|
180
|
+
|
|
181
|
+
// 非文本消息暂不处理
|
|
182
|
+
if (msg.type !== 'text' || !msg.text) {
|
|
183
|
+
console.log('[WeChat] Unsupported message type or no text')
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const text = msg.text.trim()
|
|
188
|
+
await this._processChat(userId, text, msg)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 处理对话
|
|
193
|
+
*/
|
|
194
|
+
async _processChat(userId, text, originalMsg) {
|
|
195
|
+
const sessionInfo = this._getSessionAgent(userId)
|
|
196
|
+
if (!sessionInfo) {
|
|
197
|
+
console.error('[WeChat] No session agent available')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { agent, sessionId } = sessionInfo
|
|
202
|
+
|
|
203
|
+
// 使用 SessionPlugin 添加用户消息到历史
|
|
204
|
+
if (this._sessionPlugin) {
|
|
205
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 发送正在输入状态
|
|
209
|
+
try {
|
|
210
|
+
await this._bot.sendTyping(userId)
|
|
211
|
+
} catch { /* typing 失败不影响回复 */ }
|
|
212
|
+
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
let fullResponse = ''
|
|
217
|
+
|
|
218
|
+
// 使用流式响应
|
|
219
|
+
for await (const chunk of agent.chatStream(text, {
|
|
220
|
+
sessionId: sessionId
|
|
221
|
+
})) {
|
|
222
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
223
|
+
fullResponse += chunk.text
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 保存助手回复到历史(使用 SessionPlugin)
|
|
228
|
+
if (this._sessionPlugin) {
|
|
229
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 发送回复
|
|
233
|
+
if (fullResponse) {
|
|
234
|
+
await this._bot.reply(originalMsg, fullResponse)
|
|
235
|
+
console.log(`[WeChat] 回复成功 (${fullResponse.length} 字符)`)
|
|
236
|
+
} else {
|
|
237
|
+
await this._bot.reply(originalMsg, '抱歉,我没有收到有效的回复。')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('[WeChat] Chat error:', err)
|
|
242
|
+
await this._bot.reply(originalMsg, `发生错误:${err.message}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 权限检查
|
|
248
|
+
*/
|
|
249
|
+
_checkPermission(userId) {
|
|
250
|
+
if (!this.config.allowedUsers || this.config.allowedUsers.length === 0) {
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
return this.config.allowedUsers.includes(userId)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 清除会话
|
|
258
|
+
*/
|
|
259
|
+
_clearSession(userId) {
|
|
260
|
+
// 清除 SessionPlugin 中的会话
|
|
261
|
+
if (this._sessionPlugin) {
|
|
262
|
+
const sessionId = `weixin_${userId}`
|
|
263
|
+
this._sessionPlugin.deleteSession(sessionId)
|
|
264
|
+
}
|
|
265
|
+
// 清除 agent 缓存
|
|
266
|
+
this._agentsCache.delete(userId)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 获取插件状态
|
|
271
|
+
*/
|
|
272
|
+
getStatus() {
|
|
273
|
+
// 从 SessionPlugin 获取 WeChat 相关会话
|
|
274
|
+
let sessions = []
|
|
275
|
+
if (this._sessionPlugin) {
|
|
276
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
277
|
+
sessions = allSessions
|
|
278
|
+
.filter(s => s.id.startsWith('weixin_'))
|
|
279
|
+
.map(s => ({
|
|
280
|
+
userId: s.id.replace('weixin_', ''),
|
|
281
|
+
historyLength: s.messageCount,
|
|
282
|
+
lastActive: s.lastActive
|
|
283
|
+
}))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
connected: !!this._bot,
|
|
288
|
+
sessionCount: sessions.length,
|
|
289
|
+
sessions,
|
|
290
|
+
config: {
|
|
291
|
+
forceLogin: this.config.forceLogin,
|
|
292
|
+
qrcodeTerminal: this.config.qrcodeTerminal
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 停止 Bot
|
|
299
|
+
*/
|
|
300
|
+
stopBot() {
|
|
301
|
+
if (this._bot) {
|
|
302
|
+
this._bot.stop()
|
|
303
|
+
this._bot = null
|
|
304
|
+
console.log('[WeChat] Bot stopped')
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
reload(framework) {
|
|
309
|
+
this._framework = framework
|
|
310
|
+
this._initialized = false
|
|
311
|
+
this.stopBot()
|
|
312
|
+
this.start(framework)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
uninstall(framework) {
|
|
316
|
+
this.stopBot()
|
|
317
|
+
this._agentsCache.clear()
|
|
318
|
+
this._sessionPlugin = null
|
|
319
|
+
this._framework = null
|
|
320
|
+
// 恢复 stderr
|
|
321
|
+
if (this._origStderrWrite) {
|
|
322
|
+
process.stderr.write = this._origStderrWrite
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|