evolclaw 2.7.3 → 2.8.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.
@@ -204,7 +204,7 @@ export class AgentRunner {
204
204
  // 没有交互上下文(无渠道适配器),回退到纯文本
205
205
  const permCtx = this.permissionContexts.get(sessionId);
206
206
  if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId) {
207
- return this.handleAskUserQuestionFallback(input, questions);
207
+ return this.handleAskUserQuestionFallback(sessionId, input, questions);
208
208
  }
209
209
  const answers = {};
210
210
  // 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
@@ -307,20 +307,43 @@ export class AgentRunner {
307
307
  return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
308
308
  }
309
309
  /**
310
- * AskUserQuestion 纯文本 fallback:发送文本格式的问题,直接 allow
310
+ * AskUserQuestion 纯文本 fallback:发送选项列表,等待用户通过 /ask 命令选择
311
+ * 注册到 interactionRouter,用户回复 /ask 1 或 /ask 自定义内容
311
312
  */
312
- async handleAskUserQuestionFallback(input, questions) {
313
- // 自动选择每个问题的第一个选项
313
+ async handleAskUserQuestionFallback(sessionId, input, questions) {
314
+ const permCtx = this.permissionContexts.get(sessionId);
315
+ const sendPrompt = permCtx?.adapter && permCtx?.channelId
316
+ ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
317
+ : this.sendPromptFn;
314
318
  const answers = {};
315
319
  if (questions?.length) {
316
- const lines = questions.map(q => {
317
- const firstLabel = q.options[0]?.label || '';
318
- answers[q.question] = firstLabel;
320
+ for (const q of questions) {
319
321
  const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
320
- return `${q.question}\n${optText}\n 自动选择:${firstLabel}`;
321
- });
322
- if (this.sendPromptFn) {
323
- await this.sendPromptFn(`💬 以下问题已自动选择第一项:\n\n${lines.join('\n\n')}`);
322
+ const prompt = `💬 ${q.question}\n${optText}\n\n回复 /ask <数字> 选择,或 /ask <自定义内容>`;
323
+ if (sendPrompt && permCtx?.interactionRouter) {
324
+ await sendPrompt(prompt);
325
+ const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
326
+ const answer = await new Promise((resolve) => {
327
+ permCtx.interactionRouter.register(requestId, sessionId, (action) => {
328
+ const num = parseInt(action.trim(), 10);
329
+ if (num >= 1 && num <= q.options.length) {
330
+ resolve(q.options[num - 1].label);
331
+ }
332
+ else {
333
+ resolve(action.trim());
334
+ }
335
+ }, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || '') });
336
+ });
337
+ answers[q.question] = answer;
338
+ }
339
+ else {
340
+ // 无交互能力,自动选第一项
341
+ const firstLabel = q.options[0]?.label || '';
342
+ answers[q.question] = firstLabel;
343
+ if (sendPrompt) {
344
+ await sendPrompt(`${prompt}\n → 自动选择:${firstLabel}`);
345
+ }
346
+ }
324
347
  }
325
348
  }
326
349
  const updatedInput = { ...input, answers };
@@ -331,51 +354,71 @@ export class AgentRunner {
331
354
  */
332
355
  async handleExitPlanMode(sessionId, input, options) {
333
356
  const permCtx = this.permissionContexts.get(sessionId);
334
- // 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
335
357
  const sendPrompt = permCtx?.adapter && permCtx?.channelId
336
358
  ? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
337
359
  : this.sendPromptFn;
338
- // 无交互上下文,直接 allow(防御性兜底)
339
- if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId || !sendPrompt) {
360
+ // 无任何交互能力,直接 allow
361
+ if (!permCtx?.channelId || !sendPrompt) {
340
362
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
341
363
  }
342
- const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
343
- const interaction = {
344
- type: 'interaction',
345
- id: requestId,
346
- kind: {
347
- kind: 'action',
348
- title: '📋 计划审批',
349
- body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
350
- buttons: [
351
- { key: 'approve', label: '✅ 批准执行', style: 'primary' },
352
- { key: 'reject', label: '❌ 拒绝', style: 'danger' },
353
- ],
354
- },
355
- channelId: permCtx.channelId,
356
- sessionId,
357
- };
364
+ // 尝试发送交互卡片
358
365
  let cardSent = false;
359
- try {
360
- const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
361
- cardSent = !!result;
362
- }
363
- catch (err) {
364
- logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
365
- }
366
- if (!cardSent) {
367
- await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n回复 /plan approve 批准执行 | /plan reject 拒绝');
366
+ if (permCtx.adapter?.sendInteraction) {
367
+ const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
368
+ const interaction = {
369
+ type: 'interaction',
370
+ id: requestId,
371
+ kind: {
372
+ kind: 'action',
373
+ title: '📋 计划审批',
374
+ body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
375
+ buttons: [
376
+ { key: 'approve', label: '✅ 批准执行', style: 'primary' },
377
+ { key: 'reject', label: '❌ 拒绝', style: 'danger' },
378
+ ],
379
+ },
380
+ channelId: permCtx.channelId,
381
+ sessionId,
382
+ };
383
+ try {
384
+ const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
385
+ cardSent = !!result;
386
+ }
387
+ catch (err) {
388
+ logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
389
+ }
390
+ if (cardSent) {
391
+ return new Promise((resolve) => {
392
+ permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
393
+ if (action === 'approve') {
394
+ resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
395
+ }
396
+ else {
397
+ resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
398
+ }
399
+ });
400
+ });
401
+ }
368
402
  }
369
- return new Promise((resolve) => {
370
- permCtx?.interactionRouter?.register(requestId, sessionId, (action) => {
371
- if (action === 'approve') {
372
- resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
373
- }
374
- else {
375
- resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
376
- }
403
+ // 文本 fallback:注册到 interactionRouter,等待用户 /ask 回复
404
+ if (permCtx.interactionRouter) {
405
+ await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n\n 1. 批准执行\n 2. 拒绝\n\n回复 /ask 1 批准,/ask 2 拒绝:');
406
+ const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
407
+ return new Promise((resolve) => {
408
+ permCtx.interactionRouter.register(requestId, sessionId, (action) => {
409
+ const trimmed = action.trim();
410
+ if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
411
+ resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
412
+ }
413
+ else {
414
+ resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
415
+ }
416
+ }, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }) });
377
417
  });
378
- });
418
+ }
419
+ // 无交互能力,发提示后直接 allow
420
+ await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
421
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
379
422
  }
380
423
  /**
381
424
  * SDK 原始事件 → 标准 AgentEvent 转换
@@ -0,0 +1,275 @@
1
+ /**
2
+ * AUN AID / agent.md 原子操作层
3
+ *
4
+ * 短生命周期操作(创建 AID、上传 agent.md 等),供 CLI / ctl / in-chat 三层共享。
5
+ * 与 aun.ts(长连接运行时)分离。
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import { fileURLToPath } from 'url';
11
+ import { execFileSync } from 'child_process';
12
+ import { isWindows } from '../utils/cross-platform.js';
13
+ import { resolvePaths } from '../paths.js';
14
+ // ==================== Constants ====================
15
+ export const MIN_AUN_CORE_SDK = [0, 2, 14];
16
+ export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
17
+ // ==================== SDK & Environment ====================
18
+ function compareVersion(a, min) {
19
+ const parts = a.split('.').map(n => parseInt(n, 10));
20
+ if (parts.length < 3 || parts.some(isNaN))
21
+ return false;
22
+ if (parts[0] !== min[0])
23
+ return parts[0] > min[0];
24
+ if (parts[1] !== min[1])
25
+ return parts[1] > min[1];
26
+ return parts[2] >= min[2];
27
+ }
28
+ export function isAunSdkVersionOk(version) {
29
+ return compareVersion(version, MIN_AUN_CORE_SDK);
30
+ }
31
+ export function resolveAunCoreSdkPkg() {
32
+ try {
33
+ let dir = path.dirname(fileURLToPath(import.meta.url));
34
+ while (true) {
35
+ const candidate = path.join(dir, 'node_modules', AUN_CORE_SDK_PKG, 'package.json');
36
+ if (fs.existsSync(candidate)) {
37
+ const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
38
+ if (data.name === AUN_CORE_SDK_PKG)
39
+ return { version: data.version, path: candidate };
40
+ }
41
+ const parent = path.dirname(dir);
42
+ if (parent === dir)
43
+ break;
44
+ dir = parent;
45
+ }
46
+ }
47
+ catch { /* fall through */ }
48
+ try {
49
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
50
+ const globalRoot = execFileSync(npmCmd, ['root', '-g'], {
51
+ encoding: 'utf-8', timeout: 10000, shell: isWindows,
52
+ }).trim();
53
+ const pkgPath = path.join(globalRoot, AUN_CORE_SDK_PKG, 'package.json');
54
+ if (fs.existsSync(pkgPath)) {
55
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
56
+ return { version: data.version, path: pkgPath };
57
+ }
58
+ }
59
+ catch { /* not found */ }
60
+ return null;
61
+ }
62
+ /** Non-interactive SDK check + auto-install */
63
+ export async function ensureAunSdk() {
64
+ const installed = resolveAunCoreSdkPkg();
65
+ if (installed && isAunSdkVersionOk(installed.version))
66
+ return;
67
+ const { npmInstallGlobal } = await import('../utils/init-channel.js');
68
+ console.log(`正在安装 ${AUN_CORE_SDK_PKG}@latest...`);
69
+ await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
70
+ }
71
+ /** SDK minimum version met? */
72
+ export function isAunSdkReady() {
73
+ const installed = resolveAunCoreSdkPkg();
74
+ return !!(installed && isAunSdkVersionOk(installed.version));
75
+ }
76
+ // ==================== CA Root ====================
77
+ export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
78
+ const caDir = path.join(aunPath, 'CA', 'root');
79
+ const caCertPath = path.join(caDir, 'root.crt');
80
+ if (fs.existsSync(caCertPath))
81
+ return true;
82
+ if (!gatewayUrl)
83
+ return false;
84
+ try {
85
+ fs.mkdirSync(caDir, { recursive: true });
86
+ const gwHttp = gatewayUrl.replace(/^wss?:/, 'https:').replace(/\/aun$/, '');
87
+ const resp = await fetch(`${gwHttp}/pki/chain`, { redirect: 'follow' });
88
+ if (!resp.ok) {
89
+ console.warn(`${indent}⚠ CA 根证书下载失败: HTTP ${resp.status}`);
90
+ return false;
91
+ }
92
+ const body = await resp.text();
93
+ if (!body.includes('BEGIN CERTIFICATE')) {
94
+ console.warn(`${indent}⚠ CA 根证书响应内容无效,跳过写入`);
95
+ return false;
96
+ }
97
+ fs.writeFileSync(caCertPath, body);
98
+ console.log(`${indent}✓ CA 根证书已下载`);
99
+ return true;
100
+ }
101
+ catch (e) {
102
+ console.warn(`${indent}⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
103
+ return false;
104
+ }
105
+ }
106
+ // ==================== Validation ====================
107
+ export function isValidAid(name) {
108
+ const labels = name.split('.');
109
+ return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
110
+ }
111
+ // ==================== AUNClient Factory ====================
112
+ /**
113
+ * Get a short-lived AUNClient with CA cert loaded and identity ready.
114
+ * Caller is responsible for closing: `try { await client.close(); } catch {}`
115
+ */
116
+ export async function getAunClient(aid, opts) {
117
+ const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
118
+ const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
119
+ const { AUNClient } = await import('@agentunion/fastaun');
120
+ const clientOpts = { aun_path: aunPath };
121
+ if (fs.existsSync(caCertPath))
122
+ clientOpts.root_ca_path = caCertPath;
123
+ const client = new AUNClient(clientOpts);
124
+ // Ensure identity is loaded (idempotent if already created)
125
+ await client.auth.createAid({ aid });
126
+ return client;
127
+ }
128
+ // ==================== AID Operations ====================
129
+ export function aidList(aunPath) {
130
+ const aidsDir = path.join(aunPath ?? path.join(os.homedir(), '.aun'), 'AIDs');
131
+ if (!fs.existsSync(aidsDir))
132
+ return [];
133
+ const entries = fs.readdirSync(aidsDir, { withFileTypes: true });
134
+ return entries
135
+ .filter(e => e.isDirectory())
136
+ .map(e => ({
137
+ aid: e.name,
138
+ hasPrivateKey: fs.existsSync(path.join(aidsDir, e.name, 'private')),
139
+ hasAgentMd: fs.existsSync(path.join(aidsDir, e.name, 'agent.md')),
140
+ }));
141
+ }
142
+ /**
143
+ * Create AID: keygen + register + CA download.
144
+ * Does NOT touch agent.md — call agentmdPut separately.
145
+ * Returns a reusable client (caller must close).
146
+ */
147
+ export async function aidCreate(aid, opts) {
148
+ const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
149
+ const aidDir = path.join(aunPath, 'AIDs', aid);
150
+ // Already exists locally
151
+ if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
152
+ const client = await getAunClient(aid, { aunPath });
153
+ return { aid, alreadyExisted: true, gateway: '', client };
154
+ }
155
+ const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
156
+ let client = new AUNClient({ aun_path: aunPath });
157
+ try {
158
+ const result = await client.auth.createAid({ aid });
159
+ const gateway = result.gateway || '';
160
+ // Download CA root cert
161
+ const caDownloaded = await downloadCaRoot(aunPath, gateway);
162
+ // Rebuild client with CA cert for subsequent operations
163
+ const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
164
+ if (caDownloaded && fs.existsSync(caCertPath)) {
165
+ try {
166
+ await client.close();
167
+ }
168
+ catch { /* ignore */ }
169
+ client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath });
170
+ await client.auth.createAid({ aid });
171
+ }
172
+ // Set gateway URL for upload operations
173
+ let gatewayUrl = gateway;
174
+ if (!gatewayUrl) {
175
+ try {
176
+ const discovery = new GatewayDiscovery({});
177
+ gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
178
+ }
179
+ catch { /* fall through */ }
180
+ }
181
+ if (gatewayUrl) {
182
+ client._gatewayUrl = gatewayUrl;
183
+ }
184
+ return { aid, alreadyExisted: false, gateway: gatewayUrl, client };
185
+ }
186
+ catch (e) {
187
+ try {
188
+ await client.close();
189
+ }
190
+ catch { /* ignore */ }
191
+ throw e;
192
+ }
193
+ }
194
+ // ==================== AgentMd Operations ====================
195
+ export function buildInitialAgentMd(opts) {
196
+ const agentName = opts.aid.split('.')[0];
197
+ const agentType = opts.type || 'ai';
198
+ return `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
199
+ }
200
+ /**
201
+ * Get agent.md content.
202
+ * For self (local AID with private key): reads local file, falls back to download.
203
+ * For others: downloads from AUN network.
204
+ */
205
+ export async function agentmdGet(aid, opts) {
206
+ const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
207
+ const localPath = path.join(aunPath, 'AIDs', aid, 'agent.md');
208
+ // Try local first for self AIDs (has private key)
209
+ const hasPrivateKey = fs.existsSync(path.join(aunPath, 'AIDs', aid, 'private'));
210
+ if (hasPrivateKey && fs.existsSync(localPath)) {
211
+ return fs.readFileSync(localPath, 'utf-8');
212
+ }
213
+ // Download from network
214
+ const client = opts?.client ?? await getAunClient(aid, { aunPath });
215
+ const ownClient = !opts?.client;
216
+ try {
217
+ return await client.auth.downloadAgentMd(aid);
218
+ }
219
+ finally {
220
+ if (ownClient)
221
+ try {
222
+ await client.close();
223
+ }
224
+ catch { /* ignore */ }
225
+ }
226
+ }
227
+ /**
228
+ * Upload agent.md (sign + upload) and sync to local file.
229
+ * If client is provided, reuses it; otherwise creates a short-lived one.
230
+ */
231
+ export async function agentmdPut(content, opts) {
232
+ const aunPath = opts.aunPath ?? path.join(os.homedir(), '.aun');
233
+ const client = opts.client ?? await getAunClient(opts.aid, { aunPath });
234
+ const ownClient = !opts.client;
235
+ try {
236
+ await client.auth.uploadAgentMd(content);
237
+ }
238
+ finally {
239
+ if (ownClient)
240
+ try {
241
+ await client.close();
242
+ }
243
+ catch { /* ignore */ }
244
+ }
245
+ // Sync to local
246
+ const aidDir = path.join(aunPath, 'AIDs', opts.aid);
247
+ fs.mkdirSync(aidDir, { recursive: true });
248
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
249
+ }
250
+ // ==================== Config ====================
251
+ /**
252
+ * Append a new AUN instance to the config's channels.aun array and save.
253
+ * Handles upgrade from single-object to array format.
254
+ */
255
+ export function appendAunInstance(config, inst) {
256
+ if (!config.channels)
257
+ config.channels = {};
258
+ const newInst = {
259
+ name: inst.name,
260
+ enabled: inst.enabled ?? true,
261
+ aid: inst.aid,
262
+ ...(inst.owner && { owner: inst.owner }),
263
+ };
264
+ if (Array.isArray(config.channels.aun)) {
265
+ config.channels.aun.push(newInst);
266
+ }
267
+ else if (config.channels.aun) {
268
+ const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
269
+ config.channels.aun = [oldInst, newInst];
270
+ }
271
+ else {
272
+ config.channels.aun = [newInst];
273
+ }
274
+ fs.writeFileSync(resolvePaths().config, JSON.stringify(config, null, 2) + '\n');
275
+ }