evolcore 0.0.20 → 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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- 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/msg.md +16 -0
- 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 +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
package/dist/paths.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { DAEMON_CONFIG_FILENAME, DEFAULT_RUNTIME_HOME_DIRNAME, INSTANCE_SOCKET_FILENAME, RUNTIME_HOME_ENV, WINDOWS_PIPE_PREFIX, } from './product.js';
|
|
6
|
+
import { migrateLegacyAgentJsonlLogs } from './core/audit/log-integrity.js';
|
|
6
7
|
const isWindows = process.platform === 'win32';
|
|
7
8
|
let _root = null;
|
|
8
9
|
export function resolveRoot() {
|
|
@@ -118,9 +119,6 @@ export function agentVenuesDir(aid) {
|
|
|
118
119
|
export function agentSessionsDir(aid) {
|
|
119
120
|
return path.join(agentDir(aid), 'sessions');
|
|
120
121
|
}
|
|
121
|
-
export function agentLogsDir(aid) {
|
|
122
|
-
return path.join(agentDir(aid), 'logs');
|
|
123
|
-
}
|
|
124
122
|
export function agentDataDir(aid) {
|
|
125
123
|
return path.join(agentDir(aid), 'data');
|
|
126
124
|
}
|
|
@@ -133,6 +131,9 @@ export function agentHandoffDir(aid) {
|
|
|
133
131
|
export function agentOutboxPath(aid) {
|
|
134
132
|
return path.join(agentDataDir(aid), 'outbox.jsonl');
|
|
135
133
|
}
|
|
134
|
+
export function agentActivityOutboxPath(aid) {
|
|
135
|
+
return path.join(agentDataDir(aid), 'activity-outbox.jsonl');
|
|
136
|
+
}
|
|
136
137
|
/** Directory-safe channel instance key used by connector runtime state. */
|
|
137
138
|
function encodePathKey(value) {
|
|
138
139
|
return encodeURIComponent(value);
|
|
@@ -161,11 +162,84 @@ export function channelStatePath(channelKey, ...parts) {
|
|
|
161
162
|
export function agentTriggersDir(aid) {
|
|
162
163
|
return path.join(agentDir(aid), 'triggers');
|
|
163
164
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
function stripTrailingWindowsSeparators(value) {
|
|
166
|
+
const stripped = value.replace(/[\\/]+$/g, '');
|
|
167
|
+
// Keep a drive-root separator. `C:` is a drive-relative path on Windows.
|
|
168
|
+
if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
|
|
169
|
+
return `${stripped}\\`;
|
|
168
170
|
}
|
|
171
|
+
return stripped;
|
|
172
|
+
}
|
|
173
|
+
function normalizeWindowsSocketRoot(root) {
|
|
174
|
+
// Use win32 semantics explicitly: EVOLCORE_HOME can come from Git Bash even
|
|
175
|
+
// when the daemon itself runs under native Windows Node.
|
|
176
|
+
return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
|
|
177
|
+
}
|
|
178
|
+
function windowsSocketPathForRoot(root) {
|
|
179
|
+
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
|
|
180
|
+
return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
|
|
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
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Return the canonical and pre-normalization Windows daemon pipe names.
|
|
202
|
+
*
|
|
203
|
+
* The extra candidates keep a daemon started by an older EvolCore build
|
|
204
|
+
* reachable while it is still running after the CLI has been upgraded.
|
|
205
|
+
*/
|
|
206
|
+
export function getWindowsInstanceSocketPathCandidates(root) {
|
|
207
|
+
const normalized = path.win32.normalize(root);
|
|
208
|
+
const normalizedForward = stripTrailingWindowsSeparators(normalized.replace(/\\/g, '/'));
|
|
209
|
+
const canonical = normalizeWindowsSocketRoot(root);
|
|
210
|
+
const baseVariants = [
|
|
211
|
+
canonical,
|
|
212
|
+
root,
|
|
213
|
+
normalized,
|
|
214
|
+
normalizedForward,
|
|
215
|
+
canonical.replace(/\\/g, '/'),
|
|
216
|
+
root.replace(/\\/g, '/'),
|
|
217
|
+
root.replace(/\//g, '\\'),
|
|
218
|
+
];
|
|
219
|
+
const variants = new Set();
|
|
220
|
+
for (const variant of baseVariants) {
|
|
221
|
+
const stripped = stripTrailingWindowsSeparators(variant);
|
|
222
|
+
variants.add(variant);
|
|
223
|
+
variants.add(stripped);
|
|
224
|
+
if (stripped) {
|
|
225
|
+
variants.add(`${stripped}\\`);
|
|
226
|
+
variants.add(`${stripped}/`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const candidates = [];
|
|
231
|
+
for (const variant of variants) {
|
|
232
|
+
const socket = windowsSocketPathForRoot(variant);
|
|
233
|
+
if (seen.has(socket))
|
|
234
|
+
continue;
|
|
235
|
+
seen.add(socket);
|
|
236
|
+
candidates.push(socket);
|
|
237
|
+
}
|
|
238
|
+
return candidates;
|
|
239
|
+
}
|
|
240
|
+
function resolveInstanceSocketPath(root) {
|
|
241
|
+
if (isWindows)
|
|
242
|
+
return getWindowsInstanceSocketPathCandidates(root)[0];
|
|
169
243
|
const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
|
|
170
244
|
// Linux/macOS Unix-domain sockets have a small sockaddr path limit
|
|
171
245
|
// (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
|
|
@@ -196,6 +270,7 @@ export function ensureDataDirs() {
|
|
|
196
270
|
fs.mkdirSync(p.outboxDir, { recursive: true });
|
|
197
271
|
fs.mkdirSync(p.eckDir, { recursive: true });
|
|
198
272
|
fs.mkdirSync(aidsDir(), { recursive: true });
|
|
273
|
+
migrateLegacyAgentJsonlLogs(p.agentsDir, p.logs);
|
|
199
274
|
migrateFromAun();
|
|
200
275
|
}
|
|
201
276
|
const MIGRATION_MARKER = '.migrated-from-aun';
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* (Phase 6)真正需要时接入 Runner,当前提供明确的"未实现"占位,避免空壳代码。
|
|
13
13
|
*/
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import fs from 'fs';
|
|
16
15
|
export class ResponseModeContextBuilder {
|
|
17
16
|
/** (sessionId::modeId) → sessionState,跨消息保持 */
|
|
18
17
|
stateCache = new Map();
|
|
@@ -22,7 +21,7 @@ export class ResponseModeContextBuilder {
|
|
|
22
21
|
*/
|
|
23
22
|
build(modeId, deps) {
|
|
24
23
|
const sessionState = this.getOrCreateState(deps.session.id, modeId);
|
|
25
|
-
const dataDir =
|
|
24
|
+
const dataDir = path.join(deps.agentDir, 'response-system', modeId);
|
|
26
25
|
return {
|
|
27
26
|
session: deps.session,
|
|
28
27
|
agentConfig: deps.agentConfig,
|
|
@@ -63,9 +62,4 @@ export class ResponseModeContextBuilder {
|
|
|
63
62
|
}
|
|
64
63
|
return state;
|
|
65
64
|
}
|
|
66
|
-
ensureDataDir(agentDir, modeId) {
|
|
67
|
-
const dir = path.join(agentDir, 'response-system', modeId);
|
|
68
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
-
return dir;
|
|
70
|
-
}
|
|
71
65
|
}
|
|
@@ -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;
|
package/dist/trigger/feedback.js
CHANGED
|
@@ -37,8 +37,11 @@ export class TriggerFeedbackDispatcher {
|
|
|
37
37
|
const result = await this.dispatchDirect(input, renderedText, target);
|
|
38
38
|
if (result.error)
|
|
39
39
|
return { status: 'failed', reason: result.error.code, feedback, effects: result.effects, error: result.error };
|
|
40
|
+
const replyStatus = statusFromReply(input.reply);
|
|
40
41
|
return {
|
|
41
|
-
status:
|
|
42
|
+
status: result.effects.some(effect => effect.status === 'queued') && replyStatus !== 'failed'
|
|
43
|
+
? 'degraded'
|
|
44
|
+
: replyStatus,
|
|
42
45
|
reason: reasonFromReply(input.reply),
|
|
43
46
|
feedback,
|
|
44
47
|
effects: result.effects,
|
|
@@ -204,6 +207,7 @@ export class TriggerFeedbackDispatcher {
|
|
|
204
207
|
originChannelType: input.trigger.origin?.channelType
|
|
205
208
|
?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
|
|
206
209
|
originControl: input.trigger.origin?.channelKey === 'daemon',
|
|
210
|
+
authorizedBy: input.trigger.authorizedBy,
|
|
207
211
|
},
|
|
208
212
|
};
|
|
209
213
|
outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
|
|
@@ -247,12 +251,24 @@ export class TriggerFeedbackDispatcher {
|
|
|
247
251
|
};
|
|
248
252
|
}
|
|
249
253
|
if (event?.type === 'trigger:skipped') {
|
|
254
|
+
const interruption = event.executionState === 'interrupted' && event.decisionSource === 'infrastructure'
|
|
255
|
+
? {
|
|
256
|
+
reasonCode: event.reasonCode ?? event.reason,
|
|
257
|
+
decisionSource: event.decisionSource,
|
|
258
|
+
executionState: event.executionState,
|
|
259
|
+
...(event.generation !== undefined ? { generation: event.generation } : {}),
|
|
260
|
+
...(event.currentGeneration !== undefined ? { currentGeneration: event.currentGeneration } : {}),
|
|
261
|
+
}
|
|
262
|
+
: undefined;
|
|
250
263
|
return {
|
|
251
|
-
status: 'failed',
|
|
264
|
+
status: interruption ? 'skipped' : 'failed',
|
|
252
265
|
reason: event.reason,
|
|
266
|
+
...(interruption ? { interruption } : {}),
|
|
253
267
|
feedback,
|
|
254
268
|
effects: [this.inboundEffect('success', target, session.id, startedAt)],
|
|
255
|
-
error:
|
|
269
|
+
error: interruption
|
|
270
|
+
? null
|
|
271
|
+
: { code: event.reason || 'target_session_skipped', message: event.reason || 'target session skipped' },
|
|
256
272
|
};
|
|
257
273
|
}
|
|
258
274
|
return {
|
|
@@ -379,7 +395,7 @@ export class TriggerFeedbackDispatcher {
|
|
|
379
395
|
if (isAunTarget && !targetChatType(target)) {
|
|
380
396
|
throw Object.assign(new Error('AUN trigger feedback requires an explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
|
|
381
397
|
}
|
|
382
|
-
await raceWithAbort(binding.adapter.send(buildEnvelope({
|
|
398
|
+
const receipt = await raceWithAbort(binding.adapter.send(buildEnvelope({
|
|
383
399
|
taskId: `trigger:${input.runId}`,
|
|
384
400
|
channel: binding.adapter.channelKey,
|
|
385
401
|
channelId: target.channelId,
|
|
@@ -394,16 +410,60 @@ export class TriggerFeedbackDispatcher {
|
|
|
394
410
|
isFinal: true,
|
|
395
411
|
format: 'plain',
|
|
396
412
|
}), cancellation.signal);
|
|
397
|
-
|
|
413
|
+
const common = {
|
|
398
414
|
type: 'message.send',
|
|
399
|
-
status: 'success',
|
|
400
415
|
channelKey: target.channelKey,
|
|
401
416
|
channelType: channelTypeFromKey(target.channelKey),
|
|
402
417
|
channelId: target.channelId,
|
|
403
418
|
attempt,
|
|
404
419
|
startedAt,
|
|
405
420
|
finishedAt: Date.now(),
|
|
406
|
-
}
|
|
421
|
+
};
|
|
422
|
+
if (receipt?.status === 'sent' && receipt.messageId) {
|
|
423
|
+
effects.push({ ...common, status: 'success', operationId: receipt.operationId, messageId: receipt.messageId });
|
|
424
|
+
return { effects, error: null };
|
|
425
|
+
}
|
|
426
|
+
if (receipt?.status === 'queued') {
|
|
427
|
+
effects.push({
|
|
428
|
+
...common,
|
|
429
|
+
status: 'queued',
|
|
430
|
+
operationId: receipt.operationId,
|
|
431
|
+
outboxId: receipt.outboxId,
|
|
432
|
+
error: receipt.error,
|
|
433
|
+
code: receipt.code,
|
|
434
|
+
});
|
|
435
|
+
return { effects, error: null };
|
|
436
|
+
}
|
|
437
|
+
if (receipt?.status === 'failed') {
|
|
438
|
+
effects.push({
|
|
439
|
+
...common,
|
|
440
|
+
status: 'failed',
|
|
441
|
+
operationId: receipt.operationId,
|
|
442
|
+
outboxId: receipt.outboxId,
|
|
443
|
+
error: receipt.error,
|
|
444
|
+
code: receipt.code,
|
|
445
|
+
});
|
|
446
|
+
return {
|
|
447
|
+
effects,
|
|
448
|
+
error: {
|
|
449
|
+
code: String(receipt.code ?? 'send_failed'),
|
|
450
|
+
message: receipt.error,
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (receipt?.status === 'suppressed') {
|
|
455
|
+
const message = `channel suppressed trigger feedback: ${receipt.reason}`;
|
|
456
|
+
effects.push({ ...common, status: 'skipped', error: message, code: 'SEND_SUPPRESSED' });
|
|
457
|
+
return { effects, error: { code: 'send_suppressed', message } };
|
|
458
|
+
}
|
|
459
|
+
if (isAunTarget) {
|
|
460
|
+
const message = 'AUN adapter returned no delivery receipt';
|
|
461
|
+
effects.push({ ...common, status: 'failed', error: message, code: 'MISSING_SEND_RECEIPT' });
|
|
462
|
+
return { effects, error: { code: 'missing_send_receipt', message } };
|
|
463
|
+
}
|
|
464
|
+
// Non-AUN adapters are still allowed to use the legacy void result
|
|
465
|
+
// until their receipt migration is complete.
|
|
466
|
+
effects.push({ ...common, status: 'success' });
|
|
407
467
|
return { effects, error: null };
|
|
408
468
|
}
|
|
409
469
|
catch (err) {
|
|
@@ -527,6 +587,9 @@ export class TriggerFeedbackDispatcher {
|
|
|
527
587
|
const status = Number(err?.status ?? err?.statusCode ?? err?.code);
|
|
528
588
|
const rawCode = String(err?.code ?? err?.errorCode ?? '').toLowerCase();
|
|
529
589
|
const lower = `${message} ${rawCode}`.toLowerCase();
|
|
590
|
+
if (rawCode === 'outbox_full') {
|
|
591
|
+
return { retryable: false, code: 'OUTBOX_FULL', message };
|
|
592
|
+
}
|
|
530
593
|
if (status === 408 || status === 429 || status >= 500) {
|
|
531
594
|
return { retryable: true, code: 'send_retryable_error', message };
|
|
532
595
|
}
|
package/dist/trigger/history.js
CHANGED
|
@@ -2,7 +2,11 @@ import crypto from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { isManagedSessionRuntimeDir, SESSION_RUNTIME_DIR_ENV } from '../cli/task-context.js';
|
|
5
|
+
import { findMessageLogOperationMessageId } from '../core/message/message-log.js';
|
|
6
|
+
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
7
|
+
import { resolvePaths } from '../paths.js';
|
|
5
8
|
import { definitionRevision } from './validation.js';
|
|
9
|
+
import * as aunOutbox from '../aun/outbox.js';
|
|
6
10
|
export class TriggerHistoryStore {
|
|
7
11
|
rootDir;
|
|
8
12
|
agentAid;
|
|
@@ -66,7 +70,7 @@ export class TriggerHistoryStore {
|
|
|
66
70
|
continue;
|
|
67
71
|
if (triggerId && event.triggerId !== triggerId)
|
|
68
72
|
continue;
|
|
69
|
-
out.push(event);
|
|
73
|
+
out.push(hydrateHistoryEvent(event));
|
|
70
74
|
}
|
|
71
75
|
return out;
|
|
72
76
|
}
|
|
@@ -80,7 +84,7 @@ export class TriggerHistoryStore {
|
|
|
80
84
|
if (!event || !isRunEvent(event))
|
|
81
85
|
continue;
|
|
82
86
|
if (event.agentAid === this.agentAid && event.triggerId === triggerId) {
|
|
83
|
-
out.push(event.audit);
|
|
87
|
+
out.push(hydrateTriggerAudit(event.audit));
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
return out;
|
|
@@ -95,7 +99,7 @@ export class TriggerHistoryStore {
|
|
|
95
99
|
const event = parseHistoryEvent(lines[i], this.agentAid);
|
|
96
100
|
if (!event || !isRunEvent(event) || !pending.has(event.triggerId))
|
|
97
101
|
continue;
|
|
98
|
-
latest.set(event.triggerId, event.audit);
|
|
102
|
+
latest.set(event.triggerId, hydrateTriggerAudit(event.audit));
|
|
99
103
|
pending.delete(event.triggerId);
|
|
100
104
|
}
|
|
101
105
|
return latest;
|
|
@@ -125,7 +129,7 @@ export class TriggerHistoryStore {
|
|
|
125
129
|
}
|
|
126
130
|
if (!isRunEvent(event))
|
|
127
131
|
continue;
|
|
128
|
-
const record = event.audit;
|
|
132
|
+
const record = hydrateTriggerAudit(event.audit);
|
|
129
133
|
if (record.status !== 'completed'
|
|
130
134
|
&& record.status !== 'degraded'
|
|
131
135
|
&& record.status !== 'noop'
|
|
@@ -310,6 +314,77 @@ function finiteNumber(value) {
|
|
|
310
314
|
function nonNegativeInteger(value) {
|
|
311
315
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
312
316
|
}
|
|
317
|
+
function hydrateHistoryEvent(event) {
|
|
318
|
+
if (!isRunEvent(event))
|
|
319
|
+
return event;
|
|
320
|
+
return { ...event, audit: hydrateTriggerAudit(event.audit) };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolve queued AUN effects against the durable outbox at read time. The
|
|
324
|
+
* original audit is append-only, so this projection is what lets a later
|
|
325
|
+
* expiry become visibly failed without pretending that a missing outbox row
|
|
326
|
+
* was delivered.
|
|
327
|
+
*/
|
|
328
|
+
function hydrateTriggerAudit(audit) {
|
|
329
|
+
let changed = false;
|
|
330
|
+
let deliveryFailure;
|
|
331
|
+
const effects = audit.effects.map(effect => {
|
|
332
|
+
if (effect.status !== 'queued' || !effect.outboxId)
|
|
333
|
+
return effect;
|
|
334
|
+
const state = aunOutbox.deliveryState(audit.agentAid, effect.outboxId);
|
|
335
|
+
if (state.status === 'sent') {
|
|
336
|
+
changed = true;
|
|
337
|
+
return { ...effect, status: 'success', messageId: state.messageId };
|
|
338
|
+
}
|
|
339
|
+
if (state.status === 'failed') {
|
|
340
|
+
changed = true;
|
|
341
|
+
deliveryFailure ??= { code: String(state.code ?? 'OUTBOX_DELIVERY_FAILED'), message: state.error };
|
|
342
|
+
return { ...effect, status: 'failed', error: state.error, ...(state.code !== undefined ? { code: state.code } : {}) };
|
|
343
|
+
}
|
|
344
|
+
if (state.status === 'missing' && isAunEffect(effect)) {
|
|
345
|
+
const messageId = findDeliveredEffectMessageId(audit.agentAid, effect);
|
|
346
|
+
changed = true;
|
|
347
|
+
if (messageId) {
|
|
348
|
+
return { ...effect, status: 'success', messageId };
|
|
349
|
+
}
|
|
350
|
+
const code = 'OUTBOX_RESULT_MISSING';
|
|
351
|
+
const error = 'outbox delivery result is missing and no remote message_id was recorded';
|
|
352
|
+
deliveryFailure ??= { code, message: error };
|
|
353
|
+
return { ...effect, status: 'failed', error, code };
|
|
354
|
+
}
|
|
355
|
+
return effect;
|
|
356
|
+
});
|
|
357
|
+
if (!changed)
|
|
358
|
+
return audit;
|
|
359
|
+
if (!deliveryFailure) {
|
|
360
|
+
const hasQueuedEffect = effects.some(effect => effect.status === 'queued');
|
|
361
|
+
const hasFailedEffect = effects.some(effect => effect.status === 'failed');
|
|
362
|
+
const canRestoreReplyStatus = audit.status === 'degraded'
|
|
363
|
+
&& !hasQueuedEffect
|
|
364
|
+
&& !hasFailedEffect
|
|
365
|
+
&& !audit.anomalies?.length;
|
|
366
|
+
const status = canRestoreReplyStatus
|
|
367
|
+
? audit.reply?.outcome === 'noop' ? 'noop' : 'completed'
|
|
368
|
+
: audit.status;
|
|
369
|
+
return { ...audit, status, effects };
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
...audit,
|
|
373
|
+
status: 'failed',
|
|
374
|
+
reason: deliveryFailure.code,
|
|
375
|
+
effects,
|
|
376
|
+
error: deliveryFailure,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function isAunEffect(effect) {
|
|
380
|
+
return effect.channelType === 'aun' || effect.channelKey?.startsWith('aun#') === true;
|
|
381
|
+
}
|
|
382
|
+
function findDeliveredEffectMessageId(agentAid, effect) {
|
|
383
|
+
if (!effect.operationId || !effect.channelId)
|
|
384
|
+
return undefined;
|
|
385
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', effect.channelId, agentAid);
|
|
386
|
+
return findMessageLogOperationMessageId(chatDir, effect.operationId);
|
|
387
|
+
}
|
|
313
388
|
function listAuditLogFiles(logDir) {
|
|
314
389
|
let entries;
|
|
315
390
|
try {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const HISTORY_CHANNELS = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
|
|
2
1
|
/**
|
|
3
2
|
* Turns legacy raw session-file references into the controlled history CLI.
|
|
4
3
|
* The replacement deliberately keeps the explicit peer/channel scope but
|
|
5
4
|
* never carries a filesystem location into a Trigger prompt.
|
|
6
5
|
*/
|
|
7
6
|
export function rewriteLegacyTriggerSessionPaths(prompt, triggerAid) {
|
|
7
|
+
const historyChannels = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
|
|
8
8
|
const legacyPath = /data\/sessions\/([A-Za-z0-9_-]+)\/([^/\s`"'<>]+)(?:\/([^/\s`"'<>]+))?\/messages\.jsonl/g;
|
|
9
9
|
return prompt.replace(legacyPath, (match, channel, first, second) => {
|
|
10
|
-
if (!
|
|
10
|
+
if (!historyChannels.has(channel))
|
|
11
11
|
return match;
|
|
12
12
|
const isAun = channel === 'aun';
|
|
13
13
|
const target = isAun ? second : first;
|
package/dist/trigger/parser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CronExpressionParser } from 'cron-parser';
|
|
|
2
2
|
const TRIGGER_UPDATE_FLAGS = new Set([
|
|
3
3
|
'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
|
|
4
4
|
'prompt', 'name',
|
|
5
|
+
'script-file',
|
|
5
6
|
'model', 'effort', 'permission',
|
|
6
7
|
'max-runs', 'max-duration', 'concurrency', 'missed-policy',
|
|
7
8
|
]);
|
|
@@ -113,6 +114,7 @@ const PERMISSION_MODES = new Set([
|
|
|
113
114
|
'auto',
|
|
114
115
|
'request',
|
|
115
116
|
'bypass',
|
|
117
|
+
'fullaccess',
|
|
116
118
|
]);
|
|
117
119
|
const TRIGGER_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
118
120
|
function parsePermissionModeFlag(flags, opts = {}) {
|
|
@@ -124,10 +126,10 @@ function parsePermissionModeFlag(flags, opts = {}) {
|
|
|
124
126
|
if (raw === 'inherit') {
|
|
125
127
|
if (opts.allowInherit)
|
|
126
128
|
return { ok: true, value: null };
|
|
127
|
-
return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request 或
|
|
129
|
+
return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request、bypass 或 fullaccess' };
|
|
128
130
|
}
|
|
129
131
|
if (!PERMISSION_MODES.has(raw)) {
|
|
130
|
-
return { ok: false, error: '--permission 只接受 readonly、auto、request 或
|
|
132
|
+
return { ok: false, error: '--permission 只接受 readonly、auto、request、bypass 或 fullaccess' };
|
|
131
133
|
}
|
|
132
134
|
return { ok: true, value: raw };
|
|
133
135
|
}
|
|
@@ -551,7 +553,7 @@ export function parseTriggerUpdateArgv(nameOrId, args, opts = {}) {
|
|
|
551
553
|
const parsedFlags = flagsFromArgv(args, 'update');
|
|
552
554
|
if (!parsedFlags.ok)
|
|
553
555
|
return { ok: false, error: withPromptFileHint(parsedFlags.error, args, opts) };
|
|
554
|
-
const parsed = parseTriggerUpdateFlags(nameOrId, parsedFlags.flags, opts);
|
|
556
|
+
const parsed = parseTriggerUpdateFlags(nameOrId, parsedFlags.flags, { ...opts, allowScriptFile: true });
|
|
555
557
|
return parsed.ok ? parsed : { ok: false, error: withPromptFileHint(parsed.error, args, opts) };
|
|
556
558
|
}
|
|
557
559
|
function flagsFromArgv(args, command) {
|
|
@@ -584,6 +586,9 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
|
|
|
584
586
|
if (!TRIGGER_UPDATE_FLAGS.has(flag))
|
|
585
587
|
return { ok: false, error: `update 不支持参数 --${flag}` };
|
|
586
588
|
}
|
|
589
|
+
if (flags.has('script-file') && !opts.allowScriptFile) {
|
|
590
|
+
return { ok: false, error: '--script-file 仅支持 ec trigger update CLI;请通过受控 CLI 上传脚本文件' };
|
|
591
|
+
}
|
|
587
592
|
const parsed = commonParsed(flags, { update: true, ...opts });
|
|
588
593
|
if (!parsed.ok)
|
|
589
594
|
return parsed;
|
|
@@ -606,6 +611,11 @@ function parseTriggerUpdateFlags(nameOrId, flags, opts = {}) {
|
|
|
606
611
|
update.name = value.name;
|
|
607
612
|
if (value.prompt !== undefined)
|
|
608
613
|
update.prompt = value.prompt;
|
|
614
|
+
const scriptFile = optionalFlagString(flags, 'script-file');
|
|
615
|
+
if ('error' in scriptFile)
|
|
616
|
+
return { ok: false, error: scriptFile.error };
|
|
617
|
+
if (scriptFile.value !== undefined)
|
|
618
|
+
update.scriptFile = scriptFile.value;
|
|
609
619
|
if (value.model !== undefined)
|
|
610
620
|
update.model = value.model;
|
|
611
621
|
if (value.effort !== undefined)
|
|
@@ -120,12 +120,12 @@ export class TriggerRuntimeScheduler {
|
|
|
120
120
|
}
|
|
121
121
|
listItems(definitions) {
|
|
122
122
|
const latestRuns = this.audit.latest(definitions.map(definition => definition.id));
|
|
123
|
-
return definitions.map(definition => this.buildListItem(definition, latestRuns.get(definition.id)));
|
|
123
|
+
return definitions.map(definition => this.buildListItem(definition, latestRuns.get(definition.id), this.stats(definition.id)));
|
|
124
124
|
}
|
|
125
125
|
listItem(definition) {
|
|
126
|
-
return this.buildListItem(definition, this.audit.recent(definition.id, 1)[0]);
|
|
126
|
+
return this.buildListItem(definition, this.audit.recent(definition.id, 1)[0], this.stats(definition.id));
|
|
127
127
|
}
|
|
128
|
-
buildListItem(definition, lastRun) {
|
|
128
|
+
buildListItem(definition, lastRun, stats = this.stats(definition.id)) {
|
|
129
129
|
const schedule = this.state.readSchedule(definition.id);
|
|
130
130
|
return {
|
|
131
131
|
id: definition.id,
|
|
@@ -139,6 +139,10 @@ export class TriggerRuntimeScheduler {
|
|
|
139
139
|
...(definition.feedback.target ? { target: definition.feedback.target } : {}),
|
|
140
140
|
},
|
|
141
141
|
...(schedule ? { nextFireAt: schedule.nextFireAt } : {}),
|
|
142
|
+
fireCount: stats.fireCount,
|
|
143
|
+
failCount: stats.failCount,
|
|
144
|
+
...(stats.lastFiredAt !== undefined ? { lastFiredAt: stats.lastFiredAt } : {}),
|
|
145
|
+
...(stats.lastResult ? { lastResult: stats.lastResult } : {}),
|
|
142
146
|
...(lastRun ? { lastStatus: lastRun.status, lastFinishedAt: lastRun.finishedAt } : {}),
|
|
143
147
|
updatedAt: definition.updatedAt,
|
|
144
148
|
};
|
|
@@ -627,6 +631,7 @@ export class TriggerRuntimeScheduler {
|
|
|
627
631
|
effects: feedbackResult.effects,
|
|
628
632
|
error: feedbackResult.error,
|
|
629
633
|
conflictRunId: feedbackResult.conflictRunId,
|
|
634
|
+
interruption: feedbackResult.interruption,
|
|
630
635
|
causation,
|
|
631
636
|
});
|
|
632
637
|
if (!payload.dryRun) {
|
|
@@ -1147,6 +1152,7 @@ export class TriggerRuntimeScheduler {
|
|
|
1147
1152
|
anomalies,
|
|
1148
1153
|
effects: feedbackResult.effects,
|
|
1149
1154
|
error: feedbackResult.error,
|
|
1155
|
+
interruption: feedbackResult.interruption,
|
|
1150
1156
|
causation,
|
|
1151
1157
|
});
|
|
1152
1158
|
this.audit.write(audit);
|
|
@@ -1469,6 +1475,7 @@ export class TriggerRuntimeScheduler {
|
|
|
1469
1475
|
status: input.status,
|
|
1470
1476
|
reason: input.reason,
|
|
1471
1477
|
conflictRunId: input.conflictRunId,
|
|
1478
|
+
...(input.interruption ? { interruption: input.interruption } : {}),
|
|
1472
1479
|
...(activeRun?.executionSessionId ? { executionSessionId: activeRun.executionSessionId } : {}),
|
|
1473
1480
|
...(attempts?.length ? { attempts } : {}),
|
|
1474
1481
|
...(finalAttempt ? { finalAttempt: finalAttempt.attempt } : {}),
|
|
@@ -1783,6 +1790,7 @@ export class TriggerRuntimeScheduler {
|
|
|
1783
1790
|
type: 'trigger:skipped',
|
|
1784
1791
|
...base,
|
|
1785
1792
|
reason: audit.reason ?? 'skipped',
|
|
1793
|
+
...(audit.interruption ?? {}),
|
|
1786
1794
|
});
|
|
1787
1795
|
}
|
|
1788
1796
|
}
|
|
@@ -1900,6 +1908,15 @@ function errorReply(runId, durationMs, reason, text) {
|
|
|
1900
1908
|
}
|
|
1901
1909
|
function attemptSummaryFromExecution(attempt, attemptId, execution) {
|
|
1902
1910
|
if (execution.feedbackResult) {
|
|
1911
|
+
if (execution.feedbackResult.interruption) {
|
|
1912
|
+
return {
|
|
1913
|
+
status: 'interrupted',
|
|
1914
|
+
error: {
|
|
1915
|
+
code: execution.feedbackResult.interruption.reasonCode,
|
|
1916
|
+
message: execution.feedbackResult.reason ?? execution.feedbackResult.interruption.reasonCode,
|
|
1917
|
+
},
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1903
1920
|
const failed = execution.feedbackResult.status !== 'completed';
|
|
1904
1921
|
return {
|
|
1905
1922
|
status: failed ? 'failed' : 'completed',
|
|
@@ -13,6 +13,7 @@ const PERMISSION_MODES = new Set([
|
|
|
13
13
|
'auto',
|
|
14
14
|
'request',
|
|
15
15
|
'bypass',
|
|
16
|
+
'fullaccess',
|
|
16
17
|
]);
|
|
17
18
|
export function normalizeTriggerDefinition(input, opts = {}) {
|
|
18
19
|
if (!isObject(input))
|
|
@@ -234,6 +235,7 @@ function normalizeV3Phase2(raw, opts) {
|
|
|
234
235
|
enabled: raw.enabled === undefined ? true : requiredBoolean(raw.enabled, 'enabled'),
|
|
235
236
|
name,
|
|
236
237
|
description: optionalString(raw.description),
|
|
238
|
+
authorizedBy: optionalString(raw.authorizedBy),
|
|
237
239
|
createdAt,
|
|
238
240
|
updatedAt,
|
|
239
241
|
origin: normalizeOrigin(raw.origin),
|
|
@@ -438,6 +440,9 @@ function normalizeExecution(value, _opts) {
|
|
|
438
440
|
onError: normalizeOnError(raw.onError),
|
|
439
441
|
noopSentinel: optionalString(raw.noopSentinel) ?? DEFAULT_NOOP_SENTINEL,
|
|
440
442
|
};
|
|
443
|
+
if (execution.type === 'script' && execution.permissionMode === 'fullaccess') {
|
|
444
|
+
throw new Error('script Trigger already executes as the daemon OS user and cannot declare execution.permissionMode=fullaccess');
|
|
445
|
+
}
|
|
441
446
|
return execution;
|
|
442
447
|
}
|
|
443
448
|
function normalizeExecutionBaseagent(value) {
|
|
@@ -478,7 +483,7 @@ function normalizeTriggerPermissionMode(value) {
|
|
|
478
483
|
if (mode === 'edit' || mode === 'noask' || mode === 'plan') {
|
|
479
484
|
return normalizePermissionModeContract(mode).mode;
|
|
480
485
|
}
|
|
481
|
-
throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass');
|
|
486
|
+
throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass, fullaccess');
|
|
482
487
|
}
|
|
483
488
|
function normalizeScript(value) {
|
|
484
489
|
if (!isObject(value))
|