evolclaw 3.0.0 → 3.1.1
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/README.md +1 -1
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +47 -12
- package/dist/agents/codex-runner.js +2 -0
- package/dist/agents/gemini-runner.js +9 -9
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/aun/aid/identity.js +28 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/msg/group.js +3 -1
- package/dist/aun/msg/p2p.js +42 -1
- package/dist/channels/aun.js +427 -146
- package/dist/channels/dingtalk.js +3 -1
- package/dist/channels/feishu.js +128 -7
- package/dist/channels/qqbot.js +3 -1
- package/dist/channels/wechat.js +4 -1
- package/dist/channels/wecom.js +3 -1
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +418 -40
- package/dist/cli/init.js +3 -4
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +666 -0
- package/dist/config-store.js +82 -5
- package/dist/core/channel-loader.js +23 -10
- package/dist/core/command-handler.js +127 -99
- package/dist/core/evolagent.js +5 -10
- package/dist/core/message/im-renderer.js +93 -48
- package/dist/core/message/items-formatter.js +11 -4
- package/dist/core/message/message-bridge.js +11 -2
- package/dist/core/message/message-log.js +8 -1
- package/dist/core/message/message-processor.js +194 -127
- package/dist/core/message/message-queue.js +10 -3
- package/dist/core/permission.js +95 -3
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-manager.js +103 -65
- package/dist/core/trigger/manager.js +16 -0
- package/dist/core/trigger/parser.js +110 -0
- package/dist/core/trigger/scheduler.js +7 -1
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +186 -19
- package/dist/net-check.js +640 -0
- package/dist/paths.js +31 -40
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +10 -0
- package/dist/utils/cross-platform.js +17 -8
- package/dist/utils/error-utils.js +27 -15
- package/dist/utils/instance-registry.js +6 -5
- package/dist/utils/log-writer.js +2 -1
- package/dist/utils/logger.js +10 -0
- package/dist/utils/npm-ops.js +35 -3
- package/dist/utils/process-introspect.js +16 -38
- package/dist/utils/stats.js +216 -2
- package/dist/watch-msg.js +26 -11
- package/evolclaw-install-aun.md +14 -2
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +72 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +73 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +7 -5
- package/dist/agents/templates.js +0 -122
- package/dist/data/prompts.md +0 -137
- package/kits/aun/meta.md +0 -25
- package/kits/aun/role.md +0 -25
- package/kits/templates/group.md +0 -20
- package/kits/templates/private.md +0 -9
- package/kits/templates/system-fragments/personal-context.md +0 -3
- package/kits/templates/system-fragments/self-intro.md +0 -5
- package/kits/templates/system-fragments/speaker-intro.md +0 -5
- package/kits/templates/system-fragments/venue-intro.md +0 -5
- /package/kits/{channels → docs/channels}/aun.md +0 -0
- /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
- /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
package/dist/core/permission.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import { renderActionAsText } from './interaction-router.js';
|
|
3
4
|
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
4
5
|
// 危险命令黑名单(正则表达式)
|
|
@@ -89,9 +90,15 @@ export function summarizeToolInput(toolName, input) {
|
|
|
89
90
|
return '';
|
|
90
91
|
const extractors = {
|
|
91
92
|
'Read': (i) => i.file_path,
|
|
92
|
-
'Edit': (i) => i
|
|
93
|
+
'Edit': (i) => formatEditSummary(i),
|
|
93
94
|
'Write': (i) => i.file_path,
|
|
94
|
-
'Bash': (i) =>
|
|
95
|
+
'Bash': (i) => {
|
|
96
|
+
const cmd = i.command?.substring(0, 80) || '';
|
|
97
|
+
const desc = i.description;
|
|
98
|
+
if (desc && cmd)
|
|
99
|
+
return `${cmd} | ${desc}`;
|
|
100
|
+
return cmd || desc;
|
|
101
|
+
},
|
|
95
102
|
'Grep': (i) => `pattern: ${i.pattern}`,
|
|
96
103
|
'Glob': (i) => `pattern: ${i.pattern}`,
|
|
97
104
|
'Agent': (i) => i.description || i.prompt?.substring(0, 80),
|
|
@@ -110,6 +117,8 @@ export function summarizeToolInput(toolName, input) {
|
|
|
110
117
|
},
|
|
111
118
|
'TaskCreate': (i) => i.subject || i.description?.substring(0, 80),
|
|
112
119
|
'TaskUpdate': (i) => i.status ? `${i.taskId} → ${i.status}` : i.taskId,
|
|
120
|
+
'TaskOutput': (i) => `${i.task_id || '?'}${i.block === false ? ' (non-blocking)' : ''}${i.timeout ? ` timeout=${i.timeout}ms` : ''}`,
|
|
121
|
+
'TaskStop': (i) => i.task_id || i.shell_id || '?',
|
|
113
122
|
'NotebookEdit': (i) => i.notebook_path,
|
|
114
123
|
'WebFetch': (i) => i.url,
|
|
115
124
|
'WebSearch': (i) => i.query?.substring(0, 80),
|
|
@@ -131,6 +140,81 @@ export function summarizeToolInput(toolName, input) {
|
|
|
131
140
|
|| input.url
|
|
132
141
|
|| '';
|
|
133
142
|
}
|
|
143
|
+
/** 为 Edit 工具生成 diff 风格摘要 */
|
|
144
|
+
function formatEditSummary(input) {
|
|
145
|
+
const filePath = input.file_path || '';
|
|
146
|
+
const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
|
|
147
|
+
const newStr = typeof input.new_string === 'string' ? input.new_string : '';
|
|
148
|
+
if (!oldStr && !newStr)
|
|
149
|
+
return filePath;
|
|
150
|
+
const MAX_DIFF_LINES = 14;
|
|
151
|
+
const oldLines = oldStr.split('\n');
|
|
152
|
+
const newLines = newStr.split('\n');
|
|
153
|
+
// 尝试从文件中定位 old_string 的起始行号
|
|
154
|
+
let startLine = 0; // 0-based; 0 means unknown
|
|
155
|
+
if (filePath && oldStr) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
const idx = content.indexOf(oldStr);
|
|
159
|
+
if (idx >= 0) {
|
|
160
|
+
startLine = content.slice(0, idx).split('\n').length; // 1-based
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// 文件不可读,行号留空
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const diffLines = [];
|
|
168
|
+
// 找公共前缀行数
|
|
169
|
+
let prefixLen = 0;
|
|
170
|
+
while (prefixLen < oldLines.length && prefixLen < newLines.length && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
171
|
+
prefixLen++;
|
|
172
|
+
}
|
|
173
|
+
// 找公共后缀行数
|
|
174
|
+
let suffixLen = 0;
|
|
175
|
+
while (suffixLen < oldLines.length - prefixLen &&
|
|
176
|
+
suffixLen < newLines.length - prefixLen &&
|
|
177
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
178
|
+
suffixLen++;
|
|
179
|
+
}
|
|
180
|
+
const CONTEXT = 2;
|
|
181
|
+
// 计算行号宽度(用于对齐)
|
|
182
|
+
const maxLineNo = startLine > 0 ? startLine + oldLines.length - 1 : 0;
|
|
183
|
+
const newMaxLineNo = startLine > 0 ? startLine + prefixLen + (newLines.length - suffixLen - prefixLen) - 1 : 0;
|
|
184
|
+
const padWidth = startLine > 0 ? Math.max(maxLineNo, newMaxLineNo).toString().length : 0;
|
|
185
|
+
// 格式化一行:行号 + 标记 + 内容
|
|
186
|
+
// 使用 Unicode 符号避免飞书 Markdown 将 "- " 解析为列表
|
|
187
|
+
const fmtLine = (lineNo, marker, text) => {
|
|
188
|
+
if (startLine > 0) {
|
|
189
|
+
return `${lineNo.toString().padStart(padWidth)} ${marker} ${text}`;
|
|
190
|
+
}
|
|
191
|
+
return `${marker} ${text}`;
|
|
192
|
+
};
|
|
193
|
+
// 上下文前缀(最多 CONTEXT 行)
|
|
194
|
+
const ctxStart = Math.max(0, prefixLen - CONTEXT);
|
|
195
|
+
for (let i = ctxStart; i < prefixLen; i++) {
|
|
196
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
197
|
+
}
|
|
198
|
+
// 删除行
|
|
199
|
+
const removedEnd = oldLines.length - suffixLen;
|
|
200
|
+
for (let i = prefixLen; i < removedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
201
|
+
diffLines.push(fmtLine(startLine + i, '−', oldLines[i]));
|
|
202
|
+
}
|
|
203
|
+
// 新增行(行号从 prefixLen 位置开始递增)
|
|
204
|
+
const addedEnd = newLines.length - suffixLen;
|
|
205
|
+
for (let i = prefixLen; i < addedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
206
|
+
diffLines.push(fmtLine(startLine + i, '+', newLines[i]));
|
|
207
|
+
}
|
|
208
|
+
// 上下文后缀(最多 CONTEXT 行)
|
|
209
|
+
const ctxEnd = Math.min(oldLines.length, removedEnd + CONTEXT);
|
|
210
|
+
for (let i = removedEnd; i < ctxEnd && diffLines.length < MAX_DIFF_LINES + 2; i++) {
|
|
211
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
212
|
+
}
|
|
213
|
+
if (diffLines.length > MAX_DIFF_LINES + 2) {
|
|
214
|
+
diffLines.splice(MAX_DIFF_LINES, diffLines.length, ' ...');
|
|
215
|
+
}
|
|
216
|
+
return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
|
|
217
|
+
}
|
|
134
218
|
export class PermissionGateway {
|
|
135
219
|
pending = new Map();
|
|
136
220
|
timeout = 5 * 60 * 1000;
|
|
@@ -222,7 +306,15 @@ export class PermissionGateway {
|
|
|
222
306
|
await sendPrompt(renderActionAsText(interaction));
|
|
223
307
|
}
|
|
224
308
|
return new Promise((resolve) => {
|
|
225
|
-
|
|
309
|
+
const timer = setTimeout(() => {
|
|
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 });
|
|
226
318
|
// 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
|
|
227
319
|
if (context?.interactionRouter) {
|
|
228
320
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerIdentityCache - 对端身份缓存管理
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 从对端的 agent.md 确定身份(human / agent)
|
|
6
|
+
* 2. 缓存到关系层文件(30天时效)
|
|
7
|
+
* 3. 支持入站和出站消息的身份查询
|
|
8
|
+
*
|
|
9
|
+
* 信源:对端的 agent.md(通过 AUN SDK 下载并验签)
|
|
10
|
+
* 判定规则:type !== 'human' → agent
|
|
11
|
+
* 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import { logger } from '../../utils/logger.js';
|
|
17
|
+
/**
|
|
18
|
+
* 对端身份缓存管理器
|
|
19
|
+
*/
|
|
20
|
+
export class PeerIdentityCache {
|
|
21
|
+
/** 缓存最大时效:30 天 */
|
|
22
|
+
static CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* 获取 peer-identity.json 文件路径
|
|
25
|
+
*/
|
|
26
|
+
static getFilePath(channel, peerId, agentDir) {
|
|
27
|
+
const peerKey = `${channel}#${encodeURIComponent(peerId)}`;
|
|
28
|
+
return path.join(agentDir, 'relations', peerKey, 'peer-identity.json');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 从文件读取缓存
|
|
32
|
+
* @returns PeerIdentity | null(缓存不存在)
|
|
33
|
+
*/
|
|
34
|
+
static get(channel, peerId, agentDir) {
|
|
35
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 检查缓存是否需要刷新
|
|
46
|
+
* @param maxAgeMs 最大缓存时间(默认 30 天)
|
|
47
|
+
* @returns true=需要刷新
|
|
48
|
+
*/
|
|
49
|
+
static needsRefresh(channel, peerId, agentDir, maxAgeMs = this.CACHE_MAX_AGE_MS) {
|
|
50
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
51
|
+
if (!cached)
|
|
52
|
+
return true;
|
|
53
|
+
return Date.now() - cached.lastCheckedAt > maxAgeMs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 从 agent.md 更新身份信息
|
|
57
|
+
* @param agentMd 已验签的 agent.md 内容
|
|
58
|
+
*/
|
|
59
|
+
static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
|
|
60
|
+
// 解析 type 和 name
|
|
61
|
+
const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
|
|
62
|
+
const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
63
|
+
const type = typeMatch?.[1] || 'unknown';
|
|
64
|
+
const isAgent = type !== 'human';
|
|
65
|
+
const name = nameMatch?.[1]?.trim();
|
|
66
|
+
// 计算 hash
|
|
67
|
+
const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
|
|
68
|
+
// 构建身份信息
|
|
69
|
+
const identity = {
|
|
70
|
+
aid: peerId,
|
|
71
|
+
type,
|
|
72
|
+
isAgent,
|
|
73
|
+
name,
|
|
74
|
+
agentMdHash,
|
|
75
|
+
verifiedAt,
|
|
76
|
+
lastCheckedAt: Date.now(),
|
|
77
|
+
source: 'agentmd',
|
|
78
|
+
};
|
|
79
|
+
// 写入文件
|
|
80
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
84
|
+
logger.debug(`[PeerIdentityCache] Updated: ${channel}#${peerId} type=${type} isAgent=${isAgent}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
|
|
88
|
+
}
|
|
89
|
+
return identity;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 标记为 unknown(验签失败或无 agent.md)
|
|
93
|
+
*/
|
|
94
|
+
static markUnknown(channel, peerId, agentDir) {
|
|
95
|
+
const identity = {
|
|
96
|
+
aid: peerId,
|
|
97
|
+
type: 'unknown',
|
|
98
|
+
isAgent: true, // 验签失败 → 当做 agent(安全策略)
|
|
99
|
+
agentMdHash: '',
|
|
100
|
+
verifiedAt: 0,
|
|
101
|
+
lastCheckedAt: Date.now(),
|
|
102
|
+
source: 'unknown',
|
|
103
|
+
};
|
|
104
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
107
|
+
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
108
|
+
logger.debug(`[PeerIdentityCache] Marked unknown: ${channel}#${peerId}`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
logger.warn(`[PeerIdentityCache] Failed to write unknown cache: ${filePath} err=${err}`);
|
|
112
|
+
}
|
|
113
|
+
return identity;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 完整流程:检查缓存 → 需要刷新则下载 agent.md → 更新缓存
|
|
117
|
+
*
|
|
118
|
+
* @param channel 渠道类型(如 'aun')
|
|
119
|
+
* @param peerId 对端 ID(AUN 是 AID)
|
|
120
|
+
* @param agentDir agent 数据根目录
|
|
121
|
+
* @param aunClient AUN SDK client(需要有 fetchAgentMd 方法)
|
|
122
|
+
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
123
|
+
* @returns PeerIdentity
|
|
124
|
+
*/
|
|
125
|
+
static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
|
|
126
|
+
// 1. 检查缓存
|
|
127
|
+
if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
|
|
128
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
129
|
+
if (cached) {
|
|
130
|
+
logger.debug(`[PeerIdentityCache] Cache hit: ${channel}#${peerId} type=${cached.type} age=${Math.floor((Date.now() - cached.lastCheckedAt) / 1000 / 60 / 60 / 24)}d`);
|
|
131
|
+
return cached;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 2. 下载并验签 agent.md(SDK 自动验签)
|
|
135
|
+
try {
|
|
136
|
+
logger.debug(`[PeerIdentityCache] Fetching agent.md: ${channel}#${peerId}`);
|
|
137
|
+
const result = await aunClient.fetchAgentMd(peerId);
|
|
138
|
+
const agentMd = result.content;
|
|
139
|
+
// 3. 更新缓存
|
|
140
|
+
return this.updateFromAgentMd(channel, peerId, agentDir, agentMd, Date.now());
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
// 验签失败或下载失败 → 标记为 unknown,当做 agent
|
|
144
|
+
logger.warn(`[PeerIdentityCache] Failed to fetch agent.md: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
return this.markUnknown(channel, peerId, agentDir);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 清除指定对端的缓存
|
|
150
|
+
*/
|
|
151
|
+
static clear(channel, peerId, agentDir) {
|
|
152
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(filePath);
|
|
155
|
+
logger.debug(`[PeerIdentityCache] Cleared: ${channel}#${peerId}`);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// 文件不存在,忽略
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -31,9 +31,16 @@ export class SessionManager {
|
|
|
31
31
|
setSessionModeResolver(resolver) {
|
|
32
32
|
this.sessionModeResolver = resolver;
|
|
33
33
|
}
|
|
34
|
-
resolveDefaultSessionMode(channel, chatType) {
|
|
34
|
+
resolveDefaultSessionMode(channel, chatType, peerType) {
|
|
35
35
|
const ct = chatType || 'private';
|
|
36
|
-
|
|
36
|
+
// 来源2:群聊强制 proactive
|
|
37
|
+
if (ct === 'group')
|
|
38
|
+
return 'proactive';
|
|
39
|
+
// 来源3:非 human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
|
|
40
|
+
if (peerType && peerType !== 'human' && peerType !== 'unknown')
|
|
41
|
+
return 'proactive';
|
|
42
|
+
// 来源1:agent 配置默认值
|
|
43
|
+
const resolved = this.sessionModeResolver?.(channel, ct, peerType);
|
|
37
44
|
return resolved || 'interactive';
|
|
38
45
|
}
|
|
39
46
|
registerFileAdapter(adapter) {
|
|
@@ -176,12 +183,75 @@ export class SessionManager {
|
|
|
176
183
|
const metaPath = this.metaFilePath(targetDir, session.id);
|
|
177
184
|
appendJsonl(metaPath, file);
|
|
178
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* 比较两个 SessionFile 是否在内容上相等(忽略 updatedAt / updatedAtStr)。
|
|
188
|
+
* 用于跳过"没真变化"的写入,避免 jsonl 写放大。
|
|
189
|
+
*/
|
|
190
|
+
sessionFilesEqual(a, b) {
|
|
191
|
+
const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
|
|
192
|
+
return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Append meta + write active.json,但只在 session 内容(除 updatedAt 外)真正变化时才写。
|
|
196
|
+
* prev 是修改前的快照(用于 diff),next 是修改后的 session。
|
|
197
|
+
* 返回是否发生了写入。
|
|
198
|
+
*/
|
|
199
|
+
writeSessionIfChanged(channel, channelId, prev, next) {
|
|
200
|
+
if (prev) {
|
|
201
|
+
const prevFile = sessionToFile(prev);
|
|
202
|
+
const nextFile = sessionToFile(next);
|
|
203
|
+
if (this.sessionFilesEqual(prevFile, nextFile))
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
next.updatedAt = Date.now();
|
|
207
|
+
this.appendMeta(channel, channelId, next);
|
|
208
|
+
const active = this.readActive(channel, channelId);
|
|
209
|
+
if (active && active.id === next.id) {
|
|
210
|
+
// 保留 active.json 中已有的 activeTask(markProcessing 写入的处理状态)
|
|
211
|
+
if (active.processingState && !next.processingState) {
|
|
212
|
+
next.processingState = active.processingState;
|
|
213
|
+
}
|
|
214
|
+
this.writeActive(channel, channelId, next);
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
179
218
|
readMetaLatest(metaFilePath) {
|
|
180
219
|
const file = readLastJsonlLine(metaFilePath);
|
|
181
220
|
if (!file)
|
|
182
221
|
return undefined;
|
|
183
222
|
return fileToSession(file);
|
|
184
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* 为 by-sessionId 改方法加载"当前 session 状态"。
|
|
226
|
+
*
|
|
227
|
+
* 设计契约(docs/refactor/01-db-to-fs.md):
|
|
228
|
+
* active.json 是热路径权威源。.jsonl 是历史档案。
|
|
229
|
+
*
|
|
230
|
+
* 读取策略:
|
|
231
|
+
* 1. 先按 sessionId 定位 .jsonl 文件(确认 session 存在 + 拿到 channel/channelId)
|
|
232
|
+
* 2. 优先读 active.json(如果 active.id === sessionId)—— 当前状态
|
|
233
|
+
* 3. 否则 fallback 到 .jsonl 末行 —— 非活跃 session 的更新(如多 session 并存时改非 active 那个)
|
|
234
|
+
*
|
|
235
|
+
* 返回 { current, prev }:
|
|
236
|
+
* - current 用于 caller 修改后写回
|
|
237
|
+
* - prev 是 current 的初始快照(用于 writeSessionIfChanged 的 diff 检查)
|
|
238
|
+
*/
|
|
239
|
+
loadSessionForUpdate(sessionId) {
|
|
240
|
+
const found = this.findSessionFileById(sessionId);
|
|
241
|
+
if (!found)
|
|
242
|
+
return undefined;
|
|
243
|
+
// 先读 .jsonl 末行拿 channel/channelId(active.json 文件路径需要这两个)
|
|
244
|
+
const fromJsonl = this.readMetaLatest(found.metaPath);
|
|
245
|
+
if (!fromJsonl)
|
|
246
|
+
return undefined;
|
|
247
|
+
// 优先用 active.json 的当前状态(如果它就是这个 sessionId)
|
|
248
|
+
const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
|
|
249
|
+
const base = (active && active.id === sessionId) ? active : fromJsonl;
|
|
250
|
+
// 深拷贝避免 caller 改 current 时污染 prev
|
|
251
|
+
const current = JSON.parse(JSON.stringify(base));
|
|
252
|
+
const prev = JSON.parse(JSON.stringify(base));
|
|
253
|
+
return { current, prev };
|
|
254
|
+
}
|
|
185
255
|
validateSessionFile(session) {
|
|
186
256
|
const agentSessionId = session.agentSessionId;
|
|
187
257
|
if (!agentSessionId)
|
|
@@ -193,12 +263,9 @@ export class SessionManager {
|
|
|
193
263
|
if (adapter.checkExists(session.projectPath, agentSessionId))
|
|
194
264
|
return agentSessionId;
|
|
195
265
|
logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
|
|
266
|
+
const prev = JSON.parse(JSON.stringify(session));
|
|
196
267
|
session.agentSessionId = undefined;
|
|
197
|
-
this.
|
|
198
|
-
const active = this.readActive(session.channel, session.channelId);
|
|
199
|
-
if (active && active.id === session.id) {
|
|
200
|
-
this.writeActive(session.channel, session.channelId, session);
|
|
201
|
-
}
|
|
268
|
+
this.writeSessionIfChanged(session.channel, session.channelId, prev, session);
|
|
202
269
|
return undefined;
|
|
203
270
|
}
|
|
204
271
|
getActiveChatType(channel, channelId) {
|
|
@@ -332,9 +399,9 @@ export class SessionManager {
|
|
|
332
399
|
return result;
|
|
333
400
|
}
|
|
334
401
|
// ─── Session lifecycle ───
|
|
335
|
-
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType) {
|
|
402
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType, peerType) {
|
|
336
403
|
if (threadId) {
|
|
337
|
-
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType);
|
|
404
|
+
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType);
|
|
338
405
|
session.identity = this.resolveIdentity(channel, userId);
|
|
339
406
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
340
407
|
session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
@@ -399,6 +466,7 @@ export class SessionManager {
|
|
|
399
466
|
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
400
467
|
if (existing) {
|
|
401
468
|
const validSessionId = this.validateSessionFile(existing);
|
|
469
|
+
const prev = JSON.parse(JSON.stringify({ ...existing, agentSessionId: validSessionId }));
|
|
402
470
|
const session = { ...existing, agentSessionId: validSessionId };
|
|
403
471
|
session.identity = this.resolveIdentity(channel, userId);
|
|
404
472
|
if (!session.metadata)
|
|
@@ -416,9 +484,7 @@ export class SessionManager {
|
|
|
416
484
|
if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
|
|
417
485
|
session.metadata.peerName = metadata.peerName;
|
|
418
486
|
}
|
|
419
|
-
|
|
420
|
-
this.appendMeta(channel, channelId, session);
|
|
421
|
-
this.writeActive(channel, channelId, session);
|
|
487
|
+
this.writeSessionIfChanged(channel, channelId, prev, session);
|
|
422
488
|
return session;
|
|
423
489
|
}
|
|
424
490
|
// Create new session
|
|
@@ -435,7 +501,7 @@ export class SessionManager {
|
|
|
435
501
|
threadId: '',
|
|
436
502
|
agentId: agentId || 'claude',
|
|
437
503
|
chatType: chatType || 'private',
|
|
438
|
-
sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private'),
|
|
504
|
+
sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private', peerType),
|
|
439
505
|
metadata: sessionMetadata,
|
|
440
506
|
name: name || '默认会话',
|
|
441
507
|
createdAt: Date.now(),
|
|
@@ -457,12 +523,10 @@ export class SessionManager {
|
|
|
457
523
|
return session;
|
|
458
524
|
}
|
|
459
525
|
async updateSession(sessionId, updates) {
|
|
460
|
-
const
|
|
461
|
-
if (!
|
|
462
|
-
return;
|
|
463
|
-
const current = this.readMetaLatest(found.metaPath);
|
|
464
|
-
if (!current)
|
|
526
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
527
|
+
if (!loaded)
|
|
465
528
|
return;
|
|
529
|
+
const { current, prev } = loaded;
|
|
466
530
|
if (updates.chatType !== undefined)
|
|
467
531
|
current.chatType = updates.chatType;
|
|
468
532
|
if (updates.name !== undefined)
|
|
@@ -473,14 +537,9 @@ export class SessionManager {
|
|
|
473
537
|
current.metadata = updates.metadata;
|
|
474
538
|
if ('agentSessionId' in updates)
|
|
475
539
|
current.agentSessionId = updates.agentSessionId ?? undefined;
|
|
476
|
-
current.
|
|
477
|
-
this.appendMeta(current.channel, current.channelId, current);
|
|
478
|
-
const active = this.readActive(current.channel, current.channelId);
|
|
479
|
-
if (active && active.id === sessionId) {
|
|
480
|
-
this.writeActive(current.channel, current.channelId, current);
|
|
481
|
-
}
|
|
540
|
+
this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
|
|
482
541
|
}
|
|
483
|
-
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType) {
|
|
542
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType) {
|
|
484
543
|
const chatDir = this.ensureResolvedChatDir(channel, channelId);
|
|
485
544
|
const threadIndex = readThreadIndex(chatDir);
|
|
486
545
|
const existingMetaId = threadIndex[threadId];
|
|
@@ -511,7 +570,7 @@ export class SessionManager {
|
|
|
511
570
|
threadId,
|
|
512
571
|
agentId: agentId || 'claude',
|
|
513
572
|
chatType: inheritedChatType,
|
|
514
|
-
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
573
|
+
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType, peerType),
|
|
515
574
|
metadata,
|
|
516
575
|
name: name || '话题会话',
|
|
517
576
|
createdAt: Date.now(),
|
|
@@ -581,25 +640,19 @@ export class SessionManager {
|
|
|
581
640
|
const active = this.readActive(channel, channelId);
|
|
582
641
|
if (!active)
|
|
583
642
|
return;
|
|
643
|
+
const prev = JSON.parse(JSON.stringify(active));
|
|
584
644
|
active.agentSessionId = agentSessionId;
|
|
585
|
-
|
|
586
|
-
this.appendMeta(channel, channelId, active);
|
|
587
|
-
this.writeActive(channel, channelId, active);
|
|
645
|
+
this.writeSessionIfChanged(channel, channelId, prev, active);
|
|
588
646
|
}
|
|
589
647
|
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (!found)
|
|
593
|
-
return;
|
|
594
|
-
const current = this.readMetaLatest(found.metaPath);
|
|
595
|
-
if (!current)
|
|
648
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
649
|
+
if (!loaded)
|
|
596
650
|
return;
|
|
651
|
+
const { current, prev } = loaded;
|
|
597
652
|
current.agentSessionId = agentSessionId;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (active && active.id === sessionId) {
|
|
602
|
-
this.writeActive(current.channel, current.channelId, current);
|
|
653
|
+
const wrote = this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
|
|
654
|
+
if (wrote) {
|
|
655
|
+
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
603
656
|
}
|
|
604
657
|
}
|
|
605
658
|
async switchAgent(channel, channelId, projectPath, newAgentId) {
|
|
@@ -650,10 +703,9 @@ export class SessionManager {
|
|
|
650
703
|
const active = this.readActive(channel, channelId);
|
|
651
704
|
if (!active)
|
|
652
705
|
return;
|
|
706
|
+
const prev = JSON.parse(JSON.stringify(active));
|
|
653
707
|
active.agentSessionId = undefined;
|
|
654
|
-
|
|
655
|
-
this.appendMeta(channel, channelId, active);
|
|
656
|
-
this.writeActive(channel, channelId, active);
|
|
708
|
+
this.writeSessionIfChanged(channel, channelId, prev, active);
|
|
657
709
|
}
|
|
658
710
|
getOwnerChatId(targetChannel, ownerPeerId) {
|
|
659
711
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
@@ -742,34 +794,20 @@ export class SessionManager {
|
|
|
742
794
|
return target;
|
|
743
795
|
}
|
|
744
796
|
updateMetadata(sessionId, metadata) {
|
|
745
|
-
const
|
|
746
|
-
if (!
|
|
747
|
-
return;
|
|
748
|
-
const current = this.readMetaLatest(found.metaPath);
|
|
749
|
-
if (!current)
|
|
797
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
798
|
+
if (!loaded)
|
|
750
799
|
return;
|
|
800
|
+
const { current, prev } = loaded;
|
|
751
801
|
current.metadata = metadata;
|
|
752
|
-
current.
|
|
753
|
-
this.appendMeta(current.channel, current.channelId, current);
|
|
754
|
-
const active = this.readActive(current.channel, current.channelId);
|
|
755
|
-
if (active && active.id === sessionId) {
|
|
756
|
-
this.writeActive(current.channel, current.channelId, current);
|
|
757
|
-
}
|
|
802
|
+
this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
|
|
758
803
|
}
|
|
759
804
|
async renameSession(sessionId, newName) {
|
|
760
|
-
const
|
|
761
|
-
if (!
|
|
762
|
-
return false;
|
|
763
|
-
const current = this.readMetaLatest(found.metaPath);
|
|
764
|
-
if (!current)
|
|
805
|
+
const loaded = this.loadSessionForUpdate(sessionId);
|
|
806
|
+
if (!loaded)
|
|
765
807
|
return false;
|
|
808
|
+
const { current, prev } = loaded;
|
|
766
809
|
current.name = newName;
|
|
767
|
-
current.
|
|
768
|
-
this.appendMeta(current.channel, current.channelId, current);
|
|
769
|
-
const active = this.readActive(current.channel, current.channelId);
|
|
770
|
-
if (active && active.id === sessionId) {
|
|
771
|
-
this.writeActive(current.channel, current.channelId, current);
|
|
772
|
-
}
|
|
810
|
+
this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
|
|
773
811
|
return true;
|
|
774
812
|
}
|
|
775
813
|
async unbindSession(sessionId) {
|
|
@@ -92,6 +92,22 @@ export class TriggerManager {
|
|
|
92
92
|
}
|
|
93
93
|
return { active, history };
|
|
94
94
|
}
|
|
95
|
+
update(id, patch) {
|
|
96
|
+
const t = this.triggers.get(id);
|
|
97
|
+
if (!t)
|
|
98
|
+
throw new Error(`触发器不存在:${id}`);
|
|
99
|
+
// Check name uniqueness if name is being changed
|
|
100
|
+
if (patch.name && patch.name !== t.name) {
|
|
101
|
+
for (const other of this.triggers.values()) {
|
|
102
|
+
if (other.id !== id && other.name === patch.name) {
|
|
103
|
+
throw new Error(`触发器名称已存在:${patch.name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
Object.assign(t, patch, { updatedAt: Date.now() });
|
|
108
|
+
this.save();
|
|
109
|
+
return t;
|
|
110
|
+
}
|
|
95
111
|
updateFireStats(id, firedAt) {
|
|
96
112
|
const t = this.triggers.get(id);
|
|
97
113
|
if (!t)
|