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.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/gateway-config.js +26 -10
  20. package/dist/core/agent-reload-coordinator.js +53 -0
  21. package/dist/core/auth/operation-authorizer.js +32 -147
  22. package/dist/core/auth/operation-catalog.js +80 -0
  23. package/dist/core/bootstrap-messages.js +50 -0
  24. package/dist/core/bootstrap-service.js +85 -10
  25. package/dist/core/channel-loader.js +23 -6
  26. package/dist/core/command/agent-control.js +14 -11
  27. package/dist/core/command/menu-handler.js +67 -76
  28. package/dist/core/command/slash-handler.js +4 -4
  29. package/dist/core/evolagent-registry.js +125 -35
  30. package/dist/core/evolagent.js +8 -3
  31. package/dist/core/inference/text-inference.js +38 -4
  32. package/dist/core/message/message-bridge.js +1 -1
  33. package/dist/core/message/message-log.js +22 -0
  34. package/dist/core/message/message-queue.js +19 -4
  35. package/dist/core/model/model-catalog.js +143 -24
  36. package/dist/core/model/model-diagnostics.js +28 -10
  37. package/dist/core/permission/index.js +1 -0
  38. package/dist/core/permission/readonly-shell-query.js +532 -0
  39. package/dist/core/permission/shell-environment.js +46 -0
  40. package/dist/core/permission/tool-policy.js +231 -93
  41. package/dist/core/protected-paths.js +10 -7
  42. package/dist/core/runner-reload-transaction.js +57 -0
  43. package/dist/index.js +262 -84
  44. package/dist/ipc.js +29 -11
  45. package/dist/utils/aid-bind.js +3 -8
  46. package/dist/utils/log-writer.js +6 -10
  47. package/dist/utils/logger.js +5 -5
  48. package/kits/docs/evolcore/msg.md +13 -0
  49. package/kits/rules/01-overview.md +9 -0
  50. package/kits/schemas/agent-config.schema.3.json +1 -1
  51. package/kits/schemas/agent-config.schema.4.json +1 -1
  52. package/kits/schemas/relation-config.schema.2.json +1 -1
  53. package/kits/schemas/role-config.schema.1.json +1 -1
  54. package/kits/templates/roles/admin.json +5 -0
  55. package/kits/templates/roles/member.json +17 -0
  56. package/kits/templates/roles/visitor.json +8 -0
  57. package/kits/templates/system-fragments/bootstrap.md +12 -6
  58. package/kits/templates/system-fragments/channel.md +6 -0
  59. package/kits/templates/system-fragments/session.md +2 -0
  60. package/package.json +2 -1
  61. package/skills/eclink/SKILL.md +15 -3
  62. package/skills/eclink/agents/openai.yaml +3 -3
@@ -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 { setupRawKeyListener } from './raw-key-input.js';
21
+ import { askInit, InitCancelledError, isInitCancelledError, setupInitRawKeyListener, waitForInitDelay, } from './init-cancel.js';
22
22
  function ask(rl, question) {
23
- return new Promise(resolve => rl.question(question, resolve));
23
+ return askInit(rl, question);
24
24
  }
25
- export async function runDaemonOwnerQrBindFlow(ownerMode = 'append') {
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 null;
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 null;
42
+ return { action: 'manual' };
43
43
  }
44
44
  if (!status.controlAid?.connected) {
45
45
  console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
46
46
  }
47
- const begin = await ipcQuery(p.socket, {
48
- type: 'bind.begin',
49
- bindType: 'daemon',
50
- ownerMode,
51
- }, 3000);
52
- if (!begin?.ok) {
53
- console.log(`❌ 创建绑定任务失败: ${begin?.error || 'unknown error'}`);
54
- return null;
55
- }
56
- await printQr(begin.qrData);
57
- console.log('\n请用 evol app 扫描上方二维码完成绑定');
58
- console.log('按 q 取消 | 10 分钟内有效\n');
59
- const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
60
- if (result === 'quit') {
61
- await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
62
- console.log('已取消');
63
- return null;
64
- }
65
- if (result === 'skip') {
66
- await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
67
- console.log('已跳过扫码');
68
- return null;
69
- }
70
- if (!result) {
71
- console.log('绑定超时,请重新生成二维码');
72
- return null;
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, ownerMode = 'append') {
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 null;
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 null;
97
+ return { action: 'manual' };
97
98
  }
98
99
  if (!status.controlAid?.connected) {
99
100
  console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
100
101
  }
101
- const begin = await ipcQuery(p.socket, {
102
- type: 'bind.begin',
103
- bindType: 'agent',
104
- targetAid,
105
- agentName,
106
- ownerMode,
107
- }, 3000);
108
- if (!begin?.ok) {
109
- console.log(`❌ 创建 agent 绑定任务失败: ${begin?.error || 'unknown error'}`);
110
- return null;
111
- }
112
- await printQr(begin.qrData);
113
- console.log('\n请用 evol app 扫描上方二维码,10 分钟内有效');
114
- console.log('按 q 退出 | 按 s 跳过扫码手动输入 agent owner AID\n');
115
- const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
116
- if (result === 'quit') {
117
- await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
118
- console.log('已取消');
119
- return null;
120
- }
121
- if (result === 'skip') {
122
- await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
123
- console.log('已跳过扫码');
124
- return null;
125
- }
126
- if (!result) {
127
- console.log('绑定超时');
128
- return null;
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 quit = false;
201
- let skip = false;
202
+ let keyAction = null;
203
+ let expired = false;
202
204
  const p = resolvePaths();
203
- const cleanup = setupRawKeyListener((key) => {
204
- if (key === 'q' || key === '\u0003')
205
- quit = true;
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 (Date.now() < expiresAt) {
211
- if (quit)
212
- return 'quit';
213
- if (skip)
214
- return 'skip';
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 };
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 (status?.ok && (status.status === 'expired' || status.status === 'cancelled')) {
220
- return null;
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
- const res = await fetch(`${this.baseUrl}/oauth/v1/app/registration`, {
259
- method: 'POST',
260
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
261
- body,
262
- });
263
- const text = await res.text();
264
- if (!text)
265
- return {};
266
- return JSON.parse(text);
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\n');
321
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret | 连续按两次 Esc 退出\n');
295
322
  let userAction = null;
296
- const cleanup = setupRawKeyListener((key) => {
297
- if (key === 'q' || key === '\u0003')
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 new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
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 new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
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 new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
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
- while (Date.now() < deadline) {
525
- const status = await pollQRStatus(currentPollUrl, qrResp.qrcode);
526
- switch (status.status) {
527
- case 'wait':
528
- process.stdout.write('.');
529
- break;
530
- case 'scaned':
531
- if (!scannedPrinted) {
532
- console.log('\n👀 已扫码,请在微信中确认...');
533
- scannedPrinted = true;
534
- }
535
- break;
536
- case 'scaned_but_redirect':
537
- if (status.redirect_host) {
538
- currentPollUrl = `https://${status.redirect_host}`;
539
- }
540
- break;
541
- case 'expired':
542
- console.error('\n二维码已过期');
543
- return null;
544
- case 'confirmed':
545
- if (!status.ilink_bot_id || !status.bot_token) {
546
- console.error('\n登录失败:服务器未返回完整信息');
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
- return {
550
- baseUrl: status.baseurl || DEFAULT_BASE_URL,
551
- token: status.bot_token,
552
- userId: status.ilink_user_id,
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
- await new Promise(r => setTimeout(r, 1000));
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
- if (!target)
636
- return;
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 绑定 — ${daemonConfig.aid}\n`);
639
- const result = await runDaemonOwnerQrBindFlow('append');
640
- if (result?.boundAid) {
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
- await promptDaemonOwnerManually(rl);
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 绑定 — ${target.aid}\n`);
649
- const result = await runAgentOwnerQrBindFlow(target.aid, agentConfig?.aid ?? target.aid, 'append');
650
- if (result?.boundAid) {
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
- await promptAgentOwnerForAunManually(rl, target.aid);
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('/')}` : ''}/q`);
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
- const res = await fetch(`${DINGTALK_BASE_URL}${path}`, {
722
- method: 'POST',
723
- headers: { 'Content-Type': 'application/json' },
724
- body: JSON.stringify(payload),
725
- });
726
- if (!res.ok)
727
- throw new Error(`DingTalk API ${path} failed: ${res.status}`);
728
- const data = await res.json();
729
- if (data.errcode && data.errcode !== 0) {
730
- throw new Error(`DingTalk API ${path}: ${data.errmsg || 'unknown error'} (errcode=${data.errcode})`);
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 跳过扫码手动输入\n');
845
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入 | 连续按两次 Esc 退出\n');
764
846
  let userAction = null;
765
- const cleanup = setupRawKeyListener((key) => {
766
- if (key === 'q' || key === '\u0003')
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 new Promise(r => setTimeout(r, pollInterval * 1000));
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 跳过扫码手动输入\n');
1073
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入 | 连续按两次 Esc 退出\n');
976
1074
  let userAction = null;
977
- const cleanup = setupRawKeyListener((key) => {
978
- if (key === 'q' || key === '\u0003')
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 new Promise(r => setTimeout(r, QQBOT_POLL_INTERVAL_MS));
992
- const pollRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_POLL_PATH}`, {
993
- method: 'POST',
994
- headers: qqbotApiHeaders(),
995
- body: JSON.stringify({ task_id: taskId }),
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 分钟内有效...\n');
1185
- const credentials = await pollWecomQrCredentials(session.scode, {
1186
- onStatus: status => {
1187
- if (status !== 'waiting')
1188
- console.log(` 扫码状态: ${status}`);
1189
- },
1190
- });
1191
- botId = credentials.botId;
1192
- secret = credentials.secret;
1193
- console.log(' ✓ 已取得 Bot ID 和 Secret');
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) => new Promise(r => rl.question(q, r));
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');