evolcore 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/install-codex-managed-hooks.mjs +26 -3
- package/dist/agents/claude-runner.js +11 -1
- package/dist/agents/codex/diagnostics.js +42 -11
- package/dist/agents/codex-runner.js +27 -3
- package/dist/agents/ecagent-runner.js +17 -7
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +113 -7
- package/dist/cli/aun-commands.js +37 -13
- package/dist/cli/daemon-commands.js +10 -4
- package/dist/cli/fs-command.js +1 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +46 -15
- package/dist/cli/thread-command.js +12 -4
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/core/auth/operation-authorizer.js +21 -2
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/command-handler.js +7 -1
- package/dist/core/command/menu-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/ipc-registration.js +11 -1
- package/dist/core/memory/batch-meta.js +1 -0
- package/dist/core/memory/consolidation-context.js +97 -14
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +55 -0
- package/dist/core/memory/memory-writer.js +20 -4
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/message-bridge.js +3 -3
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +15 -4
- package/dist/core/permission/ec-command-parser.js +76 -40
- package/dist/core/permission/tool-policy.js +32 -2
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/session/session-manager.js +56 -1
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/system-session/memory.js +87 -20
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +246 -32
- package/dist/utils/cross-platform.js +6 -3
- package/kits/docs/evolcore/fs-architecture.md +5 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +5 -3
- package/kits/docs/evolcore/memory-consolidation.md +1 -2
- package/kits/docs/evolcore/memory-tags.md +1 -1
- package/kits/docs/evolcore/memory.md +2 -2
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +25 -9
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -0
- package/kits/eck_manifest.memory.json +20 -0
- package/kits/rules/01-overview.md +3 -3
- package/kits/rules/07-memory-and-thread.md +1 -1
- package/kits/skills/ec-group/SKILL.md +6 -3
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/notes.md +1 -1
- package/kits/templates/memory-consolidation/workflow.md +36 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/memory.md +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.0.35 (2026-10-19)
|
|
9
|
+
|
|
10
|
+
### 权限与记忆
|
|
11
|
+
|
|
12
|
+
- 规范 Claude Code 与 ecagent 的 Bash 调用说明,要求明确命令目标并标注低、中、高风险等级;缺失或格式错误时提供具体提示,避免权限模式不同导致调用说明不一致。
|
|
13
|
+
- agent 级记忆整理会扫描所有关系记忆并按关系归类,避免遗漏其他对端的过期记忆。
|
|
14
|
+
- 首次设定尚未完成时阻止记忆整理会话启动,避免记忆被提前认领后无法正常处理。
|
|
15
|
+
- 细化只读 Shell 操作的拒绝原因,提升受限命令的诊断可读性。
|
|
16
|
+
|
|
17
|
+
### Codex 与运行稳定性
|
|
18
|
+
|
|
19
|
+
- 保留 Codex 模型服务重连进度,并在最终失败时展示实际重试次数,避免只显示笼统的请求失败。
|
|
20
|
+
- 统一 Codex managed hook 的包根目录与安装器解析,升级监视器暂存运行文件时同步携带 `bin` 目录,提升升级和重启过程的一致性。
|
|
21
|
+
- 补充生命周期启动失败时的 stderr 诊断输出,便于定位启动异常。
|
|
22
|
+
- 修复 `agent.md` 显示名解析,改用 YAML 结构化解析以正确处理引号和特殊字符。
|
|
23
|
+
- 修复话题摘要写入时的游标竞态,确保 `coveredThroughTs` 在追加摘要前读取。
|
|
24
|
+
|
|
25
|
+
### 消息与依赖兼容
|
|
26
|
+
|
|
27
|
+
- 规范 AUN 原始错误展示,截断错误响应中附带的 HTML 页面,保留有用的错误信息并减少无关输出。
|
|
28
|
+
- 将 AUN SDK 最低运行版本提升至 `0.5.20`,使用经过当前版本验证的 SDK 依赖。
|
|
29
|
+
|
|
8
30
|
## 0.0.34 (2026-10-18)
|
|
9
31
|
|
|
10
32
|
### 模型与上下文
|
|
@@ -22,7 +22,25 @@ function hasArgument(name) {
|
|
|
22
22
|
return process.argv.includes(name);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/** Path where the elevated installer writes a structured failure record for the non-elevated parent. */
|
|
26
|
+
function installerFailurePath(requirements) {
|
|
27
|
+
const configDir = requirements
|
|
28
|
+
? path.dirname(path.resolve(requirements))
|
|
29
|
+
: path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex');
|
|
30
|
+
return path.join(configDir, '.evolcore-install-failure.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeFailureRecord(requirements, message) {
|
|
34
|
+
try {
|
|
35
|
+
const record = { time: new Date().toISOString(), message };
|
|
36
|
+
fs.writeFileSync(installerFailurePath(requirements), JSON.stringify(record) + '\n', { mode: 0o644 });
|
|
37
|
+
} catch {
|
|
38
|
+
// Best-effort; parent falls back to "installer exited with code 1" if missing.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fail(message, requirements) {
|
|
43
|
+
writeFailureRecord(requirements, message);
|
|
26
44
|
process.stderr.write(`[evolcore] ${message}\n`);
|
|
27
45
|
process.exit(1);
|
|
28
46
|
}
|
|
@@ -653,7 +671,7 @@ function main() {
|
|
|
653
671
|
const hookPath = path.join(targetDirectory, 'pre-tool-use.mjs');
|
|
654
672
|
const sourceHook = path.join(packageRoot, 'bin', 'codex-managed-hook.mjs');
|
|
655
673
|
for (const file of [sourceHook, nodePath]) {
|
|
656
|
-
if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}
|
|
674
|
+
if (!fs.statSync(file).isFile()) fail(`Required Hook runtime file was not found: ${file}`, requirements);
|
|
657
675
|
}
|
|
658
676
|
if (hasArgument('--repair')) {
|
|
659
677
|
const result = repairManagedHookInstallation({
|
|
@@ -674,7 +692,12 @@ function main() {
|
|
|
674
692
|
}
|
|
675
693
|
|
|
676
694
|
if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) {
|
|
695
|
+
// Resolve requirements now so writeFailureRecord has a path even if main()
|
|
696
|
+
// throws before the local `requirements` variable is assigned.
|
|
697
|
+
const earlyRequirements = argument('--requirements')
|
|
698
|
+
? path.resolve(argument('--requirements'))
|
|
699
|
+
: path.join(process.env.ProgramData || path.join(os.homedir(), 'ProgramData'), 'OpenAI', 'Codex', 'requirements.toml');
|
|
677
700
|
try { main(); } catch (error) {
|
|
678
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
701
|
+
fail(error instanceof Error ? error.message : String(error), earlyRequirements);
|
|
679
702
|
}
|
|
680
703
|
}
|
|
@@ -15,7 +15,7 @@ import crypto from 'crypto';
|
|
|
15
15
|
import { logger } from '../utils/logger.js';
|
|
16
16
|
import { createClaudeStartupArgumentsTooLongError, isClaudeStartupArgumentsTooLong, createClaudeCodeStartupError, isClaudeCodeStartupProcessError, } from '../utils/error-utils.js';
|
|
17
17
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
18
|
-
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
18
|
+
import { checkBashDescription, checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
19
19
|
import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
|
|
20
20
|
import { summarizeToolInput, summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
21
21
|
import { encodePath } from '../utils/cross-platform.js';
|
|
@@ -2391,6 +2391,16 @@ export class AgentRunner {
|
|
|
2391
2391
|
delete toolInput.pages;
|
|
2392
2392
|
}
|
|
2393
2393
|
let updatedInput = toolInput === originalInput ? undefined : toolInput;
|
|
2394
|
+
// Bash description 格式约定先于一切放行分支(含系统会话与 fullaccess)。
|
|
2395
|
+
// TODO(bash-description): 该约束缺少验证——实测模型被拒后不会补 description 重试,
|
|
2396
|
+
// 话题链等链路会被整条堵死。在补齐测试与重试引导前先短路放行,恢复时删除本开关。
|
|
2397
|
+
if (process.env.EVOLCORE_BASH_DESCRIPTION_ENFORCE === '1') {
|
|
2398
|
+
const descriptionCheck = checkBashDescription(toolName, toolInput);
|
|
2399
|
+
if (descriptionCheck) {
|
|
2400
|
+
await recordPreflightDenial(toolName, toolInput, descriptionCheck, input.tool_use_id ?? input.toolUseID);
|
|
2401
|
+
return hookDecision('deny', descriptionCheck.message, updatedInput);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2394
2404
|
// 系统会话:模块判定即最终结论,不进入下面任何按角色 / permissionMode 的分支
|
|
2395
2405
|
const systemSessionCheck = this.permissionContexts.get(sessionId)?.systemSessionCheck;
|
|
2396
2406
|
if (systemSessionCheck) {
|
|
@@ -3,8 +3,10 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
6
8
|
import { parse as parseToml } from 'smol-toml';
|
|
7
|
-
|
|
9
|
+
import { TARGET_PACKAGE_ROOT_ENV } from '../../product.js';
|
|
8
10
|
export function checkCodexProjectPath(value, options = {}) {
|
|
9
11
|
const configured = typeof value === 'string' ? value.trim() : '';
|
|
10
12
|
if (!configured) {
|
|
@@ -239,9 +241,6 @@ function runtimeFileMayExist(file) {
|
|
|
239
241
|
return !['ENOENT', 'ENOTDIR'].includes(error?.code || '');
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
|
-
function managedBinding(source, launcher, hookPath) {
|
|
243
|
-
return managedHookInstaller.managedBinding(source, launcher, hookPath);
|
|
244
|
-
}
|
|
245
244
|
/** Read-only inspection of the machine-wide Windows Codex managed hook. */
|
|
246
245
|
export function inspectCodexManagedHook(options = {}) {
|
|
247
246
|
const paths = codexManagedHookPaths(options);
|
|
@@ -250,6 +249,21 @@ export function inspectCodexManagedHook(options = {}) {
|
|
|
250
249
|
const requirements = path.resolve(options.requirements || paths.requirements);
|
|
251
250
|
const managedDir = path.resolve(options.managedDir || paths.managedDir);
|
|
252
251
|
const hookPath = path.join(managedDir, 'pre-tool-use.mjs');
|
|
252
|
+
let managedHookInstaller;
|
|
253
|
+
try {
|
|
254
|
+
const installer = fileURLToPath(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url));
|
|
255
|
+
managedHookInstaller = createRequire(import.meta.url)(installer);
|
|
256
|
+
if (typeof managedHookInstaller.managedBinding !== 'function') {
|
|
257
|
+
throw new Error('managedBinding export is unavailable');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
262
|
+
return {
|
|
263
|
+
status: 'unsafe', requirements, managedDir, hookPath,
|
|
264
|
+
reason: `managed-hook installer could not be loaded: ${detail}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
253
267
|
const configDirState = realDirectoryState(path.dirname(requirements));
|
|
254
268
|
const managedDirState = realDirectoryState(managedDir);
|
|
255
269
|
if ([configDirState, managedDirState].includes('unsafe')) {
|
|
@@ -312,9 +326,9 @@ export function inspectCodexManagedHook(options = {}) {
|
|
|
312
326
|
catch (error) {
|
|
313
327
|
return { status: 'unavailable', requirements, managedDir, hookPath, reason: error instanceof Error ? error.message : String(error) };
|
|
314
328
|
}
|
|
315
|
-
const expectedPackageRoot = path.resolve(options.packageRoot || process.cwd());
|
|
329
|
+
const expectedPackageRoot = path.resolve(process.env[TARGET_PACKAGE_ROOT_ENV] || options.packageRoot || process.cwd());
|
|
316
330
|
try {
|
|
317
|
-
const binding = managedBinding(source, launcher, hookPath);
|
|
331
|
+
const binding = managedHookInstaller.managedBinding(source, launcher, hookPath);
|
|
318
332
|
if (!binding || config.allow_managed_hooks_only !== true || config.features?.hooks !== true
|
|
319
333
|
|| normalizedPath(config.hooks?.windows_managed_dir) !== normalizedPath(managedDir)
|
|
320
334
|
|| (config.hooks?.managed_dir !== undefined
|
|
@@ -382,9 +396,10 @@ export async function ensureCodexManagedHook(options) {
|
|
|
382
396
|
});
|
|
383
397
|
if (!paths)
|
|
384
398
|
return { status: 'skipped', reason: 'Windows managed hooks are not applicable on this platform' };
|
|
399
|
+
const packageRoot = process.env[TARGET_PACKAGE_ROOT_ENV] || options.packageRoot;
|
|
385
400
|
const inspectOptions = {
|
|
386
401
|
platform: platformName,
|
|
387
|
-
packageRoot
|
|
402
|
+
packageRoot,
|
|
388
403
|
requirements: options.requirements || paths.requirements,
|
|
389
404
|
managedDir: options.managedDir || paths.managedDir,
|
|
390
405
|
programData: options.programData,
|
|
@@ -414,7 +429,9 @@ export async function ensureCodexManagedHook(options) {
|
|
|
414
429
|
|| rechecked.repairAmbiguous !== state.repairAmbiguous) {
|
|
415
430
|
return { ...rechecked, repaired: false, error: 'managed-hook state changed during the startup check; retry to confirm the current binding' };
|
|
416
431
|
}
|
|
417
|
-
const installer =
|
|
432
|
+
const installer = process.env[TARGET_PACKAGE_ROOT_ENV]
|
|
433
|
+
? fileURLToPath(new URL('../../../bin/install-codex-managed-hooks.mjs', import.meta.url))
|
|
434
|
+
: path.join(path.resolve(packageRoot), 'bin', 'install-codex-managed-hooks.mjs');
|
|
418
435
|
if (!fs.existsSync(installer)) {
|
|
419
436
|
return { ...state, repaired: false, error: `managed-hook installer is missing: ${installer}` };
|
|
420
437
|
}
|
|
@@ -438,15 +455,29 @@ export async function ensureCodexManagedHook(options) {
|
|
|
438
455
|
if (result.error)
|
|
439
456
|
return { ok: false, detail: result.error.message };
|
|
440
457
|
if (result.status !== 0) {
|
|
441
|
-
const
|
|
442
|
-
|
|
458
|
+
const stderrDetail = (result.stderr || '').replace(/#<\s*CLIXML[\s\S]*?<\/Objs>\s*/gi, '').trim();
|
|
459
|
+
// The elevated child process runs in a separate window and its stderr is
|
|
460
|
+
// not captured through the Start-Process pipe. Read the structured failure
|
|
461
|
+
// record the installer writes to ProgramData before exiting.
|
|
462
|
+
let fileDetail;
|
|
463
|
+
try {
|
|
464
|
+
const failurePath = path.join(path.dirname(path.resolve(inspectOptions.requirements || paths.requirements)), '.evolcore-install-failure.json');
|
|
465
|
+
const raw = fs.readFileSync(failurePath, 'utf8');
|
|
466
|
+
const parsed = JSON.parse(raw);
|
|
467
|
+
if (typeof parsed.message === 'string' && parsed.message)
|
|
468
|
+
fileDetail = parsed.message;
|
|
469
|
+
fs.rmSync(failurePath, { force: true });
|
|
470
|
+
}
|
|
471
|
+
catch { /* best-effort */ }
|
|
472
|
+
const detail = fileDetail || stderrDetail || `installer exited with code ${result.status}`;
|
|
473
|
+
return { ok: false, detail };
|
|
443
474
|
}
|
|
444
475
|
return { ok: true };
|
|
445
476
|
});
|
|
446
477
|
options.onInstallRequired?.(state);
|
|
447
478
|
let elevated;
|
|
448
479
|
try {
|
|
449
|
-
elevated = runElevated(installer, path.resolve(
|
|
480
|
+
elevated = runElevated(installer, path.resolve(packageRoot));
|
|
450
481
|
}
|
|
451
482
|
catch (error) {
|
|
452
483
|
elevated = { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
@@ -88,6 +88,24 @@ class AsyncEventQueue {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
function parseCodexRetryProgress(message) {
|
|
92
|
+
const match = message.match(/\bReconnecting\.\.\.\s*(\d+)\s*\/\s*(\d+)\b/i);
|
|
93
|
+
if (!match)
|
|
94
|
+
return undefined;
|
|
95
|
+
const count = Number(match[1]);
|
|
96
|
+
const limit = Number(match[2]);
|
|
97
|
+
if (!Number.isSafeInteger(count) || !Number.isSafeInteger(limit) || count < 1 || limit < count)
|
|
98
|
+
return undefined;
|
|
99
|
+
return { count, limit };
|
|
100
|
+
}
|
|
101
|
+
function createCodexRetryMetadataError(message, state) {
|
|
102
|
+
const error = new Error(message);
|
|
103
|
+
if (state.modelRetryCount !== undefined)
|
|
104
|
+
error.codexRetryCount = state.modelRetryCount;
|
|
105
|
+
if (state.modelRetryLimit !== undefined)
|
|
106
|
+
error.codexRetryLimit = state.modelRetryLimit;
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
91
109
|
function fallbackErrorCodeForToolDenial(decisionSource, policyCode) {
|
|
92
110
|
if (decisionSource === 'policy' && policyCode === 'apply_patch_invalid_argument')
|
|
93
111
|
return 'INVALID_ARGUMENT';
|
|
@@ -4931,7 +4949,7 @@ export class CodexRunner {
|
|
|
4931
4949
|
lastReportedUsage: state.tokenUsage?.last,
|
|
4932
4950
|
})}`);
|
|
4933
4951
|
if (isRetryableError(new Error(turn.error.message))) {
|
|
4934
|
-
throw
|
|
4952
|
+
throw createCodexRetryMetadataError(turn.error.message, state);
|
|
4935
4953
|
}
|
|
4936
4954
|
yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
|
|
4937
4955
|
}
|
|
@@ -4953,6 +4971,11 @@ export class CodexRunner {
|
|
|
4953
4971
|
// Codex owns retries within this turn. Throwing here closes our subscription
|
|
4954
4972
|
// before its recovery can finish; runQuery must not submit the input again.
|
|
4955
4973
|
if (params.willRetry === true) {
|
|
4974
|
+
const retryProgress = parseCodexRetryProgress(message);
|
|
4975
|
+
if (retryProgress) {
|
|
4976
|
+
state.modelRetryCount = Math.max(state.modelRetryCount ?? 0, retryProgress.count);
|
|
4977
|
+
state.modelRetryLimit = retryProgress.limit;
|
|
4978
|
+
}
|
|
4956
4979
|
yield {
|
|
4957
4980
|
type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
|
|
4958
4981
|
message: isOutputTokenLimitError(new Error(message))
|
|
@@ -4961,8 +4984,9 @@ export class CodexRunner {
|
|
|
4961
4984
|
};
|
|
4962
4985
|
break;
|
|
4963
4986
|
}
|
|
4964
|
-
if (isRetryableError(new Error(message)))
|
|
4965
|
-
throw
|
|
4987
|
+
if (isRetryableError(new Error(message))) {
|
|
4988
|
+
throw createCodexRetryMetadataError(message, state);
|
|
4989
|
+
}
|
|
4966
4990
|
yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
|
|
4967
4991
|
break;
|
|
4968
4992
|
}
|
|
@@ -10,7 +10,7 @@ import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
|
10
10
|
import { agentSessionsDir, resolveRoot } from '../paths.js';
|
|
11
11
|
import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
|
|
12
12
|
import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
|
|
13
|
-
import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
13
|
+
import { checkBashDescription, checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
|
|
14
14
|
import { logger } from '../utils/logger.js';
|
|
15
15
|
import { ErrorType } from '../utils/error-utils.js';
|
|
16
16
|
import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
|
|
@@ -700,7 +700,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
|
|
|
700
700
|
name: 'Bash', label: 'Bash', description: 'Run a shell command in the project.',
|
|
701
701
|
parameters: Type.Object({
|
|
702
702
|
command: Type.String(), timeout: Type.Optional(Type.Number({ minimum: 1, maximum: 600_000 })),
|
|
703
|
-
description: Type.Optional(Type.String()),
|
|
703
|
+
description: Type.Optional(Type.String({ description: '<命令目标描述> [<风险等级>],风险等级为 低/中/高 之一,例如 "查看工作区未提交的改动 [低]"。缺失或格式不符时调用被拒。' })),
|
|
704
704
|
}), executionMode: 'sequential',
|
|
705
705
|
async execute(_id, params, signal) {
|
|
706
706
|
const result = await env.exec(params.command, {
|
|
@@ -1214,11 +1214,6 @@ export class EcagentRunner {
|
|
|
1214
1214
|
}
|
|
1215
1215
|
async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
|
|
1216
1216
|
const permissionContext = this.permissionContexts.get(sessionId);
|
|
1217
|
-
if (mode === 'fullaccess') {
|
|
1218
|
-
return hasTrustedFullAccessContext(permissionContext)
|
|
1219
|
-
? {}
|
|
1220
|
-
: { block: true, reason: 'Untrusted fullaccess execution context' };
|
|
1221
|
-
}
|
|
1222
1217
|
const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
|
|
1223
1218
|
const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
|
|
1224
1219
|
auditToolPreflightDenial({
|
|
@@ -1257,6 +1252,21 @@ export class EcagentRunner {
|
|
|
1257
1252
|
logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
|
|
1258
1253
|
}
|
|
1259
1254
|
};
|
|
1255
|
+
// Bash description 格式约定先于一切放行分支(含 fullaccess 与系统会话)。
|
|
1256
|
+
// TODO(bash-description): 与 claude-runner 同因——该约束缺少验证,先短路放行;
|
|
1257
|
+
// 恢复强制时把两个 runner 的开关一起打开。
|
|
1258
|
+
if (process.env.EVOLCORE_BASH_DESCRIPTION_ENFORCE === '1') {
|
|
1259
|
+
const descriptionCheck = checkBashDescription(toolName, input);
|
|
1260
|
+
if (descriptionCheck) {
|
|
1261
|
+
await recordBlockedOperation(descriptionCheck.policyCode);
|
|
1262
|
+
return { block: true, reason: descriptionCheck.message };
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (mode === 'fullaccess') {
|
|
1266
|
+
return hasTrustedFullAccessContext(permissionContext)
|
|
1267
|
+
? {}
|
|
1268
|
+
: { block: true, reason: 'Untrusted fullaccess execution context' };
|
|
1269
|
+
}
|
|
1260
1270
|
// 系统会话:模块判定即最终结论,不进入 preflight / permissionMode 分支
|
|
1261
1271
|
if (permissionContext?.systemSessionCheck) {
|
|
1262
1272
|
const decision = permissionContext.systemSessionCheck(toolName, input);
|
package/dist/aun/aid/agentmd.js
CHANGED
|
@@ -179,8 +179,19 @@ export function parseAgentDisplayName(content) {
|
|
|
179
179
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
180
180
|
if (!frontmatter)
|
|
181
181
|
return undefined;
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
try {
|
|
183
|
+
const document = parseDocument(frontmatter);
|
|
184
|
+
if (document.errors.length > 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
const fields = document.toJS();
|
|
187
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields))
|
|
188
|
+
return undefined;
|
|
189
|
+
const name = fields.name;
|
|
190
|
+
return typeof name === 'string' ? name.trim() || undefined : undefined;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
184
195
|
}
|
|
185
196
|
function displayNameKey(aid, aunPath) {
|
|
186
197
|
const root = path.resolve(aunPath ?? resolveRoot());
|
package/dist/aun/aid/client.js
CHANGED
|
@@ -39,7 +39,7 @@ export function suppressSdkLogs() {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
// ==================== SDK package ====================
|
|
42
|
-
export const MIN_AUN_CORE_SDK = [0, 5,
|
|
42
|
+
export const MIN_AUN_CORE_SDK = [0, 5, 20];
|
|
43
43
|
export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
|
|
44
44
|
function compareVersion(a, min) {
|
|
45
45
|
const parts = a.split('.').map(n => parseInt(n, 10));
|
|
@@ -597,6 +597,9 @@ export class ManagedMsgOperationError extends Error {
|
|
|
597
597
|
}
|
|
598
598
|
const HISTORY_VALUE_FLAGS = new Set([
|
|
599
599
|
'--session', '--limit', '--before', '--after', '--direction', '--format', '--channel-key', '--peer-id',
|
|
600
|
+
// 话题过滤是托管协作的常规用法(文档 `thread-chain.md` 明确写了
|
|
601
|
+
// `ec msg history --thread <id>`);只会收窄结果集,不扩大可读范围。
|
|
602
|
+
'--thread',
|
|
600
603
|
]);
|
|
601
604
|
function failMsg(code, reason) {
|
|
602
605
|
return { ok: false, code, reason };
|
|
@@ -720,6 +723,15 @@ export function resolveManagedMsgOperation(argv) {
|
|
|
720
723
|
return failMsg('INVALID_ARGUMENT', '--after must be earlier than --before');
|
|
721
724
|
}
|
|
722
725
|
const target = channelKey === undefined ? String(positionals[0] || '').replace(/^@/, '') : undefined;
|
|
726
|
+
const threadId = values.get('--thread');
|
|
727
|
+
if (threadId !== undefined) {
|
|
728
|
+
if (kind !== 'history')
|
|
729
|
+
return failMsg('INVALID_ARGUMENT', '--thread is only supported for history');
|
|
730
|
+
if (channelKey !== undefined)
|
|
731
|
+
return failMsg('INVALID_ARGUMENT', '--thread cannot be combined with --channel-key/--peer-id');
|
|
732
|
+
if (!/^thread-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(threadId))
|
|
733
|
+
return failMsg('INVALID_ARGUMENT', 'invalid --thread id');
|
|
734
|
+
}
|
|
723
735
|
const canonicalArgv = ['msg', kind, from];
|
|
724
736
|
if (target !== undefined)
|
|
725
737
|
canonicalArgv.push(target);
|
|
@@ -735,6 +747,8 @@ export function resolveManagedMsgOperation(argv) {
|
|
|
735
747
|
canonicalArgv.push('--after', values.get('--after'));
|
|
736
748
|
if (values.has('--direction'))
|
|
737
749
|
canonicalArgv.push('--direction', values.get('--direction'));
|
|
750
|
+
if (threadId !== undefined)
|
|
751
|
+
canonicalArgv.push('--thread', threadId);
|
|
738
752
|
return {
|
|
739
753
|
ok: true,
|
|
740
754
|
command: {
|
|
@@ -749,6 +763,7 @@ export function resolveManagedMsgOperation(argv) {
|
|
|
749
763
|
before,
|
|
750
764
|
after,
|
|
751
765
|
direction,
|
|
766
|
+
...(threadId !== undefined ? { threadId } : {}),
|
|
752
767
|
},
|
|
753
768
|
};
|
|
754
769
|
}
|
|
@@ -766,6 +781,7 @@ export async function executeManagedMsgOperation(command) {
|
|
|
766
781
|
before: command.before,
|
|
767
782
|
after: command.after,
|
|
768
783
|
direction: command.direction,
|
|
784
|
+
threadId: command.threadId,
|
|
769
785
|
})
|
|
770
786
|
: await msgOnline({
|
|
771
787
|
from: command.from,
|
package/dist/aun/msg/p2p.js
CHANGED
|
@@ -279,8 +279,24 @@ export async function msgSend(args) {
|
|
|
279
279
|
catch { /* ignore */ }
|
|
280
280
|
}
|
|
281
281
|
// 2. 决定 chatmode(遵循来源1-3)
|
|
282
|
-
// 私聊:非 human 对端 → proactive
|
|
283
|
-
|
|
282
|
+
// 私聊:非 human 对端 → proactive;人类对端只有 owner/admin 才 interactive,
|
|
283
|
+
// 其余(含角色未知)按 proactive 保守处理。判定逻辑集中在 peer-mode。
|
|
284
|
+
const { chatmodeForPeerIdentity } = await import('../../core/message/peer-mode.js');
|
|
285
|
+
const { resolvePeerRoleDetail } = await import('../../config/peer-role-resolver.js');
|
|
286
|
+
let peerRole = null;
|
|
287
|
+
try {
|
|
288
|
+
peerRole = resolvePeerRoleDetail({
|
|
289
|
+
selfAid: args.from,
|
|
290
|
+
channelKey: `aun#${args.from}#main`,
|
|
291
|
+
channelType: 'aun',
|
|
292
|
+
chatType: 'private',
|
|
293
|
+
actorId: args.to,
|
|
294
|
+
conversationId: args.to,
|
|
295
|
+
peerType: peerIdentity.type,
|
|
296
|
+
}).effectiveRole;
|
|
297
|
+
}
|
|
298
|
+
catch { /* 角色解析失败时按未知处理(proactive) */ }
|
|
299
|
+
const chatmode = chatmodeForPeerIdentity(peerIdentity, peerRole);
|
|
284
300
|
// 3. 构建 payload
|
|
285
301
|
if (!payload) {
|
|
286
302
|
payload = buildSimplePayload(args.body);
|
package/dist/channels/aun.js
CHANGED
|
@@ -557,10 +557,23 @@ function setIfDefined(target, key, value) {
|
|
|
557
557
|
if (value !== undefined)
|
|
558
558
|
target[key] = value;
|
|
559
559
|
}
|
|
560
|
+
function truncateRawErrorForDisplay(raw) {
|
|
561
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
562
|
+
return undefined;
|
|
563
|
+
const text = raw.trim();
|
|
564
|
+
const htmlStart = text.toLowerCase().indexOf('<html');
|
|
565
|
+
return htmlStart >= 0 ? text.slice(0, htmlStart).trimEnd() : text;
|
|
566
|
+
}
|
|
567
|
+
function projectedRawError(raw, displayRaw) {
|
|
568
|
+
if (typeof raw !== 'string')
|
|
569
|
+
return raw;
|
|
570
|
+
return raw.trim() ? displayRaw : raw;
|
|
571
|
+
}
|
|
560
572
|
function appendRawErrorText(text, raw) {
|
|
561
|
-
|
|
573
|
+
const displayRaw = truncateRawErrorForDisplay(raw);
|
|
574
|
+
if (!displayRaw || text.includes(displayRaw))
|
|
562
575
|
return text;
|
|
563
|
-
return `${text}\n原因:${
|
|
576
|
+
return `${text}\n原因:${displayRaw}`;
|
|
564
577
|
}
|
|
565
578
|
/**
|
|
566
579
|
* 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
|
|
@@ -667,6 +680,8 @@ export class AUNChannel {
|
|
|
667
680
|
traceWriter = null;
|
|
668
681
|
eventBus = null;
|
|
669
682
|
queuedHandler = null;
|
|
683
|
+
/** `topic-renamed` 消费器,由 runtime 注入(见 setThreadRenameHandler)。 */
|
|
684
|
+
threadRenameHandler = null;
|
|
670
685
|
groupInviteHandler;
|
|
671
686
|
groupInviteConnectedHandler;
|
|
672
687
|
groupInviteDeliveryGate;
|
|
@@ -1396,6 +1411,44 @@ export class AUNChannel {
|
|
|
1396
1411
|
return sanitized;
|
|
1397
1412
|
return undefined;
|
|
1398
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* 处理对端 `topic-renamed` 通知:校验字段后交给注入的改名消费器落索引,
|
|
1416
|
+
* 成功后发 `thread:renamed` 让前端刷新。全程不进入 Agent 消息队列。
|
|
1417
|
+
* 通知是尽力而为的,任何失败只记日志,不影响后续消息处理。
|
|
1418
|
+
*/
|
|
1419
|
+
applyInboundTopicRename(params) {
|
|
1420
|
+
const payload = params.payload && typeof params.payload === 'object' ? params.payload : null;
|
|
1421
|
+
const threadId = typeof payload?.threadId === 'string' ? payload.threadId : undefined;
|
|
1422
|
+
const name = this.sanitizeTopicName(payload?.topicName);
|
|
1423
|
+
const nameUpdatedAt = typeof payload?.nameUpdatedAt === 'number' && Number.isFinite(payload.nameUpdatedAt)
|
|
1424
|
+
? payload.nameUpdatedAt : undefined;
|
|
1425
|
+
const applier = this.threadRenameHandler;
|
|
1426
|
+
if (!applier || !threadId || !name || nameUpdatedAt === undefined || !params.peer) {
|
|
1427
|
+
logger.debug(`${this.logPrefix()} topic-renamed ignored: peer=${params.peer || '<none>'} thread=${threadId || '<none>'} hasName=${!!name}`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
void applier({
|
|
1431
|
+
selfAid: params.selfAid, peer: params.peer, peerKind: params.peerKind,
|
|
1432
|
+
threadId, name, nameUpdatedAt,
|
|
1433
|
+
}).then(result => {
|
|
1434
|
+
if (!result.applied) {
|
|
1435
|
+
logger.debug(`${this.logPrefix()} topic-renamed not applied (${result.reason ?? 'unknown'}): thread=${threadId}`);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
this.eventBus?.publish?.({
|
|
1439
|
+
type: 'thread:renamed',
|
|
1440
|
+
sessionId: result.sessionId,
|
|
1441
|
+
peer: params.peer,
|
|
1442
|
+
threadId,
|
|
1443
|
+
name,
|
|
1444
|
+
nameUpdatedAt,
|
|
1445
|
+
by: params.senderAid ?? '',
|
|
1446
|
+
});
|
|
1447
|
+
logger.info(`${this.logPrefix()} topic-renamed applied: thread=${threadId} name=${JSON.stringify(name)}`);
|
|
1448
|
+
}).catch(error => {
|
|
1449
|
+
logger.warn(`${this.logPrefix()} topic-renamed failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1399
1452
|
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
|
|
1400
1453
|
if (!groupId)
|
|
1401
1454
|
throw new Error('buildGroupReplyContext requires groupId');
|
|
@@ -2524,6 +2577,14 @@ export class AUNChannel {
|
|
|
2524
2577
|
if (p2pPayloadType === 'text') {
|
|
2525
2578
|
import('../utils/msg-diag.js').then(m => m.diagLog('aun_in', { aid: this.config.aid, from: fromAid, to: this.config.aid, msgId: messageId, text })).catch(() => { });
|
|
2526
2579
|
}
|
|
2580
|
+
// topic-renamed:对端话题改名通知。只更新本端话题索引,不进 Agent 队列。
|
|
2581
|
+
if (p2pPayloadType === 'topic-renamed') {
|
|
2582
|
+
this.applyInboundTopicRename({
|
|
2583
|
+
payload, selfAid: this.config.aid, peer: fromAid, peerKind: 'private',
|
|
2584
|
+
senderAid: fromAid,
|
|
2585
|
+
});
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2527
2588
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
2528
2589
|
if (p2pPayloadType === 'action_card_reply') {
|
|
2529
2590
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
@@ -2655,6 +2716,14 @@ export class AUNChannel {
|
|
|
2655
2716
|
logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
|
|
2656
2717
|
return;
|
|
2657
2718
|
}
|
|
2719
|
+
// topic-renamed:群内话题改名通知。只更新本端索引,不进 Agent 队列。
|
|
2720
|
+
if (inboundType === 'topic-renamed') {
|
|
2721
|
+
this.applyInboundTopicRename({
|
|
2722
|
+
payload, selfAid: this.config.aid, peer: groupId || senderAid, peerKind: 'group',
|
|
2723
|
+
senderAid,
|
|
2724
|
+
});
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2658
2727
|
const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId);
|
|
2659
2728
|
if (!payloadMentionEntries)
|
|
2660
2729
|
return;
|
|
@@ -3765,6 +3834,13 @@ export class AUNChannel {
|
|
|
3765
3834
|
}, delayMs);
|
|
3766
3835
|
}
|
|
3767
3836
|
// ── Public API (same interface as before) ───────────────────
|
|
3837
|
+
/**
|
|
3838
|
+
* 对端发来的话题改名通知(`topic-renamed`)。由 runtime 注入,交给
|
|
3839
|
+
* ThreadService.applyRemoteRename 落本地索引;不进入 Agent 消息队列。
|
|
3840
|
+
*/
|
|
3841
|
+
setThreadRenameHandler(handler) {
|
|
3842
|
+
this.threadRenameHandler = handler;
|
|
3843
|
+
}
|
|
3768
3844
|
setEventBus(bus) {
|
|
3769
3845
|
// 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
|
|
3770
3846
|
if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
@@ -4611,7 +4687,23 @@ export class AUNChannel {
|
|
|
4611
4687
|
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
4612
4688
|
}
|
|
4613
4689
|
const info = await this.fetchPeerInfo(args.to);
|
|
4614
|
-
|
|
4690
|
+
// 与 p2p 发送路径共用同一判定:非人类 → proactive;人类需 owner/admin 才 interactive。
|
|
4691
|
+
const { chatmodeForPeerIdentity } = await import('../core/message/peer-mode.js');
|
|
4692
|
+
let peerRole = null;
|
|
4693
|
+
try {
|
|
4694
|
+
const { resolvePeerRoleDetail } = await import('../config/peer-role-resolver.js');
|
|
4695
|
+
peerRole = resolvePeerRoleDetail({
|
|
4696
|
+
selfAid: this.config.aid,
|
|
4697
|
+
channelKey: `aun#${this.config.aid}#main`,
|
|
4698
|
+
channelType: 'aun',
|
|
4699
|
+
chatType: 'private',
|
|
4700
|
+
actorId: args.to,
|
|
4701
|
+
conversationId: args.to,
|
|
4702
|
+
peerType: info.type ?? undefined,
|
|
4703
|
+
}).effectiveRole;
|
|
4704
|
+
}
|
|
4705
|
+
catch { /* 角色解析失败按未知处理 */ }
|
|
4706
|
+
const chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
|
|
4615
4707
|
if (!finalPayload.chatmode)
|
|
4616
4708
|
finalPayload.chatmode = chatmode;
|
|
4617
4709
|
const parentCausation = normalizeCausation(args.causation);
|
|
@@ -6426,14 +6518,16 @@ export class AUNChannelPlugin {
|
|
|
6426
6518
|
return;
|
|
6427
6519
|
}
|
|
6428
6520
|
case 'system.error': {
|
|
6521
|
+
const raw = truncateRawErrorForDisplay(payload.raw);
|
|
6522
|
+
const projectedRaw = projectedRawError(payload.raw, raw);
|
|
6429
6523
|
const errorPayload = {
|
|
6430
6524
|
type: payload.recoverable === true ? 'notice' : 'error',
|
|
6431
6525
|
...taskBase(),
|
|
6432
6526
|
subtype: payload.subtype,
|
|
6433
6527
|
text: payload.recoverable === true
|
|
6434
6528
|
? payload.text
|
|
6435
|
-
: appendRawErrorText(payload.text,
|
|
6436
|
-
...(
|
|
6529
|
+
: appendRawErrorText(payload.text, raw),
|
|
6530
|
+
...(projectedRaw !== undefined ? { raw: projectedRaw } : {}),
|
|
6437
6531
|
...(payload.reason !== undefined ? { reason: payload.reason } : {}),
|
|
6438
6532
|
...(payload.recoverable === true
|
|
6439
6533
|
? { severity: 'warn' }
|
|
@@ -6443,14 +6537,16 @@ export class AUNChannelPlugin {
|
|
|
6443
6537
|
return;
|
|
6444
6538
|
}
|
|
6445
6539
|
case 'result.error': {
|
|
6540
|
+
const raw = truncateRawErrorForDisplay(payload.raw);
|
|
6541
|
+
const projectedRaw = projectedRawError(payload.raw, raw);
|
|
6446
6542
|
const errorPayload = {
|
|
6447
6543
|
type: 'error',
|
|
6448
6544
|
...taskBase(),
|
|
6449
|
-
text: appendRawErrorText(payload.text,
|
|
6545
|
+
text: appendRawErrorText(payload.text, raw),
|
|
6450
6546
|
};
|
|
6451
6547
|
setIfDefined(errorPayload, 'subtype', payload.subtype);
|
|
6452
6548
|
setIfDefined(errorPayload, 'reason', payload.reason);
|
|
6453
|
-
setIfDefined(errorPayload, 'raw',
|
|
6549
|
+
setIfDefined(errorPayload, 'raw', projectedRaw);
|
|
6454
6550
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6455
6551
|
return;
|
|
6456
6552
|
}
|
|
@@ -6776,6 +6872,16 @@ export class AUNChannelPlugin {
|
|
|
6776
6872
|
},
|
|
6777
6873
|
registerHooks(hookCtx) {
|
|
6778
6874
|
channel.setEventBus(hookCtx.eventBus);
|
|
6875
|
+
// 话题改名通知消费器:直接改本端索引,不经过 Agent 队列。
|
|
6876
|
+
channel.setThreadRenameHandler(async (input) => {
|
|
6877
|
+
const { ThreadService } = await import('../core/thread/thread-service.js');
|
|
6878
|
+
const service = new ThreadService(hookCtx.sessionManager);
|
|
6879
|
+
const result = await service.applyRemoteRename(input);
|
|
6880
|
+
const session = result.applied
|
|
6881
|
+
? service.findInIndex(input.selfAid, input.peer, input.threadId)?.sessionId
|
|
6882
|
+
: undefined;
|
|
6883
|
+
return { applied: result.applied, reason: result.reason, sessionId: session };
|
|
6884
|
+
});
|
|
6779
6885
|
if (channel.setOnChannelDown) {
|
|
6780
6886
|
channel.setOnChannelDown(() => {
|
|
6781
6887
|
hookCtx.eventBus.publish({
|