evolcore 0.0.10 → 0.0.11
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +123 -42
- package/dist/agents/codex-app-server-client.js +33 -9
- package/dist/agents/codex-runner.js +58 -8
- package/dist/agents/ecagent-runner.js +17 -2
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +131 -128
- package/dist/cli/agent-command.js +16 -9
- package/dist/cli/agent.js +82 -19
- package/dist/cli/daemon-commands.js +21 -2
- package/dist/cli/index.js +76 -61
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/gateway-config.js +26 -10
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/operation-authorizer.js +32 -147
- package/dist/core/auth/operation-catalog.js +80 -0
- package/dist/core/bootstrap-messages.js +50 -0
- package/dist/core/bootstrap-service.js +85 -10
- package/dist/core/channel-loader.js +23 -6
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/menu-handler.js +67 -76
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +8 -3
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/message-bridge.js +1 -1
- package/dist/core/message/message-log.js +22 -0
- package/dist/core/message/message-queue.js +19 -4
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +231 -93
- package/dist/core/protected-paths.js +10 -7
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/index.js +262 -84
- package/dist/ipc.js +29 -11
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/rules/01-overview.md +9 -0
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +17 -0
- package/kits/templates/roles/visitor.json +8 -0
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +2 -1
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
package/dist/cli/init-channel.js
CHANGED
|
@@ -18,16 +18,16 @@ import * as platform from '../utils/cross-platform.js';
|
|
|
18
18
|
import { cleanupInstances } from '../utils/instance-registry.js';
|
|
19
19
|
import { bindContactAlias, getContactSnapshot } from '../config/contact-book.js';
|
|
20
20
|
import { listStaticAgentAdmins } from '../config/peer-role-resolver.js';
|
|
21
|
-
import {
|
|
21
|
+
import { askInit, InitCancelledError, isInitCancelledError, setupInitRawKeyListener, waitForInitDelay, } from './init-cancel.js';
|
|
22
22
|
function ask(rl, question) {
|
|
23
|
-
return
|
|
23
|
+
return askInit(rl, question);
|
|
24
24
|
}
|
|
25
|
-
export async function runDaemonOwnerQrBindFlow(
|
|
25
|
+
export async function runDaemonOwnerQrBindFlow() {
|
|
26
26
|
const p = resolvePaths();
|
|
27
27
|
const daemonConfig = loadDaemonConfig();
|
|
28
28
|
if (!daemonConfig.aid) {
|
|
29
29
|
console.log('❌ 未找到控制 AID,请先运行 ec init');
|
|
30
|
-
return
|
|
30
|
+
return { action: 'manual' };
|
|
31
31
|
}
|
|
32
32
|
let status = await ipcQuery(p.socket, { type: 'status' }, 1000);
|
|
33
33
|
let startedBootstrapDaemon = false;
|
|
@@ -39,39 +39,40 @@ export async function runDaemonOwnerQrBindFlow(ownerMode = 'append') {
|
|
|
39
39
|
try {
|
|
40
40
|
if (!status) {
|
|
41
41
|
console.log('❌ daemon 未就绪,无法创建绑定任务');
|
|
42
|
-
return
|
|
42
|
+
return { action: 'manual' };
|
|
43
43
|
}
|
|
44
44
|
if (!status.controlAid?.connected) {
|
|
45
45
|
console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
47
|
+
while (true) {
|
|
48
|
+
const begin = await ipcQuery(p.socket, {
|
|
49
|
+
type: 'bind.begin',
|
|
50
|
+
bindType: 'daemon',
|
|
51
|
+
}, 3000);
|
|
52
|
+
if (!begin?.ok) {
|
|
53
|
+
console.log(`❌ 创建绑定任务失败: ${begin?.error || 'unknown error'}`);
|
|
54
|
+
return { action: 'manual' };
|
|
55
|
+
}
|
|
56
|
+
await printQr(begin.qrData);
|
|
57
|
+
console.log('\n请用 evol app 扫描上方二维码完成绑定');
|
|
58
|
+
console.log('按 r 重新生成 | 按 m 手动输入 AID | 连续按两次 Esc 或 Ctrl+C 退出 | 10 分钟内有效\n');
|
|
59
|
+
const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
|
|
60
|
+
if (result === 'retry') {
|
|
61
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
62
|
+
console.log('\n正在生成新的绑定二维码...\n');
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (result === 'interrupt') {
|
|
66
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
67
|
+
process.exitCode = 130;
|
|
68
|
+
return { action: 'interrupted' };
|
|
69
|
+
}
|
|
70
|
+
if (result === 'manual' || !result) {
|
|
71
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
72
|
+
return { action: 'manual' };
|
|
73
|
+
}
|
|
74
|
+
return { action: 'bound', boundAid: result.boundAid, boundName: result.boundName };
|
|
73
75
|
}
|
|
74
|
-
return { boundAid: result.boundAid, boundName: result.boundName };
|
|
75
76
|
}
|
|
76
77
|
finally {
|
|
77
78
|
if (startedBootstrapDaemon) {
|
|
@@ -79,12 +80,12 @@ export async function runDaemonOwnerQrBindFlow(ownerMode = 'append') {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
export async function runAgentOwnerQrBindFlow(targetAid, agentName
|
|
83
|
+
export async function runAgentOwnerQrBindFlow(targetAid, agentName) {
|
|
83
84
|
const p = resolvePaths();
|
|
84
85
|
const daemonConfig = loadDaemonConfig();
|
|
85
86
|
if (!daemonConfig.aid) {
|
|
86
87
|
console.log('❌ 未找到控制 AID,请先运行 ec init');
|
|
87
|
-
return
|
|
88
|
+
return { action: 'manual' };
|
|
88
89
|
}
|
|
89
90
|
let status = await ipcQuery(p.socket, { type: 'status' }, 1000);
|
|
90
91
|
if (!status) {
|
|
@@ -93,42 +94,43 @@ export async function runAgentOwnerQrBindFlow(targetAid, agentName, ownerMode =
|
|
|
93
94
|
}
|
|
94
95
|
if (!status) {
|
|
95
96
|
console.log('❌ daemon 未就绪,无法创建 agent 绑定任务');
|
|
96
|
-
return
|
|
97
|
+
return { action: 'manual' };
|
|
97
98
|
}
|
|
98
99
|
if (!status.controlAid?.connected) {
|
|
99
100
|
console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
102
|
+
while (true) {
|
|
103
|
+
const begin = await ipcQuery(p.socket, {
|
|
104
|
+
type: 'bind.begin',
|
|
105
|
+
bindType: 'agent',
|
|
106
|
+
targetAid,
|
|
107
|
+
agentName,
|
|
108
|
+
}, 3000);
|
|
109
|
+
if (!begin?.ok) {
|
|
110
|
+
console.log(`❌ 创建 agent 绑定任务失败: ${begin?.error || 'unknown error'}`);
|
|
111
|
+
return { action: 'manual' };
|
|
112
|
+
}
|
|
113
|
+
await printQr(begin.qrData);
|
|
114
|
+
console.log('\n请用 evol app 扫描上方二维码完成绑定');
|
|
115
|
+
console.log('按 r 重新生成 | 按 m 手动输入 AID | 连续按两次 Esc 或 Ctrl+C 退出 | 10 分钟内有效\n');
|
|
116
|
+
const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
|
|
117
|
+
if (result === 'retry') {
|
|
118
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
119
|
+
console.log('\n正在生成新的绑定二维码...\n');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (result === 'interrupt') {
|
|
123
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
124
|
+
process.exitCode = 130;
|
|
125
|
+
return { action: 'interrupted' };
|
|
126
|
+
}
|
|
127
|
+
if (result === 'manual' || !result) {
|
|
128
|
+
await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
|
|
129
|
+
return { action: 'manual' };
|
|
130
|
+
}
|
|
131
|
+
await reloadAgentAfterBind(targetAid);
|
|
132
|
+
return { action: 'bound', boundAid: result.boundAid, boundName: result.boundName };
|
|
129
133
|
}
|
|
130
|
-
await reloadAgentAfterBind(targetAid);
|
|
131
|
-
return { boundAid: result.boundAid, boundName: result.boundName };
|
|
132
134
|
}
|
|
133
135
|
async function waitForIpcStatus(timeoutMs) {
|
|
134
136
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -197,36 +199,53 @@ async function printQr(qrData) {
|
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
async function pollBindStatusWithKeyboard(taskId, expiresAt) {
|
|
200
|
-
let
|
|
201
|
-
let
|
|
202
|
+
let keyAction = null;
|
|
203
|
+
let expired = false;
|
|
202
204
|
const p = resolvePaths();
|
|
203
|
-
const cleanup =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (key === 's' || key === 'S')
|
|
207
|
-
skip = true;
|
|
208
|
-
});
|
|
205
|
+
const cleanup = setupInitRawKeyListener((key) => {
|
|
206
|
+
keyAction = ownerBindKeyAction(key) ?? keyAction;
|
|
207
|
+
}, () => { keyAction = 'interrupt'; });
|
|
209
208
|
try {
|
|
210
|
-
while (
|
|
211
|
-
if (
|
|
212
|
-
return
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
while (true) {
|
|
210
|
+
if (keyAction)
|
|
211
|
+
return keyAction;
|
|
212
|
+
if (!expired && Date.now() >= expiresAt)
|
|
213
|
+
expired = true;
|
|
214
|
+
if (!expired) {
|
|
215
|
+
const status = await ipcQuery(p.socket, { type: 'bind.status', taskId }, 1500);
|
|
216
|
+
if (status?.ok && status.status === 'bound') {
|
|
217
|
+
return { boundAid: status.boundAid, boundName: status.boundName };
|
|
218
|
+
}
|
|
219
|
+
if (status?.ok && status.status === 'expired')
|
|
220
|
+
expired = true;
|
|
221
|
+
if (status?.ok && status.status === 'cancelled')
|
|
222
|
+
return null;
|
|
218
223
|
}
|
|
219
|
-
if (
|
|
220
|
-
|
|
224
|
+
if (expired) {
|
|
225
|
+
console.log('\n绑定二维码已过期。');
|
|
226
|
+
console.log('按 r 重新生成 | 按 m 手动输入 AID | 连续按两次 Esc 或 Ctrl+C 退出\n');
|
|
227
|
+
if (!process.stdin.isTTY)
|
|
228
|
+
return null;
|
|
229
|
+
while (!keyAction)
|
|
230
|
+
await new Promise(r => setTimeout(r, 100));
|
|
231
|
+
return keyAction;
|
|
221
232
|
}
|
|
222
233
|
await new Promise(r => setTimeout(r, 1000));
|
|
223
234
|
}
|
|
224
|
-
return null;
|
|
225
235
|
}
|
|
226
236
|
finally {
|
|
227
237
|
cleanup();
|
|
228
238
|
}
|
|
229
239
|
}
|
|
240
|
+
export function ownerBindKeyAction(key) {
|
|
241
|
+
if (key === '\u0003')
|
|
242
|
+
return 'interrupt';
|
|
243
|
+
if (key === 'm' || key === 'M')
|
|
244
|
+
return 'manual';
|
|
245
|
+
if (key === 'r' || key === 'R')
|
|
246
|
+
return 'retry';
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
230
249
|
// ==================== Feishu ====================
|
|
231
250
|
const FEISHU_PROD_URL = 'https://accounts.feishu.cn';
|
|
232
251
|
const LARK_PROD_URL = 'https://accounts.larksuite.com';
|
|
@@ -240,30 +259,38 @@ class FeishuQrRegistrationClient {
|
|
|
240
259
|
setDomain(isLark) {
|
|
241
260
|
this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
|
|
242
261
|
}
|
|
243
|
-
async init() {
|
|
244
|
-
return this.postRegistration('init', {});
|
|
262
|
+
async init(signal) {
|
|
263
|
+
return this.postRegistration('init', {}, signal);
|
|
245
264
|
}
|
|
246
|
-
async begin() {
|
|
265
|
+
async begin(signal) {
|
|
247
266
|
return this.postRegistration('begin', {
|
|
248
267
|
archetype: 'PersonalAgent',
|
|
249
268
|
auth_method: 'client_secret',
|
|
250
269
|
request_user_info: 'open_id',
|
|
251
|
-
});
|
|
270
|
+
}, signal);
|
|
252
271
|
}
|
|
253
|
-
async poll(deviceCode) {
|
|
254
|
-
return this.postRegistration('poll', { device_code: deviceCode });
|
|
272
|
+
async poll(deviceCode, signal) {
|
|
273
|
+
return this.postRegistration('poll', { device_code: deviceCode }, signal);
|
|
255
274
|
}
|
|
256
|
-
async postRegistration(action, extraParams) {
|
|
275
|
+
async postRegistration(action, extraParams, signal) {
|
|
257
276
|
const body = new URLSearchParams({ action, ...extraParams }).toString();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
277
|
+
try {
|
|
278
|
+
const res = await fetch(`${this.baseUrl}/oauth/v1/app/registration`, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
281
|
+
body,
|
|
282
|
+
signal,
|
|
283
|
+
});
|
|
284
|
+
const text = await res.text();
|
|
285
|
+
if (!text)
|
|
286
|
+
return {};
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (signal?.aborted)
|
|
291
|
+
throw new InitCancelledError();
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
267
294
|
}
|
|
268
295
|
}
|
|
269
296
|
async function runQrRegistrationFlow() {
|
|
@@ -291,14 +318,22 @@ async function runQrRegistrationFlow() {
|
|
|
291
318
|
console.log(`请在浏览器中打开此链接扫码: ${beginResult.verification_uri_complete}\n`);
|
|
292
319
|
}
|
|
293
320
|
console.log('请用飞书/Lark 扫描上方二维码...\n');
|
|
294
|
-
console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret
|
|
321
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret | 连续按两次 Esc 退出\n');
|
|
295
322
|
let userAction = null;
|
|
296
|
-
|
|
297
|
-
|
|
323
|
+
let initCancelled = false;
|
|
324
|
+
const cancelController = new AbortController();
|
|
325
|
+
const cancel = () => {
|
|
326
|
+
initCancelled = true;
|
|
327
|
+
cancelController.abort();
|
|
328
|
+
};
|
|
329
|
+
const cleanup = setupInitRawKeyListener((key) => {
|
|
330
|
+
if (key === 'q')
|
|
298
331
|
userAction = QUIT;
|
|
299
332
|
if (key === 's')
|
|
300
333
|
userAction = SKIP;
|
|
301
|
-
|
|
334
|
+
if (key === '\u0003')
|
|
335
|
+
cancel();
|
|
336
|
+
}, cancel);
|
|
302
337
|
const startedAt = Date.now();
|
|
303
338
|
let pollIntervalSeconds = Number(beginResult.interval ?? 5);
|
|
304
339
|
const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
|
|
@@ -306,11 +341,15 @@ async function runQrRegistrationFlow() {
|
|
|
306
341
|
let currentDomain = 'feishu';
|
|
307
342
|
try {
|
|
308
343
|
while (Date.now() - startedAt < expireInSeconds * 1000) {
|
|
344
|
+
if (initCancelled)
|
|
345
|
+
throw new InitCancelledError();
|
|
309
346
|
if (userAction === QUIT)
|
|
310
347
|
return QUIT;
|
|
311
348
|
if (userAction === SKIP)
|
|
312
349
|
return SKIP;
|
|
313
|
-
const pollResult = await client.poll(beginResult.device_code);
|
|
350
|
+
const pollResult = await client.poll(beginResult.device_code, cancelController.signal);
|
|
351
|
+
if (initCancelled)
|
|
352
|
+
throw new InitCancelledError();
|
|
314
353
|
if (pollResult.user_info?.tenant_brand === 'lark' && !domainResolved) {
|
|
315
354
|
client.setDomain(true);
|
|
316
355
|
currentDomain = 'lark';
|
|
@@ -325,12 +364,12 @@ async function runQrRegistrationFlow() {
|
|
|
325
364
|
};
|
|
326
365
|
}
|
|
327
366
|
if (pollResult.error === 'authorization_pending') {
|
|
328
|
-
await
|
|
367
|
+
await waitForInitDelay(pollIntervalSeconds * 1000, cancelController.signal);
|
|
329
368
|
continue;
|
|
330
369
|
}
|
|
331
370
|
if (pollResult.error === 'slow_down') {
|
|
332
371
|
pollIntervalSeconds += 5;
|
|
333
|
-
await
|
|
372
|
+
await waitForInitDelay(pollIntervalSeconds * 1000, cancelController.signal);
|
|
334
373
|
continue;
|
|
335
374
|
}
|
|
336
375
|
if (pollResult.error === 'access_denied') {
|
|
@@ -342,7 +381,7 @@ async function runQrRegistrationFlow() {
|
|
|
342
381
|
if (pollResult.error) {
|
|
343
382
|
throw new Error(`扫码注册失败: ${pollResult.error}${pollResult.error_description ? ` - ${pollResult.error_description}` : ''}`);
|
|
344
383
|
}
|
|
345
|
-
await
|
|
384
|
+
await waitForInitDelay(pollIntervalSeconds * 1000, cancelController.signal);
|
|
346
385
|
}
|
|
347
386
|
throw new Error('等待扫码结果超时');
|
|
348
387
|
}
|
|
@@ -374,6 +413,8 @@ export async function runFeishuQrFlow() {
|
|
|
374
413
|
return result;
|
|
375
414
|
}
|
|
376
415
|
catch (error) {
|
|
416
|
+
if (isInitCancelledError(error))
|
|
417
|
+
throw error;
|
|
377
418
|
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
378
419
|
return null;
|
|
379
420
|
}
|
|
@@ -436,6 +477,8 @@ export async function cmdInitFeishu() {
|
|
|
436
477
|
}
|
|
437
478
|
}
|
|
438
479
|
catch (error) {
|
|
480
|
+
if (isInitCancelledError(error))
|
|
481
|
+
throw error;
|
|
439
482
|
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
440
483
|
process.exit(1);
|
|
441
484
|
}
|
|
@@ -480,11 +523,13 @@ async function fetchQRCode(baseUrl) {
|
|
|
480
523
|
throw new Error(`QR fetch failed: ${res.status}`);
|
|
481
524
|
return (await res.json());
|
|
482
525
|
}
|
|
483
|
-
async function pollQRStatus(baseUrl, qrcode) {
|
|
526
|
+
async function pollQRStatus(baseUrl, qrcode, signal) {
|
|
484
527
|
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
485
528
|
const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
486
529
|
const controller = new AbortController();
|
|
487
530
|
const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
|
|
531
|
+
const onExternalAbort = () => controller.abort();
|
|
532
|
+
signal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
488
533
|
try {
|
|
489
534
|
const res = await fetch(url, {
|
|
490
535
|
headers: { 'iLink-App-ClientVersion': '1' },
|
|
@@ -498,10 +543,15 @@ async function pollQRStatus(baseUrl, qrcode) {
|
|
|
498
543
|
catch (err) {
|
|
499
544
|
clearTimeout(timer);
|
|
500
545
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
546
|
+
if (signal?.aborted)
|
|
547
|
+
throw new InitCancelledError();
|
|
501
548
|
return { status: 'wait' };
|
|
502
549
|
}
|
|
503
550
|
throw err;
|
|
504
551
|
}
|
|
552
|
+
finally {
|
|
553
|
+
signal?.removeEventListener('abort', onExternalAbort);
|
|
554
|
+
}
|
|
505
555
|
}
|
|
506
556
|
export async function runWechatQrFlow() {
|
|
507
557
|
const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
|
|
@@ -518,44 +568,62 @@ export async function runWechatQrFlow() {
|
|
|
518
568
|
console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
|
|
519
569
|
}
|
|
520
570
|
console.log('请用微信扫描上方二维码...\n');
|
|
571
|
+
console.log('连续按两次 Esc 或 Ctrl+C 退出\n');
|
|
521
572
|
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
522
573
|
let scannedPrinted = false;
|
|
523
574
|
let currentPollUrl = DEFAULT_BASE_URL;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
575
|
+
let initCancelled = false;
|
|
576
|
+
const cancelController = new AbortController();
|
|
577
|
+
const cancel = () => {
|
|
578
|
+
initCancelled = true;
|
|
579
|
+
cancelController.abort();
|
|
580
|
+
};
|
|
581
|
+
const cleanup = setupInitRawKeyListener(key => {
|
|
582
|
+
if (key === '\u0003')
|
|
583
|
+
cancel();
|
|
584
|
+
}, cancel);
|
|
585
|
+
try {
|
|
586
|
+
while (Date.now() < deadline) {
|
|
587
|
+
if (initCancelled)
|
|
588
|
+
throw new InitCancelledError();
|
|
589
|
+
const status = await pollQRStatus(currentPollUrl, qrResp.qrcode, cancelController.signal);
|
|
590
|
+
switch (status.status) {
|
|
591
|
+
case 'wait':
|
|
592
|
+
process.stdout.write('.');
|
|
593
|
+
break;
|
|
594
|
+
case 'scaned':
|
|
595
|
+
if (!scannedPrinted) {
|
|
596
|
+
console.log('\n👀 已扫码,请在微信中确认...');
|
|
597
|
+
scannedPrinted = true;
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
case 'scaned_but_redirect':
|
|
601
|
+
if (status.redirect_host) {
|
|
602
|
+
currentPollUrl = `https://${status.redirect_host}`;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
case 'expired':
|
|
606
|
+
console.error('\n二维码已过期');
|
|
547
607
|
return null;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
608
|
+
case 'confirmed':
|
|
609
|
+
if (!status.ilink_bot_id || !status.bot_token) {
|
|
610
|
+
console.error('\n登录失败:服务器未返回完整信息');
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
baseUrl: status.baseurl || DEFAULT_BASE_URL,
|
|
615
|
+
token: status.bot_token,
|
|
616
|
+
userId: status.ilink_user_id,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
await waitForInitDelay(1000, cancelController.signal);
|
|
554
620
|
}
|
|
555
|
-
|
|
621
|
+
console.log('\n登录超时');
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
cleanup();
|
|
556
626
|
}
|
|
557
|
-
console.log('\n登录超时');
|
|
558
|
-
return null;
|
|
559
627
|
}
|
|
560
628
|
export async function cmdInitWechat() {
|
|
561
629
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -632,26 +700,35 @@ export async function cmdInitAun() {
|
|
|
632
700
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
633
701
|
try {
|
|
634
702
|
const target = await pickAunOwnerBindTarget(rl, daemonConfig.aid);
|
|
635
|
-
|
|
636
|
-
|
|
703
|
+
const runQrFlow = async (flow) => {
|
|
704
|
+
rl.pause();
|
|
705
|
+
try {
|
|
706
|
+
return await flow();
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
rl.resume();
|
|
710
|
+
}
|
|
711
|
+
};
|
|
637
712
|
if (target.kind === 'daemon') {
|
|
638
|
-
console.log(`\n📡 EvolCore daemon owner
|
|
639
|
-
const result = await runDaemonOwnerQrBindFlow(
|
|
640
|
-
if (result
|
|
713
|
+
console.log(`\n📡 EvolCore daemon owner 追加绑定 — ${daemonConfig.aid}\n`);
|
|
714
|
+
const result = await runQrFlow(() => runDaemonOwnerQrBindFlow());
|
|
715
|
+
if (result.action === 'bound') {
|
|
641
716
|
console.log(`\n✅ daemon owner 已绑定: ${result.boundAid}`);
|
|
642
717
|
return;
|
|
643
718
|
}
|
|
644
|
-
|
|
719
|
+
if (result.action === 'manual')
|
|
720
|
+
await promptDaemonOwnerManually(rl);
|
|
645
721
|
return;
|
|
646
722
|
}
|
|
647
723
|
const agentConfig = loadAgent(target.aid);
|
|
648
|
-
console.log(`\n📡 Agent owner
|
|
649
|
-
const result = await runAgentOwnerQrBindFlow(target.aid, agentConfig?.aid ?? target.aid
|
|
650
|
-
if (result
|
|
724
|
+
console.log(`\n📡 Agent owner 追加绑定 — ${target.aid}\n`);
|
|
725
|
+
const result = await runQrFlow(() => runAgentOwnerQrBindFlow(target.aid, agentConfig?.aid ?? target.aid));
|
|
726
|
+
if (result.action === 'bound') {
|
|
651
727
|
console.log(`\n✅ agent owner 已绑定: ${result.boundAid}`);
|
|
652
728
|
return;
|
|
653
729
|
}
|
|
654
|
-
|
|
730
|
+
if (result.action === 'manual')
|
|
731
|
+
await promptAgentOwnerForAunManually(rl, target.aid);
|
|
655
732
|
}
|
|
656
733
|
finally {
|
|
657
734
|
rl.close();
|
|
@@ -659,24 +736,21 @@ export async function cmdInitAun() {
|
|
|
659
736
|
}
|
|
660
737
|
async function pickAunOwnerBindTarget(rl, daemonAid) {
|
|
661
738
|
const { agents } = loadAllAgents();
|
|
662
|
-
console.log('\n请选择 AUN owner
|
|
739
|
+
console.log('\n请选择 AUN owner 追加绑定目标:');
|
|
663
740
|
console.log(` d. daemon owner (${daemonAid})`);
|
|
664
741
|
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
|
665
742
|
for (let i = 0; i < agents.length; i++) {
|
|
666
743
|
console.log(` ${letters[i]}. agent owner (${agents[i].aid})`);
|
|
667
744
|
}
|
|
668
|
-
console.log(' q. 取消');
|
|
669
745
|
const validAgentLetters = letters.slice(0, agents.length).split('');
|
|
670
746
|
while (true) {
|
|
671
747
|
const input = (await ask(rl, '请选择 [d]: ')).trim().toLowerCase() || 'd';
|
|
672
|
-
if (input === 'q')
|
|
673
|
-
return null;
|
|
674
748
|
if (input === 'd')
|
|
675
749
|
return { kind: 'daemon' };
|
|
676
750
|
if (validAgentLetters.includes(input)) {
|
|
677
751
|
return { kind: 'agent', aid: agents[letters.indexOf(input)].aid };
|
|
678
752
|
}
|
|
679
|
-
console.log(`无效选择,请输入 d${validAgentLetters.length ? `/${validAgentLetters.join('/')}` : ''}
|
|
753
|
+
console.log(`无效选择,请输入 d${validAgentLetters.length ? `/${validAgentLetters.join('/')}` : ''}`);
|
|
680
754
|
}
|
|
681
755
|
}
|
|
682
756
|
async function promptDaemonOwnerManually(rl) {
|
|
@@ -717,19 +791,27 @@ async function promptAgentOwnerForAunManually(rl, aid) {
|
|
|
717
791
|
// ==================== DingTalk ====================
|
|
718
792
|
const DINGTALK_BASE_URL = 'https://oapi.dingtalk.com';
|
|
719
793
|
const DINGTALK_SOURCE = 'openClaw';
|
|
720
|
-
async function dingtalkApiPost(path, payload) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
794
|
+
async function dingtalkApiPost(path, payload, signal) {
|
|
795
|
+
try {
|
|
796
|
+
const res = await fetch(`${DINGTALK_BASE_URL}${path}`, {
|
|
797
|
+
method: 'POST',
|
|
798
|
+
headers: { 'Content-Type': 'application/json' },
|
|
799
|
+
body: JSON.stringify(payload),
|
|
800
|
+
signal,
|
|
801
|
+
});
|
|
802
|
+
if (!res.ok)
|
|
803
|
+
throw new Error(`DingTalk API ${path} failed: ${res.status}`);
|
|
804
|
+
const data = await res.json();
|
|
805
|
+
if (data.errcode && data.errcode !== 0) {
|
|
806
|
+
throw new Error(`DingTalk API ${path}: ${data.errmsg || 'unknown error'} (errcode=${data.errcode})`);
|
|
807
|
+
}
|
|
808
|
+
return data;
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
if (signal?.aborted)
|
|
812
|
+
throw new InitCancelledError();
|
|
813
|
+
throw error;
|
|
731
814
|
}
|
|
732
|
-
return data;
|
|
733
815
|
}
|
|
734
816
|
async function runDingtalkQrFlow() {
|
|
735
817
|
// Step 1: init → nonce
|
|
@@ -760,25 +842,37 @@ async function runDingtalkQrFlow() {
|
|
|
760
842
|
}
|
|
761
843
|
console.log('请用钉钉扫描上方二维码...\n');
|
|
762
844
|
console.log('提示: 扫码页面标注 "OpenClaw" 是钉钉生态接入桥,可放心使用。\n');
|
|
763
|
-
console.log('按 q 退出 | 按 s
|
|
845
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入 | 连续按两次 Esc 退出\n');
|
|
764
846
|
let userAction = null;
|
|
765
|
-
|
|
766
|
-
|
|
847
|
+
let initCancelled = false;
|
|
848
|
+
const cancelController = new AbortController();
|
|
849
|
+
const cancel = () => {
|
|
850
|
+
initCancelled = true;
|
|
851
|
+
cancelController.abort();
|
|
852
|
+
};
|
|
853
|
+
const cleanup = setupInitRawKeyListener((key) => {
|
|
854
|
+
if (key === 'q')
|
|
767
855
|
userAction = QUIT;
|
|
768
856
|
if (key === 's')
|
|
769
857
|
userAction = SKIP;
|
|
770
|
-
|
|
858
|
+
if (key === '\u0003')
|
|
859
|
+
cancel();
|
|
860
|
+
}, cancel);
|
|
771
861
|
const pollInterval = Math.max(Number(beginData.interval ?? 3), 2);
|
|
772
862
|
const expiresIn = Number(beginData.expires_in ?? 7200);
|
|
773
863
|
const startedAt = Date.now();
|
|
774
864
|
try {
|
|
775
865
|
while (Date.now() - startedAt < expiresIn * 1000) {
|
|
866
|
+
if (initCancelled)
|
|
867
|
+
throw new InitCancelledError();
|
|
776
868
|
if (userAction === QUIT)
|
|
777
869
|
return QUIT;
|
|
778
870
|
if (userAction === SKIP)
|
|
779
871
|
return SKIP;
|
|
780
|
-
await
|
|
781
|
-
const pollData = await dingtalkApiPost('/app/registration/poll', { device_code: deviceCode });
|
|
872
|
+
await waitForInitDelay(pollInterval * 1000, cancelController.signal);
|
|
873
|
+
const pollData = await dingtalkApiPost('/app/registration/poll', { device_code: deviceCode }, cancelController.signal);
|
|
874
|
+
if (initCancelled)
|
|
875
|
+
throw new InitCancelledError();
|
|
782
876
|
const status = (pollData.status || '').trim().toUpperCase();
|
|
783
877
|
if (status === 'SUCCESS') {
|
|
784
878
|
if (!pollData.client_id || !pollData.client_secret) {
|
|
@@ -814,6 +908,8 @@ export async function runDingtalkQrFlowSimple() {
|
|
|
814
908
|
return result;
|
|
815
909
|
}
|
|
816
910
|
catch (error) {
|
|
911
|
+
if (isInitCancelledError(error))
|
|
912
|
+
throw error;
|
|
817
913
|
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
818
914
|
return null;
|
|
819
915
|
}
|
|
@@ -889,6 +985,8 @@ export async function cmdInitDingtalk() {
|
|
|
889
985
|
}
|
|
890
986
|
}
|
|
891
987
|
catch (error) {
|
|
988
|
+
if (isInitCancelledError(error))
|
|
989
|
+
throw error;
|
|
892
990
|
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
893
991
|
process.exit(1);
|
|
894
992
|
}
|
|
@@ -972,31 +1070,54 @@ async function runQQBotBindFlow() {
|
|
|
972
1070
|
console.log(`请在浏览器中打开此链接扫码: ${qrUrl}\n`);
|
|
973
1071
|
}
|
|
974
1072
|
console.log('请用 QQ 扫描上方二维码绑定机器人...\n');
|
|
975
|
-
console.log('按 q 退出 | 按 s
|
|
1073
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入 | 连续按两次 Esc 退出\n');
|
|
976
1074
|
let userAction = null;
|
|
977
|
-
|
|
978
|
-
|
|
1075
|
+
let initCancelled = false;
|
|
1076
|
+
const cancelController = new AbortController();
|
|
1077
|
+
const cancel = () => {
|
|
1078
|
+
initCancelled = true;
|
|
1079
|
+
cancelController.abort();
|
|
1080
|
+
};
|
|
1081
|
+
const cleanup = setupInitRawKeyListener((key) => {
|
|
1082
|
+
if (key === 'q')
|
|
979
1083
|
userAction = QUIT;
|
|
980
1084
|
if (key === 's')
|
|
981
1085
|
userAction = SKIP;
|
|
982
|
-
|
|
1086
|
+
if (key === '\u0003')
|
|
1087
|
+
cancel();
|
|
1088
|
+
}, cancel);
|
|
983
1089
|
const startedAt = Date.now();
|
|
984
1090
|
try {
|
|
985
1091
|
// Step 3: Poll for bind result
|
|
986
1092
|
while (Date.now() - startedAt < QQBOT_POLL_TIMEOUT_MS) {
|
|
1093
|
+
if (initCancelled)
|
|
1094
|
+
throw new InitCancelledError();
|
|
987
1095
|
if (userAction === QUIT)
|
|
988
1096
|
return QUIT;
|
|
989
1097
|
if (userAction === SKIP)
|
|
990
1098
|
return SKIP;
|
|
991
|
-
await
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1099
|
+
await waitForInitDelay(QQBOT_POLL_INTERVAL_MS, cancelController.signal);
|
|
1100
|
+
let pollRes;
|
|
1101
|
+
try {
|
|
1102
|
+
pollRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_POLL_PATH}`, {
|
|
1103
|
+
method: 'POST',
|
|
1104
|
+
headers: qqbotApiHeaders(),
|
|
1105
|
+
body: JSON.stringify({ task_id: taskId }),
|
|
1106
|
+
signal: cancelController.signal,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
if (cancelController.signal.aborted)
|
|
1111
|
+
throw new InitCancelledError();
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
if (initCancelled)
|
|
1115
|
+
throw new InitCancelledError();
|
|
997
1116
|
if (!pollRes.ok)
|
|
998
1117
|
continue; // transient error, keep polling
|
|
999
1118
|
const pollData = await pollRes.json();
|
|
1119
|
+
if (initCancelled)
|
|
1120
|
+
throw new InitCancelledError();
|
|
1000
1121
|
if (pollData.retcode !== 0)
|
|
1001
1122
|
continue;
|
|
1002
1123
|
const status = pollData.data?.status ?? 0 /* QQBotBindStatus.NONE */;
|
|
@@ -1033,6 +1154,8 @@ export async function runQQBotBindFlowSimple() {
|
|
|
1033
1154
|
return result;
|
|
1034
1155
|
}
|
|
1035
1156
|
catch (error) {
|
|
1157
|
+
if (isInitCancelledError(error))
|
|
1158
|
+
throw error;
|
|
1036
1159
|
console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
|
|
1037
1160
|
return null;
|
|
1038
1161
|
}
|
|
@@ -1108,6 +1231,8 @@ export async function cmdInitQQBot() {
|
|
|
1108
1231
|
}
|
|
1109
1232
|
}
|
|
1110
1233
|
catch (error) {
|
|
1234
|
+
if (isInitCancelledError(error))
|
|
1235
|
+
throw error;
|
|
1111
1236
|
console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
|
|
1112
1237
|
process.exit(1);
|
|
1113
1238
|
}
|
|
@@ -1181,18 +1306,41 @@ export async function cmdInitWecom() {
|
|
|
1181
1306
|
});
|
|
1182
1307
|
});
|
|
1183
1308
|
console.log(`也可在浏览器打开:${session.browserUrl}`);
|
|
1184
|
-
console.log('等待扫码确认,二维码 5
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1309
|
+
console.log('等待扫码确认,二维码 5 分钟内有效;连续按两次 Esc 或 Ctrl+C 退出...\n');
|
|
1310
|
+
let initCancelled = false;
|
|
1311
|
+
const cancelController = new AbortController();
|
|
1312
|
+
const cancel = () => {
|
|
1313
|
+
initCancelled = true;
|
|
1314
|
+
cancelController.abort();
|
|
1315
|
+
};
|
|
1316
|
+
const cleanup = setupInitRawKeyListener(key => {
|
|
1317
|
+
if (key === '\u0003')
|
|
1318
|
+
cancel();
|
|
1319
|
+
}, cancel);
|
|
1320
|
+
try {
|
|
1321
|
+
const credentials = await pollWecomQrCredentials(session.scode, {
|
|
1322
|
+
signal: cancelController.signal,
|
|
1323
|
+
onStatus: status => {
|
|
1324
|
+
if (status !== 'waiting')
|
|
1325
|
+
console.log(` 扫码状态: ${status}`);
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
botId = credentials.botId;
|
|
1329
|
+
secret = credentials.secret;
|
|
1330
|
+
console.log(' ✓ 已取得 Bot ID 和 Secret');
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
if (initCancelled)
|
|
1334
|
+
throw new InitCancelledError();
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
finally {
|
|
1338
|
+
cleanup();
|
|
1339
|
+
}
|
|
1194
1340
|
}
|
|
1195
1341
|
catch (error) {
|
|
1342
|
+
if (isInitCancelledError(error))
|
|
1343
|
+
throw error;
|
|
1196
1344
|
console.log(` ⚠ 扫码接入失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
1197
1345
|
console.log(' 将切换到手工输入模式。\n');
|
|
1198
1346
|
}
|
|
@@ -1468,7 +1616,7 @@ export function getChannelCredentialCollector(type) {
|
|
|
1468
1616
|
case 'wecom':
|
|
1469
1617
|
return async () => {
|
|
1470
1618
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1471
|
-
const ask = (q) =>
|
|
1619
|
+
const ask = (q) => askInit(rl, q);
|
|
1472
1620
|
try {
|
|
1473
1621
|
console.log('企业微信 AI Bot 配置\n');
|
|
1474
1622
|
console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
|