evolclaw 3.1.11 → 3.3.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.
- package/CHANGELOG.md +41 -0
- package/README.md +27 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +538 -325
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +98 -151
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +44 -13
- package/dist/cli/index.js +207 -46
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +192 -85
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +48 -11
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +754 -172
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/evolagent-registry.js +4 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +215 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +52 -22
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +336 -68
- package/dist/core/message/message-queue.js +15 -8
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +40 -7
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/kit-renderer.js +5 -1
- package/dist/{agents → eck}/manifest-engine.js +127 -35
- package/dist/{agents → eck}/message-renderer.js +26 -1
- package/dist/index.js +185 -8
- package/dist/ipc.js +22 -0
- package/dist/paths.js +7 -3
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/utils/channel-helpers.js +0 -46
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileCache —— 统一的文件缓存(daemon-only)。
|
|
3
|
+
*
|
|
4
|
+
* daemon 每条消息处理时读大量文件(manifest、fragment、md、working memory、
|
|
5
|
+
* 关系级 preferences 等)。本类统一缓存"文件 → 解析后内容",按策略门控变化检查。
|
|
6
|
+
*
|
|
7
|
+
* 设计与边界详见 docs/file-cache-design.md:
|
|
8
|
+
* - 不接管 EvolAgent 的 merged config(agent/defaults 由 EvolAgent 持有权威副本)。
|
|
9
|
+
* - daemon-only:CLI 短命进程仍直读最新盘值,不用本缓存。
|
|
10
|
+
* - 只缓存"文件 → 解析后内容",不缓存按 vars 渲染后的结果。
|
|
11
|
+
*
|
|
12
|
+
* 策略(reload/重启永远全量失效,无视策略;策略只决定"平时每次读怎么检查"):
|
|
13
|
+
* - on-reload:平时不检查,直接用缓存(kits 文件、persona)。靠 reload/重启刷新。
|
|
14
|
+
* - manual:同 on-reload,额外支持显式 invalidate(file) 单刷。
|
|
15
|
+
* - mtime:每次读 statSync 门控 mtime,变了自动重读(working.md、preferences.json)。
|
|
16
|
+
*
|
|
17
|
+
* 容量:项数可无界增长的组(如 relation-prefs,每 peer 一文件)设 LRU 硬上限
|
|
18
|
+
* (见 GROUP_CAPS),命中/写入移到末尾、超限驱逐同组最旧项;无 timer。
|
|
19
|
+
* 项数固定的组(kits、per-agent 身份层)不设限。
|
|
20
|
+
*
|
|
21
|
+
* 监控:内置命中/读盘/驱逐/失效计数(总计 + 按 group + 按 policy),stats() 导出
|
|
22
|
+
* 快照供 watch web 的 Cache 页展示(经 IPC 'cache-stats')。计数是整数自增,热路径无感。
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
const GROUP_NONE = '(none)'; // group 未指定时的归类键
|
|
26
|
+
function newCounters() {
|
|
27
|
+
return { gets: 0, hits: 0, misses: 0, statChecks: 0, reReads: 0, evictions: 0, invalidations: 0 };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 按组的容量上限(LRU)。只有"项数可无界增长"的组才设限——典型是关系级
|
|
31
|
+
* preferences,每个 peer 一个文件,daemon 长跑接触的 peer 越来越多。
|
|
32
|
+
* kits(项数随包固定)、per-agent 身份层(agent 数量有限)不设限,不在此表即无限。
|
|
33
|
+
* 命中/写入都把 key 移到 Map 末尾(Map 保留插入序 → 头部即最久未用),
|
|
34
|
+
* 超限时从头部驱逐同组最旧项。无 timer,确定性 bound。
|
|
35
|
+
*/
|
|
36
|
+
const GROUP_CAPS = {
|
|
37
|
+
'relation-prefs': 512,
|
|
38
|
+
};
|
|
39
|
+
export class FileCache {
|
|
40
|
+
cache = new Map();
|
|
41
|
+
// 监控计数器(总计 + 按 group + 按 policy)。group 未指定归入 GROUP_NONE。
|
|
42
|
+
since = Date.now();
|
|
43
|
+
totals = newCounters();
|
|
44
|
+
byGroup = new Map();
|
|
45
|
+
byPolicy = {
|
|
46
|
+
'on-reload': newCounters(), 'manual': newCounters(), 'mtime': newCounters(),
|
|
47
|
+
};
|
|
48
|
+
/** 取(或建)某 group 的计数器。 */
|
|
49
|
+
groupCounters(group) {
|
|
50
|
+
const key = group ?? GROUP_NONE;
|
|
51
|
+
let c = this.byGroup.get(key);
|
|
52
|
+
if (!c) {
|
|
53
|
+
c = newCounters();
|
|
54
|
+
this.byGroup.set(key, c);
|
|
55
|
+
}
|
|
56
|
+
return c;
|
|
57
|
+
}
|
|
58
|
+
/** 同一事件按 总计/group/policy 三维各加一次。delta 缺省 1。 */
|
|
59
|
+
bump(group, policy, field, delta = 1) {
|
|
60
|
+
this.totals[field] += delta;
|
|
61
|
+
this.groupCounters(group)[field] += delta;
|
|
62
|
+
this.byPolicy[policy][field] += delta;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 读取并缓存文件。loader 把原始内容(UTF-8 字符串;文件不存在时为 null)转成目标值。
|
|
66
|
+
* 同一 file 的 policy/group 以首次注册为准。
|
|
67
|
+
* opts.read 可注入自定义读取器(如 atomicRead 保留崩溃恢复);缺省 readFileOrNull。
|
|
68
|
+
*/
|
|
69
|
+
get(file, loader, opts) {
|
|
70
|
+
const existing = this.cache.get(file);
|
|
71
|
+
const read = opts.read ?? readFileOrNull;
|
|
72
|
+
this.bump(opts.group, opts.policy, 'gets');
|
|
73
|
+
if (opts.policy === 'mtime') {
|
|
74
|
+
const mtimeMs = statMtime(file);
|
|
75
|
+
this.bump(opts.group, opts.policy, 'statChecks');
|
|
76
|
+
if (existing && existing.mtimeMs === mtimeMs) {
|
|
77
|
+
this.bump(opts.group, opts.policy, 'hits');
|
|
78
|
+
this.touch(file, existing);
|
|
79
|
+
return existing.value;
|
|
80
|
+
}
|
|
81
|
+
// 已有条目但 mtime 变了 → 带外改后重读(区别于首次 miss)
|
|
82
|
+
if (existing)
|
|
83
|
+
this.bump(opts.group, opts.policy, 'reReads');
|
|
84
|
+
this.bump(opts.group, opts.policy, 'misses');
|
|
85
|
+
const raw = read(file);
|
|
86
|
+
const value = loader(raw);
|
|
87
|
+
this.store(file, { policy: 'mtime', group: opts.group, mtimeMs, value, bytes: approxBytes(raw) });
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
// on-reload / manual:命中即用,不检查文件
|
|
91
|
+
if (existing) {
|
|
92
|
+
this.bump(opts.group, opts.policy, 'hits');
|
|
93
|
+
this.touch(file, existing);
|
|
94
|
+
return existing.value;
|
|
95
|
+
}
|
|
96
|
+
this.bump(opts.group, opts.policy, 'misses');
|
|
97
|
+
const raw = read(file);
|
|
98
|
+
const value = loader(raw);
|
|
99
|
+
this.store(file, { policy: opts.policy, group: opts.group, value, bytes: approxBytes(raw) });
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
/** 命中时把 key 移到 Map 末尾,维持 LRU 顺序(仅对设了容量上限的组有意义)。 */
|
|
103
|
+
touch(file, entry) {
|
|
104
|
+
if (entry.group === undefined || GROUP_CAPS[entry.group] === undefined)
|
|
105
|
+
return;
|
|
106
|
+
this.cache.delete(file);
|
|
107
|
+
this.cache.set(file, entry);
|
|
108
|
+
}
|
|
109
|
+
/** 写入并对设限组做 LRU 驱逐(踢同组最久未用项)。 */
|
|
110
|
+
store(file, entry) {
|
|
111
|
+
this.cache.delete(file); // 确保重写时移到末尾
|
|
112
|
+
this.cache.set(file, entry);
|
|
113
|
+
const cap = entry.group !== undefined ? GROUP_CAPS[entry.group] : undefined;
|
|
114
|
+
if (cap === undefined)
|
|
115
|
+
return;
|
|
116
|
+
// Map 按插入序遍历,头部即最久未用;驱逐同组超出容量的最旧项。
|
|
117
|
+
let count = 0;
|
|
118
|
+
for (const e of this.cache.values())
|
|
119
|
+
if (e.group === entry.group)
|
|
120
|
+
count++;
|
|
121
|
+
if (count <= cap)
|
|
122
|
+
return;
|
|
123
|
+
for (const [k, e] of this.cache) {
|
|
124
|
+
if (count <= cap)
|
|
125
|
+
break;
|
|
126
|
+
if (e.group === entry.group) {
|
|
127
|
+
this.cache.delete(k);
|
|
128
|
+
count--;
|
|
129
|
+
this.bump(e.group, e.policy, 'evictions');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** 读纯文本的便捷封装(文件不存在返回 null)。 */
|
|
134
|
+
getText(file, opts) {
|
|
135
|
+
return this.get(file, (raw) => raw, opts);
|
|
136
|
+
}
|
|
137
|
+
/** 单文件失效(manual 策略单刷 / 精确失效)。 */
|
|
138
|
+
invalidate(file) {
|
|
139
|
+
const entry = this.cache.get(file);
|
|
140
|
+
if (this.cache.delete(file) && entry)
|
|
141
|
+
this.bump(entry.group, entry.policy, 'invalidations');
|
|
142
|
+
}
|
|
143
|
+
/** 按组失效(reload 钩子失效一组,如 'kits' / 'agent-files:<aid>')。 */
|
|
144
|
+
invalidateGroup(group) {
|
|
145
|
+
for (const [file, entry] of this.cache) {
|
|
146
|
+
if (entry.group === group) {
|
|
147
|
+
this.cache.delete(file);
|
|
148
|
+
this.bump(entry.group, entry.policy, 'invalidations');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** 全量失效(reload / 升级兜底)。 */
|
|
153
|
+
invalidateAll() {
|
|
154
|
+
for (const entry of this.cache.values())
|
|
155
|
+
this.bump(entry.group, entry.policy, 'invalidations');
|
|
156
|
+
this.cache.clear();
|
|
157
|
+
}
|
|
158
|
+
/** 当前缓存条目数(诊断用)。 */
|
|
159
|
+
size() {
|
|
160
|
+
return this.cache.size;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 运行时统计快照(监控用)。累计计数 O(1) 取出;占用(size/内存/容量)即时遍历
|
|
164
|
+
* 当前 entries 算出——条目数有限(kits 固定、relation-prefs 有 LRU 上限),便宜。
|
|
165
|
+
*/
|
|
166
|
+
stats() {
|
|
167
|
+
const occupancy = {};
|
|
168
|
+
for (const entry of this.cache.values()) {
|
|
169
|
+
const key = entry.group ?? GROUP_NONE;
|
|
170
|
+
let occ = occupancy[key];
|
|
171
|
+
if (!occ) {
|
|
172
|
+
occ = { size: 0, bytes: 0, cap: GROUP_CAPS[key] ?? null };
|
|
173
|
+
occupancy[key] = occ;
|
|
174
|
+
}
|
|
175
|
+
occ.size++;
|
|
176
|
+
occ.bytes += entry.bytes;
|
|
177
|
+
}
|
|
178
|
+
const byGroup = {};
|
|
179
|
+
for (const [k, c] of this.byGroup)
|
|
180
|
+
byGroup[k] = { ...c };
|
|
181
|
+
return {
|
|
182
|
+
since: this.since,
|
|
183
|
+
size: this.cache.size,
|
|
184
|
+
totals: { ...this.totals },
|
|
185
|
+
byGroup,
|
|
186
|
+
byPolicy: {
|
|
187
|
+
'on-reload': { ...this.byPolicy['on-reload'] },
|
|
188
|
+
'manual': { ...this.byPolicy['manual'] },
|
|
189
|
+
'mtime': { ...this.byPolicy['mtime'] },
|
|
190
|
+
},
|
|
191
|
+
occupancy,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function statMtime(file) {
|
|
196
|
+
try {
|
|
197
|
+
return fs.statSync(file).mtimeMs;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null; // 文件不存在
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function readFileOrNull(file) {
|
|
204
|
+
try {
|
|
205
|
+
return fs.readFileSync(file, 'utf-8');
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** 缓存值内存占用近似:用读入的原始字符串长度(字节级近似),null 记 0。 */
|
|
212
|
+
function approxBytes(raw) {
|
|
213
|
+
return raw === null ? 0 : raw.length;
|
|
214
|
+
}
|
|
215
|
+
/** daemon 单例。CLI 进程不应使用本实例。 */
|
|
216
|
+
export const fileCache = new FileCache();
|
|
@@ -300,6 +300,9 @@ export class EvolAgentRegistry {
|
|
|
300
300
|
}
|
|
301
301
|
// swap config 后再起新 channel —— startChannel hook 需要看到新 config
|
|
302
302
|
oldAgent.swapConfig(raw, merged);
|
|
303
|
+
// 热重载也刷新身份层缓存(persona / working 等 fileCache 'agent-files:<aid>' 组),
|
|
304
|
+
// 使 personal 文件改动经 reload 即时生效,不必重启。
|
|
305
|
+
oldAgent.invalidatePersonaCache();
|
|
303
306
|
for (const ch of toAdd) {
|
|
304
307
|
await hooks.startChannel(oldAgent, ch);
|
|
305
308
|
addedSuccessfully.push(ch);
|
|
@@ -353,6 +356,7 @@ export class EvolAgentRegistry {
|
|
|
353
356
|
toInfo(agent) {
|
|
354
357
|
return {
|
|
355
358
|
name: agent.name,
|
|
359
|
+
aid: agent.aid,
|
|
356
360
|
status: agent.status,
|
|
357
361
|
channels: agent.channelInstanceNames(),
|
|
358
362
|
projectPath: agent.projectPath,
|
package/dist/core/evolagent.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import { logger } from '../utils/logger.js';
|
|
4
3
|
import { saveAgent } from '../config-store.js';
|
|
5
4
|
import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
|
|
6
5
|
import { agentPersonalDir } from '../paths.js';
|
|
6
|
+
import { fileCache } from './daemon-file-cache.js';
|
|
7
7
|
/**
|
|
8
8
|
* EvolAgent —— 一个 self-agent 的运行时表示。
|
|
9
9
|
*
|
|
@@ -213,40 +213,45 @@ export class EvolAgent {
|
|
|
213
213
|
this.merged.dispatch = value;
|
|
214
214
|
this.persist();
|
|
215
215
|
}
|
|
216
|
+
/** 读取观察者模式开关(默认 false)。 */
|
|
217
|
+
getObservable() {
|
|
218
|
+
return this.merged.observable === true;
|
|
219
|
+
}
|
|
220
|
+
/** 设置观察者模式开关:开启后入站/出站消息各转发一份给 owners[]。 */
|
|
221
|
+
setObservable(value) {
|
|
222
|
+
if (value)
|
|
223
|
+
this.rawAgent.observable = true;
|
|
224
|
+
else
|
|
225
|
+
delete this.rawAgent.observable;
|
|
226
|
+
this.merged.observable = value;
|
|
227
|
+
this.persist();
|
|
228
|
+
}
|
|
216
229
|
// ── Personal layer ────────────────────────────────────────────────────
|
|
217
|
-
|
|
230
|
+
/** 本 agent 身份层文件在 fileCache 的组名(带 aid,避免 reload 单个 agent 误失效他人)。 */
|
|
231
|
+
agentFilesGroup() {
|
|
232
|
+
return `agent-files:${this.aid}`;
|
|
233
|
+
}
|
|
218
234
|
/**
|
|
219
|
-
* 读取 personal/persona.md
|
|
220
|
-
*
|
|
235
|
+
* 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 没有任何
|
|
236
|
+
* 写入命令、由 agent 自己带外改写,与 working memory 同样改了即应生效,故每次读
|
|
237
|
+
* stat 比对、变了自动重读。文件不存在返回 null。
|
|
221
238
|
*/
|
|
222
239
|
getPersona() {
|
|
223
|
-
if (this._personaCache !== undefined)
|
|
224
|
-
return this._personaCache;
|
|
225
240
|
const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
|
|
226
|
-
|
|
227
|
-
this._personaCache = fs.readFileSync(personaPath, 'utf-8').trim() || null;
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
this._personaCache = null;
|
|
231
|
-
}
|
|
232
|
-
return this._personaCache;
|
|
241
|
+
return fileCache.get(personaPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
|
|
233
242
|
}
|
|
234
243
|
/**
|
|
235
|
-
* 读取 personal/memory/working.md
|
|
244
|
+
* 读取 personal/memory/working.md 内容。走 fileCache(mtime 门控):
|
|
245
|
+
* agent 在对话中改写 working memory、不触发 reload,故每次读 stat 比对、
|
|
246
|
+
* 变了自动重读,既即时反映又避免无谓重读。
|
|
236
247
|
*/
|
|
237
248
|
getWorkingMemory() {
|
|
238
249
|
const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
|
|
239
|
-
|
|
240
|
-
const content = fs.readFileSync(workingPath, 'utf-8').trim();
|
|
241
|
-
return content || null;
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
250
|
+
return fileCache.get(workingPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
|
|
246
251
|
}
|
|
247
|
-
/**
|
|
252
|
+
/** 清除本 agent 身份层缓存(reload 后重新读取)。只失效自己的文件组,不波及他人。 */
|
|
248
253
|
invalidatePersonaCache() {
|
|
249
|
-
this.
|
|
254
|
+
fileCache.invalidateGroup(this.agentFilesGroup());
|
|
250
255
|
}
|
|
251
256
|
// ── Context(喂给 message-processor / command-handler) ──────────────
|
|
252
257
|
getContext(_channelKey, chatType, globalChatmode) {
|
|
@@ -68,6 +68,14 @@ export class InteractionRouter {
|
|
|
68
68
|
const handler = this.handlers.get(response.id);
|
|
69
69
|
if (!handler)
|
|
70
70
|
return false;
|
|
71
|
+
// Initiator 校验(集中式 backstop):非发起者的操作直接丢弃,不消费 handler、不解除等待,
|
|
72
|
+
// 让真正的发起者仍可继续操作。身份只信渠道传入的已认证 operatorId(来自消息信封,非 payload 自报)。
|
|
73
|
+
// 渠道层若已自行校验(如飞书的 reject toast),此处不会重复命中(operatorId 已匹配)。
|
|
74
|
+
if (handler.initiatorId && response.operatorId
|
|
75
|
+
&& response.operatorId !== handler.initiatorId) {
|
|
76
|
+
logger.info(`[InteractionRouter] rejected non-initiator: operator=${response.operatorId} initiator=${handler.initiatorId} id=${response.id}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
71
79
|
if (handler.timer)
|
|
72
80
|
clearTimeout(handler.timer);
|
|
73
81
|
this.handlers.delete(response.id);
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { agentCreateNonInteractive, agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, agentReload, } from '../../cli/agent.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { resolvePaths } from '../../paths.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { loadAgent, saveAgent } from '../../config-store.js';
|
|
6
|
+
import { CreateStatusWriter, readCreateStatus } from './create-status.js';
|
|
7
|
+
/** 把 cli/agent.ts 的 error 字符串映射为结构化错误码 */
|
|
8
|
+
function classifyError(error) {
|
|
9
|
+
if (/already exists/i.test(error))
|
|
10
|
+
return 'CONFLICT';
|
|
11
|
+
if (/not found/i.test(error))
|
|
12
|
+
return 'NOT_FOUND';
|
|
13
|
+
if (/invalid|must be|required|缺少/i.test(error))
|
|
14
|
+
return 'INVALID_ARGS';
|
|
15
|
+
return 'INTERNAL';
|
|
16
|
+
}
|
|
17
|
+
/** 后台异步:实际创建 agent + 落 model/chatmode,全程写构建进度(D3)。
|
|
18
|
+
* 失败仅写日志 + create-status,不回传(受理即返回)。
|
|
19
|
+
* agentSet key 对照 cli/agent.ts 的 setNestedValue:
|
|
20
|
+
* model → 'models.default'(ModelsBlock.default);chatmode → 'chatmode'(ChatmodeBlock 对象)。 */
|
|
21
|
+
async function runCreateInBackground(opts) {
|
|
22
|
+
const agentDir = path.join(resolvePaths().agentsDir, opts.aid);
|
|
23
|
+
const w = new CreateStatusWriter(agentDir, opts.aid);
|
|
24
|
+
let curPhase = 'validating'; // 跟踪当前环节,供 catch 兜底时标注正确 phase
|
|
25
|
+
try {
|
|
26
|
+
// onPhase 把 agentCreateNonInteractive 内部环节(0-3、5)映射到进度文件
|
|
27
|
+
const res = await agentCreateNonInteractive({
|
|
28
|
+
aid: opts.aid, name: opts.name, baseagent: opts.baseagent,
|
|
29
|
+
project: opts.project, owner: opts.owner,
|
|
30
|
+
onPhase: (phase, state, detail) => {
|
|
31
|
+
if (state === 'begin') {
|
|
32
|
+
curPhase = phase;
|
|
33
|
+
w.begin(phase);
|
|
34
|
+
}
|
|
35
|
+
else if (state === 'done')
|
|
36
|
+
w.done(phase, detail);
|
|
37
|
+
else if (state === 'warn')
|
|
38
|
+
w.warn(phase, detail);
|
|
39
|
+
else if (state === 'failed')
|
|
40
|
+
w.finishFailed(phase, detail ?? 'failed');
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!('ok' in res) || res.ok !== true) {
|
|
44
|
+
// 硬失败:onPhase('failed') 已写终态;这里仅兜底日志(防回调未覆盖的 return 路径)
|
|
45
|
+
const err = res.error;
|
|
46
|
+
logger.warn(`[agent-control] create ${opts.aid} failed: ${err}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// 环节 4:applying_config(model/chatmode,agentCreateNonInteractive 之外)
|
|
50
|
+
if (opts.model || opts.chatmode) {
|
|
51
|
+
curPhase = 'applying_config';
|
|
52
|
+
w.begin('applying_config');
|
|
53
|
+
let warned;
|
|
54
|
+
if (opts.model) {
|
|
55
|
+
const r = await agentSet(opts.aid, 'models.default', opts.model);
|
|
56
|
+
if (!('ok' in r) || !r.ok)
|
|
57
|
+
warned = `model: ${r.error}`;
|
|
58
|
+
}
|
|
59
|
+
if (opts.chatmode) {
|
|
60
|
+
const r = await agentSet(opts.aid, 'chatmode', JSON.stringify(opts.chatmode));
|
|
61
|
+
if (!('ok' in r) || !r.ok)
|
|
62
|
+
warned = `${warned ? warned + '; ' : ''}chatmode: ${r.error}`;
|
|
63
|
+
}
|
|
64
|
+
if (warned) {
|
|
65
|
+
logger.warn(`[agent-control] applying_config ${opts.aid}: ${warned}`);
|
|
66
|
+
w.warn('applying_config', warned);
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
w.done('applying_config');
|
|
70
|
+
}
|
|
71
|
+
w.finishReady();
|
|
72
|
+
logger.info(`[agent-control] create ${opts.aid} ready`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
const msg = e?.message || String(e);
|
|
76
|
+
logger.warn(`[agent-control] create ${opts.aid} threw at ${curPhase}: ${msg}`);
|
|
77
|
+
w.finishFailed(curPhase, msg); // 兜底终态,标注真实失败环节
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** name=agent 的 menu.action 执行。peerId 自动填为新 agent 的 owner。
|
|
81
|
+
* create 受理即返回(D3);delete/enable/disable 同步等结果。
|
|
82
|
+
* 调用方负责传入已兜底的 args.project(见 command-handler 装配)。 */
|
|
83
|
+
export async function execAgentAction(action, args, peerId) {
|
|
84
|
+
const a = args ?? {};
|
|
85
|
+
if (action === 'create') {
|
|
86
|
+
if (!peerId)
|
|
87
|
+
return { error: '缺少发起者 AID(无法绑定 owner)', code: 'INVALID_ARGS' };
|
|
88
|
+
if (!a.aid || !a.name || !a.baseagent) {
|
|
89
|
+
return { error: '缺少必填参数:aid / name / baseagent', code: 'INVALID_ARGS' };
|
|
90
|
+
}
|
|
91
|
+
if (!a.project || typeof a.project !== 'string') {
|
|
92
|
+
return { error: 'project 缺失且无法兜底(需 defaults.projects.rootPath/defaultPath)', code: 'INVALID_ARGS' };
|
|
93
|
+
}
|
|
94
|
+
// D3: 受理即返回,重副作用转后台
|
|
95
|
+
// 受理即返回;后台 promise fire-and-forget。runCreateInBackground 内部有 try/catch,
|
|
96
|
+
// 但 CreateStatusWriter 构造(mkdir/写盘)在 try 之前,故再加一层兜底防未处理拒绝。
|
|
97
|
+
void runCreateInBackground({
|
|
98
|
+
aid: a.aid, name: a.name, baseagent: a.baseagent,
|
|
99
|
+
project: a.project, owner: peerId,
|
|
100
|
+
model: a.model, chatmode: a.chatmode,
|
|
101
|
+
}).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
|
|
102
|
+
return { data: { accepted: true, aid: a.aid } };
|
|
103
|
+
}
|
|
104
|
+
if (action === 'delete') {
|
|
105
|
+
if (!a.aid)
|
|
106
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
107
|
+
const res = await agentDelete(a.aid, false);
|
|
108
|
+
if (!('ok' in res) || res.ok !== true)
|
|
109
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
110
|
+
return { data: { aid: res.aid, purged: res.purged } };
|
|
111
|
+
}
|
|
112
|
+
if (action === 'enable' || action === 'disable') {
|
|
113
|
+
if (!a.aid)
|
|
114
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
115
|
+
const res = action === 'enable' ? await agentEnable(a.aid) : await agentDisable(a.aid);
|
|
116
|
+
if (!('ok' in res) || res.ok !== true)
|
|
117
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
118
|
+
return { data: { aid: res.aid, enabled: res.enabled, reloaded: res.reloaded } };
|
|
119
|
+
}
|
|
120
|
+
if (action === 'reload') {
|
|
121
|
+
if (!a.aid)
|
|
122
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
123
|
+
const res = await agentReload(a.aid);
|
|
124
|
+
if (!('ok' in res) || res.ok !== true)
|
|
125
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
126
|
+
return { data: { aid: a.aid, reloaded: true } };
|
|
127
|
+
}
|
|
128
|
+
if (action === 'update') {
|
|
129
|
+
return await execAgentUpdate(a);
|
|
130
|
+
}
|
|
131
|
+
return { error: `不支持的 action: ${action}`, code: 'INVALID_ARGS' };
|
|
132
|
+
}
|
|
133
|
+
/** name=agent 的 menu.action=update:仅落盘 config patch,不触发 reload。
|
|
134
|
+
* 直接 loadAgent + saveAgent(不走 agentSet,避免其内部自动 evolagent.reload)——
|
|
135
|
+
* 重载由用户在 Agents 页操作列手动触发(带任务执行检查)。
|
|
136
|
+
* AUN 渠道绑定 agent 顶层 aid,不可通过 patch 编辑:拒绝改 aid、拒绝 channels 数组里出现 aun 条目。
|
|
137
|
+
* 可写字段:baseagents / projects / owners / chatmode / channels(非 aun)。 */
|
|
138
|
+
export async function execAgentUpdate(args) {
|
|
139
|
+
const a = args ?? {};
|
|
140
|
+
if (!a.aid)
|
|
141
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
142
|
+
const p = a.patch ?? {};
|
|
143
|
+
if (p.aid !== undefined) {
|
|
144
|
+
return { error: 'aid 不可修改(AUN 身份绑定,如需换 AID 请删除后重建)', code: 'INVALID_ARGS' };
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(p.channels) && p.channels.some((c) => c?.type === 'aun')) {
|
|
147
|
+
return { error: 'AUN 渠道不可通过 patch 编辑(由 agent aid 隐式管理)', code: 'INVALID_ARGS' };
|
|
148
|
+
}
|
|
149
|
+
const config = loadAgent(a.aid);
|
|
150
|
+
if (!config)
|
|
151
|
+
return { error: `Agent "${a.aid}" not found`, code: 'NOT_FOUND' };
|
|
152
|
+
let touched = false;
|
|
153
|
+
if (p.baseagents !== undefined) {
|
|
154
|
+
config.baseagents = p.baseagents;
|
|
155
|
+
touched = true;
|
|
156
|
+
}
|
|
157
|
+
if (p.projects !== undefined) {
|
|
158
|
+
config.projects = p.projects;
|
|
159
|
+
touched = true;
|
|
160
|
+
}
|
|
161
|
+
if (p.owners !== undefined) {
|
|
162
|
+
config.owners = p.owners;
|
|
163
|
+
touched = true;
|
|
164
|
+
}
|
|
165
|
+
if (p.chatmode !== undefined) {
|
|
166
|
+
config.chatmode = p.chatmode;
|
|
167
|
+
touched = true;
|
|
168
|
+
}
|
|
169
|
+
if (p.channels !== undefined) {
|
|
170
|
+
config.channels = p.channels;
|
|
171
|
+
touched = true;
|
|
172
|
+
}
|
|
173
|
+
if (!touched)
|
|
174
|
+
return { error: 'patch 为空,无可写字段', code: 'INVALID_ARGS' };
|
|
175
|
+
try {
|
|
176
|
+
saveAgent(config);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return { error: e?.message || String(e), code: classifyError(e?.message || String(e)) };
|
|
180
|
+
}
|
|
181
|
+
return { data: { aid: a.aid, saved: true } };
|
|
182
|
+
}
|
|
183
|
+
/** project 兜底:显式值 > rootPath 合成 > defaultPath > undefined */
|
|
184
|
+
export function resolveProjectPath(explicit, aid, defaults) {
|
|
185
|
+
if (explicit && explicit.trim())
|
|
186
|
+
return explicit;
|
|
187
|
+
const root = defaults?.projects?.rootPath;
|
|
188
|
+
if (root)
|
|
189
|
+
return path.join(root, aid.split('.')[0]);
|
|
190
|
+
return defaults?.projects?.defaultPath;
|
|
191
|
+
}
|
|
192
|
+
/** name=agent 的 menu.query:查单个 agent 详情,附构建进度(D3)。 */
|
|
193
|
+
export async function execAgentQuery(args) {
|
|
194
|
+
const aid = args?.aid;
|
|
195
|
+
if (!aid)
|
|
196
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
197
|
+
const res = await agentShow(aid);
|
|
198
|
+
if (!('ok' in res) || res.ok !== true)
|
|
199
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
200
|
+
// 叠加构建进度(create 受理后、ready 前可见;ready 后文件仍在,可反映软失败 warn)
|
|
201
|
+
const agentDir = path.join(resolvePaths().agentsDir, aid);
|
|
202
|
+
const progress = readCreateStatus(agentDir);
|
|
203
|
+
return { data: progress ? { ...res, createProgress: progress } : res };
|
|
204
|
+
}
|
|
205
|
+
/** name=agent 的 menu.options:列出 agent(enabled 默认 / all) */
|
|
206
|
+
export async function execAgentOptions(args) {
|
|
207
|
+
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
208
|
+
const res = await agentList();
|
|
209
|
+
if (!('ok' in res) || res.ok !== true)
|
|
210
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
211
|
+
const agents = scope === 'all'
|
|
212
|
+
? res.agents
|
|
213
|
+
: res.agents.filter((x) => x.status !== 'disabled');
|
|
214
|
+
return { data: { agents, scope } };
|
|
215
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const FILE = 'create-status.json';
|
|
4
|
+
export function readCreateStatus(agentDir) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = fs.readFileSync(path.join(agentDir, FILE), 'utf-8');
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** 删除构建进度文件(agent 删除时清理)。非 purge 删除只移除 config.json,
|
|
14
|
+
* 故需显式清理本文件,避免目录残留陈旧进度。 */
|
|
15
|
+
export function removeCreateStatus(agentDir) {
|
|
16
|
+
try {
|
|
17
|
+
fs.rmSync(path.join(agentDir, FILE), { force: true });
|
|
18
|
+
}
|
|
19
|
+
catch { /* ignore */ }
|
|
20
|
+
}
|
|
21
|
+
/** 构建进度写入器。每次状态变更原子落盘(写临时文件 + rename)。 */
|
|
22
|
+
export class CreateStatusWriter {
|
|
23
|
+
agentDir;
|
|
24
|
+
status;
|
|
25
|
+
constructor(agentDir, aid) {
|
|
26
|
+
this.agentDir = agentDir;
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
this.status = { aid, status: 'in_progress', currentPhase: null, steps: [], error: null, startedAt: now, updatedAt: now };
|
|
29
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
30
|
+
this.flush();
|
|
31
|
+
}
|
|
32
|
+
begin(phase) {
|
|
33
|
+
this.status.currentPhase = phase;
|
|
34
|
+
this.status.steps.push({ phase, state: 'in_progress', ts: Date.now() });
|
|
35
|
+
this.flush();
|
|
36
|
+
}
|
|
37
|
+
done(phase, detail) { this.mark(phase, 'done', detail); }
|
|
38
|
+
warn(phase, detail) { this.mark(phase, 'warn', detail); }
|
|
39
|
+
finishReady() { this.status.status = 'ready'; this.status.currentPhase = null; this.flush(); }
|
|
40
|
+
finishFailed(phase, error) {
|
|
41
|
+
this.mark(phase, 'failed', error);
|
|
42
|
+
this.status.status = 'failed';
|
|
43
|
+
this.status.error = error;
|
|
44
|
+
this.status.currentPhase = null;
|
|
45
|
+
this.flush();
|
|
46
|
+
}
|
|
47
|
+
mark(phase, state, detail) {
|
|
48
|
+
const step = [...this.status.steps].reverse().find(s => s.phase === phase);
|
|
49
|
+
if (step) {
|
|
50
|
+
step.state = state;
|
|
51
|
+
if (detail)
|
|
52
|
+
step.detail = detail;
|
|
53
|
+
step.ts = Date.now();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.status.steps.push({ phase, state, detail, ts: Date.now() });
|
|
57
|
+
}
|
|
58
|
+
this.flush();
|
|
59
|
+
}
|
|
60
|
+
flush() {
|
|
61
|
+
this.status.updatedAt = Date.now();
|
|
62
|
+
const file = path.join(this.agentDir, FILE);
|
|
63
|
+
const tmp = `${file}.tmp`;
|
|
64
|
+
fs.writeFileSync(tmp, JSON.stringify(this.status, null, 2));
|
|
65
|
+
fs.renameSync(tmp, file);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -308,21 +308,24 @@ export class IMRenderer {
|
|
|
308
308
|
clearTimeout(this.timer);
|
|
309
309
|
this.timer = undefined;
|
|
310
310
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
item.text = stripCtxErr(item.text);
|
|
319
|
-
}
|
|
320
|
-
// 文件标记过滤
|
|
321
|
-
if (this.opts.fileMarkerPattern) {
|
|
322
|
-
this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
|
|
311
|
+
if (isFinal) {
|
|
312
|
+
// 上下文错误短语过滤:剔除错误关键词本身,保留前后内容。
|
|
313
|
+
// 只在最终 flush 清理,避免中间定时 flush trim 掉 Markdown 块级换行。
|
|
314
|
+
const ctxErrPattern = /prompt is too long|input is too long|context too long|context limit|context_length_exceeded|上下文过长/gi;
|
|
315
|
+
const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
|
|
316
|
+
this.textBuffer = stripCtxErr(this.textBuffer);
|
|
317
|
+
this.allText = stripCtxErr(this.allText);
|
|
323
318
|
for (const item of this.itemsQueue) {
|
|
324
319
|
if (item.kind === 'text')
|
|
325
|
-
item.text = item.text
|
|
320
|
+
item.text = stripCtxErr(item.text);
|
|
321
|
+
}
|
|
322
|
+
// 文件标记过滤
|
|
323
|
+
if (this.opts.fileMarkerPattern) {
|
|
324
|
+
this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
|
|
325
|
+
for (const item of this.itemsQueue) {
|
|
326
|
+
if (item.kind === 'text')
|
|
327
|
+
item.text = item.text.replace(this.opts.fileMarkerPattern, '');
|
|
328
|
+
}
|
|
326
329
|
}
|
|
327
330
|
}
|
|
328
331
|
// 清掉空 text items
|
|
@@ -356,6 +359,25 @@ export class IMRenderer {
|
|
|
356
359
|
this.lastFlush = Date.now();
|
|
357
360
|
this.flushCount++;
|
|
358
361
|
}
|
|
362
|
+
// 1.5 非最终定时 flush:把已累积的文本块作为独立 result.text 发出。
|
|
363
|
+
// 每个 text 事件本身是完整语义块(runner 已合并流式 delta),工具调用前的
|
|
364
|
+
// 文本一向作为独立气泡发送(见 message-processor 的 flushText 调用)。
|
|
365
|
+
// 这里补上「文本块后面没有紧跟 tool_use」的情况——例如 readonly 拒绝写文件时
|
|
366
|
+
// SDK 直接拒绝、不产生 tool_use 事件,文本会一直滞留 buffer,直到下一个
|
|
367
|
+
// tool_use 才被 flushText 带出,并与其后的文本合并成一条(用户侧表现为:
|
|
368
|
+
// 第一条文本等待一分多钟后才和第二条凑成一条发出)。定时器到期即发,根除滞留。
|
|
369
|
+
if (!isFinal && this.textBuffer.length > 0) {
|
|
370
|
+
const text = this.textBuffer;
|
|
371
|
+
this.textBuffer = '';
|
|
372
|
+
const payload = { kind: 'result.text', text, isFinal: false };
|
|
373
|
+
this.sentContent = true;
|
|
374
|
+
this.sendChain = this.sendChain
|
|
375
|
+
.then(() => this.opts.send(payload))
|
|
376
|
+
.catch(e => logger.warn('[IMRenderer] timed result.text send failed:', e));
|
|
377
|
+
await this.sendChain;
|
|
378
|
+
this.lastFlush = Date.now();
|
|
379
|
+
this.flushCount++;
|
|
380
|
+
}
|
|
359
381
|
// 2. isFinal=true 时单独发最终回复文本
|
|
360
382
|
if (isFinal && finalText.length > 0) {
|
|
361
383
|
const payload = { kind: 'result.text', text: finalText, isFinal: true };
|