evolclaw 2.5.0 → 2.5.1
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/dist/agents/claude-runner.js +53 -8
- package/dist/channels/aun.js +87 -0
- package/dist/cli.js +17 -1
- package/dist/core/command-handler.js +103 -29
- package/dist/core/message/message-bridge.js +19 -5
- package/dist/core/message/message-processor.js +6 -3
- package/dist/core/permission.js +7 -11
- package/dist/utils/init-channel.js +22 -4
- package/dist/utils/init.js +52 -1
- package/evolclaw-install.md +54 -0
- package/package.json +3 -2
|
@@ -259,14 +259,7 @@ export class AgentRunner {
|
|
|
259
259
|
}
|
|
260
260
|
// 等待用户交互
|
|
261
261
|
const answer = await new Promise((resolve) => {
|
|
262
|
-
const timer = setTimeout(() => {
|
|
263
|
-
permCtx?.interactionRouter?.cancel(requestId);
|
|
264
|
-
permCtx?.cancelIntercept?.(sessionId);
|
|
265
|
-
logger.info(`[AgentRunner] AskUserQuestion timeout for ${requestId}`);
|
|
266
|
-
resolve(null);
|
|
267
|
-
}, 5 * 60 * 1000);
|
|
268
262
|
permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
|
|
269
|
-
clearTimeout(timer);
|
|
270
263
|
if (action === 'cancel') {
|
|
271
264
|
resolve(null);
|
|
272
265
|
}
|
|
@@ -292,7 +285,7 @@ export class AgentRunner {
|
|
|
292
285
|
});
|
|
293
286
|
});
|
|
294
287
|
if (answer === null) {
|
|
295
|
-
//
|
|
288
|
+
// 取消,自动选第一项
|
|
296
289
|
const firstLabel = q.options[0]?.label || '';
|
|
297
290
|
answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
|
|
298
291
|
}
|
|
@@ -323,6 +316,54 @@ export class AgentRunner {
|
|
|
323
316
|
const updatedInput = { ...input, answers };
|
|
324
317
|
return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
|
|
325
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* 处理 ExitPlanMode 工具调用:plan mode 审批,等待用户批准后才继续执行
|
|
321
|
+
*/
|
|
322
|
+
async handleExitPlanMode(sessionId, input, options) {
|
|
323
|
+
const permCtx = this.permissionContexts.get(sessionId);
|
|
324
|
+
const sendPrompt = this.sendPromptFn;
|
|
325
|
+
// 无交互上下文,直接 allow(防御性兜底)
|
|
326
|
+
if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId || !sendPrompt) {
|
|
327
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
328
|
+
}
|
|
329
|
+
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
330
|
+
const interaction = {
|
|
331
|
+
type: 'interaction',
|
|
332
|
+
id: requestId,
|
|
333
|
+
kind: {
|
|
334
|
+
kind: 'action',
|
|
335
|
+
title: '📋 计划审批',
|
|
336
|
+
body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
|
|
337
|
+
buttons: [
|
|
338
|
+
{ key: 'approve', label: '✅ 批准执行', style: 'primary' },
|
|
339
|
+
{ key: 'reject', label: '❌ 拒绝', style: 'danger' },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
channelId: permCtx.channelId,
|
|
343
|
+
sessionId,
|
|
344
|
+
};
|
|
345
|
+
let cardSent = false;
|
|
346
|
+
try {
|
|
347
|
+
const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
|
|
348
|
+
cardSent = !!result;
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
|
|
352
|
+
}
|
|
353
|
+
if (!cardSent) {
|
|
354
|
+
await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n回复 /plan approve 批准执行 | /plan reject 拒绝');
|
|
355
|
+
}
|
|
356
|
+
return new Promise((resolve) => {
|
|
357
|
+
permCtx?.interactionRouter?.register(requestId, sessionId, (action) => {
|
|
358
|
+
if (action === 'approve') {
|
|
359
|
+
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
326
367
|
/**
|
|
327
368
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
328
369
|
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
@@ -538,6 +579,10 @@ export class AgentRunner {
|
|
|
538
579
|
if (toolName === 'AskUserQuestion') {
|
|
539
580
|
return await this.handleAskUserQuestion(sessionId, input, options);
|
|
540
581
|
}
|
|
582
|
+
// 特殊处理:ExitPlanMode 工具(plan mode 审批)
|
|
583
|
+
if (toolName === 'ExitPlanMode') {
|
|
584
|
+
return await this.handleExitPlanMode(sessionId, input, options);
|
|
585
|
+
}
|
|
541
586
|
// bypass 模式:一律 allow
|
|
542
587
|
if (this.permissionMode === 'bypass') {
|
|
543
588
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
|
package/dist/channels/aun.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AUNClient, GatewayDiscovery } from '@eleans/aun-core-sdk';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
5
6
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
6
7
|
import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
|
|
7
8
|
import { resolvePaths } from '../paths.js';
|
|
@@ -275,6 +276,8 @@ export class AUNChannel {
|
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
logger.info(`[AUN] Connected as ${this._aid}`);
|
|
279
|
+
// Send welcome message to owner after first connection
|
|
280
|
+
await this.sendWelcomeMessage();
|
|
278
281
|
}
|
|
279
282
|
catch (e) {
|
|
280
283
|
logger.error(`[AUN] Connection failed: ${e}`);
|
|
@@ -282,6 +285,90 @@ export class AUNChannel {
|
|
|
282
285
|
return;
|
|
283
286
|
}
|
|
284
287
|
}
|
|
288
|
+
async sendWelcomeMessage() {
|
|
289
|
+
try {
|
|
290
|
+
const owner = this.config.owner;
|
|
291
|
+
if (!owner) {
|
|
292
|
+
logger.info('[AUN] No owner configured, skipping welcome message');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Check agent.md initialized field
|
|
296
|
+
const aid = this.config.aid;
|
|
297
|
+
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
298
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
299
|
+
if (!fs.existsSync(agentMdPath)) {
|
|
300
|
+
logger.warn('[AUN] agent.md not found, skipping welcome message');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const agentMdContent = fs.readFileSync(agentMdPath, 'utf-8');
|
|
304
|
+
const match = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
|
|
305
|
+
if (!match) {
|
|
306
|
+
logger.warn('[AUN] agent.md frontmatter not found');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const frontmatter = match[1];
|
|
310
|
+
const initializedMatch = frontmatter.match(/^initialized:\s*(true|false)/m);
|
|
311
|
+
if (!initializedMatch || initializedMatch[1] === 'true') {
|
|
312
|
+
logger.info('[AUN] Agent already initialized, skipping welcome message');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Generate new agent.md with proper fields
|
|
316
|
+
const ownerShortId = owner.split('@')[0].slice(0, 8);
|
|
317
|
+
const newAgentMd = `---
|
|
318
|
+
aid: "${aid}"
|
|
319
|
+
name: "${ownerShortId}的Evol助手"
|
|
320
|
+
type: "codeagent"
|
|
321
|
+
version: "1.0.0"
|
|
322
|
+
description: "EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道"
|
|
323
|
+
tags:
|
|
324
|
+
- evolclaw
|
|
325
|
+
- ai-agent
|
|
326
|
+
- gateway
|
|
327
|
+
initialized: true
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
# ${ownerShortId}的Evol助手
|
|
331
|
+
|
|
332
|
+
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
333
|
+
`;
|
|
334
|
+
// Write locally
|
|
335
|
+
fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
|
|
336
|
+
logger.info('[AUN] Updated agent.md with initialized=true');
|
|
337
|
+
// Publish to AUN network via auth.uploadAgentMd
|
|
338
|
+
try {
|
|
339
|
+
await this.client.auth.uploadAgentMd(newAgentMd);
|
|
340
|
+
logger.info('[AUN] Published agent.md to AUN network');
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
logger.warn(`[AUN] Failed to publish agent.md: ${e}`);
|
|
344
|
+
}
|
|
345
|
+
// Send welcome message
|
|
346
|
+
const welcomeText = `🎉 欢迎使用 EvolClaw!
|
|
347
|
+
|
|
348
|
+
我是您的 AI Agent 网关,已成功连接到 AUN 网络。
|
|
349
|
+
|
|
350
|
+
📋 **日常使用方法**:
|
|
351
|
+
|
|
352
|
+
1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
|
|
353
|
+
2. **查看帮助**:发送 \`/help\` 查看所有可用命令
|
|
354
|
+
3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
|
|
355
|
+
4. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
356
|
+
5. **查看 Agent 信息**:发送 \`/agentmd\` 查看 agent.md 内容
|
|
357
|
+
6. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
358
|
+
|
|
359
|
+
💡 **提示**:
|
|
360
|
+
- 直接发送消息即可与 Claude/Codex 对话
|
|
361
|
+
- 支持多项目会话管理,每个项目独立会话
|
|
362
|
+
- 所有命令以 \`/\` 开头
|
|
363
|
+
|
|
364
|
+
现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
|
|
365
|
+
await this.sendMessage(owner, welcomeText);
|
|
366
|
+
logger.info(`[AUN] Welcome message sent to owner: ${owner}`);
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
logger.warn(`[AUN] Failed to send welcome message: ${e}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
285
372
|
// ── Event handlers ──────────────────────────────────────────
|
|
286
373
|
async downloadAttachment(att, channelId) {
|
|
287
374
|
const ownerAid = att.owner_aid || this._aid || '';
|
package/dist/cli.js
CHANGED
|
@@ -1306,6 +1306,10 @@ async function cmdCtl(args) {
|
|
|
1306
1306
|
}
|
|
1307
1307
|
}
|
|
1308
1308
|
// ==================== Main ====================
|
|
1309
|
+
function getArgValue(args, flag) {
|
|
1310
|
+
const idx = args.indexOf(flag);
|
|
1311
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1312
|
+
}
|
|
1309
1313
|
export async function main(args) {
|
|
1310
1314
|
const cmd = args[0] || 'start';
|
|
1311
1315
|
switch (cmd) {
|
|
@@ -1329,7 +1333,19 @@ export async function main(args) {
|
|
|
1329
1333
|
await cmdInitWecom();
|
|
1330
1334
|
}
|
|
1331
1335
|
else {
|
|
1332
|
-
|
|
1336
|
+
const nonInteractive = args.includes('--non-interactive');
|
|
1337
|
+
if (nonInteractive) {
|
|
1338
|
+
await cmdInit({
|
|
1339
|
+
nonInteractive: true,
|
|
1340
|
+
defaultPath: getArgValue(args, '--default-path') || process.cwd(),
|
|
1341
|
+
channel: getArgValue(args, '--channel') || 'aun',
|
|
1342
|
+
aunAid: getArgValue(args, '--aun-aid'),
|
|
1343
|
+
aunOwner: getArgValue(args, '--aun-owner'),
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
await cmdInit();
|
|
1348
|
+
}
|
|
1333
1349
|
}
|
|
1334
1350
|
break;
|
|
1335
1351
|
case 'start':
|
|
@@ -112,7 +112,7 @@ const aliases = {
|
|
|
112
112
|
'/rw': '/rewind'
|
|
113
113
|
};
|
|
114
114
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
115
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name
|
|
115
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity'];
|
|
116
116
|
export class CommandHandler {
|
|
117
117
|
sessionManager;
|
|
118
118
|
config;
|
|
@@ -338,23 +338,23 @@ export class CommandHandler {
|
|
|
338
338
|
items.push({
|
|
339
339
|
group: '项目管理',
|
|
340
340
|
commands: [
|
|
341
|
-
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
342
|
-
{ cmd: '/p',
|
|
343
|
-
...(isOwner ? [{ cmd: '/bind',
|
|
341
|
+
{ cmd: '/pwd', label: '显示当前项目路径', desc: '查看当前会话绑定的项目目录' },
|
|
342
|
+
{ cmd: '/p', label: '列出或切换项目', desc: '切换到其他已配置的项目', next: { type: 'select', dynamic: true } },
|
|
343
|
+
...(isOwner ? [{ cmd: '/bind', label: '绑定新项目目录', desc: '将当前会话绑定到指定项目路径', next: { type: 'text' } }] : []),
|
|
344
344
|
]
|
|
345
345
|
});
|
|
346
346
|
}
|
|
347
347
|
items.push({
|
|
348
348
|
group: '会话管理',
|
|
349
349
|
commands: [
|
|
350
|
-
{ cmd: '/new',
|
|
351
|
-
{ cmd: '/s',
|
|
352
|
-
{ cmd: '/name',
|
|
353
|
-
{ cmd: '/del',
|
|
350
|
+
{ cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
|
|
351
|
+
{ cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
|
|
352
|
+
{ cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
|
|
353
|
+
{ cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
|
|
354
354
|
...(isAdmin ? [
|
|
355
|
-
{ cmd: '/fork',
|
|
356
|
-
{ cmd: '/rewind',
|
|
357
|
-
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
355
|
+
{ cmd: '/fork', label: '分支当前会话', desc: '基于当前会话创建独立分支', next: { type: 'text' } },
|
|
356
|
+
{ cmd: '/rewind', label: '查看历史/撤销指定轮次', desc: '回退会话到指定轮次,可选择撤销文件改动' },
|
|
357
|
+
{ cmd: '/compact', label: '压缩会话上下文', desc: '将长对话压缩为摘要以节省 token' },
|
|
358
358
|
] : []),
|
|
359
359
|
]
|
|
360
360
|
});
|
|
@@ -362,31 +362,55 @@ export class CommandHandler {
|
|
|
362
362
|
items.push({
|
|
363
363
|
group: 'Agent 与模型',
|
|
364
364
|
commands: [
|
|
365
|
-
{ cmd: '/agent',
|
|
366
|
-
{ cmd: '/model',
|
|
367
|
-
{ cmd: '/effort',
|
|
365
|
+
{ cmd: '/agent', label: '切换 Agent 后端', desc: '切换当前会话使用的 AI 后端', next: { type: 'select', dynamic: true } },
|
|
366
|
+
{ cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
|
|
367
|
+
{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
|
|
368
|
+
{ value: 'low', label: 'Low' },
|
|
369
|
+
{ value: 'medium', label: 'Medium' },
|
|
370
|
+
{ value: 'high', label: 'High' },
|
|
371
|
+
{ value: 'max', label: 'Max' },
|
|
372
|
+
] } },
|
|
368
373
|
]
|
|
369
374
|
});
|
|
370
375
|
items.push({
|
|
371
376
|
group: '权限管理',
|
|
372
377
|
commands: [
|
|
373
|
-
{ cmd: '/perm',
|
|
378
|
+
{ cmd: '/perm', label: '权限模式管理', desc: '控制工具调用的审批策略', next: { type: 'select', items: [
|
|
379
|
+
...(isOwner ? [
|
|
380
|
+
{ value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
|
|
381
|
+
{ value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
|
|
382
|
+
{ value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
|
|
383
|
+
{ value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
|
|
384
|
+
{ value: 'request', label: '请求模式', desc: '所有操作均需审批' },
|
|
385
|
+
{ value: 'noask', label: '静默模式', desc: '不弹出审批,自动拒绝未授权操作' },
|
|
386
|
+
] : []),
|
|
387
|
+
{ value: 'allow', label: '允许此操作', desc: '本次允许当前待审批操作' },
|
|
388
|
+
{ value: 'always', label: '始终允许', desc: '永久允许同类操作' },
|
|
389
|
+
{ value: 'deny', label: '拒绝此操作', desc: '拒绝当前待审批操作' },
|
|
390
|
+
] } },
|
|
374
391
|
]
|
|
375
392
|
});
|
|
376
393
|
items.push({
|
|
377
394
|
group: '运维',
|
|
378
395
|
commands: [
|
|
379
|
-
{ cmd: '/status', label: '显示会话状态' },
|
|
380
|
-
{ cmd: '/stop', label: '中断当前任务' },
|
|
381
|
-
{ cmd: '/check', label: '检查渠道状态' },
|
|
382
|
-
{ cmd: '/activity',
|
|
396
|
+
{ cmd: '/status', label: '显示会话状态', desc: '查看当前会话、项目、Agent 的详细状态' },
|
|
397
|
+
{ cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' },
|
|
398
|
+
{ cmd: '/check', label: '检查渠道状态', desc: '检查各消息渠道的连接健康状态' },
|
|
399
|
+
{ cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
|
|
400
|
+
{ value: 'all', label: '全部显示', desc: '所有用户均可见中间输出' },
|
|
401
|
+
{ value: 'dm', label: '仅私聊', desc: '仅私聊中显示中间输出' },
|
|
402
|
+
{ value: 'owner', label: '仅 owner 私聊', desc: '仅 owner 的私聊中显示' },
|
|
403
|
+
{ value: 'none', label: '不显示', desc: '关闭所有中间输出' },
|
|
404
|
+
] } },
|
|
383
405
|
...(isAdmin ? [
|
|
384
|
-
{ cmd: '/restart',
|
|
406
|
+
{ cmd: '/restart', label: '重启/重连', desc: '重启服务或重连指定渠道', next: { type: 'select', dynamic: true } },
|
|
385
407
|
] : []),
|
|
386
408
|
...(isOwner ? [
|
|
387
|
-
{ cmd: '/
|
|
388
|
-
{ cmd: '/
|
|
389
|
-
|
|
409
|
+
{ cmd: '/send', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
410
|
+
{ cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
|
|
411
|
+
{ value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
|
|
412
|
+
{ value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
|
|
413
|
+
] } },
|
|
390
414
|
] : []),
|
|
391
415
|
]
|
|
392
416
|
});
|
|
@@ -395,22 +419,67 @@ export class CommandHandler {
|
|
|
395
419
|
items.push({
|
|
396
420
|
group: '其他',
|
|
397
421
|
commands: [
|
|
398
|
-
{ cmd: '/status', label: '显示会话状态' },
|
|
399
|
-
{ cmd: '/check', label: '检查渠道健康' },
|
|
422
|
+
{ cmd: '/status', label: '显示会话状态', desc: '查看当前会话的基本状态' },
|
|
423
|
+
{ cmd: '/check', label: '检查渠道健康', desc: '检查消息渠道连接状态' },
|
|
400
424
|
]
|
|
401
425
|
});
|
|
402
426
|
}
|
|
403
427
|
items.push({
|
|
404
428
|
group: '帮助',
|
|
405
429
|
commands: [
|
|
406
|
-
{ cmd: '/help', label: '显示帮助信息' },
|
|
430
|
+
{ cmd: '/help', label: '显示帮助信息', desc: '列出所有可用命令及说明' },
|
|
407
431
|
]
|
|
408
432
|
});
|
|
409
433
|
return items;
|
|
410
434
|
}
|
|
411
|
-
/**
|
|
412
|
-
|
|
413
|
-
|
|
435
|
+
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
436
|
+
async getSubMenuItems(cmd, channel, channelId, userId) {
|
|
437
|
+
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
438
|
+
if (cmd === '/s' || cmd === '/del') {
|
|
439
|
+
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
440
|
+
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
441
|
+
const items = sessions
|
|
442
|
+
.filter(s => !active || s.id !== active.id)
|
|
443
|
+
.map(s => {
|
|
444
|
+
const shortId = s.agentSessionId ? s.agentSessionId.substring(0, 8) : '';
|
|
445
|
+
const time = s.updatedAt ? formatIdleTime(Date.now() - s.updatedAt) : '';
|
|
446
|
+
const parts = [shortId, time].filter(Boolean).join(' · ');
|
|
447
|
+
return {
|
|
448
|
+
value: s.name || s.id.slice(0, 8),
|
|
449
|
+
label: s.name || s.id.slice(0, 8),
|
|
450
|
+
desc: parts || undefined,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
if (cmd === '/s') {
|
|
454
|
+
items.push({ value: 'cli', label: '查看 CLI 会话', desc: '列出未导入的 CLI 本地会话' });
|
|
455
|
+
}
|
|
456
|
+
return items;
|
|
457
|
+
}
|
|
458
|
+
if (cmd === '/p') {
|
|
459
|
+
const list = this.config.projects?.list || {};
|
|
460
|
+
return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
|
|
461
|
+
}
|
|
462
|
+
if (cmd === '/agent') {
|
|
463
|
+
return [...this.agentMap.keys()].map(name => ({ value: name, label: name }));
|
|
464
|
+
}
|
|
465
|
+
if (cmd === '/model') {
|
|
466
|
+
const agent = this.getAgent(session?.agentId);
|
|
467
|
+
if (hasModelSwitcher(agent) && agent.listModels) {
|
|
468
|
+
const models = await agent.listModels() ?? [];
|
|
469
|
+
if (models.length > 0)
|
|
470
|
+
return models.map((m) => ({ value: m, label: m }));
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
if (cmd === '/restart') {
|
|
475
|
+
const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
|
|
476
|
+
const channels = [...this.adapters.keys()].map(name => ({ value: name, label: name, desc: '重连此渠道' }));
|
|
477
|
+
if (isOwner)
|
|
478
|
+
channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
|
|
479
|
+
return channels;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
414
483
|
isCommand(content) {
|
|
415
484
|
return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
|
|
416
485
|
}
|
|
@@ -1879,6 +1948,8 @@ export class CommandHandler {
|
|
|
1879
1948
|
return response;
|
|
1880
1949
|
}
|
|
1881
1950
|
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
1951
|
+
if (normalizedContent === '/bind')
|
|
1952
|
+
return '用法: /bind <路径>';
|
|
1882
1953
|
if (normalizedContent.startsWith('/bind ')) {
|
|
1883
1954
|
if (!isOwner)
|
|
1884
1955
|
return '❌ 无权限:此命令仅限 owner 使用';
|
|
@@ -2205,6 +2276,9 @@ export class CommandHandler {
|
|
|
2205
2276
|
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
|
|
2206
2277
|
}
|
|
2207
2278
|
// /rename 或 /name 命令:重命名当前会话
|
|
2279
|
+
if (normalizedContent === '/rename' || normalizedContent === '/name') {
|
|
2280
|
+
return '用法: /name <新名称> 或 /rename <新名称>';
|
|
2281
|
+
}
|
|
2208
2282
|
if (normalizedContent.startsWith('/rename ')) {
|
|
2209
2283
|
const newName = normalizedContent.slice(8).trim();
|
|
2210
2284
|
if (!newName)
|
|
@@ -152,13 +152,27 @@ export class MessageBridge {
|
|
|
152
152
|
return false;
|
|
153
153
|
if (parsed.type === 'menu.query') {
|
|
154
154
|
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
if (parsed.cmd) {
|
|
156
|
+
// 动态子菜单查询
|
|
157
|
+
const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
|
|
158
|
+
const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
|
|
159
|
+
if (adapter?.sendCustomPayload) {
|
|
160
|
+
adapter.sendCustomPayload(msg.channelId, response);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await sendReply(msg.channelId, response);
|
|
164
|
+
}
|
|
159
165
|
}
|
|
160
166
|
else {
|
|
161
|
-
|
|
167
|
+
// 全量菜单
|
|
168
|
+
const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
|
|
169
|
+
const response = JSON.stringify({ type: 'menu.response', items });
|
|
170
|
+
if (adapter?.sendCustomPayload) {
|
|
171
|
+
adapter.sendCustomPayload(msg.channelId, response);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await sendReply(msg.channelId, response);
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
return true;
|
|
164
178
|
}
|
|
@@ -337,15 +337,18 @@ export class MessageProcessor {
|
|
|
337
337
|
// 保存当前 flusher,用于 compact 事件
|
|
338
338
|
this.currentFlusher = flusher;
|
|
339
339
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
340
|
+
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
341
|
+
const capturedChannelId = message.channelId;
|
|
342
|
+
const capturedReplyContext = this.getReplyContext(message);
|
|
340
343
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
341
344
|
agent.setSendPrompt(async (text) => {
|
|
342
|
-
await adapter.sendText(
|
|
345
|
+
await adapter.sendText(capturedChannelId, text, capturedReplyContext);
|
|
343
346
|
});
|
|
344
347
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
345
348
|
agent.setPermissionContext?.(session.id, {
|
|
346
349
|
adapter,
|
|
347
|
-
channelId:
|
|
348
|
-
replyContext:
|
|
350
|
+
channelId: capturedChannelId,
|
|
351
|
+
replyContext: capturedReplyContext,
|
|
349
352
|
interactionRouter: this.interactionRouter,
|
|
350
353
|
interceptNextMessage: this.messageQueue
|
|
351
354
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
package/dist/core/permission.js
CHANGED
|
@@ -94,6 +94,12 @@ export function summarizeToolInput(toolName, input) {
|
|
|
94
94
|
'Glob': (i) => `pattern: ${i.pattern}`,
|
|
95
95
|
'Agent': (i) => i.description || i.prompt?.substring(0, 80),
|
|
96
96
|
'Skill': (i) => i.skill ? `${i.skill}${i.args ? ' ' + i.args : ''}` : undefined,
|
|
97
|
+
'ExitPlanMode': (i) => {
|
|
98
|
+
if (i.allowedPrompts?.length) {
|
|
99
|
+
return `计划包含 ${i.allowedPrompts.length} 项操作权限`;
|
|
100
|
+
}
|
|
101
|
+
return '计划审批';
|
|
102
|
+
},
|
|
97
103
|
'TodoWrite': (i) => {
|
|
98
104
|
if (Array.isArray(i.todos)) {
|
|
99
105
|
return i.todos.map((t) => t.content || t.task || t.text).filter(Boolean).join(', ').substring(0, 80);
|
|
@@ -186,7 +192,6 @@ export class PermissionGateway {
|
|
|
186
192
|
},
|
|
187
193
|
channelId: context?.channelId || '',
|
|
188
194
|
sessionId,
|
|
189
|
-
expiresAt: Date.now() + this.timeout,
|
|
190
195
|
};
|
|
191
196
|
// 尝试富交互
|
|
192
197
|
let interactionSent = false;
|
|
@@ -204,16 +209,7 @@ export class PermissionGateway {
|
|
|
204
209
|
await sendPrompt(`🔐 权限请求\n工具:${toolName}\n操作:${displaySummary}${reasonLine}\n\n回复 /perm allow 本次允许 | always 始终允许 | deny 拒绝`);
|
|
205
210
|
}
|
|
206
211
|
return new Promise((resolve) => {
|
|
207
|
-
|
|
208
|
-
this.pending.delete(requestId);
|
|
209
|
-
// 清理 router 注册(仅删除本次请求,不影响其他交互)
|
|
210
|
-
if (interactionSent && context?.interactionRouter) {
|
|
211
|
-
context.interactionRouter.cancel(requestId);
|
|
212
|
-
}
|
|
213
|
-
this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId });
|
|
214
|
-
resolve('deny');
|
|
215
|
-
}, this.timeout);
|
|
216
|
-
this.pending.set(requestId, { sessionId, toolName, resolve, timer });
|
|
212
|
+
this.pending.set(requestId, { sessionId, toolName, resolve, timer: setTimeout(() => { }, 0) });
|
|
217
213
|
// 如果发了交互卡片,同时注册到 InteractionRouter
|
|
218
214
|
if (interactionSent && context?.interactionRouter) {
|
|
219
215
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|
|
@@ -18,7 +18,7 @@ import { normalizeChannelInstances } from '../config.js';
|
|
|
18
18
|
import { selectInstance } from './init.js';
|
|
19
19
|
import { isWindows } from './cross-platform.js';
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
|
-
async function npmInstallGlobal(pkg) {
|
|
21
|
+
export async function npmInstallGlobal(pkg) {
|
|
22
22
|
try {
|
|
23
23
|
await execFileAsync('npm', ['install', '-g', pkg], { timeout: 180000 });
|
|
24
24
|
}
|
|
@@ -575,7 +575,7 @@ function compareVersion(a, min) {
|
|
|
575
575
|
return parts[1] > min[1];
|
|
576
576
|
return parts[2] >= min[2];
|
|
577
577
|
}
|
|
578
|
-
function resolveAunCoreSdkPkg() {
|
|
578
|
+
export function resolveAunCoreSdkPkg() {
|
|
579
579
|
try {
|
|
580
580
|
const esmRequire = createRequire(import.meta.url);
|
|
581
581
|
const entry = esmRequire.resolve(AUN_CORE_SDK_PKG);
|
|
@@ -703,7 +703,7 @@ export async function setupAunAid(rl, _config) {
|
|
|
703
703
|
const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
|
|
704
704
|
const agentType = typeInput === 'human' ? 'human' : 'ai';
|
|
705
705
|
const agentName = aid.split('.')[0];
|
|
706
|
-
const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\n---\n`;
|
|
706
|
+
const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
|
|
707
707
|
try {
|
|
708
708
|
await client.auth.uploadAgentMd(agentMdContent);
|
|
709
709
|
console.log(' ✓ agent.md 已发布');
|
|
@@ -732,7 +732,24 @@ export async function setupAunAid(rl, _config) {
|
|
|
732
732
|
break;
|
|
733
733
|
// default: retry with new AID
|
|
734
734
|
}
|
|
735
|
-
|
|
735
|
+
// Owner 必填
|
|
736
|
+
console.log('\n📋 Owner 配置');
|
|
737
|
+
console.log(' Owner 将接收欢迎消息并拥有管理权限');
|
|
738
|
+
let owner = '';
|
|
739
|
+
while (!owner) {
|
|
740
|
+
const ownerInput = (await ask(rl, ' Owner AID (必填): ')).trim();
|
|
741
|
+
if (!ownerInput) {
|
|
742
|
+
console.log(' ⚠ Owner AID 不能为空');
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (!isValidAid(ownerInput)) {
|
|
746
|
+
console.log(' ⚠ Owner AID 格式无效');
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
owner = ownerInput;
|
|
750
|
+
console.log(` ✓ Owner 已设置: ${owner}`);
|
|
751
|
+
}
|
|
752
|
+
return { aid, owner };
|
|
736
753
|
}
|
|
737
754
|
export async function cmdInitAun() {
|
|
738
755
|
const p = resolvePaths();
|
|
@@ -761,6 +778,7 @@ export async function cmdInitAun() {
|
|
|
761
778
|
config.channels.aun = {
|
|
762
779
|
enabled: true,
|
|
763
780
|
aid: result.aid,
|
|
781
|
+
owner: result.owner,
|
|
764
782
|
};
|
|
765
783
|
if (!config.channels.defaultChannel)
|
|
766
784
|
config.channels.defaultChannel = 'aun';
|
package/dist/utils/init.js
CHANGED
|
@@ -376,7 +376,7 @@ async function offerRichContentRenderer(rl, config) {
|
|
|
376
376
|
// ==================== AUN AID Helpers ====================
|
|
377
377
|
// Moved to init-channel.ts
|
|
378
378
|
// ==================== Main ====================
|
|
379
|
-
export async function cmdInit() {
|
|
379
|
+
export async function cmdInit(options) {
|
|
380
380
|
const p = resolvePaths();
|
|
381
381
|
ensureDataDirs();
|
|
382
382
|
if (fs.existsSync(p.pid)) {
|
|
@@ -393,6 +393,56 @@ export async function cmdInit() {
|
|
|
393
393
|
console.log(`❌ 找不到示例配置: ${sampleSrc}`);
|
|
394
394
|
return;
|
|
395
395
|
}
|
|
396
|
+
// 非交互式模式
|
|
397
|
+
if (options?.nonInteractive) {
|
|
398
|
+
const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
|
|
399
|
+
const defaultPath = options.defaultPath || path.join(os.homedir(), 'evolclaw-project');
|
|
400
|
+
if (!fs.existsSync(defaultPath))
|
|
401
|
+
fs.mkdirSync(defaultPath, { recursive: true });
|
|
402
|
+
config.projects.defaultPath = defaultPath;
|
|
403
|
+
config.projects.list = { [path.basename(defaultPath)]: defaultPath };
|
|
404
|
+
if (options.channel === 'aun' && options.aunAid) {
|
|
405
|
+
// 自动安装 AUN SDK
|
|
406
|
+
const { resolveAunCoreSdkPkg, npmInstallGlobal } = await import('./init-channel.js');
|
|
407
|
+
if (!resolveAunCoreSdkPkg()) {
|
|
408
|
+
console.log('正在安装 @eleans/aun-core-sdk...');
|
|
409
|
+
await npmInstallGlobal('@eleans/aun-core-sdk@latest');
|
|
410
|
+
}
|
|
411
|
+
// 创建 AID(如果本地不存在)
|
|
412
|
+
const aunPath = path.join(os.homedir(), '.aun');
|
|
413
|
+
const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
|
|
414
|
+
if (!fs.existsSync(path.join(aidDir, 'private'))) {
|
|
415
|
+
const { AUNClient } = await import('@eleans/aun-core-sdk');
|
|
416
|
+
const client = new AUNClient({ aun_path: aunPath });
|
|
417
|
+
const domain = options.aunAid.split('.').slice(1).join('.');
|
|
418
|
+
client._gatewayUrl = `wss://gateway.${domain}:443/aun`;
|
|
419
|
+
await client.auth.createAid({ aid: options.aunAid });
|
|
420
|
+
// 写入初始 agent.md(initialized: false)
|
|
421
|
+
const agentName = options.aunAid.split('.')[0];
|
|
422
|
+
const agentMd = `---\naid: "${options.aunAid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
|
|
423
|
+
try {
|
|
424
|
+
await client.auth.uploadAgentMd(agentMd);
|
|
425
|
+
fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMd, 'utf-8');
|
|
426
|
+
}
|
|
427
|
+
catch { }
|
|
428
|
+
try {
|
|
429
|
+
await client.close();
|
|
430
|
+
}
|
|
431
|
+
catch { }
|
|
432
|
+
}
|
|
433
|
+
config.channels.aun = {
|
|
434
|
+
enabled: true,
|
|
435
|
+
aid: options.aunAid,
|
|
436
|
+
...(options.aunOwner && { owner: options.aunOwner }),
|
|
437
|
+
};
|
|
438
|
+
config.channels.defaultChannel = 'aun';
|
|
439
|
+
}
|
|
440
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
441
|
+
console.log(`✓ 已创建配置文件: ${p.config}`);
|
|
442
|
+
setupEnvVar(resolveRoot());
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// 交互式模式
|
|
396
446
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
397
447
|
try {
|
|
398
448
|
if (fs.existsSync(p.config)) {
|
|
@@ -490,6 +540,7 @@ export async function cmdInit() {
|
|
|
490
540
|
config.channels.aun = {
|
|
491
541
|
enabled: true,
|
|
492
542
|
aid: result.aid,
|
|
543
|
+
owner: result.owner,
|
|
493
544
|
};
|
|
494
545
|
channelConfigured = true;
|
|
495
546
|
config.channels.defaultChannel = 'aun';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
请引导我完成 EvolClaw 的安装和配置。EvolClaw 是一个轻量级 AI Agent 网关,连接 Claude/Codex 到飞书、微信等 IM 通道。
|
|
2
|
+
|
|
3
|
+
**重要提示**:安装完成后,首次在 IM 渠道(飞书/微信)中与 EvolClaw 交互的用户将自动成为 owner,拥有完整管理权限。
|
|
4
|
+
|
|
5
|
+
按以下步骤执行:
|
|
6
|
+
|
|
7
|
+
## 第 1 步:环境检查
|
|
8
|
+
|
|
9
|
+
运行 `node --version` 和 `npm --version`,确认 Node.js >= 18、npm >= 8。
|
|
10
|
+
如果不符合要求,提示用户先升级 Node.js(https://nodejs.org)后停止。
|
|
11
|
+
|
|
12
|
+
## 第 2 步:确认安装
|
|
13
|
+
|
|
14
|
+
使用 AskUserQuestion 询问:
|
|
15
|
+
- 问题:是否立即安装 EvolClaw?
|
|
16
|
+
- 选项 A:立即安装
|
|
17
|
+
- 选项 B:仅显示命令,我稍后手动执行
|
|
18
|
+
|
|
19
|
+
如果选择 B,输出 `npm install -g evolclaw` 后结束。
|
|
20
|
+
|
|
21
|
+
## 第 3 步:执行安装
|
|
22
|
+
|
|
23
|
+
运行 `npm install -g evolclaw`,然后 `evolclaw --version` 验证。
|
|
24
|
+
|
|
25
|
+
如果失败且是权限问题,提示:
|
|
26
|
+
```
|
|
27
|
+
sudo npm install -g evolclaw
|
|
28
|
+
# 或配置 npm prefix(推荐):
|
|
29
|
+
npm config set prefix ~/.npm-global && export PATH=~/.npm-global/bin:$PATH
|
|
30
|
+
npm install -g evolclaw
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 第 4 步:初始化配置
|
|
34
|
+
|
|
35
|
+
运行 `evolclaw init`。
|
|
36
|
+
|
|
37
|
+
提示用户:EvolClaw 自动继承 Claude Code CLI 的 API Key,无需重复配置。
|
|
38
|
+
|
|
39
|
+
## 第 5 步:配置消息渠道
|
|
40
|
+
|
|
41
|
+
使用 AskUserQuestion(multiSelect: true)询问需要配置哪些渠道:
|
|
42
|
+
- 飞书:运行 `evolclaw init feishu`(扫码登录)
|
|
43
|
+
- 微信:运行 `evolclaw init wechat`(扫码登录)
|
|
44
|
+
- 暂不配置:跳过
|
|
45
|
+
|
|
46
|
+
## 第 6 步:启动并验证
|
|
47
|
+
|
|
48
|
+
运行 `evolclaw start`,再运行 `evolclaw status` 确认状态为 Running。
|
|
49
|
+
|
|
50
|
+
安装完成后告知用户:
|
|
51
|
+
- 在飞书/微信中发消息验证连接
|
|
52
|
+
- 发送 `/help` 查看所有命令
|
|
53
|
+
- 发送 `/bind <项目路径>` 绑定项目目录
|
|
54
|
+
- 常用命令:`evolclaw stop/restart/logs`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolclaw",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
12
|
"!dist/experimental/",
|
|
13
|
-
"data/evolclaw.sample.json"
|
|
13
|
+
"data/evolclaw.sample.json",
|
|
14
|
+
"evolclaw-install.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"dev": "tsx watch src/index.ts",
|