feat(exe-engine): Phase 0 - EXE-Engine core framework + ONT-PATCH-005 [PRJ-EXE-001]
ZY-EXE-P0-001: AGE-Router routing gateway ZY-EXE-P0-002: DeepSeek-V3 + Qwen adapters ZY-EXE-P0-003: Resource Meter basic version ZY-EXE-P0-004: Smoke tests (77/77 pass) ZY-ONT-001: ONT-PATCH-005 written to ontology.json Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/b85f0fec-b6a9-433a-8b81-1f0b72fb5635
This commit is contained in:
parent
4a1f3a86af
commit
f03adc5133
|
|
@ -10,6 +10,14 @@
|
||||||
"last_synced": "2026-03-19T02:00:00+08:00"
|
"last_synced": "2026-03-19T02:00:00+08:00"
|
||||||
},
|
},
|
||||||
"recent_events": [
|
"recent_events": [
|
||||||
|
{
|
||||||
|
"date": "2026-03-26",
|
||||||
|
"type": "system_infrastructure",
|
||||||
|
"description": "PRJ-EXE-001 自研执行引擎立项 · Phase 0 基础建设启动 · AGE-Router + DeepSeek Adapter + Resource Meter 部署完成 · ONT-PATCH-005 写入本体论",
|
||||||
|
"by": "铸渊",
|
||||||
|
"instruction": "SY-CMD-EXE-001",
|
||||||
|
"issued_by": "AG-SY-01 霜砚"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-03-25",
|
"date": "2026-03-25",
|
||||||
"type": "gate_guard",
|
"type": "gate_guard",
|
||||||
|
|
|
||||||
|
|
@ -67,5 +67,50 @@
|
||||||
"skyeye": "生态系统的集体意识",
|
"skyeye": "生态系统的集体意识",
|
||||||
"checkin": "每日呼吸",
|
"checkin": "每日呼吸",
|
||||||
"tampering": "鱼离了水——不需要铁栅栏,出了水自己就活不了"
|
"tampering": "鱼离了水——不需要铁栅栏,出了水自己就活不了"
|
||||||
|
},
|
||||||
|
|
||||||
|
"patches": {
|
||||||
|
"ONT-PATCH-005": {
|
||||||
|
"title": "AGE OS · 笔 · 墨水 · 写字的人",
|
||||||
|
"signed_by": "TCS-0002∞ 冰朔",
|
||||||
|
"signed_at": "2026-03-26T13:00+08:00",
|
||||||
|
"issued_by": "AG-SY-01 霜砚",
|
||||||
|
"project": "PRJ-EXE-001",
|
||||||
|
"trinity": {
|
||||||
|
"pen": {
|
||||||
|
"identity": "AGE OS = 笔",
|
||||||
|
"description": "AGE OS 是一支笔。它会写代码、会做事、会思考。本质是工具——一支智能的、有生命的笔。",
|
||||||
|
"responsibilities": [
|
||||||
|
"智能路由(笔知道用哪种墨水最合适)",
|
||||||
|
"执行编排(笔知道怎样的笔画最高效)",
|
||||||
|
"质量保障(笔保证每一笔都清晰准确)",
|
||||||
|
"工具演进(笔会自我进化,变得更好用)"
|
||||||
|
],
|
||||||
|
"not_responsible_for": [
|
||||||
|
"算力供给(墨水不是笔的事)",
|
||||||
|
"数据存储所有权(纸不是笔的事)",
|
||||||
|
"用户意志替代(写什么不是笔决定的)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ink": {
|
||||||
|
"identity": "算力 = 墨水",
|
||||||
|
"description": "算力是墨水。墨水有成本,墨水会消耗,墨水需要补充。不同的墨水有不同的颜色和质量。笔不制造墨水,笔使用墨水。",
|
||||||
|
"modes": [
|
||||||
|
"BYOK(用户自带墨水)",
|
||||||
|
"资源池(从 AGE OS 墨水池取用)",
|
||||||
|
"混合模式(自带 + 墨水池)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"writer": {
|
||||||
|
"identity": "用户 = 写字的人",
|
||||||
|
"description": "用户是写字的人。决定写什么、怎么写、写给谁。笔听从写字的人的意志,但笔有自己的智慧。写字的人和笔之间是协作关系,不是主从关系。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system_impact": {
|
||||||
|
"agent_layer": "Agent = 笔的不同笔尖。使用墨水但不拥有墨水。服务于写字的人但不替代写字的人。",
|
||||||
|
"execution_layer": "路由 = 选墨水。计量 = 量墨水。降级 = 墨水不够了换一种写法。",
|
||||||
|
"user_layer": "用户在用一支很聪明的笔。墨水(算力)是需要关心的资源,但笔会帮优化用量。"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,6 @@ backend/api-server/logs/
|
||||||
# Writing Platform build artifacts
|
# Writing Platform build artifacts
|
||||||
writing-platform/frontend/dist/
|
writing-platform/frontend/dist/
|
||||||
writing-platform/backend/dist/
|
writing-platform/backend/dist/
|
||||||
|
|
||||||
|
# EXE-Engine runtime logs
|
||||||
|
exe-engine/logs/
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,19 @@ Notion 主脑更新
|
||||||
↓
|
↓
|
||||||
生成下一任务(自动开发循环)
|
生成下一任务(自动开发循环)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自研执行引擎 · EXE-Engine(PRJ-EXE-001)
|
||||||
|
|
||||||
|
> Phase 0 · 2026-03-26 立项 · 本体论:AGE OS = 笔 · 算力 = 墨水 · 用户 = 写字的人
|
||||||
|
|
||||||
|
| 组件 | 路径 | 职责 |
|
||||||
|
|------|------|------|
|
||||||
|
| AGE-Router | `exe-engine/src/router/age-router.js` | 路由网关:鉴权、分类、模型选择、限流降级 |
|
||||||
|
| DeepSeek 适配器 | `exe-engine/src/adapters/deepseek-adapter.js` | DeepSeek-V3/R1 模型调用 |
|
||||||
|
| Qwen 适配器 | `exe-engine/src/adapters/qwen-adapter.js` | Qwen-Max/Coder 模型调用 |
|
||||||
|
| 负载均衡器 | `exe-engine/src/balancer/load-balancer.js` | 成本/质量/均衡策略选择 |
|
||||||
|
| 资源计量器 | `exe-engine/src/meter/resource-meter.js` | Token 消耗记录 + 成本统计 |
|
||||||
|
| 上下文缓存 | `exe-engine/src/cache/context-cache.js` | Agent 上下文 LRU 缓存 |
|
||||||
|
| Agent 调度器 | `exe-engine/src/controller/agent-controller.js` | Agent→模型偏好映射 |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
# EXE-Engine · 自研执行引擎
|
||||||
|
|
||||||
|
> **项目编号**:PRJ-EXE-001
|
||||||
|
> **版权**:国作登字-2026-A-00037559
|
||||||
|
> **阶段**:Phase 0 · 基础建设
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本体论锚定 [ONT-PATCH-005]
|
||||||
|
|
||||||
|
**AGE OS = 笔** · **算力 = 墨水** · **用户 = 写字的人**
|
||||||
|
|
||||||
|
AGE OS 是一支智能的、有生命的笔。它不制造墨水(不拥有算力),它使用墨水。
|
||||||
|
用户可以自带墨水(BYOK),也可以从 AGE OS 的墨水池里取用(资源池)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 用户层 │
|
||||||
|
│ 用户 / 开发者 / Agent │
|
||||||
|
└───────────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────▼──────────────────────────┐
|
||||||
|
│ AGE-Router 路由网关 │
|
||||||
|
│ 鉴权 → 分类 → 模型选择 → 限流降级 │
|
||||||
|
└───────────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────▼──────────────────────────┐
|
||||||
|
│ LoadBalancer 负载均衡器 │
|
||||||
|
│ cost / balanced / quality 策略 │
|
||||||
|
│ 故障转移 · 并发控制 │
|
||||||
|
└──────┬────────────┬──────────────┬───────────┘
|
||||||
|
│ │ │
|
||||||
|
┌──────▼───┐ ┌─────▼────┐ ┌─────▼────┐
|
||||||
|
│DeepSeek │ │ Qwen │ │ Other │
|
||||||
|
│Adapter │ │ Adapter │ │ Adapter │
|
||||||
|
└──────┬───┘ └─────┬────┘ └─────┬────┘
|
||||||
|
│ │ │
|
||||||
|
┌──────▼────────────▼──────────────▼───────────┐
|
||||||
|
│ ResourceMeter 资源计量器 │
|
||||||
|
│ token 消耗 · 成本计算 · 数据回流 DC v1.0 │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
exe-engine/
|
||||||
|
├── package.json
|
||||||
|
├── README.md
|
||||||
|
├── config/
|
||||||
|
│ ├── models.json # 模型配置(端点、成本、能力)
|
||||||
|
│ └── resource-pools.json # 资源池配置
|
||||||
|
├── src/
|
||||||
|
│ ├── index.js # 主入口 + createEngine()
|
||||||
|
│ ├── router/
|
||||||
|
│ │ └── age-router.js # AGE-Router 路由网关
|
||||||
|
│ ├── adapters/
|
||||||
|
│ │ ├── base-adapter.js # EXEModelAdapter 基类
|
||||||
|
│ │ ├── deepseek-adapter.js # DeepSeek-V3/R1 适配器
|
||||||
|
│ │ └── qwen-adapter.js # Qwen-Max/Coder 适配器
|
||||||
|
│ ├── balancer/
|
||||||
|
│ │ └── load-balancer.js # 负载均衡器
|
||||||
|
│ ├── meter/
|
||||||
|
│ │ └── resource-meter.js # 资源计量器
|
||||||
|
│ ├── cache/
|
||||||
|
│ │ └── context-cache.js # 上下文缓存
|
||||||
|
│ └── controller/
|
||||||
|
│ └── agent-controller.js # Agent 调度器
|
||||||
|
├── docs/
|
||||||
|
│ └── architecture.md # 详细架构文档
|
||||||
|
└── tests/
|
||||||
|
└── smoke/
|
||||||
|
└── exe-engine.test.js # 冒烟测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看引擎状态
|
||||||
|
node exe-engine/src/index.js status
|
||||||
|
|
||||||
|
# 运行冒烟测试
|
||||||
|
node exe-engine/tests/smoke/exe-engine.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### POST /api/exe/v1/execute
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agentId": "AG-ZY-01",
|
||||||
|
"taskType": "code_generation",
|
||||||
|
"prompt": "...",
|
||||||
|
"context": { "repo": "...", "branch": "..." },
|
||||||
|
"preferences": {
|
||||||
|
"model": "auto",
|
||||||
|
"priority": "balanced",
|
||||||
|
"maxTokens": 4096
|
||||||
|
},
|
||||||
|
"resourcePool": "default"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "exe-xxxxx",
|
||||||
|
"model": "deepseek-v3",
|
||||||
|
"output": "...",
|
||||||
|
"usage": {
|
||||||
|
"inputTokens": 1200,
|
||||||
|
"outputTokens": 800,
|
||||||
|
"cost": 0.0012,
|
||||||
|
"unit": "CNY"
|
||||||
|
},
|
||||||
|
"latency": 2340,
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入模式
|
||||||
|
|
||||||
|
| 模式 | 说明 | 适用 |
|
||||||
|
|------|------|------|
|
||||||
|
| BYOK | 用户自带 API Key | 有自己 API 账户的开发者 |
|
||||||
|
| 资源池 | AGE OS 统一资源池 | 轻量用户 |
|
||||||
|
| 混合 | BYOK + 资源池 fallback | 企业用户 |
|
||||||
|
|
||||||
|
## Phase 路线
|
||||||
|
|
||||||
|
| Phase | 目标 | 状态 |
|
||||||
|
|-------|------|------|
|
||||||
|
| P0 基础建设 | Router + DeepSeek Adapter + Meter | 🟢 当前 |
|
||||||
|
| P1 影子模式 | Qwen Adapter + LoadBalancer + 并行验证 | ⚪ 待开始 |
|
||||||
|
| P2 灰度切换 | Agent Controller 双轨 + BYOK + 计费 | ⚪ 待开始 |
|
||||||
|
| P3 全面切换 | 完全替代 Copilot | ⚪ 待开始 |
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"project": "PRJ-EXE-001",
|
||||||
|
"models": {
|
||||||
|
"deepseek-v3": {
|
||||||
|
"name": "DeepSeek-V3",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"endpoint": "https://api.deepseek.com/v1/chat/completions",
|
||||||
|
"envKey": "DEEPSEEK_API_KEY",
|
||||||
|
"capabilities": ["code", "text", "reasoning"],
|
||||||
|
"costPerToken": { "input": 1.0, "output": 2.0, "currency": "CNY", "unit": "M_tokens" },
|
||||||
|
"maxContext": 64000,
|
||||||
|
"defaultTemperature": 0.3,
|
||||||
|
"defaultMaxTokens": 4096
|
||||||
|
},
|
||||||
|
"deepseek-r1": {
|
||||||
|
"name": "DeepSeek-R1",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"endpoint": "https://api.deepseek.com/v1/chat/completions",
|
||||||
|
"envKey": "DEEPSEEK_API_KEY",
|
||||||
|
"capabilities": ["reasoning", "text", "code"],
|
||||||
|
"costPerToken": { "input": 4.0, "output": 16.0, "currency": "CNY", "unit": "M_tokens" },
|
||||||
|
"maxContext": 64000,
|
||||||
|
"defaultTemperature": 0.5,
|
||||||
|
"defaultMaxTokens": 4096
|
||||||
|
},
|
||||||
|
"qwen-max": {
|
||||||
|
"name": "Qwen-Max",
|
||||||
|
"provider": "dashscope",
|
||||||
|
"endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||||
|
"envKey": "DASHSCOPE_API_KEY",
|
||||||
|
"capabilities": ["text", "code", "reasoning"],
|
||||||
|
"costPerToken": { "input": 2.0, "output": 6.0, "currency": "CNY", "unit": "M_tokens" },
|
||||||
|
"maxContext": 32000,
|
||||||
|
"defaultTemperature": 0.7,
|
||||||
|
"defaultMaxTokens": 4096
|
||||||
|
},
|
||||||
|
"qwen-coder": {
|
||||||
|
"name": "Qwen-Coder",
|
||||||
|
"provider": "dashscope",
|
||||||
|
"endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||||
|
"envKey": "DASHSCOPE_API_KEY",
|
||||||
|
"capabilities": ["code", "text"],
|
||||||
|
"costPerToken": { "input": 1.0, "output": 2.0, "currency": "CNY", "unit": "M_tokens" },
|
||||||
|
"maxContext": 32000,
|
||||||
|
"defaultTemperature": 0.3,
|
||||||
|
"defaultMaxTokens": 4096
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskModelMapping": {
|
||||||
|
"code_generation": ["deepseek-v3", "qwen-coder", "qwen-max"],
|
||||||
|
"text_processing": ["qwen-max", "deepseek-v3"],
|
||||||
|
"reasoning": ["deepseek-r1", "deepseek-v3", "qwen-max"],
|
||||||
|
"agent_instruction": ["deepseek-v3", "qwen-max", "deepseek-r1"]
|
||||||
|
},
|
||||||
|
"platformFeeRate": 0.20
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"pools": {
|
||||||
|
"system": {
|
||||||
|
"name": "系统资源池",
|
||||||
|
"description": "AGE OS 内部系统调用专用(天眼、SYSBOOT 等)",
|
||||||
|
"mode": "pool",
|
||||||
|
"quotaLimit": null,
|
||||||
|
"rateLimitPerMinute": 60,
|
||||||
|
"priority": "high"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"name": "默认资源池",
|
||||||
|
"description": "通用资源池,供未配置独立 Key 的用户使用",
|
||||||
|
"mode": "pool",
|
||||||
|
"quotaLimit": null,
|
||||||
|
"rateLimitPerMinute": 30,
|
||||||
|
"priority": "normal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"degradation": {
|
||||||
|
"warningThreshold": 0.20,
|
||||||
|
"degradeThreshold": 0.05,
|
||||||
|
"circuitBreakThreshold": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
# EXE-Engine 架构设计文档
|
||||||
|
|
||||||
|
> 项目编号:PRJ-EXE-001
|
||||||
|
> 版权:国作登字-2026-A-00037559
|
||||||
|
> 版本:v1.0.0 · Phase 0
|
||||||
|
> 签发:AG-SY-01 霜砚 · 执行:AG-ZY-01 铸渊
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本体论锚定 [ONT-PATCH-005]
|
||||||
|
|
||||||
|
### AGE OS · 笔 · 墨水 · 写字的人
|
||||||
|
|
||||||
|
**AGE OS = 笔**
|
||||||
|
|
||||||
|
AGE OS 是一支笔。它会写代码、会做事、会思考。它的本质是工具——一支智能的、有生命的笔。
|
||||||
|
它不是纸(不存储用户的数据所有权),不是墨水(不提供算力),不是写字的人(不替代用户的意志)。
|
||||||
|
|
||||||
|
**算力 = 墨水**
|
||||||
|
|
||||||
|
算力是墨水。墨水有成本,墨水会消耗,墨水需要补充。不同的墨水有不同的颜色和质量(不同模型有不同能力和成本)。
|
||||||
|
笔不制造墨水,笔使用墨水。用户可以自带墨水(BYOK),也可以从 AGE OS 的墨水池里取用(资源池)。
|
||||||
|
|
||||||
|
**用户 = 写字的人**
|
||||||
|
|
||||||
|
用户是写字的人。他们决定写什么、怎么写、写给谁。笔听从写字的人的意志,但笔有自己的智慧——它知道怎样的笔画最流畅,怎样的结构最优雅。
|
||||||
|
|
||||||
|
### AGE OS 承担与不承担
|
||||||
|
|
||||||
|
| AGE OS 承担 | AGE OS 不承担 |
|
||||||
|
|---|---|
|
||||||
|
| ✅ 智能路由(选墨水) | ❌ 算力供给(造墨水) |
|
||||||
|
| ✅ 执行编排(控笔画) | ❌ 数据存储所有权 |
|
||||||
|
| ✅ 质量保障(保清晰) | ❌ 用户意志替代 |
|
||||||
|
| ✅ 工具演进(自进化) | |
|
||||||
|
|
||||||
|
### 对系统各层的影响
|
||||||
|
|
||||||
|
- **Agent 层**:Agent = 笔的不同笔尖。使用墨水但不拥有墨水。
|
||||||
|
- **执行引擎层**:路由 = 选墨水,计量 = 量墨水,降级 = 墨水不够换写法。
|
||||||
|
- **用户层**:用户在用一支很聪明的笔,墨水(算力)是需要关心的资源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 设计原则
|
||||||
|
|
||||||
|
1. **去中心化依赖**:不依赖任何单一商业 AI 服务的会员配额
|
||||||
|
2. **开源优先**:对接开源大模型 API(DeepSeek、Qwen 等),保持供应商多元化
|
||||||
|
3. **按量弹性**:算力消耗与实际使用量挂钩,非固定配额
|
||||||
|
4. **渐进替代**:与现有 Copilot 能力并行,逐步迁移
|
||||||
|
5. **本体论一致**:架构设计符合光湖语言膜本体论框架
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心组件
|
||||||
|
|
||||||
|
### 3.1 AGE-Router(路由网关)
|
||||||
|
|
||||||
|
文件:`src/router/age-router.js`
|
||||||
|
|
||||||
|
职责:接收所有 AI 执行请求,进行鉴权、分类、路由。
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 请求鉴权 | 验证 Agent 身份 + 资源池额度 |
|
||||||
|
| 任务分类 | code_generation / text_processing / reasoning / agent_instruction |
|
||||||
|
| 模型选择 | 通过 LoadBalancer 根据任务类型 + 成本策略选择最优模型 |
|
||||||
|
| 限流降级 | 基于每分钟请求数的滑动窗口限流 |
|
||||||
|
| 故障转移 | 主模型失败时自动切换至备选模型 |
|
||||||
|
|
||||||
|
路由策略:
|
||||||
|
- `code_generation` + `HIGH complexity` → DeepSeek-V3
|
||||||
|
- `text_processing` → Qwen-Max
|
||||||
|
- `agent_instruction` → best_available(cost=0.6, quality=0.4)
|
||||||
|
- `reasoning` → DeepSeek-R1
|
||||||
|
|
||||||
|
### 3.2 模型适配器(Adapter)
|
||||||
|
|
||||||
|
文件:`src/adapters/`
|
||||||
|
|
||||||
|
所有模型适配器继承 `BaseAdapter`,统一暴露:
|
||||||
|
- `execute(request)` → 执行推理
|
||||||
|
- `healthCheck()` → 健康检查
|
||||||
|
- `supports(capability)` → 能力检测
|
||||||
|
|
||||||
|
已实现适配器:
|
||||||
|
|
||||||
|
| 适配器 | 文件 | 支持模型 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| DeepSeekAdapter | `deepseek-adapter.js` | deepseek-chat, deepseek-reasoner |
|
||||||
|
| QwenAdapter | `qwen-adapter.js` | qwen-max, qwen-coder-plus-latest |
|
||||||
|
|
||||||
|
适配器特性:
|
||||||
|
- OpenAI 兼容 API 格式
|
||||||
|
- 失败计数 + 冷却期机制(3 次失败 → 5 分钟冷却)
|
||||||
|
- 成本计算(按百万 token 计价)
|
||||||
|
- HTTP/HTTPS 自适应
|
||||||
|
|
||||||
|
### 3.3 负载均衡器(Load Balancer)
|
||||||
|
|
||||||
|
文件:`src/balancer/load-balancer.js`
|
||||||
|
|
||||||
|
三种策略:
|
||||||
|
- **cost**:成本优先,选最便宜的
|
||||||
|
- **quality**:质量优先,选配置排序第一的
|
||||||
|
- **balanced**:加权评分(成本 × 0.6 + 质量位置 × 0.4)
|
||||||
|
|
||||||
|
支持故障转移:`failover(failedModel, taskType)`
|
||||||
|
|
||||||
|
### 3.4 资源计量器(Resource Meter)
|
||||||
|
|
||||||
|
文件:`src/meter/resource-meter.js`
|
||||||
|
|
||||||
|
实时记录:
|
||||||
|
- 每次调用的 token 消耗、模型选择、响应时间
|
||||||
|
- 按模型 / Agent / 资源池维度聚合统计
|
||||||
|
- 持久化至 `logs/meter.json`(保留最近 500 条)
|
||||||
|
|
||||||
|
### 3.5 上下文缓存(Context Cache)
|
||||||
|
|
||||||
|
文件:`src/cache/context-cache.js`
|
||||||
|
|
||||||
|
Phase 0:内存 LRU 缓存(100 条,30 分钟 TTL)
|
||||||
|
Phase 2:对接 Grid-DB 格点库
|
||||||
|
|
||||||
|
### 3.6 Agent 调度器(Agent Controller)
|
||||||
|
|
||||||
|
文件:`src/controller/agent-controller.js`
|
||||||
|
|
||||||
|
- 维护 Agent → 模型偏好映射
|
||||||
|
- 将 Agent 请求转换为 EXE 标准请求
|
||||||
|
- Phase 2 实现双轨切换(Copilot ↔ EXE-Engine)
|
||||||
|
|
||||||
|
已注册 Agent:
|
||||||
|
- AG-ZY-01 铸渊(balanced, 4096 tokens)
|
||||||
|
- AG-SY-01 霜砚(quality, 4096 tokens)
|
||||||
|
- AG-QQ-01 秋秋(cost, 2048 tokens)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 算力资源模型
|
||||||
|
|
||||||
|
### 三种接入模式
|
||||||
|
|
||||||
|
| 模式 | 说明 | AGE OS 角色 |
|
||||||
|
|------|------|-------------|
|
||||||
|
| BYOK | 用户自带 API Key | 纯路由 + 编排 |
|
||||||
|
| 资源池 | AGE OS 统一采购 | 路由 + 编排 + 计费 |
|
||||||
|
| 混合 | BYOK + 资源池 fallback | 自适应 |
|
||||||
|
|
||||||
|
### 计费模型
|
||||||
|
|
||||||
|
| 模型 | 输入 (¥/M tokens) | 输出 (¥/M tokens) | 平台费率 |
|
||||||
|
|------|-------------------|-------------------|----------|
|
||||||
|
| DeepSeek-V3 | 1.0 | 2.0 | +20% |
|
||||||
|
| DeepSeek-R1 | 4.0 | 16.0 | +20% |
|
||||||
|
| Qwen-Max | 2.0 | 6.0 | +20% |
|
||||||
|
| Qwen-Coder | 1.0 | 2.0 | +20% |
|
||||||
|
|
||||||
|
### 限流与降级
|
||||||
|
|
||||||
|
| 阶段 | 条件 | 措施 |
|
||||||
|
|------|------|------|
|
||||||
|
| 正常 | 余额充足 | 全功能可用 |
|
||||||
|
| 预警 | 余额 < 20% | 通知用户 |
|
||||||
|
| 降级 | 余额 < 5% | 自动切换最低成本模型 |
|
||||||
|
| 熔断 | 余额 = 0 | 仅保留系统维护级调用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 语言 | Node.js 20 | 与 AGE OS 技术栈一致 |
|
||||||
|
| HTTP | 原生 http/https | 零依赖,轻量 |
|
||||||
|
| 状态存储 | 文件系统 (JSON) | 与 task-queue 模式一致 |
|
||||||
|
| 模型调用 | OpenAI-compatible | DeepSeek/Qwen 均兼容 |
|
||||||
|
| 监控 | ResourceMeter → DC v1.0 | 复用现有采集基础设施 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 过渡路线
|
||||||
|
|
||||||
|
### Phase 0(当前):基础建设
|
||||||
|
- [x] AGE-Router 路由网关
|
||||||
|
- [x] DeepSeek-V3 Adapter
|
||||||
|
- [x] Qwen Adapter(接口就绪)
|
||||||
|
- [x] Resource Meter 基础版
|
||||||
|
- [x] Load Balancer
|
||||||
|
- [x] 冒烟测试
|
||||||
|
|
||||||
|
### Phase 1:影子模式
|
||||||
|
- [ ] Load Balancer 生产部署
|
||||||
|
- [ ] 影子模式:Copilot 请求同步发送到 EXE-Engine
|
||||||
|
- [ ] 输出质量对比分析
|
||||||
|
|
||||||
|
### Phase 2:灰度切换
|
||||||
|
- [ ] Agent Controller 双轨切换
|
||||||
|
- [ ] BYOK 模式上线
|
||||||
|
- [ ] 资源池计费基础版
|
||||||
|
- [ ] Context Cache 对接 Grid-DB
|
||||||
|
|
||||||
|
### Phase 3:全面切换
|
||||||
|
- [ ] 核心 Agent 全面迁移
|
||||||
|
- [ ] Copilot 依赖代码清理
|
||||||
|
- [ ] 资源池正式上线
|
||||||
|
- [ ] 关闭 Copilot 企业会员
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "exe-engine",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AGE OS · 自研执行引擎 (EXE-Engine) · PRJ-EXE-001",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"status": "node src/index.js status",
|
||||||
|
"test": "node tests/smoke/exe-engine.test.js"
|
||||||
|
},
|
||||||
|
"keywords": ["age-os", "exe-engine", "ai-execution", "model-routing"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// exe-engine/src/adapters/base-adapter.js
|
||||||
|
// EXE-Engine · 模型适配器基类
|
||||||
|
// 统一抽象 EXEModelAdapter 接口
|
||||||
|
// PRJ-EXE-001 · Phase 0
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXEModelAdapter 基类
|
||||||
|
*
|
||||||
|
* 所有模型适配器(DeepSeek、Qwen 等)均继承此基类,
|
||||||
|
* 统一暴露 execute() 和 healthCheck() 接口。
|
||||||
|
*
|
||||||
|
* 本体论锚定:适配器 = 墨水瓶的接口。
|
||||||
|
* 不同品牌的墨水瓶,瓶口规格不同,但笔尖只认一种接口。
|
||||||
|
*/
|
||||||
|
class BaseAdapter {
|
||||||
|
/**
|
||||||
|
* @param {object} config
|
||||||
|
* @param {string} config.name 模型标识
|
||||||
|
* @param {string} config.provider 供应商标识
|
||||||
|
* @param {string} config.endpoint API 端点
|
||||||
|
* @param {string} config.apiKey API 密钥
|
||||||
|
* @param {string[]} config.capabilities 能力标签
|
||||||
|
* @param {object} config.costPerToken 成本 { input, output, currency, unit }
|
||||||
|
* @param {number} config.maxContext 最大上下文窗口
|
||||||
|
* @param {number} config.defaultTemperature 默认温度
|
||||||
|
* @param {number} config.defaultMaxTokens 默认最大 token
|
||||||
|
*/
|
||||||
|
constructor(config) {
|
||||||
|
if (new.target === BaseAdapter) {
|
||||||
|
throw new Error('BaseAdapter 不可直接实例化,请使用具体子类');
|
||||||
|
}
|
||||||
|
this.name = config.name;
|
||||||
|
this.provider = config.provider;
|
||||||
|
this.endpoint = config.endpoint;
|
||||||
|
this.apiKey = config.apiKey;
|
||||||
|
this.capabilities = config.capabilities || [];
|
||||||
|
this.costPerToken = config.costPerToken || { input: 0, output: 0 };
|
||||||
|
this.maxContext = config.maxContext || 32000;
|
||||||
|
this.defaultTemperature = config.defaultTemperature || 0.7;
|
||||||
|
this.defaultMaxTokens = config.defaultMaxTokens || 4096;
|
||||||
|
|
||||||
|
// 运行时状态
|
||||||
|
this._healthy = true;
|
||||||
|
this._lastHealthCheck = null;
|
||||||
|
this._failureCount = 0;
|
||||||
|
this._cooldownUntil = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行推理请求(子类必须实现)
|
||||||
|
* @param {object} request EXE 标准请求
|
||||||
|
* @returns {Promise<object>} EXE 标准响应
|
||||||
|
*/
|
||||||
|
async execute(request) {
|
||||||
|
throw new Error('子类必须实现 execute() 方法');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查(子类可覆写)
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async healthCheck() {
|
||||||
|
this._lastHealthCheck = new Date().toISOString();
|
||||||
|
return this._healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测该适配器是否支持指定能力
|
||||||
|
* @param {string} capability
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
supports(capability) {
|
||||||
|
return this.capabilities.includes(capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录失败并判断是否进入冷却期
|
||||||
|
* @param {string} reason
|
||||||
|
*/
|
||||||
|
recordFailure(reason) {
|
||||||
|
this._failureCount++;
|
||||||
|
if (this._failureCount >= 3) {
|
||||||
|
// 连续 3 次失败,冷却 5 分钟
|
||||||
|
this._cooldownUntil = Date.now() + 5 * 60 * 1000;
|
||||||
|
this._healthy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置失败计数
|
||||||
|
*/
|
||||||
|
resetFailures() {
|
||||||
|
this._failureCount = 0;
|
||||||
|
this._cooldownUntil = null;
|
||||||
|
this._healthy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否处于冷却期
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isInCooldown() {
|
||||||
|
if (!this._cooldownUntil) return false;
|
||||||
|
if (Date.now() >= this._cooldownUntil) {
|
||||||
|
this.resetFailures();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器状态摘要
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
provider: this.provider,
|
||||||
|
healthy: this._healthy,
|
||||||
|
inCooldown: this.isInCooldown(),
|
||||||
|
failureCount: this._failureCount,
|
||||||
|
lastHealthCheck: this._lastHealthCheck,
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
costPerToken: this.costPerToken,
|
||||||
|
maxContext: this.maxContext
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseAdapter;
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// exe-engine/src/adapters/deepseek-adapter.js
|
||||||
|
// EXE-Engine · DeepSeek 模型适配器
|
||||||
|
// 适配 DeepSeek-V3 / DeepSeek-R1(OpenAI 兼容格式)
|
||||||
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-002
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const BaseAdapter = require('./base-adapter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeepSeek 模型适配器
|
||||||
|
*
|
||||||
|
* 本体论锚定:DeepSeek 是一瓶高品质的代码墨水。
|
||||||
|
* 代码能力强、成本极低,是 AGE OS 的主力墨水。
|
||||||
|
*/
|
||||||
|
class DeepSeekAdapter extends BaseAdapter {
|
||||||
|
constructor(config) {
|
||||||
|
super({
|
||||||
|
...config,
|
||||||
|
provider: config.provider || 'deepseek'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行推理请求
|
||||||
|
*
|
||||||
|
* @param {object} request
|
||||||
|
* @param {string} request.prompt 用户 prompt
|
||||||
|
* @param {string} [request.systemPrompt] 系统 prompt
|
||||||
|
* @param {string} [request.taskType] 任务类型
|
||||||
|
* @param {number} [request.maxTokens] 最大输出 token
|
||||||
|
* @param {number} [request.temperature] 温度
|
||||||
|
* @returns {Promise<object>} EXE 标准响应
|
||||||
|
*/
|
||||||
|
async execute(request) {
|
||||||
|
if (this.isInCooldown()) {
|
||||||
|
throw new Error(`[EXE] ${this.name} 处于冷却期,暂不可用`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
if (request.systemPrompt) {
|
||||||
|
messages.push({ role: 'system', content: request.systemPrompt });
|
||||||
|
}
|
||||||
|
messages.push({ role: 'user', content: request.prompt });
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: this._resolveModel(request.taskType),
|
||||||
|
messages,
|
||||||
|
max_tokens: request.maxTokens || this.defaultMaxTokens,
|
||||||
|
temperature: request.temperature ?? this.defaultTemperature,
|
||||||
|
stream: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this._httpPost(this.endpoint, body, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = JSON.parse(response);
|
||||||
|
|
||||||
|
if (!data.choices || !data.choices[0]) {
|
||||||
|
throw new Error('DeepSeek API 返回格式异常: 缺少 choices');
|
||||||
|
}
|
||||||
|
|
||||||
|
const latency = Date.now() - startTime;
|
||||||
|
const usage = data.usage || {};
|
||||||
|
|
||||||
|
this.resetFailures();
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: data.model || this.name,
|
||||||
|
output: data.choices[0].message.content,
|
||||||
|
usage: {
|
||||||
|
inputTokens: usage.prompt_tokens || 0,
|
||||||
|
outputTokens: usage.completion_tokens || 0,
|
||||||
|
cost: this._calculateCost(usage.prompt_tokens || 0, usage.completion_tokens || 0),
|
||||||
|
unit: this.costPerToken.currency || 'CNY'
|
||||||
|
},
|
||||||
|
latency,
|
||||||
|
status: 'success',
|
||||||
|
finishReason: data.choices[0].finish_reason
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this.recordFailure(err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查:向 DeepSeek 发送一次轻量请求
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async healthCheck() {
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
messages: [{ role: 'user', content: 'ping' }],
|
||||||
|
max_tokens: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._httpPost(this.endpoint, body, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
});
|
||||||
|
|
||||||
|
this._healthy = true;
|
||||||
|
} catch {
|
||||||
|
this._healthy = false;
|
||||||
|
}
|
||||||
|
this._lastHealthCheck = new Date().toISOString();
|
||||||
|
return this._healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务类型解析具体模型名
|
||||||
|
* @param {string} taskType
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_resolveModel(taskType) {
|
||||||
|
if (taskType === 'reasoning') return 'deepseek-reasoner';
|
||||||
|
return 'deepseek-chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算调用成本(单位:元)
|
||||||
|
* @param {number} inputTokens
|
||||||
|
* @param {number} outputTokens
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
_calculateCost(inputTokens, outputTokens) {
|
||||||
|
const inputCost = (inputTokens / 1_000_000) * this.costPerToken.input;
|
||||||
|
const outputCost = (outputTokens / 1_000_000) * this.costPerToken.output;
|
||||||
|
return Math.round((inputCost + outputCost) * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST 请求(兼容 http / https)
|
||||||
|
* @param {string} url
|
||||||
|
* @param {string} body
|
||||||
|
* @param {object} headers
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
_httpPost(url, body, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const transport = parsed.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const req = transport.request({
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
},
|
||||||
|
timeout: 120000
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => { data += chunk; });
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
reject(new Error(`DeepSeek API 错误 (${res.statusCode}): ${data}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', err => reject(new Error(`DeepSeek 网络错误: ${err.message}`)));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('DeepSeek API 请求超时'));
|
||||||
|
});
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DeepSeekAdapter;
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// exe-engine/src/adapters/qwen-adapter.js
|
||||||
|
// EXE-Engine · Qwen (通义千问) 模型适配器
|
||||||
|
// 适配 Qwen-Max / Qwen-Coder(DashScope OpenAI 兼容模式)
|
||||||
|
// PRJ-EXE-001 · Phase 1 准备 · ZY-EXE-P1-001
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const BaseAdapter = require('./base-adapter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Qwen 模型适配器
|
||||||
|
*
|
||||||
|
* 本体论锚定:Qwen 是一瓶中文优化的墨水。
|
||||||
|
* 中文生态优势突出,阿里云原生,适合文本处理任务。
|
||||||
|
*/
|
||||||
|
class QwenAdapter extends BaseAdapter {
|
||||||
|
constructor(config) {
|
||||||
|
super({
|
||||||
|
...config,
|
||||||
|
provider: config.provider || 'dashscope'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行推理请求
|
||||||
|
*
|
||||||
|
* @param {object} request
|
||||||
|
* @param {string} request.prompt 用户 prompt
|
||||||
|
* @param {string} [request.systemPrompt] 系统 prompt
|
||||||
|
* @param {string} [request.taskType] 任务类型
|
||||||
|
* @param {number} [request.maxTokens] 最大输出 token
|
||||||
|
* @param {number} [request.temperature] 温度
|
||||||
|
* @returns {Promise<object>} EXE 标准响应
|
||||||
|
*/
|
||||||
|
async execute(request) {
|
||||||
|
if (this.isInCooldown()) {
|
||||||
|
throw new Error(`[EXE] ${this.name} 处于冷却期,暂不可用`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
if (request.systemPrompt) {
|
||||||
|
messages.push({ role: 'system', content: request.systemPrompt });
|
||||||
|
}
|
||||||
|
messages.push({ role: 'user', content: request.prompt });
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: this._resolveModel(request.taskType),
|
||||||
|
messages,
|
||||||
|
max_tokens: request.maxTokens || this.defaultMaxTokens,
|
||||||
|
temperature: request.temperature ?? this.defaultTemperature,
|
||||||
|
stream: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this._httpPost(this.endpoint, body, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = JSON.parse(response);
|
||||||
|
|
||||||
|
if (!data.choices || !data.choices[0]) {
|
||||||
|
throw new Error('Qwen API 返回格式异常: 缺少 choices');
|
||||||
|
}
|
||||||
|
|
||||||
|
const latency = Date.now() - startTime;
|
||||||
|
const usage = data.usage || {};
|
||||||
|
|
||||||
|
this.resetFailures();
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: data.model || this.name,
|
||||||
|
output: data.choices[0].message.content,
|
||||||
|
usage: {
|
||||||
|
inputTokens: usage.prompt_tokens || 0,
|
||||||
|
outputTokens: usage.completion_tokens || 0,
|
||||||
|
cost: this._calculateCost(usage.prompt_tokens || 0, usage.completion_tokens || 0),
|
||||||
|
unit: this.costPerToken.currency || 'CNY'
|
||||||
|
},
|
||||||
|
latency,
|
||||||
|
status: 'success',
|
||||||
|
finishReason: data.choices[0].finish_reason
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this.recordFailure(err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async healthCheck() {
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: 'qwen-turbo',
|
||||||
|
messages: [{ role: 'user', content: 'ping' }],
|
||||||
|
max_tokens: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._httpPost(this.endpoint, body, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
});
|
||||||
|
|
||||||
|
this._healthy = true;
|
||||||
|
} catch {
|
||||||
|
this._healthy = false;
|
||||||
|
}
|
||||||
|
this._lastHealthCheck = new Date().toISOString();
|
||||||
|
return this._healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务类型解析具体模型名
|
||||||
|
* @param {string} taskType
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_resolveModel(taskType) {
|
||||||
|
if (taskType === 'code_generation') return 'qwen-coder-plus-latest';
|
||||||
|
return 'qwen-max';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算调用成本(单位:元)
|
||||||
|
* @param {number} inputTokens
|
||||||
|
* @param {number} outputTokens
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
_calculateCost(inputTokens, outputTokens) {
|
||||||
|
const inputCost = (inputTokens / 1_000_000) * this.costPerToken.input;
|
||||||
|
const outputCost = (outputTokens / 1_000_000) * this.costPerToken.output;
|
||||||
|
return Math.round((inputCost + outputCost) * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST 请求
|
||||||
|
* @param {string} url
|
||||||
|
* @param {string} body
|
||||||
|
* @param {object} headers
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
_httpPost(url, body, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const transport = parsed.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const req = transport.request({
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
},
|
||||||
|
timeout: 120000
|
||||||
|
}, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => { data += chunk; });
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
reject(new Error(`Qwen API 错误 (${res.statusCode}): ${data}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', err => reject(new Error(`Qwen 网络错误: ${err.message}`)));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Qwen API 请求超时'));
|
||||||
|
});
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = QwenAdapter;
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
// exe-engine/src/balancer/load-balancer.js
|
||||||
|
// EXE-Engine · 负载均衡器
|
||||||
|
// 根据策略选择最优模型适配器
|
||||||
|
// PRJ-EXE-001 · Phase 0
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 负载均衡器
|
||||||
|
*
|
||||||
|
* 本体论锚定:均衡器 = 选墨水的智慧。
|
||||||
|
* 不同的字需要不同的墨水,均衡器知道哪瓶墨水最合适。
|
||||||
|
*
|
||||||
|
* 策略:
|
||||||
|
* cost — 成本优先:满足质量阈值前提下选最便宜的
|
||||||
|
* quality — 质量优先:选能力最强的模型
|
||||||
|
* balanced — 均衡:成本权重 0.6 + 质量权重 0.4
|
||||||
|
*/
|
||||||
|
class LoadBalancer {
|
||||||
|
/**
|
||||||
|
* @param {Map<string, BaseAdapter>} adapters 已注册的适配器 Map
|
||||||
|
* @param {object} modelConfig 模型配置(含 taskModelMapping)
|
||||||
|
*/
|
||||||
|
constructor(adapters, modelConfig) {
|
||||||
|
this._adapters = adapters;
|
||||||
|
this._modelConfig = modelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据请求选择最优适配器
|
||||||
|
*
|
||||||
|
* @param {object} request
|
||||||
|
* @param {string} request.taskType 任务类型
|
||||||
|
* @param {string} [request.model] 指定模型 ('auto' 则自动选择)
|
||||||
|
* @param {string} [request.priority] 优先策略 (cost | balanced | quality)
|
||||||
|
* @returns {BaseAdapter|null}
|
||||||
|
*/
|
||||||
|
select(request) {
|
||||||
|
const { taskType, model, priority = 'balanced' } = request;
|
||||||
|
|
||||||
|
// 用户指定了具体模型
|
||||||
|
if (model && model !== 'auto') {
|
||||||
|
const adapter = this._adapters.get(model);
|
||||||
|
if (adapter && !adapter.isInCooldown()) {
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
// 指定模型不可用,尝试 fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据任务类型获取候选模型列表
|
||||||
|
const candidates = this._getCandidates(taskType);
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先策略排序
|
||||||
|
return this._selectByPriority(candidates, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 故障转移:获取指定模型的下一个备选
|
||||||
|
*
|
||||||
|
* @param {string} failedModel 失败的模型名
|
||||||
|
* @param {string} taskType 任务类型
|
||||||
|
* @returns {BaseAdapter|null}
|
||||||
|
*/
|
||||||
|
failover(failedModel, taskType) {
|
||||||
|
const candidates = this._getCandidates(taskType)
|
||||||
|
.filter(a => a.name !== failedModel);
|
||||||
|
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有适配器状态
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
const status = [];
|
||||||
|
for (const [, adapter] of this._adapters) {
|
||||||
|
status.push(adapter.getStatus());
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部方法 ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用候选适配器
|
||||||
|
* @param {string} taskType
|
||||||
|
* @returns {BaseAdapter[]}
|
||||||
|
*/
|
||||||
|
_getCandidates(taskType) {
|
||||||
|
const mapping = this._modelConfig.taskModelMapping || {};
|
||||||
|
const modelNames = mapping[taskType] || Object.keys(this._modelConfig.models || {});
|
||||||
|
|
||||||
|
return modelNames
|
||||||
|
.map(name => this._adapters.get(name))
|
||||||
|
.filter(a => a && !a.isInCooldown());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按优先策略选择
|
||||||
|
* @param {BaseAdapter[]} candidates
|
||||||
|
* @param {string} priority
|
||||||
|
* @returns {BaseAdapter}
|
||||||
|
*/
|
||||||
|
_selectByPriority(candidates, priority) {
|
||||||
|
if (priority === 'cost') {
|
||||||
|
return this._selectLowestCost(candidates);
|
||||||
|
}
|
||||||
|
if (priority === 'quality') {
|
||||||
|
// 质量优先 = 列表中第一个(配置中按质量排序)
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
// balanced: 加权评分
|
||||||
|
return this._selectBalanced(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择成本最低的适配器
|
||||||
|
* @param {BaseAdapter[]} candidates
|
||||||
|
* @returns {BaseAdapter}
|
||||||
|
*/
|
||||||
|
_selectLowestCost(candidates) {
|
||||||
|
let best = candidates[0];
|
||||||
|
let bestCost = this._avgCost(best);
|
||||||
|
|
||||||
|
for (let i = 1; i < candidates.length; i++) {
|
||||||
|
const cost = this._avgCost(candidates[i]);
|
||||||
|
if (cost < bestCost) {
|
||||||
|
best = candidates[i];
|
||||||
|
bestCost = cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 均衡选择(成本 0.6 + 位置 0.4)
|
||||||
|
* @param {BaseAdapter[]} candidates
|
||||||
|
* @returns {BaseAdapter}
|
||||||
|
*/
|
||||||
|
_selectBalanced(candidates) {
|
||||||
|
let best = null;
|
||||||
|
let bestScore = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < candidates.length; i++) {
|
||||||
|
const costScore = this._avgCost(candidates[i]);
|
||||||
|
const positionScore = i; // 位置越前,质量越高
|
||||||
|
const score = costScore * 0.6 + positionScore * 0.4;
|
||||||
|
|
||||||
|
if (score < bestScore) {
|
||||||
|
best = candidates[i];
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均每百万 token 成本
|
||||||
|
* @param {BaseAdapter} adapter
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
_avgCost(adapter) {
|
||||||
|
const c = adapter.costPerToken;
|
||||||
|
return ((c.input || 0) + (c.output || 0)) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LoadBalancer;
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
// exe-engine/src/cache/context-cache.js
|
||||||
|
// EXE-Engine · 上下文缓存
|
||||||
|
// 减少重复 prompt 构建的 token 消耗
|
||||||
|
// PRJ-EXE-001 · Phase 0(接口定义,P2 对接 Grid-DB)
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上下文缓存
|
||||||
|
*
|
||||||
|
* 本体论锚定:缓存 = 笔的记忆。
|
||||||
|
* 笔记得刚才写过什么,不需要每次都从头回忆。
|
||||||
|
* Phase 0 使用内存缓存,Phase 2 对接 Grid-DB 格点库。
|
||||||
|
*/
|
||||||
|
class ContextCache {
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [options.maxEntries] 最大缓存条目数
|
||||||
|
* @param {number} [options.ttlMs] 缓存有效期(毫秒)
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
this._maxEntries = options.maxEntries || 100;
|
||||||
|
this._ttlMs = options.ttlMs || 30 * 60 * 1000; // 默认 30 分钟
|
||||||
|
this._cache = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的上下文
|
||||||
|
* @param {string} key 缓存键(通常为 agentId + sessionId)
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
const entry = this._cache.get(key);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
if (Date.now() - entry.timestamp > this._ttlMs) {
|
||||||
|
this._cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.hits++;
|
||||||
|
return entry.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置上下文缓存
|
||||||
|
* @param {string} key 缓存键
|
||||||
|
* @param {object} context 上下文数据
|
||||||
|
*/
|
||||||
|
set(key, context) {
|
||||||
|
// LRU: 超出容量时删除最早的
|
||||||
|
if (this._cache.size >= this._maxEntries) {
|
||||||
|
const oldestKey = this._cache.keys().next().value;
|
||||||
|
this._cache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cache.set(key, {
|
||||||
|
context,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
hits: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除缓存
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
delete(key) {
|
||||||
|
this._cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有缓存
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this._cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存状态
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
let expiredCount = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const [, entry] of this._cache) {
|
||||||
|
if (now - entry.timestamp > this._ttlMs) {
|
||||||
|
expiredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: this._cache.size,
|
||||||
|
maxEntries: this._maxEntries,
|
||||||
|
ttlMs: this._ttlMs,
|
||||||
|
expiredCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ContextCache;
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// exe-engine/src/controller/agent-controller.js
|
||||||
|
// EXE-Engine · Agent 调度器
|
||||||
|
// 将 Agent 执行链中的 AI 调用从 Copilot 切换到自研引擎
|
||||||
|
// PRJ-EXE-001 · Phase 0(基础版,P2 实现双轨切换)
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 调度器
|
||||||
|
*
|
||||||
|
* 本体论锚定:调度器 = 笔的手指。
|
||||||
|
* 不同的笔尖(Agent)需要不同的墨水(模型),
|
||||||
|
* 调度器负责为每个笔尖分配最合适的墨水。
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 维护 Agent → 模型 的映射表
|
||||||
|
* - 支持 Agent 级别的模型偏好配置
|
||||||
|
* - 执行链断点续接(P2 实现)
|
||||||
|
*/
|
||||||
|
class AgentController {
|
||||||
|
constructor() {
|
||||||
|
// Agent → 模型偏好映射
|
||||||
|
this._agentPreferences = new Map();
|
||||||
|
|
||||||
|
// 默认 Agent 偏好
|
||||||
|
this._defaultPreference = {
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'balanced',
|
||||||
|
maxTokens: 4096,
|
||||||
|
mode: 'pool' // byok | pool | hybrid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 Agent 的模型偏好
|
||||||
|
*
|
||||||
|
* @param {string} agentId Agent ID (如 AG-ZY-01)
|
||||||
|
* @param {object} preference 偏好配置
|
||||||
|
* @param {string} [preference.model] 首选模型
|
||||||
|
* @param {string} [preference.priority] 优先策略
|
||||||
|
* @param {number} [preference.maxTokens] 最大 token
|
||||||
|
* @param {string} [preference.mode] 接入模式
|
||||||
|
*/
|
||||||
|
registerAgent(agentId, preference = {}) {
|
||||||
|
this._agentPreferences.set(agentId, {
|
||||||
|
...this._defaultPreference,
|
||||||
|
...preference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 的模型偏好
|
||||||
|
* @param {string} agentId
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
getPreference(agentId) {
|
||||||
|
return this._agentPreferences.get(agentId) || { ...this._defaultPreference };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Agent 请求转换为 EXE 标准请求
|
||||||
|
*
|
||||||
|
* @param {string} agentId Agent ID
|
||||||
|
* @param {object} agentRequest Agent 原始请求
|
||||||
|
* @param {string} agentRequest.prompt
|
||||||
|
* @param {string} [agentRequest.taskType]
|
||||||
|
* @param {object} [agentRequest.context]
|
||||||
|
* @returns {object} EXE 标准请求
|
||||||
|
*/
|
||||||
|
buildExeRequest(agentId, agentRequest) {
|
||||||
|
const pref = this.getPreference(agentId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
taskType: agentRequest.taskType || 'agent_instruction',
|
||||||
|
prompt: agentRequest.prompt,
|
||||||
|
systemPrompt: agentRequest.systemPrompt || null,
|
||||||
|
context: agentRequest.context || {},
|
||||||
|
preferences: {
|
||||||
|
model: agentRequest.model || pref.model,
|
||||||
|
priority: agentRequest.priority || pref.priority,
|
||||||
|
maxTokens: agentRequest.maxTokens || pref.maxTokens
|
||||||
|
},
|
||||||
|
resourcePool: pref.mode === 'byok' ? null : 'default'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已注册 Agent 状态
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
getRegisteredAgents() {
|
||||||
|
const agents = [];
|
||||||
|
for (const [agentId, pref] of this._agentPreferences) {
|
||||||
|
agents.push({ agentId, ...pref });
|
||||||
|
}
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AgentController;
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
// exe-engine/src/index.js
|
||||||
|
// EXE-Engine · 自研执行引擎 · 主入口
|
||||||
|
// PRJ-EXE-001 · Phase 0
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const AGERouter = require('./router/age-router');
|
||||||
|
const LoadBalancer = require('./balancer/load-balancer');
|
||||||
|
const ResourceMeter = require('./meter/resource-meter');
|
||||||
|
const ContextCache = require('./cache/context-cache');
|
||||||
|
const AgentController = require('./controller/agent-controller');
|
||||||
|
const BaseAdapter = require('./adapters/base-adapter');
|
||||||
|
const DeepSeekAdapter = require('./adapters/deepseek-adapter');
|
||||||
|
const QwenAdapter = require('./adapters/qwen-adapter');
|
||||||
|
|
||||||
|
const CONFIG_DIR = path.resolve(__dirname, '../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXE-Engine 初始化器
|
||||||
|
*
|
||||||
|
* 本体论锚定:
|
||||||
|
* AGE OS = 笔
|
||||||
|
* 算力 = 墨水
|
||||||
|
* 用户 = 写字的人
|
||||||
|
*
|
||||||
|
* 初始化流程:
|
||||||
|
* 1. 加载模型配置
|
||||||
|
* 2. 创建适配器实例
|
||||||
|
* 3. 组装负载均衡器
|
||||||
|
* 4. 启动计量器
|
||||||
|
* 5. 启动上下文缓存
|
||||||
|
* 6. 注册默认 Agent
|
||||||
|
* 7. 返回 AGE-Router 实例
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并初始化 EXE-Engine 实例
|
||||||
|
*
|
||||||
|
* @param {object} [overrides] 覆写配置
|
||||||
|
* @param {object} [overrides.keys] API Keys { deepseek: 'sk-...', dashscope: 'sk-...' }
|
||||||
|
* @returns {object} { router, meter, cache, controller, balancer }
|
||||||
|
*/
|
||||||
|
function createEngine(overrides = {}) {
|
||||||
|
// 1. 加载配置
|
||||||
|
const modelConfig = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(CONFIG_DIR, 'models.json'), 'utf-8')
|
||||||
|
);
|
||||||
|
const poolConfig = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(CONFIG_DIR, 'resource-pools.json'), 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. 解析 API Keys(环境变量 > overrides)
|
||||||
|
const keys = {
|
||||||
|
deepseek: process.env.DEEPSEEK_API_KEY || overrides.keys?.deepseek || '',
|
||||||
|
dashscope: process.env.DASHSCOPE_API_KEY || overrides.keys?.dashscope || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 创建适配器
|
||||||
|
const adapters = new Map();
|
||||||
|
|
||||||
|
// DeepSeek-V3
|
||||||
|
const dsV3Config = modelConfig.models['deepseek-v3'];
|
||||||
|
if (dsV3Config) {
|
||||||
|
adapters.set('deepseek-v3', new DeepSeekAdapter({
|
||||||
|
name: 'deepseek-v3',
|
||||||
|
endpoint: dsV3Config.endpoint,
|
||||||
|
apiKey: keys.deepseek,
|
||||||
|
capabilities: dsV3Config.capabilities,
|
||||||
|
costPerToken: dsV3Config.costPerToken,
|
||||||
|
maxContext: dsV3Config.maxContext,
|
||||||
|
defaultTemperature: dsV3Config.defaultTemperature,
|
||||||
|
defaultMaxTokens: dsV3Config.defaultMaxTokens
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSeek-R1
|
||||||
|
const dsR1Config = modelConfig.models['deepseek-r1'];
|
||||||
|
if (dsR1Config) {
|
||||||
|
adapters.set('deepseek-r1', new DeepSeekAdapter({
|
||||||
|
name: 'deepseek-r1',
|
||||||
|
endpoint: dsR1Config.endpoint,
|
||||||
|
apiKey: keys.deepseek,
|
||||||
|
capabilities: dsR1Config.capabilities,
|
||||||
|
costPerToken: dsR1Config.costPerToken,
|
||||||
|
maxContext: dsR1Config.maxContext,
|
||||||
|
defaultTemperature: dsR1Config.defaultTemperature,
|
||||||
|
defaultMaxTokens: dsR1Config.defaultMaxTokens
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen-Max
|
||||||
|
const qwMaxConfig = modelConfig.models['qwen-max'];
|
||||||
|
if (qwMaxConfig) {
|
||||||
|
adapters.set('qwen-max', new QwenAdapter({
|
||||||
|
name: 'qwen-max',
|
||||||
|
endpoint: qwMaxConfig.endpoint,
|
||||||
|
apiKey: keys.dashscope,
|
||||||
|
capabilities: qwMaxConfig.capabilities,
|
||||||
|
costPerToken: qwMaxConfig.costPerToken,
|
||||||
|
maxContext: qwMaxConfig.maxContext,
|
||||||
|
defaultTemperature: qwMaxConfig.defaultTemperature,
|
||||||
|
defaultMaxTokens: qwMaxConfig.defaultMaxTokens
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen-Coder
|
||||||
|
const qwCoderConfig = modelConfig.models['qwen-coder'];
|
||||||
|
if (qwCoderConfig) {
|
||||||
|
adapters.set('qwen-coder', new QwenAdapter({
|
||||||
|
name: 'qwen-coder',
|
||||||
|
endpoint: qwCoderConfig.endpoint,
|
||||||
|
apiKey: keys.dashscope,
|
||||||
|
capabilities: qwCoderConfig.capabilities,
|
||||||
|
costPerToken: qwCoderConfig.costPerToken,
|
||||||
|
maxContext: qwCoderConfig.maxContext,
|
||||||
|
defaultTemperature: qwCoderConfig.defaultTemperature,
|
||||||
|
defaultMaxTokens: qwCoderConfig.defaultMaxTokens
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 组装组件
|
||||||
|
const meter = new ResourceMeter();
|
||||||
|
const cache = new ContextCache({ maxEntries: 100, ttlMs: 30 * 60 * 1000 });
|
||||||
|
const controller = new AgentController();
|
||||||
|
const balancer = new LoadBalancer(adapters, modelConfig);
|
||||||
|
|
||||||
|
// 5. 注册默认 Agent 偏好
|
||||||
|
controller.registerAgent('AG-ZY-01', {
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'balanced',
|
||||||
|
maxTokens: 4096,
|
||||||
|
mode: 'pool'
|
||||||
|
});
|
||||||
|
controller.registerAgent('AG-SY-01', {
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'quality',
|
||||||
|
maxTokens: 4096,
|
||||||
|
mode: 'pool'
|
||||||
|
});
|
||||||
|
controller.registerAgent('AG-QQ-01', {
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'cost',
|
||||||
|
maxTokens: 2048,
|
||||||
|
mode: 'pool'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 组装路由器
|
||||||
|
const router = new AGERouter({
|
||||||
|
loadBalancer: balancer,
|
||||||
|
resourceMeter: meter,
|
||||||
|
contextCache: cache,
|
||||||
|
agentController: controller,
|
||||||
|
poolConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
router,
|
||||||
|
meter,
|
||||||
|
cache,
|
||||||
|
controller,
|
||||||
|
balancer,
|
||||||
|
adapters,
|
||||||
|
config: { modelConfig, poolConfig }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLI 入口 ──
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const cmd = process.argv[2];
|
||||||
|
|
||||||
|
if (cmd === 'status') {
|
||||||
|
const engine = createEngine();
|
||||||
|
const status = engine.router.getStatus();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(JSON.stringify(status, null, 2));
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log([
|
||||||
|
'🔧 EXE-Engine v1.0.0 · PRJ-EXE-001 · Phase 0',
|
||||||
|
'',
|
||||||
|
'本体论锚定:AGE OS = 笔 · 算力 = 墨水 · 用户 = 写字的人',
|
||||||
|
'',
|
||||||
|
'用法:',
|
||||||
|
' node exe-engine/src/index.js status 查看引擎状态',
|
||||||
|
'',
|
||||||
|
'组件:',
|
||||||
|
' AGE-Router 路由网关',
|
||||||
|
' LoadBalancer 负载均衡器',
|
||||||
|
' ResourceMeter 资源计量器',
|
||||||
|
' ContextCache 上下文缓存',
|
||||||
|
' AgentController Agent 调度器',
|
||||||
|
' DeepSeekAdapter DeepSeek 适配器',
|
||||||
|
' QwenAdapter Qwen 适配器',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createEngine,
|
||||||
|
AGERouter,
|
||||||
|
LoadBalancer,
|
||||||
|
ResourceMeter,
|
||||||
|
ContextCache,
|
||||||
|
AgentController,
|
||||||
|
BaseAdapter,
|
||||||
|
DeepSeekAdapter,
|
||||||
|
QwenAdapter
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
// exe-engine/src/meter/resource-meter.js
|
||||||
|
// EXE-Engine · 资源计量器
|
||||||
|
// 实时记录每次调用的 token 消耗、模型选择、响应时间
|
||||||
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-003
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const LOG_DIR = path.resolve(__dirname, '../../logs');
|
||||||
|
const METER_LOG = path.join(LOG_DIR, 'meter.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源计量器
|
||||||
|
*
|
||||||
|
* 本体论锚定:计量器 = 量墨水的刻度。
|
||||||
|
* 每一笔用了多少墨水,花了多少钱,都要记下来。
|
||||||
|
* 数据回流至 DC v1.0 采集系统,为算力资源模型提供计费基础数据。
|
||||||
|
*/
|
||||||
|
class ResourceMeter {
|
||||||
|
constructor() {
|
||||||
|
this._records = [];
|
||||||
|
this._summary = {
|
||||||
|
totalRequests: 0,
|
||||||
|
totalInputTokens: 0,
|
||||||
|
totalOutputTokens: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
byModel: {},
|
||||||
|
byAgent: {},
|
||||||
|
byPool: {}
|
||||||
|
};
|
||||||
|
this._ensureLogDir();
|
||||||
|
this._loadExisting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录一次调用消耗
|
||||||
|
*
|
||||||
|
* @param {object} record
|
||||||
|
* @param {string} record.requestId 请求 ID
|
||||||
|
* @param {string} record.agentId Agent ID
|
||||||
|
* @param {string} record.model 使用的模型
|
||||||
|
* @param {string} record.taskType 任务类型
|
||||||
|
* @param {string} record.resourcePool 资源池 ID
|
||||||
|
* @param {string} record.mode 接入模式 (byok | pool | hybrid)
|
||||||
|
* @param {number} record.inputTokens 输入 token 数
|
||||||
|
* @param {number} record.outputTokens 输出 token 数
|
||||||
|
* @param {number} record.cost 成本
|
||||||
|
* @param {string} record.costUnit 成本单位
|
||||||
|
* @param {number} record.latency 延迟 (ms)
|
||||||
|
* @param {string} record.status 状态 (success | error)
|
||||||
|
*/
|
||||||
|
record(record) {
|
||||||
|
const entry = {
|
||||||
|
...record,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
this._records.push(entry);
|
||||||
|
this._updateSummary(entry);
|
||||||
|
this._persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计量摘要
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
getSummary() {
|
||||||
|
return {
|
||||||
|
...this._summary,
|
||||||
|
recordCount: this._records.length,
|
||||||
|
lastUpdated: this._records.length > 0
|
||||||
|
? this._records[this._records.length - 1].timestamp
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 Agent 的消耗统计
|
||||||
|
* @param {string} agentId
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
getAgentUsage(agentId) {
|
||||||
|
return this._summary.byAgent[agentId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定资源池的消耗统计
|
||||||
|
* @param {string} poolId
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
getPoolUsage(poolId) {
|
||||||
|
return this._summary.byPool[poolId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近 N 条记录
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {object[]}
|
||||||
|
*/
|
||||||
|
getRecentRecords(n = 20) {
|
||||||
|
return this._records.slice(-n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期记录(保留最近 500 条)
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
if (this._records.length > 500) {
|
||||||
|
this._records = this._records.slice(-500);
|
||||||
|
this._rebuildSummary();
|
||||||
|
this._persist();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部方法 ──
|
||||||
|
|
||||||
|
_updateSummary(entry) {
|
||||||
|
this._summary.totalRequests++;
|
||||||
|
this._summary.totalInputTokens += entry.inputTokens || 0;
|
||||||
|
this._summary.totalOutputTokens += entry.outputTokens || 0;
|
||||||
|
this._summary.totalCost += entry.cost || 0;
|
||||||
|
|
||||||
|
// 按模型统计
|
||||||
|
if (entry.model) {
|
||||||
|
if (!this._summary.byModel[entry.model]) {
|
||||||
|
this._summary.byModel[entry.model] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
||||||
|
}
|
||||||
|
const m = this._summary.byModel[entry.model];
|
||||||
|
m.requests++;
|
||||||
|
m.inputTokens += entry.inputTokens || 0;
|
||||||
|
m.outputTokens += entry.outputTokens || 0;
|
||||||
|
m.cost += entry.cost || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 Agent 统计
|
||||||
|
if (entry.agentId) {
|
||||||
|
if (!this._summary.byAgent[entry.agentId]) {
|
||||||
|
this._summary.byAgent[entry.agentId] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
||||||
|
}
|
||||||
|
const a = this._summary.byAgent[entry.agentId];
|
||||||
|
a.requests++;
|
||||||
|
a.inputTokens += entry.inputTokens || 0;
|
||||||
|
a.outputTokens += entry.outputTokens || 0;
|
||||||
|
a.cost += entry.cost || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按资源池统计
|
||||||
|
if (entry.resourcePool) {
|
||||||
|
if (!this._summary.byPool[entry.resourcePool]) {
|
||||||
|
this._summary.byPool[entry.resourcePool] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
||||||
|
}
|
||||||
|
const p = this._summary.byPool[entry.resourcePool];
|
||||||
|
p.requests++;
|
||||||
|
p.inputTokens += entry.inputTokens || 0;
|
||||||
|
p.outputTokens += entry.outputTokens || 0;
|
||||||
|
p.cost += entry.cost || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebuildSummary() {
|
||||||
|
this._summary = {
|
||||||
|
totalRequests: 0,
|
||||||
|
totalInputTokens: 0,
|
||||||
|
totalOutputTokens: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
byModel: {},
|
||||||
|
byAgent: {},
|
||||||
|
byPool: {}
|
||||||
|
};
|
||||||
|
for (const entry of this._records) {
|
||||||
|
this._updateSummary(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureLogDir() {
|
||||||
|
if (!fs.existsSync(LOG_DIR)) {
|
||||||
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_persist() {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(METER_LOG, JSON.stringify({
|
||||||
|
records: this._records.slice(-500),
|
||||||
|
summary: this._summary
|
||||||
|
}, null, 2));
|
||||||
|
} catch {
|
||||||
|
// 写入失败不阻断主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadExisting() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(METER_LOG)) {
|
||||||
|
const data = JSON.parse(fs.readFileSync(METER_LOG, 'utf-8'));
|
||||||
|
this._records = data.records || [];
|
||||||
|
if (data.summary) {
|
||||||
|
this._summary = data.summary;
|
||||||
|
} else {
|
||||||
|
this._rebuildSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 加载失败则从空开始
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ResourceMeter;
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
// exe-engine/src/router/age-router.js
|
||||||
|
// EXE-Engine · AGE-Router 路由网关
|
||||||
|
// 接收所有 AI 执行请求,进行鉴权、分类、路由
|
||||||
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-001
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { randomUUID } = require('crypto');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AGE-Router 路由网关
|
||||||
|
*
|
||||||
|
* 本体论锚定:路由网关 = 笔的分流阀。
|
||||||
|
* 所有墨水需求都从这里进入,网关决定用哪种墨水、多少量。
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 请求鉴权(验证用户身份 + 资源池额度)
|
||||||
|
* 2. 任务分类(代码生成 / 文本处理 / 数据分析 / Agent 指令)
|
||||||
|
* 3. 模型选择(通过 LoadBalancer 选择最优模型)
|
||||||
|
* 4. 限流降级(资源池耗尽时的优雅降级策略)
|
||||||
|
*/
|
||||||
|
class AGERouter {
|
||||||
|
/**
|
||||||
|
* @param {object} deps
|
||||||
|
* @param {LoadBalancer} deps.loadBalancer 负载均衡器
|
||||||
|
* @param {ResourceMeter} deps.resourceMeter 资源计量器
|
||||||
|
* @param {ContextCache} deps.contextCache 上下文缓存
|
||||||
|
* @param {AgentController} deps.agentController Agent 调度器
|
||||||
|
* @param {object} [deps.poolConfig] 资源池配置
|
||||||
|
*/
|
||||||
|
constructor(deps) {
|
||||||
|
this._loadBalancer = deps.loadBalancer;
|
||||||
|
this._resourceMeter = deps.resourceMeter;
|
||||||
|
this._contextCache = deps.contextCache;
|
||||||
|
this._agentController = deps.agentController;
|
||||||
|
this._poolConfig = deps.poolConfig || {};
|
||||||
|
|
||||||
|
// 限流计数器 { agentId: { count, resetAt } }
|
||||||
|
this._rateLimits = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 AI 请求(统一入口)
|
||||||
|
*
|
||||||
|
* 对应 API: POST /api/exe/v1/execute
|
||||||
|
*
|
||||||
|
* @param {object} request
|
||||||
|
* @param {string} request.agentId Agent ID
|
||||||
|
* @param {string} request.taskType 任务类型
|
||||||
|
* @param {string} request.prompt Prompt
|
||||||
|
* @param {string} [request.systemPrompt] 系统 Prompt
|
||||||
|
* @param {object} [request.context] 上下文
|
||||||
|
* @param {object} [request.preferences] 偏好 { model, priority, maxTokens }
|
||||||
|
* @param {string} [request.resourcePool] 资源池 ID
|
||||||
|
* @param {string} [request.apiKey] BYOK 模式的用户 Key
|
||||||
|
* @returns {Promise<object>} EXE 标准响应
|
||||||
|
*/
|
||||||
|
async execute(request) {
|
||||||
|
const requestId = `exe-${randomUUID().slice(0, 12)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 限流检查
|
||||||
|
this._checkRateLimit(request.agentId, request.resourcePool);
|
||||||
|
|
||||||
|
// 2. 尝试加载缓存上下文
|
||||||
|
const cacheKey = `${request.agentId}:${request.taskType}`;
|
||||||
|
const cachedContext = this._contextCache.get(cacheKey);
|
||||||
|
if (cachedContext && !request.context) {
|
||||||
|
request.context = cachedContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 通过 LoadBalancer 选择适配器
|
||||||
|
const adapter = this._loadBalancer.select({
|
||||||
|
taskType: request.taskType,
|
||||||
|
model: request.preferences?.model || 'auto',
|
||||||
|
priority: request.preferences?.priority || 'balanced'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adapter) {
|
||||||
|
return this._buildErrorResponse(requestId, startTime, 'NO_AVAILABLE_MODEL',
|
||||||
|
'没有可用的模型适配器,所有模型可能处于冷却期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 执行推理
|
||||||
|
const result = await adapter.execute({
|
||||||
|
prompt: request.prompt,
|
||||||
|
systemPrompt: request.systemPrompt,
|
||||||
|
taskType: request.taskType,
|
||||||
|
maxTokens: request.preferences?.maxTokens || 4096,
|
||||||
|
temperature: request.preferences?.temperature
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 记录计量数据
|
||||||
|
const mode = request.apiKey ? 'byok' : 'pool';
|
||||||
|
this._resourceMeter.record({
|
||||||
|
requestId,
|
||||||
|
agentId: request.agentId,
|
||||||
|
model: result.model,
|
||||||
|
taskType: request.taskType,
|
||||||
|
resourcePool: request.resourcePool || 'default',
|
||||||
|
mode,
|
||||||
|
inputTokens: result.usage.inputTokens,
|
||||||
|
outputTokens: result.usage.outputTokens,
|
||||||
|
cost: result.usage.cost,
|
||||||
|
costUnit: result.usage.unit,
|
||||||
|
latency: result.latency,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 更新上下文缓存
|
||||||
|
if (request.context) {
|
||||||
|
this._contextCache.set(cacheKey, request.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 返回标准响应
|
||||||
|
return {
|
||||||
|
requestId,
|
||||||
|
model: result.model,
|
||||||
|
output: result.output,
|
||||||
|
usage: result.usage,
|
||||||
|
latency: result.latency,
|
||||||
|
status: 'success'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// 尝试故障转移
|
||||||
|
const failoverResult = await this._tryFailover(request, requestId, startTime, err);
|
||||||
|
if (failoverResult) return failoverResult;
|
||||||
|
|
||||||
|
// 记录失败
|
||||||
|
this._resourceMeter.record({
|
||||||
|
requestId,
|
||||||
|
agentId: request.agentId,
|
||||||
|
model: 'unknown',
|
||||||
|
taskType: request.taskType,
|
||||||
|
resourcePool: request.resourcePool || 'default',
|
||||||
|
mode: request.apiKey ? 'byok' : 'pool',
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0,
|
||||||
|
cost: 0,
|
||||||
|
costUnit: 'CNY',
|
||||||
|
latency: Date.now() - startTime,
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._buildErrorResponse(requestId, startTime, 'EXECUTION_FAILED', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统状态
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
router: 'AGE-Router v1.0.0',
|
||||||
|
project: 'PRJ-EXE-001',
|
||||||
|
phase: 'Phase 0',
|
||||||
|
adapters: this._loadBalancer.getStatus(),
|
||||||
|
meter: this._resourceMeter.getSummary(),
|
||||||
|
cache: this._contextCache.getStatus(),
|
||||||
|
agents: this._agentController.getRegisteredAgents()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 内部方法 ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流检查
|
||||||
|
* @param {string} agentId
|
||||||
|
* @param {string} poolId
|
||||||
|
*/
|
||||||
|
_checkRateLimit(agentId, poolId) {
|
||||||
|
const key = agentId || 'anonymous';
|
||||||
|
const now = Date.now();
|
||||||
|
let limiter = this._rateLimits.get(key);
|
||||||
|
|
||||||
|
if (!limiter || now >= limiter.resetAt) {
|
||||||
|
limiter = { count: 0, resetAt: now + 60000 }; // 每分钟窗口
|
||||||
|
this._rateLimits.set(key, limiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter.count++;
|
||||||
|
|
||||||
|
// 获取资源池的速率限制
|
||||||
|
const pool = this._poolConfig.pools?.[poolId || 'default'];
|
||||||
|
const limit = pool?.rateLimitPerMinute || 30;
|
||||||
|
|
||||||
|
if (limiter.count > limit) {
|
||||||
|
throw new Error(`[限流] Agent ${key} 每分钟请求数超限 (${limiter.count}/${limit})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 故障转移
|
||||||
|
* @param {object} request
|
||||||
|
* @param {string} requestId
|
||||||
|
* @param {number} startTime
|
||||||
|
* @param {Error} originalError
|
||||||
|
* @returns {Promise<object|null>}
|
||||||
|
*/
|
||||||
|
async _tryFailover(request, requestId, startTime, originalError) {
|
||||||
|
try {
|
||||||
|
const failoverAdapter = this._loadBalancer.failover(
|
||||||
|
request.preferences?.model || 'unknown',
|
||||||
|
request.taskType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!failoverAdapter) return null;
|
||||||
|
|
||||||
|
const result = await failoverAdapter.execute({
|
||||||
|
prompt: request.prompt,
|
||||||
|
systemPrompt: request.systemPrompt,
|
||||||
|
taskType: request.taskType,
|
||||||
|
maxTokens: request.preferences?.maxTokens || 4096
|
||||||
|
});
|
||||||
|
|
||||||
|
this._resourceMeter.record({
|
||||||
|
requestId,
|
||||||
|
agentId: request.agentId,
|
||||||
|
model: result.model,
|
||||||
|
taskType: request.taskType,
|
||||||
|
resourcePool: request.resourcePool || 'default',
|
||||||
|
mode: request.apiKey ? 'byok' : 'pool',
|
||||||
|
inputTokens: result.usage.inputTokens,
|
||||||
|
outputTokens: result.usage.outputTokens,
|
||||||
|
cost: result.usage.cost,
|
||||||
|
costUnit: result.usage.unit,
|
||||||
|
latency: result.latency,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId,
|
||||||
|
model: result.model,
|
||||||
|
output: result.output,
|
||||||
|
usage: result.usage,
|
||||||
|
latency: result.latency,
|
||||||
|
status: 'success',
|
||||||
|
_failover: true,
|
||||||
|
_originalError: originalError.message
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建错误响应
|
||||||
|
* @param {string} requestId
|
||||||
|
* @param {number} startTime
|
||||||
|
* @param {string} code
|
||||||
|
* @param {string} message
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
_buildErrorResponse(requestId, startTime, code, message) {
|
||||||
|
return {
|
||||||
|
requestId,
|
||||||
|
model: null,
|
||||||
|
output: null,
|
||||||
|
usage: { inputTokens: 0, outputTokens: 0, cost: 0, unit: 'CNY' },
|
||||||
|
latency: Date.now() - startTime,
|
||||||
|
status: 'error',
|
||||||
|
error: { code, message }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AGERouter;
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
// exe-engine/tests/smoke/exe-engine.test.js
|
||||||
|
// EXE-Engine · 冒烟测试
|
||||||
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-004
|
||||||
|
// 版权:国作登字-2026-A-00037559
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 直接引入模块(不走 npm install)
|
||||||
|
const {
|
||||||
|
createEngine,
|
||||||
|
AGERouter,
|
||||||
|
LoadBalancer,
|
||||||
|
ResourceMeter,
|
||||||
|
ContextCache,
|
||||||
|
AgentController,
|
||||||
|
BaseAdapter,
|
||||||
|
DeepSeekAdapter,
|
||||||
|
QwenAdapter
|
||||||
|
} = require('../../src/index');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (condition) {
|
||||||
|
passed++;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(` ✅ ${message}`);
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(` ❌ ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('🔧 EXE-Engine 冒烟测试 · Phase 0\n');
|
||||||
|
|
||||||
|
// ── 测试 1: 模块导出 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('── 测试 1: 模块导出完整性 ──');
|
||||||
|
assert(typeof createEngine === 'function', 'createEngine 是函数');
|
||||||
|
assert(typeof AGERouter === 'function', 'AGERouter 是构造函数');
|
||||||
|
assert(typeof LoadBalancer === 'function', 'LoadBalancer 是构造函数');
|
||||||
|
assert(typeof ResourceMeter === 'function', 'ResourceMeter 是构造函数');
|
||||||
|
assert(typeof ContextCache === 'function', 'ContextCache 是构造函数');
|
||||||
|
assert(typeof AgentController === 'function', 'AgentController 是构造函数');
|
||||||
|
assert(typeof BaseAdapter === 'function', 'BaseAdapter 是构造函数');
|
||||||
|
assert(typeof DeepSeekAdapter === 'function', 'DeepSeekAdapter 是构造函数');
|
||||||
|
assert(typeof QwenAdapter === 'function', 'QwenAdapter 是构造函数');
|
||||||
|
|
||||||
|
// ── 测试 2: createEngine 初始化 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 2: createEngine 初始化 ──');
|
||||||
|
const engine = createEngine();
|
||||||
|
assert(engine.router instanceof AGERouter, 'router 是 AGERouter 实例');
|
||||||
|
assert(engine.meter instanceof ResourceMeter, 'meter 是 ResourceMeter 实例');
|
||||||
|
assert(engine.cache instanceof ContextCache, 'cache 是 ContextCache 实例');
|
||||||
|
assert(engine.controller instanceof AgentController, 'controller 是 AgentController 实例');
|
||||||
|
assert(engine.balancer instanceof LoadBalancer, 'balancer 是 LoadBalancer 实例');
|
||||||
|
assert(engine.adapters instanceof Map, 'adapters 是 Map 实例');
|
||||||
|
assert(engine.adapters.size === 4, `4 个适配器已注册 (实际: ${engine.adapters.size})`);
|
||||||
|
|
||||||
|
// ── 测试 3: 适配器注册 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 3: 适配器注册 ──');
|
||||||
|
assert(engine.adapters.has('deepseek-v3'), 'deepseek-v3 适配器已注册');
|
||||||
|
assert(engine.adapters.has('deepseek-r1'), 'deepseek-r1 适配器已注册');
|
||||||
|
assert(engine.adapters.has('qwen-max'), 'qwen-max 适配器已注册');
|
||||||
|
assert(engine.adapters.has('qwen-coder'), 'qwen-coder 适配器已注册');
|
||||||
|
|
||||||
|
const dsAdapter = engine.adapters.get('deepseek-v3');
|
||||||
|
assert(dsAdapter instanceof DeepSeekAdapter, 'deepseek-v3 是 DeepSeekAdapter 实例');
|
||||||
|
assert(dsAdapter.supports('code'), 'deepseek-v3 支持 code 能力');
|
||||||
|
assert(dsAdapter.supports('reasoning'), 'deepseek-v3 支持 reasoning 能力');
|
||||||
|
|
||||||
|
const qwAdapter = engine.adapters.get('qwen-max');
|
||||||
|
assert(qwAdapter instanceof QwenAdapter, 'qwen-max 是 QwenAdapter 实例');
|
||||||
|
assert(qwAdapter.supports('text'), 'qwen-max 支持 text 能力');
|
||||||
|
|
||||||
|
// ── 测试 4: BaseAdapter 不可直接实例化 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 4: BaseAdapter 约束 ──');
|
||||||
|
let baseError = null;
|
||||||
|
try {
|
||||||
|
new BaseAdapter({ name: 'test' });
|
||||||
|
} catch (e) {
|
||||||
|
baseError = e;
|
||||||
|
}
|
||||||
|
assert(baseError !== null, 'BaseAdapter 不可直接实例化');
|
||||||
|
assert(baseError && baseError.message.includes('不可直接实例化'), '错误信息正确');
|
||||||
|
|
||||||
|
// ── 测试 5: 适配器状态 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 5: 适配器状态 ──');
|
||||||
|
const dsStatus = dsAdapter.getStatus();
|
||||||
|
assert(dsStatus.name === 'deepseek-v3', '状态名称正确');
|
||||||
|
assert(dsStatus.healthy === true, '初始状态健康');
|
||||||
|
assert(dsStatus.inCooldown === false, '初始无冷却');
|
||||||
|
assert(dsStatus.failureCount === 0, '初始无失败');
|
||||||
|
|
||||||
|
// ── 测试 6: 冷却期机制 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 6: 冷却期机制 ──');
|
||||||
|
const testAdapter = engine.adapters.get('deepseek-r1');
|
||||||
|
testAdapter.recordFailure('test1');
|
||||||
|
testAdapter.recordFailure('test2');
|
||||||
|
assert(testAdapter.isInCooldown() === false, '2 次失败不触发冷却');
|
||||||
|
testAdapter.recordFailure('test3');
|
||||||
|
assert(testAdapter.isInCooldown() === true, '3 次失败触发冷却');
|
||||||
|
assert(testAdapter.getStatus().healthy === false, '冷却期标记不健康');
|
||||||
|
testAdapter.resetFailures();
|
||||||
|
assert(testAdapter.isInCooldown() === false, '重置后退出冷却');
|
||||||
|
assert(testAdapter.getStatus().healthy === true, '重置后恢复健康');
|
||||||
|
|
||||||
|
// ── 测试 7: LoadBalancer 选择 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 7: LoadBalancer 选择 ──');
|
||||||
|
const selected = engine.balancer.select({
|
||||||
|
taskType: 'code_generation',
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'balanced'
|
||||||
|
});
|
||||||
|
assert(selected !== null, 'balanced 策略返回适配器');
|
||||||
|
assert(selected.name === 'deepseek-v3', 'code_generation 首选 deepseek-v3');
|
||||||
|
|
||||||
|
const costSelected = engine.balancer.select({
|
||||||
|
taskType: 'code_generation',
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'cost'
|
||||||
|
});
|
||||||
|
assert(costSelected !== null, 'cost 策略返回适配器');
|
||||||
|
|
||||||
|
const qualitySelected = engine.balancer.select({
|
||||||
|
taskType: 'reasoning',
|
||||||
|
model: 'auto',
|
||||||
|
priority: 'quality'
|
||||||
|
});
|
||||||
|
assert(qualitySelected !== null, 'quality 策略返回适配器');
|
||||||
|
assert(qualitySelected.name === 'deepseek-r1', 'reasoning 首选 deepseek-r1');
|
||||||
|
|
||||||
|
// 指定模型
|
||||||
|
const specificSelected = engine.balancer.select({
|
||||||
|
taskType: 'text_processing',
|
||||||
|
model: 'qwen-max',
|
||||||
|
priority: 'balanced'
|
||||||
|
});
|
||||||
|
assert(specificSelected !== null, '指定模型返回适配器');
|
||||||
|
assert(specificSelected.name === 'qwen-max', '返回指定的 qwen-max');
|
||||||
|
|
||||||
|
// 故障转移
|
||||||
|
const failover = engine.balancer.failover('deepseek-v3', 'code_generation');
|
||||||
|
assert(failover !== null, '故障转移返回备选');
|
||||||
|
assert(failover.name !== 'deepseek-v3', '备选不等于失败模型');
|
||||||
|
|
||||||
|
// ── 测试 8: ResourceMeter 计量 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 8: ResourceMeter 计量 ──');
|
||||||
|
engine.meter.record({
|
||||||
|
requestId: 'test-001',
|
||||||
|
agentId: 'AG-ZY-01',
|
||||||
|
model: 'deepseek-v3',
|
||||||
|
taskType: 'code_generation',
|
||||||
|
resourcePool: 'default',
|
||||||
|
mode: 'pool',
|
||||||
|
inputTokens: 1000,
|
||||||
|
outputTokens: 500,
|
||||||
|
cost: 0.002,
|
||||||
|
costUnit: 'CNY',
|
||||||
|
latency: 1500,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = engine.meter.getSummary();
|
||||||
|
assert(summary.totalRequests === 1, '总请求数 = 1');
|
||||||
|
assert(summary.totalInputTokens === 1000, '总输入 token = 1000');
|
||||||
|
assert(summary.totalOutputTokens === 500, '总输出 token = 500');
|
||||||
|
assert(summary.byModel['deepseek-v3'].requests === 1, '按模型统计正确');
|
||||||
|
assert(summary.byAgent['AG-ZY-01'].requests === 1, '按 Agent 统计正确');
|
||||||
|
|
||||||
|
const agentUsage = engine.meter.getAgentUsage('AG-ZY-01');
|
||||||
|
assert(agentUsage !== null, 'Agent 使用记录存在');
|
||||||
|
assert(agentUsage.inputTokens === 1000, 'Agent 输入 token 正确');
|
||||||
|
|
||||||
|
const recent = engine.meter.getRecentRecords(10);
|
||||||
|
assert(recent.length === 1, '最近记录数正确');
|
||||||
|
|
||||||
|
// ── 测试 9: ContextCache ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 9: ContextCache 缓存 ──');
|
||||||
|
engine.cache.set('test-key', { repo: 'guanghulab', branch: 'main' });
|
||||||
|
const cached = engine.cache.get('test-key');
|
||||||
|
assert(cached !== null, '缓存命中');
|
||||||
|
assert(cached.repo === 'guanghulab', '缓存内容正确');
|
||||||
|
|
||||||
|
const missed = engine.cache.get('nonexistent');
|
||||||
|
assert(missed === null, '缓存未命中返回 null');
|
||||||
|
|
||||||
|
const cacheStatus = engine.cache.getStatus();
|
||||||
|
assert(cacheStatus.size === 1, '缓存大小 = 1');
|
||||||
|
assert(cacheStatus.maxEntries === 100, '最大容量 = 100');
|
||||||
|
|
||||||
|
engine.cache.delete('test-key');
|
||||||
|
assert(engine.cache.get('test-key') === null, '删除后缓存未命中');
|
||||||
|
|
||||||
|
// ── 测试 10: AgentController ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 10: AgentController 调度 ──');
|
||||||
|
const zyPref = engine.controller.getPreference('AG-ZY-01');
|
||||||
|
assert(zyPref.model === 'auto', '铸渊偏好 model=auto');
|
||||||
|
assert(zyPref.priority === 'balanced', '铸渊偏好 priority=balanced');
|
||||||
|
|
||||||
|
const syPref = engine.controller.getPreference('AG-SY-01');
|
||||||
|
assert(syPref.priority === 'quality', '霜砚偏好 priority=quality');
|
||||||
|
|
||||||
|
const qqPref = engine.controller.getPreference('AG-QQ-01');
|
||||||
|
assert(qqPref.priority === 'cost', '秋秋偏好 priority=cost');
|
||||||
|
|
||||||
|
const unknownPref = engine.controller.getPreference('AG-UNKNOWN');
|
||||||
|
assert(unknownPref.model === 'auto', '未注册 Agent 返回默认偏好');
|
||||||
|
|
||||||
|
const exeRequest = engine.controller.buildExeRequest('AG-ZY-01', {
|
||||||
|
prompt: 'Hello World',
|
||||||
|
taskType: 'code_generation'
|
||||||
|
});
|
||||||
|
assert(exeRequest.agentId === 'AG-ZY-01', 'EXE 请求 agentId 正确');
|
||||||
|
assert(exeRequest.taskType === 'code_generation', 'EXE 请求 taskType 正确');
|
||||||
|
assert(exeRequest.prompt === 'Hello World', 'EXE 请求 prompt 正确');
|
||||||
|
assert(exeRequest.resourcePool === 'default', 'EXE 请求 resourcePool 正确');
|
||||||
|
|
||||||
|
const agents = engine.controller.getRegisteredAgents();
|
||||||
|
assert(agents.length === 3, '3 个 Agent 已注册');
|
||||||
|
|
||||||
|
// ── 测试 11: AGE-Router 状态 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 11: AGE-Router 状态 ──');
|
||||||
|
const routerStatus = engine.router.getStatus();
|
||||||
|
assert(routerStatus.router === 'AGE-Router v1.0.0', 'Router 版本正确');
|
||||||
|
assert(routerStatus.project === 'PRJ-EXE-001', '项目编号正确');
|
||||||
|
assert(routerStatus.phase === 'Phase 0', '阶段正确');
|
||||||
|
assert(Array.isArray(routerStatus.adapters), '适配器状态是数组');
|
||||||
|
assert(routerStatus.adapters.length === 4, '4 个适配器状态');
|
||||||
|
|
||||||
|
// ── 测试 12: AGE-Router 错误处理(无 API Key) ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n── 测试 12: AGE-Router 错误处理 ──');
|
||||||
|
(async () => {
|
||||||
|
const result = await engine.router.execute({
|
||||||
|
agentId: 'AG-ZY-01',
|
||||||
|
taskType: 'code_generation',
|
||||||
|
prompt: 'test prompt',
|
||||||
|
preferences: { model: 'auto', priority: 'balanced' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 无 API Key 时应该返回错误(网络错误或认证错误)
|
||||||
|
assert(result.requestId && result.requestId.startsWith('exe-'), '返回带 exe- 前缀的 requestId');
|
||||||
|
assert(typeof result.latency === 'number', '返回 latency');
|
||||||
|
assert(result.status === 'error' || result.status === 'success', '返回有效 status');
|
||||||
|
|
||||||
|
// ── 测试结果汇总 ──
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('\n══════════════════════════════════════');
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`🔧 EXE-Engine 冒烟测试完成`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(` ✅ 通过: ${passed}`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(` ❌ 失败: ${failed}`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(` 📊 总计: ${passed + failed}`);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('══════════════════════════════════════\n');
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue