evolclaw 3.1.6 → 3.1.8
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 +22 -0
- package/dist/agents/kit-renderer.js +78 -332
- package/dist/agents/manifest-engine.js +243 -0
- package/dist/agents/message-renderer.js +115 -0
- package/dist/channels/aun.js +57 -7
- package/dist/cli/index.js +6 -6
- package/dist/core/message/message-bridge.js +2 -7
- package/dist/core/message/message-processor.js +56 -7
- package/dist/core/message/message-queue.js +16 -1
- package/dist/core/trigger/scheduler.js +23 -7
- package/dist/index.js +48 -48
- package/kits/docs/context-assembly.md +86 -0
- package/kits/docs/prompt-loading-architecture.md +234 -0
- 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/kits/templates/system-fragments/venue.md +2 -2
- package/package.json +3 -2
- package/assets/wechat-group-qr.jpeg +0 -0
- package/dist/cli/watch-web/debug-log.js +0 -18
- package/dist/cli/watch-web/server.js +0 -306
- package/dist/cli/watch-web/sources/aid.js +0 -63
- package/dist/cli/watch-web/sources/msg.js +0 -70
- package/dist/cli/watch-web/sources/session.js +0 -638
- package/dist/cli/watch-web/sources/types.js +0 -10
- package/dist/cli/watch-web/static/app.js +0 -546
- package/dist/cli/watch-web/static/index.html +0 -54
- package/dist/cli/watch-web/static/style.css +0 -247
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// 声明式 manifest 渲染引擎(共享)。
|
|
2
|
+
// 系统提示词渲染(kit-renderer)与消息渲染(message-renderer)共用同一套
|
|
3
|
+
// when 求值、模板渲染、路径解析、manifest 加载/缓存原语,避免两套实现漂移。
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { kitsDir, resolveRoot, getPackageRoot } from '../paths.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
// ── Manifest loading / cache (keyed by filename) ──
|
|
9
|
+
const _manifestCache = new Map();
|
|
10
|
+
/** 清空所有 manifest 缓存(manifest 结构变更后调用)。 */
|
|
11
|
+
export function invalidateManifestCache() {
|
|
12
|
+
_manifestCache.clear();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 加载并合并 manifest。基础文件在 $KITS/<filename>,
|
|
16
|
+
* 覆盖文件在 $EVOLCLAW_HOME/eck/<filename>(可选)。结果按 order 升序缓存。
|
|
17
|
+
*/
|
|
18
|
+
export function loadManifest(filename) {
|
|
19
|
+
const cached = _manifestCache.get(filename);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
const sections = loadAndMergeManifest(filename);
|
|
23
|
+
_manifestCache.set(filename, sections);
|
|
24
|
+
logger.info(`[ManifestEngine] Loaded ${filename}: ${sections.length} sections`);
|
|
25
|
+
return sections;
|
|
26
|
+
}
|
|
27
|
+
function loadAndMergeManifest(filename) {
|
|
28
|
+
const kitsPath = path.join(kitsDir(), filename);
|
|
29
|
+
const eckPath = path.join(resolveRoot(), 'eck', filename);
|
|
30
|
+
let base;
|
|
31
|
+
try {
|
|
32
|
+
base = JSON.parse(fs.readFileSync(kitsPath, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
logger.error(`[ManifestEngine] Failed to load kits/${filename}: ${err}`);
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (!fs.existsSync(eckPath))
|
|
39
|
+
return sortSections(base.sections);
|
|
40
|
+
try {
|
|
41
|
+
const override = JSON.parse(fs.readFileSync(eckPath, 'utf-8'));
|
|
42
|
+
if (override.mode === 'replace')
|
|
43
|
+
return sortSections(override.sections);
|
|
44
|
+
const merged = new Map();
|
|
45
|
+
for (const s of base.sections)
|
|
46
|
+
merged.set(s.id, { ...s });
|
|
47
|
+
for (const s of override.sections) {
|
|
48
|
+
const existing = merged.get(s.id);
|
|
49
|
+
merged.set(s.id, existing ? { ...existing, ...s } : s);
|
|
50
|
+
}
|
|
51
|
+
return sortSections([...merged.values()]);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
logger.warn(`[ManifestEngine] Failed to load eck override for ${filename}, using kits only: ${err}`);
|
|
55
|
+
return sortSections(base.sections);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function sortSections(sections) {
|
|
59
|
+
return sections.slice().sort((a, b) => a.order - b.order);
|
|
60
|
+
}
|
|
61
|
+
// ── When condition evaluation ──
|
|
62
|
+
export function evaluateWhen(when, vars) {
|
|
63
|
+
if (when === 'always')
|
|
64
|
+
return true;
|
|
65
|
+
if (when.var !== undefined) {
|
|
66
|
+
const val = vars[when.var];
|
|
67
|
+
if (when.eq !== undefined) {
|
|
68
|
+
if (when.eq === null)
|
|
69
|
+
return val === null || val === undefined;
|
|
70
|
+
return val === when.eq;
|
|
71
|
+
}
|
|
72
|
+
if (when.neq !== undefined) {
|
|
73
|
+
if (when.neq === null)
|
|
74
|
+
return val !== null && val !== undefined;
|
|
75
|
+
return val !== when.neq;
|
|
76
|
+
}
|
|
77
|
+
if (when.in !== undefined)
|
|
78
|
+
return when.in.includes(val);
|
|
79
|
+
if (when.nin !== undefined)
|
|
80
|
+
return !when.nin.includes(val);
|
|
81
|
+
}
|
|
82
|
+
if (when.any)
|
|
83
|
+
return when.any.some(k => isTruthy(vars[k]));
|
|
84
|
+
if (when.all)
|
|
85
|
+
return when.all.every(k => isTruthy(vars[k]));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
export function isTruthy(val) {
|
|
89
|
+
return val !== undefined && val !== null && val !== false && val !== '' && val !== 0;
|
|
90
|
+
}
|
|
91
|
+
// ── Template rendering ──
|
|
92
|
+
function resolveConditions(template, vars) {
|
|
93
|
+
// 只匹配**最内层** {{?...}}...{{/}} 块(逐字符负向前瞻排除嵌套),do/while 由内向外消解。
|
|
94
|
+
const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}((?:(?!\{\{\?)[^])*?)\{\{\/\}\}/;
|
|
95
|
+
let result = template;
|
|
96
|
+
let prev;
|
|
97
|
+
do {
|
|
98
|
+
prev = result;
|
|
99
|
+
result = result.replace(inner, (_match, key, op, value, body) => {
|
|
100
|
+
if (op === '=')
|
|
101
|
+
return String(vars[key]) === value ? body : '';
|
|
102
|
+
if (op === '!=')
|
|
103
|
+
return String(vars[key]) !== value ? body : '';
|
|
104
|
+
return isTruthy(vars[key]) ? body : '';
|
|
105
|
+
});
|
|
106
|
+
} while (result !== prev);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 渲染模板:条件块 + 变量替换。stripBlankLines=true 时删除空行(系统提示词用,
|
|
111
|
+
* 紧凑);false 时保留空行(消息正文用,正文多段结构不能被压扁)。
|
|
112
|
+
*/
|
|
113
|
+
export function renderTemplate(template, vars, stripBlankLines = true) {
|
|
114
|
+
let result = resolveConditions(template, vars);
|
|
115
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
116
|
+
const val = vars[key];
|
|
117
|
+
if (!isTruthy(val))
|
|
118
|
+
return '';
|
|
119
|
+
return String(val);
|
|
120
|
+
});
|
|
121
|
+
if (stripBlankLines)
|
|
122
|
+
return result.split('\n').filter(line => line.trim() !== '').join('\n');
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
export function resolvePathWithDiag(rawPath, vars) {
|
|
126
|
+
const unresolved = [];
|
|
127
|
+
let resolved = rawPath.replace(/\$([A-Z_]+)/g, (_m, name) => {
|
|
128
|
+
const val = vars[name];
|
|
129
|
+
if (val === undefined || val === null || val === false || val === '') {
|
|
130
|
+
unresolved.push(`$${name}`);
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
return String(val);
|
|
134
|
+
});
|
|
135
|
+
resolved = resolved.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
|
|
136
|
+
const val = vars[key];
|
|
137
|
+
if (val === undefined || val === null || val === false || val === '') {
|
|
138
|
+
unresolved.push(`{{${key}}}`);
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
return String(val);
|
|
142
|
+
});
|
|
143
|
+
if (!resolved || resolved.includes('$') || resolved.includes('{{')) {
|
|
144
|
+
return { resolved: resolved || null, status: 'unresolved-vars', unresolvedTokens: unresolved };
|
|
145
|
+
}
|
|
146
|
+
if (unresolved.length > 0) {
|
|
147
|
+
return { resolved, status: 'unresolved-vars', unresolvedTokens: unresolved };
|
|
148
|
+
}
|
|
149
|
+
// 路径规范化:模板里 ../ 等相对片段折叠成真实路径
|
|
150
|
+
resolved = path.normalize(resolved);
|
|
151
|
+
if (!fs.existsSync(resolved)) {
|
|
152
|
+
return { resolved, status: 'not-exist', unresolvedTokens: unresolved };
|
|
153
|
+
}
|
|
154
|
+
return { resolved, status: 'ok', unresolvedTokens: unresolved };
|
|
155
|
+
}
|
|
156
|
+
function resolvePath(rawPath, vars) {
|
|
157
|
+
const r = resolvePathWithDiag(rawPath, vars);
|
|
158
|
+
return r.status === 'ok' ? r.resolved : null;
|
|
159
|
+
}
|
|
160
|
+
// ── Section content loading ──
|
|
161
|
+
/** 返回 [filePath, rawContent][];按 sessionId 缓存已读文件内容。 */
|
|
162
|
+
export function loadSectionFiles(section, vars, sessionCache) {
|
|
163
|
+
if (section.type === 'file' && section.file) {
|
|
164
|
+
const result = loadFileSection(section.file, vars, sessionCache);
|
|
165
|
+
return result ? [result] : [];
|
|
166
|
+
}
|
|
167
|
+
if (section.type === 'directory' && section.path) {
|
|
168
|
+
const resolved = resolvePath(section.path, vars);
|
|
169
|
+
if (!resolved)
|
|
170
|
+
return [];
|
|
171
|
+
return readDirectoryFiles(resolved, section.pattern)
|
|
172
|
+
.map(([name, content]) => [path.join(resolved, name), content]);
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
function loadFileSection(filePath, vars, sessionCache) {
|
|
177
|
+
const resolved = resolvePath(filePath, vars);
|
|
178
|
+
if (!resolved)
|
|
179
|
+
return null;
|
|
180
|
+
if (sessionCache.has(resolved))
|
|
181
|
+
return [resolved, sessionCache.get(resolved)];
|
|
182
|
+
try {
|
|
183
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
184
|
+
sessionCache.set(resolved, content);
|
|
185
|
+
return [resolved, content];
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readDirectoryFiles(dirPath, pattern) {
|
|
192
|
+
const glob = pattern || '*.md';
|
|
193
|
+
try {
|
|
194
|
+
const files = fs.readdirSync(dirPath).filter(f => matchGlob(f, glob)).sort();
|
|
195
|
+
return files.map(f => [f, fs.readFileSync(path.join(dirPath, f), 'utf-8')]);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function matchGlob(filename, pattern) {
|
|
202
|
+
const regex = pattern
|
|
203
|
+
.replace(/\./g, '\\.')
|
|
204
|
+
.replace(/\*/g, '.*')
|
|
205
|
+
.replace(/\{([^}]+)\}/g, (_, alts) => `(${alts.split(',').join('|')})`);
|
|
206
|
+
return new RegExp(`^${regex}$`).test(filename);
|
|
207
|
+
}
|
|
208
|
+
export function buildPathMappings(vars) {
|
|
209
|
+
const pkgRoot = getPackageRoot();
|
|
210
|
+
const evolHome = String(vars['EVOLCLAW_HOME'] || resolveRoot());
|
|
211
|
+
const selfAid = vars['selfAid'] ? String(vars['selfAid']) : '';
|
|
212
|
+
const currentProject = vars['CURRENT_PROJECT'] ? String(vars['CURRENT_PROJECT']) : '';
|
|
213
|
+
const mappings = [
|
|
214
|
+
{ prefix: path.join(pkgRoot, 'kits', 'rules'), alias: '$KITS_RULES' },
|
|
215
|
+
{ prefix: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'), alias: '$KITS_FRAGMENTS' },
|
|
216
|
+
{ prefix: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'), alias: '$KITS_MESSAGE_FRAGMENTS' },
|
|
217
|
+
{ prefix: path.join(pkgRoot, 'kits', 'templates'), alias: '$KITS_TEMPLATES' },
|
|
218
|
+
{ prefix: path.join(pkgRoot, 'kits', 'docs'), alias: '$KITS_DOCS' },
|
|
219
|
+
{ prefix: path.join(pkgRoot, 'kits'), alias: '$KITS' },
|
|
220
|
+
{ prefix: pkgRoot, alias: '$PACKAGE_ROOT' },
|
|
221
|
+
];
|
|
222
|
+
if (selfAid) {
|
|
223
|
+
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'personal'), alias: '$PERSONAL_DIR' });
|
|
224
|
+
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'relations'), alias: '$RELATIONS_DIR' });
|
|
225
|
+
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid, 'venues'), alias: '$VENUES_DIR' });
|
|
226
|
+
mappings.push({ prefix: path.join(evolHome, 'agents', selfAid), alias: '$AGENT_DIR' });
|
|
227
|
+
}
|
|
228
|
+
mappings.push({ prefix: evolHome, alias: '$EVOLCLAW_HOME' });
|
|
229
|
+
if (currentProject)
|
|
230
|
+
mappings.push({ prefix: currentProject, alias: '$CURRENT_PROJECT' });
|
|
231
|
+
mappings.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
232
|
+
return mappings;
|
|
233
|
+
}
|
|
234
|
+
export function shortenPath(filePath, mappings) {
|
|
235
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
236
|
+
for (const { prefix, alias } of mappings) {
|
|
237
|
+
const normalizedPrefix = prefix.replace(/\\/g, '/');
|
|
238
|
+
if (normalized.startsWith(normalizedPrefix)) {
|
|
239
|
+
return alias + normalized.slice(normalizedPrefix.length);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return filePath;
|
|
243
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Message rendering layer: render a batch of sub-messages one-by-one through the
|
|
2
|
+
// message manifest, then assemble the final body fed to the base agent. Shares the
|
|
3
|
+
// manifest-engine primitives with system-prompt rendering, with two key differences:
|
|
4
|
+
// 1. The raw message text {{content}} is injected as a LITERAL in the final step,
|
|
5
|
+
// never going through template parsing again (otherwise {{...}} inside a user
|
|
6
|
+
// message would be treated as a template -- garbled at best, injection at worst).
|
|
7
|
+
// 2. Blank lines are preserved (multi-paragraph message bodies must not be squashed).
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import { eckDebugDir } from '../paths.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { loadManifest, evaluateWhen, renderTemplate, loadSectionFiles, } from './manifest-engine.js';
|
|
14
|
+
const MESSAGE_MANIFEST_FILE = 'eck_message_manifest.json';
|
|
15
|
+
// ── time formatting (per IANA timezone) ──
|
|
16
|
+
function timeParts(epochMs, timeZone, opts) {
|
|
17
|
+
const d = new Date(epochMs);
|
|
18
|
+
const p = new Intl.DateTimeFormat('en-US', { ...(timeZone ? { timeZone } : {}), ...opts }).formatToParts(d);
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const part of p)
|
|
21
|
+
out[part.type] = part.value;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** "2026-06-04 21:33:07 +08:00" */
|
|
25
|
+
export function formatLocalTime(epochMs, timeZone) {
|
|
26
|
+
const g = timeParts(epochMs, timeZone, {
|
|
27
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
28
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
|
29
|
+
});
|
|
30
|
+
const hour = g.hour === '24' ? '00' : g.hour; // hour12:false may yield "24" at midnight
|
|
31
|
+
const off = timeParts(epochMs, timeZone, { timeZoneName: 'longOffset' }).timeZoneName || '';
|
|
32
|
+
const offset = off.replace(/^GMT/, '') || '+00:00';
|
|
33
|
+
return `${g.year}-${g.month}-${g.day} ${hour}:${g.minute}:${g.second} ${offset}`;
|
|
34
|
+
}
|
|
35
|
+
// ── single item render ──
|
|
36
|
+
function renderOneItem(item, sessionVars, sessionCache, contentSentinel) {
|
|
37
|
+
const sections = loadManifest(MESSAGE_MANIFEST_FILE);
|
|
38
|
+
// item-level vars: session vars overlaid with this message's own sender/timestamp.
|
|
39
|
+
const itemVars = {
|
|
40
|
+
...sessionVars,
|
|
41
|
+
peerId: item.peerId ?? sessionVars.peerId,
|
|
42
|
+
peerName: item.peerName ?? sessionVars.peerName,
|
|
43
|
+
peerType: item.peerType ?? sessionVars.peerType,
|
|
44
|
+
sameDevice: item.sameDevice ?? sessionVars.sameDevice,
|
|
45
|
+
sameNetwork: item.sameNetwork ?? sessionVars.sameNetwork,
|
|
46
|
+
sameEgressIp: item.sameEgressIp ?? sessionVars.sameEgressIp,
|
|
47
|
+
now: formatLocalTime(item.timestamp ?? Date.now(), sessionVars.timezone ? String(sessionVars.timezone) : undefined),
|
|
48
|
+
// content held as a per-call random sentinel, swapped back post-render.
|
|
49
|
+
// Using a UUID means no real message can collide with it.
|
|
50
|
+
content: contentSentinel,
|
|
51
|
+
};
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const section of sections) {
|
|
54
|
+
if (section.enabled === false)
|
|
55
|
+
continue;
|
|
56
|
+
if (!evaluateWhen(section.when, itemVars))
|
|
57
|
+
continue;
|
|
58
|
+
const files = loadSectionFiles(section, itemVars, sessionCache);
|
|
59
|
+
for (const [, rawContent] of files) {
|
|
60
|
+
const rendered = section.needsInjection
|
|
61
|
+
? renderTemplate(rawContent, itemVars, /* stripBlankLines */ false)
|
|
62
|
+
: rawContent;
|
|
63
|
+
// swap the sentinel back to the real message text (literal replace, no parsing).
|
|
64
|
+
const withContent = rendered.split(contentSentinel).join(item.content);
|
|
65
|
+
if (withContent.trim())
|
|
66
|
+
out.push(withContent.replace(/\s+$/, ''));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// if the manifest produced nothing, fall back to raw text -- never drop a message.
|
|
70
|
+
return out.length > 0 ? out.join('\n') : item.content;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Render each sub-message then assemble the final message body.
|
|
74
|
+
* Also collects all images across items in order, preserving per-item attribution.
|
|
75
|
+
*
|
|
76
|
+
* One item -> single render; many (group batch / same-peer merge) -> each carries
|
|
77
|
+
* its own sender and timestamp.
|
|
78
|
+
*/
|
|
79
|
+
export function renderMessageBody(items, sessionVars, sessionId) {
|
|
80
|
+
if (!items || items.length === 0)
|
|
81
|
+
return { body: '', images: [] };
|
|
82
|
+
// One random sentinel per renderMessageBody call -- impossible for user text to match.
|
|
83
|
+
const contentSentinel = `\x00ECMSG-${randomUUID()}\x00`;
|
|
84
|
+
const sessionCache = new Map(); // render-local cache (template files are small & fixed)
|
|
85
|
+
const renderedParts = [];
|
|
86
|
+
const allImages = [];
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
renderedParts.push(renderOneItem(item, sessionVars, sessionCache, contentSentinel));
|
|
89
|
+
if (item.images && item.images.length > 0)
|
|
90
|
+
allImages.push(...item.images);
|
|
91
|
+
}
|
|
92
|
+
const body = renderedParts.join('\n\n');
|
|
93
|
+
writeMessageDebug(sessionId, items, body);
|
|
94
|
+
return { body, images: allImages };
|
|
95
|
+
}
|
|
96
|
+
// ── Debug ──
|
|
97
|
+
function writeMessageDebug(sessionId, items, body) {
|
|
98
|
+
try {
|
|
99
|
+
const ts = new Date().toISOString().replace(/[T:.]/g, '-').slice(0, 19);
|
|
100
|
+
const out = [
|
|
101
|
+
`# Message Render`,
|
|
102
|
+
`- sessionId: ${sessionId}`,
|
|
103
|
+
`- items: ${items.length}`,
|
|
104
|
+
`- images: ${items.reduce((n, i) => n + (i.images?.length ?? 0), 0)}`,
|
|
105
|
+
``,
|
|
106
|
+
`## Rendered body`,
|
|
107
|
+
``,
|
|
108
|
+
body,
|
|
109
|
+
].join('\n');
|
|
110
|
+
fs.writeFile(path.join(eckDebugDir(), `msg-render-${ts}.md`), out, () => { });
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
logger.debug(`[MessageRenderer] debug write failed: ${e}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/channels/aun.js
CHANGED
|
@@ -1352,6 +1352,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1352
1352
|
}).catch(err => {
|
|
1353
1353
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
1354
1354
|
});
|
|
1355
|
+
// Observer forward: inbound
|
|
1356
|
+
this.forwardToOwners('inbound', {
|
|
1357
|
+
from: event.userId || event.channelId || '',
|
|
1358
|
+
to: this.config.aid,
|
|
1359
|
+
seq: event.seq,
|
|
1360
|
+
payload: { type: 'text', text: event.text },
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* 观察者模式转发:将消息副本以 observer.forward 格式发给所有 owners。
|
|
1365
|
+
* 仅在 AgentConfig.observable === true 时执行;owners 为空或无法加载配置时静默跳过。
|
|
1366
|
+
*/
|
|
1367
|
+
forwardToOwners(direction, original) {
|
|
1368
|
+
if (!this.connected || !this.client)
|
|
1369
|
+
return;
|
|
1370
|
+
const agentConfig = loadAgent(this.config.aid);
|
|
1371
|
+
if (!agentConfig?.observable)
|
|
1372
|
+
return;
|
|
1373
|
+
const owners = agentConfig.owners ?? [];
|
|
1374
|
+
if (owners.length === 0)
|
|
1375
|
+
return;
|
|
1376
|
+
const forwardPayload = {
|
|
1377
|
+
type: 'observer.forward',
|
|
1378
|
+
direction,
|
|
1379
|
+
agent_aid: this.config.aid,
|
|
1380
|
+
original: {
|
|
1381
|
+
from: original.from,
|
|
1382
|
+
to: original.to,
|
|
1383
|
+
...(original.seq != null ? { seq: original.seq } : {}),
|
|
1384
|
+
timestamp: Date.now(),
|
|
1385
|
+
payload: original.payload,
|
|
1386
|
+
},
|
|
1387
|
+
};
|
|
1388
|
+
for (const ownerAid of owners) {
|
|
1389
|
+
const encrypt = this.shouldEncrypt(ownerAid);
|
|
1390
|
+
this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
|
|
1391
|
+
.catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
|
|
1392
|
+
}
|
|
1355
1393
|
}
|
|
1356
1394
|
handleEcho(event) {
|
|
1357
1395
|
const ts = () => {
|
|
@@ -1828,6 +1866,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1828
1866
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
1829
1867
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1830
1868
|
this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
|
|
1869
|
+
// Observer forward: outbound (group)
|
|
1870
|
+
this.forwardToOwners('outbound', {
|
|
1871
|
+
from: this.config.aid,
|
|
1872
|
+
to: channelId,
|
|
1873
|
+
payload: { type: 'text', text: finalText },
|
|
1874
|
+
});
|
|
1831
1875
|
}
|
|
1832
1876
|
}
|
|
1833
1877
|
else {
|
|
@@ -1841,6 +1885,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1841
1885
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
|
|
1842
1886
|
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1843
1887
|
this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
|
|
1888
|
+
// Observer forward: outbound (private)
|
|
1889
|
+
this.forwardToOwners('outbound', {
|
|
1890
|
+
from: this.config.aid,
|
|
1891
|
+
to: targetAid,
|
|
1892
|
+
payload: { type: 'text', text: finalText },
|
|
1893
|
+
});
|
|
1844
1894
|
}
|
|
1845
1895
|
}
|
|
1846
1896
|
return true;
|
|
@@ -2579,25 +2629,25 @@ export class AUNChannelPlugin {
|
|
|
2579
2629
|
return;
|
|
2580
2630
|
}
|
|
2581
2631
|
case 'status.progress':
|
|
2582
|
-
channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2632
|
+
channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2583
2633
|
return;
|
|
2584
2634
|
case 'status.started':
|
|
2585
|
-
channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
|
|
2635
|
+
channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2586
2636
|
return;
|
|
2587
2637
|
case 'status.queued':
|
|
2588
|
-
channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx);
|
|
2638
|
+
channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2589
2639
|
return;
|
|
2590
2640
|
case 'status.completed':
|
|
2591
|
-
channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
|
|
2641
|
+
channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2592
2642
|
return;
|
|
2593
2643
|
case 'status.interrupted':
|
|
2594
|
-
channel.sendProcessingStatus(channelId, 'interrupted', envelope.taskId, envelope.taskId, ctx);
|
|
2644
|
+
channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2595
2645
|
return;
|
|
2596
2646
|
case 'status.error':
|
|
2597
|
-
channel.sendProcessingStatus(channelId, 'error', envelope.taskId, envelope.taskId, ctx);
|
|
2647
|
+
channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2598
2648
|
return;
|
|
2599
2649
|
case 'status.timeout':
|
|
2600
|
-
channel.sendProcessingStatus(channelId, 'timeout', envelope.taskId, envelope.taskId, ctx);
|
|
2650
|
+
channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2601
2651
|
return;
|
|
2602
2652
|
case 'interaction': {
|
|
2603
2653
|
const req = payload.interaction;
|
package/dist/cli/index.js
CHANGED
|
@@ -2102,22 +2102,22 @@ async function cmdWatchAid() {
|
|
|
2102
2102
|
platform.onShutdown(cleanup);
|
|
2103
2103
|
}
|
|
2104
2104
|
async function cmdWatchWeb() {
|
|
2105
|
-
//
|
|
2105
|
+
// evolclaw-web 是独立插件包(可执行命令),按需安装。
|
|
2106
2106
|
// 复用 npm-ops.npmInstallGlobal(含 EACCES→sudo 回退、Windows npm.cmd、超时)。
|
|
2107
2107
|
const { execFileSync } = await import('child_process');
|
|
2108
2108
|
const home = resolvePaths().root;
|
|
2109
|
-
if (!platform.commandExists('
|
|
2110
|
-
process.stdout.write('📦
|
|
2109
|
+
if (!platform.commandExists('evolclaw-web')) {
|
|
2110
|
+
process.stdout.write('📦 evolclaw-web 未安装,正在从 npm 安装...\n');
|
|
2111
2111
|
const { npmInstallGlobal } = await import('../utils/npm-ops.js');
|
|
2112
2112
|
try {
|
|
2113
|
-
await npmInstallGlobal('
|
|
2113
|
+
await npmInstallGlobal('evolclaw-web');
|
|
2114
2114
|
}
|
|
2115
2115
|
catch (e) {
|
|
2116
|
-
process.stderr.write(`❌ 安装
|
|
2116
|
+
process.stderr.write(`❌ 安装 evolclaw-web 失败: ${e?.stderr || e?.message || e}\n 可手动安装: npm install -g evolclaw-web\n`);
|
|
2117
2117
|
process.exit(1);
|
|
2118
2118
|
}
|
|
2119
2119
|
}
|
|
2120
|
-
execFileSync('
|
|
2120
|
+
execFileSync('evolclaw-web', ['--home', home], { stdio: 'inherit' });
|
|
2121
2121
|
}
|
|
2122
2122
|
async function cmdRestartMonitor() {
|
|
2123
2123
|
const p = resolvePaths();
|
|
@@ -135,13 +135,8 @@ export class MessageBridge {
|
|
|
135
135
|
const effectiveProjectPath = owningAgent?.projectPath
|
|
136
136
|
?? this.defaultProjectPath;
|
|
137
137
|
const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType, undefined, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType);
|
|
138
|
-
// 4.
|
|
139
|
-
|
|
140
|
-
if (channelInfo?.policy) {
|
|
141
|
-
const prefix = channelInfo.policy.messagePrefix(chatType, msg.peerName);
|
|
142
|
-
if (prefix)
|
|
143
|
-
content = prefix + content;
|
|
144
|
-
}
|
|
138
|
+
// 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
|
|
139
|
+
// 消息日志因此保存干净原文。policy.messagePrefix 暂保留(未来清理)。
|
|
145
140
|
// 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
|
|
146
141
|
const fullMessage = {
|
|
147
142
|
channel: channelName,
|
|
@@ -11,6 +11,7 @@ import { summarizeToolInput } from '../permission.js';
|
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
13
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
14
|
+
import { renderMessageBody } from '../../agents/message-renderer.js';
|
|
14
15
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
15
16
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
16
17
|
import { formatPeerKey } from '../relation/peer-key.js';
|
|
@@ -31,6 +32,20 @@ function currentTzOffset() {
|
|
|
31
32
|
const abs = Math.abs(off);
|
|
32
33
|
return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
|
|
33
34
|
}
|
|
35
|
+
/** 当前本地日期 YYYY-MM-DD(按运行环境时区)。系统提示词用,一天才变一次(缓存友好)。 */
|
|
36
|
+
function currentLocalDate() {
|
|
37
|
+
const d = new Date();
|
|
38
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
39
|
+
}
|
|
40
|
+
/** 当前本地星期几(中文,如「星期四」)。 */
|
|
41
|
+
function currentWeekday() {
|
|
42
|
+
try {
|
|
43
|
+
return new Intl.DateTimeFormat('zh-CN', { weekday: 'long' }).format(new Date());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][new Date().getDay()];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
34
49
|
function getContextTooLongHint(agent) {
|
|
35
50
|
if (canCompactAgent(agent)) {
|
|
36
51
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -61,6 +76,7 @@ function canCompactAgent(agent) {
|
|
|
61
76
|
export function buildEnvelope(opts) {
|
|
62
77
|
return {
|
|
63
78
|
taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
79
|
+
sessionId: opts.sessionId,
|
|
64
80
|
channel: opts.channel,
|
|
65
81
|
channelId: opts.channelId,
|
|
66
82
|
agentName: opts.agentName ?? '<unknown>',
|
|
@@ -433,6 +449,7 @@ export class MessageProcessor {
|
|
|
433
449
|
const isAutonomous = session.sessionMode === 'autonomous';
|
|
434
450
|
const envelope = buildEnvelope({
|
|
435
451
|
taskId,
|
|
452
|
+
sessionId: session.id,
|
|
436
453
|
channel: message.channel,
|
|
437
454
|
channelId: message.channelId,
|
|
438
455
|
agentName: agentNameForStats,
|
|
@@ -560,9 +577,12 @@ export class MessageProcessor {
|
|
|
560
577
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
561
578
|
const prevInterruptReason = this.interruptedSessions.get(session.id);
|
|
562
579
|
this.interruptedSessions.delete(session.id);
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
580
|
+
const wasInterrupted = prevInterruptReason === 'new_message' && !!session.agentSessionId;
|
|
581
|
+
const wrapPrompt = (body) => wasInterrupted
|
|
582
|
+
? `【新消息插入】\n\n${body}\n\n【请无视之前中断继续处理】`
|
|
583
|
+
: body;
|
|
584
|
+
// 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
|
|
585
|
+
let effectivePrompt = wrapPrompt(message.content);
|
|
566
586
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
567
587
|
let effectiveSystemPrompt;
|
|
568
588
|
let modelOverride;
|
|
@@ -644,6 +664,7 @@ export class MessageProcessor {
|
|
|
644
664
|
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
645
665
|
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
646
666
|
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
667
|
+
KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
|
|
647
668
|
// evolclaw 运行模式:dev=源码仓库 | install=全局安装包
|
|
648
669
|
evolclawMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
|
|
649
670
|
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
@@ -659,9 +680,9 @@ export class MessageProcessor {
|
|
|
659
680
|
peerName: peerName || undefined,
|
|
660
681
|
peerRole: session.identity?.role || 'anonymous',
|
|
661
682
|
peerType: message.peerType || undefined,
|
|
662
|
-
sameDevice: message.sameDevice
|
|
663
|
-
sameNetwork: message.sameNetwork
|
|
664
|
-
sameEgressIp: message.sameEgressIp
|
|
683
|
+
sameDevice: message.sameDevice ?? false,
|
|
684
|
+
sameNetwork: message.sameNetwork ?? false,
|
|
685
|
+
sameEgressIp: message.sameEgressIp ?? false,
|
|
665
686
|
groupId: session.metadata?.groupId || undefined,
|
|
666
687
|
chatType: session.chatType || null,
|
|
667
688
|
channel: currentChannelType || null,
|
|
@@ -678,6 +699,8 @@ export class MessageProcessor {
|
|
|
678
699
|
// 时区(把 ISO 时间戳转本地时间用)+ OS 环境
|
|
679
700
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
|
|
680
701
|
tzOffset: currentTzOffset(),
|
|
702
|
+
localDate: currentLocalDate(),
|
|
703
|
+
weekday: currentWeekday(),
|
|
681
704
|
osInfo: OS_INFO,
|
|
682
705
|
threadId: session.threadId || undefined,
|
|
683
706
|
// Stage 3: sessionKey 持久化字段
|
|
@@ -698,13 +721,39 @@ export class MessageProcessor {
|
|
|
698
721
|
if (kitContext)
|
|
699
722
|
contextParts.push(kitContext);
|
|
700
723
|
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
724
|
+
// 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
|
|
725
|
+
// 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
|
|
726
|
+
let renderResult;
|
|
727
|
+
const hasContent = message.content.trim() || (message.items && message.items.length > 0);
|
|
728
|
+
if (hasContent) {
|
|
729
|
+
try {
|
|
730
|
+
const renderItems = message.items && message.items.length > 0
|
|
731
|
+
? message.items
|
|
732
|
+
: [{
|
|
733
|
+
peerId: message.peerId, peerName: peerName || undefined,
|
|
734
|
+
peerType: message.peerType,
|
|
735
|
+
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
736
|
+
content: message.content, timestamp: message.timestamp,
|
|
737
|
+
images: message.images,
|
|
738
|
+
}];
|
|
739
|
+
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
740
|
+
if (renderResult.body.trim())
|
|
741
|
+
effectivePrompt = wrapPrompt(renderResult.body);
|
|
742
|
+
else
|
|
743
|
+
effectivePrompt = wrapPrompt(message.content);
|
|
744
|
+
}
|
|
745
|
+
catch (e) {
|
|
746
|
+
logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
|
|
747
|
+
effectivePrompt = wrapPrompt(message.content);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
701
750
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
702
751
|
const MAX_RETRIES = 3;
|
|
703
752
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
704
753
|
let streamRegistered = false;
|
|
705
754
|
try {
|
|
706
755
|
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
707
|
-
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
756
|
+
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
708
757
|
agent.registerStream(streamKey, stream);
|
|
709
758
|
streamRegistered = true;
|
|
710
759
|
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|