evolcore 0.0.24 → 0.0.26
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 +59 -0
- package/bin/ec.js +12 -1
- package/bin/install-codex-managed-hooks.mjs +28 -1
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +54 -13
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +128 -42
- package/dist/agents/ecagent-runner.js +14 -18
- package/dist/agents/gemini-runner.js +24 -9
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +135 -10
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +53 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +125 -5
- package/dist/cli/agent-create.js +151 -11
- package/dist/cli/agent.js +119 -6
- package/dist/cli/aun-commands.js +74 -7
- package/dist/cli/config.js +27 -8
- package/dist/cli/contact.js +63 -6
- package/dist/cli/daemon-commands.js +90 -25
- package/dist/cli/diag.js +31 -0
- package/dist/cli/handoff-command.js +35 -3
- package/dist/cli/index.js +7 -1
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +38 -23
- package/dist/cli/restart-monitor.js +57 -10
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +165 -3
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +66 -8
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +87 -8
- package/dist/core/auth/operation-catalog.js +51 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/agent-control.js +54 -25
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +152 -20
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +13 -3
- package/dist/core/command/menu-handler.js +88 -31
- package/dist/core/command/menu-protocol.js +6 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +77 -12
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +3 -3
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +48 -23
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +47 -7
- package/dist/index.js +195 -31
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/price-resolver.js +28 -10
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/model-prices.jsonl +3 -3
- package/dist/utils/npm-ops.js +176 -35
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -1
- package/kits/docs/evolcore/agent-create.md +30 -11
- package/kits/docs/evolcore/agent.md +18 -8
- package/kits/docs/evolcore/aid.md +6 -4
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +7 -0
- package/kits/docs/evolcore/contact.md +12 -5
- package/kits/docs/evolcore/menu-client.md +11 -3
- package/kits/docs/evolcore/msg.md +1 -0
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/docs/venues/group.md +12 -0
- package/kits/eck_manifest.json +12 -0
- package/kits/rules/03-identity.md +3 -1
- package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
- package/kits/templates/roles/admin.json +14 -2
- package/kits/templates/roles/member.json +3 -2
- package/kits/templates/roles/visitor.json +4 -3
- package/kits/templates/system-fragments/bootstrap.md +16 -5
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +6 -4
- package/skills/eclink/SKILL.md +0 -84
- package/skills/eclink/agents/openai.yaml +0 -4
|
@@ -33,6 +33,9 @@ function normalizeReturnContent(content) {
|
|
|
33
33
|
function contentHash(content) {
|
|
34
34
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
35
35
|
}
|
|
36
|
+
function newReturnResultId() {
|
|
37
|
+
return `hr-${crypto.randomBytes(8).toString('hex')}`;
|
|
38
|
+
}
|
|
36
39
|
function boundedQueryLimit(limit) {
|
|
37
40
|
if (typeof limit !== 'number' || !Number.isInteger(limit))
|
|
38
41
|
return HANDOFF_QUERY_DEFAULT_LIMIT;
|
|
@@ -111,6 +114,9 @@ export class HandoffStore {
|
|
|
111
114
|
fs.renameSync(pendingPath, filePath);
|
|
112
115
|
}
|
|
113
116
|
create(input) {
|
|
117
|
+
if (input.handoffContext !== undefined && (typeof input.handoffContext !== 'string'
|
|
118
|
+
|| !input.handoffContext.trim() || input.handoffContext.length > 64 * 1024))
|
|
119
|
+
throw new Error('invalid handoff context');
|
|
114
120
|
const now = input.now ?? Date.now();
|
|
115
121
|
const handoffId = newHandoffId();
|
|
116
122
|
const instance = {
|
|
@@ -123,6 +129,7 @@ export class HandoffStore {
|
|
|
123
129
|
payload: clonePayload(input.payload),
|
|
124
130
|
encrypt: input.encrypt,
|
|
125
131
|
...(input.contextOnly ? { contextOnly: true } : {}),
|
|
132
|
+
...(input.handoffContext ? { context: input.handoffContext } : {}),
|
|
126
133
|
...(input.delivery ? { delivery: input.delivery } : {}),
|
|
127
134
|
},
|
|
128
135
|
...(input.originAuthorization
|
|
@@ -143,6 +150,7 @@ export class HandoffStore {
|
|
|
143
150
|
return_content: null,
|
|
144
151
|
return_content_hash: null,
|
|
145
152
|
origin_delivery_message_id: null,
|
|
153
|
+
...((input.returnPolicy ?? 'required') === 'required' && !input.contextOnly ? { return_results: [] } : {}),
|
|
146
154
|
attention_required: false,
|
|
147
155
|
attention_reason: null,
|
|
148
156
|
version: 1,
|
|
@@ -401,7 +409,12 @@ export class HandoffStore {
|
|
|
401
409
|
if ('ok' in selected)
|
|
402
410
|
return selected;
|
|
403
411
|
const { instance, selectedBy } = selected;
|
|
404
|
-
|
|
412
|
+
const directSessionMatch = instance.target_session_id === input.currentSessionId
|
|
413
|
+
&& instance.consumed_target_session_id === input.currentSessionId;
|
|
414
|
+
const recoveryAuthorized = !!input.recovery
|
|
415
|
+
&& input.recovery.recoverySessionId === input.currentSessionId
|
|
416
|
+
&& input.recovery.actorId === instance.origin_authorization?.actorId;
|
|
417
|
+
if (!directSessionMatch && !recoveryAuthorized) {
|
|
405
418
|
return {
|
|
406
419
|
ok: false,
|
|
407
420
|
code: 'HANDOFF_TARGET_SESSION_MISMATCH',
|
|
@@ -430,6 +443,110 @@ export class HandoffStore {
|
|
|
430
443
|
};
|
|
431
444
|
}
|
|
432
445
|
const hash = contentHash(normalized);
|
|
446
|
+
const previousState = instance.state;
|
|
447
|
+
// New required handoffs expose an append-only result stream. The parent
|
|
448
|
+
// remains return_pending while each result is delivered and consumed.
|
|
449
|
+
if (instance.return_policy === 'required' && Array.isArray(instance.return_results)) {
|
|
450
|
+
if (instance.state !== 'return_pending' && instance.state !== 'completed') {
|
|
451
|
+
const existing = instance.return_results.find(result => result.content_hash === hash);
|
|
452
|
+
if (existing) {
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
code: 'HANDOFF_RETURN_ALREADY_APPLIED',
|
|
456
|
+
handoff_id: instance.handoff_id,
|
|
457
|
+
selected_by: selectedBy,
|
|
458
|
+
previous_state: instance.state,
|
|
459
|
+
state: instance.state,
|
|
460
|
+
idempotent: true,
|
|
461
|
+
result_id: existing.result_id,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
code: 'HANDOFF_NOT_RETURNABLE',
|
|
467
|
+
error: 'handoff is not return_pending',
|
|
468
|
+
handoff_id: instance.handoff_id,
|
|
469
|
+
state: instance.state,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (!normalized.trim()) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
code: 'HANDOFF_RETURN_CONTENT_REQUIRED',
|
|
476
|
+
error: 'return content is required',
|
|
477
|
+
handoff_id: instance.handoff_id,
|
|
478
|
+
state: instance.state,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const existing = instance.return_results.find(result => result.content_hash === hash);
|
|
482
|
+
if (existing) {
|
|
483
|
+
return {
|
|
484
|
+
ok: true,
|
|
485
|
+
code: 'HANDOFF_RETURN_ALREADY_APPLIED',
|
|
486
|
+
handoff_id: instance.handoff_id,
|
|
487
|
+
selected_by: selectedBy,
|
|
488
|
+
previous_state: instance.state,
|
|
489
|
+
state: instance.state,
|
|
490
|
+
idempotent: true,
|
|
491
|
+
result_id: existing.result_id,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const now = input.now ?? Date.now();
|
|
495
|
+
const previousVersion = instance.version;
|
|
496
|
+
const result = {
|
|
497
|
+
result_id: newReturnResultId(),
|
|
498
|
+
seq: instance.return_results.reduce((max, item) => Math.max(max, item.seq), 0) + 1,
|
|
499
|
+
content: normalized,
|
|
500
|
+
content_hash: hash,
|
|
501
|
+
state: 'origin_queued',
|
|
502
|
+
origin_delivery_message_id: null,
|
|
503
|
+
return_causation: normalizeCausation(input.returnCausation),
|
|
504
|
+
created_at: now,
|
|
505
|
+
updated_at: now,
|
|
506
|
+
completed_at: null,
|
|
507
|
+
};
|
|
508
|
+
instance.return_results.push(result);
|
|
509
|
+
instance.return_causation = result.return_causation;
|
|
510
|
+
instance.version++;
|
|
511
|
+
instance.updated_at = now;
|
|
512
|
+
try {
|
|
513
|
+
this.appendEvent(input.selfAid, 'return_accepted', instance.handoff_id, {
|
|
514
|
+
operationKey: `handoff-return:${instance.handoff_id}:${result.result_id}`,
|
|
515
|
+
fromVersion: previousVersion,
|
|
516
|
+
toVersion: instance.version,
|
|
517
|
+
details: {
|
|
518
|
+
return_policy: instance.return_policy,
|
|
519
|
+
result_id: result.result_id,
|
|
520
|
+
seq: result.seq,
|
|
521
|
+
...(recoveryAuthorized
|
|
522
|
+
? { recovery: true, recovery_session_id: input.currentSessionId, recovery_actor_id: input.recovery?.actorId }
|
|
523
|
+
: {}),
|
|
524
|
+
},
|
|
525
|
+
now,
|
|
526
|
+
});
|
|
527
|
+
this.write(input.selfAid, instance);
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return {
|
|
531
|
+
ok: false,
|
|
532
|
+
code: 'HANDOFF_STORE_WRITE_FAILED',
|
|
533
|
+
error: 'failed to persist handoff return',
|
|
534
|
+
handoff_id: instance.handoff_id,
|
|
535
|
+
state: previousState,
|
|
536
|
+
retryable: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
code: 'HANDOFF_RETURN_ACCEPTED',
|
|
542
|
+
handoff_id: instance.handoff_id,
|
|
543
|
+
selected_by: selectedBy,
|
|
544
|
+
previous_state: previousState,
|
|
545
|
+
state: instance.state,
|
|
546
|
+
idempotent: false,
|
|
547
|
+
result_id: result.result_id,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
433
550
|
if (instance.state !== 'return_pending') {
|
|
434
551
|
if (instance.return_content_hash) {
|
|
435
552
|
if (instance.return_content_hash === hash) {
|
|
@@ -469,7 +586,6 @@ export class HandoffStore {
|
|
|
469
586
|
};
|
|
470
587
|
}
|
|
471
588
|
const now = input.now ?? Date.now();
|
|
472
|
-
const previousState = instance.state;
|
|
473
589
|
const previousVersion = instance.version;
|
|
474
590
|
instance.state = instance.return_policy === 'none' ? 'completed' : 'origin_queued';
|
|
475
591
|
instance.return_content = instance.return_policy === 'none' ? null : normalized;
|
|
@@ -483,7 +599,12 @@ export class HandoffStore {
|
|
|
483
599
|
operationKey: `handoff-return:${instance.handoff_id}`,
|
|
484
600
|
fromVersion: previousVersion,
|
|
485
601
|
toVersion: instance.version,
|
|
486
|
-
details: {
|
|
602
|
+
details: {
|
|
603
|
+
return_policy: instance.return_policy,
|
|
604
|
+
...(recoveryAuthorized
|
|
605
|
+
? { recovery: true, recovery_session_id: input.currentSessionId, recovery_actor_id: input.recovery?.actorId }
|
|
606
|
+
: {}),
|
|
607
|
+
},
|
|
487
608
|
now,
|
|
488
609
|
});
|
|
489
610
|
this.write(input.selfAid, instance);
|
|
@@ -524,6 +645,31 @@ export class HandoffStore {
|
|
|
524
645
|
this.write(selfAid, instance);
|
|
525
646
|
return instance;
|
|
526
647
|
}
|
|
648
|
+
recordOriginResultEnqueued(selfAid, handoffId, resultId, messageId, now = Date.now()) {
|
|
649
|
+
const instance = this.get(selfAid, handoffId);
|
|
650
|
+
if (!instance)
|
|
651
|
+
throw new Error('handoff not found');
|
|
652
|
+
const result = instance.return_results?.find(item => item.result_id === resultId);
|
|
653
|
+
if (!result)
|
|
654
|
+
throw new Error('handoff result not found');
|
|
655
|
+
if (result.state !== 'origin_queued')
|
|
656
|
+
throw new Error(`handoff result state ${result.state}, expected origin_queued`);
|
|
657
|
+
const previous = instance.version;
|
|
658
|
+
result.state = 'origin_delivered';
|
|
659
|
+
result.origin_delivery_message_id = messageId;
|
|
660
|
+
result.updated_at = now;
|
|
661
|
+
instance.version++;
|
|
662
|
+
instance.updated_at = now;
|
|
663
|
+
this.appendEvent(selfAid, 'origin_enqueue_succeeded', handoffId, {
|
|
664
|
+
operationKey: messageId,
|
|
665
|
+
fromVersion: previous,
|
|
666
|
+
toVersion: instance.version,
|
|
667
|
+
details: { result_id: resultId },
|
|
668
|
+
now,
|
|
669
|
+
});
|
|
670
|
+
this.write(selfAid, instance);
|
|
671
|
+
return instance;
|
|
672
|
+
}
|
|
527
673
|
recordOriginEnqueueFailed(selfAid, handoffId, error, now = Date.now()) {
|
|
528
674
|
const instance = this.requireState(selfAid, handoffId, 'origin_queued');
|
|
529
675
|
this.appendEvent(selfAid, 'origin_enqueue_failed', handoffId, {
|
|
@@ -532,6 +678,19 @@ export class HandoffStore {
|
|
|
532
678
|
now,
|
|
533
679
|
});
|
|
534
680
|
}
|
|
681
|
+
recordOriginResultEnqueueFailed(selfAid, handoffId, resultId, error, now = Date.now()) {
|
|
682
|
+
const instance = this.get(selfAid, handoffId);
|
|
683
|
+
if (!instance)
|
|
684
|
+
throw new Error('handoff not found');
|
|
685
|
+
if (!instance.return_results?.some(item => item.result_id === resultId)) {
|
|
686
|
+
throw new Error('handoff result not found');
|
|
687
|
+
}
|
|
688
|
+
this.appendEvent(selfAid, 'origin_enqueue_failed', handoffId, {
|
|
689
|
+
operationKey: `origin-deliver:${handoffId}:${resultId}:${instance.origin_session_id}`,
|
|
690
|
+
details: { error, result_id: resultId },
|
|
691
|
+
now,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
535
694
|
completeOriginContext(selfAid, handoffId, now = Date.now()) {
|
|
536
695
|
const instance = this.requireState(selfAid, handoffId, 'origin_delivered');
|
|
537
696
|
const previous = instance.version;
|
|
@@ -548,6 +707,35 @@ export class HandoffStore {
|
|
|
548
707
|
this.write(selfAid, instance);
|
|
549
708
|
return instance;
|
|
550
709
|
}
|
|
710
|
+
completeOriginResult(selfAid, handoffId, resultId, now = Date.now()) {
|
|
711
|
+
const instance = this.get(selfAid, handoffId);
|
|
712
|
+
if (!instance)
|
|
713
|
+
throw new Error('handoff not found');
|
|
714
|
+
const result = instance.return_results?.find(item => item.result_id === resultId);
|
|
715
|
+
if (!result)
|
|
716
|
+
throw new Error('handoff result not found');
|
|
717
|
+
if (result.state !== 'origin_delivered')
|
|
718
|
+
throw new Error(`handoff result state ${result.state}, expected origin_delivered`);
|
|
719
|
+
const previous = instance.version;
|
|
720
|
+
result.state = 'completed';
|
|
721
|
+
result.updated_at = now;
|
|
722
|
+
result.completed_at = now;
|
|
723
|
+
if (instance.return_results.every(item => item.state === 'completed')) {
|
|
724
|
+
instance.state = 'completed';
|
|
725
|
+
instance.completed_at = now;
|
|
726
|
+
}
|
|
727
|
+
instance.version++;
|
|
728
|
+
instance.updated_at = now;
|
|
729
|
+
this.appendEvent(selfAid, 'origin_context_consumed', handoffId, {
|
|
730
|
+
operationKey: result.origin_delivery_message_id ?? undefined,
|
|
731
|
+
fromVersion: previous,
|
|
732
|
+
toVersion: instance.version,
|
|
733
|
+
details: { result_id: resultId },
|
|
734
|
+
now,
|
|
735
|
+
});
|
|
736
|
+
this.write(selfAid, instance);
|
|
737
|
+
return instance;
|
|
738
|
+
}
|
|
551
739
|
discard(selfAid, handoffId, reason, details = {}, now = Date.now()) {
|
|
552
740
|
const instance = this.get(selfAid, handoffId);
|
|
553
741
|
if (!instance)
|
|
@@ -617,7 +805,8 @@ export class HandoffStore {
|
|
|
617
805
|
};
|
|
618
806
|
}
|
|
619
807
|
requestSummary(instance) {
|
|
620
|
-
|
|
808
|
+
const sent = classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
|
|
809
|
+
return instance.request.context ? `${sent}\n\n[本地交接上下文]\n${instance.request.context}` : sent;
|
|
621
810
|
}
|
|
622
811
|
selectReturnCandidate(input) {
|
|
623
812
|
if (input.handoffId) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from '../../utils/logger.js';
|
|
2
2
|
import { summarizeToolInput, toolInputForDisplay } from '../../utils/tool-summary.js';
|
|
3
3
|
import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
|
|
4
|
+
import { tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
4
5
|
import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
|
|
5
6
|
export class IMRenderer {
|
|
6
7
|
opts;
|
|
@@ -79,9 +80,9 @@ export class IMRenderer {
|
|
|
79
80
|
}
|
|
80
81
|
/** 是否有会抑制兜底错误消息的正文/过程内容;lifecycle 边界不算。 */
|
|
81
82
|
hasNonLifecycleContent() {
|
|
82
|
-
return this.textBuffer.length > 0 || this.itemsQueue.some(it => {
|
|
83
|
+
return this.textBuffer.trim().length > 0 || this.itemsQueue.some(it => {
|
|
83
84
|
if (it.kind === 'text')
|
|
84
|
-
return it.text.length > 0;
|
|
85
|
+
return it.text.trim().length > 0;
|
|
85
86
|
return it.kind !== 'started' && it.kind !== 'completed';
|
|
86
87
|
});
|
|
87
88
|
}
|
|
@@ -95,6 +96,21 @@ export class IMRenderer {
|
|
|
95
96
|
return;
|
|
96
97
|
if (this.textBuffer.length === 0)
|
|
97
98
|
return;
|
|
99
|
+
// 文件标记过滤后若只剩空白,应立即消费并告警;尤其避免 final-only
|
|
100
|
+
// 模式把工具调用前的空白继续带到最终回复。
|
|
101
|
+
const rawText = this.opts.fileMarkerPattern
|
|
102
|
+
? this.textBuffer.replace(this.opts.fileMarkerPattern, '')
|
|
103
|
+
: this.textBuffer;
|
|
104
|
+
if (!rawText.trim()) {
|
|
105
|
+
if (this.timer) {
|
|
106
|
+
clearTimeout(this.timer);
|
|
107
|
+
this.timer = undefined;
|
|
108
|
+
}
|
|
109
|
+
this.textBuffer = '';
|
|
110
|
+
this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
|
|
111
|
+
this.hasSendableText(rawText, 'flushText');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
98
114
|
if (this.suppressIntermediateText()) {
|
|
99
115
|
this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
|
|
100
116
|
return;
|
|
@@ -103,14 +119,8 @@ export class IMRenderer {
|
|
|
103
119
|
clearTimeout(this.timer);
|
|
104
120
|
this.timer = undefined;
|
|
105
121
|
}
|
|
106
|
-
// 文件标记过滤:tool_use 前的文本块也不能把 marker 暴露给用户
|
|
107
|
-
const rawText = this.opts.fileMarkerPattern
|
|
108
|
-
? this.textBuffer.replace(this.opts.fileMarkerPattern, '')
|
|
109
|
-
: this.textBuffer;
|
|
110
122
|
this.textBuffer = '';
|
|
111
|
-
|
|
112
|
-
return;
|
|
113
|
-
// 清掉 itemsQueue 中的 text items(已发出)
|
|
123
|
+
// 清掉已消费的 text items,包括被拦截的纯空白正文。
|
|
114
124
|
this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
|
|
115
125
|
const payload = { kind: 'result.text', text: rawText, isFinal: false };
|
|
116
126
|
this.sentContent = true;
|
|
@@ -164,6 +174,7 @@ export class IMRenderer {
|
|
|
164
174
|
this.emitProgress('text', outputTokens, turn);
|
|
165
175
|
if (!text)
|
|
166
176
|
return;
|
|
177
|
+
// 保留流式片段之间的空格/换行;仅在发送完整正文时判定是否纯空白。
|
|
167
178
|
// 同一窗口内连续 text delta 合并到最后一个 text item
|
|
168
179
|
const last = this.itemsQueue[this.itemsQueue.length - 1];
|
|
169
180
|
if (last && last.kind === 'text') {
|
|
@@ -302,6 +313,8 @@ export class IMRenderer {
|
|
|
302
313
|
async sendOperationalNoticeAsText(text) {
|
|
303
314
|
if (!this.opts.operationalNoticesAsText || this.opts.envelope.chatmode === 'proactive' || !text)
|
|
304
315
|
return false;
|
|
316
|
+
if (!this.hasSendableText(text, 'operational-notice'))
|
|
317
|
+
return false;
|
|
305
318
|
// 保持与流式正文的到达顺序:先发出已缓冲的正文,再投影运行状态。
|
|
306
319
|
await this.flushText();
|
|
307
320
|
const payload = { kind: 'result.text', text, isFinal: false };
|
|
@@ -315,6 +328,17 @@ export class IMRenderer {
|
|
|
315
328
|
synthCallId() {
|
|
316
329
|
return `synth-${this.opts.envelope.taskId}-${++this.syntheticCallSeq}`;
|
|
317
330
|
}
|
|
331
|
+
/** Reject blank bodies before sending, without trimming meaningful formatting. */
|
|
332
|
+
hasSendableText(text, source) {
|
|
333
|
+
if (text.trim())
|
|
334
|
+
return true;
|
|
335
|
+
// An unused buffer is normal. A nonempty whitespace body is diagnostic evidence.
|
|
336
|
+
if (text.length > 0) {
|
|
337
|
+
const { envelope, agentAid } = this.opts;
|
|
338
|
+
logger.warn(`[IMRenderer] Attempted to send empty message, skipping: code=EMPTY_MESSAGE source=${source} task=${envelope.taskId} session=${envelope.sessionId ?? '<none>'} agent=${agentAid ?? envelope.agentName} textLength=${text.length} text=${JSON.stringify(text.slice(0, 80))}`);
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
318
342
|
/** Convert a structured failed receipt into the same error shape as a rejected sender. */
|
|
319
343
|
receiptError(receipt) {
|
|
320
344
|
return Object.assign(new Error(receipt.error), {
|
|
@@ -331,6 +355,18 @@ export class IMRenderer {
|
|
|
331
355
|
: undefined;
|
|
332
356
|
return value.code === 'OUTBOX_FULL' || cause?.code === 'OUTBOX_FULL';
|
|
333
357
|
}
|
|
358
|
+
isPermanentActivityError(error) {
|
|
359
|
+
const value = error && typeof error === 'object' ? error : {};
|
|
360
|
+
const cause = value.cause && typeof value.cause === 'object'
|
|
361
|
+
? value.cause
|
|
362
|
+
: undefined;
|
|
363
|
+
const code = String(value.code ?? cause?.code ?? '');
|
|
364
|
+
return value.permanent === true
|
|
365
|
+
|| value.status === 'failed'
|
|
366
|
+
|| code === 'AUN_SEND_FAILED'
|
|
367
|
+
|| code === '-1'
|
|
368
|
+
|| /payload is too large/i.test(String(value.message ?? value.error ?? ''));
|
|
369
|
+
}
|
|
334
370
|
/**
|
|
335
371
|
* Serialize durable sends while keeping the chain usable after a rejected
|
|
336
372
|
* operation. Activity queue saturation is best-effort: it is logged, but
|
|
@@ -344,9 +380,19 @@ export class IMRenderer {
|
|
|
344
380
|
}
|
|
345
381
|
});
|
|
346
382
|
const handled = operation.catch(error => {
|
|
347
|
-
if (payload.kind === 'activity.batch'
|
|
348
|
-
|
|
349
|
-
|
|
383
|
+
if (payload.kind === 'activity.batch') {
|
|
384
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
385
|
+
if (this.isOutboxFull(error)) {
|
|
386
|
+
logger.warn(`[IMRenderer] activity outbox full; continuing without activity batch: ${detail}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (this.isPermanentActivityError(error)) {
|
|
390
|
+
// Activity is best-effort. A permanent structured-send failure (for
|
|
391
|
+
// example an AUN payload-size rejection) must not requeue the same
|
|
392
|
+
// item indefinitely or prevent the final result.text from flushing.
|
|
393
|
+
logger.warn(`[IMRenderer] activity delivery failed; dropping activity batch and continuing: ${detail}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
350
396
|
}
|
|
351
397
|
throw error;
|
|
352
398
|
});
|
|
@@ -432,22 +478,25 @@ export class IMRenderer {
|
|
|
432
478
|
// 文件标记过滤:marker 任何时候都不该出现在用户文本里,必须在 *每次* flush
|
|
433
479
|
// (含非最终的定时 flush)执行。否则文本块若滞留 buffer 后被定时器触发,会走
|
|
434
480
|
// 下方非-final 的 result.text 路径,把 [SEND_FILE:...] 原样发给用户。
|
|
435
|
-
//
|
|
481
|
+
// 先保留空白,以便空正文告警仍能记录原长度。
|
|
436
482
|
if (this.opts.fileMarkerPattern) {
|
|
437
|
-
|
|
438
|
-
const replaced = s.replace(this.opts.fileMarkerPattern, '');
|
|
439
|
-
return isFinal ? replaced.trim() : replaced;
|
|
440
|
-
};
|
|
441
|
-
this.textBuffer = strip(this.textBuffer);
|
|
483
|
+
this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '');
|
|
442
484
|
for (const item of this.itemsQueue) {
|
|
443
485
|
if (item.kind === 'text')
|
|
444
486
|
item.text = item.text.replace(this.opts.fileMarkerPattern, '');
|
|
445
487
|
}
|
|
446
488
|
}
|
|
489
|
+
if (!this.hasSendableText(this.textBuffer, isFinal ? 'final' : 'flush')) {
|
|
490
|
+
this.textBuffer = '';
|
|
491
|
+
}
|
|
492
|
+
else if (isFinal && this.opts.fileMarkerPattern) {
|
|
493
|
+
// 保持既有最终文件标记过滤语义;中间文本的格式原样保留。
|
|
494
|
+
this.textBuffer = this.textBuffer.trim();
|
|
495
|
+
}
|
|
447
496
|
// 清掉空 text items
|
|
448
497
|
const items = this.itemsQueue.filter(it => {
|
|
449
498
|
if (it.kind === 'text')
|
|
450
|
-
return it.text.length > 0;
|
|
499
|
+
return it.text.trim().length > 0;
|
|
451
500
|
return true;
|
|
452
501
|
});
|
|
453
502
|
this.itemsQueue = [];
|
|
@@ -544,6 +593,8 @@ export class IMRenderer {
|
|
|
544
593
|
if (!item)
|
|
545
594
|
return;
|
|
546
595
|
if (item.kind === 'text') {
|
|
596
|
+
if (!this.hasSendableText(item.text, 'proactive'))
|
|
597
|
+
return;
|
|
547
598
|
this.hasEmittedText = true;
|
|
548
599
|
this.allText += item.text;
|
|
549
600
|
}
|
|
@@ -633,7 +684,7 @@ export class IMRenderer {
|
|
|
633
684
|
if (isContextTooLongText(event.error || ''))
|
|
634
685
|
return null;
|
|
635
686
|
const text = getErrorMessage(new Error(event.error || '任务执行失败'), undefined, false);
|
|
636
|
-
return { kind: 'notice', text, severity: 'warn' };
|
|
687
|
+
return { kind: 'notice', text: tagErrorTextWithEvolcoreVersion(text), severity: 'warn' };
|
|
637
688
|
}
|
|
638
689
|
case 'complete': {
|
|
639
690
|
if (event.queryFinal === false)
|
|
@@ -650,7 +701,7 @@ export class IMRenderer {
|
|
|
650
701
|
const text = getErrorMessage(new Error(errText), event.terminalReason, false);
|
|
651
702
|
const item = {
|
|
652
703
|
kind: 'summary',
|
|
653
|
-
text,
|
|
704
|
+
text: tagErrorTextWithEvolcoreVersion(text),
|
|
654
705
|
is_error: true,
|
|
655
706
|
};
|
|
656
707
|
if (event.subtype !== undefined)
|
|
@@ -25,7 +25,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
|
|
|
25
25
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
26
26
|
import { renderActionAsText } from '../interaction-router.js';
|
|
27
27
|
import { planApprovalRoute } from '../permission/approval-gateway.js';
|
|
28
|
-
import { parseFullAccessCommand } from '../command/slash-gate.js';
|
|
28
|
+
import { guardGroupSlashCommand, parseFullAccessCommand } from '../command/slash-gate.js';
|
|
29
29
|
import { isFullAccessEnabled } from '../../config-store.js';
|
|
30
30
|
import { auditFullAccessEvent } from '../auth/authorization-audit.js';
|
|
31
31
|
import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './message-admission.js';
|
|
@@ -500,9 +500,31 @@ export class MessageBridge {
|
|
|
500
500
|
async sendAdmissionControlReply(context, msg, sendReply, text) {
|
|
501
501
|
if (!text || !consumeAdmissionReplyBudget(context))
|
|
502
502
|
return false;
|
|
503
|
-
|
|
503
|
+
if (context.chatType === 'group' && /^\s*\/(?:bind|request)(?:\s|$)/i.test(msg.content)) {
|
|
504
|
+
await this.sendSlashControlReply(context.channelKey, msg, sendReply, { kind: 'command.error', text });
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
await sendReply(msg.channelId, text, msg.replyContext);
|
|
508
|
+
}
|
|
504
509
|
return true;
|
|
505
510
|
}
|
|
511
|
+
/** Controls handled before the normal slash handler still need structured group feedback. */
|
|
512
|
+
async sendSlashControlReply(channel, msg, sendReply, payload, adapter = this.processor.getChannelInfo?.(channel)?.adapter) {
|
|
513
|
+
if (msg.chatType === 'group' && adapter?.send) {
|
|
514
|
+
await adapter.send(buildEnvelope({
|
|
515
|
+
taskId: `cmd-${randomBytes(5).toString('hex')}`,
|
|
516
|
+
channel,
|
|
517
|
+
channelId: msg.channelId,
|
|
518
|
+
chatmode: 'interactive',
|
|
519
|
+
replyContext: {
|
|
520
|
+
...msg.replyContext,
|
|
521
|
+
...(msg.threadId ? { threadId: msg.threadId } : {}),
|
|
522
|
+
},
|
|
523
|
+
}), payload);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
await sendReply(msg.channelId, payload.text, msg.replyContext);
|
|
527
|
+
}
|
|
506
528
|
admissionDeniedNotice(context, channelType) {
|
|
507
529
|
if (context.contactStatus === 'pending') {
|
|
508
530
|
return '联系人申请审核中;可再次发送 /request 更新说明。';
|
|
@@ -801,6 +823,16 @@ export class MessageBridge {
|
|
|
801
823
|
: { isMenu: false };
|
|
802
824
|
let menuResponseWarning;
|
|
803
825
|
if (menuControl.isMenu) {
|
|
826
|
+
// Menu is an AUN private control plane. Group messages must never
|
|
827
|
+
// execute Menu commands or participate in token issuance/rotation.
|
|
828
|
+
if (msg.chatType === 'group') {
|
|
829
|
+
this.logMenuDiagnostic('unsupported-chat-type', channelName, msg, menuControl, 'MENU_PRIVATE_ONLY');
|
|
830
|
+
logMenuRequestCompleted(menuControl.request, undefined, this.menuFlowContext(channelName, msg), {
|
|
831
|
+
delivery: 'dropped',
|
|
832
|
+
reason: 'unsupported-chat-type',
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
804
836
|
// AUN captures this mark at its adapter boundary. Legacy/test
|
|
805
837
|
// adapters fall back to the bridge entry mark captured above.
|
|
806
838
|
Object.assign(msg, menuTiming);
|
|
@@ -837,7 +869,10 @@ export class MessageBridge {
|
|
|
837
869
|
const tokenSupplied = Object.prototype.hasOwnProperty.call(menuControl.request, 'menu_token');
|
|
838
870
|
const tokenValid = (menuTokenRequired || tokenSupplied)
|
|
839
871
|
&& !!tokenOwnerAid
|
|
840
|
-
&& this.menuTokenStore.use(tokenOwnerAid, msg.peerId, menuControl.request.menu_token
|
|
872
|
+
&& this.menuTokenStore.use(tokenOwnerAid, msg.peerId, menuControl.request.menu_token, {
|
|
873
|
+
deviceId: msg.peerDeviceId,
|
|
874
|
+
slotId: msg.peerSlotId,
|
|
875
|
+
});
|
|
841
876
|
if (menuTokenRequired && !tokenValid) {
|
|
842
877
|
this.logMenuDiagnostic('token-required', channelName, msg, menuControl, 'MENU_TOKEN_REQUIRED');
|
|
843
878
|
await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, {
|
|
@@ -915,6 +950,11 @@ export class MessageBridge {
|
|
|
915
950
|
// internal per-call authorization carried into ResponseEngine.
|
|
916
951
|
const fullAccessCommand = parseFullAccessCommand(content);
|
|
917
952
|
if (fullAccessCommand.matched) {
|
|
953
|
+
const groupGuard = guardGroupSlashCommand(content, chatType);
|
|
954
|
+
if (groupGuard?.kind === 'command.error') {
|
|
955
|
+
await this.sendSlashControlReply(channelName, msg, sendReply, groupGuard, adapter);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
918
958
|
const reject = async (text) => {
|
|
919
959
|
logger.warn(`[MessageBridge] fullaccess command rejected: channel=${channelName} `
|
|
920
960
|
+ `actor=${authSubject.principalId ?? authSubject.actorId ?? '<none>'} reason=${text}`);
|
|
@@ -1002,6 +1042,13 @@ export class MessageBridge {
|
|
|
1002
1042
|
// 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
|
|
1003
1043
|
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
1004
1044
|
const cmdContent = contentForCmd || content;
|
|
1045
|
+
if (msg.source !== 'handoff' && !executionPermissionOverride) {
|
|
1046
|
+
const groupGuard = guardGroupSlashCommand(cmdContent, chatType);
|
|
1047
|
+
if (groupGuard?.kind === 'command.error') {
|
|
1048
|
+
await this.sendSlashControlReply(channelName, msg, sendReply, groupGuard, adapter);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1005
1052
|
const isCmd = msg.source !== 'handoff'
|
|
1006
1053
|
// Once /fa has been accepted, its remainder is always model input.
|
|
1007
1054
|
// A prompt such as `/fa /status` must not be re-routed as the
|
|
@@ -1411,7 +1458,10 @@ export class MessageBridge {
|
|
|
1411
1458
|
}
|
|
1412
1459
|
}
|
|
1413
1460
|
const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
1414
|
-
|
|
1461
|
+
// Separate installations can generate identical request IDs. Token
|
|
1462
|
+
// issuance and replay must remain scoped to the authenticated requester.
|
|
1463
|
+
const dedupKey = [msg.selfAID || channel, channelType, msg.chatType || 'private', scope, msg.peerId,
|
|
1464
|
+
msg.peerDeviceId || '', msg.peerSlotId || '', msg.threadId || '', parsed.id].join('\u001f');
|
|
1415
1465
|
let afterResponse;
|
|
1416
1466
|
const deduped = await this.menuDeduper.execute(dedupKey, menuPayloadFingerprint(parsed.raw), () => isTokenRequest
|
|
1417
1467
|
? this.issueMenuTokenResponse(header, channel, msg)
|
|
@@ -1452,7 +1502,9 @@ export class MessageBridge {
|
|
|
1452
1502
|
case 'menu.list':
|
|
1453
1503
|
if (args?.view === 'catalog') {
|
|
1454
1504
|
validateMenuCatalogArgs(args);
|
|
1455
|
-
const data = await this.cmdHandler.getMenuCatalog(channel, msg.channelId, authSubject, args.context, msg.isControlChannel ? 'control' : 'agent'
|
|
1505
|
+
const data = await this.cmdHandler.getMenuCatalog(channel, msg.channelId, authSubject, args.context, msg.isControlChannel ? 'control' : 'agent', !msg.isControlChannel || (msg.encrypted === true
|
|
1506
|
+
&& msg.protectedHeaders?.auth_method !== 'remote-access-token'
|
|
1507
|
+
&& msg.protectedHeaders?.auth_method !== 'ecweb-token'));
|
|
1456
1508
|
return menuSuccess(header, data);
|
|
1457
1509
|
}
|
|
1458
1510
|
if (args?.view !== undefined) {
|
|
@@ -1615,6 +1667,9 @@ export class MessageBridge {
|
|
|
1615
1667
|
clientName: this.menuTokenHeaderValue(headers.client_name ?? headers.app_name),
|
|
1616
1668
|
clientVersion: this.menuTokenHeaderValue(headers.client_version ?? headers.app_version),
|
|
1617
1669
|
evolVersion: this.menuTokenHeaderValue(headers.evol_version),
|
|
1670
|
+
}, {
|
|
1671
|
+
deviceId: msg.peerDeviceId,
|
|
1672
|
+
slotId: msg.peerSlotId,
|
|
1618
1673
|
});
|
|
1619
1674
|
if (!issued) {
|
|
1620
1675
|
return menuFailure(header, { code: 'MENU_TOKEN_REJECTED', message: '授权申请未通过' });
|
|
@@ -1732,20 +1787,25 @@ export class MessageBridge {
|
|
|
1732
1787
|
logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
|
|
1733
1788
|
const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
|
|
1734
1789
|
const cmdResult = await this.cmdHandler.handle(content, channel, channelId, async (_cid, text, opts, updatePayload) => {
|
|
1735
|
-
|
|
1790
|
+
const payload = updatePayload ?? (chatType === 'group' ? { kind: 'command.result', text } : undefined);
|
|
1791
|
+
if (payload && adapter?.send) {
|
|
1736
1792
|
const envelope = buildEnvelope({
|
|
1737
1793
|
taskId: `cmd-update-${randomBytes(5).toString('hex')}`,
|
|
1738
1794
|
channel,
|
|
1739
1795
|
channelId,
|
|
1740
1796
|
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|
|
1741
1797
|
chatmode: 'interactive',
|
|
1742
|
-
replyContext:
|
|
1798
|
+
replyContext: {
|
|
1799
|
+
...(opts ?? {}),
|
|
1800
|
+
...(replyContext ?? {}),
|
|
1801
|
+
metadata: { ...opts?.metadata, ...replyContext?.metadata },
|
|
1802
|
+
},
|
|
1743
1803
|
});
|
|
1744
|
-
await adapter.send(envelope,
|
|
1804
|
+
await adapter.send(envelope, payload);
|
|
1745
1805
|
return;
|
|
1746
1806
|
}
|
|
1747
1807
|
await sendReply(text, opts, updatePayload);
|
|
1748
|
-
}, userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
|
|
1808
|
+
}, userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext, true);
|
|
1749
1809
|
logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
|
|
1750
1810
|
if (cmdResult === undefined)
|
|
1751
1811
|
return false;
|