evolcore 0.0.22 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +24 -14
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/install-codex-managed-hooks.mjs +373 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +30 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +407 -139
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +31 -1
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +142 -45
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/authorization-audit.js +4 -0
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/command/command-handler.js +41 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +141 -33
- package/dist/core/command/menu-protocol.js +119 -0
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +42 -22
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/response-engine.js +74 -73
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +244 -25
- package/dist/core/runtime-lock.js +13 -8
- package/dist/index.js +40 -29
- package/dist/ipc.js +123 -81
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/npm-ops.js +47 -15
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +11 -3
package/dist/channels/wechat.js
CHANGED
|
@@ -40,6 +40,8 @@ const BACKOFF_DELAY_MS = 30_000;
|
|
|
40
40
|
const RETRY_DELAY_MS = 2_000;
|
|
41
41
|
const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
|
|
42
42
|
const SESSION_EXPIRED_ERRCODE = -14;
|
|
43
|
+
const INVALID_SYNC_CURSOR_ERRCODE = -1;
|
|
44
|
+
const MAX_SYNC_CURSOR_BYTES = 64 * 1024;
|
|
43
45
|
export class WechatApiError extends Error {
|
|
44
46
|
endpoint;
|
|
45
47
|
ret;
|
|
@@ -71,6 +73,26 @@ export class WechatContextTokenRefreshRequiredError extends Error {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* iLink cursors are opaque text, so validation deliberately stays conservative:
|
|
78
|
+
* reject values that cannot be safely round-tripped through the UTF-8 state file
|
|
79
|
+
* without imposing a specific encoding on future protocol versions.
|
|
80
|
+
*/
|
|
81
|
+
export function validateWechatSyncCursor(cursor) {
|
|
82
|
+
if (typeof cursor !== 'string') {
|
|
83
|
+
return { valid: false, reason: 'invalid_type' };
|
|
84
|
+
}
|
|
85
|
+
if (Buffer.byteLength(cursor, 'utf8') > MAX_SYNC_CURSOR_BYTES) {
|
|
86
|
+
return { valid: false, reason: 'too_large' };
|
|
87
|
+
}
|
|
88
|
+
if (cursor.includes('\uFFFD')) {
|
|
89
|
+
return { valid: false, reason: 'invalid_utf8' };
|
|
90
|
+
}
|
|
91
|
+
if (/[\s\u0000-\u001F\u007F-\u009F]/u.test(cursor)) {
|
|
92
|
+
return { valid: false, reason: 'invalid_character' };
|
|
93
|
+
}
|
|
94
|
+
return { valid: true };
|
|
95
|
+
}
|
|
74
96
|
/** Classify fetch/undici failures using the categories exposed by Tencent 2.4.6. */
|
|
75
97
|
export function classifyFetchError(err) {
|
|
76
98
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
@@ -196,6 +218,7 @@ export class WechatChannel {
|
|
|
196
218
|
pendingContextMessages = new Map();
|
|
197
219
|
typingTicketCache = new Map();
|
|
198
220
|
getUpdatesBuf = '';
|
|
221
|
+
lastRejectedSyncCursor;
|
|
199
222
|
syncBufPath;
|
|
200
223
|
contextTokensPath;
|
|
201
224
|
onSessionExpired;
|
|
@@ -243,14 +266,24 @@ export class WechatChannel {
|
|
|
243
266
|
throw new Error('WeChat reauthentication required; run: ec init wechat');
|
|
244
267
|
}
|
|
245
268
|
// 恢复游标
|
|
269
|
+
this.getUpdatesBuf = '';
|
|
270
|
+
this.lastRejectedSyncCursor = undefined;
|
|
246
271
|
try {
|
|
247
272
|
if (fs.existsSync(this.syncBufPath)) {
|
|
248
|
-
|
|
249
|
-
|
|
273
|
+
const restoredCursor = fs.readFileSync(this.syncBufPath, 'utf-8');
|
|
274
|
+
const validation = validateWechatSyncCursor(restoredCursor);
|
|
275
|
+
if (validation.valid) {
|
|
276
|
+
this.getUpdatesBuf = restoredCursor;
|
|
277
|
+
logger.info(`[WeChat] Restored sync cursor (${Buffer.byteLength(restoredCursor, 'utf8')} bytes)`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.clearSyncCursor(`persisted cursor is invalid (${validation.reason})`, restoredCursor);
|
|
281
|
+
}
|
|
250
282
|
}
|
|
251
283
|
}
|
|
252
|
-
catch {
|
|
253
|
-
|
|
284
|
+
catch (err) {
|
|
285
|
+
logger.warn(`[WeChat] Failed to restore sync cursor; starting from an empty cursor: ${err instanceof Error ? err.message : String(err)}`);
|
|
286
|
+
this.getUpdatesBuf = '';
|
|
254
287
|
}
|
|
255
288
|
// 通知 ilink 后端:bot 上线
|
|
256
289
|
try {
|
|
@@ -431,13 +464,22 @@ export class WechatChannel {
|
|
|
431
464
|
}
|
|
432
465
|
consecutiveFailures = 0;
|
|
433
466
|
// 保存游标
|
|
434
|
-
if (resp.get_updates_buf) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
467
|
+
if (resp.get_updates_buf !== undefined) {
|
|
468
|
+
const validation = validateWechatSyncCursor(resp.get_updates_buf);
|
|
469
|
+
if (!validation.valid) {
|
|
470
|
+
this.clearSyncCursor(`server returned an invalid cursor (${validation.reason})`, typeof resp.get_updates_buf === 'string' ? resp.get_updates_buf : undefined);
|
|
438
471
|
}
|
|
439
|
-
|
|
440
|
-
|
|
472
|
+
else if (resp.get_updates_buf) {
|
|
473
|
+
if (resp.get_updates_buf !== this.lastRejectedSyncCursor) {
|
|
474
|
+
this.lastRejectedSyncCursor = undefined;
|
|
475
|
+
}
|
|
476
|
+
this.getUpdatesBuf = resp.get_updates_buf;
|
|
477
|
+
try {
|
|
478
|
+
fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// best-effort
|
|
482
|
+
}
|
|
441
483
|
}
|
|
442
484
|
}
|
|
443
485
|
// 处理消息
|
|
@@ -450,6 +492,17 @@ export class WechatChannel {
|
|
|
450
492
|
return;
|
|
451
493
|
if (this.isSessionPaused())
|
|
452
494
|
return;
|
|
495
|
+
if (err instanceof WechatApiError &&
|
|
496
|
+
err.endpoint === 'ilink/bot/getupdates' &&
|
|
497
|
+
err.hasCode(INVALID_SYNC_CURSOR_ERRCODE) &&
|
|
498
|
+
this.getUpdatesBuf &&
|
|
499
|
+
this.getUpdatesBuf !== this.lastRejectedSyncCursor) {
|
|
500
|
+
const rejectedCursor = this.getUpdatesBuf;
|
|
501
|
+
this.clearSyncCursor('getUpdates returned ret=-1', rejectedCursor);
|
|
502
|
+
consecutiveFailures = 0;
|
|
503
|
+
logger.warn('[WeChat] Retrying getUpdates immediately with an empty sync cursor');
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
453
506
|
consecutiveFailures++;
|
|
454
507
|
if (err instanceof WechatApiError) {
|
|
455
508
|
logger.error(`[WeChat] getUpdates API error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
|
|
@@ -470,6 +523,24 @@ export class WechatChannel {
|
|
|
470
523
|
}
|
|
471
524
|
}
|
|
472
525
|
}
|
|
526
|
+
clearSyncCursor(reason, rejectedCursor) {
|
|
527
|
+
if (rejectedCursor)
|
|
528
|
+
this.lastRejectedSyncCursor = rejectedCursor;
|
|
529
|
+
this.getUpdatesBuf = '';
|
|
530
|
+
try {
|
|
531
|
+
let persistedAction = 'no persisted cursor';
|
|
532
|
+
if (fs.existsSync(this.syncBufPath)) {
|
|
533
|
+
const quarantinePath = `${this.syncBufPath}.corrupt-${Date.now()}`;
|
|
534
|
+
fs.renameSync(this.syncBufPath, quarantinePath);
|
|
535
|
+
persistedAction = `quarantined as ${path.basename(quarantinePath)}`;
|
|
536
|
+
}
|
|
537
|
+
logger.warn(`[WeChat] Cleared sync cursor: ${reason} (${persistedAction})`);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
logger.warn(`[WeChat] Cleared in-memory sync cursor but failed to quarantine persisted cursor: ` +
|
|
541
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
473
544
|
// ── Inbound Message Handler ──────────────────────────────────────────
|
|
474
545
|
async handleInboundMessage(msg) {
|
|
475
546
|
if (msg.message_type !== MSG_TYPE_USER)
|
package/dist/cli/aun-commands.js
CHANGED
|
@@ -299,6 +299,14 @@ function renderManagedGroupResult(args, result, formatJson) {
|
|
|
299
299
|
console.log(`✓ 成功 ${result.added?.length ?? 0},失败 ${result.failed?.length ?? 0}`);
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
|
+
if (sub === 'invites') {
|
|
303
|
+
console.log(`✓ ${result.invites?.length ?? 0} 个待处理邀请`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (sub === 'accept-invite' || sub === 'reject-invite' || sub === 'revoke-invite') {
|
|
307
|
+
console.log(`✓ group ${sub} 完成`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
302
310
|
console.log(`✓ group ${sub} 完成`);
|
|
303
311
|
}
|
|
304
312
|
export async function cmdAid(args) {
|
|
@@ -1558,7 +1566,11 @@ export async function cmdGroup(args) {
|
|
|
1558
1566
|
成员:
|
|
1559
1567
|
join <from> <group-id> [--message M] [--answer A] 申请加入
|
|
1560
1568
|
leave <from> <group-id> 退出群
|
|
1561
|
-
invite <from> <group-id> <member-aid> [<member-aid>...]
|
|
1569
|
+
invite <from> <group-id> <member-aid> [<member-aid>...] 创建定向邀请
|
|
1570
|
+
invites <from> [--page N] [--size N] 查看收到的邀请
|
|
1571
|
+
accept-invite <from> <invite-id> 接受邀请
|
|
1572
|
+
reject-invite <from> <invite-id> 拒绝邀请
|
|
1573
|
+
revoke-invite <from> <invite-id> 撤回邀请
|
|
1562
1574
|
kick <from> <group-id> <member-aid> 踢出成员
|
|
1563
1575
|
members <from> <group-id> [--page N] [--size N] 列出群成员
|
|
1564
1576
|
online <from> <group-id> 查看在线成员
|
|
@@ -1582,6 +1594,7 @@ Options:
|
|
|
1582
1594
|
--text <说明> 附件说明文字(仅 --file 模式)
|
|
1583
1595
|
--transcript <text> 语音转写(仅 --as voice)
|
|
1584
1596
|
--group-id <aid> 指定自定义群 AID(仅 create)
|
|
1597
|
+
--member-type <type> 定向邀请成员类型:human|ai(仅 invite)
|
|
1585
1598
|
-- end-of-options:其后所有参数按正文处理
|
|
1586
1599
|
(用于发送恰好等于某 flag 的文本,如 send a g -- --encrypt)
|
|
1587
1600
|
|
|
@@ -1617,7 +1630,7 @@ Options:
|
|
|
1617
1630
|
renderManagedGroupResult(args, result, formatJson);
|
|
1618
1631
|
return;
|
|
1619
1632
|
}
|
|
1620
|
-
const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
|
|
1633
|
+
const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupInvites, groupAcceptInvite, groupRejectInvite, groupRevokeInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
|
|
1621
1634
|
const commonOpts = { aunPath, slotId: appSlot };
|
|
1622
1635
|
// 通用 group_id 提取(第三参数)
|
|
1623
1636
|
const requireGroupId = () => {
|
|
@@ -1693,6 +1706,31 @@ Options:
|
|
|
1693
1706
|
}
|
|
1694
1707
|
return parsed;
|
|
1695
1708
|
};
|
|
1709
|
+
if (sub === 'invites') {
|
|
1710
|
+
const page = parseNumberFlag('--page', '--page');
|
|
1711
|
+
const size = parseNumberFlag('--size', '--size');
|
|
1712
|
+
const result = await groupInvites({ from, page, size, ...commonOpts });
|
|
1713
|
+
outputResult(result, () => {
|
|
1714
|
+
const r = result;
|
|
1715
|
+
console.log(`✓ ${r.invites.length} 个待处理邀请:`);
|
|
1716
|
+
for (const invite of r.invites) {
|
|
1717
|
+
console.log(` ${invite.invite_id ?? invite.id ?? '-'} ${invite.group_id ?? invite.group_aid ?? '-'} ${invite.inviter_aid ?? invite.from ?? '-'}`);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (sub === 'accept-invite' || sub === 'reject-invite' || sub === 'revoke-invite') {
|
|
1723
|
+
const inviteId = args[2];
|
|
1724
|
+
if (!inviteId || inviteId.startsWith('--')) {
|
|
1725
|
+
console.error(`用法: ec group ${sub} <from> <invite-id>`);
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
const action = sub === 'accept-invite' ? groupAcceptInvite
|
|
1729
|
+
: sub === 'reject-invite' ? groupRejectInvite : groupRevokeInvite;
|
|
1730
|
+
const result = await action({ from, inviteId, ...commonOpts });
|
|
1731
|
+
outputResult(result, () => console.log(`✓ 已${sub === 'accept-invite' ? '接受' : sub === 'reject-invite' ? '拒绝' : '撤回'}邀请 ${inviteId}`));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1696
1734
|
const requireFlagValue = (flag, label) => {
|
|
1697
1735
|
const value = getArgValue(args, flag);
|
|
1698
1736
|
if (value === undefined || value.startsWith('--')) {
|
|
@@ -2147,7 +2185,12 @@ Options:
|
|
|
2147
2185
|
process.exit(1);
|
|
2148
2186
|
}
|
|
2149
2187
|
}
|
|
2150
|
-
const
|
|
2188
|
+
const memberType = getArgValue(args, '--member-type');
|
|
2189
|
+
if (memberType !== undefined && memberType !== 'human' && memberType !== 'ai') {
|
|
2190
|
+
console.error('❌ --member-type 必须是 human 或 ai');
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
const result = await groupInvite({ from, groupId, members, memberType: memberType, ...commonOpts });
|
|
2151
2194
|
outputResult(result, () => {
|
|
2152
2195
|
const r = result;
|
|
2153
2196
|
console.log(`✓ 成功 ${r.added.length},失败 ${r.failed.length}`);
|
|
@@ -2300,7 +2343,7 @@ Options:
|
|
|
2300
2343
|
});
|
|
2301
2344
|
return;
|
|
2302
2345
|
}
|
|
2303
|
-
console.error(`未知子命令: ${sub}\n用法: ec group [send|pull|ack|create|list|info|update|dissolve|suspend|resume|rules|join|leave|invite|kick|members|online|role|owner|ban|unban]`);
|
|
2346
|
+
console.error(`未知子命令: ${sub}\n用法: ec group [send|pull|ack|create|list|info|update|dissolve|suspend|resume|rules|join|leave|invite|invites|accept-invite|reject-invite|revoke-invite|kick|members|online|role|owner|ban|unban]`);
|
|
2304
2347
|
process.exit(1);
|
|
2305
2348
|
}
|
|
2306
2349
|
// ==================== Main ====================
|
|
@@ -2322,6 +2365,7 @@ const VALUE_FLAGS = new Set([
|
|
|
2322
2365
|
'--payload', '--title', '--description', '--text', '--transcript',
|
|
2323
2366
|
'--as', '--content-type', '--mention', '--visibility', '--join-mode',
|
|
2324
2367
|
'--group-id', '--name', '--message', '--answer', '--page', '--size',
|
|
2368
|
+
'--member-type',
|
|
2325
2369
|
'--aun-path', '--thread', '--return',
|
|
2326
2370
|
]);
|
|
2327
2371
|
const BOOLEAN_FLAGS = new Set([
|
package/dist/cli/command-log.js
CHANGED
|
@@ -59,8 +59,8 @@ export function logCliCommand(cmd, args) {
|
|
|
59
59
|
const line = JSON.stringify({
|
|
60
60
|
ts: localTimestamp(),
|
|
61
61
|
cmd,
|
|
62
|
-
//
|
|
63
|
-
command: `ec ${args.map(shellQuote).join(' ')}`,
|
|
62
|
+
// 可审计命令行(敏感参数掩码;含 ec 前缀,含空格参数自动加引号)
|
|
63
|
+
command: `ec ${loggableArgs(args).map(shellQuote).join(' ')}`,
|
|
64
64
|
selfAid: selfAid || undefined,
|
|
65
65
|
sessionId: process.env.EVOLCORE_SESSION_ID || undefined,
|
|
66
66
|
}) + '\n';
|
|
@@ -82,6 +82,25 @@ function shellQuote(arg) {
|
|
|
82
82
|
return arg;
|
|
83
83
|
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
84
84
|
}
|
|
85
|
+
function loggableArgs(args) {
|
|
86
|
+
const output = [];
|
|
87
|
+
let redactNext = false;
|
|
88
|
+
for (const arg of args) {
|
|
89
|
+
if (redactNext) {
|
|
90
|
+
output.push('[REDACTED]');
|
|
91
|
+
redactNext = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const flag = arg.replace(/^--?/, '').replace(/=.*$/, '').toLowerCase();
|
|
95
|
+
if (/^(?:remote-token|ecweb-token|menu-token|token|password|secret)$/.test(flag)) {
|
|
96
|
+
output.push(arg.includes('=') ? `${arg.slice(0, arg.indexOf('=') + 1)}[REDACTED]` : arg);
|
|
97
|
+
redactNext = !arg.includes('=');
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
output.push(arg.replace(/((?:"|')?(?:ecweb[_-]?token|menu[_-]?token|access[_-]?token|refresh[_-]?token|password|secret)(?:"|')?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|'[^']*'|[^,}\s]+)/gi, '$1"[REDACTED]"'));
|
|
101
|
+
}
|
|
102
|
+
return output;
|
|
103
|
+
}
|
|
85
104
|
/** 本地日期 tag YYYYMMDD,用于日志文件按天切分(一天一个文件)。 */
|
|
86
105
|
function dayTag() {
|
|
87
106
|
const d = new Date();
|
|
@@ -15,7 +15,7 @@ import { cmdInit, needsControlAidInit, initTail } from './init.js';
|
|
|
15
15
|
import * as platform from '../utils/cross-platform.js';
|
|
16
16
|
import { EventBus } from '../core/event-bus.js';
|
|
17
17
|
import { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
|
|
18
|
-
import { ecwebVersionRequirementError,
|
|
18
|
+
import { ecwebVersionRequirementError, resolveEcwebLaunchCommand, fetchRemotePairingCode } from '../utils/ecweb-utils.js';
|
|
19
19
|
import { isHostChinese } from '../utils/locale.js';
|
|
20
20
|
import { scanInstances, cleanupInstances, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
|
|
21
21
|
import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal, isWatchLogFile } from './watch-logs.js';
|
|
@@ -28,8 +28,9 @@ import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js
|
|
|
28
28
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
29
29
|
import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
30
30
|
import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
|
|
31
|
-
import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../
|
|
31
|
+
import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../agents/codex/app-server-registry.js';
|
|
32
32
|
import { DEFAULT_ECWEB_PORT, ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
|
|
33
|
+
import { checkCodexProjectPath, inspectCodexManagedHook } from '../agents/codex/diagnostics.js';
|
|
33
34
|
const execFileAsync = promisify(execFile);
|
|
34
35
|
async function runAutomaticInit(action) {
|
|
35
36
|
try {
|
|
@@ -177,10 +178,15 @@ function sourceBuildNeedsRefresh() {
|
|
|
177
178
|
function runSourceBuild() {
|
|
178
179
|
return new Promise(resolve => {
|
|
179
180
|
console.log('🔧 源码比 daemon 编译产物更新,自动重新构建...');
|
|
180
|
-
|
|
181
|
+
// Windows cannot spawn a .cmd shim directly with shell:false on recent
|
|
182
|
+
// Node versions. Invoke cmd.exe explicitly instead of using spawn's
|
|
183
|
+
// shell:true mode, which also avoids the DEP0190 warning.
|
|
184
|
+
const command = platform.isWindows ? (process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe') : 'npm';
|
|
185
|
+
const args = platform.isWindows ? ['/d', '/c', 'npm.cmd', 'run', 'build'] : ['run', 'build'];
|
|
186
|
+
const child = spawn(command, args, {
|
|
181
187
|
cwd: getPackageRoot(),
|
|
182
188
|
stdio: 'inherit',
|
|
183
|
-
|
|
189
|
+
windowsHide: platform.isWindows,
|
|
184
190
|
});
|
|
185
191
|
child.on('error', err => {
|
|
186
192
|
console.error(`❌ 无法启动构建进程: ${err.message}`);
|
|
@@ -215,6 +221,64 @@ function requireCompletedDataMigration(root) {
|
|
|
215
221
|
process.exitCode = 2;
|
|
216
222
|
return false;
|
|
217
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Check package updates before starting or restarting the daemon.
|
|
226
|
+
* A failed upgrade is reported, but the currently installed version remains
|
|
227
|
+
* usable and the caller continues with its normal lifecycle.
|
|
228
|
+
*/
|
|
229
|
+
export async function runUpgradeChecks(upgradeFn = signal => tryUpgrade({ signal }), ecwebUpgradeFn = signal => tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME, { signal }), timeoutMs = 60_000) {
|
|
230
|
+
console.log('📦 Checking for updates...');
|
|
231
|
+
const runWithTimeout = async (label, fn) => {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
let timer;
|
|
234
|
+
try {
|
|
235
|
+
return await Promise.race([
|
|
236
|
+
fn(controller.signal),
|
|
237
|
+
new Promise(resolve => {
|
|
238
|
+
timer = setTimeout(() => {
|
|
239
|
+
resolve({ status: 'failed', error: `${label} upgrade timed out` });
|
|
240
|
+
controller.abort();
|
|
241
|
+
}, timeoutMs);
|
|
242
|
+
}),
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
return { status: 'failed', error: error instanceof Error ? error.message : String(error) };
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (timer)
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const upgrade = await runWithTimeout('EvolCore', upgradeFn);
|
|
254
|
+
switch (upgrade.status) {
|
|
255
|
+
case 'upgraded':
|
|
256
|
+
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
257
|
+
break;
|
|
258
|
+
case 'no-update':
|
|
259
|
+
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
260
|
+
break;
|
|
261
|
+
case 'skipped':
|
|
262
|
+
console.log(upgrade.error
|
|
263
|
+
? '⏭ Skipped upgrade (network unavailable)'
|
|
264
|
+
: '⏭ Skipped upgrade check (dev mode)');
|
|
265
|
+
break;
|
|
266
|
+
case 'failed':
|
|
267
|
+
console.log(`⚠ Upgrade failed${upgrade.from && upgrade.to ? ` (${upgrade.from} → ${upgrade.to})` : ''}, continuing with current version${upgrade.error ? `: ${upgrade.error}` : ''}`);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
const ecwebUpgrade = await runWithTimeout(WEB_PACKAGE_NAME, ecwebUpgradeFn);
|
|
271
|
+
switch (ecwebUpgrade.status) {
|
|
272
|
+
case 'upgraded':
|
|
273
|
+
console.log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
|
|
274
|
+
break;
|
|
275
|
+
case 'no-update':
|
|
276
|
+
break;
|
|
277
|
+
case 'failed':
|
|
278
|
+
console.log(`⚠ ${WEB_PACKAGE_NAME} upgrade failed${ecwebUpgrade.from && ecwebUpgrade.to ? ` (${ecwebUpgrade.from} → ${ecwebUpgrade.to})` : ''}${ecwebUpgrade.error ? `: ${ecwebUpgrade.error}` : ''}`);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
218
282
|
// ==================== Commands ====================
|
|
219
283
|
/**
|
|
220
284
|
* 检测并展示跨 HOME 残留的 evolcore 主进程。
|
|
@@ -329,6 +393,9 @@ export async function cmdStart(opts = {}) {
|
|
|
329
393
|
ensureDataDirs();
|
|
330
394
|
if (!await requireFreshSourceBuild())
|
|
331
395
|
return;
|
|
396
|
+
// A stopped container may miss the daily upgrade trigger. Check once on
|
|
397
|
+
// every real start so the service does not stay on an old installation.
|
|
398
|
+
await (opts.upgradeChecks ?? runUpgradeChecks)();
|
|
332
399
|
// 旧配置自动迁移(daemon.json → 新结构)
|
|
333
400
|
const { autoMigrateIfNeeded } = await import('../config-store.js');
|
|
334
401
|
autoMigrateIfNeeded();
|
|
@@ -704,37 +771,9 @@ export async function cmdRestart(opts = {}) {
|
|
|
704
771
|
}
|
|
705
772
|
if (!await requireFreshSourceBuild())
|
|
706
773
|
return;
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
switch (upgrade.status) {
|
|
711
|
-
case 'upgraded':
|
|
712
|
-
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
713
|
-
break;
|
|
714
|
-
case 'no-update':
|
|
715
|
-
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
716
|
-
break;
|
|
717
|
-
case 'skipped':
|
|
718
|
-
console.log(upgrade.error
|
|
719
|
-
? '⏭ Skipped upgrade (network unavailable)'
|
|
720
|
-
: '⏭ Skipped upgrade check (dev mode)');
|
|
721
|
-
break;
|
|
722
|
-
case 'failed':
|
|
723
|
-
console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
// EC Web 是独立全局包,已安装时单独检查升级。
|
|
727
|
-
const ecwebUpgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME);
|
|
728
|
-
switch (ecwebUpgrade.status) {
|
|
729
|
-
case 'upgraded':
|
|
730
|
-
console.log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
|
|
731
|
-
break;
|
|
732
|
-
case 'no-update':
|
|
733
|
-
break; // silent
|
|
734
|
-
case 'failed':
|
|
735
|
-
console.log(`⚠ ${WEB_PACKAGE_NAME} upgrade failed (${ecwebUpgrade.from} → ${ecwebUpgrade.to})`);
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
774
|
+
// Keep restart behavior identical to startup: both paths use one upgrade
|
|
775
|
+
// check implementation and continue when an upgrade is unavailable.
|
|
776
|
+
await runUpgradeChecks();
|
|
738
777
|
if (!requireCompletedDataMigration(resolveRoot()))
|
|
739
778
|
return;
|
|
740
779
|
// 停止所有活 main 进程(可能不止一个)
|
|
@@ -2667,7 +2706,7 @@ async function stopEcwebIfRunning(p) {
|
|
|
2667
2706
|
throw new Error(`EC Web old process or port ${port} could not be released`);
|
|
2668
2707
|
}
|
|
2669
2708
|
// 清理 pid 文件(仅 ecweb-<pid>.json / watch-web-<pid>.json,
|
|
2670
|
-
// 不碰
|
|
2709
|
+
// 不碰 daemon-owned control-access-tokens.json 等非 pid 文件,否则会清空凭据库导致每次重启都要重新配对)
|
|
2671
2710
|
removeEcwebInstanceFiles(p);
|
|
2672
2711
|
return pids.size > 0;
|
|
2673
2712
|
}
|
|
@@ -2759,23 +2798,32 @@ export async function startEcwebIfEnabled(p, options = {}) {
|
|
|
2759
2798
|
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
2760
2799
|
const startedAt = getProcessStartTime(pid) ?? Date.now();
|
|
2761
2800
|
fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt, startedAtIso: new Date(startedAt).toISOString() }, null, 2));
|
|
2762
|
-
// 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠
|
|
2801
|
+
// 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功)。
|
|
2802
|
+
// 配对码由 daemon 提供,daemon 尚未启动时不应把已经就绪的 ECWeb 杀掉。
|
|
2803
|
+
let ready = false;
|
|
2763
2804
|
let pair = null;
|
|
2764
2805
|
for (let i = 0; i < 20; i++) {
|
|
2765
|
-
|
|
2766
|
-
if (
|
|
2806
|
+
ready = await probeEcwebReady(port);
|
|
2807
|
+
if (ready) {
|
|
2808
|
+
pair = await fetchRemotePairingCode();
|
|
2767
2809
|
break;
|
|
2810
|
+
}
|
|
2768
2811
|
await sleep(250);
|
|
2769
2812
|
}
|
|
2770
|
-
if (
|
|
2813
|
+
if (ready) {
|
|
2771
2814
|
const actualStartedAt = getProcessStartTime(pid);
|
|
2772
2815
|
if (actualStartedAt !== null) {
|
|
2773
2816
|
fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt: actualStartedAt, startedAtIso: new Date(actualStartedAt).toISOString() }, null, 2));
|
|
2774
2817
|
}
|
|
2775
|
-
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
2776
2818
|
const actionLabel = wasRunning ? '已重启' : '启动成功';
|
|
2777
2819
|
console.log(`✓ ECWeb ${actionLabel} (PID: ${pid}) http://localhost:${port}`);
|
|
2778
|
-
|
|
2820
|
+
if (pair) {
|
|
2821
|
+
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
2822
|
+
console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
|
|
2823
|
+
}
|
|
2824
|
+
else {
|
|
2825
|
+
console.log(' daemon 尚未提供配对码;EvolCore 启动后可通过 /pair 或 ECWeb 页面获取');
|
|
2826
|
+
}
|
|
2779
2827
|
return true;
|
|
2780
2828
|
}
|
|
2781
2829
|
console.log(`❌ ECWeb 启动失败:端口 ${port} 未就绪(进程 PID ${pid} 可能已退出,查看 logs/watch-web.log)`);
|
|
@@ -2801,13 +2849,13 @@ async function printEcwebAccess(port) {
|
|
|
2801
2849
|
console.log(`🔭 ECWeb http://localhost:${port}`);
|
|
2802
2850
|
let pair = null;
|
|
2803
2851
|
for (let i = 0; i < 10 && !pair; i++) {
|
|
2804
|
-
pair = await
|
|
2852
|
+
pair = await fetchRemotePairingCode();
|
|
2805
2853
|
if (!pair)
|
|
2806
2854
|
await sleep(300);
|
|
2807
2855
|
}
|
|
2808
2856
|
if (pair) {
|
|
2809
2857
|
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
2810
|
-
console.log(` 配对码: ${pair.code} (约 ${mins}
|
|
2858
|
+
console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后远程访问 token 缓存 30 天)`);
|
|
2811
2859
|
}
|
|
2812
2860
|
else {
|
|
2813
2861
|
console.log(' 配对码: 暂不可用(稍后重试 ec watch web,或查看 logs/watch-web.log)');
|
|
@@ -2924,7 +2972,56 @@ export async function cmdDiagnose() {
|
|
|
2924
2972
|
else {
|
|
2925
2973
|
console.log(`[diagnose] ✓ 已加载 ${agents.length} 个 self-agent`);
|
|
2926
2974
|
}
|
|
2927
|
-
// 3.
|
|
2975
|
+
// 3. 检查实际使用 Codex 的 Agent 项目目录。项目路径属于 Agent 的
|
|
2976
|
+
// effective config,逐个输出 AID,避免只检查第一个 agent 掩盖坏配置。
|
|
2977
|
+
const codexAgents = agents.filter(agent => {
|
|
2978
|
+
try {
|
|
2979
|
+
return resolveEffective({ self: agent.aid }).active_baseagent === 'codex';
|
|
2980
|
+
}
|
|
2981
|
+
catch (error) {
|
|
2982
|
+
console.error(`[diagnose] ❌ Agent ${agent.aid} effective config 解析失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
2983
|
+
hasError = true;
|
|
2984
|
+
return false;
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
for (const agent of codexAgents) {
|
|
2988
|
+
try {
|
|
2989
|
+
const effective = resolveEffective({ self: agent.aid });
|
|
2990
|
+
const check = checkCodexProjectPath(effective.projects?.defaultPath);
|
|
2991
|
+
if (check.status === 'ok') {
|
|
2992
|
+
console.log(`[diagnose] ✓ Codex 项目路径 ${agent.aid}: ${check.path}`);
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
console.error(`[diagnose] ❌ Codex 项目路径 ${agent.aid}: ${check.path || '(未配置)'} (${check.reason || check.status})`);
|
|
2996
|
+
hasError = true;
|
|
2997
|
+
}
|
|
2998
|
+
for (const warning of check.warnings)
|
|
2999
|
+
console.warn(`[diagnose] ⚠️ Codex 项目路径 ${agent.aid}: ${warning}`);
|
|
3000
|
+
}
|
|
3001
|
+
catch (error) {
|
|
3002
|
+
console.error(`[diagnose] ❌ Codex 项目路径检查失败 ${agent.aid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3003
|
+
hasError = true;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
if (codexAgents.length === 0) {
|
|
3007
|
+
console.log('[diagnose] - Codex managed hook: 跳过(没有 Agent 使用 Codex)');
|
|
3008
|
+
}
|
|
3009
|
+
else {
|
|
3010
|
+
const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
|
|
3011
|
+
if (hook.status === 'installed') {
|
|
3012
|
+
console.log(`[diagnose] ✓ Codex managed hook 已安装: ${hook.hookPath}`);
|
|
3013
|
+
}
|
|
3014
|
+
else if (hook.status === 'skipped') {
|
|
3015
|
+
console.log(`[diagnose] - Codex managed hook: 跳过(${hook.reason})`);
|
|
3016
|
+
}
|
|
3017
|
+
else {
|
|
3018
|
+
const prefix = hook.status === 'missing' ? '⚠️' : '❌';
|
|
3019
|
+
const writer = hook.status === 'missing' ? console.warn : console.error;
|
|
3020
|
+
writer(`[diagnose] ${prefix} Codex managed hook ${hook.status}: ${hook.reason || 'unknown error'}${hook.requirements ? ` (${hook.requirements})` : ''}`);
|
|
3021
|
+
hasError = true;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
// 4. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
|
|
2928
3025
|
if (agents.length > 0) {
|
|
2929
3026
|
try {
|
|
2930
3027
|
const merged = resolveEffective({ self: agents[0].aid });
|
package/dist/cli/index.js
CHANGED
|
@@ -46,6 +46,7 @@ AUN:
|
|
|
46
46
|
fs AUN 文件系统
|
|
47
47
|
storage 底层对象存储命令
|
|
48
48
|
rpc 调用底层 AUN RPC
|
|
49
|
+
menu-client 向远程 daemon 发送标准 Menu 请求
|
|
49
50
|
contact 联系人黑名单管理
|
|
50
51
|
net AUN 网络链路诊断
|
|
51
52
|
|
|
@@ -350,6 +351,25 @@ export async function main(args) {
|
|
|
350
351
|
await cmdRpc(args.slice(1));
|
|
351
352
|
break;
|
|
352
353
|
}
|
|
354
|
+
case 'menu-client': {
|
|
355
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
356
|
+
suppressSdkLogs();
|
|
357
|
+
const { cmdMenuClient } = await import('./menu-client.js');
|
|
358
|
+
try {
|
|
359
|
+
await cmdMenuClient(args.slice(1));
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const payload = {
|
|
363
|
+
ok: false,
|
|
364
|
+
code: typeof error?.code === 'string' ? error.code : 'MENU_CLIENT_FAILED',
|
|
365
|
+
error: error?.message ?? String(error),
|
|
366
|
+
...(error?.data === undefined ? {} : { data: error.data }),
|
|
367
|
+
};
|
|
368
|
+
console.error(JSON.stringify(payload));
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
353
373
|
case 'storage': {
|
|
354
374
|
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
355
375
|
suppressSdkLogs();
|
package/dist/cli/init.js
CHANGED
|
@@ -14,6 +14,7 @@ import { defaultProjectsRoot } from '../utils/project-path.js';
|
|
|
14
14
|
import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
|
|
15
15
|
import { autostartInstalled, autostartPlatformLabel, configureAutostart } from '../utils/autostart.js';
|
|
16
16
|
import { askInit, isInitCancelledError, printInitCancelHint } from './init-cancel.js';
|
|
17
|
+
import { AUTO_BASEAGENT_CANDIDATES, BASEAGENT_CANDIDATES, detectAvailableBaseagents, } from '../agents/baseagent-availability.js';
|
|
17
18
|
// ==================== Helpers ====================
|
|
18
19
|
function ask(rl, question) {
|
|
19
20
|
return askInit(rl, question);
|
|
@@ -27,17 +28,8 @@ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
|
|
|
27
28
|
}
|
|
28
29
|
return value;
|
|
29
30
|
}
|
|
30
|
-
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
31
|
-
const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
32
|
-
function isBaseagentAvailable(baseagent, codexAvailability) {
|
|
33
|
-
if (baseagent === 'codex')
|
|
34
|
-
return (codexAvailability ?? getCodexAppServerAvailability()).available;
|
|
35
|
-
if (baseagent === 'ecagent')
|
|
36
|
-
return true;
|
|
37
|
-
return commandExists(baseagent);
|
|
38
|
-
}
|
|
39
31
|
function detectAvailable(codexAvailability = getCodexAppServerAvailability()) {
|
|
40
|
-
return
|
|
32
|
+
return detectAvailableBaseagents(codexAvailability);
|
|
41
33
|
}
|
|
42
34
|
/** Offer installation only when the probe positively identified an old CLI. */
|
|
43
35
|
export async function maybeUpgradeCodexForInit(availability, dependencies) {
|