@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +60 -41
  2. package/README.zh-CN.md +137 -167
  3. package/config/gateway.yaml +78 -0
  4. package/dist/src/cli/connect.d.ts +26 -0
  5. package/dist/src/cli/connect.js +544 -0
  6. package/dist/src/cli/setup.d.ts +2 -0
  7. package/dist/src/cli/setup.js +97 -0
  8. package/dist/src/cli.js +410 -0
  9. package/dist/src/core/logger.d.ts +14 -0
  10. package/dist/src/core/logger.js +57 -0
  11. package/dist/{index.js → src/index.js} +40 -8
  12. package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
  13. package/dist/src/model-gateway/circuit-breaker.js +86 -0
  14. package/dist/src/model-gateway/health.d.ts +12 -0
  15. package/dist/src/model-gateway/health.js +80 -0
  16. package/dist/src/model-gateway/providers.d.ts +4 -0
  17. package/dist/src/model-gateway/providers.js +113 -0
  18. package/dist/src/model-gateway/quota.d.ts +4 -0
  19. package/dist/src/model-gateway/quota.js +107 -0
  20. package/dist/src/model-gateway/rate-limit.d.ts +12 -0
  21. package/dist/src/model-gateway/rate-limit.js +51 -0
  22. package/dist/src/model-gateway/retry.d.ts +14 -0
  23. package/dist/src/model-gateway/retry.js +48 -0
  24. package/dist/src/model-gateway/router.d.ts +4 -0
  25. package/dist/src/model-gateway/router.js +79 -0
  26. package/dist/src/model-gateway/routes.d.ts +2 -0
  27. package/dist/src/model-gateway/routes.js +172 -0
  28. package/dist/src/model-gateway/types.d.ts +47 -0
  29. package/dist/src/model-gateway/types.js +1 -0
  30. package/dist/tests/core/context-manager.test.d.ts +1 -0
  31. package/dist/tests/core/context-manager.test.js +35 -0
  32. package/dist/tests/core/router.test.d.ts +1 -0
  33. package/dist/tests/core/router.test.js +79 -0
  34. package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
  35. package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
  36. package/dist/tests/model-gateway/providers.test.d.ts +1 -0
  37. package/dist/tests/model-gateway/providers.test.js +80 -0
  38. package/dist/tests/model-gateway/quota.test.d.ts +1 -0
  39. package/dist/tests/model-gateway/quota.test.js +60 -0
  40. package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
  41. package/dist/tests/model-gateway/rate-limit.test.js +42 -0
  42. package/dist/tests/model-gateway/retry.test.d.ts +1 -0
  43. package/dist/tests/model-gateway/retry.test.js +47 -0
  44. package/dist/tests/model-gateway/router.test.d.ts +1 -0
  45. package/dist/tests/model-gateway/router.test.js +108 -0
  46. package/dist/tests/model-gateway/routes.test.d.ts +1 -0
  47. package/dist/tests/model-gateway/routes.test.js +83 -0
  48. package/docs/api.md +187 -460
  49. package/docs/architecture.md +138 -0
  50. package/docs/configuration.md +188 -0
  51. package/package.json +3 -1
  52. package/dist/cli.js +0 -246
  53. /package/dist/{adapters → src/adapters}/base.d.ts +0 -0
  54. /package/dist/{adapters → src/adapters}/base.js +0 -0
  55. /package/dist/{adapters → src/adapters}/claude-code.d.ts +0 -0
  56. /package/dist/{adapters → src/adapters}/claude-code.js +0 -0
  57. /package/dist/{adapters → src/adapters}/openclaw.d.ts +0 -0
  58. /package/dist/{adapters → src/adapters}/openclaw.js +0 -0
  59. /package/dist/{adapters → src/adapters}/process.d.ts +0 -0
  60. /package/dist/{adapters → src/adapters}/process.js +0 -0
  61. /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
  62. /package/dist/{core → src/core}/agent-registry.d.ts +0 -0
  63. /package/dist/{core → src/core}/agent-registry.js +0 -0
  64. /package/dist/{core → src/core}/config.d.ts +0 -0
  65. /package/dist/{core → src/core}/config.js +0 -0
  66. /package/dist/{core → src/core}/context-manager.d.ts +0 -0
  67. /package/dist/{core → src/core}/context-manager.js +0 -0
  68. /package/dist/{core → src/core}/event-bus.d.ts +0 -0
  69. /package/dist/{core → src/core}/event-bus.js +0 -0
  70. /package/dist/{core → src/core}/metrics.d.ts +0 -0
  71. /package/dist/{core → src/core}/metrics.js +0 -0
  72. /package/dist/{core → src/core}/router.d.ts +0 -0
  73. /package/dist/{core → src/core}/router.js +0 -0
  74. /package/dist/{core → src/core}/task-manager.d.ts +0 -0
  75. /package/dist/{core → src/core}/task-manager.js +0 -0
  76. /package/dist/{core → src/core}/vector-store.d.ts +0 -0
  77. /package/dist/{core → src/core}/vector-store.js +0 -0
  78. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  79. /package/dist/{routes → src/routes}/api.d.ts +0 -0
  80. /package/dist/{routes → src/routes}/api.js +0 -0
  81. /package/dist/{routes → src/routes}/websocket.d.ts +0 -0
  82. /package/dist/{routes → src/routes}/websocket.js +0 -0
  83. /package/dist/{types → src/types}/index.d.ts +0 -0
  84. /package/dist/{types → src/types}/index.js +0 -0
@@ -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 Server
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
- HTTP Server: http://${config.host}:${config.port}
50
- WebSocket: ws://${config.host}:${config.port}/ws
51
- SSE: http://${config.host}:${config.port}/events
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 {};