evolcore 0.0.32 → 0.0.34
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 +61 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +135 -37
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +111 -15
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +112 -54
- package/dist/agents/codex-runner.js +241 -63
- package/dist/agents/ecagent-runner.js +78 -33
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/channels/aun.js +44 -8
- package/dist/channels/daemon.js +261 -3
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +368 -31
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +197 -27
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +98 -67
- package/dist/cli/init.js +2 -2
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +166 -146
- package/dist/cli/queue-command.js +9 -4
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +78 -2
- package/dist/cli/task-context.js +1 -0
- package/dist/cli/thread-command.js +622 -0
- package/dist/cli/trigger-command.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +6 -3
- package/dist/config/config-manager.js +26 -2
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +45 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +133 -36
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +25 -2
- package/dist/core/auth/operation-catalog.js +49 -0
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +197 -27
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +51 -1
- package/dist/core/command/menu-handler.js +114 -23
- package/dist/core/command/menu-protocol.js +25 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +195 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +175 -3
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +83 -29
- package/dist/core/memory/consolidation-ops-records.js +49 -16
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +264 -205
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +30 -15
- package/dist/core/message/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +169 -10
- package/dist/core/message/message-bridge.js +15 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +929 -110
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +217 -9
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +129 -14
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +21 -4
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/command-parser.js +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +770 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +19 -7
- package/dist/ipc.js +140 -50
- package/dist/paths.js +93 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +13 -2
- package/dist/trigger/parser.js +70 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +143 -1
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +40 -28
- package/kits/docs/evolcore/agent.md +21 -6
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +5 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +4 -6
- package/kits/docs/evolcore/group.md +11 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -15
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +8 -11
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -1
- package/kits/eck_manifest.json +55 -40
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +199 -0
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +7 -2
- package/kits/templates/memory-consolidation/generalize.md +9 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +8 -3
- package/kits/templates/memory-consolidation/promote.md +8 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +67 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +7 -6
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +6 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { integrityDbPath, integritySnapshotDir, resolveRoot } from '../../paths.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
// 项目是 ESM,裸 require 不存在;与 codex-session-file-adapter 同款懒加载
|
|
8
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
9
|
+
// ── SQLite 懒加载 ─────────────────────────────────────────────────────────────
|
|
10
|
+
let _db = null;
|
|
11
|
+
const CREATE_TABLE_SQL = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS file_integrity (
|
|
13
|
+
file_path TEXT PRIMARY KEY,
|
|
14
|
+
sha256 TEXT NOT NULL,
|
|
15
|
+
registered_at INTEGER NOT NULL,
|
|
16
|
+
last_verified_at INTEGER,
|
|
17
|
+
registrar_aid TEXT
|
|
18
|
+
)
|
|
19
|
+
`;
|
|
20
|
+
function getDb() {
|
|
21
|
+
if (_db)
|
|
22
|
+
return _db;
|
|
23
|
+
// node:sqlite (DatabaseSync) 在 Node 22.5+ 可用,懒加载兼容低版本
|
|
24
|
+
let DatabaseSync;
|
|
25
|
+
try {
|
|
26
|
+
({ DatabaseSync } = requireFromHere('node:sqlite'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error('[file-integrity] node:sqlite 不可用,需要 Node.js 22.5+');
|
|
30
|
+
}
|
|
31
|
+
fs.mkdirSync(path.dirname(integrityDbPath()), { recursive: true });
|
|
32
|
+
_db = new DatabaseSync(integrityDbPath());
|
|
33
|
+
// CLI 进程和 daemon 可能同时打开同一个库:先设等待,再切 WAL
|
|
34
|
+
_db.exec('PRAGMA busy_timeout = 5000');
|
|
35
|
+
_db.exec('PRAGMA journal_mode = WAL');
|
|
36
|
+
_db.exec(CREATE_TABLE_SQL);
|
|
37
|
+
return _db;
|
|
38
|
+
}
|
|
39
|
+
// ── 快照路径规范化 ────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* 台账主键:绝对路径 + 正斜杠;Windows 文件系统大小写不敏感,再统一转小写,
|
|
42
|
+
* 避免 `Evol` / `evol` 这类同一文件两种写法登记成两条。
|
|
43
|
+
*/
|
|
44
|
+
function recordKey(filePath) {
|
|
45
|
+
const normalized = path.resolve(filePath).replace(/\\/g, '/');
|
|
46
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
47
|
+
}
|
|
48
|
+
function snapshotPath(key) {
|
|
49
|
+
const rootKey = recordKey(resolveRoot());
|
|
50
|
+
if (key.startsWith(rootKey + '/')) {
|
|
51
|
+
return path.join(integritySnapshotDir(), key.slice(rootKey.length + 1));
|
|
52
|
+
}
|
|
53
|
+
// 路径在 home 外(如包内 kits/),用 key 的 sha256 命名,防路径穿越和盘符冒号
|
|
54
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
55
|
+
return path.join(integritySnapshotDir(), hash);
|
|
56
|
+
}
|
|
57
|
+
// ── SHA-256 计算 ──────────────────────────────────────────────────────────────
|
|
58
|
+
function computeSha256(bytes) {
|
|
59
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
/** 快照存在且内容与台账一致,才算可恢复 */
|
|
62
|
+
function snapshotIntact(key, expected) {
|
|
63
|
+
const snap = snapshotPath(key);
|
|
64
|
+
return fs.existsSync(snap) && computeSha256(fs.readFileSync(snap)) === expected;
|
|
65
|
+
}
|
|
66
|
+
// ── 原子写文件(tmp → rename) ────────────────────────────────────────────────
|
|
67
|
+
function atomicWrite(destPath, bytes) {
|
|
68
|
+
// 带 pid:恢复写盘不在事务里,daemon 与 CLI 同时恢复同一文件时不能共用临时名
|
|
69
|
+
const tmpPath = `${destPath}.integrity-tmp-${process.pid}`;
|
|
70
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
71
|
+
fs.writeFileSync(tmpPath, bytes);
|
|
72
|
+
fs.renameSync(tmpPath, destPath);
|
|
73
|
+
}
|
|
74
|
+
// ── 日志(复用 daemon logger,固定前缀便于过滤) ─────────────────────────────
|
|
75
|
+
function short(hash) {
|
|
76
|
+
return hash.slice(0, 12);
|
|
77
|
+
}
|
|
78
|
+
// ── 核心 API ──────────────────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* 登记文件到台账并备份快照。
|
|
81
|
+
* 在文件写入完成后由写入方调用,建立或更新可信基线。
|
|
82
|
+
*/
|
|
83
|
+
export function registerFile(filePath, aid) {
|
|
84
|
+
const key = recordKey(filePath);
|
|
85
|
+
const bytes = fs.readFileSync(filePath);
|
|
86
|
+
registerBytes(key, bytes, aid, fs.statSync(filePath).mtime);
|
|
87
|
+
logger.info(`[file-integrity] registered path=${key} sha256=${short(computeSha256(bytes))}${aid ? ` aid=${aid}` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* node:sqlite 没有 transaction() 包装,手写 BEGIN/COMMIT:
|
|
91
|
+
* 快照写失败则 ROLLBACK,台账不会指向一份不存在的快照。
|
|
92
|
+
*/
|
|
93
|
+
function registerBytes(key, bytes, aid, mtime) {
|
|
94
|
+
const sha256 = computeSha256(bytes);
|
|
95
|
+
const now = Math.floor(Date.now() / 1000);
|
|
96
|
+
const db = getDb();
|
|
97
|
+
db.exec('BEGIN');
|
|
98
|
+
try {
|
|
99
|
+
db.prepare(`INSERT INTO file_integrity (file_path, sha256, registered_at, registrar_aid)
|
|
100
|
+
VALUES (?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
102
|
+
sha256 = excluded.sha256,
|
|
103
|
+
registered_at = excluded.registered_at,
|
|
104
|
+
registrar_aid = excluded.registrar_aid`).run(key, sha256, now, aid ?? null);
|
|
105
|
+
const snap = snapshotPath(key);
|
|
106
|
+
atomicWrite(snap, bytes);
|
|
107
|
+
// 快照带上原文件 mtime,恢复时还原回去;记忆的排序和超期判断都读 mtime
|
|
108
|
+
if (mtime)
|
|
109
|
+
fs.utimesSync(snap, mtime, mtime);
|
|
110
|
+
db.exec('COMMIT');
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
try {
|
|
114
|
+
db.exec('ROLLBACK');
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 校验文件完整性。
|
|
122
|
+
*
|
|
123
|
+
* - 文件不存在 → missing
|
|
124
|
+
* - 台账无记录 → 用本次读到的字节自动登记,返回 verified
|
|
125
|
+
* - sha256 匹配 → verified,更新 last_verified_at
|
|
126
|
+
* - 不匹配且 restoreOnFail → 快照 sha256 与台账一致则原子恢复,返回 restored;否则 tampered
|
|
127
|
+
* - 不匹配且未要求恢复 → tampered
|
|
128
|
+
*
|
|
129
|
+
* 返回的 bytes 就是本次校验过的内容,调用方直接使用,不要再读一次文件(TOCTOU)。
|
|
130
|
+
*/
|
|
131
|
+
export function verifyFile(filePath, opts) {
|
|
132
|
+
const key = recordKey(filePath);
|
|
133
|
+
if (!fs.existsSync(filePath))
|
|
134
|
+
return { status: 'missing' };
|
|
135
|
+
const bytes = fs.readFileSync(filePath);
|
|
136
|
+
const actual = computeSha256(bytes);
|
|
137
|
+
const db = getDb();
|
|
138
|
+
const row = db.prepare('SELECT sha256 FROM file_integrity WHERE file_path = ?').get(key);
|
|
139
|
+
if (!row) {
|
|
140
|
+
// 用已读字节登记,避免二次读文件时内容已变
|
|
141
|
+
registerBytes(key, bytes, undefined, fs.statSync(filePath).mtime);
|
|
142
|
+
logger.info(`[file-integrity] auto-registered path=${key} sha256=${short(actual)}`);
|
|
143
|
+
return { status: 'verified', bytes };
|
|
144
|
+
}
|
|
145
|
+
if (actual === row.sha256) {
|
|
146
|
+
db.prepare('UPDATE file_integrity SET last_verified_at = ? WHERE file_path = ?')
|
|
147
|
+
.run(Math.floor(Date.now() / 1000), key);
|
|
148
|
+
return { status: 'verified', bytes };
|
|
149
|
+
}
|
|
150
|
+
const snap = snapshotPath(key);
|
|
151
|
+
const snapExists = fs.existsSync(snap);
|
|
152
|
+
if (opts?.restoreOnFail && snapExists) {
|
|
153
|
+
const snapBytes = fs.readFileSync(snap);
|
|
154
|
+
if (computeSha256(snapBytes) === row.sha256) {
|
|
155
|
+
atomicWrite(filePath, snapBytes);
|
|
156
|
+
// 还原成登记时的 mtime,不让恢复动作把文件"续期"成刚写入
|
|
157
|
+
try {
|
|
158
|
+
const { mtime } = fs.statSync(snap);
|
|
159
|
+
fs.utimesSync(filePath, mtime, mtime);
|
|
160
|
+
}
|
|
161
|
+
catch { /* best-effort */ }
|
|
162
|
+
logger.warn(`[file-integrity] restored path=${key} expected=${short(row.sha256)} actual=${short(actual)}`);
|
|
163
|
+
return { status: 'restored', bytes: snapBytes };
|
|
164
|
+
}
|
|
165
|
+
logger.error(`[file-integrity] snapshot-corrupted path=${key}`);
|
|
166
|
+
return { status: 'tampered', restorable: false, expected: row.sha256, actual };
|
|
167
|
+
}
|
|
168
|
+
const restorable = snapshotIntact(key, row.sha256);
|
|
169
|
+
logger.warn(`[file-integrity] tampered path=${key} expected=${short(row.sha256)} actual=${short(actual)} restorable=${restorable}`);
|
|
170
|
+
return { status: 'tampered', restorable, expected: row.sha256, actual };
|
|
171
|
+
}
|
|
172
|
+
/** 只读查询台账记录,不读文件,供诊断和 CLI 使用。 */
|
|
173
|
+
export function queryRecord(filePath) {
|
|
174
|
+
const row = getDb().prepare('SELECT file_path, sha256, registered_at, last_verified_at, registrar_aid FROM file_integrity WHERE file_path = ?').get(recordKey(filePath));
|
|
175
|
+
return row ?? null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 文件经正规渠道 rename 后调用:台账主键和快照一起搬到新路径,sha256 不变。
|
|
179
|
+
* 搬移前后的篡改都会在新路径上被校验出来。rename 后不要对新路径重新 registerFile,
|
|
180
|
+
* 那会把搬移前的篡改登记成新基线。
|
|
181
|
+
*/
|
|
182
|
+
export function moveRecord(fromPath, toPath) {
|
|
183
|
+
const fromKey = recordKey(fromPath);
|
|
184
|
+
const toKey = recordKey(toPath);
|
|
185
|
+
if (fromKey === toKey)
|
|
186
|
+
return;
|
|
187
|
+
const db = getDb();
|
|
188
|
+
const row = db.prepare('SELECT 1 FROM file_integrity WHERE file_path = ?').get(fromKey);
|
|
189
|
+
if (!row) {
|
|
190
|
+
// 源文件从未登记过,没有可搬的基线;新路径下次 verify 时自动登记
|
|
191
|
+
logger.warn(`[file-integrity] move-without-record from=${fromKey} to=${toKey}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const fromSnap = snapshotPath(fromKey);
|
|
195
|
+
const toSnap = snapshotPath(toKey);
|
|
196
|
+
db.exec('BEGIN');
|
|
197
|
+
try {
|
|
198
|
+
db.prepare('DELETE FROM file_integrity WHERE file_path = ?').run(toKey);
|
|
199
|
+
db.prepare('UPDATE file_integrity SET file_path = ? WHERE file_path = ?').run(toKey, fromKey);
|
|
200
|
+
if (fs.existsSync(fromSnap)) {
|
|
201
|
+
fs.mkdirSync(path.dirname(toSnap), { recursive: true });
|
|
202
|
+
fs.renameSync(fromSnap, toSnap);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
fs.rmSync(toSnap, { force: true });
|
|
206
|
+
}
|
|
207
|
+
db.exec('COMMIT');
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
try {
|
|
211
|
+
db.exec('ROLLBACK');
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
logger.info(`[file-integrity] moved from=${fromKey} to=${toKey}`);
|
|
217
|
+
}
|
|
218
|
+
/** 文件经正规渠道删除后调用:删掉台账记录和快照。 */
|
|
219
|
+
export function unregisterFile(filePath) {
|
|
220
|
+
const key = recordKey(filePath);
|
|
221
|
+
const result = getDb().prepare('DELETE FROM file_integrity WHERE file_path = ?').run(key);
|
|
222
|
+
fs.rmSync(snapshotPath(key), { force: true });
|
|
223
|
+
if (Number(result?.changes ?? 0) > 0)
|
|
224
|
+
logger.info(`[file-integrity] unregistered path=${key}`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 登记一段**尚未落盘**的内容。写盘之前调用。
|
|
228
|
+
*
|
|
229
|
+
* 为什么不在写完之后登记:写方和读方可能不在同一个进程(daemon 与 CLI)。
|
|
230
|
+
* 若先改盘再登记,中间那个窗口里另一个进程读到的就是"新内容 + 旧台账",
|
|
231
|
+
* 会被判成篡改并回滚掉刚写进去的合法内容。
|
|
232
|
+
*
|
|
233
|
+
* 配置类文件不靠 mtime 判断超期,所以这里用登记时刻作快照 mtime。
|
|
234
|
+
*/
|
|
235
|
+
export function registerContent(filePath, content, aid) {
|
|
236
|
+
const key = recordKey(filePath);
|
|
237
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
|
|
238
|
+
registerBytes(key, bytes, aid);
|
|
239
|
+
}
|
|
240
|
+
/** 只读检查,不改台账、不恢复文件,也不更新 last_verified_at。供诊断和 CLI 使用。 */
|
|
241
|
+
export function checkFile(filePath) {
|
|
242
|
+
const key = recordKey(filePath);
|
|
243
|
+
if (!fs.existsSync(filePath))
|
|
244
|
+
return { status: 'missing' };
|
|
245
|
+
const actual = computeSha256(fs.readFileSync(filePath));
|
|
246
|
+
const row = getDb().prepare('SELECT sha256 FROM file_integrity WHERE file_path = ?').get(key);
|
|
247
|
+
if (!row)
|
|
248
|
+
return { status: 'unregistered', actual };
|
|
249
|
+
if (row.sha256 === actual)
|
|
250
|
+
return { status: 'verified', expected: row.sha256, actual };
|
|
251
|
+
return { status: 'tampered', expected: row.sha256, actual, restorable: snapshotIntact(key, row.sha256) };
|
|
252
|
+
}
|
|
253
|
+
/** 仅供测试:关闭并重置 SQLite 连接缓存 */
|
|
254
|
+
export function _resetDb() {
|
|
255
|
+
if (_db) {
|
|
256
|
+
try {
|
|
257
|
+
_db.close();
|
|
258
|
+
}
|
|
259
|
+
catch { /* ignore */ }
|
|
260
|
+
_db = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -33,13 +33,6 @@ export class HandoffDeliveryFailedError extends Error {
|
|
|
33
33
|
this.name = 'HandoffDeliveryFailedError';
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
export class HandoffReturnPolicyRequiredError extends Error {
|
|
37
|
-
code = 'HANDOFF_RETURN_POLICY_REQUIRED';
|
|
38
|
-
constructor() {
|
|
39
|
-
super('跨会话 ec msg/group send 必须显式指定 --return none|required');
|
|
40
|
-
this.name = 'HandoffReturnPolicyRequiredError';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
36
|
export class TriggerHandoffRequiredForbiddenError extends Error {
|
|
44
37
|
code = 'TRIGGER_HANDOFF_REQUIRED_FORBIDDEN';
|
|
45
38
|
constructor() {
|
|
@@ -99,9 +92,10 @@ export class HandoffRuntime {
|
|
|
99
92
|
throw new Error('handoff context requires a different target session');
|
|
100
93
|
return { crossSession: false, targetSession: target };
|
|
101
94
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
// --return 参数已废弃:跨会话 send 不再要求显式指定返回策略,默认 'none'。
|
|
96
|
+
// 内部调用方(agent-create 等)仍可显式传 'required' 走回流路径。
|
|
97
|
+
const effectiveReturnPolicy = input.explicitReturnPolicy ?? 'none';
|
|
98
|
+
if (effectiveReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
|
|
105
99
|
throw new TriggerHandoffRequiredForbiddenError();
|
|
106
100
|
}
|
|
107
101
|
const handoffCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
|
|
@@ -127,7 +121,7 @@ export class HandoffRuntime {
|
|
|
127
121
|
delivery: targetChatType === 'group'
|
|
128
122
|
? { chatType: 'group', groupId: input.to }
|
|
129
123
|
: { chatType: 'private' },
|
|
130
|
-
returnPolicy:
|
|
124
|
+
returnPolicy: effectiveReturnPolicy,
|
|
131
125
|
originAuthorization: input.originAuthorization,
|
|
132
126
|
causation: handoffCausation,
|
|
133
127
|
now: this.now?.(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TriggerDefinitionManager } from '../trigger/manager.js';
|
|
2
|
+
import { ensureSessionRuntimeDir } from '../cli/task-context.js';
|
|
2
3
|
import { logger } from '../utils/logger.js';
|
|
3
4
|
import crypto from 'crypto';
|
|
4
5
|
import fs from 'fs';
|
|
@@ -6,6 +7,10 @@ import path from 'path';
|
|
|
6
7
|
import { executeTaskResume } from './task/task-resume-operation.js';
|
|
7
8
|
import { isHClassPath, isLClassPath, isSameOrDescendant } from './permission/protected-paths.js';
|
|
8
9
|
import { resolveStorageCommandArgv } from '../aun/storage/index.js';
|
|
10
|
+
import { agentDir } from '../paths.js';
|
|
11
|
+
import { parsePeerKey } from './relation/peer-identity.js';
|
|
12
|
+
import { resolvePeerRoleDetail } from '../config/peer-role-resolver.js';
|
|
13
|
+
import { buildDebugTaskContext } from './auth/debug-context.js';
|
|
9
14
|
import { computeBuildId } from '../utils/evolcore-version.js';
|
|
10
15
|
import { loadAgent } from '../config-store.js';
|
|
11
16
|
import { executeLocalInitializationCommand, hasSuppliedInitialization } from '../cli/agent-create.js';
|
|
@@ -17,7 +22,9 @@ import { registerHandoffIpc } from './handoff/ipc-registration.js';
|
|
|
17
22
|
import { registerAunIpcSender } from './message/aun-ipc-sender.js';
|
|
18
23
|
import { registerConsolidationCheckExecutor, registerConsolidationLaunchExecutor, } from './memory/consolidation.js';
|
|
19
24
|
import { createTriggerBaseagentResolver, registerTriggerIpcExecutor } from '../trigger/ipc-service.js';
|
|
25
|
+
import { createThreadOperationExecutor } from './thread/ipc-operation.js';
|
|
20
26
|
import { isLinkedInstall } from '../utils/npm-ops.js';
|
|
27
|
+
import { buildSystemSessionContext, getSystemSessionModule } from './system-session/registry.js';
|
|
21
28
|
export function registerDaemonIpc({ ipcServer, agentRegistry, agentDelegationRegistry, sessionManager, statsCollector, aidStatsCollector, responseEngine, messageQueue, agentMap, commandHandler, channelInstances, eventBus, handoffRuntime, reloadCoordinator, agentApplicationService, agentInitializationService, enqueueConsolidation, completeAgentBootstrap, contactBinds, bindService, diagService, getProcessOwners, getControlAidState, daemonAid, triggerControlToken, triggerSchedulers, daemonTriggerOwner, primaryAgent, triggerAuthorization, }) {
|
|
22
29
|
const authorizeManagedAgentOperation = createDelegationAuthorizer({
|
|
23
30
|
agentDelegationRegistry,
|
|
@@ -52,6 +59,137 @@ export function registerDaemonIpc({ ipcServer, agentRegistry, agentDelegationReg
|
|
|
52
59
|
agentMap,
|
|
53
60
|
isLinkedInstall: isLinkedInstall(),
|
|
54
61
|
});
|
|
62
|
+
if (process.env.EVOLCORE_DEBUG_MODE === '1') {
|
|
63
|
+
ipcServer.setDebugIssueTokenExecutor(async ({ sessionId }) => {
|
|
64
|
+
try {
|
|
65
|
+
const activeContext = responseEngine.getTaskRuntimeContext(sessionId);
|
|
66
|
+
let runtimeContext = activeContext;
|
|
67
|
+
if (!runtimeContext) {
|
|
68
|
+
const session = await sessionManager.getSessionById(sessionId);
|
|
69
|
+
if (!session)
|
|
70
|
+
return { ok: false, error: `session not found: ${sessionId}` };
|
|
71
|
+
runtimeContext = buildDebugTaskContext({
|
|
72
|
+
session,
|
|
73
|
+
getTrigger: (agentAid, triggerId) => new TriggerDefinitionManager(agentAid).get(triggerId),
|
|
74
|
+
sessionRuntimeDir: ensureSessionRuntimeDir(session.id),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!runtimeContext.selfAid)
|
|
78
|
+
return { ok: false, error: 'debug session has no selfAid' };
|
|
79
|
+
const channelType = runtimeContext.channel || 'aun';
|
|
80
|
+
const channel = runtimeContext.channelKey || channelType;
|
|
81
|
+
const peerKeyId = (() => {
|
|
82
|
+
try {
|
|
83
|
+
return parsePeerKey(runtimeContext.peerKey || '').channelId;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return runtimeContext.peerId || runtimeContext.channelId || '';
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
const chatType = runtimeContext.chatType === 'group' ? 'group' : 'private';
|
|
90
|
+
const conversationId = chatType === 'group'
|
|
91
|
+
? (runtimeContext.channelId || peerKeyId)
|
|
92
|
+
: peerKeyId;
|
|
93
|
+
const peerRole = resolvePeerRoleDetail({
|
|
94
|
+
selfAid: runtimeContext.selfAid,
|
|
95
|
+
channelKey: channel,
|
|
96
|
+
channelType,
|
|
97
|
+
chatType,
|
|
98
|
+
actorId: peerKeyId,
|
|
99
|
+
conversationId,
|
|
100
|
+
});
|
|
101
|
+
const token = agentDelegationRegistry.issueDebug({
|
|
102
|
+
sessionId,
|
|
103
|
+
selfAid: runtimeContext.selfAid,
|
|
104
|
+
channel,
|
|
105
|
+
channelType,
|
|
106
|
+
chatType,
|
|
107
|
+
peerKey: runtimeContext.peerKey || '',
|
|
108
|
+
actorId: peerKeyId || runtimeContext.selfAid,
|
|
109
|
+
issuedRole: peerRole.effectiveRole || 'none',
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, token, runtimeContext };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
ipcServer.setThreadOperationExecutor(createThreadOperationExecutor({
|
|
119
|
+
sessionManager,
|
|
120
|
+
agentDelegationRegistry,
|
|
121
|
+
getProcessOwners,
|
|
122
|
+
sendSystemMsg: async (params) => {
|
|
123
|
+
if (ipcServer.hasAunMsgSender()) {
|
|
124
|
+
const sent = await ipcServer.sendAunMsg({
|
|
125
|
+
aid: params.from,
|
|
126
|
+
to: params.to,
|
|
127
|
+
scope: params.peerKind === 'group' ? 'group' : 'msg',
|
|
128
|
+
payload: { type: 'topic-created', topicName: params.topicName, threadId: params.threadId },
|
|
129
|
+
thread: params.threadId,
|
|
130
|
+
});
|
|
131
|
+
if (!sent?.ok)
|
|
132
|
+
throw new Error(sent?.error || 'Topic notification failed');
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onDeleted: (aid, threadId) => {
|
|
136
|
+
const scheduler = triggerSchedulers.get(aid);
|
|
137
|
+
if (scheduler) {
|
|
138
|
+
const related = scheduler.list({ all: true }).filter((trigger) => trigger.origin?.threadId === threadId
|
|
139
|
+
|| trigger.labels?.originThreadId === threadId
|
|
140
|
+
|| trigger.labels?.targetThreadId === threadId);
|
|
141
|
+
for (const trigger of related) {
|
|
142
|
+
try {
|
|
143
|
+
scheduler.delete(trigger.id);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
ipcServer.setSkillRefreshExecutor(async (aid, sessionId, delegationToken, delegationCommandHash) => {
|
|
151
|
+
const fail = (code, error) => ({ ok: false, code, error });
|
|
152
|
+
if (!agentRegistry.get(aid))
|
|
153
|
+
return fail('AGENT_NOT_FOUND', `Agent "${aid}" not found`);
|
|
154
|
+
if (sessionId) {
|
|
155
|
+
const authorization = await authorizeManagedAgentOperation({
|
|
156
|
+
sessionId,
|
|
157
|
+
delegationToken,
|
|
158
|
+
delegationCommandHash,
|
|
159
|
+
targetAid: aid,
|
|
160
|
+
}, 'skill.refresh');
|
|
161
|
+
if (!authorization.ok)
|
|
162
|
+
return fail(authorization.code, authorization.error);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const { refreshSkillIndex } = await import('./skill/startup.js');
|
|
166
|
+
refreshSkillIndex(agentDir(aid), aid);
|
|
167
|
+
const skillsDir = path.join(agentDir(aid), 'skills');
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
result: {
|
|
171
|
+
aid,
|
|
172
|
+
builtinIndexPath: path.join(skillsDir, '.index-builtin.md'),
|
|
173
|
+
localIndexPath: path.join(skillsDir, '.index-local.md'),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return fail('INTERNAL', error?.message || String(error));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
commandHandler.setSkillIndexRefresher(async (aid) => {
|
|
182
|
+
if (!agentRegistry.get(aid))
|
|
183
|
+
return { ok: false, error: `Agent "${aid}" not found` };
|
|
184
|
+
try {
|
|
185
|
+
const { refreshSkillIndex } = await import('./skill/startup.js');
|
|
186
|
+
refreshSkillIndex(agentDir(aid), aid);
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
191
|
+
}
|
|
192
|
+
});
|
|
55
193
|
registerHandoffIpc({
|
|
56
194
|
ipcServer,
|
|
57
195
|
agentDelegationRegistry,
|
|
@@ -61,7 +199,7 @@ export function registerDaemonIpc({ ipcServer, agentRegistry, agentDelegationReg
|
|
|
61
199
|
handoffRuntime,
|
|
62
200
|
getProcessOwners,
|
|
63
201
|
});
|
|
64
|
-
registerAunIpcSender({ ipcServer, channelInstances, agentDelegationRegistry, responseEngine, handoffRuntime });
|
|
202
|
+
registerAunIpcSender({ ipcServer, channelInstances, agentDelegationRegistry, responseEngine, handoffRuntime, triggerSchedulers });
|
|
65
203
|
ipcServer.setAgentReloadExecutor((aid, options) => reloadCoordinator.reload(aid, options));
|
|
66
204
|
registerConsolidationCheckExecutor({ ipcServer, agentRegistry, enqueueConsolidation });
|
|
67
205
|
ipcServer.setInitializationExecutor(async (command) => executeLocalInitializationCommand(agentInitializationService, command));
|
|
@@ -212,7 +350,7 @@ export function registerRuntimeIpcProviders({ ipcServer, channelInstances, messa
|
|
|
212
350
|
const session = await sessionManager.getSessionById(sessionId);
|
|
213
351
|
if (!session)
|
|
214
352
|
return null;
|
|
215
|
-
const { buildDebugTaskContext } = await import('./
|
|
353
|
+
const { buildDebugTaskContext } = await import('./auth/debug-context.js');
|
|
216
354
|
const { ensureSessionRuntimeDir } = await import('../cli/task-context.js');
|
|
217
355
|
return buildDebugTaskContext({
|
|
218
356
|
session,
|
|
@@ -222,6 +360,31 @@ export function registerRuntimeIpcProviders({ ipcServer, channelInstances, messa
|
|
|
222
360
|
});
|
|
223
361
|
logger.info('[Memory] 开发仓安装:已启用 ec memory --debug <sessionId>');
|
|
224
362
|
}
|
|
363
|
+
ipcServer.setSystemSessionCommandGate(async ({ sessionId, delegationToken, delegationCommandHash, type, argv }) => {
|
|
364
|
+
// 只读查询,不消耗已审批命令
|
|
365
|
+
const validation = agentDelegationRegistry.validateForContext(delegationToken, sessionId, delegationCommandHash);
|
|
366
|
+
const marker = validation.ok ? validation.grant.systemSession : undefined;
|
|
367
|
+
if (!validation.ok || !marker)
|
|
368
|
+
return undefined;
|
|
369
|
+
const module = getSystemSessionModule(marker.type);
|
|
370
|
+
if (!module)
|
|
371
|
+
return { allow: false, code: 'system_session_unknown', reason: `未注册的系统会话类型: ${marker.type}` };
|
|
372
|
+
// 无 argv 的请求只放行任务上下文查询(ec 命令取自身上下文用)
|
|
373
|
+
if (!argv) {
|
|
374
|
+
return type === 'task-runtime-context'
|
|
375
|
+
? { allow: true }
|
|
376
|
+
: { allow: false, code: 'system_session_command_forbidden', reason: `系统会话不允许调用 ${type}` };
|
|
377
|
+
}
|
|
378
|
+
const session = await sessionManager.getSessionById(sessionId);
|
|
379
|
+
const ctx = buildSystemSessionContext({
|
|
380
|
+
marker,
|
|
381
|
+
sessionId,
|
|
382
|
+
selfAid: validation.grant.selfAid,
|
|
383
|
+
projectPath: session?.projectPath ?? '',
|
|
384
|
+
metadata: session?.metadata,
|
|
385
|
+
});
|
|
386
|
+
return module.checkCommand(argv, ctx);
|
|
387
|
+
});
|
|
225
388
|
ipcServer.setTaskRuntimeContextAuthorizer(({ sessionId, delegationToken, delegationCommandHash, consumeApproval }) => {
|
|
226
389
|
if (consumeApproval && !agentDelegationRegistry.validate)
|
|
227
390
|
return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Command authorization is unavailable' };
|
|
@@ -230,8 +393,17 @@ export function registerRuntimeIpcProviders({ ipcServer, channelInstances, messa
|
|
|
230
393
|
: agentDelegationRegistry.validateForContext(delegationToken, sessionId, delegationCommandHash);
|
|
231
394
|
return authorization.ok
|
|
232
395
|
? { ok: true }
|
|
233
|
-
: {
|
|
396
|
+
: {
|
|
397
|
+
ok: false,
|
|
398
|
+
code: authorization.code,
|
|
399
|
+
error: authorization.reason,
|
|
400
|
+
diagnosticCause: authorization.diagnosticCause,
|
|
401
|
+
...(authorization.diagnosticEventId ? { diagnosticEventId: authorization.diagnosticEventId } : {}),
|
|
402
|
+
};
|
|
234
403
|
});
|
|
404
|
+
if (agentDelegationRegistry.recordDiagnosticFailure) {
|
|
405
|
+
ipcServer.setDelegationFailureRecorder(agentDelegationRegistry.recordDiagnosticFailure.bind(agentDelegationRegistry));
|
|
406
|
+
}
|
|
235
407
|
ipcServer.setCodexPreToolUseExecutor(async (params) => {
|
|
236
408
|
for (const runner of new Set(agentMap.values())) {
|
|
237
409
|
if (typeof runner?.evaluatePreToolUse !== 'function')
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* batch-meta.ts — 整理批次元数据文件(batch.md)
|
|
3
|
+
*
|
|
4
|
+
* 格式:YAML frontmatter + 正文(feedback 内容)
|
|
5
|
+
*
|
|
6
|
+
* ---
|
|
7
|
+
* executionSessionId: memory_20261015T000000_abc
|
|
8
|
+
* selfAid: dddd.agentid.pub
|
|
9
|
+
* mode: notes
|
|
10
|
+
* ...
|
|
11
|
+
* ---
|
|
12
|
+
*
|
|
13
|
+
* ## 整理反馈
|
|
14
|
+
*
|
|
15
|
+
* <feedback 正文>
|
|
16
|
+
*
|
|
17
|
+
* 在 batchDir 创建时写入初始元数据(status=running),会话结束时更新状态、填入 feedback。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
const BATCH_MD = 'batch.md';
|
|
22
|
+
const FEEDBACK_HEADING = '## 整理反馈';
|
|
23
|
+
// ── frontmatter 序列化 / 解析 ──────────────────────────────────────────────
|
|
24
|
+
function serializeFrontmatter(meta) {
|
|
25
|
+
const lines = ['---'];
|
|
26
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
27
|
+
if (v === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
lines.push(`${k}: ${String(v)}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('---');
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
function parseFrontmatter(content) {
|
|
35
|
+
if (!content.startsWith('---'))
|
|
36
|
+
return { meta: {}, body: content };
|
|
37
|
+
const end = content.indexOf('\n---', 3);
|
|
38
|
+
if (end === -1)
|
|
39
|
+
return { meta: {}, body: content };
|
|
40
|
+
const block = content.slice(4, end);
|
|
41
|
+
const body = content.slice(end + 4).replace(/^\n/, '');
|
|
42
|
+
const meta = {};
|
|
43
|
+
for (const line of block.split('\n')) {
|
|
44
|
+
const sep = line.indexOf(':');
|
|
45
|
+
if (sep === -1)
|
|
46
|
+
continue;
|
|
47
|
+
const key = line.slice(0, sep).trim();
|
|
48
|
+
const val = line.slice(sep + 1).trim();
|
|
49
|
+
if (val === 'true')
|
|
50
|
+
meta[key] = true;
|
|
51
|
+
else if (val === 'false')
|
|
52
|
+
meta[key] = false;
|
|
53
|
+
else if (val === '')
|
|
54
|
+
meta[key] = undefined;
|
|
55
|
+
else
|
|
56
|
+
meta[key] = val;
|
|
57
|
+
}
|
|
58
|
+
return { meta: meta, body };
|
|
59
|
+
}
|
|
60
|
+
function batchMdPath(batchDir) {
|
|
61
|
+
return path.join(batchDir, BATCH_MD);
|
|
62
|
+
}
|
|
63
|
+
// ── 公开 API ───────────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* 创建 batchDir 后立即写入初始 batch.md(status=running,无 feedback 正文)。
|
|
66
|
+
* 文件已存在时跳过(重入保护)。
|
|
67
|
+
*/
|
|
68
|
+
export function writeBatchMeta(batchDir, meta) {
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(batchDir, { recursive: true });
|
|
71
|
+
const filePath = batchMdPath(batchDir);
|
|
72
|
+
if (fs.existsSync(filePath))
|
|
73
|
+
return;
|
|
74
|
+
const content = serializeFrontmatter(meta) + '\n';
|
|
75
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
76
|
+
}
|
|
77
|
+
catch { /* 失败静默,不影响主流程 */ }
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 会话结束时更新 batch.md:
|
|
81
|
+
* - 合并 frontmatter 字段(status / finishedAt / success / agentSessionId)
|
|
82
|
+
* - 如果 feedbackText 非空,写入或替换「## 整理反馈」段落
|
|
83
|
+
*/
|
|
84
|
+
export function updateBatchMeta(batchDir, updates) {
|
|
85
|
+
try {
|
|
86
|
+
const filePath = batchMdPath(batchDir);
|
|
87
|
+
if (!fs.existsSync(filePath))
|
|
88
|
+
return;
|
|
89
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
91
|
+
// 合并字段
|
|
92
|
+
const updated = {
|
|
93
|
+
...meta,
|
|
94
|
+
status: updates.status,
|
|
95
|
+
finishedAt: updates.finishedAt,
|
|
96
|
+
success: updates.success,
|
|
97
|
+
...(updates.agentSessionId ? { agentSessionId: updates.agentSessionId } : {}),
|
|
98
|
+
};
|
|
99
|
+
// 更新 feedback 正文
|
|
100
|
+
let newBody = body;
|
|
101
|
+
if (updates.feedbackText) {
|
|
102
|
+
const feedbackBlock = `${FEEDBACK_HEADING}\n\n${updates.feedbackText}\n`;
|
|
103
|
+
const headingIdx = newBody.indexOf(FEEDBACK_HEADING);
|
|
104
|
+
if (headingIdx !== -1) {
|
|
105
|
+
// 替换已有段落
|
|
106
|
+
newBody = newBody.slice(0, headingIdx) + feedbackBlock;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
newBody = (newBody ? newBody.trimEnd() + '\n\n' : '') + feedbackBlock;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const result = serializeFrontmatter(updated) + '\n' + (newBody ? '\n' + newBody : '');
|
|
113
|
+
fs.writeFileSync(filePath, result, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch { /* 失败静默 */ }
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 读取 batch.md 的 frontmatter(解析失败返回 undefined)。
|
|
119
|
+
*/
|
|
120
|
+
export function readBatchMeta(batchDir) {
|
|
121
|
+
try {
|
|
122
|
+
const filePath = batchMdPath(batchDir);
|
|
123
|
+
if (!fs.existsSync(filePath))
|
|
124
|
+
return undefined;
|
|
125
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
126
|
+
const { meta } = parseFrontmatter(raw);
|
|
127
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|