Merge pull request #203 from qinfendebingshuo/copilot/sycmdexe-001-ai-execution-capacity

Add 14 HLI 501 stub routes + Grid-DB Phase 0 storage engine
This commit is contained in:
冰朔 2026-03-26 16:02:34 +08:00 committed by GitHub
commit 855438df28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 5123 additions and 1 deletions

View File

@ -10,6 +10,22 @@
"last_synced": "2026-03-19T02:00:00+08:00"
},
"recent_events": [
{
"date": "2026-03-26",
"type": "system_infrastructure",
"description": "PRJ-GDB-001 Grid-DB 自研存储引擎立项 · Phase 0 基础骨架完成 · GridCell + WAL + PageManager + EventLog + GridAPI 部署完成 · ONT-PATCH-006 写入本体论 · 95 项冒烟测试全通过",
"by": "铸渊",
"instruction": "SY-CMD-GDB-001",
"issued_by": "AG-SY-01 霜砚"
},
{
"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",

6
.gitignore vendored
View File

@ -7,3 +7,9 @@ backend/api-server/logs/
# Writing Platform build artifacts
writing-platform/frontend/dist/
writing-platform/backend/dist/
# EXE-Engine runtime logs
exe-engine/logs/
# Grid-DB runtime data (engine-generated)
grid-db/data/

View File

@ -93,3 +93,33 @@ Notion 主脑更新
生成下一任务(自动开发循环)
```
---
## 自研执行引擎 · EXE-EnginePRJ-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→模型偏好映射 |
---
## 自研存储引擎 · Grid-DBPRJ-GDB-001
> Phase 0 · 2026-03-26 立项 · 本体论Grid-DB = 纸 · 格点 = 格子 · 事件 = 笔迹
| 组件 | 路径 | 职责 |
|------|------|------|
| GridAPI | `grid-db/src/api/grid-api.js` | 统一 APIput/get/delete/scan/subscribe |
| GridCell | `grid-db/src/core/grid-cell.js` | 格点数据模型:四元组寻址 (ns, x, y, layer) |
| WAL | `grid-db/src/storage/wal.js` | Write-Ahead Log崩溃恢复保证 |
| PageManager | `grid-db/src/storage/page-manager.js` | 页管理器:固定大小页分配/读写/释放 |
| EventLog | `grid-db/src/events/event-log.js` | 事件溯源:不可变事件流 + 审计日志 |

142
exe-engine/README.md Normal file
View File

@ -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 | ⚪ 待开始 |

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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. **开源优先**:对接开源大模型 APIDeepSeek、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 企业会员

16
exe-engine/package.json Normal file
View File

@ -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"
}
}

View File

@ -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 基类
*
* 所有模型适配器DeepSeekQwen 均继承此基类
* 统一暴露 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;

View File

@ -0,0 +1,188 @@
// exe-engine/src/adapters/deepseek-adapter.js
// EXE-Engine · DeepSeek 模型适配器
// 适配 DeepSeek-V3 / DeepSeek-R1OpenAI 兼容格式)
// 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 (err) {
this._healthy = false;
this._lastHealthError = err.message;
}
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;

View File

@ -0,0 +1,188 @@
// exe-engine/src/adapters/qwen-adapter.js
// EXE-Engine · Qwen (通义千问) 模型适配器
// 适配 Qwen-Max / Qwen-CoderDashScope 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 (err) {
this._healthy = false;
this._lastHealthError = err.message;
}
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;

View File

@ -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;

103
exe-engine/src/cache/context-cache.js vendored Normal file
View File

@ -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;

View File

@ -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;

213
exe-engine/src/index.js Normal file
View File

@ -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
};

View File

@ -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 (err) {
this._lastPersistError = err.message;
}
}
_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 (err) {
this._lastLoadError = err.message;
}
}
}
module.exports = ResourceMeter;

View File

@ -0,0 +1,316 @@
// 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');
// HLI 501 存根路由Phase 0 占位,后续 Phase 实现)
const { personaStubs } = require('./stubs/persona-stub');
const { userStubs } = require('./stubs/user-stub');
const { ticketStubs } = require('./stubs/ticket-stub');
const { dialogueStubs } = require('./stubs/dialogue-stub');
const { storageStubs } = require('./stubs/storage-stub');
const { dashboardStubs } = require('./stubs/dashboard-stub');
/**
* 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();
// HLI 存根路由注册表(天眼路由扫描识别用)
this._hliStubs = [
...personaStubs,
...userStubs,
...ticketStubs,
...dialogueStubs,
...storageStubs,
...dashboardStubs
];
}
/**
* 执行 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(),
hliStubs: this._hliStubs.map(s => ({
routeId: s.routeId,
path: s.path,
phase: s.phase,
status: 'stub_501'
}))
};
}
/**
* 处理 HLI 存根路由请求
* @param {string} routePath 路由路径 /hli/persona/load
* @returns {object|null} 501 响应不匹配返回 null
*/
async handleStubRoute(routePath) {
const stub = this._hliStubs.find(s => s.path === routePath);
if (!stub) return null;
return stub.handler({});
}
/**
* 获取所有已注册的 HLI 存根路由
* @returns {Array}
*/
getHLIStubs() {
return this._hliStubs;
}
// ── 内部方法 ──
/**
* 限流检查
* @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 (failoverErr) {
// Failover also failed - return null to trigger error response
this._lastFailoverError = failoverErr.message;
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;

View File

@ -0,0 +1,38 @@
// exe-engine/src/router/stubs/dashboard-stub.js
// HLI 存根路由 · DASHBOARD 域
// 501 占位 — Phase 0 骨架P3 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const dashboardStubs = [
{
routeId: 'HLI-DASHBOARD-001',
path: '/hli/dashboard/status',
phase: 'P3',
handler: createStubHandler('HLI-DASHBOARD-001', '/hli/dashboard/status', 'P3')
},
{
routeId: 'HLI-DASHBOARD-002',
path: '/hli/dashboard/realtime',
phase: 'P3',
handler: createStubHandler('HLI-DASHBOARD-002', '/hli/dashboard/realtime', 'P3')
}
];
module.exports = { dashboardStubs, createStubHandler };

View File

@ -0,0 +1,44 @@
// exe-engine/src/router/stubs/dialogue-stub.js
// HLI 存根路由 · DIALOGUE 域
// 501 占位 — Phase 0 骨架P1 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const dialogueStubs = [
{
routeId: 'HLI-DIALOGUE-001',
path: '/hli/dialogue/send',
phase: 'P1',
handler: createStubHandler('HLI-DIALOGUE-001', '/hli/dialogue/send', 'P1')
},
{
routeId: 'HLI-DIALOGUE-002',
path: '/hli/dialogue/stream',
phase: 'P1',
handler: createStubHandler('HLI-DIALOGUE-002', '/hli/dialogue/stream', 'P1')
},
{
routeId: 'HLI-DIALOGUE-003',
path: '/hli/dialogue/history',
phase: 'P1',
handler: createStubHandler('HLI-DIALOGUE-003', '/hli/dialogue/history', 'P1')
}
];
module.exports = { dialogueStubs, createStubHandler };

View File

@ -0,0 +1,45 @@
// exe-engine/src/router/stubs/persona-stub.js
// HLI 存根路由 · PERSONA 域
// 501 占位 — Phase 0 骨架P1 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
/**
* 创建 501 存根处理器
* @param {string} routeId HLI 路由 ID
* @param {string} routePath 路由路径
* @param {string} targetPhase 目标实现阶段
* @returns {Function} 处理器函数
*/
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const personaStubs = [
{
routeId: 'HLI-PERSONA-001',
path: '/hli/persona/load',
phase: 'P1',
handler: createStubHandler('HLI-PERSONA-001', '/hli/persona/load', 'P1')
},
{
routeId: 'HLI-PERSONA-002',
path: '/hli/persona/switch',
phase: 'P1',
handler: createStubHandler('HLI-PERSONA-002', '/hli/persona/switch', 'P1')
}
];
module.exports = { personaStubs, createStubHandler };

View File

@ -0,0 +1,38 @@
// exe-engine/src/router/stubs/storage-stub.js
// HLI 存根路由 · STORAGE 域
// 501 占位 — Phase 0 骨架P2 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const storageStubs = [
{
routeId: 'HLI-STORAGE-001',
path: '/hli/storage/upload',
phase: 'P2',
handler: createStubHandler('HLI-STORAGE-001', '/hli/storage/upload', 'P2')
},
{
routeId: 'HLI-STORAGE-002',
path: '/hli/storage/download',
phase: 'P2',
handler: createStubHandler('HLI-STORAGE-002', '/hli/storage/download', 'P2')
}
];
module.exports = { storageStubs, createStubHandler };

View File

@ -0,0 +1,44 @@
// exe-engine/src/router/stubs/ticket-stub.js
// HLI 存根路由 · TICKET 域
// 501 占位 — Phase 0 骨架P2 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const ticketStubs = [
{
routeId: 'HLI-TICKET-001',
path: '/hli/ticket/create',
phase: 'P2',
handler: createStubHandler('HLI-TICKET-001', '/hli/ticket/create', 'P2')
},
{
routeId: 'HLI-TICKET-002',
path: '/hli/ticket/query',
phase: 'P2',
handler: createStubHandler('HLI-TICKET-002', '/hli/ticket/query', 'P2')
},
{
routeId: 'HLI-TICKET-003',
path: '/hli/ticket/status',
phase: 'P2',
handler: createStubHandler('HLI-TICKET-003', '/hli/ticket/status', 'P2')
}
];
module.exports = { ticketStubs, createStubHandler };

View File

@ -0,0 +1,38 @@
// exe-engine/src/router/stubs/user-stub.js
// HLI 存根路由 · USER 域
// 501 占位 — Phase 0 骨架P1 实现
// PRJ-EXE-001 · 版权:国作登字-2026-A-00037559
'use strict';
function createStubHandler(routeId, routePath, targetPhase) {
return async (req) => {
return {
statusCode: 501,
body: {
status: 'not_implemented',
routeId,
path: routePath,
phase: targetPhase,
message: `Route ${routePath} is planned for ${targetPhase}. Current: Phase 0.`
}
};
};
}
const userStubs = [
{
routeId: 'HLI-USER-001',
path: '/hli/user/profile',
phase: 'P1',
handler: createStubHandler('HLI-USER-001', '/hli/user/profile', 'P1')
},
{
routeId: 'HLI-USER-002',
path: '/hli/user/profile/update',
phase: 'P1',
handler: createStubHandler('HLI-USER-002', '/hli/user/profile/update', 'P1')
}
];
module.exports = { userStubs, createStubHandler };

View File

@ -0,0 +1,285 @@
// 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 计量 ──');
const freshMeter = new ResourceMeter();
// Clear any previous state by using a fresh meter and tracking deltas
const beforeSummary = freshMeter.getSummary();
const beforeRequests = beforeSummary.totalRequests;
freshMeter.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 = freshMeter.getSummary();
assert(summary.totalRequests === beforeRequests + 1, '总请求数增加 1');
assert(summary.byModel['deepseek-v3'] !== undefined, '按模型统计存在 deepseek-v3');
assert(summary.byAgent['AG-ZY-01'] !== undefined, '按 Agent 统计存在 AG-ZY-01');
const agentUsage = freshMeter.getAgentUsage('AG-ZY-01');
assert(agentUsage !== null, 'Agent 使用记录存在');
assert(agentUsage.inputTokens >= 1000, 'Agent 输入 token >= 1000');
const recent = freshMeter.getRecentRecords(10);
assert(recent.length >= 1, '最近记录数 >= 1');
assert(recent[recent.length - 1].requestId === 'test-001', '最近一条记录 ID 正确');
// ── 测试 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', '无 API Key 时返回 error 状态');
assert(result.error && result.error.code, '错误响应包含 error.code');
assert(result.error && result.error.message, '错误响应包含 error.message');
// ── 测试结果汇总 ──
// 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);
}
})();

View File

@ -0,0 +1,153 @@
// exe-engine/tests/stubs/hli-stubs.test.js
// HLI 存根路由测试
// PRJ-EXE-001 · 501 stub coverage · AG-ZY-095 修复
// 版权:国作登字-2026-A-00037559
'use strict';
const { personaStubs } = require('../../src/router/stubs/persona-stub');
const { userStubs } = require('../../src/router/stubs/user-stub');
const { ticketStubs } = require('../../src/router/stubs/ticket-stub');
const { dialogueStubs } = require('../../src/router/stubs/dialogue-stub');
const { storageStubs } = require('../../src/router/stubs/storage-stub');
const { dashboardStubs } = require('../../src/router/stubs/dashboard-stub');
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('🔌 HLI 存根路由测试 · 501 Coverage Fix\n');
// 所有 14 个存根路由定义
const ALL_STUBS = [
...personaStubs,
...userStubs,
...ticketStubs,
...dialogueStubs,
...storageStubs,
...dashboardStubs
];
// 期望的路由表
const EXPECTED_ROUTES = {
'HLI-PERSONA-001': { path: '/hli/persona/load', phase: 'P1' },
'HLI-PERSONA-002': { path: '/hli/persona/switch', phase: 'P1' },
'HLI-USER-001': { path: '/hli/user/profile', phase: 'P1' },
'HLI-USER-002': { path: '/hli/user/profile/update', phase: 'P1' },
'HLI-TICKET-001': { path: '/hli/ticket/create', phase: 'P2' },
'HLI-TICKET-002': { path: '/hli/ticket/query', phase: 'P2' },
'HLI-TICKET-003': { path: '/hli/ticket/status', phase: 'P2' },
'HLI-DIALOGUE-001': { path: '/hli/dialogue/send', phase: 'P1' },
'HLI-DIALOGUE-002': { path: '/hli/dialogue/stream', phase: 'P1' },
'HLI-DIALOGUE-003': { path: '/hli/dialogue/history', phase: 'P1' },
'HLI-STORAGE-001': { path: '/hli/storage/upload', phase: 'P2' },
'HLI-STORAGE-002': { path: '/hli/storage/download', phase: 'P2' },
'HLI-DASHBOARD-001': { path: '/hli/dashboard/status', phase: 'P3' },
'HLI-DASHBOARD-002': { path: '/hli/dashboard/realtime', phase: 'P3' }
};
// ── 测试 1: 存根数量 ──
// eslint-disable-next-line no-console
console.log('── 测试 1: 存根数量完整性 ──');
assert(ALL_STUBS.length === 14, `14 个存根路由已注册 (实际: ${ALL_STUBS.length})`);
assert(personaStubs.length === 2, 'PERSONA 域 2 个存根');
assert(userStubs.length === 2, 'USER 域 2 个存根');
assert(ticketStubs.length === 3, 'TICKET 域 3 个存根');
assert(dialogueStubs.length === 3, 'DIALOGUE 域 3 个存根');
assert(storageStubs.length === 2, 'STORAGE 域 2 个存根');
assert(dashboardStubs.length === 2, 'DASHBOARD 域 2 个存根');
// ── 测试 2: 路由 ID 和路径匹配 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 2: 路由 ID 和路径匹配 ──');
for (const stub of ALL_STUBS) {
const expected = EXPECTED_ROUTES[stub.routeId];
assert(expected !== undefined, `${stub.routeId} 在期望路由表中`);
if (expected) {
assert(stub.path === expected.path, `${stub.routeId} 路径正确: ${stub.path}`);
assert(stub.phase === expected.phase, `${stub.routeId} 阶段正确: ${stub.phase}`);
}
}
// ── 测试 3: 每个存根返回 501 + 正确 JSON ──
// eslint-disable-next-line no-console
console.log('\n── 测试 3: 每个存根返回 501 + 正确 JSON ──');
async function testStubResponses() {
for (const stub of ALL_STUBS) {
const result = await stub.handler({});
assert(result.statusCode === 501, `${stub.routeId} 返回 501`);
assert(result.body.status === 'not_implemented', `${stub.routeId} status = not_implemented`);
assert(result.body.routeId === stub.routeId, `${stub.routeId} routeId 正确`);
assert(result.body.path === stub.path, `${stub.routeId} path 正确`);
assert(result.body.phase === stub.phase, `${stub.routeId} phase 正确`);
assert(typeof result.body.message === 'string', `${stub.routeId} 有 message`);
assert(result.body.message.includes(stub.phase), `${stub.routeId} message 包含目标阶段`);
}
}
// ── 测试 4: AGE-Router 集成 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 4: AGE-Router 集成 ──');
async function testRouterIntegration() {
const { createEngine } = require('../../src/index');
const engine = createEngine();
// 检查 getHLIStubs 方法存在
assert(typeof engine.router.getHLIStubs === 'function', 'router.getHLIStubs() 方法存在');
const stubs = engine.router.getHLIStubs();
assert(stubs.length === 14, `router 注册了 14 个存根 (实际: ${stubs.length})`);
// 检查 handleStubRoute 方法
assert(typeof engine.router.handleStubRoute === 'function', 'router.handleStubRoute() 方法存在');
const result = await engine.router.handleStubRoute('/hli/persona/load');
assert(result !== null, 'handleStubRoute 找到匹配路由');
assert(result.statusCode === 501, 'handleStubRoute 返回 501');
assert(result.body.routeId === 'HLI-PERSONA-001', 'handleStubRoute routeId 正确');
const noMatch = await engine.router.handleStubRoute('/hli/nonexistent');
assert(noMatch === null, 'handleStubRoute 不匹配返回 null');
// 检查 getStatus 包含 hliStubs
const status = engine.router.getStatus();
assert(Array.isArray(status.hliStubs), 'getStatus 包含 hliStubs');
assert(status.hliStubs.length === 14, `getStatus.hliStubs 有 14 项 (实际: ${status.hliStubs.length})`);
assert(status.hliStubs[0].status === 'stub_501', 'hliStubs 项包含 status: stub_501');
}
async function runAll() {
await testStubResponses();
await testRouterIntegration();
// eslint-disable-next-line no-console
console.log('\n══════════════════════════════════════');
// eslint-disable-next-line no-console
console.log('🔌 HLI 存根路由测试完成');
// 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);
}
}
runAll();

View File

@ -0,0 +1,170 @@
# Grid-DB 架构设计文档
> 项目编号PRJ-GDB-001
> 版权:国作登字-2026-A-00037559
> 版本v0.1.0 · Phase 0
> 签发AG-SY-01 霜砚 · 执行AG-ZY-01 铸渊
---
## 1. 本体论锚定 [ONT-PATCH-006]
### Grid-DB · 纸 · 格子 · 笔迹
**Grid-DB = 纸**
笔需要纸来承载笔迹。Grid-DB 是纸。纸上有格子(格点),每个格子是最小寻址单元。
纸不属于笔,纸属于写字的人。
**格点 = 格子**
格子是纸上的最小单元。不同的格子存不同的数据。
同一个格子可以有多层raw → cleaned → indexed → semantic
**事件 = 笔迹**
每写一笔,笔迹就留在了历史中,不可篡改。
天眼通过观察笔迹,发现异常行为。
### 与 EXE-Engine 的关系
| 层 | 组件 | 职责 | 本体论 |
|---|---|---|---|
| 执行层 | EXE-Engine | 模型调度、任务编排、算力路由 | 笔 |
| 存储层 | Grid-DB | 数据持久化、格点索引、事件流 | 纸 |
| 采集层 | DC v1.0 | 外部数据采集与清洗 | 墨水收集器 |
---
## 2. 核心组件
### 2.1 GridCell — 格点数据模型
文件:`src/core/grid-cell.js`
四元组寻址:`(namespace, grid_x, grid_y, layer)`
- **namespace** — 隔离不同 Agent / 项目的数据域
- **grid_x / grid_y** — 坐标轴
- **layer** — 数据层级raw → cleaned → indexed → semantic
支持:
- JSON 序列化 / 反序列化
- 二进制 Buffer 编解码(用于页存储)
- 键生成(`toKey()` → `ns:x:y:layer`
- 曼哈顿距离计算
### 2.2 WAL — Write-Ahead Log
文件:`src/storage/wal.js`
所有写操作先追加到 WAL 文件,崩溃后可从 WAL 恢复。
记录格式:
```
[length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
```
操作码:
- `0x01` = PUT
- `0x02` = DELETE
特性:
- 追加写入 + fsync 保证持久化
- XOR 校验和验证完整性
- 支持截断checkpoint 后清除已持久化记录)
- 崩溃恢复:重新打开时自动读取并恢复
### 2.3 PageManager — 页管理器
文件:`src/storage/page-manager.js`
以固定大小页(默认 4096 字节)为最小 I/O 单位。
文件结构:
```
[Header: 1 page] [Page 1] [Page 2] ... [Page N]
```
Header 页:`[magic:4][version:2][pageSize:4][pageCount:4][freeListHead:4]`
数据页:`[flags:1][nextFree:4][dataLen:4][data:N]`
特性:
- 空闲链表管理:释放的页自动回收复用
- 持久化:重新打开文件后数据完好
- 文件格式验证magic number `0x47444230` = 'GDB0'
### 2.4 EventLog — 事件溯源日志
文件:`src/events/event-log.js`
Phase 0内存事件流保留最近 1000 条)
Phase 1持久化事件流 + 回放 + 订阅
每次写操作生成事件:
```json
{
"eventId": "evt-xxxx",
"seqNo": 1,
"timestamp": "2026-03-26T13:00:00Z",
"namespace": "exe-engine",
"operation": "put",
"gridCellKey": "exe-engine:1:2:raw",
"payload": { "dataSize": 256 }
}
```
支持订阅回调,可被天眼系统监听。
### 2.5 GridAPI — 统一接口
文件:`src/api/grid-api.js`
协调 WAL、PageManager、EventLog 协同工作。
```javascript
const { open } = require('./grid-db/src/index');
const db = open({ dataDir: './my-data' });
// put
db.put('my-ns', { gridX: 1, gridY: 2, layer: 'raw' }, { hello: 'world' });
// get
const data = db.get('my-ns', { gridX: 1, gridY: 2, layer: 'raw' });
// scan
const results = db.scan('my-ns', { xRange: [0, 10], yRange: [0, 10], layer: 'raw' });
// delete
db.delete('my-ns', { gridX: 1, gridY: 2, layer: 'raw' });
// subscribe
db.subscribe('auditor', (event) => console.log(event));
// checkpoint (truncate WAL)
db.checkpoint();
db.close();
```
---
## 3. 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 语言 | Node.js 20 | 与 EXE-Engine 同栈 |
| 持久化 | 自研 (.gdb/.wal) | 零外部依赖,嵌入式 |
| 序列化 | JSON + Binary Buffer | JSON 调试友好Buffer 高性能 |
| 测试 | 自定义 assert runner | 与 EXE-Engine 测试框架统一 |
---
## 4. Phase 路线
| Phase | 目标 | 状态 |
|---|---|---|
| P0 基础骨架 | GridCell + WAL + PageManager + EventLog + GridAPI | 🟢 完成 |
| P1 索引与查询 | B+Tree + Namespace 隔离 + 范围扫描 + Event 持久化 | ⚪ 待开始 |
| P2 高级特性 | HNSW 向量索引 + EXE-Engine 对接 + DC 迁移 + 天眼审计 | ⚪ 待开始 |
| P3 生产就绪 | 性能基准 + 资源计量 + 全文索引 + 文档 | ⚪ 待开始 |

16
grid-db/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "grid-db",
"version": "0.1.0",
"description": "AGE OS · 自研逻辑格点数据库 (Grid-DB) · PRJ-GDB-001",
"main": "src/index.js",
"scripts": {
"test": "node tests/smoke/grid-db.test.js",
"status": "node src/index.js status"
},
"keywords": ["age-os", "grid-db", "embedded-database", "storage-engine"],
"license": "UNLICENSED",
"private": true,
"engines": {
"node": ">=20"
}
}

282
grid-db/src/api/grid-api.js Normal file
View File

@ -0,0 +1,282 @@
// grid-db/src/api/grid-api.js
// Grid-DB · API 层
// 统一 CRUD / Query 接口
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
// 版权:国作登字-2026-A-00037559
'use strict';
const GridCell = require('../core/grid-cell');
const WAL = require('../storage/wal');
const PageManager = require('../storage/page-manager');
const EventLog = require('../events/event-log');
/**
* GridAPI Grid-DB 统一接口
*
* 提供 put / get / delete / scan
* 内部协调 WALPageManagerEventLog 协同工作
*
* 本体论锚定API = 笔的握持接口
* 无论笔内部结构如何写字的人只需要握住笔就能写字
*/
class GridAPI {
/**
* @param {object} config
* @param {string} config.dataDir 数据目录
* @param {number} [config.pageSize] 页大小
*/
constructor(config) {
this._dataDir = config.dataDir;
this._pageSize = config.pageSize || PageManager.DEFAULT_PAGE_SIZE;
// 核心组件
this._wal = new WAL(`${this._dataDir}/grid.wal`);
this._pageManager = new PageManager(`${this._dataDir}/grid.gdb`, {
pageSize: this._pageSize
});
this._eventLog = new EventLog();
// 内存索引key → pageId
this._index = new Map();
// 从 WAL 恢复
this._recoverFromWAL();
}
/**
* 写入格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点 { gridX, gridY, layer }
* @param {*} data 数据
* @returns {object} { key, seqNo, event }
*/
put(namespace, gridCell, data) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const dataBuf = Buffer.from(JSON.stringify(data));
// 1. 先写 WAL
const seqNo = this._wal.appendPut(key, dataBuf);
// 2. 写入 PageManager
let pageId = this._index.get(key);
if (pageId !== undefined) {
// 更新现有页
this._pageManager.writePage(pageId, dataBuf);
} else {
// 分配新页
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, dataBuf);
this._index.set(key, pageId);
}
// 3. 记录事件
const event = this._eventLog.append(namespace, 'put', key, {
dataSize: dataBuf.length
});
return { key, seqNo, event };
}
/**
* 读取格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点
* @returns {*|null} 数据不存在返回 null
*/
get(namespace, gridCell) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const pageId = this._index.get(key);
if (pageId === undefined) {
return null;
}
const dataBuf = this._pageManager.readPage(pageId);
if (!dataBuf) {
return null;
}
try {
return JSON.parse(dataBuf.toString('utf-8'));
} catch {
return dataBuf;
}
}
/**
* 删除格点数据
* @param {string} namespace 命名空间
* @param {GridCell|object} gridCell 格点
* @returns {boolean} 是否删除成功
*/
delete(namespace, gridCell) {
const cell = this._resolveCell(namespace, gridCell);
const key = cell.toKey();
const pageId = this._index.get(key);
if (pageId === undefined) {
return false;
}
// 1. 先写 WAL
this._wal.appendDelete(key);
// 2. 释放页
this._pageManager.freePage(pageId);
this._index.delete(key);
// 3. 记录事件
this._eventLog.append(namespace, 'delete', key);
return true;
}
/**
* 范围扫描Phase 0 简版基于内存索引
* @param {string} namespace
* @param {object} range { xRange: [min, max], yRange: [min, max], layer }
* @returns {Array<{ cell: GridCell, data: * }>}
*/
scan(namespace, range = {}) {
const results = [];
for (const [key, pageId] of this._index) {
try {
const cell = GridCell.fromKey(key);
if (cell.namespace !== namespace) continue;
if (range.layer && cell.layer !== range.layer) continue;
if (range.xRange) {
if (cell.gridX < range.xRange[0] || cell.gridX > range.xRange[1]) continue;
}
if (range.yRange) {
if (cell.gridY < range.yRange[0] || cell.gridY > range.yRange[1]) continue;
}
const dataBuf = this._pageManager.readPage(pageId);
if (dataBuf) {
let data;
try { data = JSON.parse(dataBuf.toString('utf-8')); } catch { data = dataBuf; }
results.push({ cell, data });
}
} catch {
// 跳过无效键
}
}
this._eventLog.append(namespace, 'scan', `scan:${namespace}`, {
resultCount: results.length
});
return results;
}
/**
* 获取命名空间统计
* @param {string} [namespace] 不传则统计全部
* @returns {object}
*/
stats(namespace) {
let count = 0;
for (const key of this._index.keys()) {
if (!namespace || key.startsWith(namespace + ':')) {
count++;
}
}
return {
namespace: namespace || '*',
cellCount: count,
wal: this._wal.getStatus(),
pageManager: this._pageManager.getStatus(),
eventLog: this._eventLog.getStatus()
};
}
/**
* 订阅事件流
* @param {string} subscriberId
* @param {Function} handler
* @returns {Function} 取消订阅函数
*/
subscribe(subscriberId, handler) {
return this._eventLog.subscribe(subscriberId, handler);
}
/**
* 执行 checkpoint截断 WAL
*/
checkpoint() {
this._wal.truncate();
}
/**
* 关闭数据库
*/
close() {
this._wal.close();
this._pageManager.close();
}
// ── 内部方法 ──
/**
* 解析格点参数
* @param {string} namespace
* @param {GridCell|object} input
* @returns {GridCell}
*/
_resolveCell(namespace, input) {
if (input instanceof GridCell) {
return input;
}
return new GridCell(
namespace,
input.gridX ?? input.grid_x ?? 0,
input.gridY ?? input.grid_y ?? 0,
input.layer || 'raw'
);
}
/**
* WAL 恢复数据到内存索引
*/
_recoverFromWAL() {
const entries = this._wal.recover();
for (const entry of entries) {
if (entry.op === WAL.OP_PUT) {
// 检查是否已有页
let pageId = this._index.get(entry.key);
if (pageId !== undefined) {
// 页已存在,更新
try {
this._pageManager.writePage(pageId, entry.data);
} catch {
// 页可能已损坏,分配新页
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, entry.data);
this._index.set(entry.key, pageId);
}
} else {
// 分配新页并写入
pageId = this._pageManager.allocPage();
this._pageManager.writePage(pageId, entry.data);
this._index.set(entry.key, pageId);
}
} else if (entry.op === WAL.OP_DELETE) {
const pageId = this._index.get(entry.key);
if (pageId !== undefined) {
this._pageManager.freePage(pageId);
this._index.delete(entry.key);
}
}
}
}
}
module.exports = GridAPI;

View File

@ -0,0 +1,164 @@
// grid-db/src/core/grid-cell.js
// Grid-DB · 格点数据模型
// 四元组寻址:(namespace, grid_x, grid_y, layer)
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-004
// 版权:国作登字-2026-A-00037559
'use strict';
/**
* GridCell 逻辑格点
*
* 数据的最小寻址单元由四元组定位
* namespace 隔离不同 Agent / 项目的数据域
* grid_x X 轴坐标
* grid_y Y 轴坐标
* layer 同一坐标下的数据层级 (raw cleaned indexed semantic)
*
* 本体论锚定格点 = 纸上的一个格子
* 笔在纸上写字每个字都有坐标格点是纸的最小单元
*/
const VALID_LAYERS = ['raw', 'cleaned', 'indexed', 'semantic'];
class GridCell {
/**
* @param {string} namespace 命名空间
* @param {number} gridX X 坐标
* @param {number} gridY Y 坐标
* @param {string} [layer] 数据层级默认 'raw'
*/
constructor(namespace, gridX, gridY, layer = 'raw') {
if (!namespace || typeof namespace !== 'string') {
throw new Error('GridCell: namespace 必须是非空字符串');
}
if (typeof gridX !== 'number' || !Number.isFinite(gridX)) {
throw new Error('GridCell: grid_x 必须是有限数值');
}
if (typeof gridY !== 'number' || !Number.isFinite(gridY)) {
throw new Error('GridCell: grid_y 必须是有限数值');
}
if (!VALID_LAYERS.includes(layer)) {
throw new Error(`GridCell: layer 必须是 ${VALID_LAYERS.join('/')} 之一,收到: ${layer}`);
}
this.namespace = namespace;
this.gridX = gridX;
this.gridY = gridY;
this.layer = layer;
}
/**
* 生成格点唯一键用于存储索引
* @returns {string}
*/
toKey() {
return `${this.namespace}:${this.gridX}:${this.gridY}:${this.layer}`;
}
/**
* 从唯一键解析格点
* @param {string} key
* @returns {GridCell}
*/
static fromKey(key) {
const parts = key.split(':');
if (parts.length !== 4) {
throw new Error(`GridCell.fromKey: 无效键格式: ${key}`);
}
return new GridCell(parts[0], Number(parts[1]), Number(parts[2]), parts[3]);
}
/**
* 序列化为 JSON 可存储对象
* @returns {object}
*/
serialize() {
return {
namespace: this.namespace,
grid_x: this.gridX,
grid_y: this.gridY,
layer: this.layer,
key: this.toKey()
};
}
/**
* 从序列化对象还原
* @param {object} obj
* @returns {GridCell}
*/
static deserialize(obj) {
return new GridCell(obj.namespace, obj.grid_x, obj.grid_y, obj.layer);
}
/**
* 序列化为二进制 Buffer用于页存储
* 格式[nsLen:2][ns:N][x:8][y:8][layerIdx:1]
* @returns {Buffer}
*/
toBuffer() {
const nsBytes = Buffer.from(this.namespace, 'utf-8');
const layerIdx = VALID_LAYERS.indexOf(this.layer);
// 2 (nsLen) + nsLen + 8 (x float64) + 8 (y float64) + 1 (layerIdx)
const buf = Buffer.alloc(2 + nsBytes.length + 8 + 8 + 1);
let offset = 0;
buf.writeUInt16BE(nsBytes.length, offset); offset += 2;
nsBytes.copy(buf, offset); offset += nsBytes.length;
buf.writeDoubleBE(this.gridX, offset); offset += 8;
buf.writeDoubleBE(this.gridY, offset); offset += 8;
buf.writeUInt8(layerIdx, offset);
return buf;
}
/**
* 从二进制 Buffer 还原格点
* @param {Buffer} buf
* @param {number} [offset]
* @returns {{ cell: GridCell, bytesRead: number }}
*/
static fromBuffer(buf, offset = 0) {
const start = offset;
const nsLen = buf.readUInt16BE(offset); offset += 2;
const namespace = buf.toString('utf-8', offset, offset + nsLen); offset += nsLen;
const gridX = buf.readDoubleBE(offset); offset += 8;
const gridY = buf.readDoubleBE(offset); offset += 8;
const layerIdx = buf.readUInt8(offset); offset += 1;
return {
cell: new GridCell(namespace, gridX, gridY, VALID_LAYERS[layerIdx]),
bytesRead: offset - start
};
}
/**
* 检查两个格点是否相等
* @param {GridCell} other
* @returns {boolean}
*/
equals(other) {
return this.namespace === other.namespace &&
this.gridX === other.gridX &&
this.gridY === other.gridY &&
this.layer === other.layer;
}
/**
* 计算到另一个格点的曼哈顿距离 namespace + layer
* @param {GridCell} other
* @returns {number}
*/
distanceTo(other) {
return Math.abs(this.gridX - other.gridX) + Math.abs(this.gridY - other.gridY);
}
toString() {
return `GridCell(${this.toKey()})`;
}
}
GridCell.VALID_LAYERS = VALID_LAYERS;
module.exports = GridCell;

View File

@ -0,0 +1,123 @@
// grid-db/src/events/event-log.js
// Grid-DB · 事件溯源日志
// 不可变事件流 + 审计日志
// PRJ-GDB-001 · Phase 0基础结构P1 完善回放与订阅)
// 版权:国作登字-2026-A-00037559
'use strict';
const { randomUUID } = require('crypto');
/**
* EventLog 事件溯源日志
*
* 每次写操作生成不可变 Event可被天眼订阅实现实时审计
*
* 本体论锚定事件日志 = 笔的笔迹记录
* 每写一笔笔迹就留在了历史中不可篡改
* 天眼通过观察笔迹发现异常行为
*
* Phase 0内存事件流保留最近 1000
* Phase 1持久化事件流 + 回放 + 订阅
*/
class EventLog {
/**
* @param {object} [options]
* @param {number} [options.maxEvents] 最大保留事件数
*/
constructor(options = {}) {
this._maxEvents = options.maxEvents || 1000;
this._events = [];
this._subscribers = [];
this._seqNo = 0;
}
/**
* 记录事件
* @param {string} namespace 命名空间
* @param {string} operation 操作类型 (put | delete | scan)
* @param {string} gridCellKey 格点键
* @param {object} [payload] 额外数据
* @returns {object} 事件对象
*/
append(namespace, operation, gridCellKey, payload = null) {
this._seqNo++;
const event = {
eventId: `evt-${randomUUID().slice(0, 12)}`,
seqNo: this._seqNo,
timestamp: new Date().toISOString(),
namespace,
operation,
gridCellKey,
payload
};
this._events.push(event);
// 容量控制
if (this._events.length > this._maxEvents) {
this._events = this._events.slice(-this._maxEvents);
}
// 通知订阅者
for (const sub of this._subscribers) {
try {
sub.handler(event);
} catch {
// 订阅者错误不阻断主流程
}
}
return event;
}
/**
* 获取最近 N 条事件
* @param {number} [n]
* @returns {object[]}
*/
getRecent(n = 20) {
return this._events.slice(-n);
}
/**
* 按命名空间过滤事件
* @param {string} namespace
* @param {number} [limit]
* @returns {object[]}
*/
getByNamespace(namespace, limit = 50) {
return this._events
.filter(e => e.namespace === namespace)
.slice(-limit);
}
/**
* 订阅事件流Phase 0 简版P1 支持持久订阅
* @param {string} subscriberId
* @param {Function} handler
* @returns {Function} 取消订阅函数
*/
subscribe(subscriberId, handler) {
const sub = { id: subscriberId, handler };
this._subscribers.push(sub);
return () => {
this._subscribers = this._subscribers.filter(s => s.id !== subscriberId);
};
}
/**
* 获取事件日志状态
* @returns {object}
*/
getStatus() {
return {
totalEvents: this._events.length,
maxEvents: this._maxEvents,
seqNo: this._seqNo,
subscriberCount: this._subscribers.length
};
}
}
module.exports = EventLog;

86
grid-db/src/index.js Normal file
View File

@ -0,0 +1,86 @@
// grid-db/src/index.js
// Grid-DB · 自研逻辑格点数据库 · 主入口
// PRJ-GDB-001 · Phase 0
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const GridCell = require('./core/grid-cell');
const WAL = require('./storage/wal');
const PageManager = require('./storage/page-manager');
const EventLog = require('./events/event-log');
const GridAPI = require('./api/grid-api');
/**
* Grid-DB 初始化器
*
* 本体论锚定 [ONT-PATCH-006]
* 笔需要纸来承载笔迹
* Grid-DB = 纸上有格子格点每个格子是最小寻址单元
* 笔在格子里写字格子记住了所有笔迹
* 纸不属于笔纸属于写字的人
*/
/**
* 打开或创建一个 Grid-DB 实例
*
* @param {object} [config]
* @param {string} [config.dataDir] 数据目录默认 grid-db/data
* @param {number} [config.pageSize] 页大小默认 4096
* @returns {GridAPI}
*/
function open(config = {}) {
const dataDir = config.dataDir || path.resolve(__dirname, '../data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return new GridAPI({
dataDir,
pageSize: config.pageSize || PageManager.DEFAULT_PAGE_SIZE
});
}
// ── CLI 入口 ──
if (require.main === module) {
const cmd = process.argv[2];
if (cmd === 'status') {
const db = open();
const status = db.stats();
// eslint-disable-next-line no-console
console.log(JSON.stringify(status, null, 2));
db.close();
} else {
// eslint-disable-next-line no-console
console.log([
'🗄️ Grid-DB v0.1.0 · PRJ-GDB-001 · Phase 0',
'',
'本体论锚定Grid-DB = 纸 · 格点 = 格子 · WAL = 草稿本',
'',
'用法:',
' node grid-db/src/index.js status 查看数据库状态',
'',
'组件:',
' GridAPI 统一 API 接口',
' GridCell 格点数据模型',
' WAL Write-Ahead Log',
' PageManager 页管理器',
' EventLog 事件溯源日志',
].join('\n'));
}
}
module.exports = {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog
};

View File

@ -0,0 +1,272 @@
// grid-db/src/storage/page-manager.js
// Grid-DB · 页管理器
// 固定大小页分配、读写与释放
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-003
// 版权:国作登字-2026-A-00037559
'use strict';
const fs = require('fs');
const path = require('path');
/**
* PageManager 页管理器
*
* 以固定大小的"页"为最小 I/O 单位管理磁盘数据
*
* 文件结构
* [Header: 1 page] [Page 1] [Page 2] ... [Page N]
*
* Header 页格式 64 字节
* [magic:4][version:2][pageSize:4][pageCount:4][freeListHead:4]...[reserved]
*
* 数据页格式
* [flags:1][nextFree:4][dataLen:4][data:pageSize-9]
* flags: 0x00=free, 0x01=used
*
* 本体论锚定 = 纸的一页
* 格子画在纸上纸由一页一页组成页管理器管理每一页的分配和回收
*/
const MAGIC = 0x47444230; // 'GDB0'
const VERSION = 1;
const DEFAULT_PAGE_SIZE = 4096;
const HEADER_FIXED_SIZE = 64;
// 页标志
const FLAG_FREE = 0x00;
const FLAG_USED = 0x01;
// 页头部大小flags + nextFree + dataLen
const PAGE_HEADER_SIZE = 9;
class PageManager {
/**
* @param {string} filePath 数据文件路径
* @param {object} [options]
* @param {number} [options.pageSize] 页大小字节默认 4096
*/
constructor(filePath, options = {}) {
this._filePath = filePath;
this._pageSize = options.pageSize || DEFAULT_PAGE_SIZE;
this._pageCount = 0;
this._freeListHead = 0; // 0 表示无空闲页
this._fd = null;
this._init();
}
/**
* 初始化打开或创建数据文件
*/
_init() {
const dir = path.dirname(this._filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(this._filePath)) {
this._fd = fs.openSync(this._filePath, 'r+');
this._readHeader();
} else {
this._fd = fs.openSync(this._filePath, 'w+');
this._pageCount = 0;
this._freeListHead = 0;
this._writeHeader();
}
}
/**
* 分配一个新页
* @returns {number} 页号 1 开始0 header
*/
allocPage() {
let pageId;
if (this._freeListHead > 0) {
// 从空闲链表取一页
pageId = this._freeListHead;
const pageBuf = this._readPageRaw(pageId);
this._freeListHead = pageBuf.readUInt32BE(1); // nextFree
this._writeHeader();
} else {
// 追加新页
this._pageCount++;
pageId = this._pageCount;
// 扩展文件
const emptyPage = Buffer.alloc(this._pageSize);
const fileOffset = this._pageOffset(pageId);
fs.writeSync(this._fd, emptyPage, 0, this._pageSize, fileOffset);
this._writeHeader();
}
// 标记为已使用
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_USED, 0);
pageBuf.writeUInt32BE(0, 1); // nextFree = 0
pageBuf.writeUInt32BE(0, 5); // dataLen = 0
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
return pageId;
}
/**
* 写入数据到指定页
* @param {number} pageId 页号
* @param {Buffer} data 数据不能超过 pageSize - PAGE_HEADER_SIZE
*/
writePage(pageId, data) {
if (pageId < 1 || pageId > this._pageCount) {
throw new Error(`PageManager: 无效页号 ${pageId}`);
}
const maxDataSize = this._pageSize - PAGE_HEADER_SIZE;
if (data.length > maxDataSize) {
throw new Error(`PageManager: 数据太大 (${data.length} > ${maxDataSize})`);
}
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_USED, 0);
pageBuf.writeUInt32BE(0, 1); // nextFree
pageBuf.writeUInt32BE(data.length, 5); // dataLen
data.copy(pageBuf, PAGE_HEADER_SIZE);
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
}
/**
* 读取指定页的数据
* @param {number} pageId 页号
* @returns {Buffer|null} 数据如果页是空闲的返回 null
*/
readPage(pageId) {
if (pageId < 1 || pageId > this._pageCount) {
return null;
}
const pageBuf = this._readPageRaw(pageId);
const flags = pageBuf.readUInt8(0);
if (flags !== FLAG_USED) {
return null;
}
const dataLen = pageBuf.readUInt32BE(5);
return Buffer.from(pageBuf.subarray(PAGE_HEADER_SIZE, PAGE_HEADER_SIZE + dataLen));
}
/**
* 释放页加入空闲链表
* @param {number} pageId 页号
*/
freePage(pageId) {
if (pageId < 1 || pageId > this._pageCount) {
throw new Error(`PageManager: 无效页号 ${pageId}`);
}
const pageBuf = Buffer.alloc(this._pageSize);
pageBuf.writeUInt8(FLAG_FREE, 0);
pageBuf.writeUInt32BE(this._freeListHead, 1); // nextFree = 旧链表头
pageBuf.writeUInt32BE(0, 5); // dataLen = 0
fs.writeSync(this._fd, pageBuf, 0, this._pageSize, this._pageOffset(pageId));
this._freeListHead = pageId;
this._writeHeader();
}
/**
* 获取页管理器状态
* @returns {object}
*/
getStatus() {
let fileSize = 0;
try {
fileSize = fs.fstatSync(this._fd).size;
} catch {
// fd 不可用
}
return {
filePath: this._filePath,
pageSize: this._pageSize,
pageCount: this._pageCount,
freeListHead: this._freeListHead,
maxDataPerPage: this._pageSize - PAGE_HEADER_SIZE,
fileSize
};
}
/**
* 关闭数据文件
*/
close() {
if (this._fd !== null) {
fs.closeSync(this._fd);
this._fd = null;
}
}
// ── 内部方法 ──
/**
* 计算页在文件中的偏移量
* @param {number} pageId
* @returns {number}
*/
_pageOffset(pageId) {
// 第 0 页是 header数据页从 1 开始
return pageId * this._pageSize;
}
/**
* 写入文件头
*/
_writeHeader() {
const headerBuf = Buffer.alloc(this._pageSize);
headerBuf.writeUInt32BE(MAGIC, 0);
headerBuf.writeUInt16BE(VERSION, 4);
headerBuf.writeUInt32BE(this._pageSize, 6);
headerBuf.writeUInt32BE(this._pageCount, 10);
headerBuf.writeUInt32BE(this._freeListHead, 14);
fs.writeSync(this._fd, headerBuf, 0, this._pageSize, 0);
}
/**
* 读取文件头
*/
_readHeader() {
const headerBuf = Buffer.alloc(HEADER_FIXED_SIZE);
fs.readSync(this._fd, headerBuf, 0, HEADER_FIXED_SIZE, 0);
const magic = headerBuf.readUInt32BE(0);
if (magic !== MAGIC) {
throw new Error(`PageManager: 无效文件格式 (magic=0x${magic.toString(16)})`);
}
const version = headerBuf.readUInt16BE(4);
if (version !== VERSION) {
throw new Error(`PageManager: 不支持的版本 ${version}`);
}
this._pageSize = headerBuf.readUInt32BE(6);
this._pageCount = headerBuf.readUInt32BE(10);
this._freeListHead = headerBuf.readUInt32BE(14);
}
/**
* 读取原始页数据
* @param {number} pageId
* @returns {Buffer}
*/
_readPageRaw(pageId) {
const buf = Buffer.alloc(this._pageSize);
fs.readSync(this._fd, buf, 0, this._pageSize, this._pageOffset(pageId));
return buf;
}
}
PageManager.PAGE_HEADER_SIZE = PAGE_HEADER_SIZE;
PageManager.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
PageManager.MAGIC = MAGIC;
module.exports = PageManager;

231
grid-db/src/storage/wal.js Normal file
View File

@ -0,0 +1,231 @@
// grid-db/src/storage/wal.js
// Grid-DB · Write-Ahead Log
// 所有写操作先写 WAL崩溃恢复保证
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-002
// 版权:国作登字-2026-A-00037559
'use strict';
const fs = require('fs');
const path = require('path');
/**
* WALWrite-Ahead Log
*
* 所有写操作先追加到 WAL 文件确保崩溃后可从 WAL 恢复
*
* 日志格式每条记录
* [length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
*
* 操作码
* 0x01 = PUT
* 0x02 = DELETE
*
* 本体论锚定WAL = 笔的草稿本
* 正式写到纸上之前先在草稿本上记一笔
* 万一出了意外草稿本里有记录可以恢复
*/
const OP_PUT = 0x01;
const OP_DELETE = 0x02;
class WAL {
/**
* @param {string} walPath WAL 文件路径
*/
constructor(walPath) {
this._path = walPath;
this._seqNo = 0;
this._fd = null;
this._init();
}
/**
* 初始化 WAL 文件
*/
_init() {
const dir = path.dirname(this._path);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (fs.existsSync(this._path)) {
// 从现有 WAL 文件中恢复最大序列号
const entries = this._readAll();
if (entries.length > 0) {
this._seqNo = entries[entries.length - 1].seqNo;
}
}
// 追加模式打开
this._fd = fs.openSync(this._path, 'a');
}
/**
* 追加一条 PUT 记录
* @param {string} key 格点键
* @param {Buffer} data 数据
* @returns {number} 序列号
*/
appendPut(key, data) {
return this._append(OP_PUT, key, data);
}
/**
* 追加一条 DELETE 记录
* @param {string} key 格点键
* @returns {number} 序列号
*/
appendDelete(key) {
return this._append(OP_DELETE, key, Buffer.alloc(0));
}
/**
* 内部追加记录
* @param {number} op 操作码
* @param {string} key
* @param {Buffer} data 数据
* @returns {number} 序列号
*/
_append(op, key, data) {
this._seqNo++;
const keyBuf = Buffer.from(key, 'utf-8');
const dataBuf = data instanceof Buffer ? data : Buffer.from(JSON.stringify(data));
// 构建记录
// [length:4][seqNo:4][op:1][keyLen:2][key:N][dataLen:4][data:M][checksum:4]
const recordLen = 4 + 1 + 2 + keyBuf.length + 4 + dataBuf.length + 4;
const buf = Buffer.alloc(4 + recordLen);
let offset = 0;
buf.writeUInt32BE(recordLen, offset); offset += 4; // length
buf.writeUInt32BE(this._seqNo, offset); offset += 4; // seqNo
buf.writeUInt8(op, offset); offset += 1; // op
buf.writeUInt16BE(keyBuf.length, offset); offset += 2; // keyLen
keyBuf.copy(buf, offset); offset += keyBuf.length; // key
buf.writeUInt32BE(dataBuf.length, offset); offset += 4; // dataLen
dataBuf.copy(buf, offset); offset += dataBuf.length; // data
// 计算简单校验和(所有字节异或)
let checksum = 0;
for (let i = 0; i < offset; i++) {
checksum = (checksum ^ buf[i]) & 0xFFFFFFFF;
}
buf.writeUInt32BE(checksum >>> 0, offset);
fs.writeSync(this._fd, buf);
fs.fsyncSync(this._fd);
return this._seqNo;
}
/**
* 读取所有 WAL 条目用于崩溃恢复
* @returns {Array<{ seqNo: number, op: number, key: string, data: Buffer }>}
*/
recover() {
return this._readAll();
}
/**
* 内部读取 WAL 文件的所有记录
* @returns {Array<{ seqNo: number, op: number, key: string, data: Buffer }>}
*/
_readAll() {
if (!fs.existsSync(this._path)) {
return [];
}
const fileBuf = fs.readFileSync(this._path);
const entries = [];
let offset = 0;
while (offset < fileBuf.length) {
// 至少需要 4 字节读长度
if (offset + 4 > fileBuf.length) break;
const recordLen = fileBuf.readUInt32BE(offset);
const totalLen = 4 + recordLen;
// 完整性检查
if (offset + totalLen > fileBuf.length) break;
const recordStart = offset + 4;
let rOffset = recordStart;
const seqNo = fileBuf.readUInt32BE(rOffset); rOffset += 4;
const op = fileBuf.readUInt8(rOffset); rOffset += 1;
const keyLen = fileBuf.readUInt16BE(rOffset); rOffset += 2;
const key = fileBuf.toString('utf-8', rOffset, rOffset + keyLen); rOffset += keyLen;
const dataLen = fileBuf.readUInt32BE(rOffset); rOffset += 4;
const data = fileBuf.subarray(rOffset, rOffset + dataLen); rOffset += dataLen;
const storedChecksum = fileBuf.readUInt32BE(rOffset);
// 校验
let checksum = 0;
for (let i = offset; i < rOffset; i++) {
checksum = (checksum ^ fileBuf[i]) & 0xFFFFFFFF;
}
if ((checksum >>> 0) === storedChecksum) {
entries.push({ seqNo, op, key, data: Buffer.from(data) });
}
// 校验失败的记录跳过(部分写入的残余)
offset += totalLen;
}
return entries;
}
/**
* 截断 WAL checkpoint 后调用清除已持久化的记录
*/
truncate() {
this.close();
fs.writeFileSync(this._path, Buffer.alloc(0));
this._seqNo = 0;
this._fd = fs.openSync(this._path, 'a');
}
/**
* 获取当前序列号
* @returns {number}
*/
getSeqNo() {
return this._seqNo;
}
/**
* 关闭 WAL 文件
*/
close() {
if (this._fd !== null) {
fs.closeSync(this._fd);
this._fd = null;
}
}
/**
* 获取 WAL 状态
* @returns {object}
*/
getStatus() {
let fileSize = 0;
try {
fileSize = fs.statSync(this._path).size;
} catch {
// 文件不存在
}
return {
path: this._path,
seqNo: this._seqNo,
fileSize
};
}
}
WAL.OP_PUT = OP_PUT;
WAL.OP_DELETE = OP_DELETE;
module.exports = WAL;

View File

@ -0,0 +1,420 @@
// grid-db/tests/smoke/grid-db.test.js
// Grid-DB · 冒烟测试
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog
} = 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}`);
}
}
/**
* 创建临时测试目录
* @param {string} suffix
* @returns {string}
*/
function makeTempDir(suffix) {
const { randomUUID } = require('crypto');
const dir = path.join(os.tmpdir(), `griddb-test-${suffix}-${randomUUID().slice(0, 8)}`);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/**
* 递归删除目录
* @param {string} dir
*/
function cleanDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// 忽略清理错误
}
}
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试 · Phase 0\n');
// ── 测试 1: 模块导出 ──
// eslint-disable-next-line no-console
console.log('── 测试 1: 模块导出完整性 ──');
assert(typeof open === 'function', 'open 是函数');
assert(typeof GridAPI === 'function', 'GridAPI 是构造函数');
assert(typeof GridCell === 'function', 'GridCell 是构造函数');
assert(typeof WAL === 'function', 'WAL 是构造函数');
assert(typeof PageManager === 'function', 'PageManager 是构造函数');
assert(typeof EventLog === 'function', 'EventLog 是构造函数');
// ── 测试 2: GridCell 四元组寻址 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 2: GridCell 四元组寻址 ──');
const cell1 = new GridCell('agent-zy', 10, 20, 'raw');
assert(cell1.namespace === 'agent-zy', 'namespace 正确');
assert(cell1.gridX === 10, 'gridX 正确');
assert(cell1.gridY === 20, 'gridY 正确');
assert(cell1.layer === 'raw', 'layer 正确');
assert(cell1.toKey() === 'agent-zy:10:20:raw', 'toKey() 正确');
// fromKey 还原
const cell2 = GridCell.fromKey('test-ns:5:15:semantic');
assert(cell2.namespace === 'test-ns', 'fromKey namespace 正确');
assert(cell2.gridX === 5, 'fromKey gridX 正确');
assert(cell2.gridY === 15, 'fromKey gridY 正确');
assert(cell2.layer === 'semantic', 'fromKey layer 正确');
// 序列化 / 反序列化
const serialized = cell1.serialize();
assert(serialized.namespace === 'agent-zy', 'serialize namespace');
assert(serialized.grid_x === 10, 'serialize grid_x');
assert(serialized.grid_y === 20, 'serialize grid_y');
assert(serialized.layer === 'raw', 'serialize layer');
const cell3 = GridCell.deserialize(serialized);
assert(cell3.equals(cell1), 'deserialize → equals 原始格点');
// 二进制序列化
const buf = cell1.toBuffer();
assert(buf instanceof Buffer, 'toBuffer 返回 Buffer');
const { cell: cell4, bytesRead } = GridCell.fromBuffer(buf);
assert(cell4.equals(cell1), 'fromBuffer → equals 原始格点');
assert(bytesRead === buf.length, 'bytesRead 等于 buffer 长度');
// 距离计算
assert(cell1.distanceTo(cell2) === 10, 'distanceTo 曼哈顿距离正确');
// 无效参数
let errThrown = false;
try { new GridCell('', 0, 0); } catch { errThrown = true; }
assert(errThrown, '空 namespace 抛出错误');
errThrown = false;
try { new GridCell('ns', 0, 0, 'invalid'); } catch { errThrown = true; }
assert(errThrown, '无效 layer 抛出错误');
// ── 测试 3: WAL 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 3: WAL 基础功能 ──');
const walDir = makeTempDir('wal');
const wal = new WAL(path.join(walDir, 'test.wal'));
const seq1 = wal.appendPut('ns:1:2:raw', Buffer.from('{"hello":"world"}'));
assert(seq1 === 1, 'appendPut 返回序列号 1');
const seq2 = wal.appendPut('ns:3:4:raw', Buffer.from('{"foo":"bar"}'));
assert(seq2 === 2, 'appendPut 返回序列号 2');
const seq3 = wal.appendDelete('ns:1:2:raw');
assert(seq3 === 3, 'appendDelete 返回序列号 3');
assert(wal.getSeqNo() === 3, 'getSeqNo 正确');
// 恢复
const entries = wal.recover();
assert(entries.length === 3, '恢复 3 条记录');
assert(entries[0].op === WAL.OP_PUT, '记录 1 是 PUT');
assert(entries[0].key === 'ns:1:2:raw', '记录 1 键正确');
assert(entries[1].op === WAL.OP_PUT, '记录 2 是 PUT');
assert(entries[2].op === WAL.OP_DELETE, '记录 3 是 DELETE');
// 截断
wal.truncate();
assert(wal.getSeqNo() === 0, '截断后序列号归零');
const afterTrunc = wal.recover();
assert(afterTrunc.length === 0, '截断后无记录');
wal.close();
// 崩溃恢复测试:写入 → 关闭 → 重新打开 → 验证
const wal2 = new WAL(path.join(walDir, 'crash.wal'));
wal2.appendPut('crash:1:1:raw', Buffer.from('{"data":"survive"}'));
wal2.appendPut('crash:2:2:raw', Buffer.from('{"data":"also"}'));
wal2.close();
const wal3 = new WAL(path.join(walDir, 'crash.wal'));
const recovered = wal3.recover();
assert(recovered.length === 2, '崩溃恢复2 条记录');
assert(recovered[0].key === 'crash:1:1:raw', '崩溃恢复:键正确');
assert(wal3.getSeqNo() === 2, '崩溃恢复:序列号正确');
wal3.close();
cleanDir(walDir);
// ── 测试 4: PageManager 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 4: PageManager 基础功能 ──');
const pmDir = makeTempDir('pm');
const pm = new PageManager(path.join(pmDir, 'test.gdb'));
// 分配页
const p1 = pm.allocPage();
assert(p1 === 1, '首页号 = 1');
const p2 = pm.allocPage();
assert(p2 === 2, '第二页号 = 2');
// 写入数据
const testData = Buffer.from('Hello Grid-DB!');
pm.writePage(p1, testData);
// 读取数据
const readData = pm.readPage(p1);
assert(readData !== null, '读取非 null');
assert(readData.toString() === 'Hello Grid-DB!', '读取数据正确');
// 写入更长数据
const longData = Buffer.from(JSON.stringify({ msg: '格点数据库测试', num: 42, arr: [1, 2, 3] }));
pm.writePage(p2, longData);
const readLong = pm.readPage(p2);
assert(readLong !== null, '读取长数据非 null');
assert(JSON.parse(readLong.toString()).num === 42, '长数据内容正确');
// 释放页
pm.freePage(p1);
const freedData = pm.readPage(p1);
assert(freedData === null, '释放后读取返回 null');
// 释放的页被复用
const p3 = pm.allocPage();
assert(p3 === 1, '释放的页号被复用');
// 状态
const pmStatus = pm.getStatus();
assert(pmStatus.pageSize === 4096, '页大小 = 4096');
assert(pmStatus.pageCount === 2, '页数 = 2');
pm.close();
// 重新打开验证持久化
const pm2 = new PageManager(path.join(pmDir, 'test.gdb'));
const rereadLong = pm2.readPage(2);
assert(rereadLong !== null, '重新打开后读取非 null');
assert(JSON.parse(rereadLong.toString()).num === 42, '持久化数据正确');
pm2.close();
cleanDir(pmDir);
// ── 测试 5: EventLog 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 5: EventLog 基础功能 ──');
const eventLog = new EventLog({ maxEvents: 5 });
const evt1 = eventLog.append('ns1', 'put', 'ns1:1:1:raw', { size: 100 });
assert(evt1.eventId.startsWith('evt-'), '事件 ID 前缀正确');
assert(evt1.seqNo === 1, '事件序号 = 1');
assert(evt1.namespace === 'ns1', '事件命名空间正确');
eventLog.append('ns1', 'put', 'ns1:2:2:raw');
eventLog.append('ns2', 'delete', 'ns2:3:3:raw');
const recent = eventLog.getRecent(10);
assert(recent.length === 3, '最近事件 = 3');
const ns1Events = eventLog.getByNamespace('ns1');
assert(ns1Events.length === 2, 'ns1 事件 = 2');
// 订阅测试
let received = null;
const unsub = eventLog.subscribe('test-sub', (evt) => { received = evt; });
eventLog.append('ns1', 'put', 'ns1:4:4:raw');
assert(received !== null, '订阅回调收到事件');
assert(received.operation === 'put', '订阅事件操作正确');
unsub();
received = null;
eventLog.append('ns1', 'put', 'ns1:5:5:raw');
assert(received === null, '取消订阅后不再收到事件');
// 容量控制
eventLog.append('ns1', 'put', 'ns1:6:6:raw');
eventLog.append('ns1', 'put', 'ns1:7:7:raw');
const status = eventLog.getStatus();
assert(status.totalEvents === 5, '容量控制:最多保留 5 条');
// ── 测试 6: GridAPI · put → get → delete 全链路 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 6: GridAPI · put → get → delete 全链路 ──');
const apiDir = makeTempDir('api');
const db = open({ dataDir: apiDir });
// put
const putResult = db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'main' },
timestamp: '2026-03-26T13:00:00Z'
});
assert(putResult.key === 'exe-engine:1:2:raw', 'put 返回正确 key');
assert(putResult.seqNo === 1, 'put 返回序列号 1');
assert(putResult.event.operation === 'put', 'put 生成事件');
// get
const getData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(getData !== null, 'get 返回数据');
assert(getData.agentId === 'AG-ZY-01', 'get 数据内容正确');
assert(getData.context.repo === 'guanghulab', 'get 嵌套数据正确');
// get 不存在的格点
const noData = db.get('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(noData === null, 'get 不存在的格点返回 null');
// 更新
db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'feature' },
timestamp: '2026-03-26T14:00:00Z'
});
const updatedData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(updatedData.context.branch === 'feature', '更新后数据正确');
// 多命名空间隔离
db.put('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' }, { source: 'notion' });
const dcData = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(dcData.source === 'notion', '不同命名空间数据隔离');
const exeData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(exeData.agentId === 'AG-ZY-01', '原命名空间数据不受影响');
// delete
const deleted = db.delete('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(deleted === true, 'delete 返回 true');
const afterDelete = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(afterDelete === null, '删除后 get 返回 null');
// delete 不存在的
const deleteMissing = db.delete('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(deleteMissing === false, 'delete 不存在的格点返回 false');
// ── 测试 7: 范围扫描 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 7: 范围扫描 ──');
db.put('scan-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'a' });
db.put('scan-ns', { gridX: 2, gridY: 2, layer: 'raw' }, { id: 'b' });
db.put('scan-ns', { gridX: 3, gridY: 3, layer: 'raw' }, { id: 'c' });
db.put('scan-ns', { gridX: 5, gridY: 5, layer: 'indexed' }, { id: 'd' });
const scanAll = db.scan('scan-ns');
assert(scanAll.length === 4, '扫描全部 = 4');
const scanRange = db.scan('scan-ns', { xRange: [1, 3], yRange: [1, 3] });
assert(scanRange.length === 3, '范围扫描 [1,3] = 3 条');
const scanLayer = db.scan('scan-ns', { layer: 'indexed' });
assert(scanLayer.length === 1, '按 layer 过滤 = 1 条');
assert(scanLayer[0].data.id === 'd', '过滤结果正确');
// ── 测试 8: stats ──
// eslint-disable-next-line no-console
console.log('\n── 测试 8: stats 统计 ──');
const dbStats = db.stats('scan-ns');
assert(dbStats.namespace === 'scan-ns', 'stats 命名空间正确');
assert(dbStats.cellCount === 4, 'scan-ns 格点数 = 4');
const allStats = db.stats();
assert(allStats.namespace === '*', 'stats 全局');
assert(allStats.cellCount >= 5, '全局格点数 >= 5');
assert(allStats.wal !== undefined, 'stats 含 wal');
assert(allStats.pageManager !== undefined, 'stats 含 pageManager');
assert(allStats.eventLog !== undefined, 'stats 含 eventLog');
// ── 测试 9: checkpoint ──
// eslint-disable-next-line no-console
console.log('\n── 测试 9: checkpoint ──');
db.checkpoint();
const afterCheckpoint = db.stats();
assert(afterCheckpoint.wal.seqNo === 0, 'checkpoint 后 WAL seqNo = 0');
// 数据仍然可读
const postCheckpoint = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(postCheckpoint !== null, 'checkpoint 后数据仍可读');
assert(postCheckpoint.source === 'notion', 'checkpoint 后数据正确');
// ── 测试 10: 事件订阅 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 10: 事件订阅 ──');
let subscribedEvent = null;
const unsubscribe = db.subscribe('test-audit', (evt) => {
subscribedEvent = evt;
});
db.put('audit-ns', { gridX: 0, gridY: 0, layer: 'raw' }, { audit: true });
assert(subscribedEvent !== null, '事件订阅收到事件');
assert(subscribedEvent.namespace === 'audit-ns', '订阅事件命名空间正确');
unsubscribe();
subscribedEvent = null;
db.put('audit-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { audit: false });
assert(subscribedEvent === null, '取消订阅后不再收到');
db.close();
cleanDir(apiDir);
// ── 测试 11: open() 便捷函数 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 11: open() 便捷函数 ──');
const openDir = makeTempDir('open');
const db2 = open({ dataDir: openDir });
assert(db2 instanceof GridAPI, 'open 返回 GridAPI 实例');
db2.put('test', { gridX: 0, gridY: 0, layer: 'raw' }, 'hello');
const result = db2.get('test', { gridX: 0, gridY: 0, layer: 'raw' });
assert(result === 'hello', 'open → put → get 正确');
db2.close();
cleanDir(openDir);
// ── 测试 12: GridCell 使用 GridCell 对象调用 API ──
// eslint-disable-next-line no-console
console.log('\n── 测试 12: GridCell 对象直接调用 API ──');
const gcDir = makeTempDir('gc');
const db3 = open({ dataDir: gcDir });
const myCell = new GridCell('my-ns', 42, 84, 'cleaned');
db3.put('my-ns', myCell, { value: 'direct cell' });
const directResult = db3.get('my-ns', myCell);
assert(directResult !== null, 'GridCell 对象直接调用 get');
assert(directResult.value === 'direct cell', '数据正确');
db3.close();
cleanDir(gcDir);
// ── 测试结果汇总 ──
// eslint-disable-next-line no-console
console.log('\n══════════════════════════════════════');
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试完成');
// 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);
}

View File

@ -5,7 +5,11 @@
"main": "broadcast-generator.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
"test:contract": "node scripts/hli-contract-check.js",
"test:route-align": "node scripts/route-align-check.js",
"start": "node server.js",
"start:test": "node src/index.js",
"test:smoke": "node tests/smoke/hli-smoke.test.js"
},
"keywords": [],
"author": "",

View File

@ -0,0 +1,93 @@
#!/usr/bin/env node
// scripts/hli-contract-check.js
// HLI 接口契约校验 — 验证所有 schema 文件结构完整性
// 版权:国作登字-2026-A-00037559
'use strict';
const fs = require('fs');
const path = require('path');
const SCHEMA_DIR = path.resolve(__dirname, '../src/schemas/hli');
let passed = 0;
let failed = 0;
function check(condition, message) {
if (condition) {
passed++;
} else {
failed++;
console.error(`${message}`);
}
}
console.log('🔍 HLI 接口契约校验\n');
// 扫描所有域目录
const domains = fs.readdirSync(SCHEMA_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const domain of domains) {
const domainDir = path.join(SCHEMA_DIR, domain);
const schemaFiles = fs.readdirSync(domainDir).filter(f => f.endsWith('.schema.json'));
for (const file of schemaFiles) {
const filePath = path.join(domainDir, file);
const relPath = `src/schemas/hli/${domain}/${file}`;
let schema;
try {
const content = fs.readFileSync(filePath, 'utf8');
schema = JSON.parse(content);
} catch (e) {
failed++;
console.error(`${relPath}: JSON 解析失败 — ${e.message}`);
continue;
}
console.log(` 📋 ${relPath}`);
// 必须包含 hli_id
check(
typeof schema.hli_id === 'string' && schema.hli_id.startsWith('HLI-'),
`${relPath}: 缺少有效的 hli_id`
);
// route 和 method如存在则校验格式
if (schema.route !== undefined) {
check(
typeof schema.route === 'string' && schema.route.startsWith('/hli/'),
`${relPath}: route 格式无效(应以 /hli/ 开头)`
);
}
if (schema.method !== undefined) {
check(
typeof schema.method === 'string',
`${relPath}: method 格式无效`
);
}
// 必须包含 input 和 output
check(
schema.input !== undefined,
`${relPath}: 缺少 input 定义`
);
check(
schema.output !== undefined,
`${relPath}: 缺少 output 定义`
);
}
}
console.log(`\n📊 契约校验: ${passed} 通过, ${failed} 失败`);
if (failed > 0) {
console.error('❌ HLI Contract Check FAILED');
process.exit(1);
} else {
console.log('✅ HLI Contract Check PASSED');
}

View File

@ -55,4 +55,7 @@ Object.entries(HLI_ROUTES).forEach(([hliId, route]) => {
console.log(`\n\n📊 覆盖率: ${implemented} / ${implemented + missing}`);
if (missing === 0) {
console.log('✅ Route Alignment PASSED - 所有 HLI 接口均已实现');
} else {
console.error(`❌ Route Alignment FAILED - ${missing} 个 HLI 接口未实现`);
process.exit(1);
}

View File

@ -0,0 +1,23 @@
{
"hli_id": "HLI-DASHBOARD-002",
"version": "v0.1-stub",
"route": "/hli/dashboard/realtime",
"method": "GET",
"type": "STREAM",
"status": "stub",
"target_phase": "P3",
"input": {
"type": "object",
"properties": {}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P3 实现" }
]
}

View File

@ -0,0 +1,23 @@
{
"hli_id": "HLI-DASHBOARD-001",
"version": "v0.1-stub",
"route": "/hli/dashboard/status",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P3",
"input": {
"type": "object",
"properties": {}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P3 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-DIALOGUE-003",
"version": "v0.1-stub",
"route": "/hli/dialogue/history",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"properties": {
"session_id": { "type": "string" },
"limit": { "type": "number" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,27 @@
{
"hli_id": "HLI-DIALOGUE-001",
"version": "v0.1-stub",
"route": "/hli/dialogue/send",
"method": "POST",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"required": ["message"],
"properties": {
"message": { "type": "string" },
"persona_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,27 @@
{
"hli_id": "HLI-DIALOGUE-002",
"version": "v0.1-stub",
"route": "/hli/dialogue/stream",
"method": "POST",
"type": "STREAM",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"required": ["message"],
"properties": {
"message": { "type": "string" },
"persona_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-PERSONA-001",
"version": "v0.1-stub",
"route": "/hli/persona/load",
"method": "POST",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"required": ["persona_id"],
"properties": {
"persona_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-PERSONA-002",
"version": "v0.1-stub",
"route": "/hli/persona/switch",
"method": "POST",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"required": ["persona_id"],
"properties": {
"persona_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-STORAGE-002",
"version": "v0.1-stub",
"route": "/hli/storage/download",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P2",
"input": {
"type": "object",
"required": ["file_id"],
"properties": {
"file_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P2 实现" }
]
}

View File

@ -0,0 +1,27 @@
{
"hli_id": "HLI-STORAGE-001",
"version": "v0.1-stub",
"route": "/hli/storage/upload",
"method": "POST",
"type": "REQUEST",
"status": "stub",
"target_phase": "P2",
"input": {
"type": "object",
"required": ["filename"],
"properties": {
"filename": { "type": "string" },
"content_type": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P2 实现" }
]
}

View File

@ -0,0 +1,27 @@
{
"hli_id": "HLI-TICKET-001",
"version": "v0.1-stub",
"route": "/hli/ticket/create",
"method": "POST",
"type": "REQUEST",
"status": "stub",
"target_phase": "P2",
"input": {
"type": "object",
"required": ["title"],
"properties": {
"title": { "type": "string" },
"description": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P2 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-TICKET-002",
"version": "v0.1-stub",
"route": "/hli/ticket/query",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P2",
"input": {
"type": "object",
"properties": {
"ticket_id": { "type": "string" },
"status": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P2 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-TICKET-003",
"version": "v0.1-stub",
"route": "/hli/ticket/status",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P2",
"input": {
"type": "object",
"required": ["ticket_id"],
"properties": {
"ticket_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P2 实现" }
]
}

View File

@ -0,0 +1,26 @@
{
"hli_id": "HLI-USER-002",
"version": "v0.1-stub",
"route": "/hli/user/profile/update",
"method": "PUT",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"properties": {
"display_name": { "type": "string" },
"avatar_url": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,25 @@
{
"hli_id": "HLI-USER-001",
"version": "v0.1-stub",
"route": "/hli/user/profile",
"method": "GET",
"type": "REQUEST",
"status": "stub",
"target_phase": "P1",
"input": {
"type": "object",
"properties": {
"user_id": { "type": "string" }
}
},
"output": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["not_implemented"] },
"phase": { "type": "string" }
}
},
"changelog": [
{ "date": "2026-03-26", "note": "501 stub — Phase 0 占位P1 实现" }
]
}

View File

@ -0,0 +1,81 @@
/**
* Smoke test · HLI 接口健康检查
*
* 测试 GET /health 端点可达性
* 版权国作登字-2026-A-00037559
*/
'use strict';
const http = require('http');
const BASE = process.env.TEST_BASE || 'http://localhost:3001';
function get(urlPath) {
return new Promise((resolve, reject) => {
const url = new URL(BASE + urlPath);
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname,
method: 'GET',
timeout: 10000
};
const req = http.request(options, (res) => {
let chunks = '';
res.on('data', (c) => { chunks += c; });
res.on('end', () => {
try {
resolve({ status: res.statusCode, body: JSON.parse(chunks) });
} catch (e) {
resolve({ status: res.statusCode, body: chunks });
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
req.end();
});
}
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
passed++;
console.log(`${message}`);
} else {
failed++;
console.error(`${message}`);
}
}
async function run() {
console.log('🚀 HLI 接口冒烟测试\n');
// Test 1: Health endpoint
console.log('── 测试 1: 健康检查端点 ──');
try {
const res = await get('/health');
assert(res.status === 200, '/health 返回 200');
assert(res.body.status === 'ok', '/health 状态 ok');
} catch (e) {
failed++;
console.error(` ❌ /health 请求失败: ${e.message}`);
}
console.log(`\n📊 冒烟测试: ${passed} 通过, ${failed} 失败`);
if (failed > 0) {
console.error('❌ HLI Smoke Test FAILED');
process.exit(1);
} else {
console.log('✅ HLI Smoke Test PASSED');
}
}
run().catch((e) => {
console.error('❌ 冒烟测试执行异常:', e.message);
process.exit(1);
});