evolclaw 2.1.2 → 2.3.0
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/README.md +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/index.js.bak
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { loadConfig, ensureDataDirs, resolvePaths, resolveAnthropicConfig } from './config.js';
|
|
2
|
-
import { SessionManager } from './core/session-manager.js';
|
|
3
|
-
import { AgentRunner } from './core/agent-runner.js';
|
|
4
|
-
import { FeishuChannel } from './channels/feishu.js';
|
|
5
|
-
import { AUNChannel } from './channels/aun.js';
|
|
6
|
-
import { WechatChannel } from './channels/wechat.js';
|
|
7
|
-
import { MessageProcessor } from './core/message-processor.js';
|
|
8
|
-
import { MessageQueue } from './core/message-queue.js';
|
|
9
|
-
import { MessageCache } from './core/message-cache.js';
|
|
10
|
-
import { CommandHandler } from './core/command-handler.js';
|
|
11
|
-
import { logger } from './utils/logger.js';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
async function main() {
|
|
15
|
-
// 过滤飞书 SDK 的 info 日志
|
|
16
|
-
const originalLog = console.log;
|
|
17
|
-
const originalInfo = console.info;
|
|
18
|
-
const filter = (...args) => {
|
|
19
|
-
const firstArg = String(args[0] || '');
|
|
20
|
-
return firstArg.includes('[info]') || firstArg.includes('[ws]');
|
|
21
|
-
};
|
|
22
|
-
console.log = (...args) => {
|
|
23
|
-
if (filter(...args))
|
|
24
|
-
return;
|
|
25
|
-
originalLog(...args);
|
|
26
|
-
};
|
|
27
|
-
console.info = (...args) => {
|
|
28
|
-
if (filter(...args))
|
|
29
|
-
return;
|
|
30
|
-
originalInfo(...args);
|
|
31
|
-
};
|
|
32
|
-
logger.info('EvolClaw starting...');
|
|
33
|
-
// 确保数据目录存在
|
|
34
|
-
ensureDataDirs();
|
|
35
|
-
// 加载配置
|
|
36
|
-
const config = loadConfig();
|
|
37
|
-
const anthropic = resolveAnthropicConfig(config);
|
|
38
|
-
logger.info('✓ Config loaded (API keys hidden)');
|
|
39
|
-
if (anthropic.baseUrl) {
|
|
40
|
-
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
41
|
-
}
|
|
42
|
-
// 初始化数据库
|
|
43
|
-
const sessionManager = new SessionManager();
|
|
44
|
-
logger.info('✓ Database initialized');
|
|
45
|
-
// 初始化 Agent Runner(带持久化回调)
|
|
46
|
-
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
|
|
47
|
-
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
48
|
-
}, anthropic.baseUrl, config);
|
|
49
|
-
if (anthropic.effort) {
|
|
50
|
-
agentRunner.setEffort(anthropic.effort);
|
|
51
|
-
}
|
|
52
|
-
logger.info('✓ Agent runner ready');
|
|
53
|
-
// 创建消息缓存
|
|
54
|
-
const messageCache = new MessageCache();
|
|
55
|
-
logger.info('✓ Message cache initialized');
|
|
56
|
-
// 定期清理过期消息(每小时)
|
|
57
|
-
setInterval(() => {
|
|
58
|
-
messageCache.cleanupExpired();
|
|
59
|
-
}, 60 * 60 * 1000);
|
|
60
|
-
// 飞书渠道(条件初始化)
|
|
61
|
-
let feishu = null;
|
|
62
|
-
if (config.channels?.feishu?.enabled !== false && config.channels?.feishu?.appId) {
|
|
63
|
-
feishu = new FeishuChannel({
|
|
64
|
-
appId: config.channels.feishu.appId,
|
|
65
|
-
appSecret: config.channels.feishu.appSecret,
|
|
66
|
-
db: sessionManager.getDatabase()
|
|
67
|
-
});
|
|
68
|
-
// 设置项目路径提供器
|
|
69
|
-
feishu.onProjectPathRequest(async (chatId) => {
|
|
70
|
-
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
|
|
71
|
-
return path.isAbsolute(session.projectPath)
|
|
72
|
-
? session.projectPath
|
|
73
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
// AUN 渠道(条件初始化)
|
|
77
|
-
let aun = null;
|
|
78
|
-
if (config.channels?.aun?.enabled !== false && config.channels?.aun?.domain) {
|
|
79
|
-
aun = new AUNChannel({ domain: config.channels.aun.domain, agentName: config.channels.aun.agentName });
|
|
80
|
-
}
|
|
81
|
-
// 创建命令处理器
|
|
82
|
-
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
83
|
-
// 创建消息处理器
|
|
84
|
-
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
|
|
85
|
-
const sendFn = async (id, text, opts) => {
|
|
86
|
-
const adapter = cmdHandler.getAdapter(channel);
|
|
87
|
-
if (!adapter)
|
|
88
|
-
return;
|
|
89
|
-
// 文件标记处理(通过 adapter.sendFile 能力判断,不按渠道名分支)
|
|
90
|
-
if (adapter.sendFile) {
|
|
91
|
-
const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
|
|
92
|
-
const fileMatches = [...text.matchAll(fileMarkerPattern)];
|
|
93
|
-
for (const match of fileMatches) {
|
|
94
|
-
const filePath = match[1].trim();
|
|
95
|
-
// 跳过占位符/代码片段中的伪路径
|
|
96
|
-
if (!filePath || /[\\[\]{}*+?|^$]/.test(filePath))
|
|
97
|
-
continue;
|
|
98
|
-
const session = await sessionManager.getActiveSession(channel, channelId);
|
|
99
|
-
const projectPath = session?.projectPath || process.cwd();
|
|
100
|
-
const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
|
|
101
|
-
try {
|
|
102
|
-
await adapter.sendFile(id, absoluteFilePath);
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
logger.error(`[${channel}] Failed to send file: ${absoluteFilePath}`, error);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
text = text.replace(fileMarkerPattern, '').trim();
|
|
109
|
-
}
|
|
110
|
-
if (text) {
|
|
111
|
-
await adapter.sendText(id, text, opts);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
115
|
-
});
|
|
116
|
-
// 回填 processor 和 messageQueue 的引用
|
|
117
|
-
cmdHandler.setProcessor(processor);
|
|
118
|
-
// 设置 compact 开始回调
|
|
119
|
-
agentRunner.setCompactStartCallback((sessionId) => {
|
|
120
|
-
processor.handleCompactStart();
|
|
121
|
-
});
|
|
122
|
-
// 创建消息队列
|
|
123
|
-
const messageQueue = new MessageQueue(async (message) => {
|
|
124
|
-
await processor.processMessage(message);
|
|
125
|
-
});
|
|
126
|
-
// 设置中断回调
|
|
127
|
-
messageQueue.setInterruptCallback(async (sessionKey) => {
|
|
128
|
-
await agentRunner.interrupt(sessionKey);
|
|
129
|
-
});
|
|
130
|
-
// 回填 messageQueue 引用
|
|
131
|
-
cmdHandler.setMessageQueue(messageQueue);
|
|
132
|
-
// 注册 Feishu 适配器(如果已初始化)
|
|
133
|
-
if (feishu) {
|
|
134
|
-
const feishuAdapter = {
|
|
135
|
-
name: 'feishu',
|
|
136
|
-
sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
|
|
137
|
-
sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
|
|
138
|
-
isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
|
|
139
|
-
};
|
|
140
|
-
const feishuOptions = {
|
|
141
|
-
systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt] 路径支持相对路径(相对项目目录)或绝对路径。系统会自动上传并发送。',
|
|
142
|
-
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
143
|
-
supportsImages: true,
|
|
144
|
-
};
|
|
145
|
-
processor.registerChannel(feishuAdapter, feishuOptions);
|
|
146
|
-
cmdHandler.registerAdapter(feishuAdapter);
|
|
147
|
-
}
|
|
148
|
-
// 注册 AUN 适配器(如果已初始化)
|
|
149
|
-
if (aun) {
|
|
150
|
-
const aunAdapter = {
|
|
151
|
-
name: 'aun',
|
|
152
|
-
sendText: (channelId, text) => aun.sendMessage(channelId, text),
|
|
153
|
-
};
|
|
154
|
-
processor.registerChannel(aunAdapter);
|
|
155
|
-
cmdHandler.registerAdapter(aunAdapter);
|
|
156
|
-
}
|
|
157
|
-
// ── WeChat 渠道(条件初始化)──
|
|
158
|
-
let wechat = null;
|
|
159
|
-
if (config.channels?.wechat?.enabled && config.channels?.wechat?.token) {
|
|
160
|
-
wechat = new WechatChannel({
|
|
161
|
-
baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
162
|
-
token: config.channels.wechat.token,
|
|
163
|
-
});
|
|
164
|
-
// 设置项目路径提供器(用于接收文件保存)
|
|
165
|
-
wechat.onProjectPathRequest(async (channelId) => {
|
|
166
|
-
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
167
|
-
return path.isAbsolute(session.projectPath)
|
|
168
|
-
? session.projectPath
|
|
169
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
170
|
-
});
|
|
171
|
-
const wechatAdapter = {
|
|
172
|
-
name: 'wechat',
|
|
173
|
-
sendText: (channelId, text) => wechat.sendMessage(channelId, text),
|
|
174
|
-
sendFile: (channelId, filePath) => wechat.sendFile(channelId, filePath),
|
|
175
|
-
};
|
|
176
|
-
const wechatOptions = {
|
|
177
|
-
systemPromptAppend: '[系统功能] 你可以发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:./report.txt]',
|
|
178
|
-
fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
|
|
179
|
-
};
|
|
180
|
-
processor.registerChannel(wechatAdapter, wechatOptions);
|
|
181
|
-
cmdHandler.registerAdapter(wechatAdapter);
|
|
182
|
-
// Session 过期通知(通过 Feishu 等其他渠道告知用户)
|
|
183
|
-
wechat.onSessionExpiredNotify(async (message) => {
|
|
184
|
-
// 尝试通过已注册的 Feishu owner 通知
|
|
185
|
-
const feishuOwner = config.channels?.feishu?.owner;
|
|
186
|
-
if (feishuOwner) {
|
|
187
|
-
try {
|
|
188
|
-
// Feishu owner ID 是 open_id,但 sendMessage 需要 chat_id
|
|
189
|
-
// 这里只记日志,因为 owner 的 chat_id 需要从 session 中获取
|
|
190
|
-
logger.warn(`[WeChat] ${message}`);
|
|
191
|
-
}
|
|
192
|
-
catch { }
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
logger.warn(`[WeChat] ${message}`);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
wechat.onMessage(async (channelId, content, userId, images) => {
|
|
199
|
-
content = content.trim();
|
|
200
|
-
// 首次交互自动绑定主人
|
|
201
|
-
if (userId && !config.channels?.wechat?.owner) {
|
|
202
|
-
const { setOwner } = await import('./config.js');
|
|
203
|
-
setOwner(config, 'wechat', userId);
|
|
204
|
-
logger.info(`[Owner] Auto-bound WeChat owner: ${userId}`);
|
|
205
|
-
}
|
|
206
|
-
// 命令快速路径
|
|
207
|
-
if (cmdHandler.isCommand(content)) {
|
|
208
|
-
const cmdResult = await cmdHandler.handle(content, 'wechat', channelId, undefined, userId);
|
|
209
|
-
if (cmdResult !== null) {
|
|
210
|
-
if (cmdResult) {
|
|
211
|
-
try {
|
|
212
|
-
await wechat.sendMessage(channelId, cmdResult);
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
logger.error('[WeChat] Failed to send command response:', error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// 获取当前项目路径
|
|
222
|
-
const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
|
|
223
|
-
// 普通消息进入队列
|
|
224
|
-
await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, images, timestamp: Date.now(), userId }, session.projectPath);
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// Feishu 消息处理
|
|
228
|
-
if (feishu) {
|
|
229
|
-
feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
|
|
230
|
-
let content = rawContent.trim();
|
|
231
|
-
// 首次交互自动绑定主人
|
|
232
|
-
if (userId && !config.channels?.feishu?.owner) {
|
|
233
|
-
const { setOwner } = await import('./config.js');
|
|
234
|
-
setOwner(config, 'feishu', userId);
|
|
235
|
-
logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
|
|
236
|
-
}
|
|
237
|
-
// 命令立即处理,不进入队列
|
|
238
|
-
if (cmdHandler.isCommand(content)) {
|
|
239
|
-
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
|
|
240
|
-
if (cmdResult !== null) {
|
|
241
|
-
if (cmdResult) {
|
|
242
|
-
try {
|
|
243
|
-
await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
logger.error('[Feishu] Failed to send command response:', error);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// 获取当前项目路径(话题会话自动创建,携带 metadata)
|
|
253
|
-
const metadata = rootId ? { feishu: { rootId } } : undefined;
|
|
254
|
-
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
|
|
255
|
-
// 群聊消息添加用户名前缀
|
|
256
|
-
const chatMode = await feishu.getChatMode(chatId);
|
|
257
|
-
if (chatMode === 'group' && userName) {
|
|
258
|
-
content = `[${userName}] ${content}`;
|
|
259
|
-
}
|
|
260
|
-
// 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
|
|
261
|
-
await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
// AUN 消息处理
|
|
265
|
-
if (aun) {
|
|
266
|
-
aun.onMessage(async (sessionId, content) => {
|
|
267
|
-
content = content.trim();
|
|
268
|
-
// 首次交互自动绑定主人
|
|
269
|
-
if (!config.channels?.aun?.owner) {
|
|
270
|
-
const { setOwner } = await import('./config.js');
|
|
271
|
-
setOwner(config, 'aun', sessionId);
|
|
272
|
-
logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
|
|
273
|
-
}
|
|
274
|
-
// 命令立即处理,不进入队列
|
|
275
|
-
if (cmdHandler.isCommand(content)) {
|
|
276
|
-
const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
|
|
277
|
-
if (cmdResult) {
|
|
278
|
-
await aun.sendMessage(sessionId, cmdResult);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// 获取当前项目路径
|
|
283
|
-
const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
|
|
284
|
-
// 普通消息进入队列
|
|
285
|
-
await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
// 连接渠道
|
|
289
|
-
const channels = [];
|
|
290
|
-
const channelInstances = [
|
|
291
|
-
...(feishu ? [{ name: 'Feishu', instance: feishu, timeout: 5000 }] : []),
|
|
292
|
-
...(aun ? [{ name: 'AUN', instance: aun }] : []),
|
|
293
|
-
...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
|
|
294
|
-
];
|
|
295
|
-
for (const { name, instance, timeout } of channelInstances) {
|
|
296
|
-
try {
|
|
297
|
-
if (timeout) {
|
|
298
|
-
await Promise.race([
|
|
299
|
-
instance.connect(),
|
|
300
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), timeout))
|
|
301
|
-
]);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
await instance.connect();
|
|
305
|
-
}
|
|
306
|
-
logger.info(`✓ ${name} connected`);
|
|
307
|
-
channels.push(name);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
logger.warn(`⚠ ${name} connection failed (will continue without it)`);
|
|
311
|
-
if (error instanceof Error) {
|
|
312
|
-
logger.warn(` Reason: ${error.message}`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
|
|
317
|
-
// 写入 ready 信号,供 restart-monitor 检测启动成功
|
|
318
|
-
const readySignalPath = resolvePaths().readySignal;
|
|
319
|
-
fs.writeFileSync(readySignalPath, String(Date.now()));
|
|
320
|
-
logger.info(`✓ Ready signal written: ${readySignalPath}`);
|
|
321
|
-
// 优雅关闭
|
|
322
|
-
const shutdown = async () => {
|
|
323
|
-
logger.info('\n\nShutting down gracefully...');
|
|
324
|
-
if (feishu)
|
|
325
|
-
await feishu.disconnect();
|
|
326
|
-
if (aun)
|
|
327
|
-
await aun.disconnect();
|
|
328
|
-
if (wechat)
|
|
329
|
-
await wechat.disconnect();
|
|
330
|
-
sessionManager.close();
|
|
331
|
-
logger.info('✓ Shutdown complete');
|
|
332
|
-
process.exit(0);
|
|
333
|
-
};
|
|
334
|
-
process.on('SIGINT', shutdown);
|
|
335
|
-
process.on('SIGTERM', shutdown);
|
|
336
|
-
}
|
|
337
|
-
main().catch((error) => {
|
|
338
|
-
logger.error('Fatal error:', error);
|
|
339
|
-
process.exit(1);
|
|
340
|
-
});
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import readline from 'readline';
|
|
3
|
-
import { resolvePaths } from '../paths.js';
|
|
4
|
-
const FEISHU_PROD_URL = 'https://accounts.feishu.cn';
|
|
5
|
-
const LARK_PROD_URL = 'https://accounts.larksuite.com';
|
|
6
|
-
const POLL_TIMEOUT_MS = 35_000;
|
|
7
|
-
const LOGIN_TIMEOUT_MS = 600_000;
|
|
8
|
-
const SKIP = Symbol('SKIP');
|
|
9
|
-
const QUIT = Symbol('QUIT');
|
|
10
|
-
function ask(rl, question) {
|
|
11
|
-
return new Promise(resolve => rl.question(question, resolve));
|
|
12
|
-
}
|
|
13
|
-
class FeishuQrRegistrationClient {
|
|
14
|
-
baseUrl;
|
|
15
|
-
constructor(isLark = false) {
|
|
16
|
-
this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
|
|
17
|
-
}
|
|
18
|
-
setDomain(isLark) {
|
|
19
|
-
this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
|
|
20
|
-
}
|
|
21
|
-
async init() {
|
|
22
|
-
return this.postRegistration('init', {});
|
|
23
|
-
}
|
|
24
|
-
async begin() {
|
|
25
|
-
return this.postRegistration('begin', {
|
|
26
|
-
archetype: 'PersonalAgent',
|
|
27
|
-
auth_method: 'client_secret',
|
|
28
|
-
request_user_info: 'open_id',
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
async poll(deviceCode) {
|
|
32
|
-
return this.postRegistration('poll', { device_code: deviceCode });
|
|
33
|
-
}
|
|
34
|
-
async postRegistration(action, extraParams) {
|
|
35
|
-
const body = new URLSearchParams({ action, ...extraParams }).toString();
|
|
36
|
-
const res = await fetch(`${this.baseUrl}/oauth/v1/app/registration`, {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
39
|
-
body,
|
|
40
|
-
});
|
|
41
|
-
const text = await res.text();
|
|
42
|
-
if (!text)
|
|
43
|
-
return {};
|
|
44
|
-
return JSON.parse(text);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function runQrRegistrationFlow() {
|
|
48
|
-
const client = new FeishuQrRegistrationClient();
|
|
49
|
-
const initResult = await client.init();
|
|
50
|
-
const authMethods = Array.isArray(initResult.supported_auth_methods) ? initResult.supported_auth_methods : [];
|
|
51
|
-
if (!authMethods.includes('client_secret')) {
|
|
52
|
-
throw new Error('当前环境不支持 client_secret 注册');
|
|
53
|
-
}
|
|
54
|
-
const beginResult = await client.begin();
|
|
55
|
-
if (!beginResult.verification_uri_complete || !beginResult.device_code) {
|
|
56
|
-
throw new Error('服务端未返回扫码链接或 device_code');
|
|
57
|
-
}
|
|
58
|
-
// 显示二维码
|
|
59
|
-
try {
|
|
60
|
-
const qrterm = await import('qrcode-terminal');
|
|
61
|
-
await new Promise(resolve => {
|
|
62
|
-
qrterm.default.generate(beginResult.verification_uri_complete, { small: true }, (qr) => {
|
|
63
|
-
console.log(qr);
|
|
64
|
-
resolve();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
console.log(`请在浏览器中打开此链接扫码: ${beginResult.verification_uri_complete}\n`);
|
|
70
|
-
}
|
|
71
|
-
console.log('请用飞书/Lark 扫描上方二维码...\n');
|
|
72
|
-
console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret\n');
|
|
73
|
-
let userAction = null;
|
|
74
|
-
const setupKeyListener = () => {
|
|
75
|
-
if (!process.stdin.isTTY)
|
|
76
|
-
return () => { };
|
|
77
|
-
process.stdin.setRawMode(true);
|
|
78
|
-
process.stdin.resume();
|
|
79
|
-
process.stdin.setEncoding('utf8');
|
|
80
|
-
const handler = (key) => {
|
|
81
|
-
if (key === 'q' || key === '\u0003')
|
|
82
|
-
userAction = QUIT;
|
|
83
|
-
if (key === 's')
|
|
84
|
-
userAction = SKIP;
|
|
85
|
-
};
|
|
86
|
-
process.stdin.on('data', handler);
|
|
87
|
-
return () => {
|
|
88
|
-
process.stdin.removeListener('data', handler);
|
|
89
|
-
process.stdin.setRawMode(false);
|
|
90
|
-
process.stdin.pause();
|
|
91
|
-
};
|
|
92
|
-
};
|
|
93
|
-
const cleanup = setupKeyListener();
|
|
94
|
-
const startedAt = Date.now();
|
|
95
|
-
let pollIntervalSeconds = Number(beginResult.interval ?? 5);
|
|
96
|
-
const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
|
|
97
|
-
let domainResolved = false;
|
|
98
|
-
let currentDomain = 'feishu';
|
|
99
|
-
try {
|
|
100
|
-
while (Date.now() - startedAt < expireInSeconds * 1000) {
|
|
101
|
-
if (userAction === QUIT)
|
|
102
|
-
return QUIT;
|
|
103
|
-
if (userAction === SKIP)
|
|
104
|
-
return SKIP;
|
|
105
|
-
const pollResult = await client.poll(beginResult.device_code);
|
|
106
|
-
if (pollResult.user_info?.tenant_brand === 'lark' && !domainResolved) {
|
|
107
|
-
client.setDomain(true);
|
|
108
|
-
currentDomain = 'lark';
|
|
109
|
-
domainResolved = true;
|
|
110
|
-
}
|
|
111
|
-
if (pollResult.client_id && pollResult.client_secret) {
|
|
112
|
-
return {
|
|
113
|
-
appId: pollResult.client_id,
|
|
114
|
-
appSecret: pollResult.client_secret,
|
|
115
|
-
domain: currentDomain,
|
|
116
|
-
openId: pollResult.user_info?.open_id ?? '',
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (pollResult.error === 'authorization_pending') {
|
|
120
|
-
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (pollResult.error === 'slow_down') {
|
|
124
|
-
pollIntervalSeconds += 5;
|
|
125
|
-
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (pollResult.error === 'access_denied') {
|
|
129
|
-
throw new Error('用户拒绝了扫码授权');
|
|
130
|
-
}
|
|
131
|
-
if (pollResult.error === 'expired_token') {
|
|
132
|
-
throw new Error('扫码会话已过期');
|
|
133
|
-
}
|
|
134
|
-
if (pollResult.error) {
|
|
135
|
-
throw new Error(`扫码注册失败: ${pollResult.error}${pollResult.error_description ? ` - ${pollResult.error_description}` : ''}`);
|
|
136
|
-
}
|
|
137
|
-
await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
|
|
138
|
-
}
|
|
139
|
-
throw new Error('等待扫码结果超时');
|
|
140
|
-
}
|
|
141
|
-
finally {
|
|
142
|
-
cleanup();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function manualInput(rl) {
|
|
146
|
-
console.log('\n手动输入模式:\n');
|
|
147
|
-
let appId = '';
|
|
148
|
-
while (!appId) {
|
|
149
|
-
appId = (await ask(rl, ' 飞书 App ID: ')).trim();
|
|
150
|
-
if (!appId)
|
|
151
|
-
console.log(' ⚠ 不能为空');
|
|
152
|
-
}
|
|
153
|
-
let appSecret = '';
|
|
154
|
-
while (!appSecret) {
|
|
155
|
-
appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
|
|
156
|
-
if (!appSecret)
|
|
157
|
-
console.log(' ⚠ 不能为空');
|
|
158
|
-
}
|
|
159
|
-
return { appId, appSecret, domain: 'unknown', openId: '' };
|
|
160
|
-
}
|
|
161
|
-
export async function runFeishuQrFlow() {
|
|
162
|
-
try {
|
|
163
|
-
const result = await runQrRegistrationFlow();
|
|
164
|
-
if (result === QUIT || result === SKIP)
|
|
165
|
-
return null;
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export async function cmdInitFeishu() {
|
|
174
|
-
const p = resolvePaths();
|
|
175
|
-
if (!fs.existsSync(p.config)) {
|
|
176
|
-
console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
180
|
-
// 检查已有配置 — 提示破坏性风险(排除占位符)
|
|
181
|
-
const existingFeishu = config.channels?.feishu;
|
|
182
|
-
const isPlaceholder = !existingFeishu?.appId ||
|
|
183
|
-
!existingFeishu?.appSecret ||
|
|
184
|
-
existingFeishu.appId.includes('your-') ||
|
|
185
|
-
existingFeishu.appId.includes('placeholder') ||
|
|
186
|
-
existingFeishu.appSecret.includes('your-') ||
|
|
187
|
-
existingFeishu.appSecret.includes('placeholder');
|
|
188
|
-
if (existingFeishu && !isPlaceholder) {
|
|
189
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
190
|
-
try {
|
|
191
|
-
console.log('⚠️ 检测到已有飞书配置:');
|
|
192
|
-
console.log(` App ID: ${existingFeishu.appId}`);
|
|
193
|
-
if (existingFeishu.owner) {
|
|
194
|
-
console.log(` 当前 Owner: ${existingFeishu.owner}`);
|
|
195
|
-
}
|
|
196
|
-
console.log('');
|
|
197
|
-
console.log('重新初始化将:');
|
|
198
|
-
console.log(' - 替换当前飞书机器人凭证(旧机器人停止工作)');
|
|
199
|
-
console.log(' - 重置 Owner 绑定为新扫码账号');
|
|
200
|
-
console.log(' - 现有会话数据保留,但需用新机器人重新发起对话');
|
|
201
|
-
console.log('');
|
|
202
|
-
const answer = (await ask(rl, '确认重新初始化?[y/N] ')).trim().toLowerCase();
|
|
203
|
-
if (answer !== 'y' && answer !== 'yes') {
|
|
204
|
-
console.log('已取消');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
rl.close();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
console.log('正在获取飞书登录二维码...\n');
|
|
213
|
-
let result;
|
|
214
|
-
try {
|
|
215
|
-
const flowResult = await runQrRegistrationFlow();
|
|
216
|
-
if (flowResult === QUIT) {
|
|
217
|
-
console.log('已退出');
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (flowResult === SKIP) {
|
|
221
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
222
|
-
try {
|
|
223
|
-
result = await manualInput(rl);
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
rl.close();
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
result = flowResult;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
235
|
-
process.exit(1);
|
|
236
|
-
}
|
|
237
|
-
// 写入配置:使用最新结构 channels.feishu
|
|
238
|
-
if (!config.channels)
|
|
239
|
-
config.channels = {};
|
|
240
|
-
config.channels.feishu = config.channels.feishu || {};
|
|
241
|
-
config.channels.feishu.appId = result.appId;
|
|
242
|
-
config.channels.feishu.appSecret = result.appSecret;
|
|
243
|
-
config.channels.feishu.enabled = true;
|
|
244
|
-
if (result.openId) {
|
|
245
|
-
config.channels.feishu.owner = result.openId;
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
delete config.channels.feishu.owner;
|
|
249
|
-
}
|
|
250
|
-
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
251
|
-
console.log(`\n✅ 飞书连接成功!`);
|
|
252
|
-
console.log(` App ID: ${result.appId}`);
|
|
253
|
-
if (result.openId) {
|
|
254
|
-
console.log(` Owner: ${result.openId}`);
|
|
255
|
-
}
|
|
256
|
-
if (result.domain !== 'unknown') {
|
|
257
|
-
console.log(` Domain: ${result.domain}`);
|
|
258
|
-
}
|
|
259
|
-
console.log(` 配置已写入: ${p.config}`);
|
|
260
|
-
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
261
|
-
}
|