evolclaw 3.1.5 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +68 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +78 -321
  4. package/dist/agents/manifest-engine.js +243 -0
  5. package/dist/agents/message-renderer.js +112 -0
  6. package/dist/aun/aid/agentmd.js +10 -3
  7. package/dist/aun/msg/group.js +2 -2
  8. package/dist/channels/aun.js +154 -18
  9. package/dist/channels/dingtalk.js +1 -1
  10. package/dist/channels/feishu.js +31 -9
  11. package/dist/channels/qqbot.js +1 -1
  12. package/dist/channels/wechat.js +1 -1
  13. package/dist/channels/wecom.js +1 -1
  14. package/dist/cli/agent.js +10 -11
  15. package/dist/cli/bench.js +1 -5
  16. package/dist/cli/help.js +8 -0
  17. package/dist/cli/index.js +91 -128
  18. package/dist/cli/init.js +37 -21
  19. package/dist/cli/link-rules.js +1 -7
  20. package/dist/cli/model.js +231 -6
  21. package/dist/config-store.js +1 -22
  22. package/dist/core/command-handler.js +181 -48
  23. package/dist/core/evolagent.js +0 -18
  24. package/dist/core/message/im-renderer.js +9 -20
  25. package/dist/core/message/message-bridge.js +9 -10
  26. package/dist/core/message/message-processor.js +188 -39
  27. package/dist/core/message/message-queue.js +15 -1
  28. package/dist/core/relation/peer-identity.js +23 -11
  29. package/dist/core/trigger/parser.js +4 -4
  30. package/dist/core/trigger/scheduler.js +43 -13
  31. package/dist/index.js +102 -52
  32. package/dist/ipc.js +1 -1
  33. package/dist/utils/error-utils.js +6 -0
  34. package/dist/utils/process-introspect.js +7 -5
  35. package/kits/docs/INDEX.md +4 -8
  36. package/kits/docs/context-assembly.md +1 -0
  37. package/kits/docs/evolclaw/INDEX.md +43 -0
  38. package/kits/docs/evolclaw/group.md +13 -6
  39. package/kits/docs/evolclaw/model.md +51 -0
  40. package/kits/docs/evolclaw/msg.md +5 -0
  41. package/kits/docs/venues/group.md +13 -1
  42. package/kits/eck_manifest.json +9 -0
  43. package/kits/eck_message_manifest.json +14 -0
  44. package/kits/rules/06-channel.md +5 -1
  45. package/kits/templates/message-fragments/item.md +2 -0
  46. package/kits/templates/system-fragments/baseagent.md +7 -1
  47. package/kits/templates/system-fragments/channel.md +7 -5
  48. package/kits/templates/system-fragments/commands.md +19 -0
  49. package/kits/templates/system-fragments/session.md +12 -0
  50. package/kits/templates/system-fragments/venue.md +15 -0
  51. package/package.json +3 -3
@@ -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,112 @@
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
+ now: formatLocalTime(item.timestamp ?? Date.now(), sessionVars.timezone ? String(sessionVars.timezone) : undefined),
45
+ // content held as a per-call random sentinel, swapped back post-render.
46
+ // Using a UUID means no real message can collide with it.
47
+ content: contentSentinel,
48
+ };
49
+ const out = [];
50
+ for (const section of sections) {
51
+ if (section.enabled === false)
52
+ continue;
53
+ if (!evaluateWhen(section.when, itemVars))
54
+ continue;
55
+ const files = loadSectionFiles(section, itemVars, sessionCache);
56
+ for (const [, rawContent] of files) {
57
+ const rendered = section.needsInjection
58
+ ? renderTemplate(rawContent, itemVars, /* stripBlankLines */ false)
59
+ : rawContent;
60
+ // swap the sentinel back to the real message text (literal replace, no parsing).
61
+ const withContent = rendered.split(contentSentinel).join(item.content);
62
+ if (withContent.trim())
63
+ out.push(withContent.replace(/\s+$/, ''));
64
+ }
65
+ }
66
+ // if the manifest produced nothing, fall back to raw text -- never drop a message.
67
+ return out.length > 0 ? out.join('\n') : item.content;
68
+ }
69
+ /**
70
+ * Render each sub-message then assemble the final message body.
71
+ * Also collects all images across items in order, preserving per-item attribution.
72
+ *
73
+ * One item -> single render; many (group batch / same-peer merge) -> each carries
74
+ * its own sender and timestamp.
75
+ */
76
+ export function renderMessageBody(items, sessionVars, sessionId) {
77
+ if (!items || items.length === 0)
78
+ return { body: '', images: [] };
79
+ // One random sentinel per renderMessageBody call -- impossible for user text to match.
80
+ const contentSentinel = `\x00ECMSG-${randomUUID()}\x00`;
81
+ const sessionCache = new Map(); // render-local cache (template files are small & fixed)
82
+ const renderedParts = [];
83
+ const allImages = [];
84
+ for (const item of items) {
85
+ renderedParts.push(renderOneItem(item, sessionVars, sessionCache, contentSentinel));
86
+ if (item.images && item.images.length > 0)
87
+ allImages.push(...item.images);
88
+ }
89
+ const body = renderedParts.join('\n\n');
90
+ writeMessageDebug(sessionId, items, body);
91
+ return { body, images: allImages };
92
+ }
93
+ // ── Debug ──
94
+ function writeMessageDebug(sessionId, items, body) {
95
+ try {
96
+ const ts = new Date().toISOString().replace(/[T:.]/g, '-').slice(0, 19);
97
+ const out = [
98
+ `# Message Render`,
99
+ `- sessionId: ${sessionId}`,
100
+ `- items: ${items.length}`,
101
+ `- images: ${items.reduce((n, i) => n + (i.images?.length ?? 0), 0)}`,
102
+ ``,
103
+ `## Rendered body`,
104
+ ``,
105
+ body,
106
+ ].join('\n');
107
+ fs.writeFile(path.join(eckDebugDir(), `msg-render-${ts}.md`), out, () => { });
108
+ }
109
+ catch (e) {
110
+ logger.debug(`[MessageRenderer] debug write failed: ${e}`);
111
+ }
112
+ }
@@ -110,6 +110,10 @@ export async function agentmdPut(content, opts) {
110
110
  * Check if agent.md is up-to-date (30-day TTL), fetch if changed.
111
111
  * Returns changed=true + content when a new version was downloaded.
112
112
  *
113
+ * verification 透传 SDK 的验签结果:
114
+ * - downloaded(changed=true)时为 SDK downloadAgentMd 的 verification
115
+ * - 命中本地缓存或网络失败 fallback 时为 undefined(调用方需自行离线验签或视为未验证)
116
+ *
113
117
  * Note: store.checkAgentMd tracks freshness via the store's in-memory cache,
114
118
  * so a freshly-built store reports local_found=false and will fetch.
115
119
  */
@@ -120,16 +124,19 @@ export async function agentmdSync(aid, opts) {
120
124
  const localPath = agentMdPath(aid);
121
125
  try {
122
126
  const check = await store.checkAgentMd(aid, 30);
123
- // In sync (cache fresh) — return local file content unchanged.
127
+ // In sync (cache fresh) — return local file content with cached verification status.
124
128
  if (check.ok && !check.data.needs_update && check.data.local_found) {
125
129
  const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
126
- return { changed: false, content };
130
+ const verification = check.data.verify_status
131
+ ? normalizeVerification({ status: check.data.verify_status, reason: check.data.verify_error || undefined })
132
+ : undefined;
133
+ return { changed: false, content, verification };
127
134
  }
128
135
  // Needs update (or check failed) — fetch fresh content.
129
136
  // SDK's downloadAgentMd persists to disk internally (AgentMdManager.saveRecord → writeContent).
130
137
  const fetched = await store.downloadAgentMd(aid);
131
138
  if (fetched.ok) {
132
- return { changed: true, content: fetched.data.content };
139
+ return { changed: true, content: fetched.data.content, verification: normalizeVerification(fetched.data.verification) };
133
140
  }
134
141
  // Fetch failed (network) — fall back to local file if present.
135
142
  const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
@@ -70,11 +70,11 @@ export async function groupPull(args) {
70
70
  export async function groupAck(args) {
71
71
  const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
72
72
  try {
73
- const result = await conn.call('group.ack', { group_id: args.groupId, seq: args.seq });
73
+ const result = await conn.call('group.ack', { group_id: args.groupId, msg_seq: args.seq });
74
74
  return {
75
75
  ok: true,
76
76
  group_id: result?.group_id ?? args.groupId,
77
- ack_seq: result?.ack_seq ?? args.seq,
77
+ ack_seq: result?.cursor ?? result?.ack_seq ?? args.seq,
78
78
  latest_message_seq: result?.latest_message_seq,
79
79
  };
80
80
  }