evolcore 0.0.28 → 0.0.29
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
|
@@ -1,190 +1,356 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// 在下一条对端消息到达、渲染该轮 prompt 时一次性消费、注入渲染层。
|
|
5
|
-
// 详见 docs/observer-insert-design.md 第一部分(§1.3 文件生命周期 / §1.4 thread 作用域)。
|
|
6
|
-
//
|
|
7
|
-
// 文件:agents/<selfAID>/sessions/<channelSessionKey>/pending-hints.jsonl(与 messages.jsonl 同级)
|
|
8
|
-
// 每行一个事件:
|
|
9
|
-
// { action:'add', id, text, threadId, ownerAid, ts }
|
|
10
|
-
// { action:'remove', targetId?, threadId, ts } // 无 targetId = 撤该 thread 全部
|
|
11
|
-
//
|
|
12
|
-
// 生命周期 = 一个消费周期(按 (对端,thread) 维度):
|
|
13
|
-
// - add/remove 追加行(append-only,处理「加了又撤」的竞态:按时间序回放抵消)
|
|
14
|
-
// - consume(对端消息到达触发):回放算「该 thread」有效集 → 返回 → 清掉该 thread 的事件。
|
|
15
|
-
// · 若文件里其它 thread 仍有未消费提示 → 重写文件只留它们(不误删别的 thread)。
|
|
16
|
-
// · 否则(无其它 thread 残留)→ 删整个文件。
|
|
17
|
-
// - 因为 consume 一次性用掉该 thread 的全部有效提示,消费后该 thread 有效集必然归零,
|
|
18
|
-
// 未来状态不依赖消费前历史,故消费即清安全,无需 consume 事件行。
|
|
19
|
-
// - 任何让「整文件」有效集归零的操作都删文件:consume 后无残留即删;remove 把提示全撤光也删。
|
|
1
|
+
// Observer insert hint storage and protocol parsing.
|
|
2
|
+
// Durable events remain pending until a successful claim is committed. Claims
|
|
3
|
+
// are process-local so a crash leaves the durable hint available for retry.
|
|
20
4
|
import fs from 'fs';
|
|
21
5
|
import path from 'path';
|
|
22
|
-
import
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { chatDirPath, appendJsonl, readAllJsonlLines, scanChatDirs } from '../session/session-fs-store.js';
|
|
23
8
|
import { logger } from '../../utils/logger.js';
|
|
24
9
|
const PENDING_HINTS_FILE = 'pending-hints.jsonl';
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
export const OBSERVER_HINT_FAILURE_MESSAGES = {
|
|
11
|
+
SESSION_NOT_FOUND: '目标 Agent session 不存在',
|
|
12
|
+
INTERRUPT_FAILED: 'Agent 当前任务中断失败',
|
|
13
|
+
INTERRUPT_TIMEOUT: '等待 Agent 当前任务中断超时',
|
|
14
|
+
QUEUE_FAILED: 'Agent 任务队列不可用',
|
|
15
|
+
QUEUE_PERSIST_FAILED: '提示入队失败',
|
|
16
|
+
RENDER_FAILED: '提示内容组装失败',
|
|
17
|
+
RUNNER_START_FAILED: 'Agent runner 启动失败',
|
|
18
|
+
RUNNER_START_SUPERSEDED: '提示启动时已被新的任务取代',
|
|
19
|
+
HINT_UNAVAILABLE: '提示已不可用',
|
|
20
|
+
};
|
|
21
|
+
const targetLocks = new Map();
|
|
22
|
+
const claimedHints = new Map();
|
|
23
|
+
let claimSequence = 0;
|
|
24
|
+
function hintsPath(sessionsDir, channelType, channelId, selfAID, channelKey) {
|
|
25
|
+
return path.join(chatDirPath(sessionsDir, channelType, channelId, selfAID, channelKey), PENDING_HINTS_FILE);
|
|
27
26
|
}
|
|
28
|
-
/** 归一 threadId:undefined/null → ''(主线程)。 */
|
|
29
27
|
function normThread(threadId) {
|
|
30
28
|
return threadId || '';
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
function normChatType(chatType) {
|
|
31
|
+
return chatType === 'group' ? 'group' : 'private';
|
|
32
|
+
}
|
|
33
|
+
function eventChatType(event) {
|
|
34
|
+
return normChatType(event.chatType);
|
|
35
|
+
}
|
|
36
|
+
function eventMode(event) {
|
|
37
|
+
return event.mode === 'interrupt' ? 'interrupt' : 'next';
|
|
38
|
+
}
|
|
39
|
+
function targetKey(sessionsDir, channelType, channelId, selfAID, chatType, threadId, channelKey) {
|
|
40
|
+
return [sessionsDir, channelType, channelId, selfAID, channelKey || '', chatType, normThread(threadId)].join('\u0000');
|
|
41
|
+
}
|
|
42
|
+
/** Serialize all operations for one target conversation/thread. */
|
|
43
|
+
export async function withHintTargetLock(sessionsDir, channelType, channelId, selfAID, chatType, threadId, fn, channelKey) {
|
|
44
|
+
const key = targetKey(sessionsDir, channelType, channelId, selfAID, chatType, threadId, channelKey);
|
|
45
|
+
const previous = targetLocks.get(key) ?? Promise.resolve();
|
|
46
|
+
let release;
|
|
47
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
48
|
+
targetLocks.set(key, current);
|
|
49
|
+
await previous.catch(() => undefined);
|
|
37
50
|
try {
|
|
38
|
-
|
|
39
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
40
|
-
const ev = {
|
|
41
|
-
action: 'add', id: hint.id, text: hint.text,
|
|
42
|
-
threadId: normThread(hint.threadId), ownerAid: hint.ownerAid, ts: hint.ts,
|
|
43
|
-
};
|
|
44
|
-
appendJsonl(fp, ev);
|
|
45
|
-
return true;
|
|
51
|
+
return await fn();
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
finally {
|
|
54
|
+
release();
|
|
55
|
+
if (targetLocks.get(key) === current)
|
|
56
|
+
targetLocks.delete(key);
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
|
-
|
|
53
|
-
* 追加一条 remove 事件。若该操作使 (对端,thread) 有效集归零,则删除整个文件。
|
|
54
|
-
* 返回是否写盘成功(供 ack 判定)。
|
|
55
|
-
*/
|
|
56
|
-
export function appendHintRemove(sessionsDir, channelType, channelId, selfAID, rm) {
|
|
59
|
+
function readEvents(fp) {
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
if (!fs.existsSync(fp))
|
|
60
|
-
return true; // 无文件 = 无可撤,幂等成功
|
|
61
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
62
|
-
const ev = {
|
|
63
|
-
action: 'remove', threadId: normThread(rm.threadId), ts: rm.ts,
|
|
64
|
-
...(rm.targetId ? { targetId: rm.targetId } : {}),
|
|
65
|
-
};
|
|
66
|
-
appendJsonl(fp, ev);
|
|
67
|
-
// 撤销后:若全部 thread 的有效集都归零,删文件(不堆死文件)。
|
|
68
|
-
if (!hasAnyEffective(fp))
|
|
69
|
-
deleteFile(fp);
|
|
70
|
-
return true;
|
|
61
|
+
return fs.existsSync(fp) ? readAllJsonlLines(fp) : [];
|
|
71
62
|
}
|
|
72
|
-
catch (
|
|
73
|
-
logger.warn(`[PendingHints]
|
|
74
|
-
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.warn(`[PendingHints] read failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
throw error;
|
|
75
66
|
}
|
|
76
67
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const events = readAllJsonlLines(fp);
|
|
80
|
-
// 按 thread 分组回放
|
|
81
|
-
const byThread = new Map();
|
|
68
|
+
function replayEffective(events) {
|
|
69
|
+
const active = new Map();
|
|
82
70
|
for (const ev of events) {
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
if (ev.action === 'add') {
|
|
72
|
+
active.set(ev.id, {
|
|
73
|
+
id: ev.id,
|
|
74
|
+
text: ev.text,
|
|
75
|
+
ownerAid: ev.ownerAid,
|
|
76
|
+
ts: ev.ts,
|
|
77
|
+
mode: eventMode(ev),
|
|
78
|
+
chatType: eventChatType(ev),
|
|
79
|
+
threadId: normThread(ev.threadId),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (ev.action === 'promote') {
|
|
83
|
+
const current = active.get(ev.targetId);
|
|
84
|
+
if (current)
|
|
85
|
+
active.set(ev.targetId, {
|
|
86
|
+
...current,
|
|
87
|
+
mode: 'interrupt',
|
|
88
|
+
...(ev.requestId ? { promoteRequestId: ev.requestId } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (ev.targetId) {
|
|
92
|
+
active.delete(ev.targetId);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
active.clear();
|
|
96
|
+
}
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
return [...active.values()].sort((a, b) => a.ts - b.ts);
|
|
99
|
+
}
|
|
100
|
+
function effectiveFor(events, chatType, threadId) {
|
|
101
|
+
return replayEffective(events.filter(ev => eventChatType(ev) === chatType && normThread(ev.threadId) === threadId));
|
|
102
|
+
}
|
|
103
|
+
function hasAnyEffective(events) {
|
|
104
|
+
const keys = new Set(events.map(ev => `${eventChatType(ev)}\u0000${normThread(ev.threadId)}`));
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
const [chatType, threadId] = key.split('\u0000');
|
|
107
|
+
if (effectiveFor(events, chatType, threadId).length > 0)
|
|
88
108
|
return true;
|
|
89
109
|
}
|
|
90
110
|
return false;
|
|
91
111
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
function deleteFile(fp) {
|
|
113
|
+
try {
|
|
114
|
+
if (fs.existsSync(fp))
|
|
115
|
+
fs.unlinkSync(fp);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.warn(`[PendingHints] delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function claimKey(claim) {
|
|
122
|
+
return targetKey(claim.sessionsDir, claim.channelType, claim.channelId, claim.selfAID, claim.chatType, claim.threadId, claim.channelKey);
|
|
123
|
+
}
|
|
124
|
+
export function pendingInterruptHints(sessionsDir, selfAID) {
|
|
125
|
+
const pending = [];
|
|
126
|
+
for (const chat of scanChatDirs(sessionsDir)) {
|
|
127
|
+
if (chat.selfAID !== selfAID || chat.channelType !== 'aun' || !chat.channelKey)
|
|
128
|
+
continue;
|
|
129
|
+
let events;
|
|
130
|
+
try {
|
|
131
|
+
events = readEvents(path.join(chat.dirPath, PENDING_HINTS_FILE));
|
|
99
132
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const keys = new Set(events.filter(event => event.action === 'add')
|
|
137
|
+
.map(event => `${eventChatType(event)}\u0000${normThread(event.threadId)}`));
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
const [chatType, threadId] = key.split('\u0000');
|
|
140
|
+
for (const hint of effectiveFor(events, chatType, threadId)) {
|
|
141
|
+
if (hint.mode !== 'interrupt')
|
|
142
|
+
continue;
|
|
143
|
+
pending.push({ channelKey: chat.channelKey, channelId: chat.channelId, chatType,
|
|
144
|
+
threadId: threadId || undefined, hintId: hint.id, requestId: hint.promoteRequestId });
|
|
145
|
+
}
|
|
105
146
|
}
|
|
106
147
|
}
|
|
107
|
-
return
|
|
148
|
+
return pending;
|
|
108
149
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* 重写文件保留它们;只有当整文件再无任何有效提示时才删除文件。
|
|
114
|
-
* 返回空数组表示该 thread 无有效提示(调用方据此决定是否注入)。
|
|
115
|
-
*/
|
|
116
|
-
export function consumeHints(sessionsDir, channelType, channelId, selfAID, threadId) {
|
|
117
|
-
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
118
|
-
if (!fs.existsSync(fp))
|
|
119
|
-
return [];
|
|
120
|
-
let all;
|
|
150
|
+
function idExistsElsewhere(fp, id) {
|
|
151
|
+
const parent = path.dirname(fp);
|
|
152
|
+
const sessionsRoot = path.dirname(parent);
|
|
153
|
+
let entries;
|
|
121
154
|
try {
|
|
122
|
-
|
|
155
|
+
entries = fs.readdirSync(sessionsRoot, { withFileTypes: true });
|
|
123
156
|
}
|
|
124
|
-
catch
|
|
125
|
-
|
|
126
|
-
return [];
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
127
159
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
return entries.some(entry => entry.isDirectory() && path.join(sessionsRoot, entry.name) !== parent
|
|
161
|
+
&& readEvents(path.join(sessionsRoot, entry.name, PENDING_HINTS_FILE))
|
|
162
|
+
.some(event => event.action === 'add' && event.id === id));
|
|
163
|
+
}
|
|
164
|
+
function isClaimed(claim, id) {
|
|
165
|
+
return claimedHints.get(claimKey(claim))?.has(id) ?? false;
|
|
166
|
+
}
|
|
167
|
+
export function appendHintAddDetailed(sessionsDir, channelType, channelId, selfAID, hint, channelKey) {
|
|
168
|
+
try {
|
|
169
|
+
const chatType = normChatType(hint.chatType);
|
|
170
|
+
const threadId = normThread(hint.threadId);
|
|
171
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID, channelKey);
|
|
172
|
+
const events = readEvents(fp);
|
|
173
|
+
const original = events.find((event) => event.action === 'add' && event.id === hint.id);
|
|
174
|
+
if (original) {
|
|
175
|
+
const same = original.text === hint.text && original.ownerAid === hint.ownerAid
|
|
176
|
+
&& eventChatType(original) === chatType && normThread(original.threadId) === threadId
|
|
177
|
+
&& eventMode(original) === (hint.mode ?? 'next');
|
|
178
|
+
if (!same)
|
|
179
|
+
return { ok: false, id: hint.id, code: 'CONFLICT' };
|
|
180
|
+
const terminal = events.filter(event => event.action !== 'add'
|
|
181
|
+
&& event.action !== 'promote' && event.targetId === hint.id
|
|
182
|
+
&& eventChatType(event) === chatType && normThread(event.threadId) === threadId).at(-1);
|
|
183
|
+
return { ok: true, id: hint.id, duplicate: true,
|
|
184
|
+
state: terminal?.action === 'consumed' ? 'consumed' : terminal ? 'removed' : 'pending' };
|
|
185
|
+
}
|
|
186
|
+
if (idExistsElsewhere(fp, hint.id))
|
|
187
|
+
return { ok: false, id: hint.id, code: 'CONFLICT' };
|
|
188
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
189
|
+
appendJsonl(fp, {
|
|
190
|
+
action: 'add',
|
|
191
|
+
id: hint.id,
|
|
192
|
+
text: hint.text,
|
|
193
|
+
mode: hint.mode === 'interrupt' ? 'interrupt' : 'next',
|
|
194
|
+
chatType,
|
|
195
|
+
threadId,
|
|
196
|
+
ownerAid: hint.ownerAid,
|
|
197
|
+
ts: hint.ts,
|
|
198
|
+
});
|
|
199
|
+
return { ok: true, id: hint.id, state: 'pending' };
|
|
135
200
|
}
|
|
136
|
-
|
|
137
|
-
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger.warn(`[PendingHints] append add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
203
|
+
return { ok: false };
|
|
138
204
|
}
|
|
139
|
-
return effective;
|
|
140
205
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
206
|
+
export function appendHintAdd(sessionsDir, channelType, channelId, selfAID, hint) {
|
|
207
|
+
try {
|
|
208
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
209
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
210
|
+
appendJsonl(fp, {
|
|
211
|
+
action: 'add', id: hint.id, text: hint.text,
|
|
212
|
+
...(hint.mode ? { mode: hint.mode } : {}),
|
|
213
|
+
...(hint.chatType ? { chatType: hint.chatType } : {}),
|
|
214
|
+
threadId: normThread(hint.threadId), ownerAid: hint.ownerAid, ts: hint.ts,
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
147
217
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.warn(`[PendingHints] append add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
return false;
|
|
151
221
|
}
|
|
152
|
-
return false;
|
|
153
222
|
}
|
|
154
|
-
function
|
|
223
|
+
export function appendHintRemoveDetailed(sessionsDir, channelType, channelId, selfAID, rm, channelKey) {
|
|
155
224
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
225
|
+
const chatType = normChatType(rm.chatType);
|
|
226
|
+
const threadId = normThread(rm.threadId);
|
|
227
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID, channelKey);
|
|
228
|
+
if (!fs.existsSync(fp))
|
|
229
|
+
return { ok: true, removedIds: [], removedHints: [] };
|
|
230
|
+
const all = readEvents(fp);
|
|
231
|
+
const effective = effectiveFor(all, chatType, threadId);
|
|
232
|
+
const targets = rm.targetId ? effective.filter(item => item.id === rm.targetId) : effective;
|
|
233
|
+
const claim = { sessionsDir, channelType, channelId, channelKey, selfAID, chatType, threadId };
|
|
234
|
+
if (targets.some(item => isClaimed(claim, item.id)))
|
|
235
|
+
return { ok: false, id: rm.targetId, code: 'IN_PROGRESS' };
|
|
236
|
+
if (targets.length === 0) {
|
|
237
|
+
const consumed = rm.targetId && all.some(event => event.action === 'consumed'
|
|
238
|
+
&& event.targetId === rm.targetId && eventChatType(event) === chatType && normThread(event.threadId) === threadId);
|
|
239
|
+
return consumed ? { ok: false, id: rm.targetId, code: 'ALREADY_CONSUMED' }
|
|
240
|
+
: { ok: true, removedIds: [], removedHints: [] };
|
|
241
|
+
}
|
|
242
|
+
fs.appendFileSync(fp, targets.map(item => JSON.stringify({
|
|
243
|
+
action: 'remove', targetId: item.id, chatType, threadId, ts: rm.ts,
|
|
244
|
+
})).join('\n') + '\n');
|
|
245
|
+
return { ok: true, removedIds: targets.map(item => item.id), removedHints: targets };
|
|
158
246
|
}
|
|
159
|
-
catch (
|
|
160
|
-
logger.warn(`[PendingHints]
|
|
247
|
+
catch (error) {
|
|
248
|
+
logger.warn(`[PendingHints] append remove failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
249
|
+
return { ok: false };
|
|
161
250
|
}
|
|
162
251
|
}
|
|
163
|
-
function
|
|
252
|
+
export function appendHintRemove(sessionsDir, channelType, channelId, selfAID, rm) {
|
|
253
|
+
const result = appendHintRemoveDetailed(sessionsDir, channelType, channelId, selfAID, rm);
|
|
254
|
+
if (result.ok) {
|
|
255
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
256
|
+
if (fs.existsSync(fp) && !hasAnyEffective(readEvents(fp)))
|
|
257
|
+
deleteFile(fp);
|
|
258
|
+
}
|
|
259
|
+
return result.ok;
|
|
260
|
+
}
|
|
261
|
+
export function appendHintPromoteDetailed(sessionsDir, channelType, channelId, selfAID, promote, channelKey) {
|
|
164
262
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
263
|
+
const chatType = normChatType(promote.chatType);
|
|
264
|
+
const threadId = normThread(promote.threadId);
|
|
265
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID, channelKey);
|
|
266
|
+
const all = readEvents(fp);
|
|
267
|
+
const existing = effectiveFor(all, chatType, threadId).find(item => item.id === promote.targetId);
|
|
268
|
+
if (!existing)
|
|
269
|
+
return { ok: false, id: promote.targetId, code: all.some(event => event.action === 'consumed'
|
|
270
|
+
&& event.targetId === promote.targetId && eventChatType(event) === chatType && normThread(event.threadId) === threadId)
|
|
271
|
+
? 'ALREADY_CONSUMED' : 'NOT_FOUND' };
|
|
272
|
+
if ((existing.mode ?? 'next') === 'interrupt') {
|
|
273
|
+
appendJsonl(fp, {
|
|
274
|
+
action: 'promote', targetId: promote.targetId, chatType, threadId,
|
|
275
|
+
...(promote.requestId ? { requestId: promote.requestId } : {}), ts: promote.ts,
|
|
276
|
+
});
|
|
277
|
+
return { ok: true, id: promote.targetId, duplicate: true, state: 'pending' };
|
|
278
|
+
}
|
|
279
|
+
appendJsonl(fp, {
|
|
280
|
+
action: 'promote', targetId: promote.targetId, chatType, threadId,
|
|
281
|
+
...(promote.requestId ? { requestId: promote.requestId } : {}), ts: promote.ts,
|
|
282
|
+
});
|
|
283
|
+
return { ok: true, id: promote.targetId, state: 'pending' };
|
|
167
284
|
}
|
|
168
|
-
catch (
|
|
169
|
-
logger.warn(`[PendingHints]
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger.warn(`[PendingHints] append promote failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
return { ok: false };
|
|
170
288
|
}
|
|
171
289
|
}
|
|
172
|
-
/**
|
|
173
|
-
export function
|
|
290
|
+
/** Backward-compatible eager consumption API. New flows should use claim/commit. */
|
|
291
|
+
export function consumeHints(sessionsDir, channelType, channelId, selfAID, threadId, chatType) {
|
|
174
292
|
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
175
293
|
if (!fs.existsSync(fp))
|
|
176
294
|
return [];
|
|
295
|
+
const all = readEvents(fp);
|
|
296
|
+
const normalizedChat = normChatType(chatType);
|
|
297
|
+
const normalizedThread = normThread(threadId);
|
|
298
|
+
const effective = effectiveFor(all, normalizedChat, normalizedThread);
|
|
299
|
+
const rest = all.filter(ev => eventChatType(ev) !== normalizedChat || normThread(ev.threadId) !== normalizedThread);
|
|
300
|
+
if (hasAnyEffective(rest)) {
|
|
301
|
+
const body = rest.map(event => JSON.stringify(event)).join('\n');
|
|
302
|
+
fs.writeFileSync(fp, `${body}${body ? '\n' : ''}`);
|
|
303
|
+
}
|
|
304
|
+
else
|
|
305
|
+
deleteFile(fp);
|
|
306
|
+
return effective;
|
|
307
|
+
}
|
|
308
|
+
export function peekHints(sessionsDir, channelType, channelId, selfAID, threadId, chatType, mode, channelKey) {
|
|
309
|
+
const effective = effectiveFor(readEvents(hintsPath(sessionsDir, channelType, channelId, selfAID, channelKey)), normChatType(chatType), normThread(threadId));
|
|
310
|
+
return mode ? effective.filter(item => (item.mode ?? 'next') === mode) : effective;
|
|
311
|
+
}
|
|
312
|
+
export function claimHints(sessionsDir, channelType, channelId, selfAID, threadId, chatType, mode = 'next', targetId, channelKey) {
|
|
313
|
+
const claim = {
|
|
314
|
+
token: '', hints: [], sessionsDir, channelType, channelId, selfAID, channelKey,
|
|
315
|
+
chatType: normChatType(chatType), threadId: normThread(threadId),
|
|
316
|
+
};
|
|
317
|
+
const effective = peekHints(sessionsDir, channelType, channelId, selfAID, threadId, chatType, mode, channelKey)
|
|
318
|
+
.filter(item => (!targetId || item.id === targetId) && !isClaimed(claim, item.id));
|
|
319
|
+
if (effective.length === 0)
|
|
320
|
+
return claim;
|
|
321
|
+
claim.token = `claim-${++claimSequence}`;
|
|
322
|
+
const ids = claimedHints.get(claimKey(claim)) ?? new Set();
|
|
323
|
+
effective.forEach(item => ids.add(item.id));
|
|
324
|
+
claimedHints.set(claimKey(claim), ids);
|
|
325
|
+
claim.hints = effective;
|
|
326
|
+
return claim;
|
|
327
|
+
}
|
|
328
|
+
export function releaseHintClaim(claim) {
|
|
329
|
+
if (!claim.token)
|
|
330
|
+
return;
|
|
331
|
+
const ids = claimedHints.get(claimKey(claim));
|
|
332
|
+
claim.hints.forEach(item => ids?.delete(item.id));
|
|
333
|
+
if (ids?.size === 0)
|
|
334
|
+
claimedHints.delete(claimKey(claim));
|
|
335
|
+
}
|
|
336
|
+
export function commitHintClaim(claim, ts = Date.now()) {
|
|
337
|
+
if (!claim.token || claim.hints.length === 0)
|
|
338
|
+
return true;
|
|
339
|
+
const fp = hintsPath(claim.sessionsDir, claim.channelType, claim.channelId, claim.selfAID, claim.channelKey);
|
|
177
340
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
341
|
+
if (!fs.existsSync(fp))
|
|
342
|
+
return false;
|
|
343
|
+
fs.appendFileSync(fp, claim.hints.map(hint => JSON.stringify({
|
|
344
|
+
action: 'consumed', targetId: hint.id, chatType: claim.chatType, threadId: claim.threadId, ts,
|
|
345
|
+
})).join('\n') + '\n');
|
|
346
|
+
releaseHintClaim(claim);
|
|
347
|
+
return true;
|
|
181
348
|
}
|
|
182
|
-
catch {
|
|
183
|
-
|
|
349
|
+
catch (error) {
|
|
350
|
+
logger.warn(`[PendingHints] commit failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
351
|
+
return false;
|
|
184
352
|
}
|
|
185
353
|
}
|
|
186
|
-
// ── 渲染接线纯函数(无 IO,便于单测) ──────────────────────────
|
|
187
|
-
/** 把有效提示转成 owner-hint SubMessage(排在对端真实 item 之前,走 inject 渲染模式)。 */
|
|
188
354
|
export function hintsToSubMessages(hints) {
|
|
189
355
|
return hints.map(h => ({
|
|
190
356
|
kind: 'owner-hint',
|
|
@@ -194,39 +360,41 @@ export function hintsToSubMessages(hints) {
|
|
|
194
360
|
timestamp: h.ts,
|
|
195
361
|
}));
|
|
196
362
|
}
|
|
197
|
-
/**
|
|
198
|
-
* 渲染失败兜底:把已消费的 owner-hint(提示已从 pending 删除、不可恢复)以纯文本前缀
|
|
199
|
-
* 拼到对端原文之前,避免提示被静默丢弃。仅在 renderMessageBody 抛错或产出空时使用。
|
|
200
|
-
*/
|
|
201
363
|
export function composeHintFallback(hintItems, content) {
|
|
202
364
|
if (!hintItems || hintItems.length === 0)
|
|
203
365
|
return content;
|
|
204
366
|
const lines = hintItems.map(h => `‹owner 提示·已验证›(仅你可见,对端无感)\n${h.content}`);
|
|
205
367
|
return [...lines, content].join('\n\n');
|
|
206
368
|
}
|
|
207
|
-
/**
|
|
208
|
-
* 解析 observer.inject payload:鉴权(from∈owners)+ 校验(add 需 channel_id+text;remove 需 channel_id)
|
|
209
|
-
* + 归一字段。纯函数,无副作用——落盘 / ack / watch 由调用方按结果执行。
|
|
210
|
-
* @param ts 用于 add 缺省 id(`inj-<ts>`);由调用方传入便于测试确定性。
|
|
211
|
-
*/
|
|
212
369
|
export function parseInjectRequest(payload, fromAid, owners, ts) {
|
|
213
|
-
const p =
|
|
370
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
214
371
|
const injectId = typeof p.id === 'string' ? p.id : undefined;
|
|
215
|
-
const action = p.action === 'remove'
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const target = (p.target && typeof p.target === 'object') ? p.target : undefined;
|
|
220
|
-
const channelId = target && typeof target.channel_id === 'string' ? target.channel_id : undefined;
|
|
221
|
-
const text = typeof p.text === 'string' ? p.text : '';
|
|
222
|
-
if (!channelId || (action === 'add' && !text.trim())) {
|
|
223
|
-
return { kind: 'reject', code: 'INVALID_TARGET', message: 'target.channel_id 必填;add 还需 text', action, injectId };
|
|
224
|
-
}
|
|
225
|
-
const chatType = target?.chat_type === 'group' ? 'group' : 'private';
|
|
372
|
+
const action = p.action === 'remove' || p.action === 'promote' ? p.action : 'add';
|
|
373
|
+
const target = p.target && typeof p.target === 'object' ? p.target : undefined;
|
|
374
|
+
const channelId = typeof target?.channel_id === 'string' ? target.channel_id : undefined;
|
|
375
|
+
const chatType = normChatType(target?.chat_type);
|
|
226
376
|
const threadId = typeof target?.thread_id === 'string' ? target.thread_id : undefined;
|
|
227
377
|
const targetId = typeof p.target_id === 'string' ? p.target_id : undefined;
|
|
228
|
-
|
|
229
|
-
|
|
378
|
+
const rejectContext = {
|
|
379
|
+
...(channelId ? { channelId } : {}), chatType, ...(threadId ? { threadId } : {}),
|
|
380
|
+
...(targetId ? { targetId } : {}),
|
|
381
|
+
};
|
|
382
|
+
if (!owners.includes(fromAid))
|
|
383
|
+
return { kind: 'reject', code: 'NOT_OWNER', message: '仅 owner 可插话', action, injectId, ...rejectContext };
|
|
384
|
+
if ((p.action !== undefined && p.action !== 'add' && p.action !== 'remove' && p.action !== 'promote')
|
|
385
|
+
|| !channelId?.trim() || (target?.chat_type !== undefined && target.chat_type !== 'private' && target.chat_type !== 'group')
|
|
386
|
+
|| (target?.thread_id !== undefined && typeof target.thread_id !== 'string')
|
|
387
|
+
|| (action === 'add' && (typeof p.text !== 'string' || !p.text.trim() || (p.mode !== undefined && p.mode !== 'next' && p.mode !== 'interrupt')))
|
|
388
|
+
|| (action === 'promote' && !targetId?.trim())) {
|
|
389
|
+
return { kind: 'reject', code: 'INVALID_TARGET', message: 'target.channel_id 必填;add 需 text,promote 需 target_id', action, injectId, ...rejectContext };
|
|
230
390
|
}
|
|
231
|
-
|
|
391
|
+
if (action === 'remove')
|
|
392
|
+
return { kind: 'remove', injectId, channelId, chatType, threadId, targetId };
|
|
393
|
+
if (action === 'promote')
|
|
394
|
+
return { kind: 'promote', injectId, channelId, chatType, threadId, targetId: targetId };
|
|
395
|
+
return {
|
|
396
|
+
kind: 'add', injectId, id: injectId || `inj-${ts}-${crypto.randomUUID()}`, text: p.text,
|
|
397
|
+
...(p.mode === 'interrupt' ? { mode: 'interrupt' } : {}),
|
|
398
|
+
channelId, chatType, threadId, ownerAid: fromAid,
|
|
399
|
+
};
|
|
232
400
|
}
|