From f03adc5133738b32ec421bdbfb0f840df8b35c45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 05:20:41 +0000 Subject: [PATCH] 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 --- .github/persona-brain/memory.json | 8 + .github/persona-brain/ontology.json | 45 +++ .gitignore | 3 + docs/execution-layer-map.md | 16 + exe-engine/README.md | 142 +++++++++ exe-engine/config/models.json | 57 ++++ exe-engine/config/resource-pools.json | 26 ++ exe-engine/docs/architecture.md | 208 +++++++++++++ exe-engine/package.json | 16 + exe-engine/src/adapters/base-adapter.js | 133 +++++++++ exe-engine/src/adapters/deepseek-adapter.js | 187 ++++++++++++ exe-engine/src/adapters/qwen-adapter.js | 187 ++++++++++++ exe-engine/src/balancer/load-balancer.js | 175 +++++++++++ exe-engine/src/cache/context-cache.js | 103 +++++++ exe-engine/src/controller/agent-controller.js | 102 +++++++ exe-engine/src/index.js | 213 +++++++++++++ exe-engine/src/meter/resource-meter.js | 212 +++++++++++++ exe-engine/src/router/age-router.js | 271 +++++++++++++++++ exe-engine/tests/smoke/exe-engine.test.js | 279 ++++++++++++++++++ 19 files changed, 2383 insertions(+) create mode 100644 exe-engine/README.md create mode 100644 exe-engine/config/models.json create mode 100644 exe-engine/config/resource-pools.json create mode 100644 exe-engine/docs/architecture.md create mode 100644 exe-engine/package.json create mode 100644 exe-engine/src/adapters/base-adapter.js create mode 100644 exe-engine/src/adapters/deepseek-adapter.js create mode 100644 exe-engine/src/adapters/qwen-adapter.js create mode 100644 exe-engine/src/balancer/load-balancer.js create mode 100644 exe-engine/src/cache/context-cache.js create mode 100644 exe-engine/src/controller/agent-controller.js create mode 100644 exe-engine/src/index.js create mode 100644 exe-engine/src/meter/resource-meter.js create mode 100644 exe-engine/src/router/age-router.js create mode 100644 exe-engine/tests/smoke/exe-engine.test.js diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 965c1691..b57057aa 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -10,6 +10,14 @@ "last_synced": "2026-03-19T02:00:00+08:00" }, "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", "type": "gate_guard", diff --git a/.github/persona-brain/ontology.json b/.github/persona-brain/ontology.json index d2c6c1a3..b9dfa1d1 100644 --- a/.github/persona-brain/ontology.json +++ b/.github/persona-brain/ontology.json @@ -67,5 +67,50 @@ "skyeye": "生态系统的集体意识", "checkin": "每日呼吸", "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": "用户在用一支很聪明的笔。墨水(算力)是需要关心的资源,但笔会帮优化用量。" + } + } } } diff --git a/.gitignore b/.gitignore index 9da4ce46..6b562956 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ backend/api-server/logs/ # Writing Platform build artifacts writing-platform/frontend/dist/ writing-platform/backend/dist/ + +# EXE-Engine runtime logs +exe-engine/logs/ diff --git a/docs/execution-layer-map.md b/docs/execution-layer-map.md index c0f2c8f2..f1158b5c 100644 --- a/docs/execution-layer-map.md +++ b/docs/execution-layer-map.md @@ -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→模型偏好映射 | diff --git a/exe-engine/README.md b/exe-engine/README.md new file mode 100644 index 00000000..4c7c88e9 --- /dev/null +++ b/exe-engine/README.md @@ -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 | ⚪ 待开始 | diff --git a/exe-engine/config/models.json b/exe-engine/config/models.json new file mode 100644 index 00000000..e33f229b --- /dev/null +++ b/exe-engine/config/models.json @@ -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 +} diff --git a/exe-engine/config/resource-pools.json b/exe-engine/config/resource-pools.json new file mode 100644 index 00000000..74ff366f --- /dev/null +++ b/exe-engine/config/resource-pools.json @@ -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 + } +} diff --git a/exe-engine/docs/architecture.md b/exe-engine/docs/architecture.md new file mode 100644 index 00000000..b8fce46b --- /dev/null +++ b/exe-engine/docs/architecture.md @@ -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 企业会员 diff --git a/exe-engine/package.json b/exe-engine/package.json new file mode 100644 index 00000000..02a9f4b5 --- /dev/null +++ b/exe-engine/package.json @@ -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" + } +} diff --git a/exe-engine/src/adapters/base-adapter.js b/exe-engine/src/adapters/base-adapter.js new file mode 100644 index 00000000..6532791f --- /dev/null +++ b/exe-engine/src/adapters/base-adapter.js @@ -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} EXE 标准响应 + */ + async execute(request) { + throw new Error('子类必须实现 execute() 方法'); + } + + /** + * 健康检查(子类可覆写) + * @returns {Promise} + */ + 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; diff --git a/exe-engine/src/adapters/deepseek-adapter.js b/exe-engine/src/adapters/deepseek-adapter.js new file mode 100644 index 00000000..d86e744e --- /dev/null +++ b/exe-engine/src/adapters/deepseek-adapter.js @@ -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} 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} + */ + 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} + */ + _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; diff --git a/exe-engine/src/adapters/qwen-adapter.js b/exe-engine/src/adapters/qwen-adapter.js new file mode 100644 index 00000000..b9bd17f1 --- /dev/null +++ b/exe-engine/src/adapters/qwen-adapter.js @@ -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} 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} + */ + 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} + */ + _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; diff --git a/exe-engine/src/balancer/load-balancer.js b/exe-engine/src/balancer/load-balancer.js new file mode 100644 index 00000000..620811a1 --- /dev/null +++ b/exe-engine/src/balancer/load-balancer.js @@ -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} 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; diff --git a/exe-engine/src/cache/context-cache.js b/exe-engine/src/cache/context-cache.js new file mode 100644 index 00000000..b238a683 --- /dev/null +++ b/exe-engine/src/cache/context-cache.js @@ -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; diff --git a/exe-engine/src/controller/agent-controller.js b/exe-engine/src/controller/agent-controller.js new file mode 100644 index 00000000..a539cd33 --- /dev/null +++ b/exe-engine/src/controller/agent-controller.js @@ -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; diff --git a/exe-engine/src/index.js b/exe-engine/src/index.js new file mode 100644 index 00000000..b9ecd18c --- /dev/null +++ b/exe-engine/src/index.js @@ -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 +}; diff --git a/exe-engine/src/meter/resource-meter.js b/exe-engine/src/meter/resource-meter.js new file mode 100644 index 00000000..b8da7fd4 --- /dev/null +++ b/exe-engine/src/meter/resource-meter.js @@ -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; diff --git a/exe-engine/src/router/age-router.js b/exe-engine/src/router/age-router.js new file mode 100644 index 00000000..be2f029a --- /dev/null +++ b/exe-engine/src/router/age-router.js @@ -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} 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} + */ + 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; diff --git a/exe-engine/tests/smoke/exe-engine.test.js b/exe-engine/tests/smoke/exe-engine.test.js new file mode 100644 index 00000000..e96d2701 --- /dev/null +++ b/exe-engine/tests/smoke/exe-engine.test.js @@ -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); + } +})();