evolcore 0.0.21 → 0.0.22
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 +43 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/claude-runner.js +14 -0
- package/dist/agents/codex-app-server-client.js +31 -5
- package/dist/agents/codex-runner.js +926 -121
- package/dist/aun/outbox.js +7 -0
- package/dist/channels/aun.js +209 -35
- package/dist/cli/daemon-commands.js +29 -8
- package/dist/cli/task-context.js +4 -0
- package/dist/cli/trigger-command.js +13 -4
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/core/auth/agent-delegation.js +12 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +66 -6
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +21 -0
- package/dist/core/command/menu-handler.js +9 -0
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/slash-handler.js +41 -18
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +21 -0
- package/dist/core/message/response-engine.js +172 -41
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +11 -10
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +46 -5
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +64 -5
- package/dist/ipc.js +97 -17
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/parser.js +10 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/logger.js +9 -4
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +2 -2
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
|
|
7
|
-
import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent,
|
|
7
|
+
import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
|
|
8
8
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
9
9
|
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
10
10
|
export function resolveManagedTempDir(env = process.env) {
|
|
@@ -1966,7 +1966,31 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1966
1966
|
policyCode: 'managed_ec_discovery_forbidden',
|
|
1967
1967
|
};
|
|
1968
1968
|
}
|
|
1969
|
-
const
|
|
1969
|
+
const parserOptions = {
|
|
1970
|
+
verifyShellExecutable: process.platform === 'win32',
|
|
1971
|
+
...(context.delegationCarrier
|
|
1972
|
+
? { expectedDelegationToken: context.delegationCarrier }
|
|
1973
|
+
: {}),
|
|
1974
|
+
};
|
|
1975
|
+
const parsedCodex = context.sessionId ? parseCodexToolCommand(input, parserOptions) : null;
|
|
1976
|
+
if (context.sessionId && parsedCodex && !parsedCodex.ok && parsedCodex.issue === 'untrusted-shell') {
|
|
1977
|
+
return {
|
|
1978
|
+
behavior: 'deny',
|
|
1979
|
+
input,
|
|
1980
|
+
message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
|
|
1981
|
+
policyCode: 'ec_shell_untrusted_executable',
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
if (context.sessionId && parsedCodex?.ok && parsedCodex.delegationToken
|
|
1985
|
+
&& !context.delegationCarrier) {
|
|
1986
|
+
return {
|
|
1987
|
+
behavior: 'deny',
|
|
1988
|
+
input,
|
|
1989
|
+
message: '🔒 PowerShell 命令中的 EVOLCORE_DELEGATION_TOKEN 必须与当前托管 thread carrier 绑定;无法确认时拒绝执行。',
|
|
1990
|
+
policyCode: 'delegation_token_unbound',
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
const argv = parsedCodex?.ok ? parsedCodex.argv : null;
|
|
1970
1994
|
const managedAidArgv = argv?.at(-2) === '--format' && argv.at(-1) === 'json'
|
|
1971
1995
|
? argv.slice(0, -2)
|
|
1972
1996
|
: argv;
|
|
@@ -2003,9 +2027,18 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2003
2027
|
}
|
|
2004
2028
|
const shellDialect = stringShellCarrier?.dialect
|
|
2005
2029
|
?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2008
|
-
|
|
2030
|
+
const hasPowerShellCarrierAssignment = stringShellCarrier?.dialect === 'powershell'
|
|
2031
|
+
&& hasPowerShellDelegationAssignment(stringShellCarrier.command);
|
|
2032
|
+
const ecCommand = hasPowerShellCarrierAssignment && parsedCodex
|
|
2033
|
+
? parsedCodex.ok && parsedCodex.argv[0] === 'ec'
|
|
2034
|
+
? { kind: 'literal' }
|
|
2035
|
+
: {
|
|
2036
|
+
kind: 'composite',
|
|
2037
|
+
issue: parsedCodex.ok ? 'shell-composition' : parsedCodex.issue,
|
|
2038
|
+
}
|
|
2039
|
+
: classifyEvolcoreShellCommand(policyCommand, shellDialect
|
|
2040
|
+
? { dialect: shellDialect, ...parserOptions }
|
|
2041
|
+
: parserOptions);
|
|
2009
2042
|
if (ecCommand.kind === 'literal') {
|
|
2010
2043
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
2011
2044
|
}
|
|
@@ -2013,6 +2046,14 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2013
2046
|
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
2014
2047
|
}
|
|
2015
2048
|
if (ecCommand.kind === 'composite') {
|
|
2049
|
+
if (ecCommand.issue === 'untrusted-shell') {
|
|
2050
|
+
return {
|
|
2051
|
+
behavior: 'deny',
|
|
2052
|
+
input,
|
|
2053
|
+
message: '🔒 Shell 可执行文件未通过 Windows Authenticode 签名校验,禁止进入 EvolCore EC 特权路径。',
|
|
2054
|
+
policyCode: 'ec_shell_untrusted_executable',
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2016
2057
|
if (ecCommand.issue === 'unsafe-expansion') {
|
|
2017
2058
|
const message = shellDialect === 'powershell'
|
|
2018
2059
|
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
@@ -828,6 +828,7 @@ export class SessionManager {
|
|
|
828
828
|
}
|
|
829
829
|
// Create new session
|
|
830
830
|
const sessionMetadata = { ...(metadata || {}) };
|
|
831
|
+
sessionMetadata.__sessionCreatedAt = Date.now();
|
|
831
832
|
const newIdentity = identity ?? this.resolveIdentity(channel, userId, chatType, identityConversationId);
|
|
832
833
|
const resolvedBaseagent = baseagent || 'claude';
|
|
833
834
|
const session = {
|
|
@@ -1111,6 +1112,33 @@ export class SessionManager {
|
|
|
1111
1112
|
}
|
|
1112
1113
|
return 'legacy_updated';
|
|
1113
1114
|
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Clear a main-session backend only when the durable latest snapshot still
|
|
1117
|
+
* points at the caller's expected ID. This is the recovery counterpart to
|
|
1118
|
+
* provider-side "session not found" errors and must never rotate a topic.
|
|
1119
|
+
*/
|
|
1120
|
+
async clearMainSessionBackendIfMatches(sessionId, expectedAgentSessionId, metadataKeys = []) {
|
|
1121
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
1122
|
+
if (!loaded)
|
|
1123
|
+
return 'missing';
|
|
1124
|
+
const { current } = loaded;
|
|
1125
|
+
if (current.threadId)
|
|
1126
|
+
return 'topic';
|
|
1127
|
+
const currentId = normalizeBackendSessionId(current.agentSessionId);
|
|
1128
|
+
if (!currentId)
|
|
1129
|
+
return 'already_clear';
|
|
1130
|
+
if (currentId !== normalizeBackendSessionId(expectedAgentSessionId))
|
|
1131
|
+
return 'mismatch';
|
|
1132
|
+
current.agentSessionId = undefined;
|
|
1133
|
+
if (metadataKeys.length > 0 && current.metadata) {
|
|
1134
|
+
const metadata = { ...current.metadata };
|
|
1135
|
+
for (const key of metadataKeys)
|
|
1136
|
+
delete metadata[key];
|
|
1137
|
+
current.metadata = metadata;
|
|
1138
|
+
}
|
|
1139
|
+
this.persistSession(current, 'sync');
|
|
1140
|
+
return 'cleared';
|
|
1141
|
+
}
|
|
1114
1142
|
async switchAgent(channel, channelId, projectPath, newBaseagent) {
|
|
1115
1143
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1116
1144
|
const identity = this.deriveChannelIdentity(channel, channelId);
|
|
@@ -1424,6 +1452,7 @@ export class SessionManager {
|
|
|
1424
1452
|
throw new Error(`[SessionManager] createNewSession: baseagent is empty for channel=${channel} channelId=${channelId}`);
|
|
1425
1453
|
}
|
|
1426
1454
|
const metadata = newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata);
|
|
1455
|
+
metadata.__sessionCreatedAt = Date.now();
|
|
1427
1456
|
if (identityMetadata?.sessionBoundaryOperationId) {
|
|
1428
1457
|
metadata.sessionBoundaryOperationId = identityMetadata.sessionBoundaryOperationId;
|
|
1429
1458
|
}
|
|
@@ -1586,6 +1615,7 @@ export class SessionManager {
|
|
|
1586
1615
|
delete metadata.agentSessions;
|
|
1587
1616
|
delete metadata.resumeAt;
|
|
1588
1617
|
delete metadata.replyContext;
|
|
1618
|
+
metadata.__sessionRenewedAt = Date.now();
|
|
1589
1619
|
const session = {
|
|
1590
1620
|
id: generateSessionId(),
|
|
1591
1621
|
channel: sourceSession.channel,
|
|
@@ -13,6 +13,9 @@ const MAX_CONTEXT_CHARS = 16_000;
|
|
|
13
13
|
const MAX_MESSAGE_CHARS = 2_000;
|
|
14
14
|
const MODEL_TIMEOUT_MS = 10_000;
|
|
15
15
|
const RECENT_DECISION_TTL_MS = 30_000;
|
|
16
|
+
const FRESH_EMPTY_SESSION_TTL_MS = 30_000;
|
|
17
|
+
const RENEWED_AT_METADATA_KEY = '__sessionRenewedAt';
|
|
18
|
+
const CREATED_AT_METADATA_KEY = '__sessionCreatedAt';
|
|
16
19
|
const NEW_CONFIDENCE_THRESHOLD = 0.85;
|
|
17
20
|
const AUXILIARY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
18
21
|
const EXPLICIT_NEW_RE = /^(?:新话题|换个话题|重新开始|新开会话|新会话)[::,,;;。!!\n]/;
|
|
@@ -91,7 +94,9 @@ export class SessionRenewService {
|
|
|
91
94
|
const routeKey = this.routeKey(request);
|
|
92
95
|
return await this.withLock(routeKey, async () => {
|
|
93
96
|
const cached = this.recentDecisions.get(routeKey);
|
|
94
|
-
if (cached
|
|
97
|
+
if (cached
|
|
98
|
+
&& cached.expiresAt > this.now()
|
|
99
|
+
&& (cached.candidateSessionId === request.session.id || cached.selectedSessionId === request.session.id)) {
|
|
95
100
|
const selected = cached.selectedSessionId === request.session.id
|
|
96
101
|
? request.session
|
|
97
102
|
: await this.sessionManager.getSessionById(cached.selectedSessionId);
|
|
@@ -121,6 +126,18 @@ export class SessionRenewService {
|
|
|
121
126
|
if (!last) {
|
|
122
127
|
if (request.isNewSession)
|
|
123
128
|
return { session: request.session, renewed: false };
|
|
129
|
+
// A renewed session is intentionally created before its first inbound
|
|
130
|
+
// message is appended. A concurrent message can therefore observe an
|
|
131
|
+
// empty, freshly-created session. Treat that state as already renewed;
|
|
132
|
+
// do not create a second logical session for the same route.
|
|
133
|
+
const metadata = request.session.metadata;
|
|
134
|
+
const renewedAt = Number(metadata?.[RENEWED_AT_METADATA_KEY] ?? metadata?.[CREATED_AT_METADATA_KEY]);
|
|
135
|
+
const sessionAgeMs = Number.isFinite(renewedAt)
|
|
136
|
+
? Math.max(0, this.now() - renewedAt)
|
|
137
|
+
: Number.POSITIVE_INFINITY;
|
|
138
|
+
if (Number.isFinite(renewedAt) && renewedAt <= this.now() && sessionAgeMs <= FRESH_EMPTY_SESSION_TTL_MS) {
|
|
139
|
+
return { session: request.session, renewed: false, decision: 'continue', source: 'missing_history' };
|
|
140
|
+
}
|
|
124
141
|
const idleMs = this.now() - request.session.updatedAt;
|
|
125
142
|
return await this.finishDecision(routeKey, request, 'new', 'missing_history', idleMs, evaluationStartedAt);
|
|
126
143
|
}
|
|
@@ -48,10 +48,14 @@ export class SessionTurnCoordinator {
|
|
|
48
48
|
&& state.lastTerminal.taskId === lease.taskId
|
|
49
49
|
&& state.lastTerminal.generation === lease.generation;
|
|
50
50
|
}
|
|
51
|
-
invalidate(sessionId, reason) {
|
|
51
|
+
invalidate(sessionId, reason, expected) {
|
|
52
52
|
const state = this.states.get(sessionId);
|
|
53
53
|
if (!state)
|
|
54
54
|
return undefined;
|
|
55
|
+
if (expected?.taskId && state.active?.taskId !== expected.taskId)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (expected?.generation !== undefined && state.active?.generation !== expected.generation)
|
|
58
|
+
return undefined;
|
|
55
59
|
if (state.active?.status === 'interrupting' && state.active.cancelReason === reason) {
|
|
56
60
|
return cloneTurnMetadata(state);
|
|
57
61
|
}
|
package/dist/index.js
CHANGED
|
@@ -88,12 +88,12 @@ import { TriggerFeedbackDispatcher } from './trigger/feedback.js';
|
|
|
88
88
|
import { TriggerRuntimeScheduler } from './trigger/scheduler.js';
|
|
89
89
|
import { TargetSessionLockRegistry } from './trigger/session-lock.js';
|
|
90
90
|
import { DaemonChannel } from './channels/daemon.js';
|
|
91
|
-
import { definitionRevision, normalizeTriggerDefinition } from './trigger/validation.js';
|
|
91
|
+
import { definitionRevision, normalizeTriggerDefinition, splitScriptCommand } from './trigger/validation.js';
|
|
92
92
|
import { applyTriggerPatch } from './trigger/patch.js';
|
|
93
93
|
import { validateModelSelectionForRole } from './core/model/model-permission.js';
|
|
94
94
|
import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
|
|
95
95
|
import { atomicWriteJson } from './core/session/session-fs-store.js';
|
|
96
|
-
import { ensureProcessManagedTempDir } from './cli/task-context.js';
|
|
96
|
+
import { DAEMON_RUNTIME_EPOCH_ENV, ensureProcessManagedTempDir } from './cli/task-context.js';
|
|
97
97
|
import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
|
|
98
98
|
import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
|
|
99
99
|
import { MAIN_PACKAGE_NAME } from './product.js';
|
|
@@ -1211,6 +1211,9 @@ async function main() {
|
|
|
1211
1211
|
: null;
|
|
1212
1212
|
bindService?.startCleanup();
|
|
1213
1213
|
const agentDelegationRegistry = new AgentDelegationRegistry();
|
|
1214
|
+
// Replace any inherited marker from a parent/previous daemon before child
|
|
1215
|
+
// runners or in-process IPC clients are created.
|
|
1216
|
+
process.env[DAEMON_RUNTIME_EPOCH_ENV] = agentDelegationRegistry.getRuntimeEpoch();
|
|
1214
1217
|
// 创建命令处理器
|
|
1215
1218
|
const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
|
|
1216
1219
|
let agentApplicationService;
|
|
@@ -2305,6 +2308,7 @@ async function main() {
|
|
|
2305
2308
|
return { ok: false, code: delegation.code, error: delegation.reason };
|
|
2306
2309
|
return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
|
|
2307
2310
|
});
|
|
2311
|
+
ipcServer.setDaemonRuntimeEpoch(agentDelegationRegistry.getRuntimeEpoch());
|
|
2308
2312
|
// Register every IPC executor/provider before exposing the endpoint. The
|
|
2309
2313
|
// function declaration is hoisted, while its invocation remains here so a
|
|
2310
2314
|
// failed bind still aborts before any AUN connection is attempted.
|
|
@@ -2836,7 +2840,7 @@ async function main() {
|
|
|
2836
2840
|
for (const runner of new Set(agentMap.values())) {
|
|
2837
2841
|
if (typeof runner?.evaluatePreToolUse !== 'function')
|
|
2838
2842
|
continue;
|
|
2839
|
-
const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal);
|
|
2843
|
+
const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput, params.signal, params.callId);
|
|
2840
2844
|
if (result.applicable)
|
|
2841
2845
|
return result;
|
|
2842
2846
|
}
|
|
@@ -4003,17 +4007,59 @@ async function main() {
|
|
|
4003
4007
|
const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
|
|
4004
4008
|
if (!existing)
|
|
4005
4009
|
throw new Error(`trigger not found: ${cmd.triggerId}`);
|
|
4010
|
+
const rawPatch = cmd.patch && typeof cmd.patch === 'object' && !Array.isArray(cmd.patch)
|
|
4011
|
+
? cmd.patch
|
|
4012
|
+
: {};
|
|
4013
|
+
const rawScriptFile = rawPatch.scriptFile;
|
|
4014
|
+
if (rawScriptFile !== undefined && rawScriptFile !== true) {
|
|
4015
|
+
throw new Error('scriptFile must be true');
|
|
4016
|
+
}
|
|
4017
|
+
const patchScriptFile = rawScriptFile === true;
|
|
4018
|
+
const { scriptFile: _scriptFile, ...definitionPatch } = rawPatch;
|
|
4019
|
+
const scriptFileBase64 = cmd.scriptFileBase64;
|
|
4020
|
+
if (patchScriptFile && scriptFileBase64 === undefined) {
|
|
4021
|
+
throw new Error('--script-file requires scriptFileBase64');
|
|
4022
|
+
}
|
|
4023
|
+
if (!patchScriptFile && scriptFileBase64 !== undefined) {
|
|
4024
|
+
throw new Error('scriptFileBase64 requires --script-file');
|
|
4025
|
+
}
|
|
4026
|
+
if (scriptFileBase64 !== undefined) {
|
|
4027
|
+
if (typeof scriptFileBase64 !== 'string') {
|
|
4028
|
+
throw new Error('scriptFileBase64 must be a base64 string');
|
|
4029
|
+
}
|
|
4030
|
+
if (!actor.daemonPrivileged) {
|
|
4031
|
+
throw new Error('updating a script Trigger requires DaemonOwner');
|
|
4032
|
+
}
|
|
4033
|
+
if (existing.execution.type !== 'script' || !existing.execution.script) {
|
|
4034
|
+
throw new Error('--script-file only applies to script Triggers');
|
|
4035
|
+
}
|
|
4036
|
+
if (scriptFileBase64.length > 6 * 1024 * 1024
|
|
4037
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(scriptFileBase64)) {
|
|
4038
|
+
throw new Error('scriptFileBase64 is invalid or exceeds the 4 MiB limit');
|
|
4039
|
+
}
|
|
4040
|
+
const scriptBytes = Buffer.from(scriptFileBase64, 'base64');
|
|
4041
|
+
if (scriptBytes.length > 4 * 1024 * 1024)
|
|
4042
|
+
throw new Error('script file exceeds the 4 MiB limit');
|
|
4043
|
+
}
|
|
4006
4044
|
const currentRevision = definitionRevision(existing);
|
|
4007
4045
|
if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
|
|
4008
4046
|
throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
|
|
4009
4047
|
}
|
|
4010
|
-
let definition =
|
|
4048
|
+
let definition = Object.keys(definitionPatch).length > 0
|
|
4049
|
+
? applyTriggerPatch(existing, definitionPatch, { fromPromptFile: cmd.promptFile === true })
|
|
4050
|
+
: existing;
|
|
4011
4051
|
rejectUnapprovedFullAccessConfiguration(definition, actor);
|
|
4012
4052
|
definition = withFullAccessConfigurationProvenance(definition, actor);
|
|
4013
4053
|
validateTriggerDefinitionForActor(definition, actor);
|
|
4014
4054
|
validateTriggerFeedbackChannels(definition);
|
|
4015
4055
|
const scheduler = schedulerFor(agentAid);
|
|
4016
|
-
const
|
|
4056
|
+
const files = scriptFileBase64 !== undefined && existing.execution.script
|
|
4057
|
+
? [{
|
|
4058
|
+
relativePath: splitScriptCommand(existing.execution.script.path).path,
|
|
4059
|
+
contentBase64: scriptFileBase64,
|
|
4060
|
+
}]
|
|
4061
|
+
: [];
|
|
4062
|
+
const trigger = scheduler.update(cmd.triggerId, definition, files);
|
|
4017
4063
|
if (trigger.execution.permissionMode === 'fullaccess') {
|
|
4018
4064
|
auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, existing.execution.permissionMode === 'fullaccess'
|
|
4019
4065
|
? 'fullaccess Trigger updated through IPC'
|
|
@@ -4152,6 +4198,19 @@ async function main() {
|
|
|
4152
4198
|
await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.suspendForDaemonExit()));
|
|
4153
4199
|
});
|
|
4154
4200
|
await shutdownStep('interrupt active message queue tasks', async () => { await queueShutdown; });
|
|
4201
|
+
await shutdownStep('dispose agent runners', async () => {
|
|
4202
|
+
const currentInstances = [...agentMap.entries()].map(([key, agent]) => {
|
|
4203
|
+
const split = key.lastIndexOf('::');
|
|
4204
|
+
return {
|
|
4205
|
+
evolagentName: split > 0 ? key.slice(0, split) : '<unknown>',
|
|
4206
|
+
baseagent: split > 0 ? key.slice(split + 2) : 'unknown',
|
|
4207
|
+
agent,
|
|
4208
|
+
};
|
|
4209
|
+
});
|
|
4210
|
+
await disposeAgentInstances(currentInstances, (instance, error) => {
|
|
4211
|
+
logger.warn(`[Shutdown] Failed to dispose runner ${instance.evolagentName}::${instance.baseagent}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4212
|
+
});
|
|
4213
|
+
});
|
|
4155
4214
|
eventBus.publish({
|
|
4156
4215
|
type: 'system:shutdown',
|
|
4157
4216
|
timestamp: Date.now()
|
package/dist/ipc.js
CHANGED
|
@@ -8,18 +8,20 @@ import path from 'path';
|
|
|
8
8
|
import { logger } from './utils/logger.js';
|
|
9
9
|
import { withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
|
|
10
10
|
import { fileCache } from './core/daemon-file-cache.js';
|
|
11
|
+
import { DAEMON_RUNTIME_EPOCH_ENV } from './cli/task-context.js';
|
|
11
12
|
import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js';
|
|
12
13
|
import { AgentReloadBusyError } from './core/agent-reload-coordinator.js';
|
|
13
|
-
import { getWindowsInstanceSocketPathCandidates, resolvePaths } from './paths.js';
|
|
14
|
+
import { getWindowsInstanceSocketPathCandidates, normalizeIpcEndpoint, resolvePaths } from './paths.js';
|
|
14
15
|
import { getProcessStartTime, isSameOrOlderProcess } from './utils/process-introspect.js';
|
|
15
16
|
import { classifyProcessTree, ProcessTreeSampler, readProcessMetrics } from './utils/process-tree-stats.js';
|
|
16
17
|
import { readSystemMemoryUsage } from './utils/system-memory.js';
|
|
17
18
|
import { hashCurrentDelegatedCommand } from './core/auth/agent-delegation.js';
|
|
18
19
|
import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
|
|
19
20
|
const isWindows = process.platform === 'win32';
|
|
20
|
-
const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
|
|
21
|
+
const isNamedPipe = (p) => isWindows && p.toLowerCase().startsWith('\\\\.\\pipe\\');
|
|
21
22
|
const CPU_SAMPLE_INTERVAL_MS = 1000;
|
|
22
23
|
const PROCESS_TREE_SAMPLE_INTERVAL_MS = 5000;
|
|
24
|
+
const MONITOR_LEASE_TTL_MS = 15_000;
|
|
23
25
|
const PROCESS_TREE_SAMPLE_WARN_MS = 250;
|
|
24
26
|
const EVENT_LOOP_LAG_WARN_MS = 100;
|
|
25
27
|
const SAMPLE_WARN_LOG_INTERVAL_MS = 60_000;
|
|
@@ -43,6 +45,8 @@ const LOCAL_CONTROL_COMMANDS = new Set([
|
|
|
43
45
|
'evolagent.resync',
|
|
44
46
|
'menu.exec',
|
|
45
47
|
'monitor-snapshot',
|
|
48
|
+
'monitor-subscribe',
|
|
49
|
+
'monitor-unsubscribe',
|
|
46
50
|
'shutdown',
|
|
47
51
|
]);
|
|
48
52
|
let ipcQuerySequence = 0;
|
|
@@ -80,9 +84,9 @@ export function summarizeMonitorAgents(agents, aids) {
|
|
|
80
84
|
};
|
|
81
85
|
}
|
|
82
86
|
export class IpcServer {
|
|
83
|
-
socketPath;
|
|
84
87
|
getStatus;
|
|
85
88
|
commandExecutor;
|
|
89
|
+
socketPath;
|
|
86
90
|
server = null;
|
|
87
91
|
agentRegistry;
|
|
88
92
|
agentApplicationService;
|
|
@@ -122,6 +126,7 @@ export class IpcServer {
|
|
|
122
126
|
wecomContactBindExecutor;
|
|
123
127
|
wechatContactBindExecutor;
|
|
124
128
|
controlToken;
|
|
129
|
+
daemonRuntimeEpoch;
|
|
125
130
|
// CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
|
|
126
131
|
// 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
|
|
127
132
|
// procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
|
|
@@ -138,6 +143,7 @@ export class IpcServer {
|
|
|
138
143
|
processTreeWorkerRequestStartedAt = 0;
|
|
139
144
|
processTreeWorkerRequestTimer = null;
|
|
140
145
|
processTreeSamplingInFlight = false;
|
|
146
|
+
processTreeMonitorLeases = new Map();
|
|
141
147
|
lastCpuTickMono = null;
|
|
142
148
|
eventLoopLagMs = 0;
|
|
143
149
|
maxEventLoopLagMs = 0;
|
|
@@ -155,9 +161,9 @@ export class IpcServer {
|
|
|
155
161
|
lastError: null,
|
|
156
162
|
};
|
|
157
163
|
constructor(socketPath, getStatus, commandExecutor) {
|
|
158
|
-
this.socketPath = socketPath;
|
|
159
164
|
this.getStatus = getStatus;
|
|
160
165
|
this.commandExecutor = commandExecutor;
|
|
166
|
+
this.socketPath = normalizeIpcEndpoint(socketPath);
|
|
161
167
|
}
|
|
162
168
|
/** Inject EvolAgentRegistry for evolagent.* IPC handlers */
|
|
163
169
|
setAgentRegistry(registry) {
|
|
@@ -171,6 +177,10 @@ export class IpcServer {
|
|
|
171
177
|
setControlToken(token) {
|
|
172
178
|
this.controlToken = Buffer.from(token);
|
|
173
179
|
}
|
|
180
|
+
/** Set the process-lifetime marker used to reject stale managed children. */
|
|
181
|
+
setDaemonRuntimeEpoch(epoch) {
|
|
182
|
+
this.daemonRuntimeEpoch = epoch;
|
|
183
|
+
}
|
|
174
184
|
/** Inject menu.* executor (ECWeb Control proxies menu requests through this) */
|
|
175
185
|
setMenuExecutor(executor) {
|
|
176
186
|
this.menuExecutor = executor;
|
|
@@ -296,11 +306,10 @@ export class IpcServer {
|
|
|
296
306
|
setWechatContactBindExecutor(executor) {
|
|
297
307
|
this.wechatContactBindExecutor = executor;
|
|
298
308
|
}
|
|
299
|
-
/** Start the background CPU
|
|
309
|
+
/** Start the lightweight background CPU sampling loop. Call after start(). */
|
|
300
310
|
startCpuTracking() {
|
|
301
311
|
if (this.cpuTimer)
|
|
302
312
|
return;
|
|
303
|
-
this.sampleProcessTree();
|
|
304
313
|
this.lastCpuTickMono = performance.now();
|
|
305
314
|
this.cpuTimer = setInterval(() => {
|
|
306
315
|
const tickMono = performance.now();
|
|
@@ -342,17 +351,30 @@ export class IpcServer {
|
|
|
342
351
|
}, CPU_SAMPLE_INTERVAL_MS);
|
|
343
352
|
// Don't keep the event loop alive for sampling alone.
|
|
344
353
|
this.cpuTimer.unref?.();
|
|
345
|
-
// Process enumeration is substantially more expensive on Windows than
|
|
346
|
-
// process.cpuUsage()/os.cpus(), so keep it at a lower refresh rate.
|
|
347
|
-
this.processTreeTimer = setInterval(() => this.sampleProcessTree(), PROCESS_TREE_SAMPLE_INTERVAL_MS);
|
|
348
|
-
this.processTreeTimer.unref?.();
|
|
349
354
|
}
|
|
350
|
-
/** Stop
|
|
355
|
+
/** Stop all CPU and process-tree sampling loops. */
|
|
351
356
|
stopCpuTracking() {
|
|
352
357
|
if (this.cpuTimer) {
|
|
353
358
|
clearInterval(this.cpuTimer);
|
|
354
359
|
this.cpuTimer = null;
|
|
355
360
|
}
|
|
361
|
+
this.processTreeMonitorLeases.clear();
|
|
362
|
+
this.stopProcessTreeTracking();
|
|
363
|
+
this.lastCpuTickMono = null;
|
|
364
|
+
}
|
|
365
|
+
startProcessTreeTracking() {
|
|
366
|
+
if (this.processTreeTimer)
|
|
367
|
+
return;
|
|
368
|
+
this.sampleProcessTree();
|
|
369
|
+
this.processTreeTimer = setInterval(() => {
|
|
370
|
+
this.pruneExpiredMonitorLeases();
|
|
371
|
+
if (this.processTreeMonitorLeases.size === 0)
|
|
372
|
+
return;
|
|
373
|
+
this.sampleProcessTree();
|
|
374
|
+
}, PROCESS_TREE_SAMPLE_INTERVAL_MS);
|
|
375
|
+
this.processTreeTimer.unref?.();
|
|
376
|
+
}
|
|
377
|
+
stopProcessTreeTracking() {
|
|
356
378
|
if (this.processTreeTimer) {
|
|
357
379
|
clearInterval(this.processTreeTimer);
|
|
358
380
|
this.processTreeTimer = null;
|
|
@@ -372,7 +394,31 @@ export class IpcServer {
|
|
|
372
394
|
this.processTreeWorker = null;
|
|
373
395
|
if (worker)
|
|
374
396
|
void worker.terminate();
|
|
375
|
-
this.
|
|
397
|
+
this.processTreeSnapshot = null;
|
|
398
|
+
this.associatedProcessKinds.clear();
|
|
399
|
+
}
|
|
400
|
+
acquireProcessTreeMonitor(leaseId) {
|
|
401
|
+
this.processTreeMonitorLeases.set(leaseId, Date.now() + MONITOR_LEASE_TTL_MS);
|
|
402
|
+
this.startProcessTreeTracking();
|
|
403
|
+
}
|
|
404
|
+
refreshProcessTreeMonitor(leaseId) {
|
|
405
|
+
if (typeof leaseId !== 'string' || !this.processTreeMonitorLeases.has(leaseId))
|
|
406
|
+
return;
|
|
407
|
+
this.processTreeMonitorLeases.set(leaseId, Date.now() + MONITOR_LEASE_TTL_MS);
|
|
408
|
+
}
|
|
409
|
+
releaseProcessTreeMonitor(leaseId) {
|
|
410
|
+
this.processTreeMonitorLeases.delete(leaseId);
|
|
411
|
+
if (this.processTreeMonitorLeases.size === 0)
|
|
412
|
+
this.stopProcessTreeTracking();
|
|
413
|
+
}
|
|
414
|
+
pruneExpiredMonitorLeases() {
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
for (const [leaseId, expiresAt] of this.processTreeMonitorLeases) {
|
|
417
|
+
if (expiresAt <= now)
|
|
418
|
+
this.processTreeMonitorLeases.delete(leaseId);
|
|
419
|
+
}
|
|
420
|
+
if (this.processTreeMonitorLeases.size === 0)
|
|
421
|
+
this.stopProcessTreeTracking();
|
|
376
422
|
}
|
|
377
423
|
sampleProcessTree() {
|
|
378
424
|
if (isWindows) {
|
|
@@ -654,6 +700,17 @@ export class IpcServer {
|
|
|
654
700
|
if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
|
|
655
701
|
return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
|
|
656
702
|
}
|
|
703
|
+
if (typeof cmd.sessionId === 'string' && cmd.sessionId
|
|
704
|
+
&& typeof cmd.delegationToken === 'string' && cmd.delegationToken
|
|
705
|
+
&& this.daemonRuntimeEpoch
|
|
706
|
+
&& cmd.daemonRuntimeEpoch !== this.daemonRuntimeEpoch) {
|
|
707
|
+
logger.warn(`[IPC] stale delegation rejected: session=${ipcLogToken(cmd.sessionId)} type=${ipcLogToken(cmd.type)} cause=daemon_runtime_epoch_mismatch`);
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
code: 'DAEMON_RESTARTED',
|
|
711
|
+
error: 'The task delegation token belongs to a previous daemon runtime; start a new task authorization and do not retry this command in the current task',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
657
714
|
switch (cmd.type) {
|
|
658
715
|
case 'status':
|
|
659
716
|
return this.getStatus();
|
|
@@ -670,6 +727,7 @@ export class IpcServer {
|
|
|
670
727
|
threadId: cmd.threadId,
|
|
671
728
|
toolName: cmd.toolName,
|
|
672
729
|
toolInput: cmd.toolInput,
|
|
730
|
+
callId: typeof cmd.callId === 'string' ? cmd.callId : undefined,
|
|
673
731
|
signal: requestSignal,
|
|
674
732
|
});
|
|
675
733
|
}
|
|
@@ -1328,6 +1386,7 @@ export class IpcServer {
|
|
|
1328
1386
|
}
|
|
1329
1387
|
}
|
|
1330
1388
|
case 'monitor-snapshot': {
|
|
1389
|
+
this.refreshProcessTreeMonitor(cmd.leaseId);
|
|
1331
1390
|
// watch web Monitor 页用:进程级 + 系统级运行指标 + 全局 stats + per-agent 汇总。
|
|
1332
1391
|
const mem = process.memoryUsage();
|
|
1333
1392
|
const systemMemory = readSystemMemoryUsage();
|
|
@@ -1407,6 +1466,20 @@ export class IpcServer {
|
|
|
1407
1466
|
},
|
|
1408
1467
|
};
|
|
1409
1468
|
}
|
|
1469
|
+
case 'monitor-subscribe': {
|
|
1470
|
+
const leaseId = typeof cmd.leaseId === 'string' ? cmd.leaseId.trim() : '';
|
|
1471
|
+
if (!leaseId || leaseId.length > 128)
|
|
1472
|
+
return { ok: false, error: 'invalid monitor lease id' };
|
|
1473
|
+
this.acquireProcessTreeMonitor(leaseId);
|
|
1474
|
+
return { ok: true, leaseTtlMs: MONITOR_LEASE_TTL_MS };
|
|
1475
|
+
}
|
|
1476
|
+
case 'monitor-unsubscribe': {
|
|
1477
|
+
const leaseId = typeof cmd.leaseId === 'string' ? cmd.leaseId.trim() : '';
|
|
1478
|
+
if (!leaseId || leaseId.length > 128)
|
|
1479
|
+
return { ok: false, error: 'invalid monitor lease id' };
|
|
1480
|
+
this.releaseProcessTreeMonitor(leaseId);
|
|
1481
|
+
return { ok: true };
|
|
1482
|
+
}
|
|
1410
1483
|
default:
|
|
1411
1484
|
return { error: `unknown command: ${cmd.type}` };
|
|
1412
1485
|
}
|
|
@@ -1425,14 +1498,15 @@ export class IpcServer {
|
|
|
1425
1498
|
*/
|
|
1426
1499
|
/** Pass `null` as timeoutMs to wait without a client-side deadline. */
|
|
1427
1500
|
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1501
|
+
const endpoint = normalizeIpcEndpoint(socketPath);
|
|
1502
|
+
let socketCandidates = [endpoint];
|
|
1503
|
+
if (isNamedPipe(endpoint)) {
|
|
1430
1504
|
const paths = resolvePaths();
|
|
1431
|
-
if (
|
|
1505
|
+
if (endpoint === paths.socket)
|
|
1432
1506
|
socketCandidates = getWindowsInstanceSocketPathCandidates(paths.root);
|
|
1433
1507
|
}
|
|
1434
1508
|
if (socketCandidates.length <= 1)
|
|
1435
|
-
return ipcQueryOnce(
|
|
1509
|
+
return ipcQueryOnce(endpoint, cmd, timeoutMs);
|
|
1436
1510
|
return (async () => {
|
|
1437
1511
|
const startedAt = Date.now();
|
|
1438
1512
|
for (const candidate of socketCandidates) {
|
|
@@ -1464,7 +1538,13 @@ function ipcQueryOnce(socketPath, cmd, timeoutMs = 3000) {
|
|
|
1464
1538
|
&& typeof authenticatedCmd.delegationCommandHash !== 'string'
|
|
1465
1539
|
? hashCurrentDelegatedCommand()
|
|
1466
1540
|
: undefined;
|
|
1467
|
-
const request =
|
|
1541
|
+
const request = {
|
|
1542
|
+
...(delegationCommandHash ? { ...authenticatedCmd, delegationCommandHash } : authenticatedCmd),
|
|
1543
|
+
...(process.env[DAEMON_RUNTIME_EPOCH_ENV]
|
|
1544
|
+
&& typeof authenticatedCmd.delegationToken === 'string'
|
|
1545
|
+
? { daemonRuntimeEpoch: process.env[DAEMON_RUNTIME_EPOCH_ENV] }
|
|
1546
|
+
: {}),
|
|
1547
|
+
};
|
|
1468
1548
|
return new Promise((resolve) => {
|
|
1469
1549
|
const startedAtMono = performance.now();
|
|
1470
1550
|
const queryId = nextIpcQueryId(request);
|
package/dist/paths.js
CHANGED
|
@@ -179,6 +179,24 @@ function windowsSocketPathForRoot(root) {
|
|
|
179
179
|
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
|
|
180
180
|
return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Normalize an IPC endpoint before it reaches node:net.
|
|
184
|
+
*
|
|
185
|
+
* Windows does not support Unix-domain socket files at ordinary filesystem
|
|
186
|
+
* paths. Callers that construct an ad-hoc `.sock` path (tests and embedded
|
|
187
|
+
* runtimes in particular) must therefore use the same deterministic named
|
|
188
|
+
* pipe on both the server and client sides. Existing named pipes and all
|
|
189
|
+
* non-Windows endpoints are preserved verbatim.
|
|
190
|
+
*/
|
|
191
|
+
export function normalizeIpcEndpoint(endpoint) {
|
|
192
|
+
// Windows object-manager paths are case-insensitive. Preserve an explicit
|
|
193
|
+
// named pipe verbatim even when a caller spells the namespace as `PIPE`.
|
|
194
|
+
if (!isWindows || endpoint.toLowerCase().startsWith('\\\\.\\pipe\\'))
|
|
195
|
+
return endpoint;
|
|
196
|
+
const normalized = path.win32.resolve(endpoint).toLowerCase();
|
|
197
|
+
const hash = crypto.createHash('sha1').update(normalized).digest('hex').slice(0, 20);
|
|
198
|
+
return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-endpoint-${hash}`;
|
|
199
|
+
}
|
|
182
200
|
/**
|
|
183
201
|
* Return the canonical and pre-normalization Windows daemon pipe names.
|
|
184
202
|
*
|
|
@@ -197,6 +197,7 @@ export const proactiveFlow = {
|
|
|
197
197
|
const queueMsg = queueLen >= 5
|
|
198
198
|
? `⚠️ 有 ${queueLen} 条消息未读,请尽快完成当前任务,或使用 ec ctl queue 读取后同步处理。`
|
|
199
199
|
: `⚠️ 有 ${queueLen} 条消息未读,可使用 ec ctl queue 读取。`;
|
|
200
|
+
ctx.recordReminder?.({ kind: 'queue_unread', text: queueMsg, queueLength: queueLen });
|
|
200
201
|
ctx.injectToModel(queueMsg);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
@@ -208,13 +209,17 @@ export const proactiveFlow = {
|
|
|
208
209
|
state.toolReportPending = true;
|
|
209
210
|
const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
|
|
210
211
|
const target = state.chatType === 'group' ? '群里' : '对方';
|
|
211
|
-
|
|
212
|
+
const reportMsg = `⚠️ 工具调用已达到 ${state.toolCount} 次,请立即用 ${cmdHint} 向${target}汇报当前情况和下一步意图。`;
|
|
213
|
+
ctx.recordReminder?.({ kind: 'tool_report', text: reportMsg, toolCount: state.toolCount });
|
|
214
|
+
ctx.injectToModel(reportMsg);
|
|
212
215
|
}
|
|
213
216
|
},
|
|
214
217
|
// ─── onToolResult:成功发送回执后解除门禁 ───
|
|
215
218
|
onToolResult(ctx) {
|
|
216
219
|
const state = ctx.state.get(STATE_KEY);
|
|
217
|
-
if (!state
|
|
220
|
+
if (!state)
|
|
221
|
+
return;
|
|
222
|
+
if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
|
|
218
223
|
return;
|
|
219
224
|
if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error))
|
|
220
225
|
return;
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
import { resolvePriceRow, BILLING_FNS } from './billing.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
/** Keep wire/log metadata readable while preserving full precision in storage. */
|
|
9
|
+
export function roundCostForOutput(value) {
|
|
10
|
+
return Number.isFinite(value) ? Number(value.toFixed(4)) : value;
|
|
11
|
+
}
|
|
8
12
|
// 网关价格表缓存(从用户覆盖层 model-prices-gateway.jsonl 读取)
|
|
9
13
|
let _gatewayPriceCache = null;
|
|
10
14
|
let _gatewayPriceCacheTs = 0;
|