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.
@@ -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
+ }
@@ -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
- // ecweb 是独立插件包(可执行命令),按需安装。
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('ecweb')) {
2110
- process.stdout.write('📦 ecweb 未安装,正在从 npm 安装...\n');
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('ecweb');
2113
+ await npmInstallGlobal('evolclaw-web');
2114
2114
  }
2115
2115
  catch (e) {
2116
- process.stderr.write(`❌ 安装 ecweb 失败: ${e?.stderr || e?.message || e}\n 可手动安装: npm install -g ecweb\n`);
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('ecweb', ['--home', home], { stdio: 'inherit' });
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. 消息前缀(由 policy 决定)
139
- const channelInfo = this.processor.getChannelInfo?.(channelName);
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 effectivePrompt = prevInterruptReason === 'new_message' && session.agentSessionId
564
- ? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
565
- : message.content;
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 || undefined,
663
- sameNetwork: message.sameNetwork || undefined,
664
- sameEgressIp: message.sameEgressIp || undefined,
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);