evolclaw 2.6.3 → 2.6.4

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.
@@ -130,6 +130,8 @@ export class AgentRunner {
130
130
  return this.permissionMode;
131
131
  }
132
132
  listModes() {
133
+ // readonly 模式暂时禁用:与 proactive 模式系统提示词存在语义冲突,
134
+ // 且 READONLY_WRITE_PATTERNS 未覆盖 evolclaw ctl send/file,契约不稳固
133
135
  return [
134
136
  { key: 'auto', nameZh: '自动', description: 'AI 分类器自动判断', available: true },
135
137
  { key: 'bypass', nameZh: '放行', description: '全部自动放行', available: true },
@@ -137,7 +139,6 @@ export class AgentRunner {
137
139
  { key: 'edit', nameZh: '编辑', description: '自动接受编辑,其他询问', available: true },
138
140
  { key: 'plan', nameZh: '规划', description: '只规划不执行', available: true },
139
141
  { key: 'noask', nameZh: '静默', description: '未批准则拒绝', available: true },
140
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
141
142
  ];
142
143
  }
143
144
  setPermissionGateway(gateway) {
@@ -77,7 +77,6 @@ export class CodexRunner {
77
77
  { key: 'bypass', nameZh: '放行', description: '全部自动(受 sandbox 约束)', available: true },
78
78
  { key: 'request', nameZh: '审批', description: '需要审批时询问', available: true },
79
79
  { key: 'noask', nameZh: '静默', description: '只执行已知安全操作', available: true },
80
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
81
80
  ];
82
81
  }
83
82
  setSendPrompt(_fn) { }
@@ -70,7 +70,6 @@ export class GeminiRunner {
70
70
  { key: 'edit', nameZh: '编辑', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
71
71
  { key: 'plan', nameZh: '规划', description: 'Gemini 规划模式', available: true },
72
72
  { key: 'noask', nameZh: '静默', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
73
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
74
73
  ];
75
74
  }
76
75
  setSendPrompt(_fn) { }
@@ -121,6 +120,9 @@ export class GeminiRunner {
121
120
  if (this.currentMode === 'plan') {
122
121
  args.push('--approval-mode=plan');
123
122
  }
123
+ else if (this.currentMode === 'noask') {
124
+ args.push('--approval-mode=default');
125
+ }
124
126
  else {
125
127
  args.push('--yolo');
126
128
  }
@@ -877,15 +877,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
877
877
  encrypt: true,
878
878
  };
879
879
  try {
880
+ const stage = payload?.stage ?? 'unknown';
880
881
  if (this.isGroupId(channelId)) {
881
882
  params.group_id = targetId;
882
883
  this.trace('OUT', 'group.thought.put', params);
883
884
  await this.client.call('group.thought.put', params);
885
+ logger.debug(`[AUN] thought.put ok group=${targetId} task=${taskId} stage=${stage}`);
884
886
  }
885
887
  else {
886
888
  params.to = targetId;
887
889
  this.trace('OUT', 'message.thought.put', params);
888
890
  await this.client.call('message.thought.put', params);
891
+ logger.debug(`[AUN] thought.put ok p2p=${targetId} task=${taskId} stage=${stage}`);
889
892
  }
890
893
  }
891
894
  catch (e) {
@@ -1095,6 +1098,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1095
1098
  this.trace('OUT', 'message.send.status', params);
1096
1099
  sendWithFallback('message.send');
1097
1100
  }
1101
+ logger.info(`[AUN] task.${status} task=${taskId} session=${sessionId} target=${channelId}`);
1098
1102
  }
1099
1103
  sendCustomPayload(channelId, payload) {
1100
1104
  if (!this.client || !this.connected)
@@ -701,7 +701,7 @@ export class CommandHandler {
701
701
  if (!hasPermissionController(permAgent)) {
702
702
  return '❌ 权限控制不可用';
703
703
  }
704
- const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
704
+ const defaultPermMode = identity.role === 'owner' ? 'bypass' : identity.role === 'admin' ? 'auto' : 'noask';
705
705
  const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
706
706
  const modes = permAgent.listModes();
707
707
  // 尝试发送交互卡片
@@ -10,6 +10,7 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
10
10
  import { summarizeToolInput } from '../permission.js';
11
11
  import { getOwner } from '../../config.js';
12
12
  import { getPackageRoot, resolveRoot } from '../../paths.js';
13
+ import { renderPromptSection } from '../../prompts/templates.js';
13
14
  /**
14
15
  * 统一消息处理器
15
16
  * 负责处理来自不同渠道的消息,协调事件流处理
@@ -313,7 +314,7 @@ export class MessageProcessor {
313
314
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
314
315
  const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
315
316
  logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
316
- logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
317
+ logger.info(`[MessageProcessor] session=${session.id} task=${taskId} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
317
318
  // 记录开始处理
318
319
  this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
319
320
  adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
@@ -353,6 +354,9 @@ export class MessageProcessor {
353
354
  }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
354
355
  // 保存当前 flusher,用于 compact 事件
355
356
  this.currentFlusher = flusher;
357
+ if (isProactive) {
358
+ logger.info(`[MessageProcessor] proactive mode: flusher silent, outputs via thought.put task=${taskId}`);
359
+ }
356
360
  // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
357
361
  // selector: context = { type: 'task', id: taskId }
358
362
  if (isProactive && adapter.putThought) {
@@ -379,9 +383,9 @@ export class MessageProcessor {
379
383
  ? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
380
384
  : undefined,
381
385
  });
382
- // 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → readonly
386
+ // 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → noask
383
387
  const role = session.identity?.role;
384
- const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'readonly';
388
+ const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'noask';
385
389
  agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
386
390
  // 标记会话为处理中(实时持久化,重启后可恢复)
387
391
  this.sessionManager.markProcessing(session.id);
@@ -396,8 +400,7 @@ export class MessageProcessor {
396
400
  // 动态构建运行时上下文提示
397
401
  const contextParts = [];
398
402
  const currentChannelType = options?.channelType || message.channel;
399
- // 1. 当前环境信息
400
- const peerLabel = session.identity?.role || 'unknown';
403
+ // 1. 构建模板变量并渲染 runtime 段
401
404
  const peerName = message.peerName || session.metadata?.peerName;
402
405
  const peerType = message.peerType;
403
406
  const peerId = message.peerId;
@@ -411,52 +414,20 @@ export class MessageProcessor {
411
414
  };
412
415
  const selfIdentity = formatIdentity(selfName, selfAid);
413
416
  const peerIdentity = formatIdentity(peerName, peerId);
414
- const envParts = [
415
- `会话通道: ${currentChannelType}`,
416
- `当前项目: ${path.basename(absoluteProjectPath)}`,
417
- ];
418
- if (session.name)
419
- envParts.push(`会话名称: ${session.name}`);
420
- if (selfIdentity)
421
- envParts.push(`当前名称: ${selfIdentity}`);
422
- envParts.push(`对端身份: ${peerLabel}`);
423
- if (peerIdentity)
424
- envParts.push(`对端名称: ${peerIdentity}`);
425
- if (peerType && peerType !== 'unknown')
426
- envParts.push(`对端类型: ${peerType}`);
427
- if (session.chatType)
428
- envParts.push(`聊天类型: ${session.chatType}`);
429
- if (session.agentId && session.agentId !== 'claude')
430
- envParts.push(`当前Agent: ${session.agentId}`);
431
- contextParts.push(`[当前环境] ${envParts.join(' | ')}`);
432
- // 只读模式提示
433
- if (session.metadata?.permissionMode === 'readonly') {
434
- const sendHint = isProactive
435
- ? '使用 evolclaw ctl file 发送'
436
- : '使用 [SEND_FILE:] 发送';
437
- contextParts.push(`[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后${sendHint}`);
438
- }
439
- // 2. 文件发送能力(按 channelType 去重,提示词只展示第一级通道名)
440
- // proactive 模式:不推送 [SEND_FILE:] 提示,统一通过 evolclaw ctl file 显式发送(与 ctl send 契约一致)
417
+ // 文件发送能力(按 channelType 去重)
418
+ let crossChannelTypes = [];
419
+ let currentCanSend = false;
441
420
  if (!isProactive) {
442
421
  const fileChannelTypes = new Set();
443
- const currentCanSend = !!channelInfo.adapter.sendFile;
422
+ currentCanSend = !!channelInfo.adapter.sendFile;
444
423
  for (const [, info] of this.channels) {
445
424
  if (info.adapter.sendFile) {
446
425
  fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
447
426
  }
448
427
  }
449
- const crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
450
- if (currentCanSend || crossChannelTypes.length > 0) {
451
- const hints = [];
452
- if (currentCanSend)
453
- hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
454
- if (crossChannelTypes.length > 0)
455
- hints.push(`[SEND_FILE:${crossChannelTypes[0]}:路径] 发送文件到指定通道(可用: ${crossChannelTypes.join('/')})`);
456
- contextParts.push(hints.join(','));
457
- }
428
+ crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
458
429
  }
459
- // 3. 当前通道能力
430
+ // 通道能力
460
431
  const capParts = [];
461
432
  if (options?.supportsImages)
462
433
  capParts.push('图片输入');
@@ -464,30 +435,32 @@ export class MessageProcessor {
464
435
  capParts.push('图片输出');
465
436
  if (channelInfo.adapter.sendFile)
466
437
  capParts.push('文件发送');
467
- if (capParts.length > 0) {
468
- contextParts.push(`[通道能力] ${capParts.join('、')}`);
469
- }
470
- // 4. 群聊 @ 规则:告知 agent 应该 @ 谁,由 agent 自行在回复中添加
438
+ contextParts.push(renderPromptSection('runtime', {
439
+ channel: currentChannelType,
440
+ project: path.basename(absoluteProjectPath),
441
+ sessionName: session.name || '',
442
+ selfIdentity: selfIdentity || '',
443
+ peerRole: session.identity?.role || 'unknown',
444
+ peerIdentity: peerIdentity || '',
445
+ peerType: peerType && peerType !== 'unknown' ? peerType : '',
446
+ chatType: session.chatType || '',
447
+ agent: session.agentId && session.agentId !== 'claude' ? session.agentId : '',
448
+ readonly: session.metadata?.permissionMode === 'readonly',
449
+ readonlySendHint: isProactive ? '使用 evolclaw ctl file 发送' : '使用 [SEND_FILE:] 发送',
450
+ fileSendCurrent: !isProactive && currentCanSend,
451
+ fileSendCross: !isProactive && crossChannelTypes.length > 0,
452
+ crossPrimary: crossChannelTypes[0] || '',
453
+ crossTypes: crossChannelTypes.join('/'),
454
+ capability: capParts.length > 0,
455
+ capabilities: capParts.join('、'),
456
+ }));
457
+ // 2. 群聊 @ 规则
471
458
  if (message.chatType === 'group' && message.peerId) {
472
- contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
459
+ contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
473
460
  }
474
- // 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
475
- // 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
476
- // 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
477
- // if (!this.skillsEnsured) {
478
- // this.ensureSkillsFile();
479
- // this.skillsEnsured = true;
480
- // }
481
- // const skillsHint = this.getSkillsHint();
482
- // if (skillsHint) {
483
- // contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
484
- // }
485
- // 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
461
+ // 3. Proactive 模式提示词
486
462
  if (isProactive) {
487
- contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
488
- '- 发送文本:evolclaw ctl send "<消息内容>"\n' +
489
- '- 发送文件:evolclaw ctl file <路径>\n' +
490
- '可多次调用。如不调用,用户将看不到任何回复。');
463
+ contextParts.push(renderPromptSection('proactive', {}));
491
464
  }
492
465
  const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
493
466
  // 可重试错误(403/429/5xx)指数退避重试,最多 3 次
@@ -823,8 +796,24 @@ export class MessageProcessor {
823
796
  // 每收到事件重置空闲超时
824
797
  const toolName = event.type === 'tool_use' ? event.name : undefined;
825
798
  resetTimer(event.type, toolName);
826
- // 记录所有事件类型
827
- logger.info(`[MessageProcessor] Event: type=${event.type}`);
799
+ // 记录所有事件类型(text / tool_use 附带摘要,便于排查)
800
+ let eventDetail = '';
801
+ if (event.type === 'text' && event.text) {
802
+ const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
803
+ eventDetail = ` text="${preview}${event.text.length > 80 ? '…' : ''}"`;
804
+ }
805
+ else if (event.type === 'tool_use') {
806
+ const input = event.input;
807
+ const desc = input?.description
808
+ || input?.file_path
809
+ || input?.pattern
810
+ || (typeof input?.command === 'string' ? input.command.slice(0, 80) : '')
811
+ || (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
812
+ || (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
813
+ || '';
814
+ eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
815
+ }
816
+ logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
828
817
  // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
829
818
  if (thoughtEmitter) {
830
819
  thoughtEmitter.emit(event).catch(() => { });
@@ -23,6 +23,7 @@ export class ThoughtEmitter {
23
23
  this.channelId = channelId;
24
24
  this.taskId = taskId;
25
25
  this.chatmode = chatmode;
26
+ logger.info(`[ThoughtEmitter] created channel=${channelId} task=${taskId} chatmode=${chatmode}`);
26
27
  }
27
28
  async emit(event) {
28
29
  // 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
@@ -315,6 +315,26 @@ export class SessionManager {
315
315
  logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
316
316
  }
317
317
  }
318
+ // Migration: readonly 模式已暂时禁用,历史会话统一转为 noask
319
+ if (hasMetadata && tableInfo.length > 0) {
320
+ const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
321
+ let migratedPerm = 0;
322
+ for (const row of rows) {
323
+ try {
324
+ const meta = JSON.parse(row.metadata);
325
+ if (meta.permissionMode === 'readonly') {
326
+ meta.permissionMode = 'noask';
327
+ this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
328
+ .run(JSON.stringify(meta), row.id);
329
+ migratedPerm++;
330
+ }
331
+ }
332
+ catch { /* skip malformed JSON */ }
333
+ }
334
+ if (migratedPerm > 0) {
335
+ logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode readonly → noask`);
336
+ }
337
+ }
318
338
  // 创建新表(首次初始化)
319
339
  this.db.exec(`
320
340
  CREATE TABLE IF NOT EXISTS sessions (
@@ -451,7 +471,7 @@ export class SessionManager {
451
471
  session.identity = this.resolveIdentity(channel, userId);
452
472
  // 新话题会话补写默认权限模式
453
473
  if (session.metadata && !session.metadata.permissionMode) {
454
- session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'readonly';
474
+ session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
455
475
  this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
456
476
  .run(JSON.stringify(session.metadata), Date.now(), session.id);
457
477
  }
@@ -562,7 +582,7 @@ export class SessionManager {
562
582
  session.identity = this.resolveIdentity(channel, userId);
563
583
  // 写入默认权限模式(基于角色,只在首次创建时设置)
564
584
  if (!sessionMetadata.permissionMode) {
565
- sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : 'readonly';
585
+ sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
566
586
  }
567
587
  this.insertSession(session);
568
588
  this.eventBus.publish({
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { ChannelLoader } from './core/channel-loader.js';
25
25
  import { AgentLoader } from './core/agent-loader.js';
26
26
  import { IpcServer } from './ipc.js';
27
27
  import { logger } from './utils/logger.js';
28
+ import { loadPromptTemplates } from './prompts/templates.js';
28
29
  import path from 'path';
29
30
  import fs from 'fs';
30
31
  async function main() {
@@ -48,6 +49,8 @@ async function main() {
48
49
  logger.info('EvolClaw starting...');
49
50
  // 确保数据目录存在
50
51
  ensureDataDirs();
52
+ // 加载提示词模板
53
+ loadPromptTemplates();
51
54
  // 加载配置
52
55
  const config = loadConfig();
53
56
  const paths = resolvePaths();
@@ -0,0 +1,122 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getPackageRoot, resolveRoot } from '../paths.js';
4
+ import { logger } from '../utils/logger.js';
5
+ const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive']);
6
+ const SECTION_RE = /^##\s+(\w+)\s*$/;
7
+ let sections = null;
8
+ let builtinSections = null;
9
+ function parseTemplate(content) {
10
+ const result = new Map();
11
+ let currentSection = null;
12
+ let currentLines = [];
13
+ for (const line of content.split('\n')) {
14
+ // Stop parsing at horizontal rule separator (documentation follows)
15
+ if (/^---\s*$/.test(line)) {
16
+ if (currentSection) {
17
+ result.set(currentSection, currentLines.join('\n').trim());
18
+ }
19
+ break;
20
+ }
21
+ const m = line.match(SECTION_RE);
22
+ if (m) {
23
+ if (currentSection) {
24
+ result.set(currentSection, currentLines.join('\n').trim());
25
+ }
26
+ const name = m[1];
27
+ if (KNOWN_SECTIONS.has(name)) {
28
+ currentSection = name;
29
+ currentLines = [];
30
+ }
31
+ else {
32
+ currentSection = null;
33
+ currentLines = [];
34
+ }
35
+ }
36
+ else if (currentSection) {
37
+ currentLines.push(line);
38
+ }
39
+ }
40
+ if (currentSection) {
41
+ result.set(currentSection, currentLines.join('\n').trim());
42
+ }
43
+ return result;
44
+ }
45
+ function loadBuiltinTemplate() {
46
+ const builtinPath = path.join(getPackageRoot(), 'dist', 'templates', 'prompts.md');
47
+ const srcPath = path.join(getPackageRoot(), 'src', 'templates', 'prompts.md');
48
+ const filePath = fs.existsSync(builtinPath) ? builtinPath : srcPath;
49
+ const content = fs.readFileSync(filePath, 'utf-8');
50
+ return parseTemplate(content);
51
+ }
52
+ export function loadPromptTemplates() {
53
+ builtinSections = loadBuiltinTemplate();
54
+ const userPath = path.join(resolveRoot(), 'data', 'prompts.md');
55
+ if (fs.existsSync(userPath)) {
56
+ try {
57
+ const content = fs.readFileSync(userPath, 'utf-8');
58
+ const parsed = parseTemplate(content);
59
+ sections = new Map(builtinSections);
60
+ for (const [key, value] of parsed) {
61
+ sections.set(key, value);
62
+ }
63
+ logger.info(`[PromptTemplates] Loaded user override: ${userPath}`);
64
+ }
65
+ catch (err) {
66
+ logger.warn(`[PromptTemplates] Failed to load user override (${userPath}), using builtin:`, err);
67
+ sections = builtinSections;
68
+ }
69
+ }
70
+ else {
71
+ sections = builtinSections;
72
+ logger.info(`[PromptTemplates] Using builtin templates`);
73
+ }
74
+ for (const name of KNOWN_SECTIONS) {
75
+ if (!sections.has(name)) {
76
+ logger.warn(`[PromptTemplates] Section "${name}" missing, using builtin fallback`);
77
+ const fallback = builtinSections.get(name);
78
+ if (fallback)
79
+ sections.set(name, fallback);
80
+ }
81
+ }
82
+ }
83
+ function isTruthy(val) {
84
+ if (val === undefined || val === null || val === false || val === '' || val === 0)
85
+ return false;
86
+ return true;
87
+ }
88
+ function renderTemplate(template, vars) {
89
+ // Pass 1: conditional sections {{?key}}...{{/}}
90
+ let result = template.replace(/\{\{\?(\w+)\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, body) => {
91
+ return isTruthy(vars[key]) ? body : '';
92
+ });
93
+ // Pass 2: variable substitution {{key}}
94
+ result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
95
+ const val = vars[key];
96
+ if (!isTruthy(val))
97
+ return '';
98
+ return String(val);
99
+ });
100
+ // Pass 3: remove blank lines
101
+ return result.split('\n').filter(line => line.trim() !== '').join('\n');
102
+ }
103
+ export function renderPromptSection(section, vars) {
104
+ if (!sections)
105
+ loadPromptTemplates();
106
+ const template = sections.get(section);
107
+ if (!template) {
108
+ logger.warn(`[PromptTemplates] Section "${section}" not found`);
109
+ return '';
110
+ }
111
+ return renderTemplate(template, vars);
112
+ }
113
+ /** Reset loaded templates (for testing) */
114
+ export function _resetTemplates() {
115
+ sections = null;
116
+ builtinSections = null;
117
+ }
118
+ /** Load templates from a raw string (for testing) */
119
+ export function _loadFromString(content) {
120
+ builtinSections = parseTemplate(content);
121
+ sections = builtinSections;
122
+ }
@@ -0,0 +1,103 @@
1
+ # EvolClaw 运行时系统提示模板
2
+
3
+ # 本文件定义 LLM 每次收到消息时注入到 system prompt 尾部的三段内容。
4
+ # 修改后执行 `evolclaw restart` 生效。
5
+ # 放在 {EVOLCLAW_HOME}/data/prompts.md 会覆盖内置默认。
6
+
7
+ ## runtime
8
+
9
+ [当前环境] 会话通道: {{channel}} | 当前项目: {{project}}{{?sessionName}} | 会话名称: {{sessionName}}{{/}}{{?selfIdentity}} | 当前名称: {{selfIdentity}}{{/}} | 对端身份: {{peerRole}}{{?peerIdentity}} | 对端名称: {{peerIdentity}}{{/}}{{?peerType}} | 对端类型: {{peerType}}{{/}}{{?chatType}} | 聊天类型: {{chatType}}{{/}}{{?agent}} | 当前Agent: {{agent}}{{/}}
10
+ {{?readonly}}[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后{{readonlySendHint}}{{/}}
11
+ {{?fileSendCurrent}}[SEND_FILE:路径] 发送文件到当前通道{{/}}
12
+ {{?fileSendCross}}[SEND_FILE:{{crossPrimary}}:路径] 发送文件到指定通道(可用: {{crossTypes}}){{/}}
13
+ {{?capability}}[通道能力] {{capabilities}}{{/}}
14
+
15
+ ## group
16
+
17
+ [群聊回复规则] 回复时必须在开头添加 @{{peerId}} 来通知对方
18
+
19
+ ## proactive
20
+
21
+ [Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:
22
+ - 发送文本:evolclaw ctl send "<消息内容>"
23
+ - 发送文件:evolclaw ctl file <路径>
24
+ 可多次调用。如不调用,用户将看不到任何回复。
25
+
26
+
27
+ ---
28
+
29
+ ## 格式说明
30
+
31
+ 模板由多个以 `## 段名` 分隔的段组成,加载器只识别 `runtime`、`group`、`proactive` 三段,其它段(包括本说明)会被忽略,可以随意增删。
32
+
33
+ **占位符语法:**
34
+
35
+ | 语法 | 作用 | 示例 |
36
+ |---|---|---|
37
+ | `{{var}}` | 变量替换。值为空串/undefined/null/false 时替换为空 | `{{project}}` → `evolclaw` |
38
+ | `{{?var}}...{{/}}` | 条件段。var 为真值时保留整段(含字面量),否则整段删除。段内可嵌套 `{{var}}` | `{{?peerId}} | @{{peerId}}{{/}}` |
39
+ | 空行 | 渲染后若某行只剩空白,整行自动删除 | 条件段删完后的空行会消失 |
40
+
41
+ **注入时机:**
42
+
43
+ | 段 | 触发条件 | 说明 |
44
+ |---|---|---|
45
+ | `runtime` | 每次消息 | 每条用户消息都会注入 |
46
+ | `group` | `chatType === 'group' && peerId` | 仅群聊消息注入 |
47
+ | `proactive` | `sessionMode === 'proactive'` | 仅 proactive 会话注入 |
48
+
49
+ 三段以换行拼接,追加到该消息的 system prompt 末尾。
50
+
51
+ ---
52
+
53
+ ## 参数说明
54
+
55
+ ### runtime 段
56
+
57
+ | 字段 | 类型 | 说明 | 示例 |
58
+ |---|---|---|---|
59
+ | `channel` | string | 当前通道类型 | `feishu` / `wechat` / `aun` |
60
+ | `project` | string | 当前项目目录名(非完整路径) | `evolclaw` |
61
+ | `sessionName` | string? | 会话名(用户通过 `/name` 设置) | `CLI开发` |
62
+ | `selfIdentity` | string? | 机器人自身标识「名称 (ID)」 | `Evol (evolai.xxx.pub)` |
63
+ | `peerRole` | string | 对端角色 | `owner` / `admin` / `guest` / `unknown` |
64
+ | `peerIdentity` | string? | 对端标识「名称 (ID)」 | `张三 (u_abc)` |
65
+ | `peerType` | string? | 对端类型(`unknown` 时为空) | `user` / `group` |
66
+ | `chatType` | string? | 聊天类型 | `private` / `group` |
67
+ | `agent` | string? | 当前 agent(`claude` 时为空不显示) | `hermes` / `gemini` |
68
+ | `readonly` | bool | 是否只读模式(触发只读行) | `true` / `false` |
69
+ | `readonlySendHint` | string | 只读模式下提示使用的发送方式 | `使用 [SEND_FILE:] 发送` |
70
+ | `fileSendCurrent` | bool | 当前通道是否支持发文件(触发该行) | `true` / `false` |
71
+ | `fileSendCross` | bool | 是否存在可跨通道发文件的其它通道 | `true` / `false` |
72
+ | `crossPrimary` | string | 跨通道发送示例用的首选通道 | `wechat` |
73
+ | `crossTypes` | string | 所有支持跨通道发送的通道列表 | `wechat/aun` |
74
+ | `capability` | bool | 是否有任何通道能力要展示(触发通道能力行) | `true` / `false` |
75
+ | `capabilities` | string | 通道能力清单 | `图片输入、图片输出、文件发送` |
76
+
77
+ ### group 段
78
+
79
+ | 字段 | 类型 | 说明 | 示例 |
80
+ |---|---|---|---|
81
+ | `peerId` | string | 对端用户 ID(@ 所需) | `ou_xxx` / `wxid_xxx` |
82
+
83
+ ### proactive 段
84
+
85
+ 无参数。
86
+
87
+ ---
88
+
89
+ ## 修改示例
90
+
91
+ **只改文案,不改结构:**
92
+
93
+ ```
94
+ ## runtime
95
+ 当前项目 {{project}},你正在和 {{peerIdentity}} 对话。
96
+ {{?readonly}}⚠ 只读模式,不要修改代码{{/}}
97
+ ```
98
+
99
+ **关闭某一行:** 模板里删掉那一行即可。内置条件段(如只读提示)删了之后,只读模式就不再在 system prompt 里出现(但权限拦截依然生效)。
100
+
101
+ **追加自定义规则:** 直接在对应段里加行文本,不需要占位符。
102
+
103
+ **用英文:** 所有文案重写成英文即可,字段含义不变。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.6.3",
3
+ "version": "2.6.4",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",