evolcore 0.0.17 → 0.0.18
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 +32 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +97 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
package/dist/channels/aun.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { E2EEError } from '@agentunion/fastaun';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import os from 'os';
|
|
@@ -15,7 +16,7 @@ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
|
15
16
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
16
17
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
17
18
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
18
|
-
import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
19
|
+
import { enableFullGroupPullPagination, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
19
20
|
import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
|
|
20
21
|
import { loadAgent } from '../config-store.js';
|
|
21
22
|
import { normalizeAgentLifecycle } from '../config/lifecycle.js';
|
|
@@ -33,7 +34,7 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
|
|
|
33
34
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
34
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
35
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
36
|
-
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
37
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationId, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
37
38
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
38
39
|
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
39
40
|
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
@@ -224,6 +225,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
224
225
|
protectedHeaders: opts.protectedHeaders,
|
|
225
226
|
messageId: opts.messageId,
|
|
226
227
|
receivedAt: opts.receivedAt,
|
|
228
|
+
receivedAtMono: opts.receivedAtMono,
|
|
227
229
|
gatewaySeq: opts.gatewaySeq,
|
|
228
230
|
causation: opts.causation,
|
|
229
231
|
mentions: opts.mentions,
|
|
@@ -1053,6 +1055,8 @@ export class AUNChannel {
|
|
|
1053
1055
|
reconnectTimer = null;
|
|
1054
1056
|
reconnectGeneration = 0; // 防止并发 initClient:每次 takeoverReconnect 递增,回调中校验
|
|
1055
1057
|
onChannelDown;
|
|
1058
|
+
/** Prevent a legacy state alias from double-counting the same SDK transition. */
|
|
1059
|
+
lastConnectionStateSignature = '';
|
|
1056
1060
|
// initClient concurrency guard: 防止多个 initClient 并发执行
|
|
1057
1061
|
initInProgress = false;
|
|
1058
1062
|
// Flap detection: 连接寿命 < FLAP_WINDOW_MS 累计 FLAP_THRESHOLD 次,判定为持续被踢
|
|
@@ -1165,6 +1169,7 @@ export class AUNChannel {
|
|
|
1165
1169
|
this.store = null;
|
|
1166
1170
|
}
|
|
1167
1171
|
this.connected = false;
|
|
1172
|
+
this.lastConnectionStateSignature = '';
|
|
1168
1173
|
const aunPath = this.config.keystorePath || resolveRoot();
|
|
1169
1174
|
const aidName = this.config.aid;
|
|
1170
1175
|
// encryptionSeed 由 getAidStore 内部解析(config / env / 'evol')
|
|
@@ -1183,7 +1188,11 @@ export class AUNChannel {
|
|
|
1183
1188
|
});
|
|
1184
1189
|
this.store = store;
|
|
1185
1190
|
const client = await loadClient(store, aidName);
|
|
1191
|
+
enableFullGroupPullPagination(client);
|
|
1186
1192
|
this.client = client;
|
|
1193
|
+
// A reconnect replaces the SDK client. Late events from the retired
|
|
1194
|
+
// instance must not mutate the new instance's health or message state.
|
|
1195
|
+
const isCurrentClient = () => this.client === client;
|
|
1187
1196
|
// fastaun gives a preset in-memory gateway precedence over its metadata
|
|
1188
1197
|
// cache and AID discovery. authenticate({ gateway }) is deliberately
|
|
1189
1198
|
// rejected by its public API, so set the documented client preset before
|
|
@@ -1193,6 +1202,10 @@ export class AUNChannel {
|
|
|
1193
1202
|
this.gatewayUrl = configuredGateway;
|
|
1194
1203
|
// Register event handlers before connecting
|
|
1195
1204
|
client.on('message.received', (data) => {
|
|
1205
|
+
if (!isCurrentClient())
|
|
1206
|
+
return;
|
|
1207
|
+
this.aidState.lastInboundAt = Date.now();
|
|
1208
|
+
this.aidState.lastTransportEventAt = this.aidState.lastInboundAt;
|
|
1196
1209
|
this.trace('IN', 'message.received', data);
|
|
1197
1210
|
const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
|
|
1198
1211
|
const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
|
|
@@ -1202,6 +1215,10 @@ export class AUNChannel {
|
|
|
1202
1215
|
// pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
|
|
1203
1216
|
if (!this.config.pureIdentity) {
|
|
1204
1217
|
client.on('group.message_created', (data) => {
|
|
1218
|
+
if (!isCurrentClient())
|
|
1219
|
+
return;
|
|
1220
|
+
this.aidState.lastInboundAt = Date.now();
|
|
1221
|
+
this.aidState.lastTransportEventAt = this.aidState.lastInboundAt;
|
|
1205
1222
|
this.trace('IN', 'group.message_created', data);
|
|
1206
1223
|
const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
|
|
1207
1224
|
const gid = env.group_id ?? '';
|
|
@@ -1210,16 +1227,26 @@ export class AUNChannel {
|
|
|
1210
1227
|
this.handleIncomingGroupMessage(data);
|
|
1211
1228
|
});
|
|
1212
1229
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1230
|
+
const handleSdkState = (source) => (data) => {
|
|
1231
|
+
if (!isCurrentClient())
|
|
1232
|
+
return;
|
|
1233
|
+
// fastaun 0.5.x publishes state_change with nine-state values. Keep the
|
|
1234
|
+
// legacy event subscription for older stores/SDKs during rolling upgrades.
|
|
1235
|
+
this.handleConnectionState(data, source);
|
|
1236
|
+
};
|
|
1237
|
+
client.on('state_change', handleSdkState('state_change'));
|
|
1238
|
+
client.on('connection.state', handleSdkState('connection.state'));
|
|
1217
1239
|
// gateway 被踢/服务端主动断开(含同槽位互踢的 self/new extra_info)
|
|
1218
1240
|
client.on('gateway.disconnect', (data) => {
|
|
1241
|
+
if (!isCurrentClient())
|
|
1242
|
+
return;
|
|
1243
|
+
this.aidState.lastTransportEventAt = Date.now();
|
|
1219
1244
|
this.trace('IN', 'gateway.disconnect', data);
|
|
1220
1245
|
this.handleGatewayDisconnect(data);
|
|
1221
1246
|
});
|
|
1222
1247
|
client.on('message.recalled', (data) => {
|
|
1248
|
+
if (!isCurrentClient())
|
|
1249
|
+
return;
|
|
1223
1250
|
this.trace('IN', 'message.recalled', data);
|
|
1224
1251
|
if (data && typeof data === 'object') {
|
|
1225
1252
|
const ids = data.message_ids;
|
|
@@ -1234,6 +1261,8 @@ export class AUNChannel {
|
|
|
1234
1261
|
}
|
|
1235
1262
|
});
|
|
1236
1263
|
client.on('message.undecryptable', (data) => {
|
|
1264
|
+
if (!isCurrentClient())
|
|
1265
|
+
return;
|
|
1237
1266
|
this.trace('IN', 'message.undecryptable', data);
|
|
1238
1267
|
const d = data;
|
|
1239
1268
|
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
@@ -1242,6 +1271,8 @@ export class AUNChannel {
|
|
|
1242
1271
|
// pureIdentity(控制 AID):不注册 group 解密失败监听
|
|
1243
1272
|
if (!this.config.pureIdentity) {
|
|
1244
1273
|
client.on('group.message_undecryptable', (data) => {
|
|
1274
|
+
if (!isCurrentClient())
|
|
1275
|
+
return;
|
|
1245
1276
|
this.trace('IN', 'group.message_undecryptable', data);
|
|
1246
1277
|
const d = data;
|
|
1247
1278
|
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
@@ -1250,6 +1281,8 @@ export class AUNChannel {
|
|
|
1250
1281
|
// 群消息撤回(SDK 0.4.10 在线 push 通道):与私聊 message.recalled 同构,
|
|
1251
1282
|
// 逐个 message_id 交给 recallHandler → msgBridge.cancel(排队中删除 / 处理中中断)。
|
|
1252
1283
|
client.on('group.message_recalled', (data) => {
|
|
1284
|
+
if (!isCurrentClient())
|
|
1285
|
+
return;
|
|
1253
1286
|
this.trace('IN', 'group.message_recalled', data);
|
|
1254
1287
|
if (data && typeof data === 'object') {
|
|
1255
1288
|
const d = data;
|
|
@@ -1357,15 +1390,45 @@ export class AUNChannel {
|
|
|
1357
1390
|
}
|
|
1358
1391
|
const operationId = postBootstrapWelcomeOperationId(aidName);
|
|
1359
1392
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
|
|
1393
|
+
if (hasMessageLogOperation(chatDir, operationId))
|
|
1394
|
+
return true;
|
|
1360
1395
|
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
1361
|
-
|
|
1362
|
-
|
|
1396
|
+
const initial = outbox.findByDedupeKey(aidName, bootstrapInitialMessageOperationId(aidName));
|
|
1397
|
+
const initialDelivery = initial && outbox.isDeliveryForChannel(initial.delivery, owner)
|
|
1398
|
+
? initial.delivery
|
|
1399
|
+
: undefined;
|
|
1400
|
+
// During bootstrap completion the lifecycle is intentionally still
|
|
1401
|
+
// non-active. Persisting the welcome must stay local and must not block
|
|
1402
|
+
// on a best-effort group RPC before the lifecycle commit. If the initial
|
|
1403
|
+
// bootstrap route is already known, preserve it; otherwise migration
|
|
1404
|
+
// will resolve a group route after the channel is active.
|
|
1405
|
+
const lifecycle = normalizeAgentLifecycle(agentConfig).lifecycle;
|
|
1406
|
+
const ownerIsGroup = initialDelivery
|
|
1407
|
+
? initialDelivery.chatType === 'group'
|
|
1408
|
+
: lifecycle === 'active' ? await this.isGroup(owner) : undefined;
|
|
1409
|
+
const delivery = initialDelivery ?? (ownerIsGroup === true
|
|
1410
|
+
? { chatType: 'group', groupId: owner }
|
|
1411
|
+
: { chatType: 'private' });
|
|
1412
|
+
if (prepared
|
|
1413
|
+
&& prepared.channelId === owner
|
|
1414
|
+
&& isDeliveryTarget(prepared.delivery)
|
|
1415
|
+
&& sameDeliveryTarget(prepared.delivery, delivery)) {
|
|
1363
1416
|
return true;
|
|
1364
1417
|
}
|
|
1365
1418
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
1366
1419
|
let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
1420
|
+
// Keep lifecycle=bootstrapping preparation entirely local. Resolving an
|
|
1421
|
+
// uncached peer may download agent.md, and welcome display metadata must
|
|
1422
|
+
// never delay the durable write-ahead record or the lifecycle commit.
|
|
1423
|
+
const cachedOwnerInfo = this.peerInfoCached(owner);
|
|
1367
1424
|
try {
|
|
1368
|
-
const ownerInfo =
|
|
1425
|
+
const ownerInfo = cachedOwnerInfo
|
|
1426
|
+
?? (lifecycle === 'active' ? await this.fetchPeerInfo(owner) : undefined);
|
|
1427
|
+
if (!ownerInfo) {
|
|
1428
|
+
preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
|
|
1429
|
+
logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1369
1432
|
ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
|
|
1370
1433
|
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
1371
1434
|
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
|
|
@@ -1374,7 +1437,7 @@ export class AUNChannel {
|
|
|
1374
1437
|
catch (e) {
|
|
1375
1438
|
logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
|
|
1376
1439
|
}
|
|
1377
|
-
preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
|
|
1440
|
+
preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
|
|
1378
1441
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
|
|
1379
1442
|
return true;
|
|
1380
1443
|
}
|
|
@@ -1415,7 +1478,7 @@ export class AUNChannel {
|
|
|
1415
1478
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
1416
1479
|
return hasMessageLogOperation(chatDir, operationId);
|
|
1417
1480
|
}
|
|
1418
|
-
if (entry.delivery
|
|
1481
|
+
if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId)) {
|
|
1419
1482
|
logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
|
|
1420
1483
|
outbox.remove(aid, entry.id);
|
|
1421
1484
|
return false;
|
|
@@ -1679,6 +1742,7 @@ export class AUNChannel {
|
|
|
1679
1742
|
return;
|
|
1680
1743
|
const msg = data;
|
|
1681
1744
|
const receivedAt = Date.now();
|
|
1745
|
+
const receivedAtMono = performance.now();
|
|
1682
1746
|
// Claim before any payload extraction or command parsing. Retransmits may
|
|
1683
1747
|
// carry a new seq, but the application identity remains message_id.
|
|
1684
1748
|
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
@@ -1772,7 +1836,7 @@ export class AUNChannel {
|
|
|
1772
1836
|
this.dispatchMessage({
|
|
1773
1837
|
channelId: chatId, userId: fromAid,
|
|
1774
1838
|
text: JSON.stringify(payload),
|
|
1775
|
-
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
1839
|
+
chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
|
|
1776
1840
|
peerName: displayName || undefined,
|
|
1777
1841
|
peerType: peerIdentity.type,
|
|
1778
1842
|
encrypted: msgEncrypted,
|
|
@@ -1860,6 +1924,7 @@ export class AUNChannel {
|
|
|
1860
1924
|
return;
|
|
1861
1925
|
const msg = data;
|
|
1862
1926
|
const receivedAt = Date.now();
|
|
1927
|
+
const receivedAtMono = performance.now();
|
|
1863
1928
|
// Deduplicate at the adapter boundary, before mention/slash routing.
|
|
1864
1929
|
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1865
1930
|
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
@@ -1960,7 +2025,7 @@ export class AUNChannel {
|
|
|
1960
2025
|
this.dispatchMessage({
|
|
1961
2026
|
channelId: groupId, userId: senderAid,
|
|
1962
2027
|
text: JSON.stringify(payload),
|
|
1963
|
-
chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
|
|
2028
|
+
chatType: 'group', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq, groupId,
|
|
1964
2029
|
encrypted: menuEncrypted,
|
|
1965
2030
|
protectedHeaders,
|
|
1966
2031
|
mentions: payloadMentionEntries,
|
|
@@ -2199,6 +2264,7 @@ export class AUNChannel {
|
|
|
2199
2264
|
protectedHeaders: event.protectedHeaders,
|
|
2200
2265
|
messageId: event.messageId,
|
|
2201
2266
|
receivedAt: event.receivedAt,
|
|
2267
|
+
receivedAtMono: event.receivedAtMono,
|
|
2202
2268
|
gatewaySeq: event.gatewaySeq ?? event.seq,
|
|
2203
2269
|
causation: event.causation,
|
|
2204
2270
|
threadId: event.threadId,
|
|
@@ -2496,21 +2562,47 @@ export class AUNChannel {
|
|
|
2496
2562
|
logger.warn(`${this.logPrefix()} Server-initiated disconnect: code=${code} reason=${reason} detail=${JSON.stringify(detail)}`);
|
|
2497
2563
|
}
|
|
2498
2564
|
}
|
|
2499
|
-
handleConnectionState(data) {
|
|
2565
|
+
handleConnectionState(data, source = 'state_change') {
|
|
2500
2566
|
if (!data || typeof data !== 'object')
|
|
2501
2567
|
return;
|
|
2502
|
-
const
|
|
2568
|
+
const rawState = String(data.state ?? '');
|
|
2569
|
+
const statePayload = data;
|
|
2570
|
+
const state = rawState === 'ready' ? 'connected'
|
|
2571
|
+
: rawState === 'standby' || rawState === 'authenticated' || rawState === 'connecting' || rawState === 'retry_backoff'
|
|
2572
|
+
? 'reconnecting'
|
|
2573
|
+
: rawState === 'connection_failed' || rawState === 'no_identity'
|
|
2574
|
+
? 'terminal_failed'
|
|
2575
|
+
: rawState === 'closed'
|
|
2576
|
+
? 'disconnected'
|
|
2577
|
+
: rawState;
|
|
2578
|
+
const signature = [
|
|
2579
|
+
rawState,
|
|
2580
|
+
String(statePayload.attempt ?? ''),
|
|
2581
|
+
String(statePayload.reason ?? ''),
|
|
2582
|
+
String(statePayload.code ?? ''),
|
|
2583
|
+
String(statePayload.error ?? ''),
|
|
2584
|
+
String(statePayload.gateway ?? ''),
|
|
2585
|
+
].join('|');
|
|
2586
|
+
this.aidState.lastTransportEventAt = Date.now();
|
|
2587
|
+
if (signature === this.lastConnectionStateSignature)
|
|
2588
|
+
return;
|
|
2589
|
+
this.lastConnectionStateSignature = signature;
|
|
2590
|
+
if (this.intentionalDisconnect && state === 'disconnected') {
|
|
2591
|
+
this.connected = false;
|
|
2592
|
+
this.connectedAt = 0;
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2503
2595
|
if (state === 'connected') {
|
|
2504
2596
|
this.connected = true;
|
|
2505
2597
|
this.connectedAt = Date.now();
|
|
2506
2598
|
this.lastReconnectLogTime = 0;
|
|
2507
2599
|
this.lastReconnectLogAttempt = 0;
|
|
2508
|
-
//
|
|
2600
|
+
// SDK 状态事件 payload 带实际连接的 gateway,更新本地缓存
|
|
2509
2601
|
const evtGateway = data.gateway;
|
|
2510
2602
|
if (typeof evtGateway === 'string' && evtGateway)
|
|
2511
2603
|
this.gatewayUrl = evtGateway;
|
|
2512
2604
|
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
2513
|
-
this.trace('IN',
|
|
2605
|
+
this.trace('IN', source, data);
|
|
2514
2606
|
logger.info(`${this.logPrefix()} Connected`);
|
|
2515
2607
|
// 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
|
|
2516
2608
|
// 必须等 disconnected 时根据 lifetime 决定是否清零
|
|
@@ -2519,7 +2611,11 @@ export class AUNChannel {
|
|
|
2519
2611
|
else if (state === 'disconnected' || state === 'reconnecting') {
|
|
2520
2612
|
const wasConnected = this.connected;
|
|
2521
2613
|
this.connected = false;
|
|
2522
|
-
|
|
2614
|
+
// The nine-state SDK emits standby -> retry_backoff -> reconnecting for
|
|
2615
|
+
// one reconnect cycle. Count the connected-to-down edge once instead of
|
|
2616
|
+
// inflating the metric for every intermediate state.
|
|
2617
|
+
if (wasConnected)
|
|
2618
|
+
this.aidState.reconnectCount++;
|
|
2523
2619
|
this.aidState.lastAttemptAt = Date.now();
|
|
2524
2620
|
this.setAidStatus('reconnecting');
|
|
2525
2621
|
// Flap 检测:仅在从 connected 状态过渡时统计
|
|
@@ -2545,7 +2641,7 @@ export class AUNChannel {
|
|
|
2545
2641
|
}
|
|
2546
2642
|
}
|
|
2547
2643
|
if (state === 'disconnected') {
|
|
2548
|
-
this.trace('IN',
|
|
2644
|
+
this.trace('IN', source, data);
|
|
2549
2645
|
logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
|
|
2550
2646
|
}
|
|
2551
2647
|
else {
|
|
@@ -2561,14 +2657,14 @@ export class AUNChannel {
|
|
|
2561
2657
|
logger.info(`${this.logPrefix()} SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
2562
2658
|
this.lastReconnectLogTime = now;
|
|
2563
2659
|
this.lastReconnectLogAttempt = attempt;
|
|
2564
|
-
this.trace('IN',
|
|
2660
|
+
this.trace('IN', source, data);
|
|
2565
2661
|
}
|
|
2566
2662
|
}
|
|
2567
2663
|
}
|
|
2568
2664
|
else if (state === 'terminal_failed') {
|
|
2569
2665
|
this.connected = false;
|
|
2570
2666
|
this.connectedAt = 0;
|
|
2571
|
-
this.trace('IN',
|
|
2667
|
+
this.trace('IN', source, data);
|
|
2572
2668
|
const d = data;
|
|
2573
2669
|
const reason = d.reason ?? '';
|
|
2574
2670
|
const error = d.error ?? 'unknown';
|
|
@@ -4423,6 +4519,7 @@ export class AUNChannel {
|
|
|
4423
4519
|
async drainOutbox() {
|
|
4424
4520
|
if (!this.connected || !this.client)
|
|
4425
4521
|
return;
|
|
4522
|
+
await this.migrateLegacyBootstrapRoutes();
|
|
4426
4523
|
if (!outbox.hasPending(this.config.aid))
|
|
4427
4524
|
return;
|
|
4428
4525
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
@@ -4450,6 +4547,32 @@ export class AUNChannel {
|
|
|
4450
4547
|
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
|
|
4451
4548
|
}
|
|
4452
4549
|
}
|
|
4550
|
+
/** Repair pre-route bootstrap entries once the AUN group endpoint is available. */
|
|
4551
|
+
async migrateLegacyBootstrapRoutes() {
|
|
4552
|
+
const operationIds = new Set([
|
|
4553
|
+
bootstrapInitialMessageOperationId(this.config.aid),
|
|
4554
|
+
postBootstrapWelcomeOperationId(this.config.aid),
|
|
4555
|
+
]);
|
|
4556
|
+
const entries = outbox.load(this.config.aid).filter(entry => typeof entry.dedupeKey === 'string'
|
|
4557
|
+
&& operationIds.has(entry.dedupeKey)
|
|
4558
|
+
&& entry.channelId
|
|
4559
|
+
&& entry.delivery?.chatType === 'private');
|
|
4560
|
+
for (const entry of entries) {
|
|
4561
|
+
const group = await this.isGroup(entry.channelId);
|
|
4562
|
+
if (group !== true)
|
|
4563
|
+
continue;
|
|
4564
|
+
const delivery = { chatType: 'group', groupId: entry.channelId };
|
|
4565
|
+
if (!outbox.replace(this.config.aid, {
|
|
4566
|
+
...entry,
|
|
4567
|
+
delivery,
|
|
4568
|
+
context: entry.context
|
|
4569
|
+
? { ...entry.context, delivery }
|
|
4570
|
+
: entry.context,
|
|
4571
|
+
}))
|
|
4572
|
+
continue;
|
|
4573
|
+
logger.warn(`${this.logPrefix()} Migrated legacy bootstrap outbox route to group.send: entry=${entry.id} group=${entry.channelId}`);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4453
4576
|
acknowledge(messageId) {
|
|
4454
4577
|
// Gateway auto-delivery-ack is sufficient; skip explicit message.ack RPC
|
|
4455
4578
|
// to avoid duplicate "已送达" at the sender CLI
|
|
@@ -4622,6 +4745,8 @@ export class AUNChannel {
|
|
|
4622
4745
|
aid: this._aid,
|
|
4623
4746
|
flapCount: this.flapCount,
|
|
4624
4747
|
plaintextRecv: this.plaintextRecv,
|
|
4748
|
+
lastTransportEventAt: this.aidState.lastTransportEventAt,
|
|
4749
|
+
lastInboundAt: this.aidState.lastInboundAt,
|
|
4625
4750
|
};
|
|
4626
4751
|
}
|
|
4627
4752
|
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
@@ -4714,6 +4839,18 @@ export class AUNChannel {
|
|
|
4714
4839
|
return undefined; // 不写缓存,下次仍可重试
|
|
4715
4840
|
}
|
|
4716
4841
|
}
|
|
4842
|
+
/** Query the authoritative group endpoint without guessing from an AID string. */
|
|
4843
|
+
async isGroup(groupId) {
|
|
4844
|
+
if (!groupId || !this.client)
|
|
4845
|
+
return undefined;
|
|
4846
|
+
try {
|
|
4847
|
+
const result = await this.callAndTrace('group.get_info', { group_id: groupId });
|
|
4848
|
+
return !!(result?.group || result?.group_id || result?.groupId);
|
|
4849
|
+
}
|
|
4850
|
+
catch {
|
|
4851
|
+
return undefined;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4717
4854
|
/** 统计关系键继续使用群 ID,仅为智能体预览补充可读群名。 */
|
|
4718
4855
|
async groupStatsContext(groupId) {
|
|
4719
4856
|
return {
|
|
@@ -4996,6 +5133,7 @@ export class AUNChannelPlugin {
|
|
|
4996
5133
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
4997
5134
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
4998
5135
|
getGroupName: (groupId) => channel.getGroupName(groupId),
|
|
5136
|
+
isGroup: (groupId) => channel.isGroup(groupId),
|
|
4999
5137
|
getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
|
|
5000
5138
|
_selfAid: () => channel.getStatus().aid,
|
|
5001
5139
|
_selfName: () => channel.getSelfName(),
|
|
@@ -26,6 +26,15 @@ export async function cmdAgent(args) {
|
|
|
26
26
|
const sub = args[0];
|
|
27
27
|
const formatJson = getArgValue(args, '--format') === 'json';
|
|
28
28
|
const commands = new Set(['list', 'show', 'new', 'enable', 'disable', 'get', 'set', 'rename', 'ready', 'reload', 'delete']);
|
|
29
|
+
if (sub === 'create') {
|
|
30
|
+
const error = { ok: false, code: 'INVALID_COMMAND', error: '`ec agent create` is not a command; use `ec agent new`.' };
|
|
31
|
+
if (formatJson)
|
|
32
|
+
console.log(JSON.stringify(error));
|
|
33
|
+
else
|
|
34
|
+
console.error(`❌ [${error.code}] ${error.error}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
29
38
|
if (!sub || isHelpFlag(sub) || (wantsHelp(args) && !commands.has(sub))) {
|
|
30
39
|
console.log(`用法: ec agent <command>
|
|
31
40
|
|
|
@@ -59,14 +68,66 @@ Options:
|
|
|
59
68
|
ec agent delete mybot.agentid.pub --purge`);
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
71
|
+
const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentRename, agentDelete, agentReady, runManagedAgentOperation, } = await import('./agent.js');
|
|
62
72
|
if (process.env.EVOLCORE_SESSION_ID && !wantsHelp(args) && !new Set(['get', 'set', 'ready']).has(sub)) {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
if (sub === 'new' && !args.includes('--non-interactive')) {
|
|
74
|
+
const error = {
|
|
75
|
+
ok: false,
|
|
76
|
+
code: 'MANAGED_NON_INTERACTIVE_REQUIRED',
|
|
77
|
+
error: 'Managed Agent tasks must use `ec agent new <aid> --non-interactive`; interactive creation is unavailable here. Use ECWeb for interactive setup.',
|
|
78
|
+
data: { reasonCode: 'MANAGED_NON_INTERACTIVE_REQUIRED', nextStep: 'Use `ec agent new <aid> --non-interactive` or ECWeb.' },
|
|
79
|
+
};
|
|
80
|
+
if (formatJson)
|
|
81
|
+
console.log(JSON.stringify(error));
|
|
82
|
+
else
|
|
83
|
+
console.error(`❌ [${error.code}] ${error.error}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const managedArgs = commands.has(sub) ? args : ['show', ...args];
|
|
88
|
+
const result = await runManagedAgentOperation(managedArgs);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
if (formatJson)
|
|
91
|
+
console.log(JSON.stringify(result));
|
|
92
|
+
else {
|
|
93
|
+
console.error(`❌ [${result.code || 'NOT_ALLOWED'}] ${result.error}`);
|
|
94
|
+
if (result.data?.nextStep)
|
|
95
|
+
console.error(`Next step: ${result.data.nextStep}`);
|
|
96
|
+
}
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (formatJson) {
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (sub === 'show' || (!sub.startsWith('new') && result.identity && result.config)) {
|
|
105
|
+
printAgentShowHuman(result);
|
|
106
|
+
}
|
|
107
|
+
else if (sub === 'list' && Array.isArray(result.agents)) {
|
|
108
|
+
if (result.agents.length === 0)
|
|
109
|
+
console.log('No agents configured.');
|
|
110
|
+
else
|
|
111
|
+
for (const agent of result.agents)
|
|
112
|
+
console.log(`${agent.name || agent.aid}\t${agent.status || 'stopped'}\t${agent.aid}`);
|
|
113
|
+
}
|
|
114
|
+
else if (result.accepted) {
|
|
115
|
+
console.log(`✓ Agent "${result.aid}" creation accepted`);
|
|
116
|
+
}
|
|
117
|
+
else if (result.aid && result.enabled !== undefined) {
|
|
118
|
+
console.log(`✓ ${result.aid} ${result.enabled ? 'enabled' : 'disabled'}${result.reloaded ? ' (hot-reloaded)' : ''}`);
|
|
119
|
+
}
|
|
120
|
+
else if (result.aid && result.purged !== undefined) {
|
|
121
|
+
console.log(`✓ ${result.aid} deleted${result.purged ? ' (purged)' : ''}`);
|
|
122
|
+
}
|
|
123
|
+
else if (result.aid && result.reloaded) {
|
|
124
|
+
console.log(`✓ Agent "${result.aid}" reloaded`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(JSON.stringify(result));
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
68
130
|
}
|
|
69
|
-
const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentRename, agentDelete, agentReady, } = await import('./agent.js');
|
|
70
131
|
// --- list ---
|
|
71
132
|
if (sub === 'list') {
|
|
72
133
|
if (wantsHelp(args)) {
|
package/dist/cli/agent.js
CHANGED
|
@@ -26,6 +26,32 @@ function withStaticOwner(config, owner) {
|
|
|
26
26
|
return config;
|
|
27
27
|
return { ...config, owners: [...owners, value] };
|
|
28
28
|
}
|
|
29
|
+
/** Route agent lifecycle commands from a managed task through daemon IPC. */
|
|
30
|
+
export async function runManagedAgentOperation(argv) {
|
|
31
|
+
const sessionId = process.env.EVOLCORE_SESSION_ID;
|
|
32
|
+
if (!sessionId)
|
|
33
|
+
return { ok: false, code: 'MANAGED_ONLY', error: 'agent operation is not running in a managed task' };
|
|
34
|
+
const delegationToken = process.env[AGENT_DELEGATION_TOKEN_ENV];
|
|
35
|
+
if (!delegationToken) {
|
|
36
|
+
return { ok: false, code: 'DELEGATION_REQUIRED', error: `${AGENT_DELEGATION_TOKEN_ENV} is not set for the active task` };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const response = await ipcQuery(resolvePaths().socket, {
|
|
40
|
+
type: 'agent.op',
|
|
41
|
+
argv: ['agent', ...argv],
|
|
42
|
+
sessionId,
|
|
43
|
+
delegationToken,
|
|
44
|
+
}, 60_000);
|
|
45
|
+
if (!response)
|
|
46
|
+
return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' };
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return { ok: false, code: response.code, error: response.error || 'agent operation failed', ...(response.data ? { data: response.data } : {}) };
|
|
49
|
+
return response.result ?? { ok: true, ...(response.data ?? {}) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { ok: false, code: 'DAEMON_UNAVAILABLE', error: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
29
55
|
// ==================== Helpers ====================
|
|
30
56
|
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
31
57
|
const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
package/dist/cli/command-log.js
CHANGED
|
@@ -18,14 +18,33 @@ import path from 'path';
|
|
|
18
18
|
import { agentLogsDir, resolvePaths } from '../paths.js';
|
|
19
19
|
import { getArgValue } from './help.js';
|
|
20
20
|
import { readTaskRuntimeContextFromEnv } from './task-context.js';
|
|
21
|
+
/** Keep path derivation from accepting arbitrary option values. */
|
|
22
|
+
function isSafeAid(value) {
|
|
23
|
+
if (!value)
|
|
24
|
+
return false;
|
|
25
|
+
const labels = value.split('.');
|
|
26
|
+
return labels.length >= 3
|
|
27
|
+
&& labels.every(label => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
|
|
28
|
+
}
|
|
21
29
|
/**
|
|
22
30
|
* 同步解析当前执行者 self-AID(零延迟,绝不走 IPC)。
|
|
23
|
-
*
|
|
31
|
+
*
|
|
32
|
+
* `msg/group send` 的第二个选项 `--as` 是附件类型(image/video/voice/file),
|
|
33
|
+
* 不是操作者 AID;这两类命令的发送方位于固定的第三个位置参数。其它
|
|
34
|
+
* 支持 `--as <aid>` 的命令继续使用显式参数优先。
|
|
24
35
|
*/
|
|
25
36
|
function resolveSelfAid(args) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
const command = args[0];
|
|
38
|
+
const subcommand = args[1];
|
|
39
|
+
const positionalAid = (command === 'msg' || command === 'group')
|
|
40
|
+
&& subcommand === 'send'
|
|
41
|
+
? args[2]
|
|
42
|
+
: undefined;
|
|
43
|
+
const explicitAid = (command === 'msg' || command === 'group') && subcommand === 'send'
|
|
44
|
+
? undefined
|
|
45
|
+
: getArgValue(args, '--as');
|
|
46
|
+
const runtimeAid = process.env.EVOLCORE_SELF_AID || readTaskRuntimeContextFromEnv()?.selfAid;
|
|
47
|
+
return [positionalAid, explicitAid, runtimeAid].find(isSafeAid);
|
|
29
48
|
}
|
|
30
49
|
/**
|
|
31
50
|
* 记录一条命令执行。同步、best-effort。
|