evolcore 0.0.28 → 0.0.29-test.0
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -0,0 +1,395 @@
|
|
|
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 };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-admin.ts — `/memory` 斜杠命令的记忆查看与维护
|
|
3
|
+
*
|
|
4
|
+
* 只做文件层面的读写,权限判定由调用方(slash-handler)负责:
|
|
5
|
+
* - 查看:概览、pending note、MEMORY.md 索引、consolidated / archive 列表、单条正文
|
|
6
|
+
* - 维护:撤销 note、归档 / 恢复正式记忆、重建索引
|
|
7
|
+
*
|
|
8
|
+
* 整理(拉起整理会话)不在这里,由 daemon 注入的 launcher 完成。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { resolveScopeDirs, archiveMemory, dropMemoryNote, listMemoryNotes } from './memory-writer.js';
|
|
13
|
+
import { rebuildIndex, DEFAULT_INDEX_MAX_BYTES } from './index-manager.js';
|
|
14
|
+
import { refreshMemoryMeta, readMemoryMeta } from './index-manager.js';
|
|
15
|
+
import { parseSignatureBlock, verifyMemoryFile } from './memory-signature.js';
|
|
16
|
+
import { readFmArray, readFmValue, readUpdatedAtMs, reviewWindowDays, DEFAULT_FORGET_AFTER_DAYS } from './staleness.js';
|
|
17
|
+
export const MEMORY_SCOPES = ['relation', 'agent', 'owner'];
|
|
18
|
+
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,63}$/;
|
|
19
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
/** 单条正文回显上限,避免刷屏 */
|
|
21
|
+
const MAX_SHOW_CHARS = 4000;
|
|
22
|
+
function ctxOf(target) {
|
|
23
|
+
return { selfAid: target.selfAid, peerKey: target.peerKey };
|
|
24
|
+
}
|
|
25
|
+
export function scopeDirs(target, scope) {
|
|
26
|
+
return resolveScopeDirs(scope, ctxOf(target), target.ownerAid);
|
|
27
|
+
}
|
|
28
|
+
export function isMemoryScope(value) {
|
|
29
|
+
return !!value && MEMORY_SCOPES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
function assertSlug(slug) {
|
|
32
|
+
if (!SLUG_RE.test(slug))
|
|
33
|
+
throw new Error(`slug 格式非法:只允许字母、数字、连字符,长度 1-64,当前值: "${slug}"`);
|
|
34
|
+
}
|
|
35
|
+
function countMd(dir) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).length;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function fmtDate(ms) {
|
|
44
|
+
if (!ms)
|
|
45
|
+
return '-';
|
|
46
|
+
return new Date(ms).toISOString().slice(0, 16).replace('T', ' ');
|
|
47
|
+
}
|
|
48
|
+
/** 去掉签名块,只留正文(含 frontmatter) */
|
|
49
|
+
function stripSignature(content) {
|
|
50
|
+
return parseSignatureBlock(content).payload.trimEnd();
|
|
51
|
+
}
|
|
52
|
+
// ── 查看 ────────────────────────────────────────────────────────────────────
|
|
53
|
+
/** 概览:各作用域条数、pending 数、索引大小、最近更新时间。 */
|
|
54
|
+
export function memoryOverview(target, scopes) {
|
|
55
|
+
const mc = target.memoryConfig;
|
|
56
|
+
const indexMax = mc?.indexMaxBytes ?? DEFAULT_INDEX_MAX_BYTES;
|
|
57
|
+
const lines = ['🧠 记忆概览'];
|
|
58
|
+
if (mc?.enableRelation === false)
|
|
59
|
+
lines.push('⚠️ 记忆系统已停用(memory.enableRelation=false)');
|
|
60
|
+
for (const scope of scopes) {
|
|
61
|
+
let dirs;
|
|
62
|
+
try {
|
|
63
|
+
dirs = scopeDirs(target, scope);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
lines.push(`\n[${scope}] 不可用:${e?.message || String(e)}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const disabled = (scope === 'agent' && mc?.enableAgent === false) || (scope === 'owner' && mc?.enableOwner === false);
|
|
70
|
+
const meta = readMemoryMeta(dirs.memoryDir);
|
|
71
|
+
const consolidated = countMd(dirs.consolidatedDir);
|
|
72
|
+
const archived = countMd(path.join(dirs.memoryDir, 'archive', 'consolidated'));
|
|
73
|
+
const indexPath = path.join(dirs.memoryDir, 'MEMORY.md');
|
|
74
|
+
const indexBytes = fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
|
|
75
|
+
lines.push(`\n[${scope}]${disabled ? '(已停用)' : ''}`);
|
|
76
|
+
if (scope === 'relation') {
|
|
77
|
+
lines.push(` 关系: ${target.peerKey}`);
|
|
78
|
+
lines.push(` 待整理 note: ${countMd(path.join(dirs.memoryDir, 'pending'))}` +
|
|
79
|
+
(fs.existsSync(path.join(dirs.memoryDir, 'processing')) ? '(有一批正在整理)' : ''));
|
|
80
|
+
}
|
|
81
|
+
if (scope === 'owner')
|
|
82
|
+
lines.push(` owner: ${target.ownerAid}`);
|
|
83
|
+
lines.push(` 正式记忆: ${consolidated} 已归档: ${archived}`);
|
|
84
|
+
lines.push(` 索引: ${indexBytes}/${indexMax} 字节`);
|
|
85
|
+
if (meta) {
|
|
86
|
+
const flags = [meta.hasToGeneralize ? '待泛化' : '', meta.hasToPromote ? '待提升' : '', meta.profileSlug ? `画像=${meta.profileSlug}` : '']
|
|
87
|
+
.filter(Boolean).join(',');
|
|
88
|
+
if (flags)
|
|
89
|
+
lines.push(` 标记: ${flags}`);
|
|
90
|
+
lines.push(` 最老记忆: ${fmtDate(meta.oldestMtimeMs)} 元数据刷新: ${fmtDate(meta.updatedAt)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
/** consolidated/(或 archive/consolidated/)下的记忆列表。 */
|
|
96
|
+
export function listMemoryFiles(target, scope, archived = false, now = Date.now()) {
|
|
97
|
+
const dirs = scopeDirs(target, scope);
|
|
98
|
+
const dir = archived ? path.join(dirs.memoryDir, 'archive', 'consolidated') : dirs.consolidatedDir;
|
|
99
|
+
if (!fs.existsSync(dir))
|
|
100
|
+
return [];
|
|
101
|
+
const forgetAfterDays = target.memoryConfig?.forgetAfterDays ?? DEFAULT_FORGET_AFTER_DAYS;
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const f of fs.readdirSync(dir).filter(n => n.endsWith('.md')).sort()) {
|
|
104
|
+
const fp = path.join(dir, f);
|
|
105
|
+
let content;
|
|
106
|
+
let mtimeMs;
|
|
107
|
+
try {
|
|
108
|
+
content = fs.readFileSync(fp, 'utf-8');
|
|
109
|
+
mtimeMs = fs.statSync(fp).mtimeMs;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const tags = readFmArray(content, 'tags');
|
|
115
|
+
const updatedAtMs = readUpdatedAtMs(content) ?? mtimeMs;
|
|
116
|
+
const windowMs = reviewWindowDays(tags, { forgetAfterDays }) * DAY_MS;
|
|
117
|
+
out.push({
|
|
118
|
+
slug: f.replace(/\.md$/, ''),
|
|
119
|
+
description: readFmValue(content, 'description') ?? '',
|
|
120
|
+
tags,
|
|
121
|
+
mtimeMs,
|
|
122
|
+
updatedAtMs,
|
|
123
|
+
signed: !!parseSignatureBlock(content).fields,
|
|
124
|
+
reviewInDays: Math.floor((updatedAtMs + windowMs - now) / DAY_MS),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
out.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
export function formatMemoryFiles(scope, files, archived = false) {
|
|
131
|
+
const title = `📚 ${scope} ${archived ? '已归档记忆' : '正式记忆'}(${files.length})`;
|
|
132
|
+
if (files.length === 0)
|
|
133
|
+
return `${title}\n(暂无)`;
|
|
134
|
+
const lines = [title];
|
|
135
|
+
for (const f of files) {
|
|
136
|
+
const tags = f.tags.length ? ` [${f.tags.join(',')}]` : '';
|
|
137
|
+
const review = archived ? '' : f.reviewInDays < 0 ? ' ⏳待审查' : ` ${f.reviewInDays}天后审查`;
|
|
138
|
+
lines.push(`- ${f.slug}${tags}${f.signed ? '' : ' ⚠️未签名'}${review}`);
|
|
139
|
+
if (f.description)
|
|
140
|
+
lines.push(` ${f.description}`);
|
|
141
|
+
lines.push(` 更新于 ${fmtDate(f.updatedAtMs)}`);
|
|
142
|
+
}
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
/** MEMORY.md 原文。 */
|
|
146
|
+
export function readMemoryIndex(target, scope) {
|
|
147
|
+
const dirs = scopeDirs(target, scope);
|
|
148
|
+
const indexPath = path.join(dirs.memoryDir, 'MEMORY.md');
|
|
149
|
+
if (!fs.existsSync(indexPath))
|
|
150
|
+
return `📑 ${scope} 索引\n(尚未生成,可用 /memory reindex ${scope} 重建)`;
|
|
151
|
+
return `📑 ${scope} 索引(MEMORY.md)\n${fs.readFileSync(indexPath, 'utf-8').trimEnd()}`;
|
|
152
|
+
}
|
|
153
|
+
/** 单条正式记忆正文 + 验签结果。 */
|
|
154
|
+
export async function showMemoryFile(target, scope, slug, archived = false) {
|
|
155
|
+
assertSlug(slug);
|
|
156
|
+
const dirs = scopeDirs(target, scope);
|
|
157
|
+
const dir = archived ? path.join(dirs.memoryDir, 'archive', 'consolidated') : dirs.consolidatedDir;
|
|
158
|
+
const fp = path.join(dir, `${slug}.md`);
|
|
159
|
+
if (!fs.existsSync(fp))
|
|
160
|
+
throw new Error(`找不到记忆: ${scope}/${archived ? 'archive/' : ''}${slug}`);
|
|
161
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
162
|
+
let verify;
|
|
163
|
+
try {
|
|
164
|
+
const r = await verifyMemoryFile(content, target.selfAid);
|
|
165
|
+
verify = r.status === 'verified' ? '✅ 签名有效' : r.status === 'unsigned' ? '⚠️ 未签名' : `❌ 签名无效(${r.reason ?? 'unknown'})`;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
verify = `⚠️ 无法验签(${e?.message || String(e)})`;
|
|
169
|
+
}
|
|
170
|
+
let body = stripSignature(content);
|
|
171
|
+
if (body.length > MAX_SHOW_CHARS)
|
|
172
|
+
body = `${body.slice(0, MAX_SHOW_CHARS)}\n…(已截断)`;
|
|
173
|
+
return `📄 ${scope}/${archived ? 'archive/' : ''}${slug} ${verify}\n${body}`;
|
|
174
|
+
}
|
|
175
|
+
/** 当前关系的 pending note 列表。 */
|
|
176
|
+
export async function formatPendingNotes(target) {
|
|
177
|
+
const { notes } = await listMemoryNotes({ taskCtx: ctxOf(target), memoryConfig: target.memoryConfig });
|
|
178
|
+
if (notes.length === 0)
|
|
179
|
+
return '📝 待整理 note(0)\n(暂无)';
|
|
180
|
+
const lines = [`📝 待整理 note(${notes.length})`];
|
|
181
|
+
for (const n of notes) {
|
|
182
|
+
const tags = n.tags.length ? ` [${n.tags.join(',')}]` : '';
|
|
183
|
+
lines.push(`- ${n.slug}${tags} ${n.updatedAt ? fmtDate(Date.parse(n.updatedAt)) : ''}`);
|
|
184
|
+
if (n.firstLine)
|
|
185
|
+
lines.push(` ${n.firstLine}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|
|
189
|
+
/** 单条 pending note 原文。 */
|
|
190
|
+
export function showPendingNote(target, slug) {
|
|
191
|
+
assertSlug(slug);
|
|
192
|
+
const fp = path.join(scopeDirs(target, 'relation').memoryDir, 'pending', `${slug}.md`);
|
|
193
|
+
if (!fs.existsSync(fp))
|
|
194
|
+
throw new Error(`找不到 note: ${slug}`);
|
|
195
|
+
let body = fs.readFileSync(fp, 'utf-8').trimEnd();
|
|
196
|
+
if (body.length > MAX_SHOW_CHARS)
|
|
197
|
+
body = `${body.slice(0, MAX_SHOW_CHARS)}\n…(已截断)`;
|
|
198
|
+
return `📝 note/${slug}\n${body}`;
|
|
199
|
+
}
|
|
200
|
+
// ── 维护 ────────────────────────────────────────────────────────────────────
|
|
201
|
+
/** 重建某作用域的 MEMORY.md 并刷新 _meta.json。 */
|
|
202
|
+
export async function reindexScope(target, scope) {
|
|
203
|
+
const dirs = scopeDirs(target, scope);
|
|
204
|
+
const r = await rebuildIndex(dirs.consolidatedDir, path.join(dirs.memoryDir, 'MEMORY.md'), scope, path.join(dirs.memoryDir, 'archive', 'invalid'), undefined, // 与 ipc 一致:验签门控尚未开启
|
|
205
|
+
target.memoryConfig?.indexMaxBytes ?? DEFAULT_INDEX_MAX_BYTES);
|
|
206
|
+
refreshMemoryMeta(dirs.memoryDir);
|
|
207
|
+
return { indexed: r.indexed };
|
|
208
|
+
}
|
|
209
|
+
/** 撤销一条 pending note(移入 pending/archive/)。 */
|
|
210
|
+
export async function dropNote(target, slug) {
|
|
211
|
+
assertSlug(slug);
|
|
212
|
+
const r = await dropMemoryNote({ taskCtx: ctxOf(target), target: slug, memoryConfig: target.memoryConfig });
|
|
213
|
+
return r.filePath;
|
|
214
|
+
}
|
|
215
|
+
/** 归档一条正式记忆并重建索引。 */
|
|
216
|
+
export async function archiveMemoryFile(target, scope, slug) {
|
|
217
|
+
assertSlug(slug);
|
|
218
|
+
await archiveMemory({ taskCtx: ctxOf(target), scope, ownerAid: target.ownerAid, targets: [slug] });
|
|
219
|
+
await reindexScope(target, scope);
|
|
220
|
+
}
|
|
221
|
+
/** 把已归档记忆恢复回 consolidated/ 并重建索引。同名已存在时拒绝,不覆盖。 */
|
|
222
|
+
export async function restoreMemoryFile(target, scope, slug) {
|
|
223
|
+
assertSlug(slug);
|
|
224
|
+
const dirs = scopeDirs(target, scope);
|
|
225
|
+
const src = path.join(dirs.memoryDir, 'archive', 'consolidated', `${slug}.md`);
|
|
226
|
+
const dest = path.join(dirs.consolidatedDir, `${slug}.md`);
|
|
227
|
+
if (!fs.existsSync(src))
|
|
228
|
+
throw new Error(`找不到已归档记忆: ${scope}/${slug}`);
|
|
229
|
+
if (fs.existsSync(dest))
|
|
230
|
+
throw new Error(`正式记忆中已存在同名文件: ${scope}/${slug},未恢复`);
|
|
231
|
+
fs.mkdirSync(dirs.consolidatedDir, { recursive: true });
|
|
232
|
+
fs.renameSync(src, dest);
|
|
233
|
+
await reindexScope(target, scope);
|
|
234
|
+
}
|