evolclaw 3.1.1 → 3.1.2
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 +407 -0
- package/README.md +1 -1
- package/SKILLS.md +311 -0
- package/dist/aun/aid/agentmd.js +7 -6
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +15 -2
- package/dist/aun/msg/upload.js +57 -18
- package/dist/channels/aun.js +56 -35
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +5 -4
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +130 -35
- package/dist/cli/index.js +76 -21
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +42 -20
- package/dist/cli/watch-msg.js +3 -1
- package/dist/config-store.js +22 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +11 -4
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +4 -4
- package/dist/core/message/im-renderer.js +4 -4
- package/dist/core/message/message-bridge.js +26 -1
- package/dist/core/message/message-processor.js +2 -24
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +4 -1
- package/dist/index.js +15 -17
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/kits/docs/INDEX.md +6 -0
- package/package.json +5 -2
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { spawn, execFileSync, execFile } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
-
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from '../paths.js';
|
|
7
|
+
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot, agentMdPath } from '../paths.js';
|
|
8
8
|
import { loadDefaults, loadAllAgents, mergeForAgent } from '../config-store.js';
|
|
9
9
|
import { resolveAnthropicConfig } from '../agents/resolve.js';
|
|
10
10
|
import { migrateProject } from '../config-store.js';
|
|
@@ -235,7 +235,7 @@ function formatLocalTime(ms) {
|
|
|
235
235
|
const d = new Date(ms);
|
|
236
236
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
237
237
|
}
|
|
238
|
-
function printStartupInfo() {
|
|
238
|
+
function printStartupInfo(opts = {}) {
|
|
239
239
|
const pkgRoot = getPackageRoot();
|
|
240
240
|
const isNpmInstall = pkgRoot.includes('node_modules');
|
|
241
241
|
const cliRunsSource = !import.meta.url.includes('/dist/');
|
|
@@ -268,7 +268,15 @@ function printStartupInfo() {
|
|
|
268
268
|
version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
|
|
269
269
|
}
|
|
270
270
|
catch { }
|
|
271
|
-
|
|
271
|
+
let aunVer = null;
|
|
272
|
+
try {
|
|
273
|
+
aunVer = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'node_modules', '@agentunion', 'fastaun', 'package.json'), 'utf-8')).version;
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
const pidPart = opts.pid ? ` (PID: ${opts.pid})` : '';
|
|
277
|
+
const aunPart = aunVer ? ` fastaun v${aunVer}` : '';
|
|
278
|
+
const prefix = opts.running ? '✓ EvolClaw is running , v' : ' EvolClaw v';
|
|
279
|
+
console.log(`${prefix}${version}${pidPart}${aunPart}`);
|
|
272
280
|
console.log(` 包路径: ${pkgRoot}`);
|
|
273
281
|
console.log(` 安装类型: ${isNpmInstall ? 'npm全局安装' : '开发仓(link)'}`);
|
|
274
282
|
console.log(` CLI执行: ${cliRunsSource ? '源码(tsx)' : '编译产物(dist)'}`);
|
|
@@ -829,7 +837,7 @@ async function cmdStatus() {
|
|
|
829
837
|
console.log('');
|
|
830
838
|
}
|
|
831
839
|
if (pid) {
|
|
832
|
-
|
|
840
|
+
printStartupInfo({ pid, running: true });
|
|
833
841
|
console.log('');
|
|
834
842
|
console.log('📊 Process Info:');
|
|
835
843
|
try {
|
|
@@ -875,8 +883,8 @@ async function cmdStatus() {
|
|
|
875
883
|
const configChannelNames = new Set();
|
|
876
884
|
for (const cfg of agents) {
|
|
877
885
|
for (const inst of cfg.channels) {
|
|
878
|
-
// effective key: <
|
|
879
|
-
configChannelNames.add(`${
|
|
886
|
+
// effective key: <type>#<urlEncode(selfPeerId)>#<name>
|
|
887
|
+
configChannelNames.add(`${inst.type}#${encodeURIComponent(cfg.aid)}#${inst.name}`);
|
|
880
888
|
}
|
|
881
889
|
}
|
|
882
890
|
for (const s of allSessions) {
|
|
@@ -1005,7 +1013,7 @@ async function cmdStatus() {
|
|
|
1005
1013
|
}
|
|
1006
1014
|
}
|
|
1007
1015
|
/**
|
|
1008
|
-
* 把 channel fingerprint 列表(`<
|
|
1016
|
+
* 把 channel fingerprint 列表(`<type>#<selfPeerId>#<name>`)折叠成展示用摘要。
|
|
1009
1017
|
*
|
|
1010
1018
|
* 聚合规则:
|
|
1011
1019
|
* - 按 type 分组
|
|
@@ -1027,8 +1035,8 @@ function summarizeChannelFingerprints(fingerprints) {
|
|
|
1027
1035
|
}
|
|
1028
1036
|
continue;
|
|
1029
1037
|
}
|
|
1030
|
-
const type = parts[
|
|
1031
|
-
const name = parts
|
|
1038
|
+
const type = parts[0];
|
|
1039
|
+
const name = parts[2];
|
|
1032
1040
|
if (!groups.has(type)) {
|
|
1033
1041
|
groups.set(type, []);
|
|
1034
1042
|
order.push(type);
|
|
@@ -1734,10 +1742,10 @@ async function cmdWatchAid() {
|
|
|
1734
1742
|
const refreshedAids = new Set();
|
|
1735
1743
|
function readLocalName(aid) {
|
|
1736
1744
|
try {
|
|
1737
|
-
const
|
|
1738
|
-
if (!fs.existsSync(
|
|
1745
|
+
const mdPath = agentMdPath(aid);
|
|
1746
|
+
if (!fs.existsSync(mdPath))
|
|
1739
1747
|
return undefined;
|
|
1740
|
-
const content = fs.readFileSync(
|
|
1748
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
1741
1749
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1742
1750
|
if (!fmMatch)
|
|
1743
1751
|
return undefined;
|
|
@@ -2463,12 +2471,14 @@ function archiveSelfHealLog(p, log) {
|
|
|
2463
2471
|
* Searches across all channel types (feishu, wechat, aun) for a matching instance.
|
|
2464
2472
|
*/
|
|
2465
2473
|
function resolveInstanceConfig(instanceName) {
|
|
2466
|
-
// 新结构:channel key 是 <
|
|
2474
|
+
// 新结构:channel key 是 <type>#<selfPeerId>#<name>,解析后从对应 agent 的 channels[] 找
|
|
2467
2475
|
const parts = instanceName.split('#');
|
|
2468
2476
|
if (parts.length === 3) {
|
|
2469
|
-
const [
|
|
2477
|
+
const [type, encodedSelfPeerId, name] = parts;
|
|
2478
|
+
const selfPeerId = decodeURIComponent(encodedSelfPeerId);
|
|
2470
2479
|
const { agents } = loadAllAgents();
|
|
2471
|
-
|
|
2480
|
+
// AUN channel 的 selfPeerId 就是 agent.aid
|
|
2481
|
+
const agent = agents.find(a => a.aid === selfPeerId);
|
|
2472
2482
|
if (!agent)
|
|
2473
2483
|
return null;
|
|
2474
2484
|
const inst = agent.channels.find((c) => c.type === type && c.name === name);
|
|
@@ -2921,7 +2931,11 @@ Options:
|
|
|
2921
2931
|
console.log(result.agentmdUploaded
|
|
2922
2932
|
? ' ✓ agent.md 已发布'
|
|
2923
2933
|
: ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
|
|
2924
|
-
console.log(
|
|
2934
|
+
console.log(result.hotLoaded
|
|
2935
|
+
? ' ✓ 已热重载,agent 已上线'
|
|
2936
|
+
: result.hotLoadError
|
|
2937
|
+
? ` ✗ 热重载失败:${result.hotLoadError}`
|
|
2938
|
+
: ' ⚠ 服务未运行,下次 evolclaw start 时生效');
|
|
2925
2939
|
}
|
|
2926
2940
|
}
|
|
2927
2941
|
else {
|
|
@@ -2943,7 +2957,11 @@ Options:
|
|
|
2943
2957
|
console.log(result.agentmdUploaded
|
|
2944
2958
|
? ' ✓ agent.md 已发布'
|
|
2945
2959
|
: ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
|
|
2946
|
-
console.log(
|
|
2960
|
+
console.log(result.hotLoaded
|
|
2961
|
+
? ' ✓ 已热重载,agent 已上线'
|
|
2962
|
+
: result.hotLoadError
|
|
2963
|
+
? ` ✗ 热重载失败:${result.hotLoadError}`
|
|
2964
|
+
: ' ⚠ 服务未运行,下次 evolclaw start 时生效');
|
|
2947
2965
|
}
|
|
2948
2966
|
}
|
|
2949
2967
|
return;
|
|
@@ -3438,8 +3456,7 @@ Options:
|
|
|
3438
3456
|
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3439
3457
|
process.exit(1);
|
|
3440
3458
|
}
|
|
3441
|
-
const
|
|
3442
|
-
const localPath = path.join(aunBase, 'AIDs', aid, 'agent.md');
|
|
3459
|
+
const localPath = agentMdPath(aid);
|
|
3443
3460
|
if (!fs.existsSync(localPath)) {
|
|
3444
3461
|
console.error(`❌ 本地无 agent.md: ${aid}`);
|
|
3445
3462
|
process.exit(1);
|
|
@@ -3747,12 +3764,15 @@ Options:
|
|
|
3747
3764
|
--app <name> 指定应用 slot(隔离 ack 游标)
|
|
3748
3765
|
--as-daemon ack 时显式以 daemon 身份(高危,会污染 daemon 游标)
|
|
3749
3766
|
--format json 输出 JSON 格式
|
|
3767
|
+
--encrypt 启用端到端加密
|
|
3768
|
+
--thread <id> 指定话题 ID(用于多话题路由)
|
|
3750
3769
|
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
3751
3770
|
--text <说明> 附件说明文字(仅 --file 模式)
|
|
3752
3771
|
--transcript <text> 语音转写(仅 --as voice)
|
|
3753
3772
|
|
|
3754
3773
|
示例:
|
|
3755
3774
|
evolclaw msg send alice.agentid.pub bob.agentid.pub "hello"
|
|
3775
|
+
evolclaw msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A"
|
|
3756
3776
|
evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
|
|
3757
3777
|
evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
|
|
3758
3778
|
evolclaw msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
|
|
@@ -3826,7 +3846,29 @@ Options:
|
|
|
3826
3846
|
body = { mode: 'text', text };
|
|
3827
3847
|
}
|
|
3828
3848
|
const encrypt = args.includes('--encrypt');
|
|
3829
|
-
const
|
|
3849
|
+
const thread = getArgValue(args, '--thread');
|
|
3850
|
+
// 文件上传进度展示(非 JSON 输出时)。仅在大文件降级到 HTTP PUT 阶段会逐块更新。
|
|
3851
|
+
let lastPctShown = -1;
|
|
3852
|
+
const onUploadProgress = formatJson ? undefined : (info) => {
|
|
3853
|
+
if (info.phase === 'inline')
|
|
3854
|
+
return; // 内联阶段不分块,跳过
|
|
3855
|
+
if (info.phase === 'http-put') {
|
|
3856
|
+
const pct = info.total > 0 ? Math.floor((info.bytes / info.total) * 100) : 0;
|
|
3857
|
+
if (pct === lastPctShown && info.bytes < info.total)
|
|
3858
|
+
return;
|
|
3859
|
+
lastPctShown = pct;
|
|
3860
|
+
const mb = (n) => (n / 1024 / 1024).toFixed(2);
|
|
3861
|
+
const eol = info.bytes >= info.total ? '\n' : '\r';
|
|
3862
|
+
process.stderr.write(` ⏫ uploading: ${pct}% (${mb(info.bytes)}/${mb(info.total)} MB)${eol}`);
|
|
3863
|
+
}
|
|
3864
|
+
else if (info.phase === 'session-create') {
|
|
3865
|
+
process.stderr.write(' ⏫ requesting upload session...\n');
|
|
3866
|
+
}
|
|
3867
|
+
else if (info.phase === 'session-complete') {
|
|
3868
|
+
process.stderr.write(' ⏫ finalizing upload...\n');
|
|
3869
|
+
}
|
|
3870
|
+
};
|
|
3871
|
+
const result = await msgSend({ from, to, body, encrypt, thread, onUploadProgress, ...commonOpts });
|
|
3830
3872
|
if (!result.ok) {
|
|
3831
3873
|
if (formatJson) {
|
|
3832
3874
|
console.log(JSON.stringify(result));
|
|
@@ -4408,15 +4450,23 @@ export async function main(args) {
|
|
|
4408
4450
|
evolclaw init wecom 企业微信 AI Bot 配置(手动输入)`);
|
|
4409
4451
|
}
|
|
4410
4452
|
else if (args[1] === 'wechat') {
|
|
4453
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4454
|
+
suppressSdkLogs();
|
|
4411
4455
|
await cmdInitWechat();
|
|
4412
4456
|
}
|
|
4413
4457
|
else if (args[1] === 'feishu') {
|
|
4458
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4459
|
+
suppressSdkLogs();
|
|
4414
4460
|
await cmdInitFeishu();
|
|
4415
4461
|
}
|
|
4416
4462
|
else if (args[1] === 'dingtalk') {
|
|
4463
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4464
|
+
suppressSdkLogs();
|
|
4417
4465
|
await cmdInitDingtalk();
|
|
4418
4466
|
}
|
|
4419
4467
|
else if (args[1] === 'qqbot') {
|
|
4468
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4469
|
+
suppressSdkLogs();
|
|
4420
4470
|
await cmdInitQQBot();
|
|
4421
4471
|
}
|
|
4422
4472
|
else if (args[1] === 'wecom') {
|
|
@@ -4429,6 +4479,8 @@ export async function main(args) {
|
|
|
4429
4479
|
process.exit(1);
|
|
4430
4480
|
}
|
|
4431
4481
|
else {
|
|
4482
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4483
|
+
suppressSdkLogs();
|
|
4432
4484
|
const nonInteractive = args.includes('--non-interactive');
|
|
4433
4485
|
await cmdInit({
|
|
4434
4486
|
nonInteractive,
|
|
@@ -4509,9 +4561,12 @@ export async function main(args) {
|
|
|
4509
4561
|
case 'ctl':
|
|
4510
4562
|
await cmdCtl(args.slice(1));
|
|
4511
4563
|
break;
|
|
4512
|
-
case 'agent':
|
|
4564
|
+
case 'agent': {
|
|
4565
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4566
|
+
suppressSdkLogs();
|
|
4513
4567
|
await cmdAgent(args.slice(1));
|
|
4514
4568
|
break;
|
|
4569
|
+
}
|
|
4515
4570
|
case 'aid': {
|
|
4516
4571
|
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4517
4572
|
suppressSdkLogs();
|
package/dist/cli/init-channel.js
CHANGED
|
@@ -10,6 +10,7 @@ import readline from 'readline';
|
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import crypto from 'crypto';
|
|
13
|
+
import { aidLocalDir } from '../paths.js';
|
|
13
14
|
import { selectInstance } from './init.js';
|
|
14
15
|
import { npmInstallGlobal } from '../utils/npm-ops.js';
|
|
15
16
|
import { loadAllAgents, loadAgent } from '../config-store.js';
|
|
@@ -470,8 +471,9 @@ export async function setupAunAid(rl, _config) {
|
|
|
470
471
|
console.log(` ⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
|
|
471
472
|
// Still write local copy as fallback
|
|
472
473
|
try {
|
|
473
|
-
|
|
474
|
-
fs.
|
|
474
|
+
const localDir = aidLocalDir(aid);
|
|
475
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
476
|
+
fs.writeFileSync(path.join(localDir, 'agent.md'), content, 'utf-8');
|
|
475
477
|
console.log(' ✓ agent.md 已写入本地');
|
|
476
478
|
}
|
|
477
479
|
catch (we) {
|
package/dist/cli/init.js
CHANGED
|
@@ -3,7 +3,7 @@ import readline from 'readline';
|
|
|
3
3
|
import { resolvePaths, ensureDataDirs } from '../paths.js';
|
|
4
4
|
import { commandExists } from '../utils/cross-platform.js';
|
|
5
5
|
import { scanInstances } from '../utils/instance-registry.js';
|
|
6
|
-
import { saveDefaultsSafe } from '../config-store.js';
|
|
6
|
+
import { saveDefaultsSafe, loadAllAgents } from '../config-store.js';
|
|
7
7
|
// ==================== Helpers ====================
|
|
8
8
|
function ask(rl, question) {
|
|
9
9
|
return new Promise(resolve => rl.question(question, resolve));
|
|
@@ -81,19 +81,21 @@ export async function cmdInit(options) {
|
|
|
81
81
|
writeDefaults(defaultsPath, chosen);
|
|
82
82
|
console.log(`✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
|
|
83
83
|
console.log(` active_baseagent: ${chosen}`);
|
|
84
|
+
const { agents } = loadAllAgents();
|
|
85
|
+
if (agents.length === 0) {
|
|
86
|
+
console.log('\n提示:尚无 agent,运行以下命令创建:');
|
|
87
|
+
console.log(' evolclaw agent new <aid>.agentid.pub');
|
|
88
|
+
}
|
|
84
89
|
return;
|
|
85
90
|
}
|
|
86
91
|
// ── 4. 交互式分支 ──
|
|
87
92
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
-
|
|
89
|
-
if (exists) {
|
|
90
|
-
const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
|
|
91
|
-
if (ans !== 'y' && ans !== 'yes') {
|
|
92
|
-
console.log(' 已取消');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
93
|
+
async function askBaseagent() {
|
|
96
94
|
const defaultBa = pickDefault(available);
|
|
95
|
+
if (available.length === 1) {
|
|
96
|
+
console.log(` baseagent: ${defaultBa}`);
|
|
97
|
+
return defaultBa;
|
|
98
|
+
}
|
|
97
99
|
let chosen = null;
|
|
98
100
|
while (chosen === null) {
|
|
99
101
|
const input = (await ask(rl, `默认 baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
|
|
@@ -107,17 +109,37 @@ export async function cmdInit(options) {
|
|
|
107
109
|
}
|
|
108
110
|
chosen = input;
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
return chosen;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
if (exists) {
|
|
116
|
+
const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
|
|
117
|
+
if (ans === 'y' || ans === 'yes') {
|
|
118
|
+
const chosen = await askBaseagent();
|
|
119
|
+
writeDefaults(defaultsPath, chosen);
|
|
120
|
+
console.log(`\n✓ 已覆盖: ${defaultsPath}`);
|
|
121
|
+
console.log(` active_baseagent: ${chosen}\n`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(' 已跳过(保留现有配置)\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const chosen = await askBaseagent();
|
|
129
|
+
writeDefaults(defaultsPath, chosen);
|
|
130
|
+
console.log(`\n✓ 已创建: ${defaultsPath}`);
|
|
131
|
+
console.log(` active_baseagent: ${chosen}\n`);
|
|
132
|
+
}
|
|
133
|
+
// ── 5. 无 agent 时自动进入 agent new ──
|
|
134
|
+
const { agents } = loadAllAgents();
|
|
135
|
+
if (agents.length === 0) {
|
|
136
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
137
|
+
console.log('下一步:创建 agent\n');
|
|
138
|
+
const { agentCreateInteractive } = await import('./agent.js');
|
|
139
|
+
const result = await agentCreateInteractive({ rl });
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
console.error(`❌ ${result.error}`);
|
|
142
|
+
}
|
|
121
143
|
}
|
|
122
144
|
}
|
|
123
145
|
finally {
|
package/dist/cli/watch-msg.js
CHANGED
|
@@ -316,7 +316,9 @@ function renderMessagesPanel(state, width, height) {
|
|
|
316
316
|
const metaTags = (m.encrypt != null || m.chatmode) ? `${MAGENTA}[${encLabel}|${modeLabel}]${RST}` : '';
|
|
317
317
|
let typeTag = '';
|
|
318
318
|
if (m.dir === 'out') {
|
|
319
|
-
const
|
|
319
|
+
const rawSource = m.source;
|
|
320
|
+
// 4 种来源: daemon | ctl | msg | cli
|
|
321
|
+
const source = (rawSource === 'ctl' || rawSource === 'msg' || rawSource === 'cli') ? rawSource : 'daemon';
|
|
320
322
|
const method = m.msgType === 'thought' ? 'thought' : 'send';
|
|
321
323
|
typeTag = `${DIM}[${source}|${method}]${RST}`;
|
|
322
324
|
}
|
package/dist/config-store.js
CHANGED
|
@@ -348,9 +348,30 @@ export function loadAgent(aid) {
|
|
|
348
348
|
if (raw.aid !== aid) {
|
|
349
349
|
throw new Error(`[config] ${p}: aid field "${raw.aid}" != directory name "${aid}"`);
|
|
350
350
|
}
|
|
351
|
-
|
|
351
|
+
const cfg = expandEnvRefs(raw);
|
|
352
|
+
if (cfg.projects?.defaultPath) {
|
|
353
|
+
cfg.projects.defaultPath = cfg.projects.defaultPath.replace(/[/\\]+$/, '');
|
|
354
|
+
}
|
|
355
|
+
return cfg;
|
|
352
356
|
}
|
|
353
357
|
export function saveAgent(value) {
|
|
358
|
+
if (!isValidAid(value.aid)) {
|
|
359
|
+
throw new Error(`[config] saveAgent: invalid aid "${value.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
|
|
360
|
+
}
|
|
361
|
+
if (value.owners) {
|
|
362
|
+
for (const o of value.owners) {
|
|
363
|
+
if (!isValidAid(o)) {
|
|
364
|
+
throw new Error(`[config] saveAgent: invalid owner AID "${o}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (value.admins) {
|
|
369
|
+
for (const a of value.admins) {
|
|
370
|
+
if (!isValidAid(a)) {
|
|
371
|
+
throw new Error(`[config] saveAgent: invalid admin AID "${a}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
354
375
|
atomicWriteJson(agentConfigPath(value.aid), value);
|
|
355
376
|
}
|
|
356
377
|
/**
|
|
@@ -128,18 +128,18 @@ export class ChannelLoader {
|
|
|
128
128
|
}
|
|
129
129
|
const SEP = '#';
|
|
130
130
|
export function formatChannelKey(k) {
|
|
131
|
-
return `${k.
|
|
131
|
+
return `${k.type}${SEP}${encodeURIComponent(k.selfPeerId)}${SEP}${k.name}`;
|
|
132
132
|
}
|
|
133
133
|
export function parseChannelKey(key) {
|
|
134
134
|
const parts = key.split(SEP);
|
|
135
135
|
if (parts.length !== 3) {
|
|
136
136
|
throw new Error(`Invalid channel key (expected 3 segments separated by '#'): ${key}`);
|
|
137
137
|
}
|
|
138
|
-
const [
|
|
139
|
-
if (!
|
|
138
|
+
const [type, encodedSelfPeerId, name] = parts;
|
|
139
|
+
if (!type || !encodedSelfPeerId || !name) {
|
|
140
140
|
throw new Error(`Invalid channel key (empty segment): ${key}`);
|
|
141
141
|
}
|
|
142
|
-
return {
|
|
142
|
+
return { type, selfPeerId: decodeURIComponent(encodedSelfPeerId), name };
|
|
143
143
|
}
|
|
144
144
|
export function tryParseChannelKey(key) {
|
|
145
145
|
try {
|
|
@@ -707,6 +707,10 @@ export class CommandHandler {
|
|
|
707
707
|
return result;
|
|
708
708
|
}
|
|
709
709
|
async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
|
|
710
|
+
// 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
|
|
711
|
+
// 不应覆盖 session 中已有的正确值
|
|
712
|
+
if (source === 'card-trigger')
|
|
713
|
+
chatType = undefined;
|
|
710
714
|
// 解析身份(按实例名)
|
|
711
715
|
const identity = this.sessionManager.resolveIdentity(channel, userId);
|
|
712
716
|
const policy = this.getPolicy(channel);
|
|
@@ -3000,8 +3004,10 @@ export class CommandHandler {
|
|
|
3000
3004
|
return null;
|
|
3001
3005
|
}
|
|
3002
3006
|
handleTrigger(content, channel, channelId, peerId, isAdmin) {
|
|
3003
|
-
|
|
3004
|
-
const
|
|
3007
|
+
// Resolve trigger manager/scheduler from the owning agent of this channel
|
|
3008
|
+
const owningAgent = this.getOwningAgent(channel);
|
|
3009
|
+
const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
|
|
3010
|
+
const manager = (owningAgent?.triggerManager ?? this.triggerManager);
|
|
3005
3011
|
// Bare /trigger → list active
|
|
3006
3012
|
if (content === '/trigger') {
|
|
3007
3013
|
if (!manager)
|
|
@@ -3386,9 +3392,10 @@ export class CommandHandler {
|
|
|
3386
3392
|
const taskId = replyContext?.metadata?.taskId;
|
|
3387
3393
|
const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
|
|
3388
3394
|
// --encrypt 覆盖 session 加密状态
|
|
3395
|
+
// 添加 source: 'ctl' 标记(用于区分 ec ctl send)
|
|
3389
3396
|
const enrichedReplyContext = forceEncrypt
|
|
3390
|
-
? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true } }
|
|
3391
|
-
: replyContext;
|
|
3397
|
+
? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true, source: 'ctl' } }
|
|
3398
|
+
: { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), source: 'ctl' } };
|
|
3392
3399
|
await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext: enrichedReplyContext }), { kind: 'result.text', text, isFinal: true });
|
|
3393
3400
|
// 出方向 jsonl 写入已下沉到 aun.ts:deliverTextEntry,message.send 成功后统一写入。
|
|
3394
3401
|
return { ok: true, result: 'ok' };
|
|
@@ -51,7 +51,7 @@ export function detectDuplicates(agents) {
|
|
|
51
51
|
export class EvolAgentRegistry {
|
|
52
52
|
_agentsDir;
|
|
53
53
|
agents = new Map();
|
|
54
|
-
/** channel key (`<
|
|
54
|
+
/** channel key (`<type>#<selfPeerId>#<name>`) → agent aid */
|
|
55
55
|
channelIndex = new Map();
|
|
56
56
|
/** 启动期被 ConfigStore 跳过的目录(命名非法 / 缺 config.json / 校验失败等) */
|
|
57
57
|
skipped = [];
|
|
@@ -199,6 +199,12 @@ export class EvolAgentRegistry {
|
|
|
199
199
|
logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${errs.join('; ')}`);
|
|
200
200
|
return null;
|
|
201
201
|
}
|
|
202
|
+
// Channel fingerprint 冲突检测(防止新 agent 复用已有 agent 的凭证)
|
|
203
|
+
const conflict = this.checkConflictForReload(raw, aid);
|
|
204
|
+
if (conflict) {
|
|
205
|
+
logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
202
208
|
const defaults = loadDefaults();
|
|
203
209
|
const merged = mergeForAgent(raw, defaults);
|
|
204
210
|
const agent = new EvolAgent(raw, merged);
|
|
@@ -224,12 +230,47 @@ export class EvolAgentRegistry {
|
|
|
224
230
|
throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
|
|
225
231
|
const defaults = loadDefaults();
|
|
226
232
|
const merged = mergeForAgent(raw, defaults);
|
|
233
|
+
// ── disabled → enabled 转换:需要完整启动流程 ──
|
|
234
|
+
if (oldAgent.status === 'disabled' && raw.enabled !== false) {
|
|
235
|
+
oldAgent.swapConfig(raw, merged);
|
|
236
|
+
const hotLoad = globalThis.__evolclaw_hotLoadAgent;
|
|
237
|
+
if (!hotLoad)
|
|
238
|
+
throw new Error(`Cannot enable agent "${aidOrName}": hot-load handler not initialized`);
|
|
239
|
+
// 从 registry 中移除旧的 disabled 实例,hotLoad 会重新创建
|
|
240
|
+
this.agents.delete(oldAgent.aid);
|
|
241
|
+
this.channelIndex.clear();
|
|
242
|
+
this.buildChannelIndex();
|
|
243
|
+
await hotLoad(oldAgent.aid);
|
|
244
|
+
logger.info(`[Reload] Agent "${aidOrName}" transitioned from disabled → enabled (full startup)`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// ── enabled → disabled 转换:断开所有 channel ──
|
|
248
|
+
if (oldAgent.status !== 'disabled' && raw.enabled === false) {
|
|
249
|
+
for (const ch of oldAgent.channelInstanceNames()) {
|
|
250
|
+
try {
|
|
251
|
+
await hooks.drainChannel(ch);
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
try {
|
|
255
|
+
await hooks.disconnectChannel(ch);
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
}
|
|
259
|
+
oldAgent.swapConfig(raw, merged);
|
|
260
|
+
oldAgent.status = 'disabled';
|
|
261
|
+
this.channelIndex.clear();
|
|
262
|
+
this.buildChannelIndex();
|
|
263
|
+
logger.info(`[Reload] Agent "${aidOrName}" disabled`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
227
266
|
const conflict = this.checkConflictForReload(raw, oldAgent.aid);
|
|
228
267
|
if (conflict)
|
|
229
268
|
throw new Error(`Channel conflict: ${conflict}`);
|
|
230
269
|
const oldChannels = new Set(oldAgent.channelInstanceNames());
|
|
231
|
-
// 计算新 channel keys
|
|
232
|
-
const
|
|
270
|
+
// 计算新 channel keys:隐式 AUN + 显式非 AUN channels(与 channelInstanceNames 逻辑一致)
|
|
271
|
+
const aunKey = oldAgent.effectiveChannelName('aun', 'main');
|
|
272
|
+
const otherKeys = raw.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
|
|
273
|
+
const newChannels = new Set([aunKey, ...otherKeys]);
|
|
233
274
|
const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
|
|
234
275
|
const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
|
|
235
276
|
const kept = [...oldChannels].filter(c => newChannels.has(c));
|
|
@@ -264,12 +305,7 @@ export class EvolAgentRegistry {
|
|
|
264
305
|
addedSuccessfully.push(ch);
|
|
265
306
|
}
|
|
266
307
|
// truly kept 的 adapter 实例已经在 oldAgent.channels 里,无需迁移
|
|
267
|
-
|
|
268
|
-
// 保持原态——swap 不改 status
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
oldAgent.status = 'running';
|
|
272
|
-
}
|
|
308
|
+
oldAgent.status = 'running';
|
|
273
309
|
// 重启触发器调度器(如果已初始化)
|
|
274
310
|
if (oldAgent.triggerScheduler) {
|
|
275
311
|
oldAgent.triggerScheduler.stop();
|
package/dist/core/evolagent.js
CHANGED
|
@@ -62,11 +62,11 @@ export class EvolAgent {
|
|
|
62
62
|
}
|
|
63
63
|
// ── Channels ──────────────────────────────────────────────────────────
|
|
64
64
|
/**
|
|
65
|
-
* effective channel key:`<
|
|
66
|
-
*
|
|
65
|
+
* effective channel key:`<type>#<urlEncode(selfPeerId)>#<name>`。
|
|
66
|
+
* AUN channel 的 selfPeerId 是 agent.aid,name 固定为 'main'。
|
|
67
67
|
*/
|
|
68
68
|
effectiveChannelName(type, rawName) {
|
|
69
|
-
return formatChannelKey({
|
|
69
|
+
return formatChannelKey({ type, selfPeerId: this.aid, name: rawName });
|
|
70
70
|
}
|
|
71
71
|
channelInstanceNames() {
|
|
72
72
|
// AUN channel 隐式存在(从 agent.aid 派生),不需要在 channels[] 里声明
|
|
@@ -97,7 +97,7 @@ export class EvolAgent {
|
|
|
97
97
|
*/
|
|
98
98
|
isAunChannelKey(channelKey) {
|
|
99
99
|
const parsed = tryParseChannelKey(channelKey);
|
|
100
|
-
return parsed?.type === 'aun' && parsed.
|
|
100
|
+
return parsed?.type === 'aun' && parsed.selfPeerId === this.aid;
|
|
101
101
|
}
|
|
102
102
|
getOwner(channelKey) {
|
|
103
103
|
if (this.isAunChannelKey(channelKey)) {
|
|
@@ -201,10 +201,10 @@ export class IMRenderer {
|
|
|
201
201
|
call_id: callId || this.synthCallId(),
|
|
202
202
|
name,
|
|
203
203
|
ok,
|
|
204
|
-
result,
|
|
205
|
-
error,
|
|
206
|
-
duration_ms: durationMs,
|
|
207
|
-
text: descText,
|
|
204
|
+
...(result !== undefined && { result }),
|
|
205
|
+
...(error !== undefined && { error }),
|
|
206
|
+
...(durationMs !== undefined && { duration_ms: durationMs }),
|
|
207
|
+
...(descText !== undefined && { text: descText }),
|
|
208
208
|
});
|
|
209
209
|
this.messageTimestamps.push(Date.now());
|
|
210
210
|
if (this.diagEnabled)
|
|
@@ -3,6 +3,8 @@ import { logger } from '../../utils/logger.js';
|
|
|
3
3
|
import { StreamDebouncer } from './stream-debouncer.js';
|
|
4
4
|
import { appendMessageLog, buildInboundEntry } from './message-log.js';
|
|
5
5
|
import { buildEnvelope } from './message-processor.js';
|
|
6
|
+
import { chatDirPath } from '../session/session-fs-store.js';
|
|
7
|
+
import { resolvePaths } from '../../paths.js';
|
|
6
8
|
/**
|
|
7
9
|
* MessageBridge — Channel 与 Core 之间的消息桥梁
|
|
8
10
|
*
|
|
@@ -74,8 +76,31 @@ export class MessageBridge {
|
|
|
74
76
|
// 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
|
|
75
77
|
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
76
78
|
const cmdContent = contentForCmd || content;
|
|
77
|
-
|
|
79
|
+
const isCmd = this.cmdHandler.isCommand(cmdContent);
|
|
80
|
+
if (isCmd) {
|
|
78
81
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
82
|
+
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
83
|
+
try {
|
|
84
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfId);
|
|
85
|
+
const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
|
|
86
|
+
const inboundChatmode = msg.replyContext?.metadata?.chatmode;
|
|
87
|
+
appendMessageLog(chatDir, buildInboundEntry({
|
|
88
|
+
from: msg.peerId || 'unknown',
|
|
89
|
+
to: msg.selfId || 'self',
|
|
90
|
+
chatType: msg.chatType || 'private',
|
|
91
|
+
groupId: msg.groupId ?? null,
|
|
92
|
+
msgId: msg.messageId ?? null,
|
|
93
|
+
content,
|
|
94
|
+
replyTo: msg.replyContext?.replyToMessageId ?? null,
|
|
95
|
+
permMode: null,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
encrypt: inboundEncrypt,
|
|
98
|
+
chatmode: inboundChatmode,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
|
|
103
|
+
}
|
|
79
104
|
}
|
|
80
105
|
if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
|
|
81
106
|
logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
|