evolcore 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -9,6 +9,7 @@ import { HANDOFF_SCHEMA_VERSION, HANDOFF_QUERY_DEFAULT_LIMIT, HANDOFF_QUERY_MAX_
|
|
|
9
9
|
import { normalizeCausation } from '../causation/context.js';
|
|
10
10
|
const SAFE_ID_RE = /^[A-Za-z0-9._-]+$/;
|
|
11
11
|
export const HANDOFF_ID_RE = /^h-[A-Za-z0-9._-]+$/;
|
|
12
|
+
const HANDOFF_SNAPSHOT_FILE_RE = /^(h-[A-Za-z0-9._-]+)\.json(?:__?)?$/;
|
|
12
13
|
const TERMINAL_HANDOFF_STATES = new Set(['failed', 'completed', 'discarded']);
|
|
13
14
|
function safeSegment(value, field) {
|
|
14
15
|
if (!value || !SAFE_ID_RE.test(value) || value === '.' || value === '..') {
|
|
@@ -55,13 +56,16 @@ export class HandoffStore {
|
|
|
55
56
|
? path.join(this.rootDir, aid, 'data', 'handoff')
|
|
56
57
|
: path.join(this.rootDir, aid);
|
|
57
58
|
}
|
|
58
|
-
handoffsDir(selfAid) {
|
|
59
|
-
return path.join(this.agentDir(selfAid), 'handoffs');
|
|
60
|
-
}
|
|
61
59
|
instancePath(selfAid, handoffId) {
|
|
62
60
|
if (!HANDOFF_ID_RE.test(handoffId))
|
|
63
61
|
throw new Error('invalid handoff_id');
|
|
64
|
-
return path.join(this.
|
|
62
|
+
return path.join(this.agentDir(selfAid), `${safeSegment(handoffId, 'handoff_id')}.json`);
|
|
63
|
+
}
|
|
64
|
+
/** Read the pre-flat layout during the transition to the flat store. */
|
|
65
|
+
legacyInstancePath(selfAid, handoffId) {
|
|
66
|
+
if (!HANDOFF_ID_RE.test(handoffId))
|
|
67
|
+
throw new Error('invalid handoff_id');
|
|
68
|
+
return path.join(this.agentDir(selfAid), 'handoffs', safeSegment(handoffId, 'handoff_id'), 'handoff.json');
|
|
65
69
|
}
|
|
66
70
|
historyPath(selfAid) {
|
|
67
71
|
return path.join(this.agentDir(selfAid), 'history.jsonl');
|
|
@@ -147,7 +151,8 @@ export class HandoffStore {
|
|
|
147
151
|
get(selfAid, handoffId) {
|
|
148
152
|
if (!HANDOFF_ID_RE.test(handoffId))
|
|
149
153
|
return null;
|
|
150
|
-
const parsed = atomicReadJson(this.instancePath(selfAid, handoffId))
|
|
154
|
+
const parsed = atomicReadJson(this.instancePath(selfAid, handoffId))
|
|
155
|
+
?? atomicReadJson(this.legacyInstancePath(selfAid, handoffId));
|
|
151
156
|
if (!parsed)
|
|
152
157
|
return null;
|
|
153
158
|
if (parsed.schema_version !== HANDOFF_SCHEMA_VERSION || parsed.handoff_id !== handoffId) {
|
|
@@ -156,14 +161,32 @@ export class HandoffStore {
|
|
|
156
161
|
return parsed;
|
|
157
162
|
}
|
|
158
163
|
list(selfAid) {
|
|
159
|
-
const dir = this.
|
|
164
|
+
const dir = this.agentDir(selfAid);
|
|
160
165
|
if (!fs.existsSync(dir))
|
|
161
166
|
return [];
|
|
162
|
-
const
|
|
167
|
+
const handoffIds = new Set();
|
|
163
168
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
164
|
-
if (!entry.
|
|
169
|
+
if (!entry.isFile())
|
|
165
170
|
continue;
|
|
166
|
-
const
|
|
171
|
+
const match = HANDOFF_SNAPSHOT_FILE_RE.exec(entry.name);
|
|
172
|
+
if (match && HANDOFF_ID_RE.test(match[1]))
|
|
173
|
+
handoffIds.add(match[1]);
|
|
174
|
+
}
|
|
175
|
+
// Keep nested snapshots visible; a later flat snapshot takes precedence.
|
|
176
|
+
const legacyDir = path.join(dir, 'handoffs');
|
|
177
|
+
try {
|
|
178
|
+
for (const entry of fs.readdirSync(legacyDir, { withFileTypes: true })) {
|
|
179
|
+
if (entry.isDirectory() && HANDOFF_ID_RE.test(entry.name))
|
|
180
|
+
handoffIds.add(entry.name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code !== 'ENOENT')
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
const result = [];
|
|
188
|
+
for (const handoffId of handoffIds) {
|
|
189
|
+
const instance = this.get(selfAid, handoffId);
|
|
167
190
|
if (instance)
|
|
168
191
|
result.push(instance);
|
|
169
192
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
|
|
2
2
|
import { buildModelRequestHeaders } from '../../agents/request-identity.js';
|
|
3
|
+
import { appendRequestQueryParams } from '../../agents/request-identity.js';
|
|
3
4
|
function apiEndpoint(baseUrl, resource) {
|
|
4
5
|
const configured = baseUrl?.trim();
|
|
5
6
|
if (!configured)
|
|
@@ -11,14 +12,6 @@ function apiEndpoint(baseUrl, resource) {
|
|
|
11
12
|
: `${pathname}/v1/${resource}`;
|
|
12
13
|
return parsed.toString();
|
|
13
14
|
}
|
|
14
|
-
function withQueryParams(url, queryParams) {
|
|
15
|
-
if (!queryParams || Object.keys(queryParams).length === 0)
|
|
16
|
-
return url;
|
|
17
|
-
const parsed = new URL(url);
|
|
18
|
-
for (const [name, value] of Object.entries(queryParams))
|
|
19
|
-
parsed.searchParams.set(name, value);
|
|
20
|
-
return parsed.toString();
|
|
21
|
-
}
|
|
22
15
|
async function parseError(response) {
|
|
23
16
|
let raw = '';
|
|
24
17
|
try {
|
|
@@ -41,7 +34,7 @@ export class AnthropicTextInferenceProvider {
|
|
|
41
34
|
this.config = config;
|
|
42
35
|
}
|
|
43
36
|
async completeText(request) {
|
|
44
|
-
const response = await fetch(
|
|
37
|
+
const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'messages'), this.config.queryParams), {
|
|
45
38
|
method: 'POST',
|
|
46
39
|
signal: request.signal,
|
|
47
40
|
headers: { ...this.headers(), ...this.config.headers },
|
|
@@ -82,7 +75,7 @@ export class OpenAITextInferenceProvider {
|
|
|
82
75
|
this.config = config;
|
|
83
76
|
}
|
|
84
77
|
async completeText(request) {
|
|
85
|
-
const response = await fetch(
|
|
78
|
+
const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'responses'), this.config.queryParams), {
|
|
86
79
|
method: 'POST',
|
|
87
80
|
signal: request.signal,
|
|
88
81
|
headers: {
|
|
@@ -118,20 +111,20 @@ export class OpenAITextInferenceProvider {
|
|
|
118
111
|
export class EcagentTextInferenceProvider {
|
|
119
112
|
config;
|
|
120
113
|
baseagent = 'ecagent';
|
|
121
|
-
constructor(config) {
|
|
114
|
+
constructor(config, agentAid) {
|
|
122
115
|
this.config = config;
|
|
123
116
|
this.config = {
|
|
124
117
|
...config,
|
|
125
118
|
headers: buildModelRequestHeaders({
|
|
126
119
|
baseagent: 'ecagent',
|
|
127
120
|
baseUrl: config.baseUrl,
|
|
128
|
-
agentAid
|
|
121
|
+
agentAid,
|
|
129
122
|
configuredHeaders: config.headers,
|
|
130
123
|
}),
|
|
131
124
|
};
|
|
132
125
|
}
|
|
133
126
|
async completeText(request) {
|
|
134
|
-
const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
|
|
127
|
+
const response = await fetch(appendRequestQueryParams(apiEndpoint(this.config.baseUrl, 'chat/completions'), this.config.queryParams), {
|
|
135
128
|
method: 'POST',
|
|
136
129
|
signal: request.signal,
|
|
137
130
|
headers: {
|
|
@@ -190,10 +183,9 @@ export function createTextInferenceProvider(baseagent, config) {
|
|
|
190
183
|
try {
|
|
191
184
|
const override = {
|
|
192
185
|
...(config.baseagents?.ecagent ?? {}),
|
|
193
|
-
evolcoreAgentAid: config.aid,
|
|
194
186
|
};
|
|
195
187
|
const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
|
|
196
|
-
return new EcagentTextInferenceProvider(resolved);
|
|
188
|
+
return new EcagentTextInferenceProvider(resolved, config.aid);
|
|
197
189
|
}
|
|
198
190
|
catch {
|
|
199
191
|
return undefined;
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { logger } from '../../utils/logger.js';
|
|
2
2
|
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
3
3
|
import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { resolvePaths } from '../../paths.js';
|
|
7
4
|
import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
|
|
8
|
-
let diagStream = null;
|
|
9
|
-
function getDiagStream() {
|
|
10
|
-
if (!diagStream) {
|
|
11
|
-
const logDir = resolvePaths().logs;
|
|
12
|
-
diagStream = fs.createWriteStream(path.join(logDir, 'im-renderer-diag.log'), { flags: 'a' });
|
|
13
|
-
}
|
|
14
|
-
return diagStream;
|
|
15
|
-
}
|
|
16
|
-
function diag(instanceId, action, meta = {}) {
|
|
17
|
-
const line = JSON.stringify({ ts: new Date().toISOString(), id: instanceId, action, ...meta });
|
|
18
|
-
getDiagStream().write(line + '\n');
|
|
19
|
-
}
|
|
20
|
-
let instanceCounter = 0;
|
|
21
5
|
export class IMRenderer {
|
|
22
6
|
opts;
|
|
23
7
|
itemsQueue = [];
|
|
@@ -28,8 +12,6 @@ export class IMRenderer {
|
|
|
28
12
|
sentContent = false;
|
|
29
13
|
flushCount = 0;
|
|
30
14
|
messageTimestamps = [];
|
|
31
|
-
instanceId;
|
|
32
|
-
diagEnabled;
|
|
33
15
|
/** 串行发送队列:保证消息按序到达 */
|
|
34
16
|
sendChain = Promise.resolve();
|
|
35
17
|
/** proactive:是否已发过 text 文本(用于去重 complete.result) */
|
|
@@ -38,16 +20,6 @@ export class IMRenderer {
|
|
|
38
20
|
syntheticCallSeq = 0;
|
|
39
21
|
constructor(opts) {
|
|
40
22
|
this.opts = opts;
|
|
41
|
-
this.diagEnabled = opts.diagEnabled ?? false;
|
|
42
|
-
this.instanceId = `R${++instanceCounter}`;
|
|
43
|
-
if (this.diagEnabled) {
|
|
44
|
-
diag(this.instanceId, 'created', {
|
|
45
|
-
chatmode: opts.envelope.chatmode,
|
|
46
|
-
flushDelay: opts.flushDelay,
|
|
47
|
-
suppressActivityItems: this.suppressActivityItems(),
|
|
48
|
-
suppressIntermediateText: this.suppressIntermediateText(),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
23
|
}
|
|
52
24
|
suppressActivityItems() {
|
|
53
25
|
return this.opts.suppressActivityItems ?? this.opts.suppressActivities ?? false;
|
|
@@ -138,10 +110,14 @@ export class IMRenderer {
|
|
|
138
110
|
this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
|
|
139
111
|
const payload = { kind: 'result.text', text: rawText, isFinal: false };
|
|
140
112
|
this.sentContent = true;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
113
|
+
try {
|
|
114
|
+
await this.sendPayload(payload);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// A failed durable enqueue must remain retryable by the final flush.
|
|
118
|
+
this.textBuffer = rawText + this.textBuffer;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
145
121
|
this.lastFlush = Date.now();
|
|
146
122
|
this.flushCount++;
|
|
147
123
|
}
|
|
@@ -195,13 +171,6 @@ export class IMRenderer {
|
|
|
195
171
|
this.textBuffer += text;
|
|
196
172
|
this.allText += text;
|
|
197
173
|
this.messageTimestamps.push(Date.now());
|
|
198
|
-
if (this.diagEnabled) {
|
|
199
|
-
diag(this.instanceId, 'addText', {
|
|
200
|
-
len: text.length,
|
|
201
|
-
preview: text.substring(0, 60),
|
|
202
|
-
bufLen: this.textBuffer.length,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
174
|
this.scheduleFlush();
|
|
206
175
|
}
|
|
207
176
|
/** 添加工具调用 */
|
|
@@ -219,8 +188,6 @@ export class IMRenderer {
|
|
|
219
188
|
text: descText,
|
|
220
189
|
});
|
|
221
190
|
this.messageTimestamps.push(Date.now());
|
|
222
|
-
if (this.diagEnabled)
|
|
223
|
-
diag(this.instanceId, 'addToolCall', { name, callId });
|
|
224
191
|
this.scheduleFlush();
|
|
225
192
|
}
|
|
226
193
|
/** 添加工具结果 */
|
|
@@ -241,8 +208,6 @@ export class IMRenderer {
|
|
|
241
208
|
...(descText !== undefined && { text: descText }),
|
|
242
209
|
});
|
|
243
210
|
this.messageTimestamps.push(Date.now());
|
|
244
|
-
if (this.diagEnabled)
|
|
245
|
-
diag(this.instanceId, 'addToolResult', { name, ok, callId });
|
|
246
211
|
this.scheduleFlush();
|
|
247
212
|
}
|
|
248
213
|
/** 添加进度提示 */
|
|
@@ -330,10 +295,7 @@ export class IMRenderer {
|
|
|
330
295
|
await this.flushText();
|
|
331
296
|
const payload = { kind: 'result.text', text, isFinal: false };
|
|
332
297
|
this.sentContent = true;
|
|
333
|
-
|
|
334
|
-
.then(() => this.opts.send(payload))
|
|
335
|
-
.catch(e => logger.warn('[IMRenderer] notice text send failed:', e));
|
|
336
|
-
await this.sendChain;
|
|
298
|
+
await this.sendPayload(payload);
|
|
337
299
|
this.lastFlush = Date.now();
|
|
338
300
|
this.flushCount++;
|
|
339
301
|
return true;
|
|
@@ -342,10 +304,48 @@ export class IMRenderer {
|
|
|
342
304
|
synthCallId() {
|
|
343
305
|
return `synth-${this.opts.envelope.taskId}-${++this.syntheticCallSeq}`;
|
|
344
306
|
}
|
|
307
|
+
/** Convert a structured failed receipt into the same error shape as a rejected sender. */
|
|
308
|
+
receiptError(receipt) {
|
|
309
|
+
return Object.assign(new Error(receipt.error), {
|
|
310
|
+
code: receipt.code,
|
|
311
|
+
outboxId: receipt.outboxId,
|
|
312
|
+
operationId: receipt.operationId,
|
|
313
|
+
status: receipt.status,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
isOutboxFull(error) {
|
|
317
|
+
const value = error && typeof error === 'object' ? error : {};
|
|
318
|
+
const cause = value.cause && typeof value.cause === 'object'
|
|
319
|
+
? value.cause
|
|
320
|
+
: undefined;
|
|
321
|
+
return value.code === 'OUTBOX_FULL' || cause?.code === 'OUTBOX_FULL';
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Serialize durable sends while keeping the chain usable after a rejected
|
|
325
|
+
* operation. Activity queue saturation is best-effort: it is logged, but
|
|
326
|
+
* must not prevent the final response from being sent.
|
|
327
|
+
*/
|
|
328
|
+
sendPayload(payload) {
|
|
329
|
+
const operation = this.sendChain.then(async () => {
|
|
330
|
+
const receipt = await this.opts.send(payload);
|
|
331
|
+
if (receipt?.status === 'failed') {
|
|
332
|
+
throw this.receiptError(receipt);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const handled = operation.catch(error => {
|
|
336
|
+
if (payload.kind === 'activity.batch' && this.isOutboxFull(error)) {
|
|
337
|
+
logger.warn(`[IMRenderer] activity outbox full; continuing without activity batch: ${error instanceof Error ? error.message : String(error)}`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
});
|
|
342
|
+
// A failed operation must not poison later sends. The caller still gets
|
|
343
|
+
// the rejection through `handled`, while the internal tail is recovered.
|
|
344
|
+
this.sendChain = handled.catch(() => { });
|
|
345
|
+
return handled;
|
|
346
|
+
}
|
|
345
347
|
scheduleFlush() {
|
|
346
348
|
if (this.timer) {
|
|
347
|
-
if (this.diagEnabled)
|
|
348
|
-
diag(this.instanceId, 'scheduleFlush:skip', { reason: 'timer_exists' });
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
const interval = this.opts.flushDelay ?? DEFAULT_FLUSH_DELAY_MS;
|
|
@@ -364,15 +364,11 @@ export class IMRenderer {
|
|
|
364
364
|
}
|
|
365
365
|
const elapsed = Date.now() - this.lastFlush;
|
|
366
366
|
const delay = Math.max(0, targetDelay - elapsed);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
targetDelay,
|
|
371
|
-
elapsed,
|
|
372
|
-
actualDelay: delay,
|
|
367
|
+
this.timer = setTimeout(() => {
|
|
368
|
+
void this.flushInternal().catch(error => {
|
|
369
|
+
logger.warn(`[IMRenderer] scheduled flush failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
373
370
|
});
|
|
374
|
-
}
|
|
375
|
-
this.timer = setTimeout(() => this.flushInternal(), delay);
|
|
371
|
+
}, delay);
|
|
376
372
|
}
|
|
377
373
|
calculateDynamicDelay(interval) {
|
|
378
374
|
const recent = this.messageTimestamps.slice(-10);
|
|
@@ -402,13 +398,14 @@ export class IMRenderer {
|
|
|
402
398
|
if (this.suppressActivityItems())
|
|
403
399
|
return;
|
|
404
400
|
const payload = { kind: 'activity.batch', items: nonThinking };
|
|
405
|
-
if (this.diagEnabled)
|
|
406
|
-
diag(this.instanceId, 'flushActivitiesOnly', { itemCount: nonThinking.length });
|
|
407
401
|
this.sentContent = true;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
402
|
+
try {
|
|
403
|
+
await this.sendPayload(payload);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
this.itemsQueue = [...nonThinking, ...this.itemsQueue];
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
412
409
|
this.lastFlush = Date.now();
|
|
413
410
|
this.flushCount++;
|
|
414
411
|
}
|
|
@@ -446,24 +443,20 @@ export class IMRenderer {
|
|
|
446
443
|
const finalText = isFinal ? this.textBuffer : '';
|
|
447
444
|
if (isFinal)
|
|
448
445
|
this.textBuffer = '';
|
|
449
|
-
if (this.diagEnabled) {
|
|
450
|
-
diag(this.instanceId, 'flush', {
|
|
451
|
-
isFinal,
|
|
452
|
-
itemCount: items.length,
|
|
453
|
-
finalTextLen: finalText.length,
|
|
454
|
-
flushCount: this.flushCount,
|
|
455
|
-
sinceLastFlush: Date.now() - this.lastFlush,
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
446
|
// 1. interactive 模式下:不发 text items(由 result.text 统一发送最终文本)
|
|
459
447
|
const itemsForBatch = this.suppressActivityItems() ? [] : items.filter(it => it.kind !== 'text');
|
|
460
448
|
if (itemsForBatch.length > 0) {
|
|
461
449
|
const payload = { kind: 'activity.batch', items: itemsForBatch };
|
|
462
450
|
this.sentContent = true;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
451
|
+
try {
|
|
452
|
+
await this.sendPayload(payload);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
this.itemsQueue = [...itemsForBatch, ...this.itemsQueue];
|
|
456
|
+
if (isFinal && finalText)
|
|
457
|
+
this.textBuffer = finalText + this.textBuffer;
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
467
460
|
this.lastFlush = Date.now();
|
|
468
461
|
this.flushCount++;
|
|
469
462
|
}
|
|
@@ -481,10 +474,13 @@ export class IMRenderer {
|
|
|
481
474
|
this.textBuffer = '';
|
|
482
475
|
const payload = { kind: 'result.text', text, isFinal: false };
|
|
483
476
|
this.sentContent = true;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
477
|
+
try {
|
|
478
|
+
await this.sendPayload(payload);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
this.textBuffer = text + this.textBuffer;
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
488
484
|
this.lastFlush = Date.now();
|
|
489
485
|
this.flushCount++;
|
|
490
486
|
}
|
|
@@ -492,10 +488,13 @@ export class IMRenderer {
|
|
|
492
488
|
if (isFinal && finalText.length > 0) {
|
|
493
489
|
const payload = { kind: 'result.text', text: finalText, isFinal: true };
|
|
494
490
|
this.sentContent = true;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
491
|
+
try {
|
|
492
|
+
await this.sendPayload(payload);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
this.textBuffer = finalText + this.textBuffer;
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
499
498
|
this.lastFlush = Date.now();
|
|
500
499
|
this.flushCount++;
|
|
501
500
|
}
|
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { AccessPolicyError, readAccessPolicy, } from '../../config/access-policy.js';
|
|
2
|
-
import { isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
|
|
2
|
+
import { getContactSnapshot, isAcceptedContact, resolveContactView, resolvePrincipal, } from '../../config/contact-book.js';
|
|
3
|
+
import { isValidAid } from '../../aun/aid/validation.js';
|
|
4
|
+
import { expirePendingContactRequests } from '../../config/contact-request-service.js';
|
|
5
|
+
/** Validate managed private AUN sends against the sender's local contact policy. */
|
|
6
|
+
export async function evaluateOutboundContactAdmission(input) {
|
|
7
|
+
const selfAid = String(input.selfAid || '').trim();
|
|
8
|
+
const targetAid = String(input.targetAid || '').trim();
|
|
9
|
+
if (!isValidAid(selfAid) || !isValidAid(targetAid)) {
|
|
10
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: 'A valid Agent AID is required for outbound contact admission' };
|
|
11
|
+
}
|
|
12
|
+
let policy;
|
|
13
|
+
try {
|
|
14
|
+
policy = readAccessPolicy(selfAid);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
18
|
+
}
|
|
19
|
+
if (policy.policyMode !== 'contacts') {
|
|
20
|
+
try {
|
|
21
|
+
if (getContactSnapshot(selfAid).contacts.get(targetAid)?.status === 'blocked') {
|
|
22
|
+
return { allow: false, code: 'CONTACT_BLOCKED', status: 'blocked', error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
27
|
+
}
|
|
28
|
+
return { allow: true, status: policy.policyMode };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await expirePendingContactRequests(selfAid);
|
|
32
|
+
const snapshot = getContactSnapshot(selfAid);
|
|
33
|
+
const entry = snapshot.contacts.get(targetAid);
|
|
34
|
+
const view = resolveContactView(selfAid, targetAid);
|
|
35
|
+
if (view.isOwner)
|
|
36
|
+
return { allow: true, status: 'owner' };
|
|
37
|
+
if (entry && (entry.status === undefined || entry.status === 'active'))
|
|
38
|
+
return { allow: true, status: 'active' };
|
|
39
|
+
const status = entry?.status ?? 'absent';
|
|
40
|
+
if (status === 'blocked')
|
|
41
|
+
return { allow: false, code: 'CONTACT_BLOCKED', status, error: `目标联系人 ${targetAid} 已被拉黑。请先解除拉黑:ec contact unblock ${targetAid} --self ${selfAid}` };
|
|
42
|
+
const addCommand = `ec contact add ${targetAid} --self ${selfAid}`;
|
|
43
|
+
if (status === 'pending')
|
|
44
|
+
return { allow: false, code: 'CONTACT_PENDING', status, error: `目标联系人 ${targetAid} 的联系人申请正在审核中,请等待 Owner 通过;如需重新提交,执行:${addCommand}` };
|
|
45
|
+
if (status === 'declined')
|
|
46
|
+
return { allow: false, code: 'CONTACT_DECLINED', status, error: `目标联系人 ${targetAid} 的联系人申请已拒绝或过期,请重新申请:${addCommand}` };
|
|
47
|
+
return { allow: false, code: 'CONTACT_NOT_ACTIVE', status: 'absent', error: `目标 ${targetAid} 不是联系人,请先申请添加:${addCommand}` };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { allow: false, code: 'CONTACT_POLICY_INVALID', status: 'absent', error: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
3
53
|
export function evaluateInboundAdmissionPreflight(input) {
|
|
4
54
|
const selfAid = String(input.selfAid || '').trim();
|
|
5
55
|
const channelType = String(input.channelType || '').trim().toLowerCase();
|