evolclaw 2.6.2 → 2.6.4
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 +2 -1
- package/dist/agents/codex-runner.js +0 -1
- package/dist/agents/gemini-runner.js +3 -1
- package/dist/channels/aun.js +166 -27
- package/dist/core/command-handler.js +111 -18
- package/dist/core/message/message-processor.js +93 -80
- package/dist/core/message/thought-emitter.js +8 -1
- package/dist/core/session/session-manager.js +22 -2
- package/dist/index.js +3 -0
- package/dist/prompts/templates.js +122 -0
- package/dist/templates/prompts.md +103 -0
- package/dist/utils/init-channel.js +26 -21
- package/package.json +1 -1
|
@@ -6,20 +6,24 @@ import { logger } from '../../utils/logger.js';
|
|
|
6
6
|
* - 不做聚合/batching,逐事件调用 adapter.putThought()
|
|
7
7
|
* - 不感知 group vs P2P,通道差异由 adapter 内部处理
|
|
8
8
|
* - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
|
|
9
|
+
* 同时写入 payload.task_id / payload.chatmode,与 message.send/group.send 保持一致
|
|
9
10
|
* - fire-and-forget:调用方不 await emit(),错误被内部捕获
|
|
10
11
|
*/
|
|
11
12
|
export class ThoughtEmitter {
|
|
12
13
|
adapter;
|
|
13
14
|
channelId;
|
|
14
15
|
taskId;
|
|
16
|
+
chatmode;
|
|
15
17
|
hasEmittedText = false;
|
|
16
|
-
constructor(adapter, channelId, taskId) {
|
|
18
|
+
constructor(adapter, channelId, taskId, chatmode = 'proactive') {
|
|
17
19
|
if (!taskId) {
|
|
18
20
|
throw new Error('[ThoughtEmitter] taskId is required at construction');
|
|
19
21
|
}
|
|
20
22
|
this.adapter = adapter;
|
|
21
23
|
this.channelId = channelId;
|
|
22
24
|
this.taskId = taskId;
|
|
25
|
+
this.chatmode = chatmode;
|
|
26
|
+
logger.info(`[ThoughtEmitter] created channel=${channelId} task=${taskId} chatmode=${chatmode}`);
|
|
23
27
|
}
|
|
24
28
|
async emit(event) {
|
|
25
29
|
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
@@ -37,6 +41,9 @@ export class ThoughtEmitter {
|
|
|
37
41
|
if (payload.stage === 'thinking') {
|
|
38
42
|
this.hasEmittedText = true;
|
|
39
43
|
}
|
|
44
|
+
// payload 也带上 task_id / chatmode(与 message.send/group.send 对齐)
|
|
45
|
+
payload.task_id = this.taskId;
|
|
46
|
+
payload.chatmode = this.chatmode;
|
|
40
47
|
try {
|
|
41
48
|
await this.adapter.putThought(this.channelId, this.taskId, payload);
|
|
42
49
|
}
|
|
@@ -315,6 +315,26 @@ export class SessionManager {
|
|
|
315
315
|
logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
+
// Migration: readonly 模式已暂时禁用,历史会话统一转为 noask
|
|
319
|
+
if (hasMetadata && tableInfo.length > 0) {
|
|
320
|
+
const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
|
|
321
|
+
let migratedPerm = 0;
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
try {
|
|
324
|
+
const meta = JSON.parse(row.metadata);
|
|
325
|
+
if (meta.permissionMode === 'readonly') {
|
|
326
|
+
meta.permissionMode = 'noask';
|
|
327
|
+
this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
|
|
328
|
+
.run(JSON.stringify(meta), row.id);
|
|
329
|
+
migratedPerm++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch { /* skip malformed JSON */ }
|
|
333
|
+
}
|
|
334
|
+
if (migratedPerm > 0) {
|
|
335
|
+
logger.info(`✓ Migrated ${migratedPerm} session(s): permissionMode readonly → noask`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
318
338
|
// 创建新表(首次初始化)
|
|
319
339
|
this.db.exec(`
|
|
320
340
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -451,7 +471,7 @@ export class SessionManager {
|
|
|
451
471
|
session.identity = this.resolveIdentity(channel, userId);
|
|
452
472
|
// 新话题会话补写默认权限模式
|
|
453
473
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
454
|
-
session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : '
|
|
474
|
+
session.metadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
|
|
455
475
|
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
456
476
|
.run(JSON.stringify(session.metadata), Date.now(), session.id);
|
|
457
477
|
}
|
|
@@ -562,7 +582,7 @@ export class SessionManager {
|
|
|
562
582
|
session.identity = this.resolveIdentity(channel, userId);
|
|
563
583
|
// 写入默认权限模式(基于角色,只在首次创建时设置)
|
|
564
584
|
if (!sessionMetadata.permissionMode) {
|
|
565
|
-
sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : '
|
|
585
|
+
sessionMetadata.permissionMode = session.identity?.role === 'owner' ? 'bypass' : session.identity?.role === 'admin' ? 'auto' : 'noask';
|
|
566
586
|
}
|
|
567
587
|
this.insertSession(session);
|
|
568
588
|
this.eventBus.publish({
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { ChannelLoader } from './core/channel-loader.js';
|
|
|
25
25
|
import { AgentLoader } from './core/agent-loader.js';
|
|
26
26
|
import { IpcServer } from './ipc.js';
|
|
27
27
|
import { logger } from './utils/logger.js';
|
|
28
|
+
import { loadPromptTemplates } from './prompts/templates.js';
|
|
28
29
|
import path from 'path';
|
|
29
30
|
import fs from 'fs';
|
|
30
31
|
async function main() {
|
|
@@ -48,6 +49,8 @@ async function main() {
|
|
|
48
49
|
logger.info('EvolClaw starting...');
|
|
49
50
|
// 确保数据目录存在
|
|
50
51
|
ensureDataDirs();
|
|
52
|
+
// 加载提示词模板
|
|
53
|
+
loadPromptTemplates();
|
|
51
54
|
// 加载配置
|
|
52
55
|
const config = loadConfig();
|
|
53
56
|
const paths = resolvePaths();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getPackageRoot, resolveRoot } from '../paths.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive']);
|
|
6
|
+
const SECTION_RE = /^##\s+(\w+)\s*$/;
|
|
7
|
+
let sections = null;
|
|
8
|
+
let builtinSections = null;
|
|
9
|
+
function parseTemplate(content) {
|
|
10
|
+
const result = new Map();
|
|
11
|
+
let currentSection = null;
|
|
12
|
+
let currentLines = [];
|
|
13
|
+
for (const line of content.split('\n')) {
|
|
14
|
+
// Stop parsing at horizontal rule separator (documentation follows)
|
|
15
|
+
if (/^---\s*$/.test(line)) {
|
|
16
|
+
if (currentSection) {
|
|
17
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
const m = line.match(SECTION_RE);
|
|
22
|
+
if (m) {
|
|
23
|
+
if (currentSection) {
|
|
24
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
25
|
+
}
|
|
26
|
+
const name = m[1];
|
|
27
|
+
if (KNOWN_SECTIONS.has(name)) {
|
|
28
|
+
currentSection = name;
|
|
29
|
+
currentLines = [];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
currentSection = null;
|
|
33
|
+
currentLines = [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (currentSection) {
|
|
37
|
+
currentLines.push(line);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (currentSection) {
|
|
41
|
+
result.set(currentSection, currentLines.join('\n').trim());
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function loadBuiltinTemplate() {
|
|
46
|
+
const builtinPath = path.join(getPackageRoot(), 'dist', 'templates', 'prompts.md');
|
|
47
|
+
const srcPath = path.join(getPackageRoot(), 'src', 'templates', 'prompts.md');
|
|
48
|
+
const filePath = fs.existsSync(builtinPath) ? builtinPath : srcPath;
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
return parseTemplate(content);
|
|
51
|
+
}
|
|
52
|
+
export function loadPromptTemplates() {
|
|
53
|
+
builtinSections = loadBuiltinTemplate();
|
|
54
|
+
const userPath = path.join(resolveRoot(), 'data', 'prompts.md');
|
|
55
|
+
if (fs.existsSync(userPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(userPath, 'utf-8');
|
|
58
|
+
const parsed = parseTemplate(content);
|
|
59
|
+
sections = new Map(builtinSections);
|
|
60
|
+
for (const [key, value] of parsed) {
|
|
61
|
+
sections.set(key, value);
|
|
62
|
+
}
|
|
63
|
+
logger.info(`[PromptTemplates] Loaded user override: ${userPath}`);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.warn(`[PromptTemplates] Failed to load user override (${userPath}), using builtin:`, err);
|
|
67
|
+
sections = builtinSections;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
sections = builtinSections;
|
|
72
|
+
logger.info(`[PromptTemplates] Using builtin templates`);
|
|
73
|
+
}
|
|
74
|
+
for (const name of KNOWN_SECTIONS) {
|
|
75
|
+
if (!sections.has(name)) {
|
|
76
|
+
logger.warn(`[PromptTemplates] Section "${name}" missing, using builtin fallback`);
|
|
77
|
+
const fallback = builtinSections.get(name);
|
|
78
|
+
if (fallback)
|
|
79
|
+
sections.set(name, fallback);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isTruthy(val) {
|
|
84
|
+
if (val === undefined || val === null || val === false || val === '' || val === 0)
|
|
85
|
+
return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function renderTemplate(template, vars) {
|
|
89
|
+
// Pass 1: conditional sections {{?key}}...{{/}}
|
|
90
|
+
let result = template.replace(/\{\{\?(\w+)\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, body) => {
|
|
91
|
+
return isTruthy(vars[key]) ? body : '';
|
|
92
|
+
});
|
|
93
|
+
// Pass 2: variable substitution {{key}}
|
|
94
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
95
|
+
const val = vars[key];
|
|
96
|
+
if (!isTruthy(val))
|
|
97
|
+
return '';
|
|
98
|
+
return String(val);
|
|
99
|
+
});
|
|
100
|
+
// Pass 3: remove blank lines
|
|
101
|
+
return result.split('\n').filter(line => line.trim() !== '').join('\n');
|
|
102
|
+
}
|
|
103
|
+
export function renderPromptSection(section, vars) {
|
|
104
|
+
if (!sections)
|
|
105
|
+
loadPromptTemplates();
|
|
106
|
+
const template = sections.get(section);
|
|
107
|
+
if (!template) {
|
|
108
|
+
logger.warn(`[PromptTemplates] Section "${section}" not found`);
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
return renderTemplate(template, vars);
|
|
112
|
+
}
|
|
113
|
+
/** Reset loaded templates (for testing) */
|
|
114
|
+
export function _resetTemplates() {
|
|
115
|
+
sections = null;
|
|
116
|
+
builtinSections = null;
|
|
117
|
+
}
|
|
118
|
+
/** Load templates from a raw string (for testing) */
|
|
119
|
+
export function _loadFromString(content) {
|
|
120
|
+
builtinSections = parseTemplate(content);
|
|
121
|
+
sections = builtinSections;
|
|
122
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# EvolClaw 运行时系统提示模板
|
|
2
|
+
|
|
3
|
+
# 本文件定义 LLM 每次收到消息时注入到 system prompt 尾部的三段内容。
|
|
4
|
+
# 修改后执行 `evolclaw restart` 生效。
|
|
5
|
+
# 放在 {EVOLCLAW_HOME}/data/prompts.md 会覆盖内置默认。
|
|
6
|
+
|
|
7
|
+
## runtime
|
|
8
|
+
|
|
9
|
+
[当前环境] 会话通道: {{channel}} | 当前项目: {{project}}{{?sessionName}} | 会话名称: {{sessionName}}{{/}}{{?selfIdentity}} | 当前名称: {{selfIdentity}}{{/}} | 对端身份: {{peerRole}}{{?peerIdentity}} | 对端名称: {{peerIdentity}}{{/}}{{?peerType}} | 对端类型: {{peerType}}{{/}}{{?chatType}} | 聊天类型: {{chatType}}{{/}}{{?agent}} | 当前Agent: {{agent}}{{/}}
|
|
10
|
+
{{?readonly}}[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后{{readonlySendHint}}{{/}}
|
|
11
|
+
{{?fileSendCurrent}}[SEND_FILE:路径] 发送文件到当前通道{{/}}
|
|
12
|
+
{{?fileSendCross}}[SEND_FILE:{{crossPrimary}}:路径] 发送文件到指定通道(可用: {{crossTypes}}){{/}}
|
|
13
|
+
{{?capability}}[通道能力] {{capabilities}}{{/}}
|
|
14
|
+
|
|
15
|
+
## group
|
|
16
|
+
|
|
17
|
+
[群聊回复规则] 回复时必须在开头添加 @{{peerId}} 来通知对方
|
|
18
|
+
|
|
19
|
+
## proactive
|
|
20
|
+
|
|
21
|
+
[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:
|
|
22
|
+
- 发送文本:evolclaw ctl send "<消息内容>"
|
|
23
|
+
- 发送文件:evolclaw ctl file <路径>
|
|
24
|
+
可多次调用。如不调用,用户将看不到任何回复。
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 格式说明
|
|
30
|
+
|
|
31
|
+
模板由多个以 `## 段名` 分隔的段组成,加载器只识别 `runtime`、`group`、`proactive` 三段,其它段(包括本说明)会被忽略,可以随意增删。
|
|
32
|
+
|
|
33
|
+
**占位符语法:**
|
|
34
|
+
|
|
35
|
+
| 语法 | 作用 | 示例 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `{{var}}` | 变量替换。值为空串/undefined/null/false 时替换为空 | `{{project}}` → `evolclaw` |
|
|
38
|
+
| `{{?var}}...{{/}}` | 条件段。var 为真值时保留整段(含字面量),否则整段删除。段内可嵌套 `{{var}}` | `{{?peerId}} | @{{peerId}}{{/}}` |
|
|
39
|
+
| 空行 | 渲染后若某行只剩空白,整行自动删除 | 条件段删完后的空行会消失 |
|
|
40
|
+
|
|
41
|
+
**注入时机:**
|
|
42
|
+
|
|
43
|
+
| 段 | 触发条件 | 说明 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `runtime` | 每次消息 | 每条用户消息都会注入 |
|
|
46
|
+
| `group` | `chatType === 'group' && peerId` | 仅群聊消息注入 |
|
|
47
|
+
| `proactive` | `sessionMode === 'proactive'` | 仅 proactive 会话注入 |
|
|
48
|
+
|
|
49
|
+
三段以换行拼接,追加到该消息的 system prompt 末尾。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 参数说明
|
|
54
|
+
|
|
55
|
+
### runtime 段
|
|
56
|
+
|
|
57
|
+
| 字段 | 类型 | 说明 | 示例 |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `channel` | string | 当前通道类型 | `feishu` / `wechat` / `aun` |
|
|
60
|
+
| `project` | string | 当前项目目录名(非完整路径) | `evolclaw` |
|
|
61
|
+
| `sessionName` | string? | 会话名(用户通过 `/name` 设置) | `CLI开发` |
|
|
62
|
+
| `selfIdentity` | string? | 机器人自身标识「名称 (ID)」 | `Evol (evolai.xxx.pub)` |
|
|
63
|
+
| `peerRole` | string | 对端角色 | `owner` / `admin` / `guest` / `unknown` |
|
|
64
|
+
| `peerIdentity` | string? | 对端标识「名称 (ID)」 | `张三 (u_abc)` |
|
|
65
|
+
| `peerType` | string? | 对端类型(`unknown` 时为空) | `user` / `group` |
|
|
66
|
+
| `chatType` | string? | 聊天类型 | `private` / `group` |
|
|
67
|
+
| `agent` | string? | 当前 agent(`claude` 时为空不显示) | `hermes` / `gemini` |
|
|
68
|
+
| `readonly` | bool | 是否只读模式(触发只读行) | `true` / `false` |
|
|
69
|
+
| `readonlySendHint` | string | 只读模式下提示使用的发送方式 | `使用 [SEND_FILE:] 发送` |
|
|
70
|
+
| `fileSendCurrent` | bool | 当前通道是否支持发文件(触发该行) | `true` / `false` |
|
|
71
|
+
| `fileSendCross` | bool | 是否存在可跨通道发文件的其它通道 | `true` / `false` |
|
|
72
|
+
| `crossPrimary` | string | 跨通道发送示例用的首选通道 | `wechat` |
|
|
73
|
+
| `crossTypes` | string | 所有支持跨通道发送的通道列表 | `wechat/aun` |
|
|
74
|
+
| `capability` | bool | 是否有任何通道能力要展示(触发通道能力行) | `true` / `false` |
|
|
75
|
+
| `capabilities` | string | 通道能力清单 | `图片输入、图片输出、文件发送` |
|
|
76
|
+
|
|
77
|
+
### group 段
|
|
78
|
+
|
|
79
|
+
| 字段 | 类型 | 说明 | 示例 |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `peerId` | string | 对端用户 ID(@ 所需) | `ou_xxx` / `wxid_xxx` |
|
|
82
|
+
|
|
83
|
+
### proactive 段
|
|
84
|
+
|
|
85
|
+
无参数。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 修改示例
|
|
90
|
+
|
|
91
|
+
**只改文案,不改结构:**
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
## runtime
|
|
95
|
+
当前项目 {{project}},你正在和 {{peerIdentity}} 对话。
|
|
96
|
+
{{?readonly}}⚠ 只读模式,不要修改代码{{/}}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**关闭某一行:** 模板里删掉那一行即可。内置条件段(如只读提示)删了之后,只读模式就不再在 system prompt 里出现(但权限拦截依然生效)。
|
|
100
|
+
|
|
101
|
+
**追加自定义规则:** 直接在对应段里加行文本,不需要占位符。
|
|
102
|
+
|
|
103
|
+
**用英文:** 所有文案重写成英文即可,字段含义不变。
|
|
@@ -728,7 +728,7 @@ export async function createAidSilent(opts) {
|
|
|
728
728
|
if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
|
|
729
729
|
return { aid: opts.aid, alreadyExisted: true };
|
|
730
730
|
}
|
|
731
|
-
const { AUNClient } = await import('@agentunion/fastaun');
|
|
731
|
+
const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
|
|
732
732
|
let client = new AUNClient({ aun_path: aunPath });
|
|
733
733
|
const result = await client.auth.createAid({ aid: opts.aid });
|
|
734
734
|
// Download CA root cert (if not already present)
|
|
@@ -742,6 +742,18 @@ export async function createAidSilent(opts) {
|
|
|
742
742
|
catch { /* ignore */ }
|
|
743
743
|
client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
|
|
744
744
|
}
|
|
745
|
+
// Set gateway URL for uploadAgentMd
|
|
746
|
+
let gatewayUrl = result.gateway || '';
|
|
747
|
+
if (!gatewayUrl) {
|
|
748
|
+
try {
|
|
749
|
+
const discovery = new GatewayDiscovery({});
|
|
750
|
+
gatewayUrl = await discovery.discover(`https://${opts.aid}/.well-known/aun-gateway`);
|
|
751
|
+
}
|
|
752
|
+
catch { /* fall through */ }
|
|
753
|
+
}
|
|
754
|
+
if (gatewayUrl) {
|
|
755
|
+
client._gatewayUrl = gatewayUrl;
|
|
756
|
+
}
|
|
745
757
|
// Write initial agent.md (initialized: false, name = aid first label)
|
|
746
758
|
const agentName = opts.aid.split('.')[0];
|
|
747
759
|
const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
|
|
@@ -790,7 +802,6 @@ export function appendAunInstance(config, inst) {
|
|
|
790
802
|
}
|
|
791
803
|
export async function setupAunAid(rl, _config) {
|
|
792
804
|
let aid = '';
|
|
793
|
-
let gatewayPort; // only used locally for AID creation, not written to config
|
|
794
805
|
// Outer loop: allows retrying with a different AID
|
|
795
806
|
while (true) {
|
|
796
807
|
// Ask AID with format validation
|
|
@@ -806,12 +817,6 @@ export async function setupAunAid(rl, _config) {
|
|
|
806
817
|
aid = '';
|
|
807
818
|
}
|
|
808
819
|
}
|
|
809
|
-
const portStr = (await ask(rl, ' Gateway 端口 [留空使用默认 443]: ')).trim();
|
|
810
|
-
gatewayPort = portStr ? parseInt(portStr, 10) : undefined;
|
|
811
|
-
if (gatewayPort !== undefined && (isNaN(gatewayPort) || gatewayPort < 1 || gatewayPort > 65535)) {
|
|
812
|
-
console.log(' ⚠ 端口号无效,使用默认 443');
|
|
813
|
-
gatewayPort = undefined;
|
|
814
|
-
}
|
|
815
820
|
// Check if AID exists locally
|
|
816
821
|
const aunPath = path.join(os.homedir(), '.aun');
|
|
817
822
|
const aidDir = path.join(aunPath, 'AIDs', aid);
|
|
@@ -828,21 +833,13 @@ export async function setupAunAid(rl, _config) {
|
|
|
828
833
|
console.log(' 正在创建 AID...');
|
|
829
834
|
let failed = false;
|
|
830
835
|
try {
|
|
831
|
-
const { AUNClient } = await import('@agentunion/fastaun');
|
|
836
|
+
const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
|
|
832
837
|
let client = new AUNClient({ aun_path: aunPath });
|
|
833
|
-
// 如果用户指定了自定义端口,手动设置 gateway URL;否则让 SDK 自动发现
|
|
834
|
-
if (gatewayPort) {
|
|
835
|
-
const domain = aid.split('.').slice(1).join('.');
|
|
836
|
-
client._gatewayUrl = `wss://gateway.${domain}:${gatewayPort}/aun`;
|
|
837
|
-
}
|
|
838
838
|
const result = await client.auth.createAid({ aid });
|
|
839
839
|
console.log(` ✓ AID ${result.aid} 创建成功`);
|
|
840
840
|
// 下载 CA 根证书(如果本地不存在),从 SDK 返回的实际网关 URL 派生
|
|
841
841
|
const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '', ' ');
|
|
842
|
-
//
|
|
843
|
-
// 必须显式传 root_ca_path 指向刚下载的 root.crt,uploadAgentMd 才能验证 server cert。
|
|
844
|
-
// 同时传 aid,否则新 client 不知道该加载哪个身份,uploadAgentMd 会报
|
|
845
|
-
// "no local identity found, call auth.createAid() first"。
|
|
842
|
+
// 重建 client:传 root_ca_path 以验证 server cert,传 aid 以加载身份
|
|
846
843
|
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
847
844
|
if (caDownloaded && fs.existsSync(caCertPath)) {
|
|
848
845
|
try {
|
|
@@ -850,10 +847,18 @@ export async function setupAunAid(rl, _config) {
|
|
|
850
847
|
}
|
|
851
848
|
catch { /* ignore */ }
|
|
852
849
|
client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid });
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
850
|
+
}
|
|
851
|
+
// 设置 gateway URL(从 createAid 返回值或 well-known 自动发现)
|
|
852
|
+
let gatewayUrl = result.gateway || '';
|
|
853
|
+
if (!gatewayUrl) {
|
|
854
|
+
try {
|
|
855
|
+
const discovery = new GatewayDiscovery({});
|
|
856
|
+
gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
|
|
856
857
|
}
|
|
858
|
+
catch { /* fall through */ }
|
|
859
|
+
}
|
|
860
|
+
if (gatewayUrl) {
|
|
861
|
+
client._gatewayUrl = gatewayUrl;
|
|
857
862
|
}
|
|
858
863
|
// Collect agent.md info and publish
|
|
859
864
|
const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
|
package/package.json
CHANGED