evolcore 0.0.29 → 0.0.30
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 +7 -0
- package/dist/agents/codex-runner.js +61 -14
- package/dist/aun/aid/agentmd.js +224 -19
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +70 -4
- package/dist/aun/group/index.js +1 -0
- package/dist/{core → aun}/group/invite-service.js +4 -4
- package/dist/channels/daemon.js +1 -1
- package/dist/cli/agent-command.js +7 -7
- package/dist/cli/aun-commands.js +97 -37
- package/dist/cli/data-command.js +2 -1
- package/dist/cli/managed-command-guard.js +2 -2
- package/dist/cli/model.js +1 -1
- package/dist/cli/response.js +1 -1
- package/dist/cli/trigger-command.js +52 -29
- package/dist/config/boot-log.js +95 -1
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/peer-role-resolver.js +21 -0
- package/dist/core/agent-ipc-registration.js +409 -0
- package/dist/core/agent-runtime.js +435 -0
- package/dist/core/auth/delegation-authorization.js +58 -0
- package/dist/core/auth/operation-catalog.js +8 -0
- package/dist/core/bootstrap-service.js +65 -1
- package/dist/core/channel-runtime.js +435 -0
- package/dist/core/command/command-handler.js +4 -1
- package/dist/core/daemon-bootstrap.js +151 -0
- package/dist/core/daemon-control.js +205 -0
- package/dist/core/daemon-lifecycle.js +225 -0
- package/dist/core/data-migration.js +4 -1
- package/dist/core/handoff/ipc-registration.js +156 -0
- package/dist/core/ipc-registration.js +457 -0
- package/dist/core/memory/consolidation-trigger.js +8 -0
- package/dist/core/memory/consolidation.js +838 -0
- package/dist/core/message/aun-ipc-sender.js +226 -0
- package/dist/core/message/outbound.js +165 -0
- package/dist/core/permission/approval-gateway.js +13 -0
- package/dist/core/permission/ec-command-parser.js +80 -35
- package/dist/core/permission/sandbox-runtime.js +23 -2
- package/dist/core/permission/tool-error-code.js +1 -0
- package/dist/core/permission/tool-policy.js +61 -24
- package/dist/core/session/session-idle-monitor.js +17 -0
- package/dist/index.js +352 -4841
- package/dist/ipc.js +2 -1
- package/dist/stats/db.js +24 -0
- package/dist/stats/index.js +1 -1
- package/dist/stats/writer.js +25 -1
- package/dist/trigger/application-service.js +157 -0
- package/dist/trigger/authorization.js +371 -0
- package/dist/trigger/ipc-service.js +406 -0
- package/dist/trigger/local-files.js +133 -0
- package/dist/trigger/manager.js +235 -22
- package/dist/trigger/scheduler.js +30 -5
- package/dist/trigger/script-executor.js +176 -39
- package/dist/trigger/script-sandbox.js +458 -0
- package/dist/trigger/system-definitions.js +188 -0
- package/dist/trigger/validation.js +0 -3
- package/dist/utils/aid-bind.js +1 -1
- package/dist/utils/evolcore-version.js +74 -0
- package/dist/utils/logger.js +16 -0
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/model.md +3 -2
- package/kits/docs/evolcore/rpc.md +4 -0
- package/kits/docs/evolcore/trigger.md +15 -1
- package/kits/schemas/relation-config.schema.9.json +1 -1
- package/kits/templates/system-fragments/commands.md +0 -1
- package/package.json +1 -1
- package/dist/core/memory/launch-consolidation.js +0 -395
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import { getPackageRoot } from '../paths.js';
|
|
5
6
|
let cachedVersion;
|
|
6
7
|
let cachedFlavor;
|
|
@@ -20,6 +21,27 @@ export function readInstalledEvolcoreVersion() {
|
|
|
20
21
|
cachedVersion = version;
|
|
21
22
|
return version;
|
|
22
23
|
}
|
|
24
|
+
export function readFastaunVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const url = import.meta.resolve?.('@agentunion/fastaun');
|
|
27
|
+
if (!url)
|
|
28
|
+
return 'unknown';
|
|
29
|
+
let dir = path.dirname(fileURLToPath(url));
|
|
30
|
+
while (dir !== path.dirname(dir)) {
|
|
31
|
+
const packagePath = path.join(dir, 'package.json');
|
|
32
|
+
if (fs.existsSync(packagePath)) {
|
|
33
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
34
|
+
if (packageJson.name === '@agentunion/fastaun')
|
|
35
|
+
return packageJson.version || 'unknown';
|
|
36
|
+
}
|
|
37
|
+
dir = path.dirname(dir);
|
|
38
|
+
}
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'unknown';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
23
45
|
/**
|
|
24
46
|
* 区分源码仓库运行与全局安装包。`package.json` 的 `files` 白名单不含 `src/`,
|
|
25
47
|
* 所以 `src/index.ts` 的存在性可以稳定区分两者:同一个版本号在 dev 下跑的是
|
|
@@ -148,3 +170,55 @@ export function readBuildIdentity(distDir, version) {
|
|
|
148
170
|
...(version ? { version } : {}),
|
|
149
171
|
};
|
|
150
172
|
}
|
|
173
|
+
export function reportEvolcoreStartup(entryUrl, report) {
|
|
174
|
+
const packageRoot = getPackageRoot();
|
|
175
|
+
const runtimeEntry = fileURLToPath(entryUrl);
|
|
176
|
+
const diskDistDir = path.join(packageRoot, 'dist');
|
|
177
|
+
const runtimeIsDist = path.resolve(path.dirname(runtimeEntry)) === path.resolve(diskDistDir);
|
|
178
|
+
const runtimeBuildIdentity = readBuildIdentity(runtimeIsDist ? path.dirname(runtimeEntry) : diskDistDir, readInstalledEvolcoreVersion());
|
|
179
|
+
if (!runtimeIsDist)
|
|
180
|
+
runtimeBuildIdentity.id = null;
|
|
181
|
+
const isLinked = fs.existsSync(path.join(packageRoot, '.git'));
|
|
182
|
+
const directoryType = runtimeIsDist ? (isLinked ? '开发仓/dist' : '安装路径/dist') : '源码(tsx)';
|
|
183
|
+
const scanDir = runtimeIsDist ? path.join(packageRoot, 'dist') : path.join(packageRoot, 'src');
|
|
184
|
+
let latestMtime = 0;
|
|
185
|
+
const scanRecursive = (directory) => {
|
|
186
|
+
try {
|
|
187
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
188
|
+
if (entry.name === 'node_modules')
|
|
189
|
+
continue;
|
|
190
|
+
const full = path.join(directory, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
scanRecursive(full);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
196
|
+
const modifiedAt = fs.statSync(full).mtimeMs;
|
|
197
|
+
if (modifiedAt > latestMtime)
|
|
198
|
+
latestMtime = modifiedAt;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
};
|
|
204
|
+
scanRecursive(scanDir);
|
|
205
|
+
let version = '?';
|
|
206
|
+
try {
|
|
207
|
+
version = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version;
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
const formatTime = (milliseconds) => {
|
|
211
|
+
const date = new Date(milliseconds);
|
|
212
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
|
|
213
|
+
};
|
|
214
|
+
console.error(`[EvolCore] EvolCore v${version}`);
|
|
215
|
+
console.error(`[EvolCore] 执行类型: ${directoryType}`);
|
|
216
|
+
console.error(`[EvolCore] 包路径: ${packageRoot}`);
|
|
217
|
+
console.error(`[EvolCore] 代码时间: ${latestMtime ? formatTime(latestMtime) : '?'}`);
|
|
218
|
+
console.error('[EvolCore] ✓ Version info printed');
|
|
219
|
+
const diskBuildId = computeBuildId(diskDistDir);
|
|
220
|
+
console.error(`[EvolCore] 运行 build ID: ${runtimeIsDist ? (runtimeBuildIdentity.id ?? '?') : 'source'}`);
|
|
221
|
+
console.error(`[EvolCore] 磁盘 dist build ID: ${diskBuildId ?? '?'}`);
|
|
222
|
+
report(`[Build] runtimeMode=${runtimeIsDist ? 'dist' : 'source'} runtimeBuildId=${runtimeBuildIdentity.id ?? 'source'} diskDistBuildId=${diskBuildId ?? 'unknown'} match=${runtimeIsDist && runtimeBuildIdentity.id !== null && runtimeBuildIdentity.id === diskBuildId}`);
|
|
223
|
+
return { runtimeIsDist, runtimeBuildIdentity, diskDistDir };
|
|
224
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -12,6 +12,22 @@ const config = {
|
|
|
12
12
|
};
|
|
13
13
|
let writers = null;
|
|
14
14
|
fs.mkdirSync(resolvePaths().logs, { recursive: true });
|
|
15
|
+
export function installChannelSdkConsoleFilter() {
|
|
16
|
+
const originalLog = console.log;
|
|
17
|
+
const originalInfo = console.info;
|
|
18
|
+
const filter = (...args) => {
|
|
19
|
+
const firstArg = String(args[0] || '');
|
|
20
|
+
return firstArg.includes('[info]') || firstArg.includes('[ws]');
|
|
21
|
+
};
|
|
22
|
+
console.log = (...args) => {
|
|
23
|
+
if (!filter(...args))
|
|
24
|
+
originalLog(...args);
|
|
25
|
+
};
|
|
26
|
+
console.info = (...args) => {
|
|
27
|
+
if (!filter(...args))
|
|
28
|
+
originalInfo(...args);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
15
31
|
function closeWriters(current) {
|
|
16
32
|
current.main.close();
|
|
17
33
|
current.message?.close();
|
|
@@ -42,7 +42,7 @@ Windows 上 Codex 托管任务出现 `helper_sandbox_lock_failed`、`SetNamedSec
|
|
|
42
42
|
| `ec storage` | 文件存储底层调试入口 | storage 调试/旧命令/底层上传下载 | 任意有渠道场景 | `storage.md` |
|
|
43
43
|
| `ec agent` | EvolAgent 生命周期 | 创建/启停/热重载/改配置 | 管理员(owner/admin) | `agent.md`;完整创建与 Bootstrap 见 `agent-create.md` |
|
|
44
44
|
| `ec contact` | Contact Book 与访问控制 | 申请添加、拉黑/解除拉黑、查拉黑 | Agent 当前私聊可申请添加;管理写操作按角色限制 | `contact.md` |
|
|
45
|
-
| `ec rpc` | 底层 AUN RPC(逃生通道) | 直接调协议方法 |
|
|
45
|
+
| `ec rpc` | 底层 AUN RPC(逃生通道) | 直接调协议方法 | 仅宿主终端;托管会话仅可查看帮助 | `rpc.md` |
|
|
46
46
|
| `ec menu-client` | 远程 Daemon Menu 请求 | 远程诊断/远程 daemon 操作/daemon 日志/监控/原始配置/跨主机 | 运维主机或具备 `ec.menu.request` 权限的托管 Agent | `menu-client.md` |
|
|
47
47
|
|
|
48
48
|
## 会话自管理类(不连 AUN,操作当前会话)
|
|
@@ -6,16 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
本页列出独立终端中 `ec aid` 的完整 CLI 能力。在 EvolCore 托管 Agent 会话中,为防止会话枚举、创建、删除或切换本地身份,只允许以下精确命令:
|
|
8
8
|
|
|
9
|
+
帮助查询不受 AID 操作范围限制:`ec aid`、`ec aid --help`、`ec aid help` 以及各子命令的 `--help` / `-h` 只显示用法,不执行身份操作。实际操作仍按下列范围鉴权。
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
12
|
ec aid show <当前 self AID> [--format json]
|
|
11
13
|
ec aid lookup <任意合法 AID> [--format json]
|
|
12
14
|
ec aid agentmd get <任意合法 AID> [--format json]
|
|
13
15
|
# 仅 request / bypass
|
|
14
16
|
ec aid agentmd put <当前 self AID> [--format json]
|
|
17
|
+
ec aid agentmd set <当前 self AID> <name|type|description|version|tags> <value> [--format json]
|
|
15
18
|
ec aid avatar <当前 self AID> (--file <path> | --url <https-url>) [--format json]
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
`set` 的 value 是单个 CLI 参数;`tags` 必须传 JSON 字符串数组,例如 `'["coding","tools","aun"]'`。set 仅更改 YAML frontmatter 对应字段,保留正文,再签名发布。`name`、`type`、`description`、`version` 与 `tags` 会按 agent.md schema 校验。
|
|
22
|
+
|
|
23
|
+
仅允许不带选项的基本形式,或在末尾附加一组完整的 `--format json`;不能使用 `--aun-path`、未知选项、管道、重定向或其它命令。所有 `allowAccess=true` 的已认证角色(包括 visitor、member、admin、owner 及自定义角色)均开放 `lookup` 和 `agentmd get`,目标可以是任意合法 AID。`show` 只允许当前 self AID:owner/admin 返回完整本地身份诊断,visitor/member 及其它非管理角色返回不含私钥存在性、密钥匹配和签名能力的脱敏结果。三个查询可在所有 `permissionMode` 执行。`agentmd put`、`agentmd set` 和 `avatar` 会发布远端名片,属于写操作,仅允许针对当前 self AID 的 `request`、`bypass`,`readonly`、`auto` 和未知模式均拒绝。头像支持 PNG、JPEG、WebP,大小上限 500KB,分辨率 128–2048 像素,宽高比 0.8–1.25;`--url` 仅支持 HTTPS,下载由 daemon 执行并做 SSRF/重定向校验。成功 JSON 返回 `sha256`、MIME 和尺寸元数据。`lookup` 探测指定公开 AID;`get` 下载并验签指定公开名片,同时刷新本地 AID 缓存。`list`、其它 AID 的 `show/agentmd put/set/avatar` 以及 `new/delete` 均拒绝。独立终端不受此托管会话限制。
|
|
24
|
+
|
|
25
|
+
`agentmd get --format json` 返回 `verification`、来源 `source`(`remote` / `local-cache`)、内容 SHA-256 和 `localState`;有远端版本响应头时还返回 ETag / Last-Modified。`agentmd put/set --format json` 返回发布后版本信息、是否执行了版本预检,以及 `atomicVersionCheck: false`。当前 fastaun upload API 不支持条件 PUT / If-Match,因此版本预检和进程内锁**不是原子远端 CAS**:多客户端并发更新仍可能在预检后覆盖;在网关或 SDK 支持条件写入前,不应将其视为完整并发锁。
|
|
19
26
|
|
|
20
27
|
## 更换头像
|
|
21
28
|
|
|
@@ -43,6 +50,7 @@ ec aid lookup <aid>
|
|
|
43
50
|
# agent.md 名片:签名上传 / 下载验签
|
|
44
51
|
ec aid agentmd put <aid>
|
|
45
52
|
ec aid agentmd get <aid>
|
|
53
|
+
ec aid agentmd set <aid> <name|type|description|version|tags> <value>
|
|
46
54
|
ec aid avatar <aid> (--file <path> | --url <https-url>)
|
|
47
55
|
|
|
48
56
|
# 删除指定本地 AID(无网络注销)
|
|
@@ -38,8 +38,9 @@ ec model check [--self <aid>]
|
|
|
38
38
|
|
|
39
39
|
生效优先级为关系、角色、agent、defaults。`--peer` 可使用 `channelType#channelId`;裸 AID 按 `aun#<aid>` 处理。
|
|
40
40
|
|
|
41
|
-
在 EvolCore
|
|
42
|
-
|
|
41
|
+
在 EvolCore 托管会话中,所有 `ec model` 子命令都通过 daemon 的 `ec ctl` 路径作用于已认证的当前会话,不能指定 `--self`、`--peer` 或 `--role`;上表的显式作用域仅适用于宿主终端。需要读取或修改持久化模型配置时,使用 `ec config`,由 daemon 按实际角色和目标范围鉴权。本机终端不具备隐式当前会话语义。
|
|
42
|
+
|
|
43
|
+
帮助查询不选择目标或执行操作,`ec model list --self <aid> --help` 等形式仍可查看帮助。
|
|
43
44
|
|
|
44
45
|
## 输出与权限
|
|
45
46
|
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
> 常规消息收发用 `ec msg` / `ec group`,文件用 `ec storage`。`ec rpc` 是这些命令未覆盖到协议方法时的低层入口。
|
|
6
6
|
|
|
7
|
+
## 运行边界
|
|
8
|
+
|
|
9
|
+
实际 RPC 调用仅供宿主终端使用;EvolCore 托管 Agent 会话只能查看 `ec rpc --help`,不能执行协议调用。此入口会直接使用指定本地 AID 建立 SDK 连接,没有托管会话的 delegated sender 和逐操作 daemon 鉴权,因此不能用于绕过 `ec msg`、`ec group` 或 `ec fs` 的权限限制。
|
|
10
|
+
|
|
7
11
|
## 用法
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
> `ec trigger` 通过 daemon IPC 直接操作 TriggerRuntimeScheduler。agent 会话内省略 `--agent` 时,CLI 使用当前任务上下文里的 agent AID;只有跨身份操作或脱离 agent 会话执行时才需要显式传 `--agent <aid>`。
|
|
6
6
|
|
|
7
|
+
## 运行边界
|
|
8
|
+
|
|
9
|
+
托管 Agent 的实际 owner 可使用 `--file`、`--prompt-file`、`--script-path` 和 `--script-file`。CLI 必须先通过 daemon 校验当前 delegation、真实关系角色和有效权限模式,再读取当前项目或本会话 `$TMPDIR` 内的文件;readonly 任务不能创建或修改 Trigger,环境变量里的 owner 声明不能代替授权。普通管理角色仍可按原权限使用内联 `--prompt`。
|
|
10
|
+
|
|
11
|
+
文件输入限制为 4 MiB,拒绝符号链接、junction、硬链接、路径逃逸、其他会话临时目录、密钥及其他 Agent 私有数据。普通定义导入必须属于当前 Agent;脚本只上传 `execution.script.path` 指定的单个文件,不允许覆盖 `trigger.json`、`active.json` 等管理状态。绝对 `--script-path` 会上传脚本内容并保存 Agent 内相对路径,不保留宿主绝对路径。`--help` / `-h` 在鉴权和文件读取前返回。
|
|
12
|
+
|
|
13
|
+
普通脚本 Trigger 按每次执行重新核验的 Agent 授权运行,`bypass` 不等于宿主权限:项目按有效模式只读或可写,脚本使用只读快照,只有当前 run 的临时工作目录可写,不继承 daemon 凭据或 IPC 入口,网络禁用。脚本快照位置不同于持久化 Trigger 目录;需要项目资源时按工作目录定位,不能依赖 Trigger 状态文件或其他脚本附件。Node `.js` 保留原 package 的模块类型。
|
|
14
|
+
|
|
15
|
+
- Linux:需要可用的 `bubblewrap`,使用独立根文件系统及进程/网络命名空间,仅绑定项目、最小系统运行时和当前 run。
|
|
16
|
+
- Windows:需要支持受限读取策略的原生 Codex CLI,以及宿主已准备好的 elevated sandbox。每次启动前先用同一个临时 canary 验证“显式授权可读、撤回授权后拒绝读取”和当前 run 可写;只有权限拒绝才算通过,文件不存在、探针超时或取消均失败关闭。旧初始化、不支持的运行时或无法证明读取隔离时返回 `SCRIPT_SANDBOX_UNAVAILABLE`,不会启动用户脚本;托管任务不会自动提权、初始化沙箱或降级为宿主执行。
|
|
17
|
+
- 其他平台:当前普通脚本执行失败关闭,不提供未隔离的回退。
|
|
18
|
+
|
|
19
|
+
有效批准的 `fullaccess` 和 daemon 自有 Trigger 仍走宿主执行路径。`fullaccess` 每次检查批准者是否仍是 DaemonOwner,不依赖普通创建者的当前关系角色;daemon 自有任务检查服务授权。普通 Agent Trigger 每次执行及重试都检查来源主体在该 Agent 上的当前角色和权限上限,撤销授权后不再启动脚本。
|
|
20
|
+
|
|
7
21
|
## 什么时候用
|
|
8
22
|
|
|
9
23
|
- 用户要求“稍后/每天/每小时/定时/周期性”执行任务
|
|
@@ -244,7 +258,7 @@ ec trigger rm <triggerId> # delete 的兼容别名
|
|
|
244
258
|
|
|
245
259
|
`update` 是 patch 语义,只提交需要修改的字段;未指定字段(包括长 prompt)保持不变。`--prompt-file <path>` 只更新非 script trigger 的 prompt,不接受完整 definition。已有 script trigger 使用 `--script-file <path>` 原地替换脚本内容;脚本路径、runtime、args 和调度配置保持不变:
|
|
246
260
|
|
|
247
|
-
`--script-file`
|
|
261
|
+
`--script-file` 通过受控 CLI 读取文件并提交给 daemon;托管任务的实际 Agent owner 在授权范围内也可替换自己的普通脚本。读取前和提交时分别校验授权,脚本原子落盘并重新验证路径。Agent 级 `fullaccess.enabled` 或创建会话的 fullaccess 身份不会自动升级 Trigger;宿主执行仍由 Trigger 自身 `execution.permissionMode=fullaccess` 和当前有效的 DaemonOwner 批准链控制。
|
|
248
262
|
|
|
249
263
|
```bash
|
|
250
264
|
ec trigger update <triggerId> --cron '0 10 * * *'
|
|
@@ -19,7 +19,6 @@ ec task 恢复暂停或异常中断的本 Agent 会话任务(resume <sessi
|
|
|
19
19
|
ec aid 身份/证书/名片/换头像(avatar --file/--url)/探测对端 $KITS_DOCS/evolcore/aid.md
|
|
20
20
|
ec fs AUN 文件系统统一入口(ls/stat/cat/cp/mv/rm/mkdir/ln/chmod/setfacl/getfacl/token/find/df/mount/approve/reject/umount;个人/群空间同一入口;没有 ec group fs) $KITS_DOCS/evolcore/fs.md
|
|
21
21
|
ec storage 文件上传/下载/配额(底层调试入口,日常优先 ec fs) $KITS_DOCS/evolcore/storage.md
|
|
22
|
-
ec rpc 底层 AUN RPC 逃生通道 $KITS_DOCS/evolcore/rpc.md
|
|
23
22
|
{{?peerId}}
|
|
24
23
|
ec msg 私聊收发消息(回复/拉取/撤回) $KITS_DOCS/evolcore/msg.md
|
|
25
24
|
{{/}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolcore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* launch-consolidation.ts — 拉起一次整理会话
|
|
3
|
-
*
|
|
4
|
-
* 两个入口共用这一处实现:
|
|
5
|
-
* - ipc.ts:手动 `ec memory consolidate --mode X`
|
|
6
|
-
* - index.ts:定时检查(五类候选)、compress / session-new 自动触发
|
|
7
|
-
*
|
|
8
|
-
* 流程:序列化上下文文件 → 写一次性触发器 → 交给 TriggerDefinitionManager。
|
|
9
|
-
* `execution.memoryMode` 会经 session.metadata 透传到渲染层与 ipc 的权限校验。
|
|
10
|
-
*/
|
|
11
|
-
import fs from 'fs';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { resolveRoot } from '../../paths.js';
|
|
14
|
-
import { serializeConsolidationContext } from './consolidation-context.js';
|
|
15
|
-
import { normalizeTriggerDefinition } from '../../trigger/validation.js';
|
|
16
|
-
import { formatChannelKey } from '../channel-loader.js';
|
|
17
|
-
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
|
-
import { logger } from '../../utils/logger.js';
|
|
19
|
-
/**
|
|
20
|
-
* 清理当前 agent 的旧整理批次。
|
|
21
|
-
* 启动新整理时调用,保留最新的一个会话历史用于查看和调试。
|
|
22
|
-
*/
|
|
23
|
-
function cleanupPreviousConsolidationBatches(selfAid) {
|
|
24
|
-
try {
|
|
25
|
-
const batchRoot = path.join(resolveRoot(), 'memory', 'consolidation-batches');
|
|
26
|
-
if (!fs.existsSync(batchRoot))
|
|
27
|
-
return;
|
|
28
|
-
// 匹配格式:YYYYMMDDTHHMMSS-<selfAid>-*
|
|
29
|
-
const escapedAid = selfAid.replace(/\./g, '\\.');
|
|
30
|
-
const pattern = new RegExp(`^\\d{8}T\\d{6}-${escapedAid}-`);
|
|
31
|
-
const entries = fs.readdirSync(batchRoot, { withFileTypes: true })
|
|
32
|
-
.filter(e => e.isDirectory() && pattern.test(e.name))
|
|
33
|
-
.map(e => ({ name: e.name, path: path.join(batchRoot, e.name) }))
|
|
34
|
-
.sort((a, b) => b.name.localeCompare(a.name)); // 最新的在前
|
|
35
|
-
// 保留最新的一个,删除其他
|
|
36
|
-
for (let i = 1; i < entries.length; i++) {
|
|
37
|
-
const dir = entries[i];
|
|
38
|
-
try {
|
|
39
|
-
fs.rmSync(dir.path, { recursive: true, force: true });
|
|
40
|
-
logger.debug(`[Memory] 清理前一次整理批次: ${dir.name}`);
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
logger.warn(`[Memory] 清理前一次整理批次失败 ${dir.name}: ${e?.message}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
48
|
-
logger.warn(`[Memory] 清理整理批次时出错: ${e?.message}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export function buildDebugTaskContext(input) {
|
|
52
|
-
const { session } = input;
|
|
53
|
-
const meta = (session.metadata ?? {});
|
|
54
|
-
const sessionType = session.sessionType ?? meta.sessionType ?? 'main';
|
|
55
|
-
let channel = session.channelType || session.channel;
|
|
56
|
-
let chatType = session.chatType || 'private';
|
|
57
|
-
let peerId = meta.peerId;
|
|
58
|
-
let peerKeyId = chatType === 'group'
|
|
59
|
-
? (meta.groupId || session.channelId)
|
|
60
|
-
: (meta.peerId || session.channelId);
|
|
61
|
-
let threadId = session.threadId || undefined;
|
|
62
|
-
if (session.channelId?.startsWith('trigger:') && session.selfAID && input.getTrigger) {
|
|
63
|
-
const triggerId = session.channelId.slice('trigger:'.length);
|
|
64
|
-
const origin = input.getTrigger(session.selfAID, triggerId)?.origin;
|
|
65
|
-
if (origin?.channelType) {
|
|
66
|
-
channel = origin.channelType;
|
|
67
|
-
chatType = origin.delivery?.chatType ?? 'private';
|
|
68
|
-
peerId = origin.peerId;
|
|
69
|
-
peerKeyId = chatType === 'group' ? origin.channelId : (origin.peerId ?? origin.channelId);
|
|
70
|
-
threadId = origin.threadId;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
sessionId: session.id,
|
|
75
|
-
baseagent: session.baseagent,
|
|
76
|
-
channel,
|
|
77
|
-
channelId: session.channelId,
|
|
78
|
-
chatType,
|
|
79
|
-
selfAid: session.selfAID,
|
|
80
|
-
peerId,
|
|
81
|
-
peerKey: channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined,
|
|
82
|
-
peerName: meta.peerName,
|
|
83
|
-
peerType: meta.peerType,
|
|
84
|
-
threadId,
|
|
85
|
-
sessionType,
|
|
86
|
-
memoryMode: meta.memoryMode,
|
|
87
|
-
memoryScope: meta.memoryScope,
|
|
88
|
-
consolidationBatchDir: meta.consolidationBatchDir ?? meta.consolidationLogBase,
|
|
89
|
-
sessionRuntimeDir: input.sessionRuntimeDir,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 序列化上下文并直接创建整理会话。
|
|
94
|
-
* 失败抛错,调用方负责决定是否记录诊断或回传错误。
|
|
95
|
-
*/
|
|
96
|
-
export async function launchConsolidation(channel, params) {
|
|
97
|
-
const { taskCtx, mode, triggeredBy, ownerAid } = params;
|
|
98
|
-
const effectiveCtx = params.peerKeyOverride
|
|
99
|
-
? { ...taskCtx, peerKey: params.peerKeyOverride }
|
|
100
|
-
: taskCtx;
|
|
101
|
-
const selfAid = effectiveCtx.selfAid;
|
|
102
|
-
if (!selfAid)
|
|
103
|
-
throw new Error('无法确定当前 agent AID(selfAid 为空)');
|
|
104
|
-
// 清理旧的整理批次(保留最新的一个历史)
|
|
105
|
-
cleanupPreviousConsolidationBatches(selfAid);
|
|
106
|
-
// notes 模式:pending/ → processing/ 快照迁移(在序列化上下文之前执行,声明认领)
|
|
107
|
-
let memoryDir;
|
|
108
|
-
if (mode === 'notes') {
|
|
109
|
-
memoryDir = effectiveCtx.peerKey
|
|
110
|
-
? path.join(resolveRoot(), 'agents', selfAid, 'relations', effectiveCtx.peerKey, 'memory')
|
|
111
|
-
: path.join(resolveRoot(), 'agents', selfAid, 'personal', 'memory');
|
|
112
|
-
const pendingDir = path.join(memoryDir, 'pending');
|
|
113
|
-
const processingDir = path.join(memoryDir, 'processing');
|
|
114
|
-
if (fs.existsSync(processingDir)) {
|
|
115
|
-
// 上一批还没收尾(会话仍在跑,或 daemon 中途重启丢了收尾),不覆盖、不合并
|
|
116
|
-
logger.warn(`[Memory] processing/ 已存在,跳过本次迁移(上一批未收尾): ${processingDir}`);
|
|
117
|
-
}
|
|
118
|
-
else if (fs.existsSync(pendingDir)) {
|
|
119
|
-
try {
|
|
120
|
-
const count = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md')).length;
|
|
121
|
-
fs.renameSync(pendingDir, processingDir);
|
|
122
|
-
// 立即重建空 pending 目录,供主会话后续写 note
|
|
123
|
-
fs.mkdirSync(pendingDir, { recursive: true });
|
|
124
|
-
logger.info(`[Memory] pending → processing 快照完成(${count} 条 note): ${processingDir}`);
|
|
125
|
-
}
|
|
126
|
-
catch (e) {
|
|
127
|
-
// 迁移失败不阻断整理会话启动,整理会话会发现 processing 目录空并输出 NOOP
|
|
128
|
-
logger.warn(`[Memory] pending → processing 迁移失败: ${e?.message}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const ctxFile = serializeConsolidationContext({
|
|
133
|
-
mode,
|
|
134
|
-
triggeredBy,
|
|
135
|
-
taskCtx: effectiveCtx,
|
|
136
|
-
ownerAid,
|
|
137
|
-
sessionRuntimeDir: effectiveCtx.sessionRuntimeDir,
|
|
138
|
-
ownerInvolved: params.ownerInvolved,
|
|
139
|
-
forgetAfterDays: params.forgetAfterDays,
|
|
140
|
-
scopeOverride: params.scopeOverride,
|
|
141
|
-
});
|
|
142
|
-
const baseagent = effectiveCtx.baseagent ?? 'claude';
|
|
143
|
-
const now = Date.now();
|
|
144
|
-
const scopeTag = params.scopeOverride ? `-${params.scopeOverride}` : '';
|
|
145
|
-
const triggerId = `memory-consolidate-${mode}${scopeTag}-${now.toString(36)}`;
|
|
146
|
-
// 整理会话的渠道信息(origin):发起时抓取,供反馈时直接投递到原会话渠道。
|
|
147
|
-
// 用 channelId/peerId/threadId 三要素——不依赖 evolcore sessionId,renew/`/new` 换会话也不影响送达。
|
|
148
|
-
const feedbackOrigin = effectiveCtx.channel && effectiveCtx.peerId ? {
|
|
149
|
-
channelKey: formatChannelKey({ type: effectiveCtx.channel, selfAID: selfAid, name: 'main' }),
|
|
150
|
-
channelId: effectiveCtx.peerId,
|
|
151
|
-
channelType: effectiveCtx.channel,
|
|
152
|
-
peerId: effectiveCtx.peerId,
|
|
153
|
-
session: effectiveCtx.threadId ? 'thread' : 'main',
|
|
154
|
-
...(effectiveCtx.threadId ? { threadId: effectiveCtx.threadId } : {}),
|
|
155
|
-
delivery: { chatType: effectiveCtx.chatType ?? 'private' },
|
|
156
|
-
} : undefined;
|
|
157
|
-
// 构造 TriggerDefinition 形状的数据载体。
|
|
158
|
-
// daemonChannel.converse() 从 ctx.trigger 读 execution.memoryMode / id / name / agentAid。
|
|
159
|
-
// 不再调 manager.create()——本函数直接创建会话,不进 trigger 系统的调度/持久化。
|
|
160
|
-
const triggerDef = normalizeTriggerDefinition({
|
|
161
|
-
$schema_version: 3.1,
|
|
162
|
-
id: triggerId,
|
|
163
|
-
agentAid: selfAid,
|
|
164
|
-
enabled: true,
|
|
165
|
-
name: triggerId,
|
|
166
|
-
description: `记忆整理(mode=${mode}${effectiveCtx.peerKey ? `,peerKey=${effectiveCtx.peerKey}` : ''})`,
|
|
167
|
-
createdAt: now,
|
|
168
|
-
updatedAt: now,
|
|
169
|
-
source: { type: 'once' },
|
|
170
|
-
execution: {
|
|
171
|
-
type: 'trigger_session',
|
|
172
|
-
prompt: [
|
|
173
|
-
'你正在执行记忆整理任务。',
|
|
174
|
-
`本次整理模式:${mode}`,
|
|
175
|
-
`整理上下文文件路径:${ctxFile.filePath}`,
|
|
176
|
-
'',
|
|
177
|
-
'请先读取上述上下文文件,再按照注入的整理规范完成本次整理。',
|
|
178
|
-
].join('\n'),
|
|
179
|
-
baseagent,
|
|
180
|
-
thread: 'by_run',
|
|
181
|
-
onError: 'retry',
|
|
182
|
-
noopSentinel: '[[NOOP]]',
|
|
183
|
-
memoryMode: mode,
|
|
184
|
-
...(params.scopeOverride ? { memoryScope: params.scopeOverride } : {}),
|
|
185
|
-
// 透传到 session.metadata,供 ipc.ts 写 ops 流水、收尾流程读取
|
|
186
|
-
consolidationBatchDir: ctxFile.batchDir,
|
|
187
|
-
...(ctxFile.chatDir ? { consolidationChatDir: ctxFile.chatDir } : {}),
|
|
188
|
-
...(memoryDir ? { consolidationMemoryDir: memoryDir } : {}),
|
|
189
|
-
},
|
|
190
|
-
// 不使用 trigger 的 feedback 机制;反馈由收尾流程直接调 adapter.send() 投递
|
|
191
|
-
feedback: { strategy: 'silent' },
|
|
192
|
-
...(feedbackOrigin ? { origin: feedbackOrigin } : {}),
|
|
193
|
-
reliability: {
|
|
194
|
-
concurrency: 'forbid',
|
|
195
|
-
missedPolicy: 'run_once',
|
|
196
|
-
retry: { maxAttempts: 0, backoffMs: 30_000 },
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
// normalizeTriggerDefinition 只保留白名单字段,自定义字段在其后补挂。
|
|
200
|
-
// 反馈目标(发起时渠道三元组)与触发方式,收尾流程据此投递 / 判断是否反馈。
|
|
201
|
-
if (feedbackOrigin) {
|
|
202
|
-
triggerDef.execution.feedbackOrigin = feedbackOrigin;
|
|
203
|
-
}
|
|
204
|
-
triggerDef.execution.consolidationTriggeredBy = triggeredBy;
|
|
205
|
-
// 直接创建整理会话并发送第一条消息——不再经 TriggerRuntimeScheduler。
|
|
206
|
-
// 用 getOrCreateConversationSession + converse,与旧 trigger_session 执行路径同源(见 daemon.ts)。
|
|
207
|
-
const projectPath = params.projectPath ?? path.join(resolveRoot(), 'agents', selfAid);
|
|
208
|
-
const runId = `${triggerId}-run`;
|
|
209
|
-
const session = await channel.getOrCreateConversationSession(triggerDef, projectPath, baseagent, runId);
|
|
210
|
-
const prompt = triggerDef.execution.prompt;
|
|
211
|
-
// 不 await:整理会话在后台跑,收尾由 task:completed/error/interrupted 事件驱动
|
|
212
|
-
void channel.converse(prompt, {
|
|
213
|
-
trigger: triggerDef,
|
|
214
|
-
runId,
|
|
215
|
-
firedAt: now,
|
|
216
|
-
projectPath,
|
|
217
|
-
baseagent,
|
|
218
|
-
session,
|
|
219
|
-
causation: undefined,
|
|
220
|
-
}).catch((e) => {
|
|
221
|
-
logger.warn(`[Memory] 整理会话执行失败(mode=${mode},session=${session.id}): ${e?.message}`);
|
|
222
|
-
});
|
|
223
|
-
logger.info(`[Memory] 已拉起整理会话(mode=${mode},triggeredBy=${triggeredBy},` +
|
|
224
|
-
`selfAid=${selfAid}${effectiveCtx.peerKey ? `,peerKey=${effectiveCtx.peerKey}` : ''},` +
|
|
225
|
-
`session=${session.id})`);
|
|
226
|
-
return {
|
|
227
|
-
triggerId,
|
|
228
|
-
contextFile: ctxFile.filePath,
|
|
229
|
-
contextFilePath: ctxFile.contextFilePath,
|
|
230
|
-
logFile: ctxFile.contextFilePath,
|
|
231
|
-
batchDir: ctxFile.batchDir,
|
|
232
|
-
executionSessionId: session.id,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* 手动 consolidate 用:在超时内轮询等 runner 分配 agentSessionId。
|
|
237
|
-
* 拿到立即返回;超时返回已拿到的部分,不抛错。
|
|
238
|
-
*
|
|
239
|
-
* 会话创建后 executionSessionId 立刻可用,但 agentSessionId 要等 runner 初始化
|
|
240
|
-
* 并开始第一轮才写入 session 记录,所以这里必须轮询等待。
|
|
241
|
-
*/
|
|
242
|
-
export async function waitForConsolidationSession(opts) {
|
|
243
|
-
const executionSessionId = opts.executionSessionId;
|
|
244
|
-
if (!executionSessionId)
|
|
245
|
-
return {};
|
|
246
|
-
const deadline = Date.now() + (opts.timeoutMs ?? 10_000);
|
|
247
|
-
const interval = opts.intervalMs ?? 250;
|
|
248
|
-
while (true) {
|
|
249
|
-
try {
|
|
250
|
-
const agentSessionId = await opts.readAgentSessionId(executionSessionId);
|
|
251
|
-
if (agentSessionId)
|
|
252
|
-
return { executionSessionId, agentSessionId };
|
|
253
|
-
}
|
|
254
|
-
catch { /* 会话记录写到一半,下一轮再读 */ }
|
|
255
|
-
if (Date.now() >= deadline)
|
|
256
|
-
return { executionSessionId };
|
|
257
|
-
await new Promise(r => setTimeout(r, interval));
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/** 把 entry 移进 destDir;重名时加时间戳后缀,不覆盖已有文件。 */
|
|
261
|
-
function moveIntoDir(srcPath, destDir, stamp) {
|
|
262
|
-
const name = path.basename(srcPath);
|
|
263
|
-
let dest = path.join(destDir, name);
|
|
264
|
-
if (fs.existsSync(dest)) {
|
|
265
|
-
if (fs.statSync(srcPath).isDirectory() && fs.statSync(dest).isDirectory()) {
|
|
266
|
-
// 目录(如 pending/archive/)逐个合并子项
|
|
267
|
-
for (const child of fs.readdirSync(srcPath))
|
|
268
|
-
moveIntoDir(path.join(srcPath, child), dest, stamp);
|
|
269
|
-
fs.rmdirSync(srcPath);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const ext = path.extname(name);
|
|
273
|
-
dest = path.join(destDir, `${path.basename(name, ext)}-${stamp}${ext}`);
|
|
274
|
-
}
|
|
275
|
-
fs.renameSync(srcPath, dest);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* 整理会话结束后的收尾,task:completed / task:error / task:interrupted 都走这里。
|
|
279
|
-
* - processing/ 里的 note:成功 → archive/notes/,失败 → 归还 pending/(下次重试)
|
|
280
|
-
* - processing/ 里的其他条目(如 pending/archive/)原样还回 pending/
|
|
281
|
-
* - 成功才写 checkpoint,失败时下次整理仍取同一段增量消息
|
|
282
|
-
* - ops 汇总无论成败都写
|
|
283
|
-
* 幂等:processing/ 不存在时只写汇总。
|
|
284
|
-
*/
|
|
285
|
-
export async function onConsolidationComplete(params) {
|
|
286
|
-
const now = params.now ?? Date.now();
|
|
287
|
-
const stamp = new Date(now).toISOString().replace(/[-:]/g, '').slice(0, 15);
|
|
288
|
-
const result = { moved: 0, checkpointWritten: false };
|
|
289
|
-
if (params.memoryDir) {
|
|
290
|
-
const processingDir = path.join(params.memoryDir, 'processing');
|
|
291
|
-
if (fs.existsSync(processingDir)) {
|
|
292
|
-
const pendingDir = path.join(params.memoryDir, 'pending');
|
|
293
|
-
const noteDest = params.success ? path.join(params.memoryDir, 'archive', 'notes') : pendingDir;
|
|
294
|
-
fs.mkdirSync(noteDest, { recursive: true });
|
|
295
|
-
fs.mkdirSync(pendingDir, { recursive: true });
|
|
296
|
-
for (const entry of fs.readdirSync(processingDir)) {
|
|
297
|
-
const src = path.join(processingDir, entry);
|
|
298
|
-
const isNote = entry.endsWith('.md') && fs.statSync(src).isFile();
|
|
299
|
-
moveIntoDir(src, isNote ? noteDest : pendingDir, stamp);
|
|
300
|
-
if (isNote)
|
|
301
|
-
result.moved++;
|
|
302
|
-
}
|
|
303
|
-
fs.rmdirSync(processingDir);
|
|
304
|
-
result.movedTo = noteDest;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (params.success && params.chatDir) {
|
|
308
|
-
const { writeCheckpoint } = await import('./consolidation-context.js');
|
|
309
|
-
writeCheckpoint(params.chatDir, now);
|
|
310
|
-
result.checkpointWritten = true;
|
|
311
|
-
}
|
|
312
|
-
const { summarizeOpsRecords, appendOpsRecord } = await import('./consolidation-ops-records.js');
|
|
313
|
-
appendOpsRecord(params.batchDir, summarizeOpsRecords(params.batchDir));
|
|
314
|
-
logger.info(`[Memory] 整理收尾(${params.success ? '成功' : '失败'}):归位 ${result.moved} 条 note` +
|
|
315
|
-
`${result.movedTo ? ` → ${result.movedTo}` : ''},checkpoint=${result.checkpointWritten}`);
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
318
|
-
/** 会话结束后把产出回填到批次任务书。 */
|
|
319
|
-
export function recordConsolidationOutcome(contextFilePath, outcome) {
|
|
320
|
-
try {
|
|
321
|
-
fs.appendFileSync(contextFilePath, `\n---\n\n## 本次产出\n\n${outcome}\n`, 'utf-8');
|
|
322
|
-
}
|
|
323
|
-
catch { /* ignore */ }
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* 整理会话失败/中断时,依据 ops.jsonl 逐条回撤成功的写操作。
|
|
327
|
-
* 回撤顺序:倒序(最后写的先撤)。
|
|
328
|
-
*
|
|
329
|
-
* create → 删除写入的文件,若有 archivedFiles 将其还原回 consolidated/
|
|
330
|
-
* update → 用 prevTags 重写文件(恢复修改前的标签,不改 updatedAt 以外的内容)
|
|
331
|
-
* archive → 将文件从 archivedTo 移回 sourceFile
|
|
332
|
-
*/
|
|
333
|
-
export async function rollbackConsolidation(batchDir, selfAid) {
|
|
334
|
-
const { readRollbackRecords, appendOpsRecord } = await import('./consolidation-ops-records.js');
|
|
335
|
-
const entries = readRollbackRecords(batchDir);
|
|
336
|
-
let rolledBack = 0;
|
|
337
|
-
const errors = [];
|
|
338
|
-
for (const entry of entries) {
|
|
339
|
-
try {
|
|
340
|
-
if (entry.op === 'create') {
|
|
341
|
-
// 删除写入的文件
|
|
342
|
-
if (entry.filePath && fs.existsSync(entry.filePath)) {
|
|
343
|
-
fs.unlinkSync(entry.filePath);
|
|
344
|
-
}
|
|
345
|
-
// 还原被归档的旧文件
|
|
346
|
-
for (const af of (entry.archivedFiles ?? [])) {
|
|
347
|
-
if (af.archivedTo && fs.existsSync(af.archivedTo)) {
|
|
348
|
-
const dest = af.source.includes('/') || af.source.includes('\\')
|
|
349
|
-
? af.source
|
|
350
|
-
: path.join(path.dirname(af.archivedTo), path.basename(af.archivedTo));
|
|
351
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
352
|
-
fs.renameSync(af.archivedTo, dest);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
rolledBack++;
|
|
356
|
-
}
|
|
357
|
-
else if (entry.op === 'update') {
|
|
358
|
-
// 恢复修改前的 tags:读文件、替换 tags 行、重写(不重签名,仅文本恢复)
|
|
359
|
-
const target = entry.target;
|
|
360
|
-
if (target && fs.existsSync(target)) {
|
|
361
|
-
const raw = fs.readFileSync(target, 'utf-8');
|
|
362
|
-
const prevTags = entry.prevTags ?? [];
|
|
363
|
-
const tagsLine = prevTags.length > 0 ? `tags: [${prevTags.join(', ')}]` : '';
|
|
364
|
-
// 替换 tags: [...] 行;若 prevTags 为空则移除该行
|
|
365
|
-
const updated = raw.replace(/^tags: \[.*\]$/m, tagsLine).replace(/^\n$/, '');
|
|
366
|
-
fs.writeFileSync(target, updated, 'utf-8');
|
|
367
|
-
rolledBack++;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
else if (entry.op === 'archive') {
|
|
371
|
-
// 将文件从 archivedTo 移回 sourceFile
|
|
372
|
-
if (entry.archivedTo && entry.sourceFile && fs.existsSync(entry.archivedTo)) {
|
|
373
|
-
fs.mkdirSync(path.dirname(entry.sourceFile), { recursive: true });
|
|
374
|
-
fs.renameSync(entry.archivedTo, entry.sourceFile);
|
|
375
|
-
rolledBack++;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
catch (e) {
|
|
380
|
-
const msg = `回撤 ${entry.op} "${entry.target}" 失败: ${e?.message}`;
|
|
381
|
-
errors.push(msg);
|
|
382
|
-
logger.warn(`[Memory] ${msg}`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
appendOpsRecord(batchDir, {
|
|
386
|
-
ts: new Date().toISOString(),
|
|
387
|
-
op: 'session-end',
|
|
388
|
-
success: false,
|
|
389
|
-
movedNotes: 0,
|
|
390
|
-
checkpointWritten: false,
|
|
391
|
-
});
|
|
392
|
-
logger.info(`[Memory] 回撤完成:回撤 ${rolledBack} 条操作,${errors.length} 个失败`);
|
|
393
|
-
return { rolledBack, errors };
|
|
394
|
-
}
|
|
395
|
-
export { resolveRoot };
|