evolclaw 2.2.0 → 2.4.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 +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +283 -95
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +232 -57
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +803 -247
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +217 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
- package/dist/index.js +140 -57
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/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 +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { logger } from '../utils/logger.js';
|
|
2
|
-
import { StreamDebouncer } from '../utils/stream-debouncer.js';
|
|
3
|
-
/**
|
|
4
|
-
* MessageBridge — Channel 与 Core 之间的消息桥梁
|
|
5
|
-
*
|
|
6
|
-
* 入站管线:Channel.onMessage → owner 绑定 → 命令路由 → session 解析
|
|
7
|
-
* → 策略前缀 → 构造 Message → debounce → ACK → enqueue
|
|
8
|
-
* 出站:命令响应通过 sendReply 回调直接发送到渠道
|
|
9
|
-
*/
|
|
10
|
-
export class MessageBridge {
|
|
11
|
-
config;
|
|
12
|
-
sessionManager;
|
|
13
|
-
processor;
|
|
14
|
-
messageQueue;
|
|
15
|
-
cmdHandler;
|
|
16
|
-
eventBus;
|
|
17
|
-
debouncers = new Map();
|
|
18
|
-
defaultDebounce;
|
|
19
|
-
constructor(config, sessionManager, processor, messageQueue, cmdHandler, eventBus) {
|
|
20
|
-
this.config = config;
|
|
21
|
-
this.sessionManager = sessionManager;
|
|
22
|
-
this.processor = processor;
|
|
23
|
-
this.messageQueue = messageQueue;
|
|
24
|
-
this.cmdHandler = cmdHandler;
|
|
25
|
-
this.eventBus = eventBus;
|
|
26
|
-
this.defaultDebounce = config.debounce ?? 2;
|
|
27
|
-
}
|
|
28
|
-
getDebouncer(channelName) {
|
|
29
|
-
let d = this.debouncers.get(channelName);
|
|
30
|
-
if (!d) {
|
|
31
|
-
const chConfig = this.config.channels?.[channelName];
|
|
32
|
-
const seconds = chConfig?.debounce ?? this.defaultDebounce;
|
|
33
|
-
d = new StreamDebouncer(seconds);
|
|
34
|
-
this.debouncers.set(channelName, d);
|
|
35
|
-
}
|
|
36
|
-
return d;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* 为渠道注册消息桥梁:入站处理管线 + 出站命令响应
|
|
40
|
-
*
|
|
41
|
-
* @param channelName 渠道标识
|
|
42
|
-
* @param onMessage 注册入站消息监听
|
|
43
|
-
* @param sendReply 出站:命令响应发送回调
|
|
44
|
-
* @param adapter 渠道适配器(用于 ACK)
|
|
45
|
-
*/
|
|
46
|
-
register(channelName, onMessage, sendReply, adapter) {
|
|
47
|
-
onMessage(async (msg) => {
|
|
48
|
-
let content = msg.content.trim();
|
|
49
|
-
// 0. 自定义消息快速路径(menu.query 等)
|
|
50
|
-
if (await this.handleCustomPayload(content, channelName, msg, sendReply, adapter))
|
|
51
|
-
return;
|
|
52
|
-
// 1. owner 绑定
|
|
53
|
-
if (msg.peerId)
|
|
54
|
-
await this.autoBindOwner(channelName, msg.peerId);
|
|
55
|
-
// 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
|
|
56
|
-
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
57
|
-
if (await this.handleCommand(contentForCmd || content, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId))
|
|
58
|
-
return;
|
|
59
|
-
// 3. session 解析(使用 Channel 层填充的 chatType)
|
|
60
|
-
const chatType = msg.chatType || 'private';
|
|
61
|
-
const metadata = {};
|
|
62
|
-
if (msg.replyContext)
|
|
63
|
-
metadata.replyContext = msg.replyContext;
|
|
64
|
-
if (chatType === 'private' && msg.peerId) {
|
|
65
|
-
metadata.peerId = msg.peerId;
|
|
66
|
-
if (msg.peerName)
|
|
67
|
-
metadata.peerName = msg.peerName;
|
|
68
|
-
}
|
|
69
|
-
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, this.config.projects?.defaultPath || process.cwd(), msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType);
|
|
70
|
-
// 4. 消息前缀(由 policy 决定)
|
|
71
|
-
const channelInfo = this.processor.getChannelInfo?.(channelName);
|
|
72
|
-
if (channelInfo?.policy) {
|
|
73
|
-
const prefix = channelInfo.policy.messagePrefix(chatType, msg.peerName);
|
|
74
|
-
if (prefix)
|
|
75
|
-
content = prefix + content;
|
|
76
|
-
}
|
|
77
|
-
// 5. 构造完整消息
|
|
78
|
-
const fullMessage = {
|
|
79
|
-
channel: channelName, channelId: msg.channelId, content,
|
|
80
|
-
chatType,
|
|
81
|
-
images: msg.images, timestamp: Date.now(),
|
|
82
|
-
peerId: msg.peerId, peerName: msg.peerName,
|
|
83
|
-
messageId: msg.messageId,
|
|
84
|
-
mentions: msg.mentions, threadId: msg.threadId,
|
|
85
|
-
replyContext: msg.replyContext,
|
|
86
|
-
};
|
|
87
|
-
// 6. ACK + debounce/enqueue
|
|
88
|
-
// ACK 在到达时立即做(每条独立 ACK),不等合并
|
|
89
|
-
// Interrupt 模式(单聊)→ 入队前 debounce 合并
|
|
90
|
-
// FIFO 模式(群聊) → 跳过 debouncer,独立入队,出队时贪心合并
|
|
91
|
-
if (fullMessage.messageId)
|
|
92
|
-
adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
|
|
93
|
-
const isInterrupt = chatType !== 'group';
|
|
94
|
-
const doEnqueue = async (m) => {
|
|
95
|
-
return this.messageQueue.enqueue(session.id, m, session.projectPath, {
|
|
96
|
-
interruptible: isInterrupt,
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
if (isInterrupt) {
|
|
100
|
-
const debouncer = this.getDebouncer(channelName);
|
|
101
|
-
if (debouncer.enabled) {
|
|
102
|
-
const debounceKey = msg.peerId ? `${session.id}:${msg.peerId}` : session.id;
|
|
103
|
-
await debouncer.submit(debounceKey, fullMessage, doEnqueue);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
await doEnqueue(fullMessage);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// 群聊 FIFO:直接入队,由 MessageQueue.processNext 出队时合并
|
|
111
|
-
await doEnqueue(fullMessage);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
/** 自定义消息快速路径:拦截 menu.query 等自定义 payload,返回 true 表示已处理 */
|
|
116
|
-
async handleCustomPayload(content, channel, msg, sendReply, adapter) {
|
|
117
|
-
let parsed;
|
|
118
|
-
try {
|
|
119
|
-
parsed = JSON.parse(content);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
if (!parsed || typeof parsed !== 'object' || !parsed.type)
|
|
125
|
-
return false;
|
|
126
|
-
if (parsed.type === 'menu.query') {
|
|
127
|
-
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
128
|
-
const isAdmin = identity.role === 'owner';
|
|
129
|
-
const items = this.cmdHandler.getMenuItems(isAdmin);
|
|
130
|
-
const response = JSON.stringify({ type: 'menu.response', items });
|
|
131
|
-
if (adapter?.sendCustomPayload) {
|
|
132
|
-
adapter.sendCustomPayload(msg.channelId, response);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
await sendReply(msg.channelId, response);
|
|
136
|
-
}
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
/** 首次交互自动绑定 owner */
|
|
142
|
-
async autoBindOwner(channel, userId) {
|
|
143
|
-
const channelConfig = this.config.channels?.[channel];
|
|
144
|
-
if (channelConfig && !channelConfig.owner) {
|
|
145
|
-
const { setOwner } = await import('../config.js');
|
|
146
|
-
setOwner(this.config, channel, userId);
|
|
147
|
-
logger.info(`[Owner] Auto-bound ${channel} owner: ${userId}`);
|
|
148
|
-
this.eventBus.publish({ type: 'channel:owner-bound', channel, userId });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/** 命令快速路径:返回 true 表示已处理 */
|
|
152
|
-
async handleCommand(content, channel, channelId, sendReply, userId, threadId) {
|
|
153
|
-
if (!this.cmdHandler.isCommand(content))
|
|
154
|
-
return false;
|
|
155
|
-
const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId);
|
|
156
|
-
if (cmdResult === null)
|
|
157
|
-
return false;
|
|
158
|
-
if (cmdResult) {
|
|
159
|
-
try {
|
|
160
|
-
await sendReply(cmdResult);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
logger.error(`[${channel}] Failed to send command response:`, error);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* 撤回消息:先查 debounce 窗口,再查 message queue。
|
|
170
|
-
* @returns true 如果找到并取消
|
|
171
|
-
*/
|
|
172
|
-
cancel(messageId) {
|
|
173
|
-
// 阶段 1: debounce 窗口(尚未入队)
|
|
174
|
-
for (const d of this.debouncers.values()) {
|
|
175
|
-
if (d.cancel(messageId))
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
// 阶段 2: 已入队但未处理(合并后 messageId 可能是逗号分隔的多个 id)
|
|
179
|
-
return this.messageQueue.cancel(messageId);
|
|
180
|
-
}
|
|
181
|
-
/** 清理资源 */
|
|
182
|
-
dispose() {
|
|
183
|
-
for (const d of this.debouncers.values())
|
|
184
|
-
d.dispose();
|
|
185
|
-
this.debouncers.clear();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,263 +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
|
-
if (!config.channels.defaultChannel)
|
|
251
|
-
config.channels.defaultChannel = 'feishu';
|
|
252
|
-
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
253
|
-
console.log(`\n✅ 飞书连接成功!`);
|
|
254
|
-
console.log(` App ID: ${result.appId}`);
|
|
255
|
-
if (result.openId) {
|
|
256
|
-
console.log(` Owner: ${result.openId}`);
|
|
257
|
-
}
|
|
258
|
-
if (result.domain !== 'unknown') {
|
|
259
|
-
console.log(` Domain: ${result.domain}`);
|
|
260
|
-
}
|
|
261
|
-
console.log(` 配置已写入: ${p.config}`);
|
|
262
|
-
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
263
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import readline from 'readline';
|
|
3
|
-
import { resolvePaths } from '../paths.js';
|
|
4
|
-
const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
5
|
-
const BOT_TYPE = '3';
|
|
6
|
-
const QR_POLL_TIMEOUT_MS = 35_000;
|
|
7
|
-
const LOGIN_TIMEOUT_MS = 480_000;
|
|
8
|
-
function ask(rl, question) {
|
|
9
|
-
return new Promise(resolve => rl.question(question, resolve));
|
|
10
|
-
}
|
|
11
|
-
async function fetchQRCode(baseUrl) {
|
|
12
|
-
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
13
|
-
const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
|
|
14
|
-
const res = await fetch(url);
|
|
15
|
-
if (!res.ok)
|
|
16
|
-
throw new Error(`QR fetch failed: ${res.status}`);
|
|
17
|
-
return (await res.json());
|
|
18
|
-
}
|
|
19
|
-
async function pollQRStatus(baseUrl, qrcode) {
|
|
20
|
-
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
21
|
-
const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
22
|
-
const controller = new AbortController();
|
|
23
|
-
const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
|
|
24
|
-
try {
|
|
25
|
-
const res = await fetch(url, {
|
|
26
|
-
headers: { 'iLink-App-ClientVersion': '1' },
|
|
27
|
-
signal: controller.signal,
|
|
28
|
-
});
|
|
29
|
-
clearTimeout(timer);
|
|
30
|
-
if (!res.ok)
|
|
31
|
-
throw new Error(`QR status failed: ${res.status}`);
|
|
32
|
-
return (await res.json());
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
clearTimeout(timer);
|
|
36
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
37
|
-
return { status: 'wait' };
|
|
38
|
-
}
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export async function runWechatQrFlow() {
|
|
43
|
-
const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
|
|
44
|
-
try {
|
|
45
|
-
const qrterm = await import('qrcode-terminal');
|
|
46
|
-
await new Promise(resolve => {
|
|
47
|
-
qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
|
|
48
|
-
console.log(qr);
|
|
49
|
-
resolve();
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
|
|
55
|
-
}
|
|
56
|
-
console.log('请用微信扫描上方二维码...\n');
|
|
57
|
-
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
58
|
-
let scannedPrinted = false;
|
|
59
|
-
while (Date.now() < deadline) {
|
|
60
|
-
const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
|
|
61
|
-
switch (status.status) {
|
|
62
|
-
case 'wait':
|
|
63
|
-
process.stdout.write('.');
|
|
64
|
-
break;
|
|
65
|
-
case 'scaned':
|
|
66
|
-
if (!scannedPrinted) {
|
|
67
|
-
console.log('\n👀 已扫码,请在微信中确认...');
|
|
68
|
-
scannedPrinted = true;
|
|
69
|
-
}
|
|
70
|
-
break;
|
|
71
|
-
case 'expired':
|
|
72
|
-
console.error('\n二维码已过期');
|
|
73
|
-
return null;
|
|
74
|
-
case 'confirmed':
|
|
75
|
-
if (!status.ilink_bot_id || !status.bot_token) {
|
|
76
|
-
console.error('\n登录失败:服务器未返回完整信息');
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
baseUrl: status.baseurl || DEFAULT_BASE_URL,
|
|
81
|
-
token: status.bot_token,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
85
|
-
}
|
|
86
|
-
console.log('\n登录超时');
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
export async function cmdInitWechat() {
|
|
90
|
-
const p = resolvePaths();
|
|
91
|
-
if (!fs.existsSync(p.config)) {
|
|
92
|
-
console.log(`❌ 配置文件不存在,请先运行 evolclaw init`);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
96
|
-
// 检查已有配置
|
|
97
|
-
if (config.channels?.wechat?.token) {
|
|
98
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
99
|
-
try {
|
|
100
|
-
const answer = (await ask(rl, '已有微信配置,是否重新登录?[y/N] ')).trim().toLowerCase();
|
|
101
|
-
if (answer !== 'y' && answer !== 'yes') {
|
|
102
|
-
console.log('已取消');
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
rl.close();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
console.log('正在获取微信登录二维码...\n');
|
|
111
|
-
const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
|
|
112
|
-
// 终端显示二维码
|
|
113
|
-
try {
|
|
114
|
-
const qrterm = await import('qrcode-terminal');
|
|
115
|
-
await new Promise(resolve => {
|
|
116
|
-
qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
|
|
117
|
-
console.log(qr);
|
|
118
|
-
resolve();
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
|
|
124
|
-
}
|
|
125
|
-
console.log('请用微信扫描上方二维码...\n');
|
|
126
|
-
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
127
|
-
let scannedPrinted = false;
|
|
128
|
-
while (Date.now() < deadline) {
|
|
129
|
-
const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
|
|
130
|
-
switch (status.status) {
|
|
131
|
-
case 'wait':
|
|
132
|
-
process.stdout.write('.');
|
|
133
|
-
break;
|
|
134
|
-
case 'scaned':
|
|
135
|
-
if (!scannedPrinted) {
|
|
136
|
-
console.log('\n👀 已扫码,请在微信中确认...');
|
|
137
|
-
scannedPrinted = true;
|
|
138
|
-
}
|
|
139
|
-
break;
|
|
140
|
-
case 'expired':
|
|
141
|
-
console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
|
|
142
|
-
process.exit(1);
|
|
143
|
-
break;
|
|
144
|
-
case 'confirmed': {
|
|
145
|
-
if (!status.ilink_bot_id || !status.bot_token) {
|
|
146
|
-
console.error('\n登录失败:服务器未返回完整信息');
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
// 写入配置
|
|
150
|
-
if (!config.channels)
|
|
151
|
-
config.channels = {};
|
|
152
|
-
config.channels.wechat = {
|
|
153
|
-
enabled: true,
|
|
154
|
-
baseUrl: status.baseurl || DEFAULT_BASE_URL,
|
|
155
|
-
token: status.bot_token,
|
|
156
|
-
};
|
|
157
|
-
if (!config.channels.defaultChannel)
|
|
158
|
-
config.channels.defaultChannel = 'wechat';
|
|
159
|
-
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
160
|
-
console.log(`\n✅ 微信连接成功!`);
|
|
161
|
-
console.log(` Bot ID: ${status.ilink_bot_id}`);
|
|
162
|
-
console.log(` User ID: ${status.ilink_user_id}`);
|
|
163
|
-
console.log(` 配置已写入: ${p.config}`);
|
|
164
|
-
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
169
|
-
}
|
|
170
|
-
console.log('\n登录超时,请重新运行');
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
package/dist/utils/ipc-client.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import net from 'net';
|
|
2
|
-
/**
|
|
3
|
-
* Query the running EvolClaw daemon via Unix socket.
|
|
4
|
-
* Returns null if the service is not running or the socket is unreachable.
|
|
5
|
-
*/
|
|
6
|
-
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
7
|
-
return new Promise((resolve) => {
|
|
8
|
-
const conn = net.connect(socketPath);
|
|
9
|
-
let buf = '';
|
|
10
|
-
const timer = setTimeout(() => {
|
|
11
|
-
conn.destroy();
|
|
12
|
-
resolve(null);
|
|
13
|
-
}, timeoutMs);
|
|
14
|
-
conn.on('connect', () => {
|
|
15
|
-
conn.write(JSON.stringify(cmd) + '\n');
|
|
16
|
-
});
|
|
17
|
-
conn.on('data', (data) => {
|
|
18
|
-
buf += data.toString();
|
|
19
|
-
const idx = buf.indexOf('\n');
|
|
20
|
-
if (idx !== -1) {
|
|
21
|
-
clearTimeout(timer);
|
|
22
|
-
try {
|
|
23
|
-
resolve(JSON.parse(buf.slice(0, idx)));
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
resolve(null);
|
|
27
|
-
}
|
|
28
|
-
conn.destroy();
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
conn.on('error', () => {
|
|
32
|
-
clearTimeout(timer);
|
|
33
|
-
resolve(null);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|