evolclaw 3.1.11 → 3.2.0

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.
@@ -95,24 +95,67 @@ export class ClaudeSessionFileAdapter {
95
95
  }
96
96
  return null;
97
97
  }
98
+ // CLI 会话白名单:只有真正由人手发起的终端会话才值得导入。
99
+ // 其它 entrypoint(sdk-ts=EvolClaw 自身、sdk-py=security-guidance 等插件的后台 SDK 会话)
100
+ // 同样把 JSONL 写进项目目录,但不是用户会话,导入它们没有意义。
101
+ static CLI_ENTRYPOINTS = new Set(['cli', 'sdk-cli']);
102
+ // entrypoint 不可变,进程内缓存 filePath→entrypoint,避免重复读文件头部
103
+ entrypointCache = new Map();
104
+ // entrypoint 字段位于首个 user 事件行(实测恒在前 4 行内,cli 会话首次出现 < 8KB)。
105
+ // 插件会话首行 queue-operation 可能携带巨大 diff(数百 KB),把 entrypoint 推到很后面,
106
+ // 因此读取上限设为 32KB:cli 会话必命中,插件会话要么命中 sdk-py、要么读不到 → 一律排除。
107
+ static ENTRYPOINT_SCAN_BYTES = 32 * 1024;
108
+ /** 读取会话文件的 entrypoint,结果缓存在实例内(entrypoint 不可变,无需失效)。 */
109
+ readEntrypoint(filePath) {
110
+ if (this.entrypointCache.has(filePath))
111
+ return this.entrypointCache.get(filePath);
112
+ let fd;
113
+ let result = null;
114
+ try {
115
+ fd = fs.openSync(filePath, 'r');
116
+ const buf = Buffer.allocUnsafe(ClaudeSessionFileAdapter.ENTRYPOINT_SCAN_BYTES);
117
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
118
+ const head = buf.toString('utf-8', 0, bytesRead);
119
+ const m = head.match(/"entrypoint":"([^"]*)"/);
120
+ result = m ? m[1] : null;
121
+ }
122
+ catch {
123
+ // keep null
124
+ }
125
+ finally {
126
+ if (fd !== undefined)
127
+ fs.closeSync(fd);
128
+ }
129
+ this.entrypointCache.set(filePath, result);
130
+ return result;
131
+ }
98
132
  scanCliSessions(projectPath) {
99
133
  const homeDir = os.homedir();
100
134
  const encodedPath = encodePath(projectPath);
101
135
  const sessionDir = path.join(homeDir, '.claude', 'projects', encodedPath);
102
136
  if (!fs.existsSync(sessionDir))
103
137
  return [];
104
- const files = fs.readdirSync(sessionDir)
138
+ const candidates = fs.readdirSync(sessionDir)
105
139
  .filter(f => f.endsWith('.jsonl'))
106
140
  .filter(f => !f.startsWith('agent-'))
107
141
  .map(f => {
108
142
  const filePath = path.join(sessionDir, f);
109
143
  const stat = fs.statSync(filePath);
110
- return { uuid: f.replace('.jsonl', ''), mtime: stat.mtimeMs, size: stat.size };
144
+ return { uuid: f.replace('.jsonl', ''), filePath, mtime: stat.mtimeMs, size: stat.size };
111
145
  })
112
146
  .filter(f => f.size > 0)
113
- .sort((a, b) => b.mtime - a.mtime)
114
- .slice(0, 10);
115
- return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
147
+ .sort((a, b) => b.mtime - a.mtime);
148
+ // 按 mtime 降序惰性判定 entrypoint,凑够 10 个白名单会话即停,避免读取全部文件。
149
+ const result = [];
150
+ for (const f of candidates) {
151
+ const entrypoint = this.readEntrypoint(f.filePath);
152
+ if (entrypoint && ClaudeSessionFileAdapter.CLI_ENTRYPOINTS.has(entrypoint)) {
153
+ result.push({ uuid: f.uuid, mtime: f.mtime });
154
+ if (result.length >= 10)
155
+ break;
156
+ }
157
+ }
158
+ return result;
116
159
  }
117
160
  async listSdkSessions(projectPath) {
118
161
  try {
@@ -0,0 +1,11 @@
1
+ import { resolvePaths } from './paths.js';
2
+ import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
3
+ /** 读 {root}/evolclaw.json。文件不存在返回 {},不报错。 */
4
+ export function loadEvolclawConfig() {
5
+ const raw = atomicReadJson(resolvePaths().evolclawJson);
6
+ return raw ?? {};
7
+ }
8
+ /** 原子写入 {root}/evolclaw.json。调用方负责传完整对象(含要保留的字段)。 */
9
+ export function saveEvolclawConfig(value) {
10
+ atomicWriteJson(resolvePaths().evolclawJson, value);
11
+ }
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-f
3
3
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
4
4
  import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './paths.js';
5
5
  import { resolveAnthropicConfig } from './agents/resolve.js';
6
- import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded } from './config-store.js';
6
+ import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded, migrateProcessConfigIfNeeded } from './config-store.js';
7
+ import { loadEvolclawConfig } from './evolclaw-config.js';
7
8
  import { CONFIG_SCHEMA_VERSION } from './types.js';
8
9
  import dotenv from 'dotenv';
9
10
  import { SessionManager } from './core/session/session-manager.js';
@@ -12,7 +13,7 @@ import { CodexAgentPlugin } from './agents/codex-runner.js';
12
13
  import { GeminiAgentPlugin } from './agents/gemini-runner.js';
13
14
  import { FeishuChannelPlugin } from './channels/feishu.js';
14
15
  import { WechatChannelPlugin } from './channels/wechat.js';
15
- import { AUNChannelPlugin } from './channels/aun.js';
16
+ import { AUNChannel, AUNChannelPlugin } from './channels/aun.js';
16
17
  import { DingtalkChannelPlugin } from './channels/dingtalk.js';
17
18
  import { QQBotChannelPlugin } from './channels/qqbot.js';
18
19
  import { WecomChannelPlugin } from './channels/wecom.js';
@@ -216,6 +217,9 @@ async function main() {
216
217
  // ── 自动迁移 ──
217
218
  migrateIdentitiesIfNeeded();
218
219
  autoMigrateIfNeeded();
220
+ // config.json(ProcessConfig)→ evolclaw.json:必须在任何 getAidStore(AUN 连接)之前,
221
+ // 否则首次读 encryptionSeed 时迁移还没发生。
222
+ migrateProcessConfigIfNeeded();
219
223
  // ── ECK 运行时初始化 ──
220
224
  initEck();
221
225
  // 加载 ECK manifest + 清理旧调试文件
@@ -223,6 +227,13 @@ async function main() {
223
227
  loadKitManifest();
224
228
  // 加载配置(新结构:defaults.json + per-agent config.json)
225
229
  const defaults = loadDefaults() ?? { $schema_version: CONFIG_SCHEMA_VERSION };
230
+ const evolclawCfg = loadEvolclawConfig();
231
+ // 进程级 menu 操作(/system /agent)鉴权:owners 来自 evolclaw.json 顶层。
232
+ // owners 为空时这些操作一律 FORBIDDEN,启动时提示如何配置。
233
+ if (!evolclawCfg.owners || evolclawCfg.owners.length === 0) {
234
+ logger.warn('[startup] evolclaw.json.owners 未配置:进程级 menu 操作(/system /agent)将一律拒绝。' +
235
+ '如需远程管理,请在 evolclaw.json 配置 owners: [<你的 AID>]');
236
+ }
226
237
  // 应用配置中的日志级别(优先于环境变量)
227
238
  // logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
228
239
  // 阶段 2c 暂跳过
@@ -584,6 +595,19 @@ async function main() {
584
595
  if (inst.registerHooks) {
585
596
  inst.registerHooks({ eventBus, sessionManager });
586
597
  }
598
+ // 4c. 观察者模式配置读取器(AUN):从 EvolAgent 的 merged config 读 observable/owners,
599
+ // 不另建缓存——EvolAgent 那份在启动/重启/热重载时统一更新,是唯一真相源。
600
+ const channelForObserver = inst.channel;
601
+ if (typeof channelForObserver.setObserverConfigResolver === 'function') {
602
+ const channelKey = inst.adapter.channelKey;
603
+ channelForObserver.setObserverConfigResolver(() => {
604
+ const owningAgent = agentRegistry.resolveByChannel(channelKey);
605
+ return {
606
+ observable: owningAgent?.getObservable() ?? false,
607
+ owners: owningAgent?.config.owners ?? [],
608
+ };
609
+ });
610
+ }
587
611
  // 5. 撤回消息 → 中断执行中任务
588
612
  inst.channel.onRecall?.((messageId) => {
589
613
  msgBridge.cancel(messageId);
@@ -675,6 +699,27 @@ async function main() {
675
699
  timestamp: Date.now()
676
700
  });
677
701
  }
702
+ // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
703
+ let controlChannel;
704
+ if (evolclawCfg.aid) {
705
+ controlChannel = new AUNChannel({
706
+ aid: evolclawCfg.aid,
707
+ agentName: evolclawCfg.aid,
708
+ channelName: 'control',
709
+ pureIdentity: true,
710
+ aunTrace: evolclawCfg.debug?.aunTrace ?? defaults.debug?.aunTrace,
711
+ aunSdkLog: evolclawCfg.debug?.aunSdkLog ?? defaults.debug?.aunSdkLog,
712
+ });
713
+ // connect() 失败不置空实例:AUNChannel 内部有无限重连(SDK auto_reconnect +
714
+ // scheduleReconnect),首连失败后台会自愈;保留实例供 status 显示 disconnected。
715
+ try {
716
+ await controlChannel.connect();
717
+ logger.info(`✓ 控制 AID 已连接: ${evolclawCfg.aid}`);
718
+ }
719
+ catch (e) {
720
+ logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
721
+ }
722
+ }
678
723
  // 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
679
724
  // 需在配置中 debug.upmsg: true 手动开启
680
725
  setTimeout(() => {
@@ -882,6 +927,9 @@ async function main() {
882
927
  errors: snap.lastHour.errors,
883
928
  avgResponseMs: snap.lastHour.avgResponseMs,
884
929
  },
930
+ controlAid: evolclawCfg.aid
931
+ ? { aid: evolclawCfg.aid, connected: controlChannel?.getAidState().status === 'connected' }
932
+ : undefined,
885
933
  };
886
934
  }, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
887
935
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
@@ -1036,6 +1084,13 @@ async function main() {
1036
1084
  const type = inst.channelType || inst.adapter.channelName;
1037
1085
  eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
1038
1086
  }
1087
+ // 断开控制 AID(daemon 进程身份)
1088
+ if (controlChannel) {
1089
+ try {
1090
+ await controlChannel.disconnect();
1091
+ }
1092
+ catch { /* ignore */ }
1093
+ }
1039
1094
  sessionManager.close();
1040
1095
  removeAll();
1041
1096
  logger.info('✓ Shutdown complete');
package/dist/ipc.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import net from 'net';
2
2
  import fs from 'fs';
3
3
  import { logger } from './utils/logger.js';
4
+ import { fileCache } from './core/cache/file-cache.js';
4
5
  const isWindows = process.platform === 'win32';
5
6
  const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
6
7
  export class IpcServer {
@@ -102,6 +103,11 @@ export class IpcServer {
102
103
  const stats = this.aunAidStatsProvider ? this.aunAidStatsProvider() : [];
103
104
  return { ok: true, stats };
104
105
  }
106
+ case 'cache-stats': {
107
+ // daemon 统一 FileCache 的只读运行统计(watch web Cache 页用)。
108
+ // 直接读单例,无需 provider 注入(与 manifest-engine 等 import 单例同款)。
109
+ return { ok: true, stats: fileCache.stats() };
110
+ }
105
111
  case 'aun-aid-stats-record-outbound': {
106
112
  if (!this.aunAidStatsRecorder)
107
113
  return { ok: false, error: 'recorder not configured' };
package/dist/paths.js CHANGED
@@ -38,7 +38,8 @@ export function resolvePaths() {
38
38
  socket: resolveInstanceSocketPath(root),
39
39
  // ── 新结构(evolclaw-directory-design.md)────────────────
40
40
  defaultsConfig: path.join(root, 'agents', 'defaults.json'),
41
- processConfig: path.join(root, 'config.json'),
41
+ processConfig: path.join(root, 'config.json'), // legacy ProcessConfig path — only for migration source
42
+ evolclawJson: path.join(root, 'evolclaw.json'),
42
43
  eckDir: path.join(root, 'eck'),
43
44
  instanceReadySignal: path.join(root, 'data', 'instance', 'ready.signal'),
44
45
  instanceSocket: resolveInstanceSocketPath(root),
@@ -182,14 +183,17 @@ function migrateFromAun() {
182
183
  catch { }
183
184
  }
184
185
  /**
185
- * 首次运行时从 assets/ 模板拷贝 config.json 和 .env 到 $EVOLCLAW_HOME。
186
+ * 首次运行时从 assets/ 模板拷贝 .env 到 $EVOLCLAW_HOME。
186
187
  * 已存在则跳过,不覆盖用户修改。
188
+ *
189
+ * 注:config.json(旧 ProcessConfig)已废弃——不再 seed,否则会与
190
+ * migrateProcessConfigIfNeeded 的归档形成每次启动重建+重迁移的循环。
191
+ * 进程级配置统一走 evolclaw.json。
187
192
  */
188
193
  function seedConfigTemplates() {
189
194
  const root = resolveRoot();
190
195
  const assetsDir = path.join(getPackageRoot(), 'assets');
191
196
  const templates = [
192
- { src: 'config.json.template', dst: 'config.json' },
193
197
  { src: '.env.template', dst: '.env' },
194
198
  ];
195
199
  for (const { src, dst } of templates) {
@@ -1,2 +1,2 @@
1
- ‹{{now}}{{?chatType=group}} · {{peerName}}{{/}}{{?sameDevice}} · 📍同设备{{/}}{{?sameNetwork}} · 🌐同网络{{/}}{{?sameEgressIp}} · 🔀同出口IP{{/}}›
1
+ ‹{{now}} · from:{{peerId}}{{?peerName}}({{peerName}}){{/}}{{?chatType=group}} → 群:{{groupLabel}} · self:{{selfAid}}{{/}}{{?chatType=private}} → self:{{selfAid}}{{/}}{{?sameDevice}} · 📍同设备{{/}}{{?sameNetwork}} · 🌐同网络{{/}}{{?sameEgressIp}} · 🔀同出口IP{{/}}{{?mentionAids}} · @:{{mentionAids}}{{/}}
2
2
  {{content}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "3.1.11",
3
+ "version": "3.2.0",
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",