evolcore 0.0.2 → 0.0.3
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 +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -719,7 +719,7 @@ export class AgentRunner {
|
|
|
719
719
|
if (!permCtx?.adapter || !permCtx?.channelId) {
|
|
720
720
|
return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
|
|
721
721
|
}
|
|
722
|
-
const adapterHasInteractionPath =
|
|
722
|
+
const adapterHasInteractionPath = permCtx.adapter.capabilities?.interaction === true;
|
|
723
723
|
if (!adapterHasInteractionPath || !permCtx.interactionRouter) {
|
|
724
724
|
return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
|
|
725
725
|
}
|
|
@@ -756,6 +756,7 @@ export class AgentRunner {
|
|
|
756
756
|
},
|
|
757
757
|
channelId: permCtx.channelId,
|
|
758
758
|
sessionId,
|
|
759
|
+
initiatorId: permCtx.userId,
|
|
759
760
|
};
|
|
760
761
|
}
|
|
761
762
|
else {
|
|
@@ -783,6 +784,7 @@ export class AgentRunner {
|
|
|
783
784
|
},
|
|
784
785
|
channelId: permCtx.channelId,
|
|
785
786
|
sessionId,
|
|
787
|
+
initiatorId: permCtx.userId,
|
|
786
788
|
};
|
|
787
789
|
}
|
|
788
790
|
let cardSent = false;
|
|
@@ -797,7 +799,10 @@ export class AgentRunner {
|
|
|
797
799
|
replyContext: permCtx.replyContext,
|
|
798
800
|
});
|
|
799
801
|
const optionLines = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
|
|
800
|
-
const
|
|
802
|
+
const answerHint = q.multiSelect
|
|
803
|
+
? '回复 /ask 1,2(多选用逗号分隔),或 /ask <自定义内容>'
|
|
804
|
+
: '回复 /ask 1,或 /ask <自定义内容>';
|
|
805
|
+
const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
|
|
801
806
|
const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
|
|
802
807
|
cardSent = !!result;
|
|
803
808
|
}
|
|
@@ -958,7 +963,9 @@ export class AgentRunner {
|
|
|
958
963
|
command: 'ask',
|
|
959
964
|
buttonArgMap: Object.fromEntries(q.options.map((_, i) => [`opt-${i}`, String(i + 1)])),
|
|
960
965
|
acceptFreeText: true,
|
|
961
|
-
freeTextHint:
|
|
966
|
+
freeTextHint: q.multiSelect
|
|
967
|
+
? '多选请回复 /ask 1,2;也可回复 /ask <自定义内容>'
|
|
968
|
+
: '或回复 /ask <自定义内容>',
|
|
962
969
|
},
|
|
963
970
|
};
|
|
964
971
|
await sendPrompt(renderActionAsText(interaction));
|
|
@@ -990,12 +997,25 @@ export class AgentRunner {
|
|
|
990
997
|
finish({ kind: 'cancelled', reason: 'aborted' });
|
|
991
998
|
};
|
|
992
999
|
permCtx.interactionRouter.register(requestId, sessionId, (action) => {
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
1000
|
+
const answer = action.trim();
|
|
1001
|
+
const selectionParts = answer.split(/[,,]/).map(part => part.trim());
|
|
1002
|
+
const selectedIndexes = selectionParts.map(part => Number(part));
|
|
1003
|
+
const hasValidNumericSelection = selectionParts.length > 0
|
|
1004
|
+
&& selectionParts.every((part, index) => (/^\d+$/.test(part)
|
|
1005
|
+
&& Number.isInteger(selectedIndexes[index])
|
|
1006
|
+
&& selectedIndexes[index] >= 1
|
|
1007
|
+
&& selectedIndexes[index] <= q.options.length));
|
|
1008
|
+
if (q.multiSelect && hasValidNumericSelection) {
|
|
1009
|
+
finish({
|
|
1010
|
+
kind: 'answered',
|
|
1011
|
+
value: [...new Set(selectedIndexes)].map(num => q.options[num - 1].label),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
else if (!q.multiSelect && hasValidNumericSelection && selectedIndexes.length === 1) {
|
|
1015
|
+
finish({ kind: 'answered', value: q.options[selectedIndexes[0] - 1].label });
|
|
996
1016
|
}
|
|
997
1017
|
else {
|
|
998
|
-
finish({ kind: 'answered', value:
|
|
1018
|
+
finish({ kind: 'answered', value: answer });
|
|
999
1019
|
}
|
|
1000
1020
|
}, {
|
|
1001
1021
|
initiatorId: permCtx.userId,
|
|
@@ -1181,7 +1201,7 @@ export class AgentRunner {
|
|
|
1181
1201
|
};
|
|
1182
1202
|
// 尝试发送交互卡片
|
|
1183
1203
|
let cardSent = false;
|
|
1184
|
-
if (permCtx.adapter?.
|
|
1204
|
+
if (permCtx.adapter?.capabilities?.interaction === true) {
|
|
1185
1205
|
// 发送计划内容:找 plans 目录中最新修改的 .md 文件
|
|
1186
1206
|
if (sendPrompt) {
|
|
1187
1207
|
try {
|
|
@@ -1283,10 +1303,20 @@ export class AgentRunner {
|
|
|
1283
1303
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
1284
1304
|
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
1285
1305
|
*/
|
|
1286
|
-
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel) {
|
|
1306
|
+
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false) {
|
|
1287
1307
|
let lastSessionId;
|
|
1308
|
+
let hasTurnActivity = false;
|
|
1309
|
+
let ignoredPreTurnResult = false;
|
|
1310
|
+
let waitingForBackgroundCompletion = false;
|
|
1311
|
+
let liveBackgroundTasks = new Set();
|
|
1312
|
+
const unsettledTasks = new Set();
|
|
1313
|
+
const terminalTasks = new Set();
|
|
1314
|
+
const ambientTasks = new Set();
|
|
1315
|
+
const awaitedTaskNotifications = new Set();
|
|
1316
|
+
const taskMetadata = new Map();
|
|
1288
1317
|
// tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
|
|
1289
1318
|
const toolUseNames = new Map();
|
|
1319
|
+
const emittedToolResultIds = new Set();
|
|
1290
1320
|
let turnCount = 0;
|
|
1291
1321
|
const seenMessageIds = new Set();
|
|
1292
1322
|
let lastModelCall;
|
|
@@ -1302,11 +1332,99 @@ export class AgentRunner {
|
|
|
1302
1332
|
yield { type: 'session_id', sessionId: event.session_id };
|
|
1303
1333
|
}
|
|
1304
1334
|
if (event.type === 'user' && event.uuid === inputId) {
|
|
1335
|
+
hasTurnActivity = true;
|
|
1305
1336
|
yield { type: 'input_accepted', inputId };
|
|
1306
1337
|
}
|
|
1338
|
+
if (event.type === 'system' && event.subtype === 'task_started' && typeof event.task_id === 'string') {
|
|
1339
|
+
const taskKind = event.subagent_type || event.task_type === 'remote_agent'
|
|
1340
|
+
? 'agent'
|
|
1341
|
+
: event.workflow_name || event.task_type === 'local_workflow'
|
|
1342
|
+
? 'workflow'
|
|
1343
|
+
: 'background';
|
|
1344
|
+
taskMetadata.set(event.task_id, {
|
|
1345
|
+
taskKind,
|
|
1346
|
+
description: event.description,
|
|
1347
|
+
subagentType: event.subagent_type,
|
|
1348
|
+
taskType: event.task_type,
|
|
1349
|
+
});
|
|
1350
|
+
if (event.skip_transcript === true) {
|
|
1351
|
+
ambientTasks.add(event.task_id);
|
|
1352
|
+
unsettledTasks.delete(event.task_id);
|
|
1353
|
+
liveBackgroundTasks.delete(event.task_id);
|
|
1354
|
+
}
|
|
1355
|
+
else if (!terminalTasks.has(event.task_id)) {
|
|
1356
|
+
unsettledTasks.add(event.task_id);
|
|
1357
|
+
hasTurnActivity = true;
|
|
1358
|
+
}
|
|
1359
|
+
yield {
|
|
1360
|
+
type: 'task_started',
|
|
1361
|
+
taskId: event.task_id,
|
|
1362
|
+
taskKind,
|
|
1363
|
+
toolUseId: event.tool_use_id,
|
|
1364
|
+
description: event.description || event.task_id,
|
|
1365
|
+
subagentType: event.subagent_type,
|
|
1366
|
+
taskType: event.task_type,
|
|
1367
|
+
workflowName: event.workflow_name,
|
|
1368
|
+
prompt: event.prompt,
|
|
1369
|
+
skipTranscript: event.skip_transcript,
|
|
1370
|
+
};
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
if (event.type === 'system' && event.subtype === 'background_tasks_changed') {
|
|
1374
|
+
const tasks = Array.isArray(event.tasks) ? event.tasks : [];
|
|
1375
|
+
liveBackgroundTasks = new Set(tasks
|
|
1376
|
+
.map((task) => task?.task_id)
|
|
1377
|
+
.filter((taskId) => (typeof taskId === 'string'
|
|
1378
|
+
&& !terminalTasks.has(taskId)
|
|
1379
|
+
&& !ambientTasks.has(taskId))));
|
|
1380
|
+
yield {
|
|
1381
|
+
type: 'background_tasks_changed',
|
|
1382
|
+
tasks: tasks.map((task) => ({
|
|
1383
|
+
taskId: task.task_id,
|
|
1384
|
+
taskKind: task.task_type === 'remote_agent'
|
|
1385
|
+
? 'agent'
|
|
1386
|
+
: task.task_type === 'local_workflow'
|
|
1387
|
+
? 'workflow'
|
|
1388
|
+
: taskMetadata.get(task.task_id)?.taskKind || 'background',
|
|
1389
|
+
taskType: task.task_type,
|
|
1390
|
+
description: task.description,
|
|
1391
|
+
})),
|
|
1392
|
+
};
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (event.type === 'system' && event.subtype === 'task_notification' && typeof event.task_id === 'string') {
|
|
1396
|
+
const metadata = taskMetadata.get(event.task_id);
|
|
1397
|
+
const taskKind = metadata?.taskKind
|
|
1398
|
+
|| (event.tool_use_id && toolUseNames.get(event.tool_use_id) === 'Agent' ? 'agent' : undefined)
|
|
1399
|
+
|| (typeof event.summary === 'string' && /^Agent\b/.test(event.summary) ? 'agent' : 'background');
|
|
1400
|
+
terminalTasks.add(event.task_id);
|
|
1401
|
+
ambientTasks.delete(event.task_id);
|
|
1402
|
+
unsettledTasks.delete(event.task_id);
|
|
1403
|
+
liveBackgroundTasks.delete(event.task_id);
|
|
1404
|
+
awaitedTaskNotifications.delete(event.task_id);
|
|
1405
|
+
yield {
|
|
1406
|
+
type: 'task_notification',
|
|
1407
|
+
taskId: event.task_id,
|
|
1408
|
+
taskKind,
|
|
1409
|
+
toolUseId: event.tool_use_id,
|
|
1410
|
+
description: metadata?.description,
|
|
1411
|
+
subagentType: metadata?.subagentType,
|
|
1412
|
+
status: event.status || 'completed',
|
|
1413
|
+
outputFile: event.output_file,
|
|
1414
|
+
summary: event.summary,
|
|
1415
|
+
usage: event.usage ? {
|
|
1416
|
+
totalTokens: event.usage.total_tokens,
|
|
1417
|
+
toolUses: event.usage.tool_uses,
|
|
1418
|
+
durationMs: event.usage.duration_ms,
|
|
1419
|
+
} : undefined,
|
|
1420
|
+
skipTranscript: event.skip_transcript,
|
|
1421
|
+
};
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1307
1424
|
if (event.type === 'stream_event') {
|
|
1308
1425
|
const streamEvent = event.event;
|
|
1309
1426
|
if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
|
|
1427
|
+
hasTurnActivity = true;
|
|
1310
1428
|
lastModelCall = {
|
|
1311
1429
|
uuid: event.uuid,
|
|
1312
1430
|
model: streamEvent.message.model,
|
|
@@ -1347,11 +1465,21 @@ export class AgentRunner {
|
|
|
1347
1465
|
}
|
|
1348
1466
|
// system: task_progress → task_progress
|
|
1349
1467
|
if (event.type === 'system' && event.subtype === 'task_progress') {
|
|
1468
|
+
const metadata = typeof event.task_id === 'string' ? taskMetadata.get(event.task_id) : undefined;
|
|
1469
|
+
if (!event.task_id || !ambientTasks.has(event.task_id))
|
|
1470
|
+
hasTurnActivity = true;
|
|
1350
1471
|
yield {
|
|
1351
1472
|
type: 'task_progress',
|
|
1473
|
+
taskId: event.task_id,
|
|
1474
|
+
taskKind: metadata?.taskKind || (event.subagent_type ? 'agent' : undefined),
|
|
1475
|
+
toolUseId: event.tool_use_id,
|
|
1476
|
+
description: event.description,
|
|
1477
|
+
subagentType: event.subagent_type || metadata?.subagentType,
|
|
1352
1478
|
summary: event.summary,
|
|
1353
|
-
|
|
1354
|
-
|
|
1479
|
+
lastToolName: event.last_tool_name,
|
|
1480
|
+
totalTokens: event.usage?.total_tokens,
|
|
1481
|
+
toolUses: event.usage?.tool_uses ?? event.tool_uses,
|
|
1482
|
+
durationMs: event.usage?.duration_ms ?? event.duration_ms,
|
|
1355
1483
|
};
|
|
1356
1484
|
}
|
|
1357
1485
|
// system: session_state_changed → state_changed
|
|
@@ -1360,6 +1488,7 @@ export class AgentRunner {
|
|
|
1360
1488
|
}
|
|
1361
1489
|
// assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
|
|
1362
1490
|
if (event.type === 'assistant' && event.message?.content) {
|
|
1491
|
+
hasTurnActivity = true;
|
|
1363
1492
|
lastAssistantUuid = event.uuid ?? lastAssistantUuid;
|
|
1364
1493
|
const msgId = event.message.id;
|
|
1365
1494
|
if (!msgId || !seenMessageIds.has(msgId)) {
|
|
@@ -1410,6 +1539,8 @@ export class AgentRunner {
|
|
|
1410
1539
|
const resultContent = typeof block.content === 'string'
|
|
1411
1540
|
? block.content
|
|
1412
1541
|
: block.content != null ? JSON.stringify(block.content) : '';
|
|
1542
|
+
if (block.tool_use_id)
|
|
1543
|
+
emittedToolResultIds.add(block.tool_use_id);
|
|
1413
1544
|
yield {
|
|
1414
1545
|
type: 'tool_result',
|
|
1415
1546
|
name: toolName,
|
|
@@ -1423,15 +1554,22 @@ export class AgentRunner {
|
|
|
1423
1554
|
}
|
|
1424
1555
|
// result → complete(含 permission_denials 提取)
|
|
1425
1556
|
if (event.type === 'result') {
|
|
1426
|
-
//
|
|
1557
|
+
// Most SDK streams surface denials as user-message tool_result blocks.
|
|
1558
|
+
// The terminal summary is only a fallback for incomplete streams.
|
|
1427
1559
|
if (Array.isArray(event.permission_denials)) {
|
|
1428
1560
|
for (const denial of event.permission_denials) {
|
|
1561
|
+
const callId = typeof denial.tool_use_id === 'string' ? denial.tool_use_id : undefined;
|
|
1562
|
+
if (callId && emittedToolResultIds.has(callId))
|
|
1563
|
+
continue;
|
|
1564
|
+
if (callId)
|
|
1565
|
+
emittedToolResultIds.add(callId);
|
|
1429
1566
|
yield {
|
|
1430
1567
|
type: 'tool_result',
|
|
1431
1568
|
name: denial.tool_name || '',
|
|
1432
1569
|
result: '',
|
|
1433
1570
|
isError: true,
|
|
1434
|
-
error: `权限被拒绝: ${denial.tool_name}`,
|
|
1571
|
+
error: `权限被拒绝: ${denial.tool_name || 'unknown tool'}`,
|
|
1572
|
+
callId,
|
|
1435
1573
|
};
|
|
1436
1574
|
}
|
|
1437
1575
|
}
|
|
@@ -1502,7 +1640,7 @@ export class AgentRunner {
|
|
|
1502
1640
|
// 降级:无逐次数据,写一条累计行
|
|
1503
1641
|
modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
|
|
1504
1642
|
}
|
|
1505
|
-
|
|
1643
|
+
const completeEvent = {
|
|
1506
1644
|
type: 'complete',
|
|
1507
1645
|
result: cleanResult,
|
|
1508
1646
|
subtype: event.subtype,
|
|
@@ -1520,10 +1658,53 @@ export class AgentRunner {
|
|
|
1520
1658
|
modelCalls,
|
|
1521
1659
|
assistantUuid: lastAssistantUuid,
|
|
1522
1660
|
};
|
|
1523
|
-
//
|
|
1661
|
+
// Fatal results belong to the query transport itself and must terminate
|
|
1662
|
+
// even when the current input was not echoed back by the CLI.
|
|
1663
|
+
if (event.is_error) {
|
|
1664
|
+
yield { ...completeEvent, queryFinal: true };
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (isResuming && !hasTurnActivity && !ignoredPreTurnResult) {
|
|
1668
|
+
ignoredPreTurnResult = true;
|
|
1669
|
+
logger.info(`[AgentRunner] Ignoring pre-turn result while resuming session=${sessionId} input=${inputId}`);
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const hasLiveTasks = liveBackgroundTasks.size > 0 || unsettledTasks.size > 0;
|
|
1673
|
+
if (hasLiveTasks) {
|
|
1674
|
+
waitingForBackgroundCompletion = true;
|
|
1675
|
+
for (const taskId of liveBackgroundTasks)
|
|
1676
|
+
awaitedTaskNotifications.add(taskId);
|
|
1677
|
+
for (const taskId of unsettledTasks)
|
|
1678
|
+
awaitedTaskNotifications.add(taskId);
|
|
1679
|
+
yield { ...completeEvent, queryFinal: false };
|
|
1680
|
+
hasTurnActivity = false;
|
|
1681
|
+
ignoredPreTurnResult = false;
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
// background_tasks_changed may report an empty set before the matching
|
|
1685
|
+
// task_notification. Do not close in that gap: the notification wakes the
|
|
1686
|
+
// parent agent for the final synthesis turn.
|
|
1687
|
+
if (waitingForBackgroundCompletion && awaitedTaskNotifications.size > 0) {
|
|
1688
|
+
yield { ...completeEvent, queryFinal: false };
|
|
1689
|
+
hasTurnActivity = false;
|
|
1690
|
+
ignoredPreTurnResult = false;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
yield { ...completeEvent, queryFinal: true };
|
|
1524
1694
|
return;
|
|
1525
1695
|
}
|
|
1526
1696
|
}
|
|
1697
|
+
if (waitingForBackgroundCompletion) {
|
|
1698
|
+
yield {
|
|
1699
|
+
type: 'complete',
|
|
1700
|
+
isError: true,
|
|
1701
|
+
subtype: 'protocol_incomplete',
|
|
1702
|
+
terminalReason: 'background_tasks_incomplete',
|
|
1703
|
+
protocolIncompleteReason: 'background_tasks_incomplete',
|
|
1704
|
+
errors: ['Claude SDK stream ended before background tasks produced a final parent result'],
|
|
1705
|
+
queryFinal: true,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1527
1708
|
}
|
|
1528
1709
|
catch (err) {
|
|
1529
1710
|
// 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
|
|
@@ -2107,7 +2288,6 @@ export class AgentRunner {
|
|
|
2107
2288
|
logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
|
|
2108
2289
|
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
2109
2290
|
msgStream.push(prompt, images, inputId);
|
|
2110
|
-
msgStream.end();
|
|
2111
2291
|
sdkStream = createQuery(msgStream);
|
|
2112
2292
|
}
|
|
2113
2293
|
else {
|
|
@@ -2124,7 +2304,7 @@ export class AgentRunner {
|
|
|
2124
2304
|
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
2125
2305
|
}
|
|
2126
2306
|
// 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
|
|
2127
|
-
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel);
|
|
2307
|
+
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId);
|
|
2128
2308
|
const self = this;
|
|
2129
2309
|
return (async function* () {
|
|
2130
2310
|
try {
|
|
@@ -704,11 +704,13 @@ export class CodexRunner {
|
|
|
704
704
|
controller.signal.addEventListener('abort', () => queue.end(), { once: true });
|
|
705
705
|
const state = {
|
|
706
706
|
threadId,
|
|
707
|
+
model: callModel,
|
|
707
708
|
streamedAgentMessageIds: new Set(),
|
|
708
709
|
agentMessageDeltaText: new Map(),
|
|
709
710
|
completedItemIds: new Set(),
|
|
710
711
|
emittedEditCallIds: new Set(),
|
|
711
712
|
completedTurnIds: new Set(),
|
|
713
|
+
openToolCalls: new Map(),
|
|
712
714
|
};
|
|
713
715
|
const unsubscribe = appServer.onNotification(notification => {
|
|
714
716
|
// 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
|
|
@@ -2083,7 +2085,10 @@ export class CodexRunner {
|
|
|
2083
2085
|
break;
|
|
2084
2086
|
}
|
|
2085
2087
|
case 'item/started': {
|
|
2086
|
-
|
|
2088
|
+
for (const event of this.mapAppServerItemStarted(params.item, state)) {
|
|
2089
|
+
this.trackAppServerToolLifecycle(event, state);
|
|
2090
|
+
yield event;
|
|
2091
|
+
}
|
|
2087
2092
|
break;
|
|
2088
2093
|
}
|
|
2089
2094
|
case 'item/agentMessage/delta': {
|
|
@@ -2099,11 +2104,17 @@ export class CodexRunner {
|
|
|
2099
2104
|
const item = params.item;
|
|
2100
2105
|
if (item?.id)
|
|
2101
2106
|
state.completedItemIds.add(item.id);
|
|
2102
|
-
|
|
2107
|
+
for (const event of this.mapAppServerItemCompleted(item, state)) {
|
|
2108
|
+
this.trackAppServerToolLifecycle(event, state);
|
|
2109
|
+
yield event;
|
|
2110
|
+
}
|
|
2103
2111
|
break;
|
|
2104
2112
|
}
|
|
2105
2113
|
case 'item/fileChange/patchUpdated': {
|
|
2106
|
-
|
|
2114
|
+
for (const event of this.mapAppServerFileChangePatchUpdated(params, state)) {
|
|
2115
|
+
this.trackAppServerToolLifecycle(event, state);
|
|
2116
|
+
yield event;
|
|
2117
|
+
}
|
|
2107
2118
|
break;
|
|
2108
2119
|
}
|
|
2109
2120
|
case 'turn/plan/updated': {
|
|
@@ -2131,6 +2142,7 @@ export class CodexRunner {
|
|
|
2131
2142
|
if (turnId)
|
|
2132
2143
|
state.completedTurnIds.add(turnId);
|
|
2133
2144
|
this.activeTurns.delete(sessionId);
|
|
2145
|
+
yield* this.reconcileOpenAppServerToolCalls(state, turnId);
|
|
2134
2146
|
if (turn.status === 'failed' && turn.error?.message) {
|
|
2135
2147
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
2136
2148
|
throw new Error(turn.error.message);
|
|
@@ -2154,6 +2166,36 @@ export class CodexRunner {
|
|
|
2154
2166
|
}
|
|
2155
2167
|
}
|
|
2156
2168
|
}
|
|
2169
|
+
trackAppServerToolLifecycle(event, state) {
|
|
2170
|
+
if (event.type !== 'tool_use' && event.type !== 'tool_result')
|
|
2171
|
+
return;
|
|
2172
|
+
if (!event.callId)
|
|
2173
|
+
return;
|
|
2174
|
+
if (event.type === 'tool_use') {
|
|
2175
|
+
state.openToolCalls.set(event.callId, event.name);
|
|
2176
|
+
}
|
|
2177
|
+
else if (event.type === 'tool_result') {
|
|
2178
|
+
state.openToolCalls.delete(event.callId);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
*reconcileOpenAppServerToolCalls(state, turnId) {
|
|
2182
|
+
if (state.openToolCalls.size === 0)
|
|
2183
|
+
return;
|
|
2184
|
+
const callIds = [...state.openToolCalls.keys()];
|
|
2185
|
+
logger.warn(`[CodexRunner] Turn completed with ${callIds.length} open tool call(s): ` +
|
|
2186
|
+
`thread=${state.threadId} turn=${turnId || state.turnId || 'unknown'} calls=${callIds.join(',')}`);
|
|
2187
|
+
for (const [callId, name] of state.openToolCalls) {
|
|
2188
|
+
yield {
|
|
2189
|
+
type: 'tool_result',
|
|
2190
|
+
name,
|
|
2191
|
+
result: '',
|
|
2192
|
+
isError: true,
|
|
2193
|
+
error: 'Codex app-server ended the turn without an item/completed notification',
|
|
2194
|
+
callId,
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
state.openToolCalls.clear();
|
|
2198
|
+
}
|
|
2157
2199
|
*mapAppServerItemStarted(item, state) {
|
|
2158
2200
|
if (!item)
|
|
2159
2201
|
return;
|
|
@@ -2458,6 +2500,7 @@ export class CodexRunner {
|
|
|
2458
2500
|
costUsd: this.pickNumber(turn.costUsd, turn.totalCostUsd, turn.total_cost_usd),
|
|
2459
2501
|
sessionTitle: typeof turn.sessionTitle === 'string' ? turn.sessionTitle : typeof turn.session_title === 'string' ? turn.session_title : undefined,
|
|
2460
2502
|
numTurns: this.pickNumber(turn.numTurns, turn.num_turns),
|
|
2503
|
+
model: state.model,
|
|
2461
2504
|
tokenUsage,
|
|
2462
2505
|
contextUsage,
|
|
2463
2506
|
};
|
package/dist/aun/outbox.js
CHANGED
|
@@ -158,3 +158,11 @@ export function hasPending(aid) {
|
|
|
158
158
|
return false;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
|
|
163
|
+
* message.send 出队速度跟不上入队速度(网关慢 / 链路抖动 / 卡片洪泛),
|
|
164
|
+
* 是命令「回复慢/无回复」的先行指标。
|
|
165
|
+
*/
|
|
166
|
+
export function pendingCount(aid) {
|
|
167
|
+
return load(aid).length;
|
|
168
|
+
}
|
package/dist/channels/aun.js
CHANGED
|
@@ -195,25 +195,37 @@ export class AUNChannel {
|
|
|
195
195
|
*/
|
|
196
196
|
async callAndTrace(method, params, opts) {
|
|
197
197
|
this.trace('OUT', method, params);
|
|
198
|
+
// RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
|
|
199
|
+
// duration 若普遍逼近 SDK 的 rpc 超时(默认 10s),说明发送管线已饱和或链路抖动,
|
|
200
|
+
// 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
|
|
201
|
+
const rpcStart = Date.now();
|
|
202
|
+
// SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
|
|
203
|
+
const SLOW_RPC_WARN_MS = 3000;
|
|
198
204
|
try {
|
|
199
205
|
const result = await this.client.call(method, params);
|
|
206
|
+
const durationMs = Date.now() - rpcStart;
|
|
200
207
|
if (!opts?.silentOk) {
|
|
201
208
|
const r = result;
|
|
202
209
|
const snap = r && typeof r === 'object'
|
|
203
|
-
? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id }
|
|
204
|
-
:
|
|
205
|
-
this.trace('OUT', `${method}.ok`, snap
|
|
210
|
+
? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id, duration_ms: durationMs }
|
|
211
|
+
: { duration_ms: durationMs };
|
|
212
|
+
this.trace('OUT', `${method}.ok`, snap);
|
|
213
|
+
}
|
|
214
|
+
if (durationMs >= SLOW_RPC_WARN_MS) {
|
|
215
|
+
logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms (发送管线可能饱和或链路抖动)`);
|
|
206
216
|
}
|
|
207
217
|
return result;
|
|
208
218
|
}
|
|
209
219
|
catch (e) {
|
|
220
|
+
const durationMs = Date.now() - rpcStart;
|
|
210
221
|
this.trace('OUT', `${method}.error`, {
|
|
211
222
|
error: e?.message ?? String(e),
|
|
212
223
|
code: e?.code,
|
|
213
224
|
name: e?.name,
|
|
225
|
+
duration_ms: durationMs,
|
|
214
226
|
});
|
|
215
227
|
if (!opts?.silentError) {
|
|
216
|
-
logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
|
|
228
|
+
logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
|
|
217
229
|
}
|
|
218
230
|
throw e;
|
|
219
231
|
}
|
|
@@ -2752,6 +2764,11 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2752
2764
|
context,
|
|
2753
2765
|
});
|
|
2754
2766
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
2767
|
+
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
2768
|
+
const backlog = outbox.pendingCount(this.config.aid);
|
|
2769
|
+
if (backlog >= 5) {
|
|
2770
|
+
logger.warn(`${this.logPrefix()} Outbox backlog=${backlog} channel=${channelId} (发送管线堵塞,回复将延迟)`);
|
|
2771
|
+
}
|
|
2755
2772
|
if (!this.connected || !this.client) {
|
|
2756
2773
|
logger.warn(`${this.logPrefix()} Not connected, message queued in outbox (id=${entry.id}). Triggering reconnect.`);
|
|
2757
2774
|
if (!this.reconnectTimer && !this.client) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomInt } from 'crypto';
|
|
2
|
+
import { isValidAid } from '../aun/aid/validation.js';
|
|
3
|
+
import { bindContactAlias } from '../config/contact-book.js';
|
|
4
|
+
export const CONTACT_BIND_CODE_TTL_MS = 10 * 60 * 1000;
|
|
5
|
+
export const CONTACT_BIND_MAX_FAILED_ATTEMPTS = 5;
|
|
6
|
+
/** In-memory, one-time channel identity proof used before writing a Contact alias. */
|
|
7
|
+
export class ContactBindCodeRegistry {
|
|
8
|
+
options;
|
|
9
|
+
pending = new Map();
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
register(req) {
|
|
14
|
+
const selfAid = String(req.selfAid || '').trim();
|
|
15
|
+
const channelName = String(req.channelName || '').trim();
|
|
16
|
+
const primaryId = String(req.primaryId || '').trim();
|
|
17
|
+
const now = req.now ?? Date.now();
|
|
18
|
+
const code = req.code ? String(req.code).trim() : generateBindCode();
|
|
19
|
+
if (!isValidAid(selfAid))
|
|
20
|
+
return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
|
|
21
|
+
if (!channelName)
|
|
22
|
+
return { ok: false, error: 'missing channelName' };
|
|
23
|
+
if (!isValidAid(primaryId))
|
|
24
|
+
return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
|
|
25
|
+
if (!/^\d{6}$/.test(code))
|
|
26
|
+
return { ok: false, error: 'binding code must be 6 digits' };
|
|
27
|
+
const key = pendingBindKey(selfAid, channelName);
|
|
28
|
+
const replaced = this.pending.has(key);
|
|
29
|
+
const item = {
|
|
30
|
+
selfAid,
|
|
31
|
+
channelName,
|
|
32
|
+
primaryId,
|
|
33
|
+
code,
|
|
34
|
+
createdAt: now,
|
|
35
|
+
expiresAt: now + CONTACT_BIND_CODE_TTL_MS,
|
|
36
|
+
failedAttempts: 0,
|
|
37
|
+
maxFailedAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
|
|
38
|
+
};
|
|
39
|
+
this.pending.set(key, item);
|
|
40
|
+
return { ok: true, code, expiresAt: item.expiresAt, replaced };
|
|
41
|
+
}
|
|
42
|
+
handle(ctx) {
|
|
43
|
+
if (ctx.channelType !== this.options.channelType)
|
|
44
|
+
return { handled: false };
|
|
45
|
+
const selfAid = String(ctx.selfAid || '').trim();
|
|
46
|
+
const channelName = String(ctx.channelName || '').trim();
|
|
47
|
+
if (!selfAid || !channelName)
|
|
48
|
+
return { handled: false };
|
|
49
|
+
const key = pendingBindKey(selfAid, channelName);
|
|
50
|
+
const item = this.pending.get(key);
|
|
51
|
+
if (!item)
|
|
52
|
+
return { handled: false };
|
|
53
|
+
const now = ctx.now ?? Date.now();
|
|
54
|
+
if (now >= item.expiresAt) {
|
|
55
|
+
this.pending.delete(key);
|
|
56
|
+
return {
|
|
57
|
+
handled: true,
|
|
58
|
+
status: 'expired',
|
|
59
|
+
remainingAttempts: 0,
|
|
60
|
+
reply: `${this.options.displayName}身份绑定码已超时,本次绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (ctx.chatType !== 'private')
|
|
64
|
+
return { handled: false };
|
|
65
|
+
const input = String(ctx.content ?? '').trim();
|
|
66
|
+
if (!/^\d{6}$/.test(input)) {
|
|
67
|
+
return {
|
|
68
|
+
handled: true,
|
|
69
|
+
status: 'format',
|
|
70
|
+
reply: '请直接发送 6 位数字绑定码。',
|
|
71
|
+
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (input !== item.code) {
|
|
75
|
+
item.failedAttempts += 1;
|
|
76
|
+
const remaining = Math.max(0, item.maxFailedAttempts - item.failedAttempts);
|
|
77
|
+
if (remaining === 0) {
|
|
78
|
+
this.pending.delete(key);
|
|
79
|
+
return {
|
|
80
|
+
handled: true,
|
|
81
|
+
status: 'failed',
|
|
82
|
+
remainingAttempts: 0,
|
|
83
|
+
reply: `绑定码无效,本次${this.options.displayName}身份绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
handled: true,
|
|
88
|
+
status: 'wrong-code',
|
|
89
|
+
remainingAttempts: remaining,
|
|
90
|
+
reply: '绑定码错误,请重新发送 6 位数字绑定码。',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const actorId = String(ctx.actorId || '').trim();
|
|
94
|
+
if (!actorId) {
|
|
95
|
+
return {
|
|
96
|
+
handled: true,
|
|
97
|
+
status: 'missing-actor',
|
|
98
|
+
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
99
|
+
reply: `无法识别当前${this.options.displayName}发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
bindContactAlias(item.selfAid, item.primaryId, this.options.channelType, actorId, item.channelName);
|
|
104
|
+
this.pending.delete(key);
|
|
105
|
+
return {
|
|
106
|
+
handled: true,
|
|
107
|
+
status: 'bound',
|
|
108
|
+
reply: `${this.options.displayName}身份绑定成功:${item.channelName}:${encodeURIComponent(actorId)} -> ${item.primaryId}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
return {
|
|
114
|
+
handled: true,
|
|
115
|
+
status: 'write-failed',
|
|
116
|
+
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
117
|
+
reply: `${this.options.displayName}身份绑定写入失败:${message}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
get(selfAid, channelName) {
|
|
122
|
+
const item = this.pending.get(pendingBindKey(selfAid, channelName));
|
|
123
|
+
return item ? { ...item } : null;
|
|
124
|
+
}
|
|
125
|
+
clear() {
|
|
126
|
+
this.pending.clear();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function generateBindCode() {
|
|
130
|
+
return String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
131
|
+
}
|
|
132
|
+
function pendingBindKey(selfAid, channelName) {
|
|
133
|
+
return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
|
|
134
|
+
}
|