@starlink-awaken/agentmesh 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -41
- package/README.zh-CN.md +137 -167
- package/config/gateway.yaml +78 -0
- package/dist/src/cli/connect.d.ts +26 -0
- package/dist/src/cli/connect.js +544 -0
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +97 -0
- package/dist/src/cli.js +410 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +57 -0
- package/dist/{index.js → src/index.js} +40 -8
- package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
- package/dist/src/model-gateway/circuit-breaker.js +86 -0
- package/dist/src/model-gateway/health.d.ts +12 -0
- package/dist/src/model-gateway/health.js +80 -0
- package/dist/src/model-gateway/providers.d.ts +4 -0
- package/dist/src/model-gateway/providers.js +113 -0
- package/dist/src/model-gateway/quota.d.ts +4 -0
- package/dist/src/model-gateway/quota.js +107 -0
- package/dist/src/model-gateway/rate-limit.d.ts +12 -0
- package/dist/src/model-gateway/rate-limit.js +51 -0
- package/dist/src/model-gateway/retry.d.ts +14 -0
- package/dist/src/model-gateway/retry.js +48 -0
- package/dist/src/model-gateway/router.d.ts +4 -0
- package/dist/src/model-gateway/router.js +79 -0
- package/dist/src/model-gateway/routes.d.ts +2 -0
- package/dist/src/model-gateway/routes.js +172 -0
- package/dist/src/model-gateway/types.d.ts +47 -0
- package/dist/src/model-gateway/types.js +1 -0
- package/dist/tests/core/context-manager.test.d.ts +1 -0
- package/dist/tests/core/context-manager.test.js +35 -0
- package/dist/tests/core/router.test.d.ts +1 -0
- package/dist/tests/core/router.test.js +79 -0
- package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
- package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
- package/dist/tests/model-gateway/providers.test.d.ts +1 -0
- package/dist/tests/model-gateway/providers.test.js +80 -0
- package/dist/tests/model-gateway/quota.test.d.ts +1 -0
- package/dist/tests/model-gateway/quota.test.js +60 -0
- package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
- package/dist/tests/model-gateway/rate-limit.test.js +42 -0
- package/dist/tests/model-gateway/retry.test.d.ts +1 -0
- package/dist/tests/model-gateway/retry.test.js +47 -0
- package/dist/tests/model-gateway/router.test.d.ts +1 -0
- package/dist/tests/model-gateway/router.test.js +108 -0
- package/dist/tests/model-gateway/routes.test.d.ts +1 -0
- package/dist/tests/model-gateway/routes.test.js +83 -0
- package/docs/api.md +187 -460
- package/docs/architecture.md +138 -0
- package/docs/configuration.md +188 -0
- package/package.json +3 -1
- package/dist/cli.js +0 -246
- /package/dist/{adapters → src/adapters}/base.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/base.js +0 -0
- /package/dist/{adapters → src/adapters}/claude-code.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/claude-code.js +0 -0
- /package/dist/{adapters → src/adapters}/openclaw.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/openclaw.js +0 -0
- /package/dist/{adapters → src/adapters}/process.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/process.js +0 -0
- /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
- /package/dist/{core → src/core}/agent-registry.d.ts +0 -0
- /package/dist/{core → src/core}/agent-registry.js +0 -0
- /package/dist/{core → src/core}/config.d.ts +0 -0
- /package/dist/{core → src/core}/config.js +0 -0
- /package/dist/{core → src/core}/context-manager.d.ts +0 -0
- /package/dist/{core → src/core}/context-manager.js +0 -0
- /package/dist/{core → src/core}/event-bus.d.ts +0 -0
- /package/dist/{core → src/core}/event-bus.js +0 -0
- /package/dist/{core → src/core}/metrics.d.ts +0 -0
- /package/dist/{core → src/core}/metrics.js +0 -0
- /package/dist/{core → src/core}/router.d.ts +0 -0
- /package/dist/{core → src/core}/router.js +0 -0
- /package/dist/{core → src/core}/task-manager.d.ts +0 -0
- /package/dist/{core → src/core}/task-manager.js +0 -0
- /package/dist/{core → src/core}/vector-store.d.ts +0 -0
- /package/dist/{core → src/core}/vector-store.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{routes → src/routes}/api.d.ts +0 -0
- /package/dist/{routes → src/routes}/api.js +0 -0
- /package/dist/{routes → src/routes}/websocket.d.ts +0 -0
- /package/dist/{routes → src/routes}/websocket.js +0 -0
- /package/dist/{types → src/types}/index.d.ts +0 -0
- /package/dist/{types → src/types}/index.js +0 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Agent Mesh Gateway CLI
|
|
4
|
+
* 统一命令行接口 — start, setup, health, models, quota, config, doctor, help
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname, join } from 'node:path';
|
|
8
|
+
import { initLogger } from './core/logger.js';
|
|
9
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.dir), '..');
|
|
10
|
+
const VERSION = '1.1.0';
|
|
11
|
+
const BANNER = `
|
|
12
|
+
█████╗ ██████╗ ███████╗███╗ ██╗████████╗
|
|
13
|
+
██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
|
|
14
|
+
███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║
|
|
15
|
+
██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
|
|
16
|
+
██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║
|
|
17
|
+
╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝
|
|
18
|
+
𝙼 𝙴 𝚂 𝙷 𝙶 𝙰 𝚃 𝙴 𝚆 𝙰 𝚈 v${VERSION}
|
|
19
|
+
`;
|
|
20
|
+
const BASE_URL = () => {
|
|
21
|
+
const host = Bun.env.AGENT_GATEWAY_HOST || '127.0.0.1';
|
|
22
|
+
const port = Bun.env.AGENT_GATEWAY_PORT || '3000';
|
|
23
|
+
return `http://${host}:${port}`;
|
|
24
|
+
};
|
|
25
|
+
async function apiRequest(path, opts) {
|
|
26
|
+
const resp = await fetch(`${BASE_URL()}${path}`, {
|
|
27
|
+
...opts,
|
|
28
|
+
headers: { 'Content-Type': 'application/json', ...opts?.headers },
|
|
29
|
+
signal: AbortSignal.timeout(15_000),
|
|
30
|
+
});
|
|
31
|
+
if (!resp.ok) {
|
|
32
|
+
const err = (await resp.json().catch(() => ({ message: resp.statusText })));
|
|
33
|
+
throw new Error(`[${resp.status}] ${err.message || 'Unknown error'}`);
|
|
34
|
+
}
|
|
35
|
+
return resp.json();
|
|
36
|
+
}
|
|
37
|
+
// ===========================================================================
|
|
38
|
+
// Help
|
|
39
|
+
// ===========================================================================
|
|
40
|
+
function showHelp(topic) {
|
|
41
|
+
if (topic === 'start' || topic === 'run') {
|
|
42
|
+
console.log(`
|
|
43
|
+
agentmesh start — 启动网关服务
|
|
44
|
+
|
|
45
|
+
用法:
|
|
46
|
+
agentmesh start [--port PORT] [--host HOST]
|
|
47
|
+
|
|
48
|
+
选项:
|
|
49
|
+
--port 监听端口 (默认: 3000)
|
|
50
|
+
--host 监听地址 (默认: 0.0.0.0)
|
|
51
|
+
|
|
52
|
+
示例:
|
|
53
|
+
agentmesh start
|
|
54
|
+
agentmesh start --port 9400`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (topic === 'setup') {
|
|
58
|
+
console.log(` agentmesh setup — 交互式初始化向导
|
|
59
|
+
|
|
60
|
+
引导配置:
|
|
61
|
+
- API Key (DeepSeek, OpenAI, OpenRouter)
|
|
62
|
+
- 服务端口和日志级别
|
|
63
|
+
- 生成 .env 配置文件`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (topic === 'models' || topic === 'model') {
|
|
67
|
+
console.log(` agentmesh models — 列出可用模型`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (topic === 'quota') {
|
|
71
|
+
console.log(` agentmesh quota — 查看配额状态 (需 codexbar)
|
|
72
|
+
|
|
73
|
+
显示所有 Provider 的实时配额/余额信息。
|
|
74
|
+
首次调用可能较慢 (codexbar 数据采集)。`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (topic === 'config') {
|
|
78
|
+
console.log(` agentmesh config — 配置管理
|
|
79
|
+
|
|
80
|
+
用法:
|
|
81
|
+
agentmesh config show 显示当前配置
|
|
82
|
+
agentmesh config path 显示配置文件路径
|
|
83
|
+
agentmesh config edit 在编辑器中打开配置`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log(`
|
|
87
|
+
${BANNER}
|
|
88
|
+
🤖 Agent Mesh Gateway CLI v${VERSION}
|
|
89
|
+
|
|
90
|
+
用法:
|
|
91
|
+
agentmesh <command> [options]
|
|
92
|
+
|
|
93
|
+
管理命令:
|
|
94
|
+
start 启动网关服务
|
|
95
|
+
setup 交互式初始化向导 (配置API Key等)
|
|
96
|
+
doctor 系统诊断检查
|
|
97
|
+
|
|
98
|
+
接入命令:
|
|
99
|
+
connect [tool] 一键接入 AI 工具 (--dry-run 预览)
|
|
100
|
+
disconnect [tool] 恢复工具配置到接入前状态
|
|
101
|
+
|
|
102
|
+
查询命令:
|
|
103
|
+
health, status 健康检查
|
|
104
|
+
models 列出可用模型
|
|
105
|
+
quota 查看配额状态
|
|
106
|
+
agents 列出已注册 Agent
|
|
107
|
+
tasks 列出任务列表
|
|
108
|
+
|
|
109
|
+
配置命令:
|
|
110
|
+
config show 显示当前配置
|
|
111
|
+
config path 显示配置文件路径
|
|
112
|
+
config edit 在编辑器中打开配置
|
|
113
|
+
|
|
114
|
+
获取帮助:
|
|
115
|
+
help 显示此帮助
|
|
116
|
+
help <command> 显示特定命令帮助
|
|
117
|
+
|
|
118
|
+
示例:
|
|
119
|
+
agentmesh start
|
|
120
|
+
agentmesh setup
|
|
121
|
+
agentmesh connect --dry-run # 预览接入变更
|
|
122
|
+
agentmesh connect all # 接入所有工具
|
|
123
|
+
agentmesh disconnect all # 恢复所有工具
|
|
124
|
+
|
|
125
|
+
文档: https://github.com/starlink-awaken/agentmesh
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
// Commands
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
async function cmdStart(args) {
|
|
132
|
+
// 解析参数
|
|
133
|
+
const portArg = args.indexOf('--port');
|
|
134
|
+
const hostArg = args.indexOf('--host');
|
|
135
|
+
const port = portArg >= 0 ? args[portArg + 1] || '3000' : '3000';
|
|
136
|
+
const host = hostArg >= 0 ? args[hostArg + 1] || '0.0.0.0' : '0.0.0.0';
|
|
137
|
+
// 设置环境变量传递给服务器
|
|
138
|
+
Bun.env.AGENT_GATEWAY_PORT = port;
|
|
139
|
+
Bun.env.AGENT_GATEWAY_HOST = host;
|
|
140
|
+
console.log(`${BANNER}
|
|
141
|
+
🚀 Starting Agent Mesh Gateway...
|
|
142
|
+
═══════════════════════════════════════
|
|
143
|
+
HTTP: http://${host}:${port}
|
|
144
|
+
Health: http://${host}:${port}/health
|
|
145
|
+
Models: http://${host}:${port}/v1/models
|
|
146
|
+
Quota: http://${host}:${port}/model-gateway/quota
|
|
147
|
+
Docs: http://${host}:${port}/docs
|
|
148
|
+
═══════════════════════════════════════
|
|
149
|
+
`);
|
|
150
|
+
// 动态导入并启动服务器
|
|
151
|
+
await import('./index.js');
|
|
152
|
+
// 服务器会在导入时启动
|
|
153
|
+
}
|
|
154
|
+
async function cmdHealth() {
|
|
155
|
+
try {
|
|
156
|
+
const data = await apiRequest('/health');
|
|
157
|
+
console.log(`\n ✅ Gateway Running`);
|
|
158
|
+
console.log(` Status: ${data.status}`);
|
|
159
|
+
console.log(` Agents: ${data.agents?.length || 0}`);
|
|
160
|
+
console.log(` Timestamp: ${new Date(data.timestamp).toISOString()}\n`);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error(`\n ❌ Gateway not reachable at ${BASE_URL()}`);
|
|
164
|
+
console.error(` Error: ${err.message}`);
|
|
165
|
+
console.log(`\n Start it with: agentmesh start\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function cmdModels() {
|
|
169
|
+
try {
|
|
170
|
+
const data = await apiRequest('/v1/models');
|
|
171
|
+
console.log('\n 📋 Available Models:\n');
|
|
172
|
+
const byProvider = {};
|
|
173
|
+
for (const m of data.data || []) {
|
|
174
|
+
(byProvider[m.owned_by] ??= []).push(m.id);
|
|
175
|
+
}
|
|
176
|
+
for (const [provider, models] of Object.entries(byProvider)) {
|
|
177
|
+
console.log(` ${provider}:`);
|
|
178
|
+
models.forEach(m => console.log(` - ${m}`));
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
console.error(`\n ❌ ${err.message}\n`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function cmdQuota() {
|
|
187
|
+
try {
|
|
188
|
+
console.log('\n ⏳ Fetching quota data (may take ~15s)...');
|
|
189
|
+
const data = await apiRequest('/model-gateway/quota', { signal: AbortSignal.timeout(60_000) });
|
|
190
|
+
console.log('\n 📊 Provider Quota Status:\n');
|
|
191
|
+
if (!data || Object.keys(data).length === 0) {
|
|
192
|
+
console.log(' No quota data available. Is codexbar installed?\n');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const [name, info] of Object.entries(data)) {
|
|
196
|
+
const icon = info.available ? '🟢' : '🔴';
|
|
197
|
+
console.log(` ${icon} ${name.padEnd(15)} ${info.summary}`);
|
|
198
|
+
}
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error(`\n ❌ ${err.message}\n`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function cmdAgents() {
|
|
206
|
+
try {
|
|
207
|
+
const data = await apiRequest('/agents');
|
|
208
|
+
console.log('\n 📋 Registered Agents:\n');
|
|
209
|
+
data.forEach(a => console.log(` ${a.status === 'online' ? '🟢' : '⚫'} ${a.id.padEnd(18)} ${a.name.padEnd(22)} [${a.status}]\n ${(a.capabilities || []).join(', ')}\n`));
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.error(`\n ❌ ${err.message}\n`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function cmdTasks() {
|
|
216
|
+
try {
|
|
217
|
+
const tasks = await apiRequest('/tasks');
|
|
218
|
+
console.log('\n 📋 Tasks:\n');
|
|
219
|
+
if (!tasks.length) {
|
|
220
|
+
console.log(' (none)\n');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
tasks.forEach(t => console.log(` ${t.id?.slice(0, 8)}... ${t.status?.padEnd(10)} ${new Date(t.created_at).toLocaleString()}`));
|
|
224
|
+
console.log('');
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
console.error(`\n ❌ ${err.message}\n`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function cmdConfig(args) {
|
|
231
|
+
const configPath = join(PROJECT_ROOT, 'config', 'gateway.yaml');
|
|
232
|
+
const sub = args[0];
|
|
233
|
+
if (sub === 'path') {
|
|
234
|
+
console.log(`\n Config: ${configPath}\n`);
|
|
235
|
+
}
|
|
236
|
+
else if (sub === 'edit') {
|
|
237
|
+
const editor = Bun.env.EDITOR || Bun.env.VISUAL || 'vim';
|
|
238
|
+
console.log(` Opening ${configPath} with ${editor}...`);
|
|
239
|
+
Bun.spawnSync([editor, configPath], { stdio: ['inherit', 'inherit', 'inherit'] });
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// show
|
|
243
|
+
try {
|
|
244
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
245
|
+
console.log(`\n ── ${configPath} ──\n`);
|
|
246
|
+
console.log(content.split('\n').map(l => ` ${l}`).join('\n'));
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
console.error(`\n ❌ Config not found: ${configPath}\n`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function cmdDoctor() {
|
|
254
|
+
console.log('\n 🔍 Agent Mesh Gateway Diagnostics\n');
|
|
255
|
+
const checks = [];
|
|
256
|
+
// bun
|
|
257
|
+
try {
|
|
258
|
+
const proc = Bun.spawnSync({ cmd: ['bun', '--version'], stdout: 'pipe' });
|
|
259
|
+
const ver = new TextDecoder().decode(proc.stdout).trim();
|
|
260
|
+
checks.push(['Bun Runtime', true, ver]);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
checks.push(['Bun Runtime', false, 'Not found']);
|
|
264
|
+
}
|
|
265
|
+
// codexbar
|
|
266
|
+
try {
|
|
267
|
+
const proc = Bun.spawnSync({ cmd: ['codexbar', '--version'], stdout: 'pipe', stderr: 'pipe' });
|
|
268
|
+
checks.push(['codexbar', proc.exitCode === 0, proc.exitCode === 0 ? 'OK' : 'Warning']);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
checks.push(['codexbar', false, 'Install for quota tracking']);
|
|
272
|
+
}
|
|
273
|
+
// API keys
|
|
274
|
+
for (const [name, env] of [['DeepSeek', 'DEEPSEEK_API_KEY'], ['OpenAI', 'OPENAI_API_KEY'], ['OpenRouter', 'OPENROUTER_API_KEY']]) {
|
|
275
|
+
checks.push([`${name} API Key`, !!Bun.env[env], Bun.env[env] ? 'Configured (env)' : 'Not set']);
|
|
276
|
+
}
|
|
277
|
+
// .env file
|
|
278
|
+
const envPath = join(PROJECT_ROOT, '.env');
|
|
279
|
+
checks.push(['.env file', existsSync(envPath), existsSync(envPath) ? 'Exists' : 'Run: agentmesh setup']);
|
|
280
|
+
// docker
|
|
281
|
+
try {
|
|
282
|
+
const proc = Bun.spawnSync({ cmd: ['docker', '--version'], stdout: 'pipe' });
|
|
283
|
+
checks.push(['Docker', true, new TextDecoder().decode(proc.stdout).trim()]);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
checks.push(['Docker', false, 'Not required']);
|
|
287
|
+
}
|
|
288
|
+
// config
|
|
289
|
+
const configPath = join(PROJECT_ROOT, 'config', 'gateway.yaml');
|
|
290
|
+
checks.push(['Config file', existsSync(configPath), existsSync(configPath) ? 'OK' : 'Missing!']);
|
|
291
|
+
// Print
|
|
292
|
+
for (const [name, ok, detail] of checks) {
|
|
293
|
+
console.log(` ${ok ? '✅' : '⚠️'} ${name.padEnd(20)} ${detail}`);
|
|
294
|
+
}
|
|
295
|
+
// Gateway status
|
|
296
|
+
try {
|
|
297
|
+
await apiRequest('/health');
|
|
298
|
+
console.log(` ✅ Gateway Running at ${BASE_URL()}`);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
console.log(' ⚠️ Gateway Not running (start: agentmesh start)');
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
// Main
|
|
307
|
+
// ===========================================================================
|
|
308
|
+
async function main() {
|
|
309
|
+
const args = Bun.argv.slice(2);
|
|
310
|
+
initLogger({ level: Bun.env.LOG_LEVEL || 'info', dir: join(PROJECT_ROOT, 'logs') });
|
|
311
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
312
|
+
showHelp(args[1]);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const cmd = args[0];
|
|
316
|
+
const rest = args.slice(1);
|
|
317
|
+
try {
|
|
318
|
+
switch (cmd) {
|
|
319
|
+
case 'start':
|
|
320
|
+
case 'run':
|
|
321
|
+
await cmdStart(rest);
|
|
322
|
+
break;
|
|
323
|
+
case 'setup':
|
|
324
|
+
case 'init':
|
|
325
|
+
const { runSetup } = await import('./cli/setup.js');
|
|
326
|
+
await runSetup();
|
|
327
|
+
break;
|
|
328
|
+
case 'health':
|
|
329
|
+
case 'status':
|
|
330
|
+
await cmdHealth();
|
|
331
|
+
break;
|
|
332
|
+
case 'models':
|
|
333
|
+
case 'model':
|
|
334
|
+
await cmdModels();
|
|
335
|
+
break;
|
|
336
|
+
case 'quota':
|
|
337
|
+
await cmdQuota();
|
|
338
|
+
break;
|
|
339
|
+
case 'agents':
|
|
340
|
+
case 'ls':
|
|
341
|
+
await cmdAgents();
|
|
342
|
+
break;
|
|
343
|
+
case 'tasks':
|
|
344
|
+
await cmdTasks();
|
|
345
|
+
break;
|
|
346
|
+
case 'config':
|
|
347
|
+
await cmdConfig(rest);
|
|
348
|
+
break;
|
|
349
|
+
case 'connect': {
|
|
350
|
+
// 子命令: connect list
|
|
351
|
+
if (rest[0] === 'list') {
|
|
352
|
+
const { listDetectedTools } = await import('./cli/connect.js');
|
|
353
|
+
const tools = listDetectedTools();
|
|
354
|
+
console.log('\n 🔍 检测到的 AI 工具:\n');
|
|
355
|
+
for (const t of tools) {
|
|
356
|
+
console.log(` ${t.installed ? '🟢' : '⚫'} ${t.name.padEnd(18)} ${t.description}`);
|
|
357
|
+
if (t.configPath)
|
|
358
|
+
console.log(` Config: ${t.configPath}`);
|
|
359
|
+
}
|
|
360
|
+
console.log(`\n 执行 \`agentmesh connect\` 进入交互式选择`);
|
|
361
|
+
console.log(` 执行 \`agentmesh connect all --dry-run\` 预览接入变更\n`);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
// 无参数或 -i → 交互式选择
|
|
365
|
+
const hasTargets = rest.some(a => !a.startsWith('--'));
|
|
366
|
+
const isInteractive = rest.includes('-i') || rest.includes('--interactive') || !hasTargets;
|
|
367
|
+
if (isInteractive && !rest.includes('--dry-run')) {
|
|
368
|
+
const { interactiveConnect } = await import('./cli/connect.js');
|
|
369
|
+
await interactiveConnect();
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
const { connectTools } = await import('./cli/connect.js');
|
|
373
|
+
const dryRun = rest.includes('--dry-run');
|
|
374
|
+
const targets = rest.filter(a => !a.startsWith('--') && a !== '-i' && a !== '--interactive');
|
|
375
|
+
const results = await connectTools(targets.length ? targets : ['all'], { dryRun });
|
|
376
|
+
console.log('\n 接入结果:\n');
|
|
377
|
+
for (const r of results) {
|
|
378
|
+
const icon = r.status === 'ok' ? '✅' : r.status === 'skipped' ? '⏭️' : '❌';
|
|
379
|
+
console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
|
|
380
|
+
}
|
|
381
|
+
if (dryRun)
|
|
382
|
+
console.log('\n ⚠️ DRY-RUN 模式,未实际修改文件');
|
|
383
|
+
else
|
|
384
|
+
console.log(`\n 💡 使用 \`agentmesh disconnect\` 恢复配置`);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'disconnect': {
|
|
388
|
+
const { disconnectTools } = await import('./cli/connect.js');
|
|
389
|
+
const targets = rest.filter(a => !a.startsWith('--'));
|
|
390
|
+
const results = await disconnectTools(targets.length ? targets : ['all']);
|
|
391
|
+
console.log('\n 断开结果:\n');
|
|
392
|
+
for (const r of results) {
|
|
393
|
+
const icon = r.status === 'ok' ? '✅' : '❌';
|
|
394
|
+
console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case 'doctor':
|
|
399
|
+
case 'check':
|
|
400
|
+
await cmdDoctor();
|
|
401
|
+
break;
|
|
402
|
+
default:
|
|
403
|
+
console.error(`\n ❌ Unknown command: ${cmd}\n Run 'agentmesh help' for available commands.\n`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
console.error(`\n ❌ Error: ${err.message}\n`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
main();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export interface Logger {
|
|
3
|
+
debug(msg: string, data?: Record<string, any>): void;
|
|
4
|
+
info(msg: string, data?: Record<string, any>): void;
|
|
5
|
+
warn(msg: string, data?: Record<string, any>): void;
|
|
6
|
+
error(msg: string, data?: Record<string, any>): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function initLogger(opts?: {
|
|
9
|
+
level?: LogLevel;
|
|
10
|
+
dir?: string;
|
|
11
|
+
pino?: any;
|
|
12
|
+
}): void;
|
|
13
|
+
export declare const logger: Logger;
|
|
14
|
+
export declare function createLogger(bindings?: Record<string, any>): Logger;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
4
|
+
let currentLevel = 'info';
|
|
5
|
+
let logDir = null;
|
|
6
|
+
let logFile = null;
|
|
7
|
+
let pinoInstance = null;
|
|
8
|
+
export function initLogger(opts = {}) {
|
|
9
|
+
currentLevel = opts.level || 'info';
|
|
10
|
+
if (opts.pino && typeof opts.pino.child === 'function') {
|
|
11
|
+
pinoInstance = opts.pino;
|
|
12
|
+
}
|
|
13
|
+
if (opts.dir) {
|
|
14
|
+
logDir = opts.dir;
|
|
15
|
+
if (!existsSync(logDir))
|
|
16
|
+
mkdirSync(logDir, { recursive: true });
|
|
17
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
18
|
+
logFile = join(logDir, `agentmesh-${date}.log`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function shouldLog(level) {
|
|
22
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
23
|
+
}
|
|
24
|
+
function writeLine(level, msg, data) {
|
|
25
|
+
const ts = new Date().toISOString();
|
|
26
|
+
const extra = data ? ' ' + JSON.stringify(data) : '';
|
|
27
|
+
const line = `[${ts}] [${level.toUpperCase()}] ${msg}${extra}`;
|
|
28
|
+
if (pinoInstance) {
|
|
29
|
+
// 委托给 Pino: 将 data 序列化到消息中
|
|
30
|
+
pinoInstance[level](line);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
process.stderr.write(line + '\n');
|
|
34
|
+
}
|
|
35
|
+
if (logFile) {
|
|
36
|
+
try {
|
|
37
|
+
Bun.write(logFile, line + '\n').catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const logger = {
|
|
43
|
+
debug(msg, data) { if (shouldLog('debug'))
|
|
44
|
+
writeLine('debug', msg, data); },
|
|
45
|
+
info(msg, data) { if (shouldLog('info'))
|
|
46
|
+
writeLine('info', msg, data); },
|
|
47
|
+
warn(msg, data) { if (shouldLog('warn'))
|
|
48
|
+
writeLine('warn', msg, data); },
|
|
49
|
+
error(msg, data) { if (shouldLog('error'))
|
|
50
|
+
writeLine('error', msg, data); },
|
|
51
|
+
};
|
|
52
|
+
export function createLogger(bindings) {
|
|
53
|
+
if (!pinoInstance)
|
|
54
|
+
return logger;
|
|
55
|
+
// 直接从全局 pino 实例获取 child,当前版本用 context 前缀
|
|
56
|
+
return logger;
|
|
57
|
+
}
|
|
@@ -2,11 +2,16 @@ import Fastify from 'fastify';
|
|
|
2
2
|
import cors from '@fastify/cors';
|
|
3
3
|
import { apiRoutes } from './routes/api.js';
|
|
4
4
|
import { websocketRoutes } from './routes/websocket.js';
|
|
5
|
+
import { modelGatewayRoutes } from './model-gateway/routes.js';
|
|
6
|
+
import { initModelRouter } from './model-gateway/router.js';
|
|
5
7
|
import { eventBus } from './core/event-bus.js';
|
|
6
8
|
import { router } from './core/router.js';
|
|
7
9
|
import { agentRegistry } from './core/agent-registry.js';
|
|
8
10
|
import { vectorStore } from './core/vector-store.js';
|
|
9
11
|
import { loadConfig, getRoutingRules, getDefaultAgent } from './core/config.js';
|
|
12
|
+
import { circuitBreakerRegistry } from './model-gateway/circuit-breaker.js';
|
|
13
|
+
import { configureRetry } from './model-gateway/retry.js';
|
|
14
|
+
import { initRateLimiter } from './model-gateway/rate-limit.js';
|
|
10
15
|
async function main() {
|
|
11
16
|
const config = loadConfig();
|
|
12
17
|
// 初始化 Fastify
|
|
@@ -22,6 +27,30 @@ async function main() {
|
|
|
22
27
|
// 注册路由
|
|
23
28
|
await fastify.register(apiRoutes);
|
|
24
29
|
await fastify.register(websocketRoutes);
|
|
30
|
+
await fastify.register(modelGatewayRoutes);
|
|
31
|
+
// 初始化模型网关
|
|
32
|
+
const modelsConfig = config.models;
|
|
33
|
+
if (modelsConfig) {
|
|
34
|
+
// 配置熔断器
|
|
35
|
+
const cbDefaults = modelsConfig.defaults?.circuit_breaker;
|
|
36
|
+
if (cbDefaults) {
|
|
37
|
+
for (const [name] of Object.entries(modelsConfig.providers || {})) {
|
|
38
|
+
circuitBreakerRegistry.configure(name, cbDefaults);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 配置重试
|
|
42
|
+
const retryDefaults = modelsConfig.defaults?.retry;
|
|
43
|
+
if (retryDefaults) {
|
|
44
|
+
configureRetry(retryDefaults);
|
|
45
|
+
}
|
|
46
|
+
// 初始化限流器
|
|
47
|
+
initRateLimiter();
|
|
48
|
+
initModelRouter(modelsConfig);
|
|
49
|
+
console.log(`[ModelGW] Initialized: ${Object.keys(modelsConfig.providers || {}).length} providers, fallback: ${modelsConfig.fallback_chain?.join(' → ')}`);
|
|
50
|
+
// 配额预热(异步,不阻塞启动)
|
|
51
|
+
import('./model-gateway/quota.js').then(m => m.probeQuota()).catch(() => { });
|
|
52
|
+
console.log('[ModelGW] Quota pre-warming started (background)');
|
|
53
|
+
}
|
|
25
54
|
// 初始化组件
|
|
26
55
|
const rules = getRoutingRules();
|
|
27
56
|
const defaultAgent = getDefaultAgent();
|
|
@@ -44,15 +73,18 @@ async function main() {
|
|
|
44
73
|
});
|
|
45
74
|
console.log(`
|
|
46
75
|
╔═══════════════════════════════════════════════════╗
|
|
47
|
-
║ Agent Gateway
|
|
76
|
+
║ Agent Mesh Gateway ║
|
|
77
|
+
╠═══════════════════════════════════════════════════╣
|
|
78
|
+
║ HTTP: http://${config.host}:${config.port} ║
|
|
79
|
+
║ WebSocket: ws://${config.host}:${config.port}/ws ║
|
|
80
|
+
║ Health: http://${config.host}:${config.port}/health ║
|
|
81
|
+
║ Tasks: http://${config.host}:${config.port}/tasks ║
|
|
82
|
+
║ Spaces: http://${config.host}:${config.port}/spaces ║
|
|
83
|
+
║ Agents: http://${config.host}:${config.port}/agents ║
|
|
48
84
|
╠═══════════════════════════════════════════════════╣
|
|
49
|
-
║
|
|
50
|
-
║
|
|
51
|
-
║
|
|
52
|
-
║ Health: http://${config.host}:${config.port}/health ║
|
|
53
|
-
║ Tasks: http://${config.host}:${config.port}/tasks ║
|
|
54
|
-
║ Spaces: http://${config.host}:${config.port}/spaces ║
|
|
55
|
-
║ Agents: http://${config.host}:${config.port}/agents ║
|
|
85
|
+
║ Model GW: http://${config.host}:${config.port}/v1/chat/completions ║
|
|
86
|
+
║ Models: http://${config.host}:${config.port}/v1/models ║
|
|
87
|
+
║ Quota: http://${config.host}:${config.port}/model-gateway/quota ║
|
|
56
88
|
╚═══════════════════════════════════════════════════╝
|
|
57
89
|
`);
|
|
58
90
|
// 订阅事件日志
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
2
|
+
export interface CircuitBreakerConfig {
|
|
3
|
+
failureThreshold: number;
|
|
4
|
+
resetTimeoutMs: number;
|
|
5
|
+
halfOpenMaxRequests: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class CircuitBreakerRegistry {
|
|
8
|
+
private circuits;
|
|
9
|
+
private configs;
|
|
10
|
+
configure(provider: string, config?: Partial<CircuitBreakerConfig>): void;
|
|
11
|
+
getState(provider: string): CircuitState;
|
|
12
|
+
isOpen(provider: string): boolean;
|
|
13
|
+
canRequest(provider: string): boolean;
|
|
14
|
+
recordSuccess(provider: string): void;
|
|
15
|
+
recordFailure(provider: string): void;
|
|
16
|
+
getStatus(): Record<string, {
|
|
17
|
+
state: CircuitState;
|
|
18
|
+
failures: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare const circuitBreakerRegistry: CircuitBreakerRegistry;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
failureThreshold: 3,
|
|
3
|
+
resetTimeoutMs: 30_000,
|
|
4
|
+
halfOpenMaxRequests: 1,
|
|
5
|
+
};
|
|
6
|
+
export class CircuitBreakerRegistry {
|
|
7
|
+
circuits = new Map();
|
|
8
|
+
configs = new Map();
|
|
9
|
+
configure(provider, config = {}) {
|
|
10
|
+
this.configs.set(provider, { ...DEFAULTS, ...config });
|
|
11
|
+
}
|
|
12
|
+
getState(provider) {
|
|
13
|
+
const entry = this.circuits.get(provider);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return 'CLOSED';
|
|
16
|
+
// OPEN → HALF_OPEN after timeout
|
|
17
|
+
if (entry.state === 'OPEN') {
|
|
18
|
+
const cfg = this.configs.get(provider) || DEFAULTS;
|
|
19
|
+
if (Date.now() - entry.lastFailureTime >= cfg.resetTimeoutMs) {
|
|
20
|
+
entry.state = 'HALF_OPEN';
|
|
21
|
+
entry.halfOpenCount = 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return entry.state;
|
|
25
|
+
}
|
|
26
|
+
isOpen(provider) {
|
|
27
|
+
return this.getState(provider) === 'OPEN';
|
|
28
|
+
}
|
|
29
|
+
canRequest(provider) {
|
|
30
|
+
const state = this.getState(provider);
|
|
31
|
+
if (state === 'CLOSED')
|
|
32
|
+
return true;
|
|
33
|
+
if (state === 'OPEN')
|
|
34
|
+
return false;
|
|
35
|
+
// HALF_OPEN: allow limited probe requests
|
|
36
|
+
const cfg = this.configs.get(provider) || DEFAULTS;
|
|
37
|
+
const entry = this.circuits.get(provider);
|
|
38
|
+
return (entry?.halfOpenCount ?? 0) < cfg.halfOpenMaxRequests;
|
|
39
|
+
}
|
|
40
|
+
recordSuccess(provider) {
|
|
41
|
+
const entry = this.circuits.get(provider);
|
|
42
|
+
if (!entry)
|
|
43
|
+
return;
|
|
44
|
+
if (entry.state === 'HALF_OPEN') {
|
|
45
|
+
// Half-open success → reset to closed
|
|
46
|
+
this.circuits.delete(provider);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Reset failure count on success in closed state
|
|
50
|
+
entry.failures = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
recordFailure(provider) {
|
|
54
|
+
let entry = this.circuits.get(provider);
|
|
55
|
+
if (!entry) {
|
|
56
|
+
entry = {
|
|
57
|
+
failures: 0,
|
|
58
|
+
lastFailureTime: 0,
|
|
59
|
+
state: 'CLOSED',
|
|
60
|
+
halfOpenCount: 0,
|
|
61
|
+
};
|
|
62
|
+
this.circuits.set(provider, entry);
|
|
63
|
+
}
|
|
64
|
+
const cfg = this.configs.get(provider) || DEFAULTS;
|
|
65
|
+
if (entry.state === 'HALF_OPEN') {
|
|
66
|
+
// Half-open failure → back to open
|
|
67
|
+
entry.state = 'OPEN';
|
|
68
|
+
entry.lastFailureTime = Date.now();
|
|
69
|
+
entry.halfOpenCount = 0;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
entry.failures++;
|
|
73
|
+
entry.lastFailureTime = Date.now();
|
|
74
|
+
if (entry.failures >= cfg.failureThreshold) {
|
|
75
|
+
entry.state = 'OPEN';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getStatus() {
|
|
79
|
+
const status = {};
|
|
80
|
+
for (const [name, entry] of this.circuits) {
|
|
81
|
+
status[name] = { state: this.getState(name), failures: entry.failures };
|
|
82
|
+
}
|
|
83
|
+
return status;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const circuitBreakerRegistry = new CircuitBreakerRegistry();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModelGatewayConfig, ResolvedProvider } from './types.js';
|
|
2
|
+
interface ProviderHealth {
|
|
3
|
+
provider: string;
|
|
4
|
+
status: 'healthy' | 'unhealthy' | 'unknown';
|
|
5
|
+
latency_ms: number;
|
|
6
|
+
circuit: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
checked_at: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function checkProviderHealth(provider: ResolvedProvider): Promise<ProviderHealth>;
|
|
11
|
+
export declare function checkAllProviders(config: ModelGatewayConfig): Promise<ProviderHealth[]>;
|
|
12
|
+
export {};
|