evolcore 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +123 -42
- package/dist/agents/codex-app-server-client.js +33 -9
- package/dist/agents/codex-runner.js +58 -8
- package/dist/agents/ecagent-runner.js +17 -2
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +131 -128
- package/dist/cli/agent-command.js +16 -9
- package/dist/cli/agent.js +82 -19
- package/dist/cli/daemon-commands.js +21 -2
- package/dist/cli/index.js +76 -61
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/gateway-config.js +26 -10
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/operation-authorizer.js +32 -147
- package/dist/core/auth/operation-catalog.js +80 -0
- package/dist/core/bootstrap-messages.js +50 -0
- package/dist/core/bootstrap-service.js +85 -10
- package/dist/core/channel-loader.js +23 -6
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/menu-handler.js +67 -76
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/data-migration.js +79 -27
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +8 -3
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/message-bridge.js +1 -1
- package/dist/core/message/message-log.js +22 -0
- package/dist/core/message/message-queue.js +19 -4
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +231 -93
- package/dist/core/protected-paths.js +10 -7
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/index.js +262 -84
- package/dist/ipc.js +29 -11
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/rules/01-overview.md +9 -0
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +17 -0
- package/kits/templates/roles/visitor.json +8 -0
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +2 -1
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
|
@@ -10,6 +10,7 @@ import { normalizePermissionMode } from '../core/permission/mode.js';
|
|
|
10
10
|
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
12
12
|
import { resolveEcagentConfig } from './baseagent.js';
|
|
13
|
+
import { buildModelRequestHeaders } from './request-identity.js';
|
|
13
14
|
const PROVIDER_ID = 'evolcore-gateway';
|
|
14
15
|
const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
|
|
15
16
|
Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
|
|
@@ -252,6 +253,7 @@ function createOpenAiTransport(config) {
|
|
|
252
253
|
authorization: `Bearer ${config.apiKey}`,
|
|
253
254
|
'content-type': 'application/json',
|
|
254
255
|
...Object.fromEntries(Object.entries(options?.headers ?? {}).filter((entry) => entry[1] !== null)),
|
|
256
|
+
...config.headers,
|
|
255
257
|
},
|
|
256
258
|
body: JSON.stringify(payload),
|
|
257
259
|
signal: controller.signal,
|
|
@@ -678,7 +680,15 @@ export class EcagentRunner {
|
|
|
678
680
|
onCompactStart;
|
|
679
681
|
modelCache;
|
|
680
682
|
constructor(config, callbacks) {
|
|
681
|
-
this.config =
|
|
683
|
+
this.config = {
|
|
684
|
+
...config,
|
|
685
|
+
headers: buildModelRequestHeaders({
|
|
686
|
+
baseagent: 'ecagent',
|
|
687
|
+
baseUrl: config.baseUrl,
|
|
688
|
+
agentAid: config.evolcoreAgentAid,
|
|
689
|
+
configuredHeaders: config.headers,
|
|
690
|
+
}),
|
|
691
|
+
};
|
|
682
692
|
this.model = config.model;
|
|
683
693
|
this.effort = config.effort;
|
|
684
694
|
this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
|
|
@@ -714,7 +724,11 @@ export class EcagentRunner {
|
|
|
714
724
|
try {
|
|
715
725
|
const signal = AbortSignal.timeout(MODEL_LIST_TIMEOUT_MS);
|
|
716
726
|
const response = await fetch(`${this.config.baseUrl.replace(/\/+$/, '')}/models`, {
|
|
717
|
-
headers: {
|
|
727
|
+
headers: {
|
|
728
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
729
|
+
...this.config.headers,
|
|
730
|
+
},
|
|
731
|
+
signal,
|
|
718
732
|
});
|
|
719
733
|
if (!response.ok)
|
|
720
734
|
throw new Error(`Failed to list ecagent models: HTTP ${response.status}`);
|
|
@@ -948,6 +962,7 @@ export class EcagentRunner {
|
|
|
948
962
|
return { block: true, reason: policy.reason || '当前会话策略拒绝此工具调用' };
|
|
949
963
|
const preflight = evaluateToolPreflight(toolName, input, {
|
|
950
964
|
sessionId,
|
|
965
|
+
selfAid: permissionContext?.selfAid,
|
|
951
966
|
channel: permissionContext?.channel,
|
|
952
967
|
userId: permissionContext?.userId,
|
|
953
968
|
role: permissionContext?.role,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
3
|
+
export const EVOLCORE_REQUEST_HEADERS = {
|
|
4
|
+
version: 'X-EvolCore-Version',
|
|
5
|
+
agentAid: 'X-EvolCore-Agent-AID',
|
|
6
|
+
baseagent: 'X-EvolCore-Base-Agent',
|
|
7
|
+
};
|
|
8
|
+
const RESERVED_HEADER_NAMES = new Set(Object.values(EVOLCORE_REQUEST_HEADERS).map(name => name.toLowerCase()));
|
|
9
|
+
const warnedReservedHeaders = new Set();
|
|
10
|
+
function officialHostname(baseagent) {
|
|
11
|
+
return baseagent === 'claude' ? 'api.anthropic.com' : 'api.openai.com';
|
|
12
|
+
}
|
|
13
|
+
export function isOfficialModelEndpoint(baseagent, baseUrl) {
|
|
14
|
+
const configured = baseUrl?.trim();
|
|
15
|
+
if (!configured)
|
|
16
|
+
return true;
|
|
17
|
+
try {
|
|
18
|
+
return new URL(configured).hostname.toLowerCase() === officialHostname(baseagent);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Invalid URLs fail at the request layer. Treat them as configured here so
|
|
22
|
+
// identity behavior does not silently change if URL validation is relaxed.
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function configuredHeadersWithoutReserved(baseagent, configuredHeaders) {
|
|
27
|
+
const headers = {};
|
|
28
|
+
for (const [name, value] of Object.entries(configuredHeaders ?? {})) {
|
|
29
|
+
const normalizedName = name.toLowerCase();
|
|
30
|
+
if (!RESERVED_HEADER_NAMES.has(normalizedName)) {
|
|
31
|
+
headers[name] = value;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const warningKey = `${baseagent}:${normalizedName}`;
|
|
35
|
+
if (!warnedReservedHeaders.has(warningKey)) {
|
|
36
|
+
warnedReservedHeaders.add(warningKey);
|
|
37
|
+
logger.warn(`[RequestIdentity] Ignoring configured reserved header ${JSON.stringify(name)} for ${baseagent}; EvolCore manages this header at runtime`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
export function buildModelRequestHeaders(input) {
|
|
43
|
+
const headers = configuredHeadersWithoutReserved(input.baseagent, input.configuredHeaders);
|
|
44
|
+
if (isOfficialModelEndpoint(input.baseagent, input.baseUrl))
|
|
45
|
+
return headers;
|
|
46
|
+
headers[EVOLCORE_REQUEST_HEADERS.version] = readInstalledEvolcoreVersion();
|
|
47
|
+
const agentAid = input.agentAid?.trim();
|
|
48
|
+
if (agentAid)
|
|
49
|
+
headers[EVOLCORE_REQUEST_HEADERS.agentAid] = agentAid;
|
|
50
|
+
headers[EVOLCORE_REQUEST_HEADERS.baseagent] = input.baseagent;
|
|
51
|
+
return headers;
|
|
52
|
+
}
|
|
53
|
+
export function _resetRequestIdentityWarningsForTests() {
|
|
54
|
+
warnedReservedHeaders.clear();
|
|
55
|
+
}
|
package/dist/aun/outbox.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
1
|
import crypto from 'crypto';
|
|
4
2
|
import { agentOutboxPath } from '../paths.js';
|
|
3
|
+
import { atomicRead, atomicWrite } from '../utils/atomic-write.js';
|
|
5
4
|
const MAX_ENTRIES_PER_AID = 20;
|
|
6
5
|
const DEFAULT_TTL = 300_000; // 5 minutes
|
|
7
6
|
function outboxFile(aid) {
|
|
@@ -13,14 +12,14 @@ function generateId() {
|
|
|
13
12
|
return `out-${ts}-${rand}`;
|
|
14
13
|
}
|
|
15
14
|
function isExpired(entry) {
|
|
15
|
+
if (entry.critical)
|
|
16
|
+
return false;
|
|
16
17
|
return Date.now() - entry.ts > entry.ttl;
|
|
17
18
|
}
|
|
18
19
|
function readEntries(aid) {
|
|
19
20
|
const file = outboxFile(aid);
|
|
20
|
-
if (!fs.existsSync(file))
|
|
21
|
-
return [];
|
|
22
21
|
try {
|
|
23
|
-
const content =
|
|
22
|
+
const content = atomicRead(file)?.trim();
|
|
24
23
|
if (!content)
|
|
25
24
|
return [];
|
|
26
25
|
return content.split('\n').map(line => {
|
|
@@ -38,22 +37,25 @@ function readEntries(aid) {
|
|
|
38
37
|
}
|
|
39
38
|
function writeEntries(aid, entries) {
|
|
40
39
|
const file = outboxFile(aid);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
catch { }
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
49
|
-
fs.writeFileSync(file, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
40
|
+
const content = entries.length > 0
|
|
41
|
+
? entries.map(e => JSON.stringify(e)).join('\n') + '\n'
|
|
42
|
+
: '';
|
|
43
|
+
atomicWrite(file, content);
|
|
50
44
|
}
|
|
51
45
|
export function enqueue(aid, opts) {
|
|
46
|
+
const existingEntries = readEntries(aid);
|
|
47
|
+
if (opts.dedupeKey) {
|
|
48
|
+
const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey && !isExpired(entry));
|
|
49
|
+
if (existing)
|
|
50
|
+
return existing;
|
|
51
|
+
}
|
|
52
52
|
const entry = {
|
|
53
53
|
id: generateId(),
|
|
54
54
|
ts: Date.now(),
|
|
55
55
|
aid,
|
|
56
56
|
channelId: opts.channelId,
|
|
57
|
+
dedupeKey: opts.dedupeKey,
|
|
58
|
+
critical: opts.critical,
|
|
57
59
|
type: opts.type,
|
|
58
60
|
contentKind: opts.contentKind,
|
|
59
61
|
payload: opts.payload,
|
|
@@ -65,17 +67,18 @@ export function enqueue(aid, opts) {
|
|
|
65
67
|
ttl: opts.ttl ?? DEFAULT_TTL,
|
|
66
68
|
postSend: opts.postSend,
|
|
67
69
|
};
|
|
68
|
-
const file = outboxFile(aid);
|
|
69
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
70
|
// Enforce cap: read existing, drop oldest if over limit
|
|
71
|
-
let entries =
|
|
72
|
-
if (entries.length >= MAX_ENTRIES_PER_AID) {
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
let entries = existingEntries;
|
|
72
|
+
if (entries.filter(candidate => !candidate.critical).length >= MAX_ENTRIES_PER_AID) {
|
|
73
|
+
const dropIndex = entries.findIndex(candidate => !candidate.critical);
|
|
74
|
+
if (dropIndex >= 0)
|
|
75
|
+
entries.splice(dropIndex, 1);
|
|
76
|
+
entries = [...entries, entry];
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
|
-
|
|
79
|
+
entries = [...entries, entry];
|
|
78
80
|
}
|
|
81
|
+
writeEntries(aid, entries);
|
|
79
82
|
return entry;
|
|
80
83
|
}
|
|
81
84
|
export function remove(aid, id) {
|
|
@@ -105,6 +108,9 @@ export function removeInteractionCards(aid, requestId) {
|
|
|
105
108
|
export function load(aid) {
|
|
106
109
|
return readEntries(aid).filter(e => !isExpired(e));
|
|
107
110
|
}
|
|
111
|
+
export function findByDedupeKey(aid, dedupeKey) {
|
|
112
|
+
return load(aid).find(entry => entry.dedupeKey === dedupeKey);
|
|
113
|
+
}
|
|
108
114
|
export function cleanup(aid) {
|
|
109
115
|
const all = readEntries(aid);
|
|
110
116
|
const valid = all.filter(e => !isExpired(e));
|
|
@@ -185,16 +191,7 @@ export async function drain(aid, sender) {
|
|
|
185
191
|
}
|
|
186
192
|
}
|
|
187
193
|
export function hasPending(aid) {
|
|
188
|
-
|
|
189
|
-
if (!fs.existsSync(file))
|
|
190
|
-
return false;
|
|
191
|
-
try {
|
|
192
|
-
const stat = fs.statSync(file);
|
|
193
|
-
return stat.size > 0;
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
194
|
+
return readEntries(aid).some(entry => !isExpired(entry));
|
|
198
195
|
}
|
|
199
196
|
/**
|
|
200
197
|
* 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
|
package/dist/channels/aun.js
CHANGED
|
@@ -7,17 +7,18 @@ import { logger, localTimestamp } from '../utils/logger.js';
|
|
|
7
7
|
import { LogWriter } from '../utils/log-writer.js';
|
|
8
8
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
9
9
|
import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
|
|
10
|
-
import { resolvePaths,
|
|
10
|
+
import { resolvePaths, agentDir as agentDirPath, resolveRoot } from '../paths.js';
|
|
11
11
|
import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
|
|
12
12
|
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
13
|
-
import { appendMessageLog, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog } from '../core/message/message-log.js';
|
|
13
|
+
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
14
14
|
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
15
15
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
16
16
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
17
17
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
18
18
|
import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
19
19
|
import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
|
|
20
|
-
import { loadAgent
|
|
20
|
+
import { loadAgent } from '../config-store.js';
|
|
21
|
+
import { normalizeAgentLifecycle } from '../config/lifecycle.js';
|
|
21
22
|
import { resolveEffective } from '../config/config-manager.js';
|
|
22
23
|
import { isManagementRole } from '../config/builtin-roles.js';
|
|
23
24
|
import { mentionModeToDispatch } from '../config/mention-mode.js';
|
|
@@ -33,6 +34,7 @@ import { recordCausationSpan } from '../core/causation/audit.js';
|
|
|
33
34
|
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
34
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
35
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
+
import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
|
|
36
38
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
37
39
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
38
40
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
@@ -200,7 +202,6 @@ export class AUNChannel {
|
|
|
200
202
|
connected = false;
|
|
201
203
|
traceWriter = null;
|
|
202
204
|
eventBus = null;
|
|
203
|
-
ownerBoundHandler = null;
|
|
204
205
|
queuedHandler = null;
|
|
205
206
|
pendingEchoMessages = new Map();
|
|
206
207
|
isEchoSending = false;
|
|
@@ -833,9 +834,9 @@ export class AUNChannel {
|
|
|
833
834
|
baseName: 'aun',
|
|
834
835
|
logDir: resolvePaths().logs,
|
|
835
836
|
rotation: 'hourly',
|
|
836
|
-
retention: { hours:
|
|
837
|
+
retention: { hours: 24 },
|
|
837
838
|
});
|
|
838
|
-
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation,
|
|
839
|
+
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 24h retention): ${this.traceWriter.activePath()}`);
|
|
839
840
|
}
|
|
840
841
|
this.aidState = {
|
|
841
842
|
aid: config.aid,
|
|
@@ -1083,11 +1084,6 @@ export class AUNChannel {
|
|
|
1083
1084
|
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
1084
1085
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
1085
1086
|
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
1086
|
-
// Send welcome message to owner after first connection
|
|
1087
|
-
// pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
|
|
1088
|
-
if (!this.config.pureIdentity) {
|
|
1089
|
-
await this.sendWelcomeMessage();
|
|
1090
|
-
}
|
|
1091
1087
|
}
|
|
1092
1088
|
catch (e) {
|
|
1093
1089
|
this.trace('OUT', 'client.connect.error', { error: String(e) });
|
|
@@ -1097,122 +1093,81 @@ export class AUNChannel {
|
|
|
1097
1093
|
throw e;
|
|
1098
1094
|
}
|
|
1099
1095
|
}
|
|
1100
|
-
async
|
|
1096
|
+
async preparePostBootstrapWelcome() {
|
|
1101
1097
|
try {
|
|
1102
1098
|
const aid = this.config.aid;
|
|
1103
1099
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
1104
|
-
// Read initialized + owners from per-agent config.json
|
|
1105
|
-
// (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
|
|
1106
|
-
// channel 启动时的快照,不会自动同步)
|
|
1107
1100
|
const agentConfig = loadAgent(aidName);
|
|
1108
1101
|
if (!agentConfig) {
|
|
1109
1102
|
logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
if (agentConfig.initialized === true) {
|
|
1113
|
-
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
1114
|
-
return;
|
|
1103
|
+
return false;
|
|
1115
1104
|
}
|
|
1116
1105
|
const owner = getFirstStaticAgentOwner(aidName);
|
|
1117
1106
|
if (!owner) {
|
|
1118
|
-
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message
|
|
1119
|
-
return;
|
|
1107
|
+
logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
|
|
1108
|
+
return false;
|
|
1120
1109
|
}
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// Fetch owner's agent.md to derive name and validate type
|
|
1126
|
-
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
1127
|
-
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
1128
|
-
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
|
|
1110
|
+
const operationId = postBootstrapWelcomeOperationId(aidName);
|
|
1111
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
|
|
1112
|
+
if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
|
|
1113
|
+
return true;
|
|
1129
1114
|
}
|
|
1130
|
-
// Name: prefer existing agent.md name if user has customized it,
|
|
1131
|
-
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
1132
1115
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
1133
|
-
|
|
1134
|
-
const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
1135
|
-
const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
|
|
1136
|
-
const aidLabel = aidName.split('.')[0];
|
|
1137
|
-
let agentDisplayName;
|
|
1138
|
-
if (currentName && currentName !== aidLabel) {
|
|
1139
|
-
agentDisplayName = currentName;
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
|
|
1143
|
-
}
|
|
1144
|
-
// Preserve user-provided description (from `agent new --description`), fallback to default
|
|
1145
|
-
const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
|
|
1146
|
-
const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
|
|
1147
|
-
const agentDescription = currentDesc
|
|
1148
|
-
? currentDesc
|
|
1149
|
-
: 'EvolCore AI Agent Gateway - 连接 Claude/Codex 到消息通道';
|
|
1150
|
-
// Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
|
|
1151
|
-
const newAgentMd = `---
|
|
1152
|
-
aid: "${aid}"
|
|
1153
|
-
name: "${agentDisplayName}"
|
|
1154
|
-
type: "codeagent"
|
|
1155
|
-
version: "1.0.0"
|
|
1156
|
-
description: "${agentDescription}"
|
|
1157
|
-
tags:
|
|
1158
|
-
- evolcore
|
|
1159
|
-
- ai-agent
|
|
1160
|
-
- gateway
|
|
1161
|
-
---
|
|
1162
|
-
|
|
1163
|
-
# ${agentDisplayName}
|
|
1164
|
-
|
|
1165
|
-
EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
1166
|
-
`;
|
|
1167
|
-
// Write locally and publish to AUN network (auto-sign)
|
|
1116
|
+
let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
1168
1117
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
catch (e) {
|
|
1174
|
-
logger.warn(`${this.logPrefix()} Failed to publish agent.md: ${e}`);
|
|
1175
|
-
}
|
|
1176
|
-
// Send welcome message
|
|
1177
|
-
const { generateWelcomeMessage } = await import('../utils/welcome.js');
|
|
1178
|
-
const welcomeText = generateWelcomeMessage({
|
|
1179
|
-
channelType: 'aun',
|
|
1180
|
-
agentName: agentDisplayName,
|
|
1181
|
-
ownerName: ownerDisplayName,
|
|
1182
|
-
includeBindingNote: true,
|
|
1183
|
-
});
|
|
1184
|
-
// First contact with Owner races against Owner's async cert fetch from
|
|
1185
|
-
// gateway PKI; a 3s pause lets the cert propagate. persist_required asks
|
|
1186
|
-
// the gateway to durably store the message so Owner can recover it via
|
|
1187
|
-
// pull if the initial E2EE push still arrives before the cert resolves.
|
|
1188
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1189
|
-
if (!this.client) {
|
|
1190
|
-
logger.warn(`${this.logPrefix()} Client disconnected before welcome message could be sent`);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
await this.callAndTrace('message.send', {
|
|
1194
|
-
to: owner,
|
|
1195
|
-
payload: { type: 'text', text: welcomeText },
|
|
1196
|
-
encrypt: this.shouldEncrypt(owner),
|
|
1197
|
-
persist_required: true,
|
|
1198
|
-
});
|
|
1199
|
-
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
1200
|
-
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
1201
|
-
try {
|
|
1202
|
-
const fresh = loadAgent(aidName);
|
|
1203
|
-
if (fresh) {
|
|
1204
|
-
fresh.initialized = true;
|
|
1205
|
-
saveAgent(fresh);
|
|
1206
|
-
logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
|
|
1118
|
+
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
1119
|
+
ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
|
|
1120
|
+
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
1121
|
+
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
|
|
1207
1122
|
}
|
|
1208
1123
|
}
|
|
1209
1124
|
catch (e) {
|
|
1210
|
-
logger.warn(`${this.logPrefix()} Failed to
|
|
1125
|
+
logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
|
|
1211
1126
|
}
|
|
1127
|
+
preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
|
|
1128
|
+
logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
|
|
1129
|
+
return true;
|
|
1212
1130
|
}
|
|
1213
1131
|
catch (e) {
|
|
1214
|
-
logger.warn(`${this.logPrefix()} Failed to
|
|
1132
|
+
logger.warn(`${this.logPrefix()} Failed to prepare post-bootstrap welcome message: ${e}`);
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async sendPostBootstrapWelcome() {
|
|
1137
|
+
if (!await this.preparePostBootstrapWelcome())
|
|
1138
|
+
return false;
|
|
1139
|
+
return this.reconcilePostBootstrapWelcome();
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Resume delivery of an already prepared completion welcome.
|
|
1143
|
+
*
|
|
1144
|
+
* Unlike sendPostBootstrapWelcome(), this method never creates a new
|
|
1145
|
+
* operation. That distinction keeps repeated `ec agent ready` calls on an
|
|
1146
|
+
* already-active agent from manufacturing another welcome message.
|
|
1147
|
+
*/
|
|
1148
|
+
async reconcilePostBootstrapWelcome() {
|
|
1149
|
+
const configuredAid = this.config.aid;
|
|
1150
|
+
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
1151
|
+
const operationId = postBootstrapWelcomeOperationId(aid);
|
|
1152
|
+
const entry = outbox.findByDedupeKey(aid, operationId);
|
|
1153
|
+
if (!entry) {
|
|
1154
|
+
const owner = getFirstStaticAgentOwner(aid);
|
|
1155
|
+
if (!owner)
|
|
1156
|
+
return false;
|
|
1157
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
1158
|
+
return hasMessageLogOperation(chatDir, operationId);
|
|
1215
1159
|
}
|
|
1160
|
+
const agentConfig = loadAgent(aid);
|
|
1161
|
+
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
|
|
1162
|
+
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
if (!this.connected || !this.client)
|
|
1166
|
+
return true;
|
|
1167
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
1168
|
+
if (sent)
|
|
1169
|
+
outbox.remove(aid, entry.id);
|
|
1170
|
+
return true;
|
|
1216
1171
|
}
|
|
1217
1172
|
// ── Event handlers ──────────────────────────────────────────
|
|
1218
1173
|
/**
|
|
@@ -2445,31 +2400,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2445
2400
|
// ── Public API (same interface as before) ───────────────────
|
|
2446
2401
|
setEventBus(bus) {
|
|
2447
2402
|
// 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
|
|
2448
|
-
if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
2449
|
-
this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
|
|
2450
|
-
}
|
|
2451
2403
|
if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
2452
2404
|
this.eventBus.unsubscribe('task:queued', this.queuedHandler);
|
|
2453
2405
|
}
|
|
2454
|
-
this.ownerBoundHandler = null;
|
|
2455
2406
|
this.queuedHandler = null;
|
|
2456
2407
|
this.eventBus = bus;
|
|
2457
2408
|
if (bus && typeof bus.subscribe === 'function') {
|
|
2458
|
-
const handler = (event) => {
|
|
2459
|
-
if (event.channelName !== this.config.channelName)
|
|
2460
|
-
return;
|
|
2461
|
-
// sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
|
|
2462
|
-
// 自身做 client 健康检查后再发
|
|
2463
|
-
if (!this.client) {
|
|
2464
|
-
logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
this.sendWelcomeMessage().catch(e => {
|
|
2468
|
-
logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
|
|
2469
|
-
});
|
|
2470
|
-
};
|
|
2471
|
-
bus.subscribe('channel:owner-bound', handler);
|
|
2472
|
-
this.ownerBoundHandler = handler;
|
|
2473
2409
|
const queuedHandler = (event) => {
|
|
2474
2410
|
if (event.channel !== this.config.channelName)
|
|
2475
2411
|
return;
|
|
@@ -2928,12 +2864,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2928
2864
|
finalText = `@${context.peerId} ` + finalText;
|
|
2929
2865
|
}
|
|
2930
2866
|
}
|
|
2867
|
+
const operationId = typeof context?.metadata?.operationId === 'string'
|
|
2868
|
+
? context.metadata.operationId
|
|
2869
|
+
: undefined;
|
|
2870
|
+
if (operationId) {
|
|
2871
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
2872
|
+
if (hasMessageLogOperation(chatDir, operationId))
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2931
2875
|
// Write-ahead: persist to outbox before attempting send
|
|
2932
2876
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2933
2877
|
channelId,
|
|
2878
|
+
dedupeKey: operationId,
|
|
2879
|
+
critical: context?.metadata?.criticalDelivery === true,
|
|
2934
2880
|
type: 'text',
|
|
2935
2881
|
text: finalText,
|
|
2936
2882
|
context,
|
|
2883
|
+
ttl: typeof context?.metadata?.outboxTtl === 'number'
|
|
2884
|
+
? context.metadata.outboxTtl
|
|
2885
|
+
: undefined,
|
|
2937
2886
|
});
|
|
2938
2887
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
2939
2888
|
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
@@ -3187,6 +3136,20 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3187
3136
|
const channelId = entry.channelId;
|
|
3188
3137
|
const finalText = entry.text;
|
|
3189
3138
|
const context = entry.context;
|
|
3139
|
+
const operationId = entry.dedupeKey
|
|
3140
|
+
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
3141
|
+
if (operationId) {
|
|
3142
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3143
|
+
if (hasMessageLogOperation(chatDir, operationId)) {
|
|
3144
|
+
logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
|
|
3145
|
+
return true;
|
|
3146
|
+
}
|
|
3147
|
+
if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
|
|
3148
|
+
const agentConfig = loadAgent(this.config.aid);
|
|
3149
|
+
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active')
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3190
3153
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
3191
3154
|
const source = context?.metadata?.source ?? 'daemon';
|
|
3192
3155
|
const payload = { type: 'text', text: finalText };
|
|
@@ -3215,11 +3178,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3215
3178
|
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
|
|
3216
3179
|
const isGroup = this.isGroupId(channelId);
|
|
3217
3180
|
const targetAid = channelId;
|
|
3181
|
+
if (operationId && entry.deliveryReceipt) {
|
|
3182
|
+
this.appendOutboundJsonl(channelId, {
|
|
3183
|
+
...classifyAunPayloadForLog(payload),
|
|
3184
|
+
msgId: entry.deliveryReceipt.messageId,
|
|
3185
|
+
encrypt: entry.deliveryReceipt.encrypt,
|
|
3186
|
+
context,
|
|
3187
|
+
isGroup,
|
|
3188
|
+
source,
|
|
3189
|
+
transport: entry.deliveryReceipt.transport,
|
|
3190
|
+
});
|
|
3191
|
+
return true;
|
|
3192
|
+
}
|
|
3218
3193
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
3219
3194
|
const encrypt = context?.metadata?.encrypted != null
|
|
3220
3195
|
? !!(context.metadata.encrypted)
|
|
3221
3196
|
: this.shouldEncrypt(encryptTarget);
|
|
3222
3197
|
const params = { payload, encrypt };
|
|
3198
|
+
if (context?.metadata?.persistRequired === true)
|
|
3199
|
+
params.persist_required = true;
|
|
3223
3200
|
try {
|
|
3224
3201
|
if (isGroup) {
|
|
3225
3202
|
params.group_id = channelId;
|
|
@@ -3237,6 +3214,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3237
3214
|
}
|
|
3238
3215
|
else {
|
|
3239
3216
|
this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
|
|
3217
|
+
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
3240
3218
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
3241
3219
|
const statsContext = await this.groupStatsContext(channelId);
|
|
3242
3220
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
|
|
@@ -3257,6 +3235,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3257
3235
|
}
|
|
3258
3236
|
else {
|
|
3259
3237
|
this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
|
|
3238
|
+
this.checkpointTextDelivery(entry, result.message_id, encrypt, result);
|
|
3260
3239
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3261
3240
|
if (causation) {
|
|
3262
3241
|
registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
|
|
@@ -3292,6 +3271,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3292
3271
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3293
3272
|
return false;
|
|
3294
3273
|
}
|
|
3274
|
+
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3295
3275
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
3296
3276
|
const statsContext = await this.groupStatsContext(channelId);
|
|
3297
3277
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode, 'send', statsContext);
|
|
@@ -3310,6 +3290,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3310
3290
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3311
3291
|
return false;
|
|
3312
3292
|
}
|
|
3293
|
+
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3313
3294
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3314
3295
|
if (causation) {
|
|
3315
3296
|
registerAunCausation(mid, this.config.aid, targetAid, causation);
|
|
@@ -3341,6 +3322,18 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3341
3322
|
}
|
|
3342
3323
|
}
|
|
3343
3324
|
}
|
|
3325
|
+
checkpointTextDelivery(entry, messageId, encrypt, result) {
|
|
3326
|
+
if (!entry.dedupeKey)
|
|
3327
|
+
return;
|
|
3328
|
+
entry.deliveryReceipt = {
|
|
3329
|
+
messageId,
|
|
3330
|
+
encrypt,
|
|
3331
|
+
transport: this.sendReceiptFromResult(result),
|
|
3332
|
+
};
|
|
3333
|
+
if (!outbox.replace(this.config.aid, entry)) {
|
|
3334
|
+
logger.warn(`${this.logPrefix()} Failed to checkpoint durable operation receipt: ${entry.dedupeKey}`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3344
3337
|
async deliverPayloadEntry(entry) {
|
|
3345
3338
|
const interactionId = entry.postSend?.type === 'register_interaction_card'
|
|
3346
3339
|
? entry.postSend.requestId
|
|
@@ -3371,13 +3364,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3371
3364
|
}
|
|
3372
3365
|
/** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
|
|
3373
3366
|
appendOutboundJsonl(channelId, descriptor) {
|
|
3367
|
+
const operationId = typeof descriptor.context?.metadata?.operationId === 'string'
|
|
3368
|
+
? descriptor.context.metadata.operationId
|
|
3369
|
+
: undefined;
|
|
3374
3370
|
try {
|
|
3375
3371
|
const { content, msgType, payloadType, payloadSummary, msgId, encrypt, transport, context, isGroup, source = 'daemon' } = descriptor;
|
|
3376
3372
|
const sessionsDir = resolvePaths().sessionsDir;
|
|
3377
3373
|
const selfAID = this.config.aid;
|
|
3378
3374
|
const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
|
|
3379
3375
|
const chatmode = context?.metadata?.chatmode;
|
|
3380
|
-
|
|
3376
|
+
const entry = buildOutboundEntry({
|
|
3381
3377
|
from: selfAID,
|
|
3382
3378
|
to: channelId,
|
|
3383
3379
|
sessionId: context?.sessionId,
|
|
@@ -3397,9 +3393,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3397
3393
|
payloadType,
|
|
3398
3394
|
payloadSummary,
|
|
3399
3395
|
source,
|
|
3400
|
-
|
|
3396
|
+
operationId,
|
|
3397
|
+
});
|
|
3398
|
+
if (operationId)
|
|
3399
|
+
appendMessageLogStrict(chatDir, entry);
|
|
3400
|
+
else
|
|
3401
|
+
appendMessageLog(chatDir, entry);
|
|
3401
3402
|
}
|
|
3402
3403
|
catch (e) {
|
|
3404
|
+
if (operationId)
|
|
3405
|
+
throw e;
|
|
3403
3406
|
logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
|
|
3404
3407
|
}
|
|
3405
3408
|
}
|