evolclaw 2.0.0 → 2.0.2

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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { SessionManager } from './core/session-manager.js';
3
3
  import { AgentRunner } from './core/agent-runner.js';
4
4
  import { FeishuChannel } from './channels/feishu.js';
5
5
  import { AUNChannel } from './channels/aun.js';
6
+ import { WechatChannel } from './channels/wechat.js';
6
7
  import { MessageProcessor } from './core/message-processor.js';
7
8
  import { MessageQueue } from './core/message-queue.js';
8
9
  import { MessageCache } from './core/message-cache.js';
@@ -53,28 +54,38 @@ async function main() {
53
54
  setInterval(() => {
54
55
  messageCache.cleanupExpired();
55
56
  }, 60 * 60 * 1000);
56
- // 飞书渠道
57
- const feishu = new FeishuChannel({
58
- appId: config.feishu.appId,
59
- appSecret: config.feishu.appSecret,
60
- db: sessionManager.getDatabase()
61
- });
62
- // 设置项目路径提供器
63
- feishu.onProjectPathRequest(async (chatId) => {
64
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
65
- return path.isAbsolute(session.projectPath)
66
- ? session.projectPath
67
- : path.resolve(process.cwd(), session.projectPath);
68
- });
69
- // AUN 渠道
70
- const aun = new AUNChannel({ domain: config.aun.domain, agentName: config.aun.agentName });
57
+ // 飞书渠道(条件初始化)
58
+ let feishu = null;
59
+ if (config.channels?.feishu?.enabled !== false && config.channels?.feishu?.appId) {
60
+ feishu = new FeishuChannel({
61
+ appId: config.channels.feishu.appId,
62
+ appSecret: config.channels.feishu.appSecret,
63
+ db: sessionManager.getDatabase()
64
+ });
65
+ // 设置项目路径提供器
66
+ feishu.onProjectPathRequest(async (chatId) => {
67
+ const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
68
+ return path.isAbsolute(session.projectPath)
69
+ ? session.projectPath
70
+ : path.resolve(process.cwd(), session.projectPath);
71
+ });
72
+ }
73
+ // AUN 渠道(条件初始化)
74
+ let aun = null;
75
+ if (config.channels?.aun?.enabled !== false && config.channels?.aun?.domain) {
76
+ aun = new AUNChannel({ domain: config.channels.aun.domain, agentName: config.channels.aun.agentName });
77
+ }
71
78
  // 创建命令处理器
72
79
  const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
73
80
  // 创建消息处理器
74
81
  const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId) => {
75
82
  const sendFn = async (id, text) => {
76
- const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
77
- if (channel === 'feishu') {
83
+ const adapter = cmdHandler.getAdapter(channel);
84
+ if (!adapter)
85
+ return;
86
+ // 文件标记处理(通过 adapter.sendFile 能力判断,不按渠道名分支)
87
+ if (adapter.sendFile) {
88
+ const fileMarkerPattern = /\[SEND_FILE:([^\]]+)\]/g;
78
89
  const fileMatches = [...text.matchAll(fileMarkerPattern)];
79
90
  for (const match of fileMatches) {
80
91
  const filePath = match[1].trim();
@@ -82,19 +93,16 @@ async function main() {
82
93
  const projectPath = session?.projectPath || process.cwd();
83
94
  const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.join(projectPath, filePath);
84
95
  try {
85
- await feishu.sendFile(id, absoluteFilePath);
96
+ await adapter.sendFile(id, absoluteFilePath);
86
97
  }
87
98
  catch (error) {
88
- logger.error(`[Feishu] Failed to send file: ${absoluteFilePath}`, error);
99
+ logger.error(`[${channel}] Failed to send file: ${absoluteFilePath}`, error);
89
100
  }
90
101
  }
91
102
  text = text.replace(fileMarkerPattern, '').trim();
92
103
  }
93
104
  if (text) {
94
- if (channel === 'feishu')
95
- await feishu.sendMessage(id, text);
96
- else if (channel === 'aun')
97
- await aun.sendMessage(id, text);
105
+ await adapter.sendText(id, text);
98
106
  }
99
107
  };
100
108
  return cmdHandler.handle(content, channel, channelId, sendFn, userId);
@@ -115,105 +123,167 @@ async function main() {
115
123
  });
116
124
  // 回填 messageQueue 引用
117
125
  cmdHandler.setMessageQueue(messageQueue);
118
- // 注册 Feishu 适配器
119
- const feishuAdapter = {
120
- name: 'feishu',
121
- sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
122
- sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
123
- isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
124
- };
125
- const feishuOptions = {
126
- systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:/path/to/file.txt] 系统会自动上传并发送。',
127
- fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
128
- supportsImages: true,
129
- };
130
- processor.registerChannel(feishuAdapter, feishuOptions);
131
- cmdHandler.registerAdapter(feishuAdapter);
132
- // 注册 AUN 适配器
133
- const aunAdapter = {
134
- name: 'aun',
135
- sendText: (channelId, text) => aun.sendMessage(channelId, text),
136
- };
137
- processor.registerChannel(aunAdapter);
138
- cmdHandler.registerAdapter(aunAdapter);
139
- // Feishu 消息处理
140
- feishu.onMessage(async (chatId, content, images, userId, userName, messageId) => {
141
- content = content.trim();
142
- // 首次交互自动绑定主人
143
- if (userId && !config.owners?.feishu) {
144
- const { setOwner } = await import('./config.js');
145
- setOwner(config, 'feishu', userId);
146
- logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
147
- }
148
- // 命令立即处理,不进入队列
149
- if (cmdHandler.isCommand(content)) {
150
- const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
151
- if (cmdResult !== null) {
152
- if (cmdResult) {
153
- try {
154
- await feishu.sendMessage(chatId, cmdResult, { forceText: true });
126
+ // 注册 Feishu 适配器(如果已初始化)
127
+ if (feishu) {
128
+ const feishuAdapter = {
129
+ name: 'feishu',
130
+ sendText: (channelId, text, options) => feishu.sendMessage(channelId, text, options),
131
+ sendFile: (channelId, filePath) => feishu.sendFile(channelId, filePath),
132
+ isGroupChat: (channelId) => feishu.getChatMode(channelId).then(m => m === 'group'),
133
+ };
134
+ const feishuOptions = {
135
+ systemPromptAppend: '[重要系统功能] 你可以通过飞书发送文件给用户。方法:在响应中使用 [SEND_FILE:文件路径] 标记。示例:文件已准备好![SEND_FILE:/path/to/file.txt] 系统会自动上传并发送。',
136
+ fileMarkerPattern: /\[SEND_FILE:([^\]]+)\]/g,
137
+ supportsImages: true,
138
+ };
139
+ processor.registerChannel(feishuAdapter, feishuOptions);
140
+ cmdHandler.registerAdapter(feishuAdapter);
141
+ }
142
+ // 注册 AUN 适配器(如果已初始化)
143
+ if (aun) {
144
+ const aunAdapter = {
145
+ name: 'aun',
146
+ sendText: (channelId, text) => aun.sendMessage(channelId, text),
147
+ };
148
+ processor.registerChannel(aunAdapter);
149
+ cmdHandler.registerAdapter(aunAdapter);
150
+ }
151
+ // ── WeChat 渠道(条件初始化)──
152
+ let wechat = null;
153
+ if (config.channels?.wechat?.enabled && config.channels?.wechat?.token) {
154
+ wechat = new WechatChannel({
155
+ baseUrl: config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com',
156
+ token: config.channels.wechat.token,
157
+ });
158
+ const wechatAdapter = {
159
+ name: 'wechat',
160
+ sendText: (channelId, text) => wechat.sendMessage(channelId, text),
161
+ };
162
+ processor.registerChannel(wechatAdapter);
163
+ cmdHandler.registerAdapter(wechatAdapter);
164
+ // Session 过期通知(通过 Feishu 等其他渠道告知用户)
165
+ wechat.onSessionExpiredNotify(async (message) => {
166
+ // 尝试通过已注册的 Feishu owner 通知
167
+ const feishuOwner = config.channels?.feishu?.owner;
168
+ if (feishuOwner) {
169
+ try {
170
+ // Feishu owner ID 是 open_id,但 sendMessage 需要 chat_id
171
+ // 这里只记日志,因为 owner 的 chat_id 需要从 session 中获取
172
+ logger.warn(`[WeChat] ${message}`);
173
+ }
174
+ catch { }
175
+ }
176
+ else {
177
+ logger.warn(`[WeChat] ${message}`);
178
+ }
179
+ });
180
+ wechat.onMessage(async (channelId, content, userId) => {
181
+ content = content.trim();
182
+ // 首次交互自动绑定主人
183
+ if (userId && !config.channels?.wechat?.owner) {
184
+ const { setOwner } = await import('./config.js');
185
+ setOwner(config, 'wechat', userId);
186
+ logger.info(`[Owner] Auto-bound WeChat owner: ${userId}`);
187
+ }
188
+ // 命令快速路径
189
+ if (cmdHandler.isCommand(content)) {
190
+ const cmdResult = await cmdHandler.handle(content, 'wechat', channelId, undefined, userId);
191
+ if (cmdResult !== null) {
192
+ if (cmdResult) {
193
+ try {
194
+ await wechat.sendMessage(channelId, cmdResult);
195
+ }
196
+ catch (error) {
197
+ logger.error('[WeChat] Failed to send command response:', error);
198
+ }
155
199
  }
156
- catch (error) {
157
- logger.error('[Feishu] Failed to send command response:', error);
200
+ return;
201
+ }
202
+ }
203
+ // 获取当前项目路径
204
+ const session = await sessionManager.getOrCreateSession('wechat', channelId, config.projects?.defaultPath || process.cwd());
205
+ // 普通消息进入队列
206
+ await messageQueue.enqueue(`wechat-${channelId}`, { channel: 'wechat', channelId, content, timestamp: Date.now(), userId }, session.projectPath);
207
+ });
208
+ }
209
+ // Feishu 消息处理
210
+ if (feishu) {
211
+ feishu.onMessage(async (chatId, content, images, userId, userName, messageId) => {
212
+ content = content.trim();
213
+ // 首次交互自动绑定主人
214
+ if (userId && !config.channels?.feishu?.owner) {
215
+ const { setOwner } = await import('./config.js');
216
+ setOwner(config, 'feishu', userId);
217
+ logger.info(`[Owner] Auto-bound owner: ${userName} (${userId})`);
218
+ }
219
+ // 命令立即处理,不进入队列
220
+ if (cmdHandler.isCommand(content)) {
221
+ const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
222
+ if (cmdResult !== null) {
223
+ if (cmdResult) {
224
+ try {
225
+ await feishu.sendMessage(chatId, cmdResult, { forceText: true });
226
+ }
227
+ catch (error) {
228
+ logger.error('[Feishu] Failed to send command response:', error);
229
+ }
158
230
  }
231
+ return;
159
232
  }
160
- return;
161
233
  }
162
- }
163
- // 获取当前项目路径
164
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
165
- // 群聊消息添加用户名前缀
166
- const chatMode = await feishu.getChatMode(chatId);
167
- if (chatMode === 'group' && userName) {
168
- content = `[${userName}] ${content}`;
169
- }
170
- // 普通消息进入队列
171
- await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group' }, session.projectPath);
172
- });
234
+ // 获取当前项目路径
235
+ const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
236
+ // 群聊消息添加用户名前缀
237
+ const chatMode = await feishu.getChatMode(chatId);
238
+ if (chatMode === 'group' && userName) {
239
+ content = `[${userName}] ${content}`;
240
+ }
241
+ // 普通消息进入队列
242
+ await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group' }, session.projectPath);
243
+ });
244
+ }
173
245
  // AUN 消息处理
174
- aun.onMessage(async (sessionId, content) => {
175
- content = content.trim();
176
- // 首次交互自动绑定主人
177
- if (!config.owners?.aun) {
178
- const { setOwner } = await import('./config.js');
179
- setOwner(config, 'aun', sessionId);
180
- logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
181
- }
182
- // 命令立即处理,不进入队列
183
- if (cmdHandler.isCommand(content)) {
184
- const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
185
- if (cmdResult) {
186
- await aun.sendMessage(sessionId, cmdResult);
187
- return;
246
+ if (aun) {
247
+ aun.onMessage(async (sessionId, content) => {
248
+ content = content.trim();
249
+ // 首次交互自动绑定主人
250
+ if (!config.channels?.aun?.owner) {
251
+ const { setOwner } = await import('./config.js');
252
+ setOwner(config, 'aun', sessionId);
253
+ logger.info(`[Owner] Auto-bound AUN owner: ${sessionId}`);
188
254
  }
189
- }
190
- // 获取当前项目路径
191
- const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
192
- // 普通消息进入队列
193
- await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
194
- });
255
+ // 命令立即处理,不进入队列
256
+ if (cmdHandler.isCommand(content)) {
257
+ const cmdResult = await cmdHandler.handle(content, 'aun', sessionId, undefined, sessionId);
258
+ if (cmdResult) {
259
+ await aun.sendMessage(sessionId, cmdResult);
260
+ return;
261
+ }
262
+ }
263
+ // 获取当前项目路径
264
+ const session = await sessionManager.getOrCreateSession('aun', sessionId, config.projects?.defaultPath || process.cwd());
265
+ // 普通消息进入队列
266
+ await messageQueue.enqueue(`aun-${sessionId}`, { channel: 'aun', channelId: sessionId, content, timestamp: Date.now(), userId: sessionId }, session.projectPath);
267
+ });
268
+ }
195
269
  // 连接渠道
196
270
  const channels = [];
197
- try {
198
- await feishu.connect();
199
- logger.info(' Feishu connected');
200
- channels.push('Feishu');
201
- }
202
- catch (error) {
203
- logger.warn('⚠ Feishu connection failed (will continue without it)');
204
- if (error instanceof Error) {
205
- logger.warn(` Reason: ${error.message}`);
271
+ const channelInstances = [
272
+ ...(feishu ? [{ name: 'Feishu', instance: feishu }] : []),
273
+ ...(aun ? [{ name: 'AUN', instance: aun }] : []),
274
+ ...(wechat ? [{ name: 'WeChat', instance: wechat }] : []),
275
+ ];
276
+ for (const { name, instance } of channelInstances) {
277
+ try {
278
+ await instance.connect();
279
+ logger.info(`✓ ${name} connected`);
280
+ channels.push(name);
206
281
  }
207
- }
208
- try {
209
- await aun.connect();
210
- logger.info('✓ AUN connected');
211
- channels.push('AUN');
212
- }
213
- catch (error) {
214
- logger.warn('⚠ AUN connection failed (will continue without it)');
215
- if (error instanceof Error) {
216
- logger.warn(` Reason: ${error.message}`);
282
+ catch (error) {
283
+ logger.warn(`⚠ ${name} connection failed (will continue without it)`);
284
+ if (error instanceof Error) {
285
+ logger.warn(` Reason: ${error.message}`);
286
+ }
217
287
  }
218
288
  }
219
289
  logger.info(`\n🚀 EvolClaw is running with ${channels.length} channel(s): ${channels.join(', ')}\n`);
@@ -224,8 +294,12 @@ async function main() {
224
294
  // 优雅关闭
225
295
  const shutdown = async () => {
226
296
  logger.info('\n\nShutting down gracefully...');
227
- await feishu.disconnect();
228
- await aun.disconnect();
297
+ if (feishu)
298
+ await feishu.disconnect();
299
+ if (aun)
300
+ await aun.disconnect();
301
+ if (wechat)
302
+ await wechat.disconnect();
229
303
  sessionManager.close();
230
304
  logger.info('✓ Shutdown complete');
231
305
  process.exit(0);
@@ -0,0 +1,261 @@
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
+ }