evolclaw 3.1.6 → 3.1.7
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 +15 -0
- package/dist/agents/kit-renderer.js +78 -332
- package/dist/agents/manifest-engine.js +243 -0
- package/dist/agents/message-renderer.js +112 -0
- package/dist/channels/aun.js +56 -6
- package/dist/cli/index.js +6 -6
- package/dist/core/message/message-bridge.js +2 -7
- package/dist/core/message/message-processor.js +50 -4
- package/dist/core/message/message-queue.js +15 -1
- package/dist/core/trigger/scheduler.js +23 -7
- package/dist/index.js +48 -48
- package/kits/eck_message_manifest.json +14 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/system-fragments/session.md +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.7 (2026-06-04)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **观察者模式(observable)** — `AgentConfig.observable` 开启后,AUN 入站/出站消息各转发一份给顶层 `owners[]`(`observer.forward` 格式),便于 owner 旁路监听 agent 会话
|
|
8
|
+
|
|
9
|
+
### Improvements
|
|
10
|
+
|
|
11
|
+
- **Trigger channelType 运行时解析** — 移除存储的 `Trigger.targetChannelType`,`buildSyntheticMessage` 改为运行时从 channel key 解析;trigger scheduler 注入与 `__upgrade-check` 播种移到 channel 注册之后,确保 `channelTypeMap` 完整
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **watch 插件改名 ecweb → evolclaw-web** — npm 拒绝 `ecweb`(与 `rrweb` 过于相似),改为独立包 `evolclaw-web` 发布;`evolclaw watch` 的安装/调用引用同步更新
|
|
16
|
+
- **cron trigger 紧密循环** — 定时器提前 ~1ms 唤醒(如 `0 9 * * *` 在 08:59:59.999 触发)会使下次触发落入 `now+50` 窗口被同轮重复触发;`nextCronFireAt` 从窗口外重算,循环守卫改读实时时钟确保收敛
|
|
17
|
+
|
|
3
18
|
## v3.1.6 (2026-06-03)
|
|
4
19
|
|
|
5
20
|
### New Features
|
|
@@ -1,133 +1,45 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { eckDebugDir } from '../paths.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
selfName: '当前 agent 的显示名',
|
|
15
|
-
hasPersona: '是否有 persona 内容',
|
|
16
|
-
hasWorkingMemory: '是否有 working memory',
|
|
17
|
-
peerId: '对端在该渠道的原生 ID',
|
|
18
|
-
peerKey: '对端跨渠道唯一标识(channel#urlEncode(peerId))',
|
|
19
|
-
peerName: '对端显示名',
|
|
20
|
-
peerRole: '对端角色(owner/admin/guest/anonymous)',
|
|
21
|
-
peerType: '对端类型(human/agent)',
|
|
22
|
-
sameDevice: '对端与本端同一物理设备(E2EE 消息 proximity,仅加密消息有值)',
|
|
23
|
-
sameNetwork: '对端与本端在同一网络内',
|
|
24
|
-
sameEgressIp: '对端与本端共享同一出口 IP',
|
|
25
|
-
groupId: '群组 ID(群聊时)',
|
|
26
|
-
chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
|
|
27
|
-
channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
|
|
28
|
-
venueUid: '场所唯一标识(预留)',
|
|
29
|
-
dispatch: '群分发模式(mention=被@才响应 / broadcast=所有消息都响应)',
|
|
30
|
-
clientType: '客户端类型(desktop/web/mobile)',
|
|
31
|
-
permissionMode: '权限模式(auto/bypass/request/edit/plan/noask/readonly)',
|
|
32
|
-
capabilities: '当前渠道支持的能力列表',
|
|
33
|
-
project: '当前项目目录名',
|
|
34
|
-
sessionId: 'evolclaw 会话 ID',
|
|
35
|
-
sessionName: '会话名称',
|
|
36
|
-
sessionKey: '会话路由键(channelType#urlEncode(channelId)#urlEncode(threadId))',
|
|
37
|
-
sessionCreatedAt: '会话创建时间(ISO)',
|
|
38
|
-
timezone: 'IANA 时区名(把 ISO 时间戳转本地时间用,如 Asia/Shanghai)',
|
|
39
|
-
tzOffset: '当前 UTC 偏移(如 +08:00)',
|
|
40
|
-
osInfo: '操作系统及版本(如 Windows 11 Pro (win32 10.0.26200))',
|
|
41
|
-
threadId: '话题 ID(多话题路由时)',
|
|
42
|
-
chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
|
|
43
|
-
readonly: '是否只读模式',
|
|
44
|
-
evolclawMode: 'evolclaw 运行模式(dev=源码仓库可直接修改 | install=全局安装包只读)',
|
|
45
|
-
baseAgent: 'base agent 规范值(claude/codex/gemini/hermes)',
|
|
46
|
-
baseAgentName: 'base agent 显示名',
|
|
47
|
-
baseAgentModel: 'base agent 引擎底座模型(evolclaw 作用域无配置时的兜底)',
|
|
48
|
-
effectiveModel: '当前实际生效模型(关系级 > agent级 > 全局 优先级解析结果)',
|
|
49
|
-
modelFallbackActive: 'evolclaw 配置的模型不可用,当前正在使用降级模型',
|
|
50
|
-
modelFallbackModel: '当前降级使用的 base agent 模型名',
|
|
51
|
-
agentSessionId: 'base agent 会话 ID',
|
|
52
|
-
};
|
|
53
|
-
function buildPathMappings(vars) {
|
|
54
|
-
const pkgRoot = getPackageRoot();
|
|
55
|
-
const evolHome = String(vars['EVOLCLAW_HOME'] || resolveRoot());
|
|
56
|
-
const selfAid = vars['selfAid'] ? String(vars['selfAid']) : '';
|
|
57
|
-
const currentProject = vars['CURRENT_PROJECT'] ? String(vars['CURRENT_PROJECT']) : '';
|
|
58
|
-
const mappings = [
|
|
59
|
-
{ prefix: path.join(pkgRoot, 'kits', 'rules'), alias: '$KITS_RULES' },
|
|
60
|
-
{ prefix: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'), alias: '$KITS_FRAGMENTS' },
|
|
61
|
-
{ prefix: path.join(pkgRoot, 'kits', 'templates'), alias: '$KITS_TEMPLATES' },
|
|
62
|
-
{ prefix: path.join(pkgRoot, 'kits', 'docs'), alias: '$KITS_DOCS' },
|
|
63
|
-
{ prefix: path.join(pkgRoot, 'kits'), alias: '$KITS' },
|
|
64
|
-
{ prefix: pkgRoot, alias: '$PACKAGE_ROOT' },
|
|
65
|
-
];
|
|
66
|
-
if (selfAid) {
|
|
67
|
-
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'personal'), alias: '$PERSONAL_DIR' });
|
|
68
|
-
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'relations'), alias: '$RELATIONS_DIR' });
|
|
69
|
-
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'venues'), alias: '$VENUES_DIR' });
|
|
70
|
-
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid), alias: '$AGENT_DIR' });
|
|
71
|
-
}
|
|
72
|
-
mappings.push({ prefix: evolHome, alias: '$EVOLCLAW_HOME' });
|
|
73
|
-
if (currentProject) {
|
|
74
|
-
mappings.push({ prefix: currentProject, alias: '$CURRENT_PROJECT' });
|
|
75
|
-
}
|
|
76
|
-
// Sort by prefix length descending so longer (more specific) paths match first
|
|
77
|
-
mappings.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
78
|
-
return mappings;
|
|
79
|
-
}
|
|
80
|
-
function shortenPath(filePath, mappings) {
|
|
81
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
82
|
-
for (const { prefix, alias } of mappings) {
|
|
83
|
-
const normalizedPrefix = prefix.replace(/\\/g, '/');
|
|
84
|
-
if (normalized.startsWith(normalizedPrefix)) {
|
|
85
|
-
const rest = normalized.slice(normalizedPrefix.length);
|
|
86
|
-
return alias + rest;
|
|
87
|
-
}
|
|
5
|
+
import { loadManifest, invalidateManifestCache, evaluateWhen, renderTemplate, resolvePathWithDiag, loadSectionFiles, buildPathMappings, shortenPath, } from './manifest-engine.js';
|
|
6
|
+
const MANIFEST_FILE = 'eck_manifest.json';
|
|
7
|
+
// ── Caches ──
|
|
8
|
+
const _sessionPathCache = new Map();
|
|
9
|
+
function getSessionCache(sessionId) {
|
|
10
|
+
let cache = _sessionPathCache.get(sessionId);
|
|
11
|
+
if (!cache) {
|
|
12
|
+
cache = new Map();
|
|
13
|
+
_sessionPathCache.set(sessionId, cache);
|
|
88
14
|
}
|
|
89
|
-
return
|
|
15
|
+
return cache;
|
|
90
16
|
}
|
|
91
|
-
// ── Cache ──
|
|
92
|
-
let _manifestCache = null;
|
|
93
|
-
const _sessionPathCache = new Map();
|
|
94
|
-
// ── Public API ──
|
|
95
17
|
export function loadKitManifest() {
|
|
96
|
-
|
|
97
|
-
logger.info(`[KitRenderer] Loaded manifest: ${
|
|
18
|
+
const sections = loadManifest(MANIFEST_FILE);
|
|
19
|
+
logger.info(`[KitRenderer] Loaded manifest: ${sections.length} sections`);
|
|
98
20
|
}
|
|
99
21
|
export function invalidateKitCache() {
|
|
100
|
-
|
|
22
|
+
invalidateManifestCache();
|
|
101
23
|
_sessionPathCache.clear();
|
|
102
24
|
}
|
|
103
25
|
export function invalidateSessionCache(sessionId) {
|
|
104
26
|
_sessionPathCache.delete(sessionId);
|
|
105
27
|
}
|
|
28
|
+
// ── Main render ──
|
|
106
29
|
export function renderKitSections(ctx) {
|
|
107
|
-
|
|
108
|
-
loadKitManifest();
|
|
109
|
-
const sections = _manifestCache;
|
|
30
|
+
const sections = loadManifest(MANIFEST_FILE);
|
|
110
31
|
const fileParts = [];
|
|
111
32
|
const fragmentParts = [];
|
|
112
33
|
const pathMappings = buildPathMappings(ctx.vars);
|
|
34
|
+
const sessionCache = getSessionCache(ctx.sessionId);
|
|
113
35
|
const diagnostics = [];
|
|
114
36
|
for (const section of sections) {
|
|
115
37
|
const rawPath = section.type === 'file' ? (section.file ?? '') : (section.path ?? '');
|
|
116
38
|
const diag = {
|
|
117
|
-
id: section.id,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
pattern: section.pattern,
|
|
122
|
-
needsInjection: section.needsInjection,
|
|
123
|
-
when: section.when,
|
|
124
|
-
enabled: section.enabled !== false,
|
|
125
|
-
whenPassed: false,
|
|
126
|
-
resolvedPath: null,
|
|
127
|
-
resolveStatus: 'no-path',
|
|
128
|
-
fileCount: 0,
|
|
129
|
-
used: false,
|
|
130
|
-
injected: false,
|
|
39
|
+
id: section.id, description: section.description, type: section.type, rawPath,
|
|
40
|
+
pattern: section.pattern, needsInjection: section.needsInjection, when: section.when,
|
|
41
|
+
enabled: section.enabled !== false, whenPassed: false, resolvedPath: null,
|
|
42
|
+
resolveStatus: 'no-path', fileCount: 0, used: false, injected: false,
|
|
131
43
|
};
|
|
132
44
|
if (section.enabled === false) {
|
|
133
45
|
diag.resolveStatus = 'skipped-disabled';
|
|
@@ -141,13 +53,13 @@ export function renderKitSections(ctx) {
|
|
|
141
53
|
continue;
|
|
142
54
|
}
|
|
143
55
|
if (rawPath) {
|
|
144
|
-
const
|
|
145
|
-
diag.resolvedPath =
|
|
146
|
-
diag.resolveStatus =
|
|
147
|
-
if (
|
|
148
|
-
diag.unresolvedTokens =
|
|
56
|
+
const r = resolvePathWithDiag(rawPath, ctx.vars);
|
|
57
|
+
diag.resolvedPath = r.resolved;
|
|
58
|
+
diag.resolveStatus = r.status;
|
|
59
|
+
if (r.unresolvedTokens.length > 0)
|
|
60
|
+
diag.unresolvedTokens = r.unresolvedTokens;
|
|
149
61
|
}
|
|
150
|
-
const files = loadSectionFiles(section, ctx);
|
|
62
|
+
const files = loadSectionFiles(section, ctx.vars, sessionCache);
|
|
151
63
|
diag.fileCount = files.length;
|
|
152
64
|
if (files.length === 0) {
|
|
153
65
|
diagnostics.push(diag);
|
|
@@ -196,216 +108,56 @@ export function cleanEckDebug() {
|
|
|
196
108
|
}
|
|
197
109
|
catch { /* dir doesn't exist yet */ }
|
|
198
110
|
}
|
|
199
|
-
// CHUNK_CONTINUE_2
|
|
200
|
-
// ── Manifest loading ──
|
|
201
|
-
function loadAndMergeManifest() {
|
|
202
|
-
const kitsPath = path.join(kitsDir(), 'eck_manifest.json');
|
|
203
|
-
const eckPath = path.join(resolveRoot(), 'eck', 'eck_manifest.json');
|
|
204
|
-
let base;
|
|
205
|
-
try {
|
|
206
|
-
base = JSON.parse(fs.readFileSync(kitsPath, 'utf-8'));
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
logger.error(`[KitRenderer] Failed to load kits/eck_manifest.json: ${err}`);
|
|
210
|
-
return [];
|
|
211
|
-
}
|
|
212
|
-
if (!fs.existsSync(eckPath)) {
|
|
213
|
-
return sortSections(base.sections);
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
const override = JSON.parse(fs.readFileSync(eckPath, 'utf-8'));
|
|
217
|
-
if (override.mode === 'replace') {
|
|
218
|
-
return sortSections(override.sections);
|
|
219
|
-
}
|
|
220
|
-
const merged = new Map();
|
|
221
|
-
for (const s of base.sections)
|
|
222
|
-
merged.set(s.id, { ...s });
|
|
223
|
-
for (const s of override.sections) {
|
|
224
|
-
const existing = merged.get(s.id);
|
|
225
|
-
if (existing) {
|
|
226
|
-
merged.set(s.id, { ...existing, ...s });
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
merged.set(s.id, s);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return sortSections([...merged.values()]);
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
logger.warn(`[KitRenderer] Failed to load eck override, using kits only: ${err}`);
|
|
236
|
-
return sortSections(base.sections);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function sortSections(sections) {
|
|
240
|
-
return sections.slice().sort((a, b) => a.order - b.order);
|
|
241
|
-
}
|
|
242
|
-
// ── Section content loading ──
|
|
243
|
-
function loadSectionFiles(section, ctx) {
|
|
244
|
-
if (section.type === 'file' && section.file) {
|
|
245
|
-
const result = loadFileSection(section.file, ctx);
|
|
246
|
-
return result ? [result] : [];
|
|
247
|
-
}
|
|
248
|
-
if (section.type === 'directory' && section.path) {
|
|
249
|
-
return loadDirectorySection(section.path, section.pattern, ctx);
|
|
250
|
-
}
|
|
251
|
-
return [];
|
|
252
|
-
}
|
|
253
|
-
function loadFileSection(filePath, ctx) {
|
|
254
|
-
const resolved = resolvePath(filePath, ctx);
|
|
255
|
-
if (!resolved)
|
|
256
|
-
return null;
|
|
257
|
-
const sessionCache = getSessionCache(ctx.sessionId);
|
|
258
|
-
if (sessionCache.has(resolved))
|
|
259
|
-
return [resolved, sessionCache.get(resolved)];
|
|
260
|
-
try {
|
|
261
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
262
|
-
sessionCache.set(resolved, content);
|
|
263
|
-
return [resolved, content];
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
function loadDirectorySection(dirPath, pattern, ctx) {
|
|
270
|
-
const resolved = resolvePath(dirPath, ctx);
|
|
271
|
-
if (!resolved)
|
|
272
|
-
return [];
|
|
273
|
-
return readDirectoryFiles(resolved, pattern).map(([name, content]) => [path.join(resolved, name), content]);
|
|
274
|
-
}
|
|
275
|
-
// ── Path resolution ──
|
|
276
|
-
function resolvePath(rawPath, ctx) {
|
|
277
|
-
const r = resolvePathWithDiag(rawPath, ctx);
|
|
278
|
-
return r.status === 'ok' ? r.resolved : null;
|
|
279
|
-
}
|
|
280
|
-
function resolvePathWithDiag(rawPath, ctx) {
|
|
281
|
-
const unresolved = [];
|
|
282
|
-
let resolved = rawPath.replace(/\$([A-Z_]+)/g, (_m, name) => {
|
|
283
|
-
const val = ctx.vars[name];
|
|
284
|
-
if (val === undefined || val === null || val === false || val === '') {
|
|
285
|
-
unresolved.push(`$${name}`);
|
|
286
|
-
return '';
|
|
287
|
-
}
|
|
288
|
-
return String(val);
|
|
289
|
-
});
|
|
290
|
-
resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
|
|
291
|
-
const val = ctx.vars[key];
|
|
292
|
-
if (val === undefined || val === null || val === false || val === '') {
|
|
293
|
-
unresolved.push(`{{${key}}}`);
|
|
294
|
-
return '';
|
|
295
|
-
}
|
|
296
|
-
return String(val);
|
|
297
|
-
});
|
|
298
|
-
if (!resolved || resolved.includes('$') || resolved.includes('{{')) {
|
|
299
|
-
return { resolved: resolved || null, status: 'unresolved-vars', unresolvedTokens: unresolved };
|
|
300
|
-
}
|
|
301
|
-
if (unresolved.length > 0) {
|
|
302
|
-
// 占位符是非必需变量,但有的解析为空——视为未解析
|
|
303
|
-
return { resolved, status: 'unresolved-vars', unresolvedTokens: unresolved };
|
|
304
|
-
}
|
|
305
|
-
if (!fs.existsSync(resolved)) {
|
|
306
|
-
return { resolved, status: 'not-exist', unresolvedTokens: unresolved };
|
|
307
|
-
}
|
|
308
|
-
return { resolved, status: 'ok', unresolvedTokens: unresolved };
|
|
309
|
-
}
|
|
310
|
-
// CHUNK_CONTINUE_5
|
|
311
|
-
// ── Directory reading ──
|
|
312
|
-
function readDirectoryFiles(dirPath, pattern) {
|
|
313
|
-
const glob = pattern || '*.md';
|
|
314
|
-
try {
|
|
315
|
-
const files = fs.readdirSync(dirPath)
|
|
316
|
-
.filter(f => matchGlob(f, glob))
|
|
317
|
-
.sort();
|
|
318
|
-
return files.map(f => {
|
|
319
|
-
const content = fs.readFileSync(path.join(dirPath, f), 'utf-8');
|
|
320
|
-
return [f, content];
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
return [];
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function matchGlob(filename, pattern) {
|
|
328
|
-
const regex = pattern
|
|
329
|
-
.replace(/\./g, '\\.')
|
|
330
|
-
.replace(/\*/g, '.*')
|
|
331
|
-
.replace(/\{([^}]+)\}/g, (_, alts) => `(${alts.split(',').join('|')})`);
|
|
332
|
-
return new RegExp(`^${regex}$`).test(filename);
|
|
333
|
-
}
|
|
334
|
-
// ── When condition evaluation ──
|
|
335
|
-
function evaluateWhen(when, vars) {
|
|
336
|
-
if (when === 'always')
|
|
337
|
-
return true;
|
|
338
|
-
if (when.var !== undefined) {
|
|
339
|
-
const val = vars[when.var];
|
|
340
|
-
if (when.eq !== undefined) {
|
|
341
|
-
// 把 undefined 视作 null 的等价物,便于 manifest 用 eq:null/neq:null 表达"未注入"
|
|
342
|
-
if (when.eq === null)
|
|
343
|
-
return val === null || val === undefined;
|
|
344
|
-
return val === when.eq;
|
|
345
|
-
}
|
|
346
|
-
if (when.neq !== undefined) {
|
|
347
|
-
if (when.neq === null)
|
|
348
|
-
return val !== null && val !== undefined;
|
|
349
|
-
return val !== when.neq;
|
|
350
|
-
}
|
|
351
|
-
if (when.in !== undefined)
|
|
352
|
-
return when.in.includes(val);
|
|
353
|
-
if (when.nin !== undefined)
|
|
354
|
-
return !when.nin.includes(val);
|
|
355
|
-
}
|
|
356
|
-
if (when.any)
|
|
357
|
-
return when.any.some(k => isTruthy(vars[k]));
|
|
358
|
-
if (when.all)
|
|
359
|
-
return when.all.every(k => isTruthy(vars[k]));
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
function isTruthy(val) {
|
|
363
|
-
return val !== undefined && val !== null && val !== false && val !== '' && val !== 0;
|
|
364
|
-
}
|
|
365
|
-
// CHUNK_CONTINUE_6
|
|
366
|
-
// ── Template rendering ──
|
|
367
|
-
function resolveConditions(template, vars) {
|
|
368
|
-
// 只匹配**最内层** {{?...}}...{{/}} 块:body 内不允许再出现 {{? ,
|
|
369
|
-
// 否则非贪婪 ([^]*?) 会匹配到嵌套内层的 {{/}},导致外层提前闭合、残留多余 {{/}}。
|
|
370
|
-
// 逐字符负向前瞻 (?!\{\{\?) 排除嵌套起始,配合 do/while 由内向外逐层消解。
|
|
371
|
-
const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}((?:(?!\{\{\?)[^])*?)\{\{\/\}\}/;
|
|
372
|
-
let result = template;
|
|
373
|
-
let prev;
|
|
374
|
-
do {
|
|
375
|
-
prev = result;
|
|
376
|
-
result = result.replace(inner, (_match, key, op, value, body) => {
|
|
377
|
-
if (op === '=')
|
|
378
|
-
return String(vars[key]) === value ? body : '';
|
|
379
|
-
if (op === '!=')
|
|
380
|
-
return String(vars[key]) !== value ? body : '';
|
|
381
|
-
return isTruthy(vars[key]) ? body : '';
|
|
382
|
-
});
|
|
383
|
-
} while (result !== prev);
|
|
384
|
-
return result;
|
|
385
|
-
}
|
|
386
|
-
function renderTemplate(template, vars) {
|
|
387
|
-
// Pass 1: resolve nested conditionals inside-out
|
|
388
|
-
let result = resolveConditions(template, vars);
|
|
389
|
-
// Pass 2: variable substitution {{key}}
|
|
390
|
-
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
391
|
-
const val = vars[key];
|
|
392
|
-
if (!isTruthy(val))
|
|
393
|
-
return '';
|
|
394
|
-
return String(val);
|
|
395
|
-
});
|
|
396
|
-
// Pass 3: remove blank lines
|
|
397
|
-
return result.split('\n').filter(line => line.trim() !== '').join('\n');
|
|
398
|
-
}
|
|
399
|
-
// ── Session cache helper ──
|
|
400
|
-
function getSessionCache(sessionId) {
|
|
401
|
-
let cache = _sessionPathCache.get(sessionId);
|
|
402
|
-
if (!cache) {
|
|
403
|
-
cache = new Map();
|
|
404
|
-
_sessionPathCache.set(sessionId, cache);
|
|
405
|
-
}
|
|
406
|
-
return cache;
|
|
407
|
-
}
|
|
408
111
|
// ── Debug output ──
|
|
112
|
+
const PARAM_DESCRIPTIONS = {
|
|
113
|
+
EVOLCLAW_HOME: '用户数据根目录',
|
|
114
|
+
PACKAGE_ROOT: 'evolclaw 包根目录',
|
|
115
|
+
CURRENT_PROJECT: '当前项目完整路径',
|
|
116
|
+
PERSONAL_DIR: '当前 agent 个人数据目录',
|
|
117
|
+
RELATIONS_DIR: '当前 agent 关系数据目录',
|
|
118
|
+
VENUES_DIR: '当前 agent 环境数据目录',
|
|
119
|
+
selfAid: '当前 agent 的 AID',
|
|
120
|
+
selfName: '当前 agent 的显示名',
|
|
121
|
+
hasPersona: '是否有 persona 内容',
|
|
122
|
+
hasWorkingMemory: '是否有 working memory',
|
|
123
|
+
peerId: '对端在该渠道的原生 ID',
|
|
124
|
+
peerKey: '对端跨渠道唯一标识(channel#urlEncode(peerId))',
|
|
125
|
+
peerName: '对端显示名',
|
|
126
|
+
peerRole: '对端角色(owner/admin/guest/anonymous)',
|
|
127
|
+
peerType: '对端类型(human/agent)',
|
|
128
|
+
sameDevice: '对端与本端同一物理设备(E2EE 消息 proximity,仅加密消息有值)',
|
|
129
|
+
sameNetwork: '对端与本端在同一网络内',
|
|
130
|
+
sameEgressIp: '对端与本端共享同一出口 IP',
|
|
131
|
+
groupId: '群组 ID(群聊时)',
|
|
132
|
+
chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
|
|
133
|
+
channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
|
|
134
|
+
venueUid: '场所唯一标识(预留)',
|
|
135
|
+
dispatch: '群分发模式(mention=被@才响应 / broadcast=所有消息都响应)',
|
|
136
|
+
clientType: '客户端类型(desktop/web/mobile)',
|
|
137
|
+
permissionMode: '权限模式(auto/bypass/request/edit/plan/noask/readonly)',
|
|
138
|
+
capabilities: '当前渠道支持的能力列表',
|
|
139
|
+
project: '当前项目目录名',
|
|
140
|
+
sessionId: 'evolclaw 会话 ID',
|
|
141
|
+
sessionName: '会话名称',
|
|
142
|
+
sessionKey: '会话路由键(channelType#urlEncode(channelId)#urlEncode(threadId))',
|
|
143
|
+
sessionCreatedAt: '会话创建时间(ISO)',
|
|
144
|
+
timezone: 'IANA 时区名(把 ISO 时间戳转本地时间用,如 Asia/Shanghai)',
|
|
145
|
+
tzOffset: '当前 UTC 偏移(如 +08:00)',
|
|
146
|
+
localDate: '当前本地日期(YYYY-MM-DD,按 timezone)',
|
|
147
|
+
weekday: '当前本地星期几(按 timezone+locale)',
|
|
148
|
+
osInfo: '操作系统及版本(如 Windows 11 Pro (win32 10.0.26200))',
|
|
149
|
+
threadId: '话题 ID(多话题路由时)',
|
|
150
|
+
chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
|
|
151
|
+
readonly: '是否只读模式',
|
|
152
|
+
evolclawMode: 'evolclaw 运行模式(dev=源码仓库可直接修改 | install=全局安装包只读)',
|
|
153
|
+
baseAgent: 'base agent 规范值(claude/codex/gemini/hermes)',
|
|
154
|
+
baseAgentName: 'base agent 显示名',
|
|
155
|
+
baseAgentModel: 'base agent 引擎底座模型(evolclaw 作用域无配置时的兜底)',
|
|
156
|
+
effectiveModel: '当前实际生效模型(关系级 > agent级 > 全局 优先级解析结果)',
|
|
157
|
+
modelFallbackActive: 'evolclaw 配置的模型不可用,当前正在使用降级模型',
|
|
158
|
+
modelFallbackModel: '当前降级使用的 base agent 模型名',
|
|
159
|
+
agentSessionId: 'base agent 会话 ID',
|
|
160
|
+
};
|
|
409
161
|
function writeDebugFiles(ctx, output, fragmentsOutput, diagnostics) {
|
|
410
162
|
const now = new Date();
|
|
411
163
|
const ts = now.toISOString().replace(/[T:.]/g, '-').slice(0, 19);
|
|
@@ -415,18 +167,13 @@ function writeDebugFiles(ctx, output, fragmentsOutput, diagnostics) {
|
|
|
415
167
|
sessionId: ctx.sessionId,
|
|
416
168
|
params: Object.entries(ctx.vars)
|
|
417
169
|
.filter(([, v]) => v !== undefined && v !== null)
|
|
418
|
-
.map(([name, value]) => ({
|
|
419
|
-
name,
|
|
420
|
-
value,
|
|
421
|
-
description: PARAM_DESCRIPTIONS[name] || '',
|
|
422
|
-
})),
|
|
170
|
+
.map(([name, value]) => ({ name, value, description: PARAM_DESCRIPTIONS[name] || '' })),
|
|
423
171
|
};
|
|
424
172
|
fs.writeFile(path.join(dir, `vars-${ts}.json`), JSON.stringify(varsData, null, 2), () => { });
|
|
425
173
|
if (output)
|
|
426
174
|
fs.writeFile(path.join(dir, `context-${ts}.md`), output, () => { });
|
|
427
|
-
if (fragmentsOutput)
|
|
175
|
+
if (fragmentsOutput)
|
|
428
176
|
fs.writeFile(path.join(dir, `fragments-${ts}.md`), fragmentsOutput, () => { });
|
|
429
|
-
}
|
|
430
177
|
fs.writeFile(path.join(dir, `manifest-${ts}.md`), formatManifestDiagnostics(ctx, diagnostics), () => { });
|
|
431
178
|
}
|
|
432
179
|
function formatManifestDiagnostics(ctx, diagnostics) {
|
|
@@ -479,7 +226,6 @@ function formatManifestDiagnostics(ctx, diagnostics) {
|
|
|
479
226
|
lines.push(`| ${idx + 1} | ${d.id} | ${status} | ${d.type} | ${rawPath} | ${resolvedShort} | ${d.fileCount} | ${d.used ? 'Y' : '·'} | ${d.injected ? 'Y' : '·'} |`);
|
|
480
227
|
});
|
|
481
228
|
lines.push('');
|
|
482
|
-
// 详细列出每个 section
|
|
483
229
|
lines.push(`## Section details`);
|
|
484
230
|
lines.push('');
|
|
485
231
|
for (const d of diagnostics) {
|