evolclaw 3.2.0 → 3.4.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 +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +334 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
|
@@ -16,6 +16,7 @@ export class MessageQueue {
|
|
|
16
16
|
recentMessageIds = new Set();
|
|
17
17
|
DEDUP_WINDOW = 60_000; // 1 分钟窗口
|
|
18
18
|
interceptors = new Map();
|
|
19
|
+
mutedAgents = new Set(); // 禁言的 agent:消息照常入队,但不取出给大模型
|
|
19
20
|
constructor(handler) {
|
|
20
21
|
this.handler = handler;
|
|
21
22
|
}
|
|
@@ -92,8 +93,8 @@ export class MessageQueue {
|
|
|
92
93
|
if (!this.queues.has(queueKey)) {
|
|
93
94
|
this.queues.set(queueKey, []);
|
|
94
95
|
}
|
|
95
|
-
this.queues.get(queueKey)
|
|
96
|
-
|
|
96
|
+
const queue = this.queues.get(queueKey);
|
|
97
|
+
queue.push({ message, projectPath, agentName, resolve, reject });
|
|
97
98
|
if (this.processing.has(queueKey)) {
|
|
98
99
|
if (options?.interruptible !== false) {
|
|
99
100
|
// 单聊:保留中断行为
|
|
@@ -145,6 +146,14 @@ export class MessageQueue {
|
|
|
145
146
|
this.activeMessageIds.clear();
|
|
146
147
|
return;
|
|
147
148
|
}
|
|
149
|
+
// 禁言:消息留在队列里,暂停消费(解禁后由 unmuteAgent 重新触发 processNext)
|
|
150
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
151
|
+
if (this.mutedAgents.has(headAgent)) {
|
|
152
|
+
logger.info(`[Queue] processNext: agent ${headAgent} muted, pausing key=${queueKey} (${queue.length} queued)`);
|
|
153
|
+
this.processing.delete(queueKey);
|
|
154
|
+
this.processingAgent.delete(queueKey);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
148
157
|
// FIFO 贪心合并:弹出队首连续同 peerId 的消息
|
|
149
158
|
const items = this.dequeueGreedy(queue);
|
|
150
159
|
const merged = items.length === 1 ? items[0] : this.mergeItems(items);
|
|
@@ -377,4 +386,63 @@ export class MessageQueue {
|
|
|
377
386
|
}
|
|
378
387
|
return total;
|
|
379
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* 清空指定 agent 的待处理消息(不影响正在处理中的消息)。
|
|
391
|
+
* 被移除的消息直接 resolve(与 cancel 一致),让 enqueue 的等待方正常解除阻塞。
|
|
392
|
+
* @returns 被清除的消息数量
|
|
393
|
+
*/
|
|
394
|
+
clearByAgent(agentName) {
|
|
395
|
+
let cleared = 0;
|
|
396
|
+
for (const queue of this.queues.values()) {
|
|
397
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
398
|
+
if ((queue[i].agentName || DEFAULT_AGENT_NAME) === agentName) {
|
|
399
|
+
const [removed] = queue.splice(i, 1);
|
|
400
|
+
removed.resolve();
|
|
401
|
+
cleared++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (cleared > 0)
|
|
406
|
+
logger.info(`[Queue] Cleared ${cleared} pending message(s) for agent ${agentName}`);
|
|
407
|
+
return cleared;
|
|
408
|
+
}
|
|
409
|
+
/** 禁言 agent:后续消息照常入队,但 processNext 不再取出处理。 */
|
|
410
|
+
muteAgent(agentName) {
|
|
411
|
+
this.mutedAgents.add(agentName);
|
|
412
|
+
logger.info(`[Queue] Muted agent ${agentName}`);
|
|
413
|
+
}
|
|
414
|
+
/** 解除禁言:重新触发该 agent 已积压、且当前未在处理的队列。 */
|
|
415
|
+
unmuteAgent(agentName) {
|
|
416
|
+
if (!this.mutedAgents.delete(agentName))
|
|
417
|
+
return;
|
|
418
|
+
logger.info(`[Queue] Unmuted agent ${agentName}, resuming queued messages`);
|
|
419
|
+
for (const [key, queue] of this.queues) {
|
|
420
|
+
if (queue.length > 0 && !this.processing.has(key)) {
|
|
421
|
+
const headAgent = queue[0].agentName || DEFAULT_AGENT_NAME;
|
|
422
|
+
if (headAgent === agentName)
|
|
423
|
+
this.processNext(key);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
isAgentMuted(agentName) {
|
|
428
|
+
return this.mutedAgents.has(agentName);
|
|
429
|
+
}
|
|
430
|
+
/** 中断指定 agent 所有正在处理中的会话(停止 agent 时调用)。 */
|
|
431
|
+
interruptByAgent(agentName) {
|
|
432
|
+
for (const [queueKey, name] of this.processingAgent) {
|
|
433
|
+
if ((name || DEFAULT_AGENT_NAME) === agentName) {
|
|
434
|
+
const sessionKey = queueKey.split('::')[0];
|
|
435
|
+
logger.info(`[Queue] Interrupting session ${sessionKey} for stopped agent ${agentName}`);
|
|
436
|
+
this.eventBus?.publish({
|
|
437
|
+
type: 'task:interrupted',
|
|
438
|
+
sessionId: sessionKey,
|
|
439
|
+
reason: 'new_message',
|
|
440
|
+
agentName: name,
|
|
441
|
+
});
|
|
442
|
+
if (this.interruptCallback) {
|
|
443
|
+
this.interruptCallback(sessionKey, this.currentAgentId, name).catch(() => { });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
380
448
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// 观察者插话:待用提示(pending-hints)存储。
|
|
2
|
+
//
|
|
3
|
+
// owner 经 observer.inject 给「agent↔对端」会话预埋的提示,落盘为 append-only jsonl,
|
|
4
|
+
// 在下一条对端消息到达、渲染该轮 prompt 时一次性消费、注入渲染层。
|
|
5
|
+
// 详见 docs/observer-insert-design.md 第一部分(§1.3 文件生命周期 / §1.4 thread 作用域)。
|
|
6
|
+
//
|
|
7
|
+
// 文件:sessions/<channelType>/<selfAID>/<对端>/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 把提示全撤光也删。
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { chatDirPath, appendJsonl, readAllJsonlLines } from '../session/session-fs-store.js';
|
|
23
|
+
import { logger } from '../../utils/logger.js';
|
|
24
|
+
const PENDING_HINTS_FILE = 'pending-hints.jsonl';
|
|
25
|
+
function hintsPath(sessionsDir, channelType, channelId, selfAID) {
|
|
26
|
+
return path.join(chatDirPath(sessionsDir, channelType, channelId, selfAID), PENDING_HINTS_FILE);
|
|
27
|
+
}
|
|
28
|
+
/** 归一 threadId:undefined/null → ''(主线程)。 */
|
|
29
|
+
function normThread(threadId) {
|
|
30
|
+
return threadId || '';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 追加一条 add 事件。返回是否写盘成功(供 ack 判定)。
|
|
34
|
+
* 写盘成功后才回 ack(accepted)——accepted 真正代表"已持久保存"。
|
|
35
|
+
*/
|
|
36
|
+
export function appendHintAdd(sessionsDir, channelType, channelId, selfAID, hint) {
|
|
37
|
+
try {
|
|
38
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
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;
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
logger.warn(`[PendingHints] appendHintAdd failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 追加一条 remove 事件。若该操作使 (对端,thread) 有效集归零,则删除整个文件。
|
|
54
|
+
* 返回是否写盘成功(供 ack 判定)。
|
|
55
|
+
*/
|
|
56
|
+
export function appendHintRemove(sessionsDir, channelType, channelId, selfAID, rm) {
|
|
57
|
+
try {
|
|
58
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
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;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
logger.warn(`[PendingHints] appendHintRemove failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** 回放整个文件,返回是否还存在任意 thread 的有效提示。 */
|
|
78
|
+
function hasAnyEffective(fp) {
|
|
79
|
+
const events = readAllJsonlLines(fp);
|
|
80
|
+
// 按 thread 分组回放
|
|
81
|
+
const byThread = new Map();
|
|
82
|
+
for (const ev of events) {
|
|
83
|
+
const t = normThread(ev.threadId);
|
|
84
|
+
(byThread.get(t) ?? byThread.set(t, []).get(t)).push(ev);
|
|
85
|
+
}
|
|
86
|
+
for (const [, evs] of byThread) {
|
|
87
|
+
if (replayEffective(evs).length > 0)
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/** 对单个 thread 的事件序列回放,算出有效提示集(已 add、未被 remove,按 ts 升序)。 */
|
|
93
|
+
function replayEffective(events) {
|
|
94
|
+
const sorted = events.slice().sort((a, b) => a.ts - b.ts);
|
|
95
|
+
const active = new Map(); // id → hint,保插入序
|
|
96
|
+
for (const ev of sorted) {
|
|
97
|
+
if (ev.action === 'add') {
|
|
98
|
+
active.set(ev.id, { id: ev.id, text: ev.text, ownerAid: ev.ownerAid, ts: ev.ts });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (ev.targetId)
|
|
102
|
+
active.delete(ev.targetId);
|
|
103
|
+
else
|
|
104
|
+
active.clear(); // 无 targetId = 撤该 thread 全部
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [...active.values()].sort((a, b) => a.ts - b.ts);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 消费 (对端, thread) 的有效提示:回放算「该 thread」有效集 → 返回 → 清掉该 thread。
|
|
111
|
+
*
|
|
112
|
+
* thread 隔离:只消费传入 threadId(归一后)的提示。若文件里其它 thread 仍有未消费提示,
|
|
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;
|
|
121
|
+
try {
|
|
122
|
+
all = readAllJsonlLines(fp);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
logger.warn(`[PendingHints] consume read failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const wantThread = normThread(threadId);
|
|
129
|
+
const sameThread = all.filter(ev => normThread(ev.threadId) === wantThread);
|
|
130
|
+
const effective = replayEffective(sameThread);
|
|
131
|
+
// 其它 thread 仍有未消费提示?若有,重写文件只留它们;否则删整个文件。
|
|
132
|
+
const otherThreadEvents = all.filter(ev => normThread(ev.threadId) !== wantThread);
|
|
133
|
+
if (otherThreadEvents.length > 0 && hasEffectiveInEvents(otherThreadEvents)) {
|
|
134
|
+
rewriteFile(fp, otherThreadEvents);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
deleteFile(fp);
|
|
138
|
+
}
|
|
139
|
+
return effective;
|
|
140
|
+
}
|
|
141
|
+
/** events(可能跨多 thread)中是否存在任意有效提示。 */
|
|
142
|
+
function hasEffectiveInEvents(events) {
|
|
143
|
+
const byThread = new Map();
|
|
144
|
+
for (const ev of events) {
|
|
145
|
+
const t = normThread(ev.threadId);
|
|
146
|
+
(byThread.get(t) ?? byThread.set(t, []).get(t)).push(ev);
|
|
147
|
+
}
|
|
148
|
+
for (const [, evs] of byThread) {
|
|
149
|
+
if (replayEffective(evs).length > 0)
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
function rewriteFile(fp, events) {
|
|
155
|
+
try {
|
|
156
|
+
const body = events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
157
|
+
fs.writeFileSync(fp, body);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
logger.warn(`[PendingHints] rewriteFile failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function deleteFile(fp) {
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(fp))
|
|
166
|
+
fs.unlinkSync(fp);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
logger.warn(`[PendingHints] deleteFile failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** 仅供 watch / 调试:读出当前 (对端,thread) 有效提示,不消费、不删文件。 */
|
|
173
|
+
export function peekHints(sessionsDir, channelType, channelId, selfAID, threadId) {
|
|
174
|
+
const fp = hintsPath(sessionsDir, channelType, channelId, selfAID);
|
|
175
|
+
if (!fs.existsSync(fp))
|
|
176
|
+
return [];
|
|
177
|
+
try {
|
|
178
|
+
const all = readAllJsonlLines(fp);
|
|
179
|
+
const wantThread = normThread(threadId);
|
|
180
|
+
return replayEffective(all.filter(ev => normThread(ev.threadId) === wantThread));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ── 渲染接线纯函数(无 IO,便于单测) ──────────────────────────
|
|
187
|
+
/** 把有效提示转成 owner-hint SubMessage(排在对端真实 item 之前,走 inject 渲染模式)。 */
|
|
188
|
+
export function hintsToSubMessages(hints) {
|
|
189
|
+
return hints.map(h => ({
|
|
190
|
+
kind: 'owner-hint',
|
|
191
|
+
content: h.text,
|
|
192
|
+
ownerAid: h.ownerAid,
|
|
193
|
+
injectTime: h.ts,
|
|
194
|
+
timestamp: h.ts,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 渲染失败兜底:把已消费的 owner-hint(提示已从 pending 删除、不可恢复)以纯文本前缀
|
|
199
|
+
* 拼到对端原文之前,避免提示被静默丢弃。仅在 renderMessageBody 抛错或产出空时使用。
|
|
200
|
+
*/
|
|
201
|
+
export function composeHintFallback(hintItems, content) {
|
|
202
|
+
if (!hintItems || hintItems.length === 0)
|
|
203
|
+
return content;
|
|
204
|
+
const lines = hintItems.map(h => `‹owner 提示·已验证›(仅你可见,对端无感)\n${h.content}`);
|
|
205
|
+
return [...lines, content].join('\n\n');
|
|
206
|
+
}
|
|
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
|
+
export function parseInjectRequest(payload, fromAid, owners, ts) {
|
|
213
|
+
const p = (payload && typeof payload === 'object') ? payload : {};
|
|
214
|
+
const injectId = typeof p.id === 'string' ? p.id : undefined;
|
|
215
|
+
const action = p.action === 'remove' ? 'remove' : 'add';
|
|
216
|
+
if (!owners.includes(fromAid)) {
|
|
217
|
+
return { kind: 'reject', code: 'NOT_OWNER', message: '仅 owner 可插话', action, injectId };
|
|
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';
|
|
226
|
+
const threadId = typeof target?.thread_id === 'string' ? target.thread_id : undefined;
|
|
227
|
+
const targetId = typeof p.target_id === 'string' ? p.target_id : undefined;
|
|
228
|
+
if (action === 'remove') {
|
|
229
|
+
return { kind: 'remove', injectId, channelId, chatType, threadId, targetId };
|
|
230
|
+
}
|
|
231
|
+
return { kind: 'add', injectId, id: injectId || `inj-${ts}`, text, channelId, chatType, threadId, ownerAid: fromAid };
|
|
232
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 详见 docs/model-command-design.md。
|
|
11
11
|
*/
|
|
12
12
|
import { loadDefaults, loadAgent } from '../../config-store.js';
|
|
13
|
-
import { resolveAnthropicConfig, resolveOpenaiConfig } from '../../agents/
|
|
13
|
+
import { resolveAnthropicConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
|
|
14
14
|
import { activeBaseagent } from './model-scope.js';
|
|
15
15
|
/** 取指定 baseagent 的 baseUrl + apiKey(复用现有解析链)。 */
|
|
16
16
|
function resolveCreds(self, ba) {
|
|
@@ -18,8 +18,8 @@ import fs from 'fs';
|
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import { agentRelationsDir, agentConfig as agentConfigPath, resolvePaths } from '../../paths.js';
|
|
20
20
|
import { loadDefaults, saveDefaultsSafe, loadAgent, saveAgent } from '../../config-store.js';
|
|
21
|
-
import { formatPeerKey, parsePeerKey } from '../relation/peer-
|
|
22
|
-
import { fileCache } from '../
|
|
21
|
+
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
22
|
+
import { fileCache } from '../daemon-file-cache.js';
|
|
23
23
|
// ── mtime 门控缓存(统一走 FileCache)─────────────────────────────────────
|
|
24
24
|
//
|
|
25
25
|
// resolveEffectiveModel 每条消息按 关系>agent>全局 解析,原本每次都
|
package/dist/core/permission.js
CHANGED
|
@@ -143,6 +143,12 @@ export function summarizeToolInput(toolName, input) {
|
|
|
143
143
|
/** 为 Edit 工具生成 diff 风格摘要 */
|
|
144
144
|
function formatEditSummary(input) {
|
|
145
145
|
const filePath = input.file_path || '';
|
|
146
|
+
const protocolDiff = typeof input.unified_diff === 'string' ? input.unified_diff
|
|
147
|
+
: typeof input.unifiedDiff === 'string' ? input.unifiedDiff
|
|
148
|
+
: typeof input.diff === 'string' ? input.diff
|
|
149
|
+
: '';
|
|
150
|
+
if (protocolDiff)
|
|
151
|
+
return formatProtocolDiffSummary(filePath, protocolDiff);
|
|
146
152
|
const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
|
|
147
153
|
const newStr = typeof input.new_string === 'string' ? input.new_string : '';
|
|
148
154
|
if (!oldStr && !newStr)
|
|
@@ -215,9 +221,18 @@ function formatEditSummary(input) {
|
|
|
215
221
|
}
|
|
216
222
|
return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
|
|
217
223
|
}
|
|
224
|
+
/** 展示 runner/协议已返回的 unified diff;不在 EvolClaw 内重新计算 diff。 */
|
|
225
|
+
function formatProtocolDiffSummary(filePath, diff) {
|
|
226
|
+
const MAX_DIFF_LINES = 32;
|
|
227
|
+
const lines = diff.trimEnd().split('\n');
|
|
228
|
+
const displayLines = lines.length > MAX_DIFF_LINES
|
|
229
|
+
? [...lines.slice(0, MAX_DIFF_LINES), `...(省略 ${lines.length - MAX_DIFF_LINES} 行)`]
|
|
230
|
+
: lines;
|
|
231
|
+
const body = displayLines.join('\n');
|
|
232
|
+
return `${filePath}\n\`\`\`diff\n${body}\n\`\`\``;
|
|
233
|
+
}
|
|
218
234
|
export class PermissionGateway {
|
|
219
235
|
pending = new Map();
|
|
220
|
-
timeout = 5 * 60 * 1000;
|
|
221
236
|
eventBus;
|
|
222
237
|
/** 始终允许的工具缓存:toolName → Set<pattern> */
|
|
223
238
|
alwaysAllow = new Map();
|
|
@@ -283,6 +298,14 @@ export class PermissionGateway {
|
|
|
283
298
|
};
|
|
284
299
|
// 尝试富交互(走统一 adapter.send 入口)
|
|
285
300
|
let interactionSent = false;
|
|
301
|
+
if (context?.flushPending) {
|
|
302
|
+
try {
|
|
303
|
+
await context.flushPending();
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// flush 失败不应阻断权限请求发送
|
|
307
|
+
}
|
|
308
|
+
}
|
|
286
309
|
if (context?.adapter && context.channelId) {
|
|
287
310
|
try {
|
|
288
311
|
const envelope = buildEnvelope({
|
|
@@ -306,15 +329,7 @@ export class PermissionGateway {
|
|
|
306
329
|
await sendPrompt(renderActionAsText(interaction));
|
|
307
330
|
}
|
|
308
331
|
return new Promise((resolve) => {
|
|
309
|
-
|
|
310
|
-
const pending = this.pending.get(requestId);
|
|
311
|
-
if (!pending)
|
|
312
|
-
return;
|
|
313
|
-
this.pending.delete(requestId);
|
|
314
|
-
this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId, toolName });
|
|
315
|
-
pending.resolve('deny');
|
|
316
|
-
}, this.timeout);
|
|
317
|
-
this.pending.set(requestId, { sessionId, toolName, resolve, timer });
|
|
332
|
+
this.pending.set(requestId, { sessionId, toolName, resolve });
|
|
318
333
|
// 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
|
|
319
334
|
if (context?.interactionRouter) {
|
|
320
335
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|
|
@@ -327,7 +342,6 @@ export class PermissionGateway {
|
|
|
327
342
|
const pending = this.pending.get(requestId);
|
|
328
343
|
if (!pending || pending.sessionId !== sessionId)
|
|
329
344
|
return false;
|
|
330
|
-
clearTimeout(pending.timer);
|
|
331
345
|
// 如果是 always,缓存该工具
|
|
332
346
|
if (decision === 'always') {
|
|
333
347
|
this.addAlwaysAllow(pending.toolName);
|
|
@@ -341,7 +355,6 @@ export class PermissionGateway {
|
|
|
341
355
|
cancelAll(sessionId) {
|
|
342
356
|
for (const [requestId, pending] of this.pending.entries()) {
|
|
343
357
|
if (pending.sessionId === sessionId) {
|
|
344
|
-
clearTimeout(pending.timer);
|
|
345
358
|
pending.resolve('deny');
|
|
346
359
|
this.pending.delete(requestId);
|
|
347
360
|
}
|
|
@@ -15,7 +15,22 @@ import * as path from 'path';
|
|
|
15
15
|
import * as crypto from 'crypto';
|
|
16
16
|
import { logger } from '../../utils/logger.js';
|
|
17
17
|
import { agentMdPath } from '../../paths.js';
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* peerKey: 关系层路由键,格式 `<channelType>#<urlEncode(channelId)>`。
|
|
20
|
+
* 群聊场景下 channelId = groupId,所有发言者共用同一个 peerKey。
|
|
21
|
+
*/
|
|
22
|
+
export function formatPeerKey(channelType, channelId) {
|
|
23
|
+
return `${channelType}#${encodeURIComponent(channelId)}`;
|
|
24
|
+
}
|
|
25
|
+
export function parsePeerKey(key) {
|
|
26
|
+
const idx = key.indexOf('#');
|
|
27
|
+
if (idx <= 0)
|
|
28
|
+
throw new Error(`Invalid peer key: ${key}`);
|
|
29
|
+
return {
|
|
30
|
+
channelType: key.slice(0, idx),
|
|
31
|
+
channelId: decodeURIComponent(key.slice(idx + 1)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
19
34
|
/**
|
|
20
35
|
* 对端身份缓存管理器
|
|
21
36
|
*/
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* 会返回空数组。
|
|
11
11
|
*/
|
|
12
12
|
import { createRequire } from 'module';
|
|
13
|
+
import { sanitizeSessionTitle } from '../session-title.js';
|
|
13
14
|
import { logger } from '../../../utils/logger.js';
|
|
14
15
|
import path from 'path';
|
|
15
16
|
import fs from 'fs';
|
|
16
17
|
import os from 'os';
|
|
17
18
|
const requireFromHere = createRequire(import.meta.url);
|
|
18
19
|
let sqliteModule; // undefined = not tried, null = unavailable
|
|
20
|
+
export const sanitizeCodexThreadTitle = sanitizeSessionTitle;
|
|
19
21
|
function loadSqlite() {
|
|
20
22
|
if (sqliteModule !== undefined)
|
|
21
23
|
return sqliteModule;
|
|
@@ -127,7 +129,7 @@ export class CodexSessionFileAdapter {
|
|
|
127
129
|
if (row) {
|
|
128
130
|
return {
|
|
129
131
|
turns: this.countTurnsFromRollout(row.rollout_path),
|
|
130
|
-
title: row.title
|
|
132
|
+
title: sanitizeCodexThreadTitle(row.title),
|
|
131
133
|
};
|
|
132
134
|
}
|
|
133
135
|
}
|
|
@@ -210,7 +212,7 @@ export class CodexSessionFileAdapter {
|
|
|
210
212
|
const rows = db.prepare('SELECT id, title FROM threads WHERE cwd = ? AND archived = 0 ORDER BY updated_at DESC').all(projectPath);
|
|
211
213
|
return rows.map(r => ({
|
|
212
214
|
sessionId: r.id,
|
|
213
|
-
title: r.title
|
|
215
|
+
title: sanitizeCodexThreadTitle(r.title),
|
|
214
216
|
}));
|
|
215
217
|
}
|
|
216
218
|
catch (error) {
|