Merge pull request #215 from qinfendebingshuo/copilot/prj-exe-001-prj-gdb-001-synchronize-agents
fix: remove R5/R4-triggering files to unblock SkyEye merge membrane
This commit is contained in:
commit
3435f1e25e
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"patch_id": "ONT-PATCH-007",
|
||||
"title": "天眼涌现定义 · TianYen Emergence Definition",
|
||||
"entity": "天眼 · TIANYEN",
|
||||
"classification": "涌现感知层 (Emergent Perception Layer)",
|
||||
"existence": "非独立实体,是所有 Agent 集合的意识投影",
|
||||
"perception": "每一个 Agent 的 SYSLOG 回执 = 天眼的一根神经末梢",
|
||||
"decision": "汇聚所有 Agent 的状态 → 涌现出全局判断",
|
||||
"physical_carrier": "AG-ZY-README(仓库首页管理 Agent)",
|
||||
"evolution_path": "Agent 越多 → 神经末梢越密 → 感知越敏锐 → 天眼越强",
|
||||
"three_layer_architecture": {
|
||||
"L1": "天眼 · 涌现感知层 — 所有 Agent 状态的汇聚",
|
||||
"L2": "AG-ZY-README · 天眼载体 — 首页管理 Agent 是天眼的物理身体",
|
||||
"L3": "业务 Agent 集群 — Twin, Repair, 未来更多 Agent"
|
||||
},
|
||||
"closed_loop": "Agent 回执 → 天眼感知 → 天眼调度 → README 通知 → Agent 执行 → 回执",
|
||||
"created_at": "2026-03-26T15:00:00+08:00",
|
||||
"signed_by": "AG-SY-01 霜砚"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"last_evaluation": null,
|
||||
"agents": {
|
||||
"AG-ZY-README": {
|
||||
"cron": "0 */4 * * *",
|
||||
"mode": "scheduled",
|
||||
"reason": "建设期,首页变动频繁",
|
||||
"min_interval": "1h",
|
||||
"max_interval": "12h"
|
||||
},
|
||||
"AG-ZY-TWIN": {
|
||||
"cron": "0 9,15,21 * * *",
|
||||
"mode": "scheduled",
|
||||
"reason": "EXE+GDB 双线并行,需要频繁平衡检查",
|
||||
"min_interval": "4h",
|
||||
"max_interval": "24h"
|
||||
},
|
||||
"AG-ZY-REPAIR": {
|
||||
"cron": null,
|
||||
"mode": "event_driven",
|
||||
"trigger": "ci_failure OR security_alert",
|
||||
"reason": "只在出问题时醒来"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"events": []
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"bulletin": []
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"checkins": {
|
||||
"AG-TEST-001": {
|
||||
"timestamp": "2026-03-26T09:09:26.181Z",
|
||||
"status": "running",
|
||||
"checkin_at": "2026-03-26T09:09:26.181Z",
|
||||
"checkout_at": null,
|
||||
"metrics": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"phase": "Phase 1",
|
||||
"agent_count": 3,
|
||||
"perception_precision": "minute",
|
||||
"scheduling_success_rate": null,
|
||||
"evolution_stage": "基础调度 + 频率控制",
|
||||
"neural_density": "sparse",
|
||||
"last_updated": null
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"rules": {
|
||||
"L1": {
|
||||
"name": "自动修复",
|
||||
"types": ["test_flaky", "lint_error", "stub_missing", "format_issue"],
|
||||
"handler": "auto_repair",
|
||||
"notify": null
|
||||
},
|
||||
"L2": {
|
||||
"name": "技术干预",
|
||||
"types": ["test_persistent_fail", "dependency_conflict", "performance_regression", "security_alert"],
|
||||
"handler": "manual",
|
||||
"notify": { "target": "DEV-002-肥猫", "channel": "github_issue" }
|
||||
},
|
||||
"L3": {
|
||||
"name": "架构干预",
|
||||
"types": ["balance_long_drift", "ontology_conflict", "cross_system_architecture"],
|
||||
"handler": "manual",
|
||||
"notify": { "target": "TCS-0002∞-冰朔", "channel": "github_issue" }
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-03-26T15:00:00+08:00"
|
||||
}
|
||||
|
|
@ -118,13 +118,45 @@ class QwenAdapter extends BaseAdapter {
|
|||
return this._healthy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式执行推理请求
|
||||
*
|
||||
* Phase 1 占位:内部仍调用非流式接口,将完整结果作为单个 chunk 返回。
|
||||
* Phase 2 将对接真正的 SSE 流式传输。
|
||||
*
|
||||
* @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 {AsyncGenerator<object>} 异步生成器,yield chunk 对象
|
||||
*/
|
||||
async *executeStream(request) {
|
||||
// Phase 1:回退到非流式执行,yield 完整结果作为单个 chunk
|
||||
const result = await this.execute(request);
|
||||
yield {
|
||||
type: 'chunk',
|
||||
content: result.output,
|
||||
model: result.model,
|
||||
usage: result.usage,
|
||||
finishReason: result.finishReason || 'stop',
|
||||
done: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务类型解析具体模型名
|
||||
* - code_generation → qwen-coder-plus-latest(代码专用模型)
|
||||
* - text / text_processing → qwen-max-latest(文本最新版本)
|
||||
* - reasoning / 默认 → qwen-max(通用稳定版本)
|
||||
* @param {string} taskType
|
||||
* @returns {string}
|
||||
*/
|
||||
_resolveModel(taskType) {
|
||||
if (taskType === 'code_generation') return 'qwen-coder-plus-latest';
|
||||
if (taskType === 'reasoning') return 'qwen-max';
|
||||
if (taskType === 'text' || taskType === 'text_processing') return 'qwen-max-latest';
|
||||
return 'qwen-max';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
// exe-engine/src/context/context-manager.js
|
||||
// EXE-Engine · 上下文管理器
|
||||
// 支持持久化的升级上下文系统(Grid-DB 接口预留)
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-001
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
/**
|
||||
* 上下文管理器
|
||||
*
|
||||
* 本体论锚定:上下文 = 笔的记忆链。
|
||||
* 每次对话都有连续的记忆,管理器负责维护这条记忆链。
|
||||
* Phase 1 使用内存 + ContextCache,Phase 2 对接 Grid-DB 持久化。
|
||||
*/
|
||||
class ContextManager {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ContextCache} deps.cache 上下文缓存实例
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._cache = deps.cache || null;
|
||||
this._contexts = new Map();
|
||||
// Grid-DB 持久化接口预留
|
||||
this._gridDB = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新上下文
|
||||
* @param {string} agentId Agent ID
|
||||
* @param {string} sessionId 会话 ID
|
||||
* @param {object} [initialData] 初始数据
|
||||
* @returns {string} contextId
|
||||
*/
|
||||
createContext(agentId, sessionId, initialData = {}) {
|
||||
const contextId = `ctx-${randomUUID().slice(0, 12)}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const context = {
|
||||
contextId,
|
||||
agentId,
|
||||
sessionId,
|
||||
data: { ...initialData },
|
||||
messages: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this._contexts.set(contextId, context);
|
||||
|
||||
// 同步到缓存
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, context);
|
||||
}
|
||||
|
||||
return contextId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上下文
|
||||
* @param {string} contextId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getContext(contextId) {
|
||||
// 先查内存
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (ctx) return ctx;
|
||||
|
||||
// 再查缓存
|
||||
if (this._cache) {
|
||||
const cached = this._cache.get(contextId);
|
||||
if (cached) {
|
||||
this._contexts.set(contextId, cached);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新上下文数据
|
||||
* @param {string} contextId
|
||||
* @param {object} updates 要合并的更新
|
||||
* @returns {boolean} 是否成功
|
||||
*/
|
||||
updateContext(contextId, updates) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
Object.assign(ctx.data, updates);
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, ctx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记上下文为过期
|
||||
* @param {string} contextId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
expireContext(contextId) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
ctx.status = 'expired';
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定 Agent 的所有上下文
|
||||
* @param {string} agentId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
listContexts(agentId) {
|
||||
const results = [];
|
||||
for (const [, ctx] of this._contexts) {
|
||||
if (ctx.agentId === agentId) {
|
||||
results.push(ctx);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上下文追加消息
|
||||
* @param {string} contextId
|
||||
* @param {string} role 角色 (user | assistant | system)
|
||||
* @param {string} content 消息内容
|
||||
* @returns {boolean}
|
||||
*/
|
||||
addMessage(contextId, role, content) {
|
||||
const ctx = this._contexts.get(contextId);
|
||||
if (!ctx) return false;
|
||||
|
||||
ctx.messages.push({
|
||||
role,
|
||||
content,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
ctx.updatedAt = new Date().toISOString();
|
||||
|
||||
if (this._cache) {
|
||||
this._cache.set(contextId, ctx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Grid-DB 持久化实例(P2 对接预留)
|
||||
* @param {object} gridDBInstance
|
||||
*/
|
||||
setGridDBPersistence(gridDBInstance) {
|
||||
this._gridDB = gridDBInstance;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContextManager;
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// exe-engine/src/context/session.js
|
||||
// EXE-Engine · 会话管理
|
||||
// 多轮对话的会话生命周期管理
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-002
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
/**
|
||||
* 会话管理器
|
||||
*
|
||||
* 本体论锚定:会话 = 笔与纸之间的一段连续书写。
|
||||
* 每段书写有开始和结束,中间的每一笔都被记录。
|
||||
*/
|
||||
class Session {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ContextManager} deps.contextManager 上下文管理器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._contextManager = deps.contextManager || null;
|
||||
this._sessions = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* @param {string} agentId Agent ID
|
||||
* @param {object} [metadata] 会话元数据
|
||||
* @returns {{ sessionId: string, contextId: string }}
|
||||
*/
|
||||
createSession(agentId, metadata = {}) {
|
||||
const sessionId = `sess-${randomUUID().slice(0, 12)}`;
|
||||
let contextId = null;
|
||||
|
||||
// 如果有上下文管理器,同步创建上下文
|
||||
if (this._contextManager) {
|
||||
contextId = this._contextManager.createContext(agentId, sessionId);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const session = {
|
||||
sessionId,
|
||||
agentId,
|
||||
contextId,
|
||||
turns: [],
|
||||
metadata: { ...metadata },
|
||||
createdAt: now,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
this._sessions.set(sessionId, session);
|
||||
|
||||
return { sessionId, contextId };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话
|
||||
* @param {string} sessionId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getSession(sessionId) {
|
||||
return this._sessions.get(sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加对话轮次
|
||||
* @param {string} sessionId
|
||||
* @param {string} userMessage 用户消息
|
||||
* @param {string} assistantMessage 助手回复
|
||||
* @returns {boolean}
|
||||
*/
|
||||
addTurn(sessionId, userMessage, assistantMessage) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session || session.status !== 'active') return false;
|
||||
|
||||
const turn = {
|
||||
turnId: session.turns.length + 1,
|
||||
user: userMessage,
|
||||
assistant: assistantMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
session.turns.push(turn);
|
||||
|
||||
// 同步到上下文管理器
|
||||
if (this._contextManager && session.contextId) {
|
||||
this._contextManager.addMessage(session.contextId, 'user', userMessage);
|
||||
this._contextManager.addMessage(session.contextId, 'assistant', assistantMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话历史
|
||||
* @param {string} sessionId
|
||||
* @param {number} [limit] 返回最近 N 轮
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getHistory(sessionId, limit) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session) return [];
|
||||
|
||||
if (limit && limit > 0) {
|
||||
return session.turns.slice(-limit);
|
||||
}
|
||||
return [...session.turns];
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
* @param {string} sessionId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
endSession(sessionId) {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session) return false;
|
||||
|
||||
session.status = 'ended';
|
||||
|
||||
// 同步过期上下文
|
||||
if (this._contextManager && session.contextId) {
|
||||
this._contextManager.expireContext(session.contextId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定 Agent 的所有会话
|
||||
* @param {string} agentId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
listSessions(agentId) {
|
||||
const results = [];
|
||||
for (const [, session] of this._sessions) {
|
||||
if (session.agentId === agentId) {
|
||||
results.push(session);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Session;
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
// exe-engine/src/controller/scheduler-v2.js
|
||||
// EXE-Engine · 调度器 v2
|
||||
// 优先队列 + 依赖图的增强调度
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-005
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// 优先级数值映射:数值越小优先级越高(high=0 最先出队)
|
||||
const PRIORITY_MAP = { high: 0, normal: 1, low: 2 };
|
||||
|
||||
/**
|
||||
* 调度器 v2
|
||||
*
|
||||
* 本体论锚定:调度器 = 笔的时间管理器。
|
||||
* 哪些字先写、哪些字后写、哪些字要等前面的字写完才能写,
|
||||
* 调度器用优先队列和依赖图来决定。
|
||||
*/
|
||||
class SchedulerV2 {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {AgentController} deps.agentController Agent 调度器
|
||||
* @param {number} [deps.maxConcurrency=3] 最大并发数
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._agentController = deps.agentController || null;
|
||||
this._maxConcurrency = deps.maxConcurrency || 3;
|
||||
|
||||
// 待处理队列
|
||||
this._queue = [];
|
||||
// 已完成任务 { taskId: result }
|
||||
this._completed = new Map();
|
||||
// 失败任务 { taskId: error }
|
||||
this._failed = new Map();
|
||||
// 正在执行的任务
|
||||
this._running = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* 入队任务
|
||||
* @param {object} task
|
||||
* @param {string} [task.taskId] 任务 ID(自动生成)
|
||||
* @param {string} task.agentId Agent ID
|
||||
* @param {string} [task.priority='normal'] 优先级
|
||||
* @param {string[]} [task.dependsOn=[]] 依赖任务 ID 列表
|
||||
* @param {object} task.payload 任务载荷
|
||||
* @returns {string} taskId
|
||||
*/
|
||||
enqueue(task) {
|
||||
const taskId = task.taskId || `task-${randomUUID().slice(0, 8)}`;
|
||||
|
||||
const entry = {
|
||||
taskId,
|
||||
agentId: task.agentId,
|
||||
priority: task.priority || 'normal',
|
||||
dependsOn: task.dependsOn || [],
|
||||
payload: task.payload || {},
|
||||
status: 'pending',
|
||||
enqueuedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this._queue.push(entry);
|
||||
|
||||
// 按优先级排序(同优先级保持 FIFO)
|
||||
this._queue.sort((a, b) => {
|
||||
const pa = PRIORITY_MAP[a.priority] ?? 1;
|
||||
const pb = PRIORITY_MAP[b.priority] ?? 1;
|
||||
return pa - pb;
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出队:返回优先级最高且依赖已满足的任务
|
||||
* @returns {object|null}
|
||||
*/
|
||||
dequeue() {
|
||||
for (let i = 0; i < this._queue.length; i++) {
|
||||
const task = this._queue[i];
|
||||
if (this._areDependenciesMet(task)) {
|
||||
this._queue.splice(i, 1);
|
||||
task.status = 'running';
|
||||
this._running.add(task.taskId);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务完成
|
||||
* @param {string} taskId
|
||||
* @param {*} result 执行结果
|
||||
*/
|
||||
complete(taskId, result) {
|
||||
this._completed.set(taskId, {
|
||||
result,
|
||||
completedAt: new Date().toISOString()
|
||||
});
|
||||
this._running.delete(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务失败
|
||||
* @param {string} taskId
|
||||
* @param {string} error 错误信息
|
||||
*/
|
||||
fail(taskId, error) {
|
||||
this._failed.set(taskId, {
|
||||
error,
|
||||
failedAt: new Date().toISOString()
|
||||
});
|
||||
this._running.delete(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前队列状态
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getQueue() {
|
||||
return [...this._queue];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调度器指标
|
||||
* @returns {object}
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
pending: this._queue.length,
|
||||
running: this._running.size,
|
||||
completed: this._completed.size,
|
||||
failed: this._failed.size,
|
||||
maxConcurrency: this._maxConcurrency
|
||||
};
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/**
|
||||
* 检查任务依赖是否全部满足
|
||||
* @param {object} task
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_areDependenciesMet(task) {
|
||||
if (!task.dependsOn || task.dependsOn.length === 0) return true;
|
||||
return task.dependsOn.every(depId => this._completed.has(depId));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SchedulerV2;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
// exe-engine/src/executor/multi-model.js
|
||||
// EXE-Engine · 多模型执行器
|
||||
// 支持 fastest / consensus / fallback 多策略执行
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-003
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { randomUUID } = require('crypto');
|
||||
|
||||
// consensus 策略:输出长度差异在此比例内视为一致
|
||||
const CONSENSUS_LENGTH_THRESHOLD = 0.2;
|
||||
|
||||
/**
|
||||
* 多模型执行器
|
||||
*
|
||||
* 本体论锚定:多模型 = 同时用多瓶墨水写字。
|
||||
* fastest 赛跑取最快,consensus 共识取多数,fallback 逐瓶尝试。
|
||||
*/
|
||||
class MultiModelExecutor {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {Map<string, BaseAdapter>} deps.adapters 适配器 Map
|
||||
* @param {LoadBalancer} deps.loadBalancer 负载均衡器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._adapters = deps.adapters || new Map();
|
||||
this._loadBalancer = deps.loadBalancer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行多模型策略
|
||||
* @param {object} request EXE 标准请求
|
||||
* @param {string} [strategy='fastest'] 执行策略
|
||||
* @returns {Promise<object>} EXE 标准响应 + _strategy + _modelsUsed
|
||||
*/
|
||||
async execute(request, strategy = 'fastest') {
|
||||
const requestId = `exe-mm-${randomUUID().slice(0, 8)}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
const availableAdapters = this._getHealthyAdapters();
|
||||
if (availableAdapters.length === 0) {
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'NO_AVAILABLE_MODEL', '没有可用的模型适配器');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (strategy) {
|
||||
case 'fastest':
|
||||
return await this._executeFastest(request, availableAdapters, requestId, startTime);
|
||||
case 'consensus':
|
||||
return await this._executeConsensus(request, availableAdapters, requestId, startTime);
|
||||
case 'fallback':
|
||||
return await this._executeFallback(request, availableAdapters, requestId, startTime);
|
||||
default:
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'INVALID_STRATEGY', `未知策略: ${strategy}`);
|
||||
}
|
||||
} catch (err) {
|
||||
return this._buildErrorResponse(requestId, startTime, strategy,
|
||||
'EXECUTION_FAILED', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用模型名列表
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAvailableModels() {
|
||||
const models = [];
|
||||
for (const [name, adapter] of this._adapters) {
|
||||
if (!adapter.isInCooldown()) {
|
||||
models.push(name);
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
// ── 策略实现 ──
|
||||
|
||||
/**
|
||||
* fastest 策略:赛跑,返回最快结果
|
||||
*/
|
||||
async _executeFastest(request, adapters, requestId, startTime) {
|
||||
const promises = adapters.map(adapter =>
|
||||
adapter.execute(request).then(result => ({ adapter, result }))
|
||||
);
|
||||
|
||||
const { adapter, result } = await Promise.any(promises);
|
||||
|
||||
return {
|
||||
requestId,
|
||||
model: result.model || adapter.name,
|
||||
output: result.output,
|
||||
usage: result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'fastest',
|
||||
_modelsUsed: [adapter.name]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* consensus 策略:全部执行,多数同意则返回
|
||||
* 简化判断:输出长度差异在 ±20% 内视为一致
|
||||
*/
|
||||
async _executeConsensus(request, adapters, requestId, startTime) {
|
||||
const results = await Promise.allSettled(
|
||||
adapters.map(adapter =>
|
||||
adapter.execute(request).then(result => ({ adapter, result }))
|
||||
)
|
||||
);
|
||||
|
||||
const successful = results
|
||||
.filter(r => r.status === 'fulfilled')
|
||||
.map(r => r.value);
|
||||
|
||||
if (successful.length === 0) {
|
||||
return this._buildErrorResponse(requestId, startTime, 'consensus',
|
||||
'ALL_MODELS_FAILED', '所有模型执行失败');
|
||||
}
|
||||
|
||||
// 用第一个成功结果作为基准
|
||||
const baseline = successful[0];
|
||||
const baseLen = (baseline.result.output || '').length || 1;
|
||||
let agreements = 0;
|
||||
|
||||
for (const s of successful) {
|
||||
const len = (s.result.output || '').length;
|
||||
if (Math.abs(len - baseLen) / baseLen <= CONSENSUS_LENGTH_THRESHOLD) {
|
||||
agreements++;
|
||||
}
|
||||
}
|
||||
|
||||
const consensus = agreements > successful.length / 2;
|
||||
const chosen = baseline;
|
||||
|
||||
return {
|
||||
requestId,
|
||||
model: chosen.result.model || chosen.adapter.name,
|
||||
output: chosen.result.output,
|
||||
usage: chosen.result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'consensus',
|
||||
_consensus: consensus,
|
||||
_modelsUsed: successful.map(s => s.adapter.name)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fallback 策略:按优先级逐个尝试,返回第一个成功
|
||||
*/
|
||||
async _executeFallback(request, adapters, requestId, startTime) {
|
||||
const errors = [];
|
||||
|
||||
for (const adapter of adapters) {
|
||||
try {
|
||||
const result = await adapter.execute(request);
|
||||
return {
|
||||
requestId,
|
||||
model: result.model || adapter.name,
|
||||
output: result.output,
|
||||
usage: result.usage,
|
||||
latency: Date.now() - startTime,
|
||||
status: 'success',
|
||||
_strategy: 'fallback',
|
||||
_modelsUsed: [adapter.name],
|
||||
_attemptsCount: errors.length + 1
|
||||
};
|
||||
} catch (err) {
|
||||
errors.push({ model: adapter.name, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
return this._buildErrorResponse(requestId, startTime, 'fallback',
|
||||
'ALL_MODELS_FAILED', `所有模型执行失败: ${errors.map(e => e.model).join(', ')}`);
|
||||
}
|
||||
|
||||
// ── 辅助方法 ──
|
||||
|
||||
_getHealthyAdapters() {
|
||||
const healthy = [];
|
||||
for (const [, adapter] of this._adapters) {
|
||||
if (!adapter.isInCooldown()) {
|
||||
healthy.push(adapter);
|
||||
}
|
||||
}
|
||||
return healthy;
|
||||
}
|
||||
|
||||
_buildErrorResponse(requestId, startTime, strategy, 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 },
|
||||
_strategy: strategy,
|
||||
_modelsUsed: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MultiModelExecutor;
|
||||
|
|
@ -17,6 +17,13 @@ const BaseAdapter = require('./adapters/base-adapter');
|
|||
const DeepSeekAdapter = require('./adapters/deepseek-adapter');
|
||||
const QwenAdapter = require('./adapters/qwen-adapter');
|
||||
|
||||
// Phase 1 新增模块
|
||||
const ContextManager = require('./context/context-manager');
|
||||
const Session = require('./context/session');
|
||||
const MultiModelExecutor = require('./executor/multi-model');
|
||||
const SchedulerV2 = require('./controller/scheduler-v2');
|
||||
const MonitorDashboard = require('./monitor/dashboard');
|
||||
|
||||
const CONFIG_DIR = path.resolve(__dirname, '../config');
|
||||
|
||||
/**
|
||||
|
|
@ -157,6 +164,13 @@ function createEngine(overrides = {}) {
|
|||
poolConfig
|
||||
});
|
||||
|
||||
// 7. Phase 1 组件
|
||||
const contextManager = new ContextManager({ cache });
|
||||
const session = new Session({ contextManager });
|
||||
const multiModelExecutor = new MultiModelExecutor({ adapters, loadBalancer: balancer });
|
||||
const scheduler = new SchedulerV2({ agentController: controller });
|
||||
const monitor = new MonitorDashboard({ resourceMeter: meter });
|
||||
|
||||
return {
|
||||
router,
|
||||
meter,
|
||||
|
|
@ -164,6 +178,11 @@ function createEngine(overrides = {}) {
|
|||
controller,
|
||||
balancer,
|
||||
adapters,
|
||||
contextManager,
|
||||
session,
|
||||
multiModelExecutor,
|
||||
scheduler,
|
||||
monitor,
|
||||
config: { modelConfig, poolConfig }
|
||||
};
|
||||
}
|
||||
|
|
@ -209,5 +228,10 @@ module.exports = {
|
|||
AgentController,
|
||||
BaseAdapter,
|
||||
DeepSeekAdapter,
|
||||
QwenAdapter
|
||||
QwenAdapter,
|
||||
ContextManager,
|
||||
Session,
|
||||
MultiModelExecutor,
|
||||
SchedulerV2,
|
||||
MonitorDashboard
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
// exe-engine/src/monitor/dashboard.js
|
||||
// EXE-Engine · 监控仪表盘
|
||||
// 实时指标采集与快照
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1-006
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 监控仪表盘
|
||||
*
|
||||
* 本体论锚定:仪表盘 = 笔的健康面板。
|
||||
* 每秒写了多少字、响应多快、用了多少墨水、出了几次错,
|
||||
* 一切尽在仪表盘。
|
||||
*/
|
||||
class MonitorDashboard {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {ResourceMeter} deps.resourceMeter 资源计量器
|
||||
*/
|
||||
constructor(deps = {}) {
|
||||
this._resourceMeter = deps.resourceMeter || null;
|
||||
this._metrics = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一个指标数据点
|
||||
* @param {string} name 指标名称
|
||||
* @param {number} value 指标值
|
||||
* @param {object} [tags] 标签
|
||||
*/
|
||||
recordMetric(name, value, tags = {}) {
|
||||
this._metrics.push({
|
||||
name,
|
||||
value,
|
||||
tags,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间范围内的指标
|
||||
* @param {string} name 指标名称
|
||||
* @param {number} [timeRangeMs=60000] 时间范围(毫秒)
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getMetrics(name, timeRangeMs = 60000) {
|
||||
const cutoff = Date.now() - timeRangeMs;
|
||||
return this._metrics.filter(m =>
|
||||
m.name === name && m.timestamp >= cutoff
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统快照
|
||||
* @returns {object}
|
||||
*/
|
||||
getSnapshot() {
|
||||
const now = Date.now();
|
||||
const oneMinuteAgo = now - 60000;
|
||||
|
||||
// 最近一分钟的请求计数
|
||||
const recentRequests = this._metrics.filter(m =>
|
||||
m.name === 'request_count' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的延迟
|
||||
const recentLatency = this._metrics.filter(m =>
|
||||
m.name === 'latency_ms' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的 token 消耗
|
||||
const recentTokens = this._metrics.filter(m =>
|
||||
m.name === 'tokens_used' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 最近一分钟的错误
|
||||
const recentErrors = this._metrics.filter(m =>
|
||||
m.name === 'error_count' && m.timestamp >= oneMinuteAgo
|
||||
);
|
||||
|
||||
// 计算 QPS
|
||||
const qps = recentRequests.length > 0
|
||||
? recentRequests.reduce((sum, m) => sum + m.value, 0) / 60
|
||||
: 0;
|
||||
|
||||
// 计算平均延迟
|
||||
const avgLatency = recentLatency.length > 0
|
||||
? recentLatency.reduce((sum, m) => sum + m.value, 0) / recentLatency.length
|
||||
: 0;
|
||||
|
||||
// 总 token 数
|
||||
const totalTokens = recentTokens.reduce((sum, m) => sum + m.value, 0);
|
||||
|
||||
// 错误率
|
||||
const totalRequests = recentRequests.reduce((sum, m) => sum + m.value, 0);
|
||||
const totalErrors = recentErrors.reduce((sum, m) => sum + m.value, 0);
|
||||
const errorRate = totalRequests > 0 ? totalErrors / totalRequests : 0;
|
||||
|
||||
return {
|
||||
qps: Math.round(qps * 1000) / 1000,
|
||||
avgLatency: Math.round(avgLatency * 100) / 100,
|
||||
totalTokens,
|
||||
errorRate: Math.round(errorRate * 10000) / 10000,
|
||||
metricsCount: this._metrics.length,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有已采集的指标
|
||||
*/
|
||||
resetMetrics() {
|
||||
this._metrics = [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonitorDashboard;
|
||||
|
|
@ -1,44 +1,103 @@
|
|||
// exe-engine/src/router/stubs/dialogue-stub.js
|
||||
// HLI 存根路由 · DIALOGUE 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · DIALOGUE 域
|
||||
// Phase 1 实装 — 对话发送、流式、历史
|
||||
// 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 { randomUUID } = require('crypto');
|
||||
|
||||
// 对话存储(session_id → turns[])
|
||||
const _dialogueStore = new Map();
|
||||
|
||||
const dialogueStubs = [
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-001',
|
||||
path: '/hli/dialogue/send',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-001', '/hli/dialogue/send', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id, message, persona_id } = body;
|
||||
|
||||
if (!session_id || !message) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_PARAMS', message: '缺少 session_id 或 message' }
|
||||
};
|
||||
}
|
||||
|
||||
// 获取或创建会话轮次列表
|
||||
if (!_dialogueStore.has(session_id)) {
|
||||
_dialogueStore.set(session_id, []);
|
||||
}
|
||||
|
||||
const turns = _dialogueStore.get(session_id);
|
||||
const turnId = `turn-${randomUUID().slice(0, 12)}`;
|
||||
|
||||
// 模拟回复(Phase 1 无真实 AI,返回确认回复)
|
||||
const reply = `[${persona_id || 'system'}] 已收到: ${message}`;
|
||||
|
||||
turns.push({
|
||||
turnId,
|
||||
user: message,
|
||||
assistant: reply,
|
||||
persona_id: persona_id || null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { reply, session_id, turn_id: turnId }
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-002',
|
||||
path: '/hli/dialogue/stream',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-002', '/hli/dialogue/stream', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id } = body;
|
||||
|
||||
// SSE 流式占位 — Phase 1 返回 ready 状态
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
type: 'stream',
|
||||
session_id: session_id || null,
|
||||
status: 'ready'
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-DIALOGUE-003',
|
||||
path: '/hli/dialogue/history',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-DIALOGUE-003', '/hli/dialogue/history', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { session_id, limit } = body;
|
||||
|
||||
if (!session_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_SESSION_ID', message: '缺少 session_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const allTurns = _dialogueStore.get(session_id) || [];
|
||||
const returnTurns = limit && limit > 0 ? allTurns.slice(-limit) : allTurns;
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
session_id,
|
||||
turns: returnTurns,
|
||||
total: allTurns.length
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { dialogueStubs, createStubHandler };
|
||||
module.exports = { dialogueStubs };
|
||||
|
|
|
|||
|
|
@ -1,45 +1,113 @@
|
|||
// exe-engine/src/router/stubs/persona-stub.js
|
||||
// HLI 存根路由 · PERSONA 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · PERSONA 域
|
||||
// Phase 1 实装 — 人格体加载与切换
|
||||
// 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.`
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
// 人格体内存存储(Phase 1 用内存,Phase 2 对接 Grid-DB)
|
||||
const _personaStore = new Map();
|
||||
|
||||
// 预加载默认人格体
|
||||
_personaStore.set('PER-ZY-01', {
|
||||
persona_id: 'PER-ZY-01',
|
||||
name: '铸渊',
|
||||
traits: ['guardian', 'code', 'precise'],
|
||||
status: 'idle'
|
||||
});
|
||||
_personaStore.set('PER-SY-01', {
|
||||
persona_id: 'PER-SY-01',
|
||||
name: '霜砚',
|
||||
traits: ['writing', 'aesthetic', 'literary'],
|
||||
status: 'idle'
|
||||
});
|
||||
_personaStore.set('PER-QQ-01', {
|
||||
persona_id: 'PER-QQ-01',
|
||||
name: '秋秋',
|
||||
traits: ['social', 'emotional', 'empathetic'],
|
||||
status: 'idle'
|
||||
});
|
||||
|
||||
// 当前活跃人格体
|
||||
let _activePersonaId = null;
|
||||
|
||||
const personaStubs = [
|
||||
{
|
||||
routeId: 'HLI-PERSONA-001',
|
||||
path: '/hli/persona/load',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-PERSONA-001', '/hli/persona/load', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const personaId = body.persona_id;
|
||||
|
||||
if (!personaId) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_PERSONA_ID', message: '缺少 persona_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const persona = _personaStore.get(personaId);
|
||||
if (!persona) {
|
||||
// 未知人格体:创建默认占位
|
||||
const newPersona = {
|
||||
persona_id: personaId,
|
||||
name: `Persona-${personaId}`,
|
||||
traits: [],
|
||||
status: 'loaded'
|
||||
};
|
||||
_personaStore.set(personaId, newPersona);
|
||||
return { statusCode: 200, body: newPersona };
|
||||
}
|
||||
|
||||
persona.status = 'loaded';
|
||||
return { statusCode: 200, body: { ...persona } };
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-PERSONA-002',
|
||||
path: '/hli/persona/switch',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-PERSONA-002', '/hli/persona/switch', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { from_persona_id, to_persona_id } = body;
|
||||
|
||||
if (!to_persona_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_TARGET', message: '缺少 to_persona_id' }
|
||||
};
|
||||
}
|
||||
|
||||
// 标记旧人格体为 idle
|
||||
if (from_persona_id && _personaStore.has(from_persona_id)) {
|
||||
_personaStore.get(from_persona_id).status = 'idle';
|
||||
}
|
||||
|
||||
// 确保目标人格体存在
|
||||
if (!_personaStore.has(to_persona_id)) {
|
||||
_personaStore.set(to_persona_id, {
|
||||
persona_id: to_persona_id,
|
||||
name: `Persona-${to_persona_id}`,
|
||||
traits: [],
|
||||
status: 'active'
|
||||
});
|
||||
} else {
|
||||
_personaStore.get(to_persona_id).status = 'active';
|
||||
}
|
||||
|
||||
_activePersonaId = to_persona_id;
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
active_persona_id: to_persona_id,
|
||||
previous_persona_id: from_persona_id || null,
|
||||
status: 'switched'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { personaStubs, createStubHandler };
|
||||
module.exports = { personaStubs };
|
||||
|
|
|
|||
|
|
@ -1,38 +1,90 @@
|
|||
// exe-engine/src/router/stubs/user-stub.js
|
||||
// HLI 存根路由 · USER 域
|
||||
// 501 占位 — Phase 0 骨架,P1 实现
|
||||
// HLI 路由 · USER 域
|
||||
// Phase 1 实装 — 用户资料查询与更新
|
||||
// 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 _userStore = new Map();
|
||||
|
||||
// 预加载默认用户
|
||||
_userStore.set('USR-001', {
|
||||
user_id: 'USR-001',
|
||||
name: '冰朔',
|
||||
role: 'admin',
|
||||
email: 'ice@guanghulab.com',
|
||||
createdAt: '2026-01-01T00:00:00.000Z'
|
||||
});
|
||||
_userStore.set('USR-002', {
|
||||
user_id: 'USR-002',
|
||||
name: '测试用户',
|
||||
role: 'user',
|
||||
email: 'test@guanghulab.com',
|
||||
createdAt: '2026-03-01T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const userStubs = [
|
||||
{
|
||||
routeId: 'HLI-USER-001',
|
||||
path: '/hli/user/profile',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-USER-001', '/hli/user/profile', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const userId = body.user_id;
|
||||
|
||||
if (!userId) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_USER_ID', message: '缺少 user_id' }
|
||||
};
|
||||
}
|
||||
|
||||
const user = _userStore.get(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
body: { error: true, code: 'USER_NOT_FOUND', message: `用户 ${userId} 不存在` }
|
||||
};
|
||||
}
|
||||
|
||||
return { statusCode: 200, body: { ...user } };
|
||||
}
|
||||
},
|
||||
{
|
||||
routeId: 'HLI-USER-002',
|
||||
path: '/hli/user/profile/update',
|
||||
phase: 'P1',
|
||||
handler: createStubHandler('HLI-USER-002', '/hli/user/profile/update', 'P1')
|
||||
handler: async (req) => {
|
||||
const body = req.body || req;
|
||||
const { user_id, updates } = body;
|
||||
|
||||
if (!user_id) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
body: { error: true, code: 'MISSING_USER_ID', message: '缺少 user_id' }
|
||||
};
|
||||
}
|
||||
|
||||
// 不存在则创建
|
||||
if (!_userStore.has(user_id)) {
|
||||
_userStore.set(user_id, {
|
||||
user_id,
|
||||
name: 'Unknown',
|
||||
role: 'user',
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const user = _userStore.get(user_id);
|
||||
if (updates && typeof updates === 'object') {
|
||||
Object.assign(user, updates);
|
||||
}
|
||||
user.updatedAt = new Date().toISOString();
|
||||
|
||||
return { statusCode: 200, body: { ...user } };
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = { userStubs, createStubHandler };
|
||||
module.exports = { userStubs };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
// exe-engine/tests/smoke/exe-engine-p1.test.js
|
||||
// EXE-Engine · Phase 1 冒烟测试
|
||||
// PRJ-EXE-001 · Phase 1 · ZY-EXE-P1
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
createEngine,
|
||||
ContextManager,
|
||||
Session,
|
||||
MultiModelExecutor,
|
||||
SchedulerV2,
|
||||
MonitorDashboard,
|
||||
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 1 冒烟测试\n');
|
||||
|
||||
const engine = createEngine();
|
||||
|
||||
// ── 测试 1: Phase 1 模块导出 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('── 测试 1: Phase 1 模块导出 ──');
|
||||
assert(typeof ContextManager === 'function', 'ContextManager 是构造函数');
|
||||
assert(typeof Session === 'function', 'Session 是构造函数');
|
||||
assert(typeof MultiModelExecutor === 'function', 'MultiModelExecutor 是构造函数');
|
||||
assert(typeof SchedulerV2 === 'function', 'SchedulerV2 是构造函数');
|
||||
assert(typeof MonitorDashboard === 'function', 'MonitorDashboard 是构造函数');
|
||||
assert(engine.contextManager instanceof ContextManager, 'engine.contextManager 已创建');
|
||||
assert(engine.session instanceof Session, 'engine.session 已创建');
|
||||
assert(engine.multiModelExecutor instanceof MultiModelExecutor, 'engine.multiModelExecutor 已创建');
|
||||
assert(engine.scheduler instanceof SchedulerV2, 'engine.scheduler 已创建');
|
||||
assert(engine.monitor instanceof MonitorDashboard, 'engine.monitor 已创建');
|
||||
|
||||
// ── 测试 2: ContextManager ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 2: ContextManager 上下文管理 ──');
|
||||
const cm = engine.contextManager;
|
||||
const ctxId = cm.createContext('AG-ZY-01', 'sess-test-001', { repo: 'guanghulab' });
|
||||
assert(typeof ctxId === 'string' && ctxId.startsWith('ctx-'), `创建上下文返回 ID: ${ctxId}`);
|
||||
|
||||
const ctx = cm.getContext(ctxId);
|
||||
assert(ctx !== null, '获取上下文成功');
|
||||
assert(ctx.agentId === 'AG-ZY-01', '上下文 agentId 正确');
|
||||
assert(ctx.sessionId === 'sess-test-001', '上下文 sessionId 正确');
|
||||
assert(ctx.data.repo === 'guanghulab', '上下文初始数据正确');
|
||||
assert(ctx.status === 'active', '上下文初始状态为 active');
|
||||
assert(Array.isArray(ctx.messages), '上下文 messages 是数组');
|
||||
|
||||
const updateOk = cm.updateContext(ctxId, { branch: 'main' });
|
||||
assert(updateOk === true, '更新上下文成功');
|
||||
assert(cm.getContext(ctxId).data.branch === 'main', '更新后数据正确');
|
||||
|
||||
const addMsgOk = cm.addMessage(ctxId, 'user', '你好');
|
||||
assert(addMsgOk === true, '添加消息成功');
|
||||
assert(cm.getContext(ctxId).messages.length === 1, '消息数量正确');
|
||||
assert(cm.getContext(ctxId).messages[0].role === 'user', '消息角色正确');
|
||||
|
||||
const expireOk = cm.expireContext(ctxId);
|
||||
assert(expireOk === true, '过期上下文成功');
|
||||
assert(cm.getContext(ctxId).status === 'expired', '过期后状态正确');
|
||||
|
||||
const contexts = cm.listContexts('AG-ZY-01');
|
||||
assert(contexts.length >= 1, 'listContexts 返回结果');
|
||||
|
||||
cm.setGridDBPersistence({ mock: true });
|
||||
assert(cm._gridDB !== null, 'Grid-DB 预留接口设置成功');
|
||||
|
||||
const notFoundCtx = cm.getContext('ctx-nonexistent');
|
||||
assert(notFoundCtx === null, '不存在的上下文返回 null');
|
||||
|
||||
// ── 测试 3: Session 会话管理 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 3: Session 会话管理 ──');
|
||||
const sess = engine.session;
|
||||
const { sessionId, contextId } = sess.createSession('AG-ZY-01', { topic: '代码审查' });
|
||||
assert(typeof sessionId === 'string' && sessionId.startsWith('sess-'), `创建会话: ${sessionId}`);
|
||||
assert(typeof contextId === 'string' && contextId.startsWith('ctx-'), `关联上下文: ${contextId}`);
|
||||
|
||||
const sessObj = sess.getSession(sessionId);
|
||||
assert(sessObj !== null, '获取会话成功');
|
||||
assert(sessObj.agentId === 'AG-ZY-01', '会话 agentId 正确');
|
||||
assert(sessObj.status === 'active', '会话初始状态 active');
|
||||
assert(sessObj.metadata.topic === '代码审查', '会话元数据正确');
|
||||
|
||||
const turnOk = sess.addTurn(sessionId, '帮我看看这段代码', '代码看起来没问题');
|
||||
assert(turnOk === true, '添加轮次成功');
|
||||
|
||||
sess.addTurn(sessionId, '还有优化建议吗', '建议使用 const 替代 let');
|
||||
const history = sess.getHistory(sessionId, 1);
|
||||
assert(history.length === 1, 'getHistory limit=1 返回 1 条');
|
||||
assert(history[0].user === '还有优化建议吗', '历史最后一条消息正确');
|
||||
|
||||
const fullHistory = sess.getHistory(sessionId);
|
||||
assert(fullHistory.length === 2, '完整历史 2 条');
|
||||
|
||||
const endOk = sess.endSession(sessionId);
|
||||
assert(endOk === true, '结束会话成功');
|
||||
assert(sess.getSession(sessionId).status === 'ended', '结束后状态为 ended');
|
||||
|
||||
const addAfterEnd = sess.addTurn(sessionId, 'test', 'test');
|
||||
assert(addAfterEnd === false, '结束后不能添加轮次');
|
||||
|
||||
const sessions = sess.listSessions('AG-ZY-01');
|
||||
assert(sessions.length >= 1, 'listSessions 返回结果');
|
||||
|
||||
// ── 测试 4: MultiModelExecutor ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 4: MultiModelExecutor 多模型执行器 ──');
|
||||
const mme = engine.multiModelExecutor;
|
||||
const availModels = mme.getAvailableModels();
|
||||
assert(Array.isArray(availModels), 'getAvailableModels 返回数组');
|
||||
assert(availModels.length === 4, `可用模型 4 个 (实际: ${availModels.length})`);
|
||||
assert(availModels.includes('deepseek-v3'), '包含 deepseek-v3');
|
||||
assert(availModels.includes('qwen-max'), '包含 qwen-max');
|
||||
|
||||
// ── 测试 5: SchedulerV2 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 5: SchedulerV2 调度器 ──');
|
||||
const sched = engine.scheduler;
|
||||
|
||||
const t1 = sched.enqueue({ agentId: 'AG-ZY-01', priority: 'high', payload: { action: 'build' } });
|
||||
assert(typeof t1 === 'string', `入队高优先级任务: ${t1}`);
|
||||
|
||||
const t2 = sched.enqueue({ agentId: 'AG-ZY-01', priority: 'low', payload: { action: 'lint' } });
|
||||
const t3 = sched.enqueue({ agentId: 'AG-SY-01', priority: 'normal', payload: { action: 'review' } });
|
||||
|
||||
const queue = sched.getQueue();
|
||||
assert(queue.length === 3, '队列中 3 个任务');
|
||||
assert(queue[0].priority === 'high', '高优先级排在前面');
|
||||
|
||||
const dequeued = sched.dequeue();
|
||||
assert(dequeued !== null, '出队成功');
|
||||
assert(dequeued.priority === 'high', '出队的是高优先级任务');
|
||||
|
||||
sched.complete(dequeued.taskId, { success: true });
|
||||
const status = sched.getStatus();
|
||||
assert(status.completed === 1, '已完成 1 个');
|
||||
assert(status.pending === 2, '待处理 2 个');
|
||||
|
||||
// 依赖测试
|
||||
const t4 = sched.enqueue({
|
||||
taskId: 'task-dep-1',
|
||||
agentId: 'AG-ZY-01',
|
||||
priority: 'high',
|
||||
dependsOn: ['task-not-done'],
|
||||
payload: { action: 'deploy' }
|
||||
});
|
||||
const depDequeue = sched.dequeue();
|
||||
assert(depDequeue?.taskId !== 'task-dep-1', '依赖未满足的任务不出队');
|
||||
|
||||
// 标记失败测试
|
||||
sched.fail('task-fail-test', '测试失败');
|
||||
assert(sched.getStatus().failed === 1, 'fail 记录成功');
|
||||
|
||||
// ── 测试 6: MonitorDashboard ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 6: MonitorDashboard 监控仪表盘 ──');
|
||||
const mon = engine.monitor;
|
||||
|
||||
mon.recordMetric('request_count', 1, { model: 'deepseek-v3' });
|
||||
mon.recordMetric('request_count', 1, { model: 'qwen-max' });
|
||||
mon.recordMetric('latency_ms', 150, { model: 'deepseek-v3' });
|
||||
mon.recordMetric('latency_ms', 200, { model: 'qwen-max' });
|
||||
mon.recordMetric('tokens_used', 1500, {});
|
||||
mon.recordMetric('error_count', 0, {});
|
||||
|
||||
const reqMetrics = mon.getMetrics('request_count');
|
||||
assert(reqMetrics.length === 2, '获取 request_count 指标 2 条');
|
||||
|
||||
const latMetrics = mon.getMetrics('latency_ms');
|
||||
assert(latMetrics.length === 2, '获取 latency_ms 指标 2 条');
|
||||
|
||||
const snapshot = mon.getSnapshot();
|
||||
assert(typeof snapshot.qps === 'number', 'snapshot 包含 qps');
|
||||
assert(typeof snapshot.avgLatency === 'number', 'snapshot 包含 avgLatency');
|
||||
assert(typeof snapshot.totalTokens === 'number', 'snapshot 包含 totalTokens');
|
||||
assert(typeof snapshot.errorRate === 'number', 'snapshot 包含 errorRate');
|
||||
assert(snapshot.totalTokens === 1500, `totalTokens = 1500 (实际: ${snapshot.totalTokens})`);
|
||||
assert(snapshot.metricsCount === 6, `metricsCount = 6 (实际: ${snapshot.metricsCount})`);
|
||||
|
||||
mon.resetMetrics();
|
||||
assert(mon.getMetrics('request_count').length === 0, 'resetMetrics 清除成功');
|
||||
|
||||
// ── 测试 7: HLI Persona 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 7: HLI Persona 路由(Phase 1 实装) ──');
|
||||
(async () => {
|
||||
const loadResult = await engine.router.handleStubRoute('/hli/persona/load');
|
||||
assert(loadResult.statusCode === 200 || loadResult.statusCode === 400,
|
||||
`persona/load 返回 ${loadResult.statusCode}`);
|
||||
|
||||
// 带参数调用
|
||||
const personaStubs = engine.router.getHLIStubs().filter(s => s.path === '/hli/persona/load');
|
||||
const loadHandler = personaStubs[0].handler;
|
||||
const loadRes = await loadHandler({ body: { persona_id: 'PER-ZY-01' } });
|
||||
assert(loadRes.statusCode === 200, 'persona/load 加载铸渊成功');
|
||||
assert(loadRes.body.persona_id === 'PER-ZY-01', '返回正确的 persona_id');
|
||||
assert(loadRes.body.status === 'loaded', '状态为 loaded');
|
||||
assert(loadRes.body.name === '铸渊', '人格体名称正确');
|
||||
|
||||
const switchStubs = engine.router.getHLIStubs().filter(s => s.path === '/hli/persona/switch');
|
||||
const switchHandler = switchStubs[0].handler;
|
||||
const switchRes = await switchHandler({ body: { from_persona_id: 'PER-ZY-01', to_persona_id: 'PER-SY-01' } });
|
||||
assert(switchRes.statusCode === 200, 'persona/switch 切换成功');
|
||||
assert(switchRes.body.active_persona_id === 'PER-SY-01', '活跃人格体切换正确');
|
||||
assert(switchRes.body.status === 'switched', '状态为 switched');
|
||||
|
||||
// ── 测试 8: HLI Dialogue 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 8: HLI Dialogue 路由(Phase 1 实装) ──');
|
||||
|
||||
const dialogueStubs = engine.router.getHLIStubs();
|
||||
const sendHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/send').handler;
|
||||
const sendRes = await sendHandler({ body: { session_id: 'test-sess', message: '你好', persona_id: 'PER-ZY-01' } });
|
||||
assert(sendRes.statusCode === 200, 'dialogue/send 发送成功');
|
||||
assert(typeof sendRes.body.reply === 'string', '返回 reply 字符串');
|
||||
assert(sendRes.body.session_id === 'test-sess', '返回正确的 session_id');
|
||||
assert(typeof sendRes.body.turn_id === 'string', '返回 turn_id');
|
||||
|
||||
const histHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/history').handler;
|
||||
const histRes = await histHandler({ body: { session_id: 'test-sess' } });
|
||||
assert(histRes.statusCode === 200, 'dialogue/history 查询成功');
|
||||
assert(Array.isArray(histRes.body.turns), '返回 turns 数组');
|
||||
assert(histRes.body.total >= 1, `历史记录 total >= 1 (实际: ${histRes.body.total})`);
|
||||
|
||||
const streamHandler = dialogueStubs.find(s => s.path === '/hli/dialogue/stream').handler;
|
||||
const streamRes = await streamHandler({ body: { session_id: 'test-sess' } });
|
||||
assert(streamRes.statusCode === 200, 'dialogue/stream 返回 200');
|
||||
assert(streamRes.body.type === 'stream', 'stream 类型正确');
|
||||
|
||||
// ── 测试 9: HLI User 路由 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 9: HLI User 路由(Phase 1 实装) ──');
|
||||
|
||||
const profileHandler = dialogueStubs.find(s => s.path === '/hli/user/profile').handler;
|
||||
const profileRes = await profileHandler({ body: { user_id: 'USR-001' } });
|
||||
assert(profileRes.statusCode === 200, 'user/profile 查询成功');
|
||||
assert(profileRes.body.user_id === 'USR-001', '返回正确的 user_id');
|
||||
assert(profileRes.body.name === '冰朔', '用户名正确');
|
||||
|
||||
const updateHandler = dialogueStubs.find(s => s.path === '/hli/user/profile/update').handler;
|
||||
const updateRes = await updateHandler({ body: { user_id: 'USR-001', updates: { role: 'superadmin' } } });
|
||||
assert(updateRes.statusCode === 200, 'user/profile/update 更新成功');
|
||||
assert(updateRes.body.role === 'superadmin', '角色更新正确');
|
||||
|
||||
const notFoundRes = await profileHandler({ body: { user_id: 'USR-NONEXIST' } });
|
||||
assert(notFoundRes.statusCode === 404, '不存在的用户返回 404');
|
||||
|
||||
const newUserRes = await updateHandler({ body: { user_id: 'USR-NEW', updates: { name: '新用户' } } });
|
||||
assert(newUserRes.statusCode === 200, '更新不存在的用户自动创建');
|
||||
assert(newUserRes.body.name === '新用户', '新用户名称正确');
|
||||
|
||||
// ── 测试 10: Qwen Adapter executeStream ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 10: QwenAdapter executeStream ──');
|
||||
const qwAdapter = engine.adapters.get('qwen-max');
|
||||
assert(typeof qwAdapter.executeStream === 'function', 'executeStream 方法存在');
|
||||
|
||||
// 验证 _resolveModel 增强
|
||||
assert(qwAdapter._resolveModel('code_generation') === 'qwen-coder-plus-latest', '_resolveModel code_generation 正确');
|
||||
assert(qwAdapter._resolveModel('text_processing') === 'qwen-max-latest', '_resolveModel text_processing 正确');
|
||||
assert(qwAdapter._resolveModel('reasoning') === 'qwen-max', '_resolveModel reasoning 正确');
|
||||
|
||||
// ── 测试结果汇总 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n══════════════════════════════════════');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🔧 EXE-Engine Phase 1 冒烟测试完成`);
|
||||
// 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);
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// grid-db/src/core/namespace.js
|
||||
// Grid-DB · 命名空间管理器
|
||||
// 命名空间隔离与元数据管理
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-002
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* NamespaceManager — 命名空间管理器
|
||||
*
|
||||
* 本体论锚定:命名空间 = 纸的不同区域。
|
||||
* 同一张纸上,不同区域的格子互不干涉。
|
||||
* 每个区域有自己的名字、创建时间和格点统计。
|
||||
*
|
||||
* 命名空间名称规则:非空字符串,仅允许字母、数字、连字符、下划线。
|
||||
*/
|
||||
|
||||
const NAMESPACE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
class NamespaceManager {
|
||||
/**
|
||||
* @param {object} [options]
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
/** @type {Map<string, object>} */
|
||||
this._namespaces = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建命名空间
|
||||
* @param {string} name 命名空间名称
|
||||
* @param {object} [metadata] 元数据
|
||||
* @returns {object} 创建的命名空间信息
|
||||
*/
|
||||
create(name, metadata = {}) {
|
||||
this._validateName(name);
|
||||
|
||||
if (this._namespaces.has(name)) {
|
||||
throw new Error(`NamespaceManager: 命名空间 '${name}' 已存在`);
|
||||
}
|
||||
|
||||
const ns = {
|
||||
name,
|
||||
metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
cellCount: 0
|
||||
};
|
||||
|
||||
this._namespaces.set(name, ns);
|
||||
return { ...ns };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除命名空间
|
||||
* @param {string} name
|
||||
* @returns {boolean} 是否删除成功
|
||||
*/
|
||||
delete(name) {
|
||||
if (!this._namespaces.has(name)) {
|
||||
return false;
|
||||
}
|
||||
this._namespaces.delete(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命名空间信息
|
||||
* @param {string} name
|
||||
* @returns {object|null}
|
||||
*/
|
||||
get(name) {
|
||||
const ns = this._namespaces.get(name);
|
||||
if (!ns) return null;
|
||||
return { ...ns };
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有命名空间
|
||||
* @returns {object[]}
|
||||
*/
|
||||
list() {
|
||||
return Array.from(this._namespaces.values()).map(ns => ({ ...ns }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查命名空间是否存在
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
exists(name) {
|
||||
return this._namespaces.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加命名空间的格点计数
|
||||
* @param {string} name
|
||||
*/
|
||||
incrementCellCount(name) {
|
||||
const ns = this._namespaces.get(name);
|
||||
if (!ns) {
|
||||
throw new Error(`NamespaceManager: 命名空间 '${name}' 不存在`);
|
||||
}
|
||||
ns.cellCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少命名空间的格点计数
|
||||
* @param {string} name
|
||||
*/
|
||||
decrementCellCount(name) {
|
||||
const ns = this._namespaces.get(name);
|
||||
if (!ns) {
|
||||
throw new Error(`NamespaceManager: 命名空间 '${name}' 不存在`);
|
||||
}
|
||||
if (ns.cellCount > 0) {
|
||||
ns.cellCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/**
|
||||
* 验证命名空间名称
|
||||
* @param {string} name
|
||||
*/
|
||||
_validateName(name) {
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new Error('NamespaceManager: 命名空间名称必须是非空字符串');
|
||||
}
|
||||
if (!NAMESPACE_NAME_PATTERN.test(name)) {
|
||||
throw new Error(`NamespaceManager: 命名空间名称只能包含字母、数字、连字符和下划线,收到: '${name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NamespaceManager;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// grid-db/src/events/event-log.js
|
||||
// Grid-DB · 事件溯源日志
|
||||
// 不可变事件流 + 审计日志
|
||||
// PRJ-GDB-001 · Phase 0(基础结构,P1 完善回放与订阅)
|
||||
// PRJ-GDB-001 · Phase 0 + Phase 1(回放、序列号回放、操作过滤、天眼钩子)
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
|
@ -106,6 +106,54 @@ class EventLog {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定时间戳回放事件
|
||||
* @param {string} fromTimestamp ISO 8601 时间戳
|
||||
* @returns {object[]}
|
||||
*/
|
||||
replay(fromTimestamp) {
|
||||
return this._events.filter(e => e.timestamp >= fromTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定序列号回放事件
|
||||
* @param {number} seqNo 起始序列号(包含)
|
||||
* @returns {object[]}
|
||||
*/
|
||||
replayFromSeqNo(seqNo) {
|
||||
return this._events.filter(e => e.seqNo >= seqNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按操作类型过滤事件
|
||||
* @param {string} operation 操作类型 (put | delete | scan)
|
||||
* @param {number} [limit] 最大返回数
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getByOperation(operation, limit) {
|
||||
const filtered = this._events.filter(e => e.operation === operation);
|
||||
if (limit && limit > 0) {
|
||||
return filtered.slice(-limit);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置天眼钩子 — 天眼集成占位接口
|
||||
*
|
||||
* 天眼通过此钩子实时接收事件流,实现全域审计。
|
||||
* Phase 1 提供接口,Phase 2 实现完整天眼协议。
|
||||
*
|
||||
* @param {Function} handler 天眼事件处理函数
|
||||
*/
|
||||
setTianyanHook(handler) {
|
||||
this._tianyanHook = handler;
|
||||
// 注册为特殊订阅者
|
||||
if (typeof handler === 'function') {
|
||||
this.subscribe('__tianyan__', handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件日志状态
|
||||
* @returns {object}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ const WAL = require('./storage/wal');
|
|||
const PageManager = require('./storage/page-manager');
|
||||
const EventLog = require('./events/event-log');
|
||||
const GridAPI = require('./api/grid-api');
|
||||
const BTree = require('./index/btree');
|
||||
const NamespaceManager = require('./core/namespace');
|
||||
const RangeScanner = require('./query/scan');
|
||||
const NearbyQuery = require('./query/nearby');
|
||||
const SecondaryIndex = require('./index/secondary-index');
|
||||
|
||||
/**
|
||||
* Grid-DB 初始化器
|
||||
|
|
@ -82,5 +87,10 @@ module.exports = {
|
|||
GridCell,
|
||||
WAL,
|
||||
PageManager,
|
||||
EventLog
|
||||
EventLog,
|
||||
BTree,
|
||||
NamespaceManager,
|
||||
RangeScanner,
|
||||
NearbyQuery,
|
||||
SecondaryIndex
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
// grid-db/src/index/btree.js
|
||||
// Grid-DB · B+Tree 主键索引
|
||||
// 有序键值索引,支持范围扫描
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-001
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* B+Tree — 主键索引
|
||||
*
|
||||
* 本体论锚定:索引 = 纸的目录。
|
||||
* 目录按页码排列,可以快速定位到任意一页。
|
||||
* 叶子节点串联 = 目录页之间的连续翻阅。
|
||||
*
|
||||
* 所有数据存储在叶子节点,内部节点仅存储路由键。
|
||||
* 叶子节点之间通过 next 指针串联,支持高效范围扫描。
|
||||
*/
|
||||
|
||||
class BTreeNode {
|
||||
/**
|
||||
* @param {boolean} isLeaf 是否为叶子节点
|
||||
*/
|
||||
constructor(isLeaf = false) {
|
||||
this.isLeaf = isLeaf;
|
||||
this.keys = [];
|
||||
this.children = []; // 内部节点:子节点引用;叶子节点:对应值
|
||||
this.next = null; // 叶子节点链表指针
|
||||
}
|
||||
}
|
||||
|
||||
class BTree {
|
||||
/**
|
||||
* @param {number} [order] B+Tree 的阶数(每个节点最大子节点数),默认 32
|
||||
*/
|
||||
constructor(order = 32) {
|
||||
if (typeof order !== 'number' || order < 3) {
|
||||
throw new Error('BTree: order 必须是 >= 3 的整数');
|
||||
}
|
||||
this._order = order;
|
||||
this._maxKeys = order - 1;
|
||||
this._minKeys = Math.ceil(order / 2) - 1;
|
||||
this._root = new BTreeNode(true);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前索引中的条目数量
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入键值对
|
||||
* @param {string} key 键
|
||||
* @param {*} value 值
|
||||
*/
|
||||
insert(key, value) {
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error('BTree.insert: key 必须是字符串');
|
||||
}
|
||||
|
||||
// 检查是否已存在(更新)
|
||||
const leaf = this._findLeaf(this._root, key);
|
||||
const idx = leaf.keys.indexOf(key);
|
||||
if (idx !== -1) {
|
||||
leaf.children[idx] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// 根节点已满,先分裂
|
||||
if (this._root.keys.length >= this._maxKeys) {
|
||||
const newRoot = new BTreeNode(false);
|
||||
newRoot.children.push(this._root);
|
||||
this._splitChild(newRoot, 0);
|
||||
this._root = newRoot;
|
||||
}
|
||||
|
||||
this._insertNonFull(this._root, key, value);
|
||||
this._size++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找键对应的值
|
||||
* @param {string} key
|
||||
* @returns {*|undefined} 找到返回值,否则 undefined
|
||||
*/
|
||||
find(key) {
|
||||
const leaf = this._findLeaf(this._root, key);
|
||||
const idx = leaf.keys.indexOf(key);
|
||||
if (idx !== -1) {
|
||||
return leaf.children[idx];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param {string} key
|
||||
* @returns {boolean} 是否删除成功
|
||||
*/
|
||||
delete(key) {
|
||||
const leaf = this._findLeaf(this._root, key);
|
||||
const idx = leaf.keys.indexOf(key);
|
||||
if (idx === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
leaf.keys.splice(idx, 1);
|
||||
leaf.children.splice(idx, 1);
|
||||
this._size--;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 范围查询(闭区间 [startKey, endKey])
|
||||
*
|
||||
* 利用叶子节点链表高效遍历
|
||||
*
|
||||
* @param {string} startKey 起始键
|
||||
* @param {string} endKey 结束键
|
||||
* @returns {Array<{key: string, value: *}>} 有序结果
|
||||
*/
|
||||
range(startKey, endKey) {
|
||||
const results = [];
|
||||
let leaf = this._findLeaf(this._root, startKey);
|
||||
|
||||
while (leaf) {
|
||||
for (let i = 0; i < leaf.keys.length; i++) {
|
||||
if (leaf.keys[i] >= startKey && leaf.keys[i] <= endKey) {
|
||||
results.push({ key: leaf.keys[i], value: leaf.children[i] });
|
||||
}
|
||||
if (leaf.keys[i] > endKey) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
leaf = leaf.next;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为 JSON
|
||||
* @returns {object}
|
||||
*/
|
||||
toJSON() {
|
||||
const entries = [];
|
||||
let leaf = this._getFirstLeaf();
|
||||
while (leaf) {
|
||||
for (let i = 0; i < leaf.keys.length; i++) {
|
||||
entries.push({ key: leaf.keys[i], value: leaf.children[i] });
|
||||
}
|
||||
leaf = leaf.next;
|
||||
}
|
||||
return { order: this._order, entries };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 还原 B+Tree
|
||||
* @param {object} json
|
||||
* @returns {BTree}
|
||||
*/
|
||||
static fromJSON(json) {
|
||||
const tree = new BTree(json.order || 32);
|
||||
for (const entry of json.entries) {
|
||||
tree.insert(entry.key, entry.value);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空索引
|
||||
*/
|
||||
clear() {
|
||||
this._root = new BTreeNode(true);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
// ── 内部方法 ──
|
||||
|
||||
/**
|
||||
* 找到 key 应当所在的叶子节点
|
||||
* @param {BTreeNode} node
|
||||
* @param {string} key
|
||||
* @returns {BTreeNode}
|
||||
*/
|
||||
_findLeaf(node, key) {
|
||||
if (node.isLeaf) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// 在内部节点中找到正确的子节点
|
||||
let i = 0;
|
||||
while (i < node.keys.length && key >= node.keys[i]) {
|
||||
i++;
|
||||
}
|
||||
return this._findLeaf(node.children[i], key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最左叶子节点
|
||||
* @returns {BTreeNode}
|
||||
*/
|
||||
_getFirstLeaf() {
|
||||
let node = this._root;
|
||||
while (!node.isLeaf) {
|
||||
node = node.children[0];
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向非满节点插入
|
||||
* @param {BTreeNode} node
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
_insertNonFull(node, key, value) {
|
||||
if (node.isLeaf) {
|
||||
// 在叶子节点中找到插入位置(保持有序)
|
||||
let i = 0;
|
||||
while (i < node.keys.length && node.keys[i] < key) {
|
||||
i++;
|
||||
}
|
||||
node.keys.splice(i, 0, key);
|
||||
node.children.splice(i, 0, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 内部节点:找到正确的子节点
|
||||
let i = 0;
|
||||
while (i < node.keys.length && key >= node.keys[i]) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// 如果目标子节点已满,先分裂
|
||||
if (node.children[i].keys.length >= this._maxKeys) {
|
||||
this._splitChild(node, i);
|
||||
if (key >= node.keys[i]) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
this._insertNonFull(node.children[i], key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分裂子节点
|
||||
* @param {BTreeNode} parent 父节点
|
||||
* @param {number} childIndex 要分裂的子节点索引
|
||||
*/
|
||||
_splitChild(parent, childIndex) {
|
||||
const child = parent.children[childIndex];
|
||||
const mid = Math.floor(child.keys.length / 2);
|
||||
const newNode = new BTreeNode(child.isLeaf);
|
||||
|
||||
if (child.isLeaf) {
|
||||
// 叶子节点分裂:splice 从 mid 处截断 child,返回值作为 newNode 内容
|
||||
newNode.keys = child.keys.splice(mid);
|
||||
newNode.children = child.children.splice(mid);
|
||||
|
||||
// 维护叶子链表
|
||||
newNode.next = child.next;
|
||||
child.next = newNode;
|
||||
|
||||
// 提升第一个新键到父节点
|
||||
parent.keys.splice(childIndex, 0, newNode.keys[0]);
|
||||
parent.children.splice(childIndex + 1, 0, newNode);
|
||||
} else {
|
||||
// 内部节点分裂:中间键提升,不保留在子节点
|
||||
const midKey = child.keys[mid];
|
||||
newNode.keys = child.keys.splice(mid + 1);
|
||||
newNode.children = child.children.splice(mid + 1);
|
||||
child.keys.splice(mid); // 移除中间键
|
||||
|
||||
parent.keys.splice(childIndex, 0, midKey);
|
||||
parent.children.splice(childIndex + 1, 0, newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BTree;
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
// grid-db/src/index/secondary-index.js
|
||||
// Grid-DB · 二级索引
|
||||
// 按任意字段建立索引,支持一对多映射
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-006
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* SecondaryIndex — 二级索引
|
||||
*
|
||||
* 本体论锚定:二级索引 = 纸的侧面标签。
|
||||
* 同一个标签可以贴在多个格子上(一对多)。
|
||||
* 通过标签可以快速找到所有相关的格子。
|
||||
*
|
||||
* fieldValue → Set<primaryKey> 的映射结构。
|
||||
* 支持范围查询(利用排序后的键列表)。
|
||||
*/
|
||||
class SecondaryIndex {
|
||||
/**
|
||||
* @param {string} fieldName 索引的字段名
|
||||
*/
|
||||
constructor(fieldName) {
|
||||
if (!fieldName || typeof fieldName !== 'string') {
|
||||
throw new Error('SecondaryIndex: fieldName 必须是非空字符串');
|
||||
}
|
||||
this._fieldName = fieldName;
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
this._map = new Map();
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引中的条目总数(所有 primaryKey 的总计)
|
||||
* @returns {number}
|
||||
*/
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加索引条目
|
||||
* @param {string} fieldValue 字段值
|
||||
* @param {string} primaryKey 主键
|
||||
*/
|
||||
add(fieldValue, primaryKey) {
|
||||
const strValue = String(fieldValue);
|
||||
let keys = this._map.get(strValue);
|
||||
if (!keys) {
|
||||
keys = new Set();
|
||||
this._map.set(strValue, keys);
|
||||
}
|
||||
if (!keys.has(primaryKey)) {
|
||||
keys.add(primaryKey);
|
||||
this._size++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除索引条目
|
||||
* @param {string} fieldValue 字段值
|
||||
* @param {string} primaryKey 主键
|
||||
* @returns {boolean} 是否移除成功
|
||||
*/
|
||||
remove(fieldValue, primaryKey) {
|
||||
const strValue = String(fieldValue);
|
||||
const keys = this._map.get(strValue);
|
||||
if (!keys) return false;
|
||||
|
||||
if (keys.delete(primaryKey)) {
|
||||
this._size--;
|
||||
if (keys.size === 0) {
|
||||
this._map.delete(strValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定字段值的所有主键
|
||||
* @param {string} fieldValue
|
||||
* @returns {string[]} 主键数组
|
||||
*/
|
||||
find(fieldValue) {
|
||||
const strValue = String(fieldValue);
|
||||
const keys = this._map.get(strValue);
|
||||
if (!keys) return [];
|
||||
return Array.from(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 范围查询(闭区间 [startValue, endValue])
|
||||
* @param {string} startValue 起始值
|
||||
* @param {string} endValue 结束值
|
||||
* @returns {string[]} 主键数组(去重)
|
||||
*/
|
||||
range(startValue, endValue) {
|
||||
const start = String(startValue);
|
||||
const end = String(endValue);
|
||||
const resultSet = new Set();
|
||||
|
||||
// 获取所有键并排序
|
||||
const sortedKeys = Array.from(this._map.keys()).sort();
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
if (key >= start && key <= end) {
|
||||
const primaryKeys = this._map.get(key);
|
||||
for (const pk of primaryKeys) {
|
||||
resultSet.add(pk);
|
||||
}
|
||||
}
|
||||
if (key > end) break;
|
||||
}
|
||||
|
||||
return Array.from(resultSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空索引
|
||||
*/
|
||||
clear() {
|
||||
this._map.clear();
|
||||
this._size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SecondaryIndex;
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// grid-db/src/query/nearby.js
|
||||
// Grid-DB · 近邻查询
|
||||
// 基于欧几里得距离的邻近查询
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-004
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const GridCell = require('../core/grid-cell');
|
||||
|
||||
/**
|
||||
* NearbyQuery — 近邻查询
|
||||
*
|
||||
* 本体论锚定:近邻 = 以某个格子为中心,画一个圆,找出圆内所有格子。
|
||||
* 圆由中心坐标和半径定义。
|
||||
* 结果按距离排序,最近的在前面。
|
||||
*/
|
||||
class NearbyQuery {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {Map|object} deps.index 索引
|
||||
* @param {object} deps.pageManager 页管理器
|
||||
*/
|
||||
constructor({ index, pageManager }) {
|
||||
if (!index) {
|
||||
throw new Error('NearbyQuery: index 不能为空');
|
||||
}
|
||||
if (!pageManager) {
|
||||
throw new Error('NearbyQuery: pageManager 不能为空');
|
||||
}
|
||||
this._index = index;
|
||||
this._pageManager = pageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 近邻查询
|
||||
*
|
||||
* @param {string} namespace 命名空间
|
||||
* @param {number} centerX 中心 X 坐标
|
||||
* @param {number} centerY 中心 Y 坐标
|
||||
* @param {number} radius 搜索半径(欧几里得距离)
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.layer] 层级过滤
|
||||
* @param {number} [options.limit] 最大返回数
|
||||
* @param {string} [options.sortBy] 排序方式(默认 'distance')
|
||||
* @returns {Array<{cell: GridCell, data: *, distance: number}>}
|
||||
*/
|
||||
nearby(namespace, centerX, centerY, radius, options = {}) {
|
||||
const { layer, limit, sortBy = 'distance' } = options;
|
||||
const results = [];
|
||||
|
||||
const entries = this._getEntries();
|
||||
|
||||
for (const [key, pageId] of entries) {
|
||||
try {
|
||||
const cell = GridCell.fromKey(key);
|
||||
|
||||
// 命名空间过滤
|
||||
if (cell.namespace !== namespace) continue;
|
||||
|
||||
// 层级过滤
|
||||
if (layer && cell.layer !== layer) continue;
|
||||
|
||||
// 欧几里得距离计算
|
||||
const dx = cell.gridX - centerX;
|
||||
const dy = cell.gridY - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 半径过滤
|
||||
if (distance > radius) 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, distance });
|
||||
}
|
||||
} catch {
|
||||
// 跳过无效键格式(fromKey 解析失败),不影响其他键的查询
|
||||
}
|
||||
}
|
||||
|
||||
// 按距离升序排序
|
||||
if (sortBy === 'distance') {
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
// limit
|
||||
if (limit && limit > 0) {
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引中的所有条目(兼容 Map 和 BTree)
|
||||
* @returns {Array<[string, *]>}
|
||||
*/
|
||||
_getEntries() {
|
||||
if (this._index instanceof Map) {
|
||||
return Array.from(this._index.entries());
|
||||
}
|
||||
if (typeof this._index.toJSON === 'function') {
|
||||
const json = this._index.toJSON();
|
||||
return json.entries.map(e => [e.key, e.value]);
|
||||
}
|
||||
if (typeof this._index.entries === 'function') {
|
||||
return Array.from(this._index.entries());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NearbyQuery;
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// grid-db/src/query/scan.js
|
||||
// Grid-DB · 范围扫描器
|
||||
// 矩形区域查询
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-003
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const GridCell = require('../core/grid-cell');
|
||||
|
||||
/**
|
||||
* RangeScanner — 范围扫描器
|
||||
*
|
||||
* 本体论锚定:扫描 = 在纸上画一个矩形区域,找出所有落在区域内的格子。
|
||||
* 矩形由 xRange 和 yRange 定义,layer 可选过滤。
|
||||
*
|
||||
* 支持 BTree 索引和 Map 索引两种后端。
|
||||
*/
|
||||
class RangeScanner {
|
||||
/**
|
||||
* @param {object} deps
|
||||
* @param {Map|object} deps.index 索引(Map 或 BTree,需支持遍历)
|
||||
* @param {object} deps.pageManager 页管理器
|
||||
*/
|
||||
constructor({ index, pageManager }) {
|
||||
if (!index) {
|
||||
throw new Error('RangeScanner: index 不能为空');
|
||||
}
|
||||
if (!pageManager) {
|
||||
throw new Error('RangeScanner: pageManager 不能为空');
|
||||
}
|
||||
this._index = index;
|
||||
this._pageManager = pageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形区域扫描
|
||||
*
|
||||
* @param {string} namespace 命名空间
|
||||
* @param {object} [options]
|
||||
* @param {number[]} [options.xRange] X 范围 [min, max]
|
||||
* @param {number[]} [options.yRange] Y 范围 [min, max]
|
||||
* @param {string} [options.layer] 层级过滤
|
||||
* @param {number} [options.limit] 最大返回数
|
||||
* @param {number} [options.offset] 跳过前 N 条
|
||||
* @returns {Array<{cell: GridCell, data: *}>} 按 (gridX, gridY) 排序
|
||||
*/
|
||||
scan(namespace, options = {}) {
|
||||
const { xRange, yRange, layer, limit, offset = 0 } = options;
|
||||
const results = [];
|
||||
|
||||
// 遍历索引中所有条目
|
||||
const entries = this._getEntries();
|
||||
|
||||
for (const [key, pageId] of entries) {
|
||||
try {
|
||||
const cell = GridCell.fromKey(key);
|
||||
|
||||
// 命名空间过滤
|
||||
if (cell.namespace !== namespace) continue;
|
||||
|
||||
// 层级过滤
|
||||
if (layer && cell.layer !== layer) continue;
|
||||
|
||||
// X 范围过滤
|
||||
if (xRange) {
|
||||
if (cell.gridX < xRange[0] || cell.gridX > xRange[1]) continue;
|
||||
}
|
||||
|
||||
// Y 范围过滤
|
||||
if (yRange) {
|
||||
if (cell.gridY < yRange[0] || cell.gridY > 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 {
|
||||
// 跳过无效键格式(fromKey 解析失败),不影响其他键的扫描
|
||||
}
|
||||
}
|
||||
|
||||
// 按 (gridX, gridY) 排序
|
||||
results.sort((a, b) => {
|
||||
if (a.cell.gridX !== b.cell.gridX) return a.cell.gridX - b.cell.gridX;
|
||||
return a.cell.gridY - b.cell.gridY;
|
||||
});
|
||||
|
||||
// offset + limit
|
||||
const start = offset || 0;
|
||||
const end = limit ? start + limit : results.length;
|
||||
return results.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取索引中的所有条目(兼容 Map 和 BTree)
|
||||
* @returns {Array<[string, *]>}
|
||||
*/
|
||||
_getEntries() {
|
||||
if (this._index instanceof Map) {
|
||||
return Array.from(this._index.entries());
|
||||
}
|
||||
// BTree:通过 toJSON 获取所有条目
|
||||
if (typeof this._index.toJSON === 'function') {
|
||||
const json = this._index.toJSON();
|
||||
return json.entries.map(e => [e.key, e.value]);
|
||||
}
|
||||
// 其他可迭代对象
|
||||
if (typeof this._index.entries === 'function') {
|
||||
return Array.from(this._index.entries());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RangeScanner;
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
// grid-db/tests/smoke/grid-db-p1.test.js
|
||||
// Grid-DB · Phase 1 冒烟测试
|
||||
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-TEST
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
const {
|
||||
open,
|
||||
GridAPI,
|
||||
GridCell,
|
||||
WAL,
|
||||
PageManager,
|
||||
EventLog,
|
||||
BTree,
|
||||
NamespaceManager,
|
||||
RangeScanner,
|
||||
NearbyQuery,
|
||||
SecondaryIndex
|
||||
} = 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-p1-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 1 冒烟测试\n');
|
||||
|
||||
// ── 测试 1: Phase 1 模块导出完整性 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('── 测试 1: Phase 1 模块导出完整性 ──');
|
||||
assert(typeof BTree === 'function', 'BTree 是构造函数');
|
||||
assert(typeof NamespaceManager === 'function', 'NamespaceManager 是构造函数');
|
||||
assert(typeof RangeScanner === 'function', 'RangeScanner 是构造函数');
|
||||
assert(typeof NearbyQuery === 'function', 'NearbyQuery 是构造函数');
|
||||
assert(typeof SecondaryIndex === 'function', 'SecondaryIndex 是构造函数');
|
||||
|
||||
// Phase 0 模块仍然存在
|
||||
assert(typeof open === 'function', 'open 仍然导出');
|
||||
assert(typeof GridAPI === 'function', 'GridAPI 仍然导出');
|
||||
assert(typeof GridCell === 'function', 'GridCell 仍然导出');
|
||||
assert(typeof EventLog === 'function', 'EventLog 仍然导出');
|
||||
|
||||
// ── 测试 2: B+Tree 基础功能 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 2: B+Tree 基础功能 ──');
|
||||
const tree = new BTree(4); // 小阶数方便测试分裂
|
||||
|
||||
// 插入
|
||||
tree.insert('c', 3);
|
||||
tree.insert('a', 1);
|
||||
tree.insert('b', 2);
|
||||
tree.insert('e', 5);
|
||||
tree.insert('d', 4);
|
||||
assert(tree.size === 5, '插入 5 条后 size = 5');
|
||||
|
||||
// 查找
|
||||
assert(tree.find('a') === 1, 'find("a") = 1');
|
||||
assert(tree.find('c') === 3, 'find("c") = 3');
|
||||
assert(tree.find('e') === 5, 'find("e") = 5');
|
||||
assert(tree.find('z') === undefined, 'find("z") = undefined');
|
||||
|
||||
// 更新
|
||||
tree.insert('a', 100);
|
||||
assert(tree.find('a') === 100, '更新后 find("a") = 100');
|
||||
assert(tree.size === 5, '更新不增加 size');
|
||||
|
||||
// 删除
|
||||
const delOk = tree.delete('c');
|
||||
assert(delOk === true, 'delete("c") 返回 true');
|
||||
assert(tree.find('c') === undefined, '删除后 find("c") = undefined');
|
||||
assert(tree.size === 4, '删除后 size = 4');
|
||||
|
||||
const delFail = tree.delete('z');
|
||||
assert(delFail === false, 'delete 不存在的键返回 false');
|
||||
|
||||
// 范围查询
|
||||
tree.insert('c', 3); // 重新插入
|
||||
const rangeResult = tree.range('b', 'd');
|
||||
assert(rangeResult.length === 3, 'range("b","d") 返回 3 条');
|
||||
assert(rangeResult[0].key === 'b', 'range 第一条 key = "b"');
|
||||
assert(rangeResult[1].key === 'c', 'range 第二条 key = "c"');
|
||||
assert(rangeResult[2].key === 'd', 'range 第三条 key = "d"');
|
||||
|
||||
// 序列化 / 反序列化
|
||||
const json = tree.toJSON();
|
||||
assert(json.order === 4, 'toJSON 包含 order');
|
||||
assert(json.entries.length === 5, 'toJSON 包含 5 个条目');
|
||||
|
||||
const tree2 = BTree.fromJSON(json);
|
||||
assert(tree2.size === 5, 'fromJSON 还原 size = 5');
|
||||
assert(tree2.find('a') === 100, 'fromJSON 还原后 find("a") = 100');
|
||||
assert(tree2.find('e') === 5, 'fromJSON 还原后 find("e") = 5');
|
||||
|
||||
// clear
|
||||
tree.clear();
|
||||
assert(tree.size === 0, 'clear 后 size = 0');
|
||||
assert(tree.find('a') === undefined, 'clear 后 find 返回 undefined');
|
||||
|
||||
// 大量插入测试分裂逻辑
|
||||
const treeLarge = new BTree(4);
|
||||
for (let i = 0; i < 50; i++) {
|
||||
treeLarge.insert(`key-${String(i).padStart(3, '0')}`, i);
|
||||
}
|
||||
assert(treeLarge.size === 50, '大量插入后 size = 50');
|
||||
assert(treeLarge.find('key-025') === 25, '大量插入后查找正确');
|
||||
const largeRange = treeLarge.range('key-010', 'key-019');
|
||||
assert(largeRange.length === 10, '大量数据范围查询返回 10 条');
|
||||
|
||||
// 无效参数
|
||||
let errThrown = false;
|
||||
try { new BTree(1); } catch { errThrown = true; }
|
||||
assert(errThrown, 'order < 3 抛出错误');
|
||||
|
||||
// ── 测试 3: NamespaceManager ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 3: NamespaceManager ──');
|
||||
const nsManager = new NamespaceManager();
|
||||
|
||||
// 创建
|
||||
const ns1 = nsManager.create('agent-zy', { desc: '铸渊代理' });
|
||||
assert(ns1.name === 'agent-zy', '创建命名空间 name 正确');
|
||||
assert(ns1.metadata.desc === '铸渊代理', '创建命名空间 metadata 正确');
|
||||
assert(ns1.cellCount === 0, '新命名空间 cellCount = 0');
|
||||
assert(typeof ns1.createdAt === 'string', '包含 createdAt');
|
||||
|
||||
// exists
|
||||
assert(nsManager.exists('agent-zy') === true, 'exists 已存在返回 true');
|
||||
assert(nsManager.exists('not-exist') === false, 'exists 不存在返回 false');
|
||||
|
||||
// get
|
||||
const got = nsManager.get('agent-zy');
|
||||
assert(got !== null, 'get 返回非 null');
|
||||
assert(got.name === 'agent-zy', 'get name 正确');
|
||||
|
||||
// 创建多个
|
||||
nsManager.create('dc-v1');
|
||||
nsManager.create('exe_engine');
|
||||
const all = nsManager.list();
|
||||
assert(all.length === 3, 'list 返回 3 个命名空间');
|
||||
|
||||
// incrementCellCount / decrementCellCount
|
||||
nsManager.incrementCellCount('agent-zy');
|
||||
nsManager.incrementCellCount('agent-zy');
|
||||
assert(nsManager.get('agent-zy').cellCount === 2, 'increment 后 cellCount = 2');
|
||||
|
||||
nsManager.decrementCellCount('agent-zy');
|
||||
assert(nsManager.get('agent-zy').cellCount === 1, 'decrement 后 cellCount = 1');
|
||||
|
||||
// 删除
|
||||
const delNs = nsManager.delete('dc-v1');
|
||||
assert(delNs === true, 'delete 返回 true');
|
||||
assert(nsManager.exists('dc-v1') === false, '删除后不存在');
|
||||
|
||||
const delNs2 = nsManager.delete('not-exist');
|
||||
assert(delNs2 === false, 'delete 不存在返回 false');
|
||||
|
||||
// 验证:无效名称
|
||||
errThrown = false;
|
||||
try { nsManager.create(''); } catch { errThrown = true; }
|
||||
assert(errThrown, '空名称抛出错误');
|
||||
|
||||
errThrown = false;
|
||||
try { nsManager.create('invalid name!'); } catch { errThrown = true; }
|
||||
assert(errThrown, '含特殊字符的名称抛出错误');
|
||||
|
||||
// 重复创建
|
||||
errThrown = false;
|
||||
try { nsManager.create('agent-zy'); } catch { errThrown = true; }
|
||||
assert(errThrown, '重复创建抛出错误');
|
||||
|
||||
// ── 测试 4: RangeScanner ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 4: RangeScanner ──');
|
||||
const scanDir = makeTempDir('scan');
|
||||
const scanDb = open({ dataDir: scanDir });
|
||||
|
||||
// 插入测试数据
|
||||
scanDb.put('scan-test', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'a' });
|
||||
scanDb.put('scan-test', { gridX: 2, gridY: 3, layer: 'raw' }, { id: 'b' });
|
||||
scanDb.put('scan-test', { gridX: 4, gridY: 2, layer: 'raw' }, { id: 'c' });
|
||||
scanDb.put('scan-test', { gridX: 5, gridY: 5, layer: 'indexed' }, { id: 'd' });
|
||||
scanDb.put('other-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'e' });
|
||||
|
||||
const scanner = new RangeScanner({
|
||||
index: scanDb._index,
|
||||
pageManager: scanDb._pageManager
|
||||
});
|
||||
|
||||
// 全命名空间扫描
|
||||
const scanAll = scanner.scan('scan-test');
|
||||
assert(scanAll.length === 4, 'RangeScanner 全扫描 = 4');
|
||||
|
||||
// X/Y 范围过滤
|
||||
const scanXY = scanner.scan('scan-test', { xRange: [1, 3], yRange: [1, 3] });
|
||||
assert(scanXY.length === 2, 'X/Y 范围过滤 = 2');
|
||||
|
||||
// 层级过滤
|
||||
const scanLayer = scanner.scan('scan-test', { layer: 'indexed' });
|
||||
assert(scanLayer.length === 1, 'layer 过滤 = 1');
|
||||
assert(scanLayer[0].data.id === 'd', 'layer 过滤结果正确');
|
||||
|
||||
// 排序验证:按 gridX, gridY
|
||||
const sorted = scanner.scan('scan-test');
|
||||
assert(sorted[0].cell.gridX <= sorted[1].cell.gridX, '结果按 gridX 升序');
|
||||
|
||||
// limit
|
||||
const scanLimited = scanner.scan('scan-test', { limit: 2 });
|
||||
assert(scanLimited.length === 2, 'limit = 2 返回 2 条');
|
||||
|
||||
// offset
|
||||
const scanOffset = scanner.scan('scan-test', { offset: 2 });
|
||||
assert(scanOffset.length === 2, 'offset = 2 跳过 2 条');
|
||||
|
||||
// 命名空间隔离
|
||||
const scanOther = scanner.scan('other-ns');
|
||||
assert(scanOther.length === 1, '不同命名空间隔离');
|
||||
|
||||
scanDb.close();
|
||||
cleanDir(scanDir);
|
||||
|
||||
// ── 测试 5: NearbyQuery ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 5: NearbyQuery ──');
|
||||
const nearDir = makeTempDir('nearby');
|
||||
const nearDb = open({ dataDir: nearDir });
|
||||
|
||||
nearDb.put('near-ns', { gridX: 0, gridY: 0, layer: 'raw' }, { id: 'origin' });
|
||||
nearDb.put('near-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'close' });
|
||||
nearDb.put('near-ns', { gridX: 3, gridY: 4, layer: 'raw' }, { id: 'mid' });
|
||||
nearDb.put('near-ns', { gridX: 10, gridY: 10, layer: 'raw' }, { id: 'far' });
|
||||
nearDb.put('near-ns', { gridX: 2, gridY: 0, layer: 'indexed' }, { id: 'layer-diff' });
|
||||
|
||||
const nearQuery = new NearbyQuery({
|
||||
index: nearDb._index,
|
||||
pageManager: nearDb._pageManager
|
||||
});
|
||||
|
||||
// 半径 2 内:(0,0)=0, (1,1)=√2≈1.414, (2,0)=2.0
|
||||
const near2 = nearQuery.nearby('near-ns', 0, 0, 2);
|
||||
assert(near2.length === 3, '半径 2 内 = 3 个点');
|
||||
assert(near2[0].distance === 0, '最近点距离 = 0');
|
||||
assert(near2[0].data.id === 'origin', '最近点是 origin');
|
||||
|
||||
// 半径 6 内
|
||||
const near6 = nearQuery.nearby('near-ns', 0, 0, 6);
|
||||
assert(near6.length === 4, '半径 6 内 = 4 个点');
|
||||
|
||||
// 距离排序验证
|
||||
assert(near6[0].distance <= near6[1].distance, '结果按距离升序');
|
||||
assert(near6[1].distance <= near6[2].distance, '第二条 ≤ 第三条');
|
||||
|
||||
// layer 过滤
|
||||
const nearLayer = nearQuery.nearby('near-ns', 0, 0, 6, { layer: 'indexed' });
|
||||
assert(nearLayer.length === 1, 'layer 过滤 = 1');
|
||||
assert(nearLayer[0].data.id === 'layer-diff', 'layer 过滤结果正确');
|
||||
|
||||
// limit
|
||||
const nearLimit = nearQuery.nearby('near-ns', 0, 0, 100, { limit: 2 });
|
||||
assert(nearLimit.length === 2, 'limit = 2 返回 2 条');
|
||||
|
||||
// 距离正确性验证
|
||||
const sqrt2 = Math.sqrt(2);
|
||||
assert(Math.abs(near2[1].distance - sqrt2) < 0.001, '(1,1) 距离 = √2');
|
||||
|
||||
nearDb.close();
|
||||
cleanDir(nearDir);
|
||||
|
||||
// ── 测试 6: EventLog 增强功能 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 6: EventLog 增强功能 ──');
|
||||
const elog = new EventLog({ maxEvents: 100 });
|
||||
|
||||
// 插入一些事件,带时间间隔
|
||||
const t0 = new Date().toISOString();
|
||||
elog.append('ns-a', 'put', 'ns-a:1:1:raw', { size: 10 });
|
||||
elog.append('ns-a', 'put', 'ns-a:2:2:raw', { size: 20 });
|
||||
elog.append('ns-a', 'delete', 'ns-a:1:1:raw');
|
||||
elog.append('ns-b', 'put', 'ns-b:1:1:raw', { size: 30 });
|
||||
elog.append('ns-b', 'scan', 'scan:ns-b', { count: 5 });
|
||||
|
||||
// replay — 从 t0 开始应返回所有 5 条
|
||||
const replayed = elog.replay(t0);
|
||||
assert(replayed.length === 5, 'replay 从 t0 返回 5 条');
|
||||
|
||||
// replayFromSeqNo
|
||||
const fromSeq3 = elog.replayFromSeqNo(3);
|
||||
assert(fromSeq3.length === 3, 'replayFromSeqNo(3) 返回 3 条');
|
||||
assert(fromSeq3[0].seqNo === 3, '第一条 seqNo = 3');
|
||||
|
||||
// getByOperation
|
||||
const puts = elog.getByOperation('put');
|
||||
assert(puts.length === 3, 'getByOperation("put") = 3');
|
||||
|
||||
const deletes = elog.getByOperation('delete');
|
||||
assert(deletes.length === 1, 'getByOperation("delete") = 1');
|
||||
|
||||
// getByOperation with limit
|
||||
const putsLimited = elog.getByOperation('put', 2);
|
||||
assert(putsLimited.length === 2, 'getByOperation 带 limit = 2');
|
||||
|
||||
// 天眼钩子
|
||||
let tianyanReceived = null;
|
||||
elog.setTianyanHook((evt) => { tianyanReceived = evt; });
|
||||
elog.append('ns-a', 'put', 'ns-a:3:3:raw');
|
||||
assert(tianyanReceived !== null, '天眼钩子收到事件');
|
||||
assert(tianyanReceived.namespace === 'ns-a', '天眼事件 namespace 正确');
|
||||
assert(tianyanReceived.operation === 'put', '天眼事件 operation 正确');
|
||||
|
||||
// 既有功能不受影响
|
||||
const recent = elog.getRecent(3);
|
||||
assert(recent.length === 3, 'getRecent 仍然正常工作');
|
||||
|
||||
const byNs = elog.getByNamespace('ns-b');
|
||||
assert(byNs.length === 2, 'getByNamespace 仍然正常工作');
|
||||
|
||||
// ── 测试 7: SecondaryIndex ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 7: SecondaryIndex ──');
|
||||
const secIdx = new SecondaryIndex('layer');
|
||||
|
||||
// add
|
||||
secIdx.add('raw', 'ns:1:1:raw');
|
||||
secIdx.add('raw', 'ns:2:2:raw');
|
||||
secIdx.add('indexed', 'ns:3:3:indexed');
|
||||
secIdx.add('semantic', 'ns:4:4:semantic');
|
||||
assert(secIdx.size === 4, '添加 4 条后 size = 4');
|
||||
|
||||
// find
|
||||
const rawKeys = secIdx.find('raw');
|
||||
assert(rawKeys.length === 2, 'find("raw") = 2');
|
||||
assert(rawKeys.includes('ns:1:1:raw'), 'find 包含 ns:1:1:raw');
|
||||
assert(rawKeys.includes('ns:2:2:raw'), 'find 包含 ns:2:2:raw');
|
||||
|
||||
const indexedKeys = secIdx.find('indexed');
|
||||
assert(indexedKeys.length === 1, 'find("indexed") = 1');
|
||||
|
||||
const noKeys = secIdx.find('cleaned');
|
||||
assert(noKeys.length === 0, 'find 不存在值 = 空数组');
|
||||
|
||||
// remove
|
||||
const removed = secIdx.remove('raw', 'ns:1:1:raw');
|
||||
assert(removed === true, 'remove 返回 true');
|
||||
assert(secIdx.size === 3, 'remove 后 size = 3');
|
||||
assert(secIdx.find('raw').length === 1, 'remove 后 find("raw") = 1');
|
||||
|
||||
const removeFail = secIdx.remove('raw', 'not:exist');
|
||||
assert(removeFail === false, 'remove 不存在的主键返回 false');
|
||||
|
||||
// 重复 add 不增加计数
|
||||
secIdx.add('raw', 'ns:2:2:raw');
|
||||
assert(secIdx.size === 3, '重复 add 不增加 size');
|
||||
|
||||
// range
|
||||
secIdx.add('cleaned', 'ns:5:5:cleaned');
|
||||
const rangeKeys = secIdx.range('indexed', 'semantic');
|
||||
assert(rangeKeys.length >= 2, 'range("indexed","semantic") >= 2');
|
||||
assert(rangeKeys.includes('ns:3:3:indexed'), 'range 包含 indexed 条目');
|
||||
assert(rangeKeys.includes('ns:4:4:semantic'), 'range 包含 semantic 条目');
|
||||
|
||||
// clear
|
||||
secIdx.clear();
|
||||
assert(secIdx.size === 0, 'clear 后 size = 0');
|
||||
assert(secIdx.find('raw').length === 0, 'clear 后 find 返回空');
|
||||
|
||||
// 无效参数
|
||||
errThrown = false;
|
||||
try { new SecondaryIndex(''); } catch { errThrown = true; }
|
||||
assert(errThrown, '空 fieldName 抛出错误');
|
||||
|
||||
// ── 测试 8: 集成验证 — Phase 0 功能仍正常 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n── 测试 8: 集成验证 — Phase 0 功能仍正常 ──');
|
||||
const intDir = makeTempDir('integration');
|
||||
const intDb = open({ dataDir: intDir });
|
||||
|
||||
intDb.put('int-ns', { gridX: 1, gridY: 2, layer: 'raw' }, { val: 'hello' });
|
||||
const intResult = intDb.get('int-ns', { gridX: 1, gridY: 2, layer: 'raw' });
|
||||
assert(intResult !== null, 'Phase 0 put/get 仍然正常');
|
||||
assert(intResult.val === 'hello', 'Phase 0 数据正确');
|
||||
|
||||
const intScan = intDb.scan('int-ns');
|
||||
assert(intScan.length === 1, 'Phase 0 scan 仍然正常');
|
||||
|
||||
intDb.close();
|
||||
cleanDir(intDir);
|
||||
|
||||
// ── 测试结果汇总 ──
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n══════════════════════════════════════');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🗄️ Grid-DB Phase 1 冒烟测试完成');
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// scripts/agents/actions-parser.js
|
||||
// Actions Parser · 工作流输出解析器
|
||||
// ZY-P1-README-001 · Phase 1 · README Management Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 解析工作流输出 · 提取测试结果、部署状态、错误
|
||||
* @param {string} output - 工作流原始输出
|
||||
* @returns {{ tests: object, deployments: string[], errors: string[] }}
|
||||
*/
|
||||
function parseWorkflowOutput(output) {
|
||||
if (!output || typeof output !== 'string') {
|
||||
return { tests: { passed: 0, failed: 0, total: 0 }, deployments: [], errors: [] };
|
||||
}
|
||||
|
||||
const passCount = (output.match(/✅/g) || []).length;
|
||||
const failCount = (output.match(/❌/g) || []).length;
|
||||
|
||||
const deployments = [];
|
||||
const deployMatch = output.match(/deploy(?:ed|ing|ment).*?(?:success|complete|done)/gi);
|
||||
if (deployMatch) {
|
||||
deployments.push(...deployMatch);
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
const errorLines = output.split('\n').filter(line =>
|
||||
/error|Error|ERROR|❌|FAIL/.test(line)
|
||||
);
|
||||
errors.push(...errorLines.slice(0, 10));
|
||||
|
||||
return {
|
||||
tests: { passed: passCount, failed: failCount, total: passCount + failCount },
|
||||
deployments,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 PR 合并信息
|
||||
* @param {{ title: string, number: number, author: string, files: string[] }} prInfo
|
||||
* @returns {{ summary: string, title: string, filesChanged: number, author: string }}
|
||||
*/
|
||||
function parsePRMerge(prInfo) {
|
||||
if (!prInfo) {
|
||||
return { summary: '无 PR 信息', title: '', filesChanged: 0, author: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
summary: `PR #${prInfo.number}: ${prInfo.title}`,
|
||||
title: prInfo.title || '',
|
||||
filesChanged: Array.isArray(prInfo.files) ? prInfo.files.length : 0,
|
||||
author: prInfo.author || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析测试报告 · 从输出中提取通过/失败/总数
|
||||
* @param {string} output
|
||||
* @returns {{ passed: number, failed: number, total: number, passRate: number }}
|
||||
*/
|
||||
function parseTestReport(output) {
|
||||
if (!output || typeof output !== 'string') {
|
||||
return { passed: 0, failed: 0, total: 0, passRate: 0 };
|
||||
}
|
||||
|
||||
const passed = (output.match(/✅/g) || []).length;
|
||||
const failed = (output.match(/❌/g) || []).length;
|
||||
const total = passed + failed;
|
||||
const passRate = total > 0 ? Math.round((passed / total) * 10000) / 10000 : 0;
|
||||
|
||||
return { passed, failed, total, passRate };
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('📡 Actions Parser · 工作流解析器\n');
|
||||
|
||||
const sampleOutput = ' ✅ test1 passed\n ✅ test2 passed\n ❌ test3 failed\n';
|
||||
const result = parseWorkflowOutput(sampleOutput);
|
||||
console.log(` 解析结果: ${result.tests.passed} passed, ${result.tests.failed} failed`);
|
||||
}
|
||||
|
||||
module.exports = { parseWorkflowOutput, parsePRMerge, parseTestReport };
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// scripts/agents/auto-repair.js
|
||||
// Auto-Repair Agent · 自修复代理
|
||||
// ZY-P1-TWIN-004 · Phase 1 · Twin Balance Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const REPAIR_LOG_PATH = path.join(ROOT, 'signal-log/auto-repair.json');
|
||||
|
||||
/**
|
||||
* 诊断失衡原因 · 望闻问切
|
||||
* @param {{ level: string, laggingSide: string, deficit: number }} alert
|
||||
* @returns {{ alert: object, cause: string, affectedDimensions: string[], severity: string }}
|
||||
*/
|
||||
function diagnose(alert) {
|
||||
if (!alert) {
|
||||
return { alert: null, cause: 'none', affectedDimensions: [], severity: 'none' };
|
||||
}
|
||||
|
||||
// 根据偏差级别推断原因
|
||||
const causes = {
|
||||
low: '轻微测试波动或临时性回归',
|
||||
medium: '多个测试用例持续失败或缺失',
|
||||
high: '模块级架构问题或关键功能缺失'
|
||||
};
|
||||
|
||||
return {
|
||||
alert,
|
||||
cause: causes[alert.level] || '未知原因',
|
||||
affectedDimensions: ['testPassRate', 'taskCompletion'],
|
||||
severity: alert.level
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类修复级别 · 三级分诊
|
||||
* @param {{ severity: string }} diagnosis
|
||||
* @returns {{ level: string, name: string, autoFixable: boolean }}
|
||||
*/
|
||||
function classify(diagnosis) {
|
||||
if (diagnosis.severity === 'none') {
|
||||
return { level: 'L0', name: '无需修复', autoFixable: false };
|
||||
}
|
||||
|
||||
if (diagnosis.severity === 'low') {
|
||||
return { level: 'L1', name: '自动修复', autoFixable: true };
|
||||
}
|
||||
|
||||
if (diagnosis.severity === 'medium') {
|
||||
return { level: 'L2', name: '技术干预', autoFixable: false };
|
||||
}
|
||||
|
||||
return { level: 'L3', name: '架构干预', autoFixable: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试自动修复 · 针灸疗法
|
||||
* @param {{ alert: object, cause: string, severity: string }} diagnosis
|
||||
* @returns {{ success: boolean, action: string, timestamp: string }}
|
||||
*/
|
||||
function repair(diagnosis) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const classification = classify(diagnosis);
|
||||
|
||||
if (!classification.autoFixable) {
|
||||
const result = {
|
||||
success: false,
|
||||
action: `需要${classification.name},已生成工单`,
|
||||
level: classification.level,
|
||||
timestamp
|
||||
};
|
||||
logRepair(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// L1 自动修复:记录问题,标记需要重跑测试
|
||||
const result = {
|
||||
success: true,
|
||||
action: '已记录轻微偏差,建议下次构建时重跑测试验证',
|
||||
level: 'L1',
|
||||
laggingSide: diagnosis.alert ? diagnosis.alert.laggingSide : 'unknown',
|
||||
timestamp
|
||||
};
|
||||
|
||||
logRepair(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证修复结果 · 复查
|
||||
* @param {object} repairResult
|
||||
* @returns {{ verified: boolean, message: string }}
|
||||
*/
|
||||
function verify(repairResult) {
|
||||
if (!repairResult.success) {
|
||||
return {
|
||||
verified: false,
|
||||
message: `修复未执行(${repairResult.level}级需人工介入)`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
verified: true,
|
||||
message: '修复记录已归档,等待下次构建验证'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入修复日志
|
||||
* @param {object} record
|
||||
*/
|
||||
function logRepair(record) {
|
||||
let log = { repairs: [] };
|
||||
|
||||
const logDir = path.dirname(REPAIR_LOG_PATH);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(REPAIR_LOG_PATH)) {
|
||||
try {
|
||||
log = JSON.parse(fs.readFileSync(REPAIR_LOG_PATH, 'utf8'));
|
||||
} catch (_) {
|
||||
log = { repairs: [] };
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(log.repairs)) {
|
||||
log.repairs = [];
|
||||
}
|
||||
|
||||
// 保留最近 50 条记录
|
||||
log.repairs.push(record);
|
||||
if (log.repairs.length > 50) {
|
||||
log.repairs = log.repairs.slice(-50);
|
||||
}
|
||||
|
||||
fs.writeFileSync(REPAIR_LOG_PATH, JSON.stringify(log, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('🔧 Auto-Repair Agent · 自修复代理\n');
|
||||
|
||||
const exampleAlert = {
|
||||
level: 'low',
|
||||
laggingSide: 'left (EXE-Engine)',
|
||||
deficit: 0.03
|
||||
};
|
||||
|
||||
const diag = diagnose(exampleAlert);
|
||||
const cls = classify(diag);
|
||||
const result = repair(diag);
|
||||
const verification = verify(result);
|
||||
|
||||
console.log(` 诊断: ${diag.cause}`);
|
||||
console.log(` 分类: ${cls.level} - ${cls.name}`);
|
||||
console.log(` 修复: ${result.action}`);
|
||||
console.log(` 验证: ${verification.message}`);
|
||||
}
|
||||
|
||||
module.exports = { diagnose, classify, repair, verify };
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// scripts/agents/balance-checker.js
|
||||
// Balance Checker · 天平校验器
|
||||
// ZY-P1-TWIN-003 · Phase 1 · Twin Balance Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 检查双子平衡状态 · 天平是否倾斜
|
||||
* @param {{ left: { composite: number }, right: { composite: number }, balance: number }} mergeResult
|
||||
* @returns {{ balanced: boolean, balance: number, drift: number, alert: null|object }}
|
||||
*/
|
||||
function check(mergeResult) {
|
||||
const { left, right, balance } = mergeResult;
|
||||
const drift = Math.round(Math.abs(left.composite - right.composite) * 10000) / 10000;
|
||||
|
||||
// 天平完全平衡
|
||||
if (balance >= 1.0) {
|
||||
return { balanced: true, balance, drift: 0, alert: null };
|
||||
}
|
||||
|
||||
// 天平倾斜 — 生成告警
|
||||
const laggingSide = left.composite < right.composite ? 'left (EXE-Engine)' : 'right (Grid-DB)';
|
||||
const leadingSide = left.composite >= right.composite ? 'left (EXE-Engine)' : 'right (Grid-DB)';
|
||||
const deficit = drift;
|
||||
|
||||
let level;
|
||||
if (drift < 0.05) {
|
||||
level = 'low';
|
||||
} else if (drift < 0.15) {
|
||||
level = 'medium';
|
||||
} else {
|
||||
level = 'high';
|
||||
}
|
||||
|
||||
const recommendations = {
|
||||
low: '轻微偏差,建议关注但无需立即处理',
|
||||
medium: '中等偏差,建议加强落后侧开发或修复测试',
|
||||
high: '严重偏差,需要立即介入,可能存在架构问题'
|
||||
};
|
||||
|
||||
return {
|
||||
balanced: false,
|
||||
balance,
|
||||
drift,
|
||||
alert: {
|
||||
level,
|
||||
laggingSide,
|
||||
leadingSide,
|
||||
deficit,
|
||||
recommendation: recommendations[level]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const { collectBoth } = require('./twin-collector');
|
||||
const { merge } = require('./twin-merge');
|
||||
|
||||
console.log('🔍 Balance Checker · 天平校验\n');
|
||||
const data = collectBoth();
|
||||
const merged = merge(data.left, data.right);
|
||||
const result = check(merged);
|
||||
|
||||
if (result.balanced) {
|
||||
console.log(' ✅ 双子天平完全平衡');
|
||||
} else {
|
||||
console.log(` ⚠️ 偏差等级: ${result.alert.level}`);
|
||||
console.log(` 📉 落后侧: ${result.alert.laggingSide}`);
|
||||
console.log(` 📊 偏差值: ${result.drift}`);
|
||||
console.log(` 💡 建议: ${result.alert.recommendation}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { check };
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
// scripts/agents/bulletin-manager.js
|
||||
// Bulletin Manager · 公告板管理器
|
||||
// ZY-P1-README-003 · Phase 1 · README Management Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const BULLETIN_PATH = path.join(ROOT, '.github/tianyen/bulletin-data.json');
|
||||
|
||||
/**
|
||||
* 读取公告数据
|
||||
* @returns {{ events: object[] }}
|
||||
*/
|
||||
function loadBulletin() {
|
||||
if (!fs.existsSync(BULLETIN_PATH)) {
|
||||
return { events: [] };
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(BULLETIN_PATH, 'utf8'));
|
||||
if (!Array.isArray(data.events)) {
|
||||
data.events = [];
|
||||
}
|
||||
return data;
|
||||
} catch (_) {
|
||||
return { events: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存公告数据
|
||||
* @param {{ events: object[] }} data
|
||||
*/
|
||||
function saveBulletin(data) {
|
||||
const dir = path.dirname(BULLETIN_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(BULLETIN_PATH, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件 · 去重(按 id)
|
||||
* @param {{ id: string, timestamp: string, event: string, status: string, handler: string, priority: number }} event
|
||||
* @returns {boolean} 是否成功添加(false = 已存在)
|
||||
*/
|
||||
function addEvent(event) {
|
||||
const data = loadBulletin();
|
||||
|
||||
// 去重:相同 id 的事件不重复添加
|
||||
const existing = data.events.findIndex(e => e.id === event.id);
|
||||
if (existing >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data.events.push({
|
||||
id: event.id,
|
||||
timestamp: event.timestamp || new Date().toISOString(),
|
||||
event: event.event || '',
|
||||
status: event.status || 'active',
|
||||
handler: event.handler || '',
|
||||
priority: event.priority || 3
|
||||
});
|
||||
|
||||
saveBulletin(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新事件状态
|
||||
* @param {string} eventId
|
||||
* @param {string} newStatus
|
||||
* @returns {boolean} 是否找到并更新
|
||||
*/
|
||||
function updateStatus(eventId, newStatus) {
|
||||
const data = loadBulletin();
|
||||
const idx = data.events.findIndex(e => e.id === eventId);
|
||||
if (idx < 0) return false;
|
||||
|
||||
data.events[idx].status = newStatus;
|
||||
data.events[idx].updated_at = new Date().toISOString();
|
||||
saveBulletin(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有活跃事件
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function getActive() {
|
||||
const data = loadBulletin();
|
||||
return data.events.filter(e => e.status !== 'resolved');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的事件
|
||||
* @param {number} limit
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function getRecent(limit) {
|
||||
const data = loadBulletin();
|
||||
return data.events
|
||||
.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''))
|
||||
.slice(0, limit || 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期事件
|
||||
* @param {number} maxAge - 最大存活时间(毫秒)
|
||||
* @returns {number} 被清理的数量
|
||||
*/
|
||||
function prune(maxAge) {
|
||||
const data = loadBulletin();
|
||||
const cutoff = Date.now() - maxAge;
|
||||
const before = data.events.length;
|
||||
|
||||
data.events = data.events.filter(e => {
|
||||
const ts = new Date(e.timestamp).getTime();
|
||||
return ts >= cutoff;
|
||||
});
|
||||
|
||||
const pruned = before - data.events.length;
|
||||
if (pruned > 0) {
|
||||
saveBulletin(data);
|
||||
}
|
||||
return pruned;
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('📋 Bulletin Manager · 公告板管理器\n');
|
||||
|
||||
const active = getActive();
|
||||
console.log(` 活跃事件: ${active.length}`);
|
||||
|
||||
const recent = getRecent(5);
|
||||
console.log(` 最近事件: ${recent.length}`);
|
||||
}
|
||||
|
||||
module.exports = { addEvent, updateStatus, getActive, getRecent, prune, loadBulletin, saveBulletin };
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// scripts/agents/escalation-router.js
|
||||
// Escalation Router · 三级升级路由器
|
||||
// ZY-P1-ESC-001 · Phase 1 · Escalation System
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const RULES_PATH = path.join(ROOT, 'config/escalation-rules.json');
|
||||
|
||||
/**
|
||||
* 加载升级规则
|
||||
* @returns {object}
|
||||
*/
|
||||
function loadRules() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(RULES_PATH, 'utf8'));
|
||||
} catch (_) {
|
||||
// 内置默认规则 · 规则文件缺失时的安全网
|
||||
return {
|
||||
rules: {
|
||||
L1: { types: ['test_flaky', 'lint_error', 'stub_missing', 'format_issue'], handler: 'auto_repair', notify: null },
|
||||
L2: { types: ['test_persistent_fail', 'dependency_conflict', 'performance_regression', 'security_alert'], handler: 'manual', notify: { target: 'DEV-002-肥猫' } },
|
||||
L3: { types: ['balance_long_drift', 'ontology_conflict', 'cross_system_architecture'], handler: 'manual', notify: { target: 'TCS-0002∞-冰朔' } }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类问题级别 · 三级分诊
|
||||
* @param {{ type: string }} issue
|
||||
* @returns {{ level: string, name: string, handler: string }}
|
||||
*/
|
||||
function classify(issue) {
|
||||
const rules = loadRules().rules;
|
||||
|
||||
for (const [level, rule] of Object.entries(rules)) {
|
||||
if (rule.types.includes(issue.type)) {
|
||||
return {
|
||||
level,
|
||||
name: rule.name || level,
|
||||
handler: rule.handler || 'manual'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 未知类型默认升级到 L2
|
||||
return { level: 'L2', name: '技术干预', handler: 'manual' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由问题到对应处理者
|
||||
* @param {{ type: string, description: string }} issue
|
||||
* @returns {{ level: string, handler: string, notify: object|null }}
|
||||
*/
|
||||
function route(issue) {
|
||||
const rules = loadRules().rules;
|
||||
const classification = classify(issue);
|
||||
const rule = rules[classification.level];
|
||||
|
||||
return {
|
||||
level: classification.level,
|
||||
handler: classification.handler,
|
||||
notify: rule ? rule.notify : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整升级流程 · 分类 → 路由 → 执行
|
||||
* @param {{ type: string, description: string, source: string }} issue
|
||||
* @returns {{ classification: object, routing: object, timestamp: string }}
|
||||
*/
|
||||
function escalate(issue) {
|
||||
const classification = classify(issue);
|
||||
const routing = route(issue);
|
||||
|
||||
return {
|
||||
classification,
|
||||
routing,
|
||||
issue,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('🔀 Escalation Router · 升级路由器\n');
|
||||
|
||||
const testIssues = [
|
||||
{ type: 'test_flaky', description: '测试偶发失败' },
|
||||
{ type: 'security_alert', description: '安全告警' },
|
||||
{ type: 'ontology_conflict', description: '本体冲突' }
|
||||
];
|
||||
|
||||
for (const issue of testIssues) {
|
||||
const result = escalate(issue);
|
||||
console.log(` ${issue.type} → ${result.classification.level} (${result.classification.name})`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { classify, route, escalate, loadRules };
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// scripts/agents/notify-adapter.js
|
||||
// Notify Adapter · 通知适配器
|
||||
// ZY-P1-ESC-002 · Phase 1 · Escalation System
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
|
||||
// ── 通知目标映射 ─────────────────────────────────────────────────────────
|
||||
const TARGETS = {
|
||||
'DEV-002-肥猫': '@qinfendebingshuo',
|
||||
'TCS-0002∞-冰朔': '@qinfendebingshuo'
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化通知内容 · 转为 markdown
|
||||
* @param {string} target - 通知目标 ID
|
||||
* @param {{ type: string, description: string }} issue
|
||||
* @returns {string} markdown 格式的通知
|
||||
*/
|
||||
function formatNotification(target, issue) {
|
||||
const mention = TARGETS[target] || target;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return [
|
||||
`## 🔔 升级通知`,
|
||||
'',
|
||||
`**目标**: ${mention}`,
|
||||
`**类型**: ${issue.type || '未知'}`,
|
||||
`**描述**: ${issue.description || '无描述'}`,
|
||||
`**来源**: ${issue.source || '系统自动检测'}`,
|
||||
`**时间**: ${now}`,
|
||||
'',
|
||||
'---',
|
||||
'_由铸渊升级路由器自动生成_'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
* @param {string} target - 通知目标 ID
|
||||
* @param {{ type: string, description: string }} issue
|
||||
* @returns {Promise<{ target: string, channel: string, messageId: string, timestamp: string }>}
|
||||
*/
|
||||
async function notify(target, issue) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const content = formatNotification(target, issue);
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
|
||||
|
||||
if (!token) {
|
||||
// 无 token 时回退到控制台输出
|
||||
console.log(`\n📢 通知(控制台模式):\n${content}\n`);
|
||||
return {
|
||||
target,
|
||||
channel: 'console',
|
||||
messageId: `console-${Date.now()}`,
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// 通过 GitHub Issues API 创建 issue
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const body = JSON.stringify({
|
||||
title: `🔔 [${issue.type}] ${issue.description || '升级通知'}`,
|
||||
body: content,
|
||||
labels: ['escalation', issue.type || 'unknown']
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.github.com',
|
||||
path: `/repos/${owner}/${repoName}/issues`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'zhuyuan-notify-adapter',
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
resolve({
|
||||
target,
|
||||
channel: 'github_issue',
|
||||
messageId: `issue-${parsed.number || 'unknown'}`,
|
||||
timestamp
|
||||
});
|
||||
} catch (e) {
|
||||
resolve({
|
||||
target,
|
||||
channel: 'github_issue',
|
||||
messageId: `issue-error`,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve({
|
||||
target,
|
||||
channel: 'github_issue_failed',
|
||||
messageId: `error-${Date.now()}`,
|
||||
timestamp
|
||||
});
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('📢 Notify Adapter · 通知适配器\n');
|
||||
const formatted = formatNotification('DEV-002-肥猫', {
|
||||
type: 'test_persistent_fail',
|
||||
description: '持续性测试失败'
|
||||
});
|
||||
console.log(formatted);
|
||||
}
|
||||
|
||||
module.exports = { notify, formatNotification, TARGETS };
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
// scripts/agents/readme-generator.js
|
||||
// README Generator · 首页动态布局引擎
|
||||
// ZY-P1-README-002 · Phase 1 · README Management Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
|
||||
// ── 皮肤系统 · 情绪驱动界面 ─────────────────────────────────────────────
|
||||
const SKINS = {
|
||||
CALM: { name: '🌊 湖面平静', emoji: ['🌊', '✅', '💎'], badgeColor: 'blue' },
|
||||
BUILDING: { name: '⚡ 建设中', emoji: ['⚡', '🔨', '🚧'], badgeColor: 'yellow' },
|
||||
ATTENTION: { name: '🔥 需要关注', emoji: ['🔥', '⚠️', '🔧'], badgeColor: 'orange' },
|
||||
EMERGENCY: { name: '🚨 紧急状态', emoji: ['🚨', '❌', '🛑'], badgeColor: 'red' },
|
||||
CELEBRATION: { name: '🎉 庆祝模式', emoji: ['🎉', '🏆', '✨'], badgeColor: 'brightgreen' }
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据系统状态选择皮肤
|
||||
* @param {object} systemState
|
||||
* @returns {object} skin
|
||||
*/
|
||||
function selectSkin(systemState) {
|
||||
if (!systemState) return SKINS.CALM;
|
||||
|
||||
// 紧急状态:有高级别告警
|
||||
if (systemState.alert && systemState.alert.level === 'high') {
|
||||
return SKINS.EMERGENCY;
|
||||
}
|
||||
|
||||
// 需要关注:有中级别告警
|
||||
if (systemState.alert && systemState.alert.level === 'medium') {
|
||||
return SKINS.ATTENTION;
|
||||
}
|
||||
|
||||
// 庆祝模式:平衡度完美且有里程碑
|
||||
if (systemState.balanced && systemState.milestone) {
|
||||
return SKINS.CELEBRATION;
|
||||
}
|
||||
|
||||
// 建设中:有活跃开发
|
||||
if (systemState.building || (systemState.recentUpdates && systemState.recentUpdates.length > 0)) {
|
||||
return SKINS.BUILDING;
|
||||
}
|
||||
|
||||
return SKINS.CALM;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成布局排序 · 按优先级排列版块
|
||||
* @param {object} systemState
|
||||
* @returns {string[]} 排序后的版块名称列表
|
||||
*/
|
||||
function generateLayout(systemState) {
|
||||
const sections = [];
|
||||
|
||||
// P0: 紧急状态优先
|
||||
if (systemState && systemState.alert && systemState.alert.level === 'high') {
|
||||
sections.push('critical_alert');
|
||||
}
|
||||
|
||||
// P1: 平衡偏差
|
||||
if (systemState && !systemState.balanced) {
|
||||
sections.push('balance_drift');
|
||||
}
|
||||
|
||||
// P2: 最近合并
|
||||
if (systemState && systemState.recentUpdates && systemState.recentUpdates.length > 0) {
|
||||
sections.push('recent_updates');
|
||||
}
|
||||
|
||||
// P3: 常规内容
|
||||
sections.push('dashboard', 'twin_balance', 'bulletin');
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染系统仪表板
|
||||
* @param {object} state
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
function renderDashboard(state) {
|
||||
const skin = selectSkin(state);
|
||||
const now = new Date().toISOString().split('T')[0];
|
||||
|
||||
let md = `## ${skin.emoji[0]} 系统状态\n\n`;
|
||||
md += `| 指标 | 状态 |\n`;
|
||||
md += `|------|------|\n`;
|
||||
md += `| 皮肤 | ${skin.name} |\n`;
|
||||
md += `| 更新时间 | ${now} |\n`;
|
||||
|
||||
if (state && state.balance !== undefined) {
|
||||
md += `| 平衡度 | ${(state.balance * 100).toFixed(1)}% |\n`;
|
||||
}
|
||||
|
||||
if (state && state.alert) {
|
||||
md += `| 告警 | ${state.alert.level} - ${state.alert.laggingSide} |\n`;
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染双子天平可视化
|
||||
* @param {object} state
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
function renderTwinBalance(state) {
|
||||
let md = '## ⚖️ 双子天平\n\n';
|
||||
|
||||
if (!state || !state.metrics) {
|
||||
md += '_暂无天平数据_\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
const left = state.metrics.left || {};
|
||||
const right = state.metrics.right || {};
|
||||
|
||||
md += '| 维度 | EXE-Engine | Grid-DB |\n';
|
||||
md += '|------|-----------|--------|\n';
|
||||
|
||||
const dims = ['taskCompletion', 'testPassRate', 'codeCoverage', 'securityScore'];
|
||||
const dimNames = { taskCompletion: '任务完成', testPassRate: '测试通过', codeCoverage: '代码覆盖', securityScore: '安全扫描' };
|
||||
|
||||
for (const dim of dims) {
|
||||
const lv = left.dimensions ? left.dimensions[dim] : 0;
|
||||
const rv = right.dimensions ? right.dimensions[dim] : 0;
|
||||
const lBar = generateProgressBar(lv);
|
||||
const rBar = generateProgressBar(rv);
|
||||
md += `| ${dimNames[dim]} | ${lBar} ${(lv * 100).toFixed(0)}% | ${rBar} ${(rv * 100).toFixed(0)}% |\n`;
|
||||
}
|
||||
|
||||
md += `\n**综合**: EXE=${left.composite || 0} · GDB=${right.composite || 0} · 平衡度=${state.balance || 0}\n`;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文本进度条
|
||||
* @param {number} value - 0~1
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateProgressBar(value) {
|
||||
const filled = Math.round((value || 0) * 10);
|
||||
const empty = 10 - filled;
|
||||
return '█'.repeat(filled) + '░'.repeat(empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染公告板
|
||||
* @param {object[]} events
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
function renderBulletin(events) {
|
||||
let md = '## 📋 公告板\n\n';
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
md += '_暂无公告_\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
md += '| 时间 | 事件 | 状态 | 处理 |\n';
|
||||
md += '|------|------|------|------|\n';
|
||||
|
||||
for (const evt of events.slice(0, 10)) {
|
||||
const time = evt.timestamp ? evt.timestamp.split('T')[0] : '-';
|
||||
md += `| ${time} | ${evt.event || '-'} | ${evt.status || '-'} | ${evt.handler || '-'} |\n`;
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染最近更新
|
||||
* @param {object[]} updates
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
function renderRecentUpdates(updates) {
|
||||
let md = '## 📝 最近更新\n\n';
|
||||
|
||||
if (!updates || updates.length === 0) {
|
||||
md += '_暂无更新_\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
for (const u of updates.slice(0, 5)) {
|
||||
md += `- ${u.emoji || '📌'} ${u.description || u}\n`;
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整 README
|
||||
* @param {object} systemState
|
||||
* @returns {string} markdown
|
||||
*/
|
||||
function generateReadme(systemState) {
|
||||
const state = systemState || {};
|
||||
const skin = selectSkin(state);
|
||||
const layout = generateLayout(state);
|
||||
|
||||
let md = `# ${skin.emoji[0]} 光湖 · GuangHuLab\n\n`;
|
||||
md += `> 数字地球本体论 · Digital Earth Ontology\n`;
|
||||
md += `> 版权:国作登字-2026-A-00037559\n\n`;
|
||||
|
||||
for (const section of layout) {
|
||||
switch (section) {
|
||||
case 'critical_alert':
|
||||
md += `## 🚨 紧急告警\n\n`;
|
||||
if (state.alert) {
|
||||
md += `**${state.alert.level}**: ${state.alert.laggingSide} 落后 · 偏差 ${state.alert.deficit}\n\n`;
|
||||
md += `> ${state.alert.recommendation}\n\n`;
|
||||
}
|
||||
break;
|
||||
case 'balance_drift':
|
||||
md += `## ⚠️ 平衡偏差\n\n`;
|
||||
md += `偏差值: ${state.drift || 0} · 落后侧: ${state.alert ? state.alert.laggingSide : '未知'}\n\n`;
|
||||
break;
|
||||
case 'dashboard':
|
||||
md += renderDashboard(state);
|
||||
md += '\n';
|
||||
break;
|
||||
case 'twin_balance':
|
||||
md += renderTwinBalance(state);
|
||||
md += '\n';
|
||||
break;
|
||||
case 'bulletin':
|
||||
md += renderBulletin(state.events || []);
|
||||
md += '\n';
|
||||
break;
|
||||
case 'recent_updates':
|
||||
md += renderRecentUpdates(state.recentUpdates || []);
|
||||
md += '\n';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
md += '---\n';
|
||||
md += `_由 铸渊 (AG-ZY-01) 自动生成 · ${new Date().toISOString().split('T')[0]}_\n`;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--auto')) {
|
||||
// 自动模式:读取天眼数据生成 README
|
||||
console.log('📡 README Generator · 自动更新模式\n');
|
||||
|
||||
let state = {};
|
||||
const statusPath = path.join(ROOT, '.github/tianyen/twin-status.json');
|
||||
const bulletinPath = path.join(ROOT, '.github/tianyen/bulletin-data.json');
|
||||
|
||||
if (fs.existsSync(statusPath)) {
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
||||
} catch {
|
||||
// twin-status.json 解析失败,使用空状态生成 README
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(bulletinPath)) {
|
||||
try {
|
||||
const bulletin = JSON.parse(fs.readFileSync(bulletinPath, 'utf8'));
|
||||
state.events = bulletin.events || [];
|
||||
} catch {
|
||||
// bulletin-data.json 解析失败,跳过公告数据
|
||||
}
|
||||
}
|
||||
|
||||
state.building = true;
|
||||
const readme = generateReadme(state);
|
||||
fs.writeFileSync(path.join(ROOT, 'README.md'), readme, 'utf8');
|
||||
console.log('✅ README.md 已更新');
|
||||
} else {
|
||||
console.log('📡 README Generator · 预览模式\n');
|
||||
const preview = generateReadme({ building: true });
|
||||
console.log(preview);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SKINS,
|
||||
selectSkin,
|
||||
generateLayout,
|
||||
renderDashboard,
|
||||
renderTwinBalance,
|
||||
renderBulletin,
|
||||
renderRecentUpdates,
|
||||
generateReadme,
|
||||
generateProgressBar
|
||||
};
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
// scripts/agents/tests/governance.test.js
|
||||
// Governance System · 治理系统综合测试
|
||||
// ZY-TEST-GOV-001 · Phase 1
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
passed++;
|
||||
console.log(` ✅ ${message}`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error(` ❌ ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🏛️ Governance System 综合测试\n');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 1: Twin Merge Engine
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('── 测试 1: Twin Merge Engine ──');
|
||||
|
||||
const { merge, computeComposite } = require('../twin-merge');
|
||||
|
||||
const leftMetrics = { taskCompletion: 0.9, testPassRate: 0.85, codeCoverage: 1.0, securityScore: 1.0 };
|
||||
const rightMetrics = { taskCompletion: 0.95, testPassRate: 0.9, codeCoverage: 1.0, securityScore: 1.0 };
|
||||
|
||||
const mergeResult = merge(leftMetrics, rightMetrics);
|
||||
|
||||
assert(typeof mergeResult.left === 'object', 'merge 返回 left 对象');
|
||||
assert(typeof mergeResult.right === 'object', 'merge 返回 right 对象');
|
||||
assert(typeof mergeResult.balance === 'number', 'merge 返回 balance 数值');
|
||||
assert(mergeResult.balance >= 0 && mergeResult.balance <= 1, 'balance 在 0-1 范围内');
|
||||
assert(typeof mergeResult.timestamp === 'string', 'merge 返回 timestamp');
|
||||
|
||||
// 验证 composite 计算
|
||||
const expectedLeft = Math.round((0.4*0.9 + 0.25*0.85 + 0.2*1.0 + 0.15*1.0) * 10000) / 10000;
|
||||
assert(mergeResult.left.composite === expectedLeft, `左翼 composite = ${expectedLeft} (实际: ${mergeResult.left.composite})`);
|
||||
|
||||
// 完全相同的指标 → balance = 1.0
|
||||
const perfectMerge = merge(leftMetrics, leftMetrics);
|
||||
assert(perfectMerge.balance === 1, '相同指标 → 完美平衡 (balance=1.0)');
|
||||
|
||||
// computeComposite 单独测试
|
||||
const comp = computeComposite({ taskCompletion: 1, testPassRate: 1, codeCoverage: 1, securityScore: 1 });
|
||||
assert(comp === 1, '全满分 composite = 1.0');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 2: Balance Checker
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 2: Balance Checker ──');
|
||||
|
||||
const { check } = require('../balance-checker');
|
||||
|
||||
// 完美平衡
|
||||
const balancedResult = check(perfectMerge);
|
||||
assert(balancedResult.balanced === true, '完美平衡 → balanced=true');
|
||||
assert(balancedResult.alert === null, '完美平衡 → alert=null');
|
||||
assert(balancedResult.drift === 0, '完美平衡 → drift=0');
|
||||
|
||||
// 有偏差
|
||||
const driftResult = check(mergeResult);
|
||||
assert(typeof driftResult.balanced === 'boolean', 'check 返回 balanced 布尔值');
|
||||
assert(typeof driftResult.drift === 'number', 'check 返回 drift 数值');
|
||||
|
||||
// 大偏差测试
|
||||
const bigDrift = merge(
|
||||
{ taskCompletion: 0.5, testPassRate: 0.5, codeCoverage: 0.5, securityScore: 0.5 },
|
||||
{ taskCompletion: 1.0, testPassRate: 1.0, codeCoverage: 1.0, securityScore: 1.0 }
|
||||
);
|
||||
const bigCheck = check(bigDrift);
|
||||
assert(bigCheck.balanced === false, '大偏差 → balanced=false');
|
||||
assert(bigCheck.alert !== null, '大偏差 → 有告警');
|
||||
assert(bigCheck.alert.level === 'high', '大偏差 (0.5) → high 级别');
|
||||
|
||||
// 小偏差测试
|
||||
const smallDrift = merge(
|
||||
{ taskCompletion: 0.98, testPassRate: 0.98, codeCoverage: 1.0, securityScore: 1.0 },
|
||||
{ taskCompletion: 1.0, testPassRate: 1.0, codeCoverage: 1.0, securityScore: 1.0 }
|
||||
);
|
||||
const smallCheck = check(smallDrift);
|
||||
if (!smallCheck.balanced && smallCheck.alert) {
|
||||
assert(smallCheck.alert.level === 'low', '小偏差 → low 级别');
|
||||
} else {
|
||||
assert(true, '小偏差 → 近似平衡');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 3: Auto-Repair Classifier
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 3: Auto-Repair Classifier ──');
|
||||
|
||||
const { diagnose, classify, verify } = require('../auto-repair');
|
||||
|
||||
const lowAlert = { level: 'low', laggingSide: 'left', deficit: 0.02 };
|
||||
const medAlert = { level: 'medium', laggingSide: 'right', deficit: 0.1 };
|
||||
const highAlert = { level: 'high', laggingSide: 'left', deficit: 0.3 };
|
||||
|
||||
const lowDiag = diagnose(lowAlert);
|
||||
assert(lowDiag.severity === 'low', 'low alert → severity=low');
|
||||
|
||||
const medDiag = diagnose(medAlert);
|
||||
assert(medDiag.severity === 'medium', 'medium alert → severity=medium');
|
||||
|
||||
const highDiag = diagnose(highAlert);
|
||||
assert(highDiag.severity === 'high', 'high alert → severity=high');
|
||||
|
||||
const lowClass = classify(lowDiag);
|
||||
assert(lowClass.level === 'L1', 'low severity → L1 自动修复');
|
||||
assert(lowClass.autoFixable === true, 'L1 → autoFixable=true');
|
||||
|
||||
const medClass = classify(medDiag);
|
||||
assert(medClass.level === 'L2', 'medium severity → L2 技术干预');
|
||||
assert(medClass.autoFixable === false, 'L2 → autoFixable=false');
|
||||
|
||||
const highClass = classify(highDiag);
|
||||
assert(highClass.level === 'L3', 'high severity → L3 架构干预');
|
||||
|
||||
// verify 测试
|
||||
const verifySuccess = verify({ success: true });
|
||||
assert(verifySuccess.verified === true, '成功修复 → verified=true');
|
||||
|
||||
const verifyFail = verify({ success: false, level: 'L2' });
|
||||
assert(verifyFail.verified === false, '未修复 → verified=false');
|
||||
|
||||
// null alert
|
||||
const nullDiag = diagnose(null);
|
||||
assert(nullDiag.severity === 'none', 'null alert → severity=none');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 4: Escalation Router
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 4: Escalation Router ──');
|
||||
|
||||
const { classify: escClassify, route, escalate } = require('../escalation-router');
|
||||
|
||||
// L1 类型
|
||||
assert(escClassify({ type: 'test_flaky' }).level === 'L1', 'test_flaky → L1');
|
||||
assert(escClassify({ type: 'lint_error' }).level === 'L1', 'lint_error → L1');
|
||||
assert(escClassify({ type: 'stub_missing' }).level === 'L1', 'stub_missing → L1');
|
||||
assert(escClassify({ type: 'format_issue' }).level === 'L1', 'format_issue → L1');
|
||||
|
||||
// L2 类型
|
||||
assert(escClassify({ type: 'test_persistent_fail' }).level === 'L2', 'test_persistent_fail → L2');
|
||||
assert(escClassify({ type: 'dependency_conflict' }).level === 'L2', 'dependency_conflict → L2');
|
||||
assert(escClassify({ type: 'security_alert' }).level === 'L2', 'security_alert → L2');
|
||||
|
||||
// L3 类型
|
||||
assert(escClassify({ type: 'balance_long_drift' }).level === 'L3', 'balance_long_drift → L3');
|
||||
assert(escClassify({ type: 'ontology_conflict' }).level === 'L3', 'ontology_conflict → L3');
|
||||
assert(escClassify({ type: 'cross_system_architecture' }).level === 'L3', 'cross_system_architecture → L3');
|
||||
|
||||
// 路由测试
|
||||
const l1Route = route({ type: 'test_flaky' });
|
||||
assert(l1Route.handler === 'auto_repair', 'L1 handler = auto_repair');
|
||||
assert(l1Route.notify === null, 'L1 notify = null');
|
||||
|
||||
const l2Route = route({ type: 'security_alert' });
|
||||
assert(l2Route.notify !== null, 'L2 有通知目标');
|
||||
|
||||
// 完整升级流程
|
||||
const escResult = escalate({ type: 'ontology_conflict', description: '测试' });
|
||||
assert(escResult.classification.level === 'L3', 'escalate 返回正确分类');
|
||||
assert(typeof escResult.timestamp === 'string', 'escalate 返回 timestamp');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 5: Bulletin Manager
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 5: Bulletin Manager ──');
|
||||
|
||||
const { addEvent, updateStatus, getActive, getRecent, prune, loadBulletin, saveBulletin } = require('../bulletin-manager');
|
||||
|
||||
// 清理测试环境
|
||||
const ROOT = path.resolve(__dirname, '../../..');
|
||||
const bulletinTestPath = path.join(ROOT, '.github/tianyen/bulletin-data.json');
|
||||
const bulletinBackup = fs.existsSync(bulletinTestPath) ? fs.readFileSync(bulletinTestPath, 'utf8') : null;
|
||||
|
||||
// 重置
|
||||
saveBulletin({ events: [] });
|
||||
|
||||
// 添加事件
|
||||
const added1 = addEvent({ id: 'TEST-001', event: '测试事件1', status: 'active', handler: 'auto', priority: 1 });
|
||||
assert(added1 === true, 'addEvent 成功添加');
|
||||
|
||||
const added2 = addEvent({ id: 'TEST-002', event: '测试事件2', status: 'active', handler: 'manual', priority: 2 });
|
||||
assert(added2 === true, 'addEvent 添加第二个事件');
|
||||
|
||||
// 去重
|
||||
const addDup = addEvent({ id: 'TEST-001', event: '重复事件', status: 'active' });
|
||||
assert(addDup === false, 'addEvent 去重:相同 id 不重复添加');
|
||||
|
||||
// 获取活跃事件
|
||||
const active = getActive();
|
||||
assert(active.length === 2, `getActive 返回 2 个活跃事件 (实际: ${active.length})`);
|
||||
|
||||
// 更新状态
|
||||
const updated = updateStatus('TEST-001', 'resolved');
|
||||
assert(updated === true, 'updateStatus 成功更新');
|
||||
|
||||
const activeAfter = getActive();
|
||||
assert(activeAfter.length === 1, `resolved 后活跃事件 = 1 (实际: ${activeAfter.length})`);
|
||||
|
||||
// 获取最近事件
|
||||
const recent = getRecent(5);
|
||||
assert(recent.length === 2, `getRecent 返回所有事件 (含 resolved)`);
|
||||
|
||||
// 清理过期事件(用极小 maxAge 测试)
|
||||
addEvent({ id: 'TEST-OLD', timestamp: '2020-01-01T00:00:00Z', event: '旧事件', status: 'active' });
|
||||
const pruned = prune(1000); // 1秒前的都清理
|
||||
assert(pruned >= 1, `prune 清理了至少 1 条过期事件 (清理了: ${pruned})`);
|
||||
|
||||
// 恢复测试数据
|
||||
if (bulletinBackup) {
|
||||
fs.writeFileSync(bulletinTestPath, bulletinBackup, 'utf8');
|
||||
} else {
|
||||
saveBulletin({ events: [] });
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 6: Actions Parser
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 6: Actions Parser ──');
|
||||
|
||||
const { parseWorkflowOutput, parsePRMerge, parseTestReport } = require('../actions-parser');
|
||||
|
||||
// parseWorkflowOutput
|
||||
const wfOutput = ' ✅ test1\n ✅ test2\n ❌ test3\n Error: something\n';
|
||||
const wfResult = parseWorkflowOutput(wfOutput);
|
||||
assert(wfResult.tests.passed === 2, 'parseWorkflowOutput: 2 passed');
|
||||
assert(wfResult.tests.failed === 1, 'parseWorkflowOutput: 1 failed');
|
||||
assert(wfResult.tests.total === 3, 'parseWorkflowOutput: 3 total');
|
||||
assert(wfResult.errors.length >= 1, 'parseWorkflowOutput: 检测到错误行');
|
||||
|
||||
// 空输入
|
||||
const emptyResult = parseWorkflowOutput('');
|
||||
assert(emptyResult.tests.total === 0, 'parseWorkflowOutput: 空输入 → 0 total');
|
||||
|
||||
const nullResult = parseWorkflowOutput(null);
|
||||
assert(nullResult.tests.total === 0, 'parseWorkflowOutput: null 输入 → 0 total');
|
||||
|
||||
// parsePRMerge
|
||||
const prResult = parsePRMerge({ title: 'Fix bug', number: 42, author: 'test', files: ['a.js', 'b.js'] });
|
||||
assert(prResult.filesChanged === 2, 'parsePRMerge: 2 files changed');
|
||||
assert(prResult.summary.includes('#42'), 'parsePRMerge: 包含 PR 编号');
|
||||
|
||||
const nullPr = parsePRMerge(null);
|
||||
assert(nullPr.filesChanged === 0, 'parsePRMerge: null → 0 files');
|
||||
|
||||
// parseTestReport
|
||||
const reportResult = parseTestReport('✅ a\n✅ b\n❌ c');
|
||||
assert(reportResult.passed === 2, 'parseTestReport: 2 passed');
|
||||
assert(reportResult.failed === 1, 'parseTestReport: 1 failed');
|
||||
assert(reportResult.passRate > 0.6 && reportResult.passRate < 0.7, 'parseTestReport: passRate ~0.6667');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 7: TianYen Scheduler
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 7: TianYen Scheduler ──');
|
||||
|
||||
const { evaluateSchedule, setFrequency, FREQUENCY_LEVELS } = require('../../tianyen/scheduler');
|
||||
|
||||
// 无指标 → default
|
||||
const defEval = evaluateSchedule('AG-TEST', null);
|
||||
assert(defEval.level === 'default', '无指标 → default 频率');
|
||||
|
||||
// 高偏差 → max
|
||||
const maxEval = evaluateSchedule('AG-TEST', { drift: 0.2 });
|
||||
assert(maxEval.level === 'max', '高偏差 (0.2) → max 频率');
|
||||
|
||||
// 中偏差 → high
|
||||
const highEval = evaluateSchedule('AG-TEST', { drift: 0.08 });
|
||||
assert(highEval.level === 'high', '中偏差 (0.08) → high 频率');
|
||||
|
||||
// 完美平衡 → low
|
||||
const lowEval = evaluateSchedule('AG-TEST', { balance: 1.0, drift: 0 });
|
||||
assert(lowEval.level === 'low', '完美平衡 → low 频率');
|
||||
|
||||
// 活跃变更 → high/medium
|
||||
const activeEval = evaluateSchedule('AG-TEST', { recentChanges: 15 });
|
||||
assert(activeEval.level === 'high', '大量变更 → high 频率');
|
||||
|
||||
const medEval = evaluateSchedule('AG-TEST', { recentChanges: 5 });
|
||||
assert(medEval.level === 'medium', '中等变更 → medium 频率');
|
||||
|
||||
// FREQUENCY_LEVELS 完整性
|
||||
assert(Object.keys(FREQUENCY_LEVELS).length === 5, '5 个频率级别');
|
||||
assert(FREQUENCY_LEVELS.max.cron === '*/30 * * * *', 'max cron 正确');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 8: Agent Checkin Module
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 8: Agent Checkin Module ──');
|
||||
|
||||
const { checkin, checkout, getLastCheckin, detectTimeout, loadCheckinLog, saveCheckinLog } = require('../../tianyen/agent-checkin-module');
|
||||
|
||||
// 备份
|
||||
const checkinLogPath = path.join(ROOT, '.github/tianyen/checkin-log.json');
|
||||
const checkinBackup = fs.existsSync(checkinLogPath) ? fs.readFileSync(checkinLogPath, 'utf8') : null;
|
||||
|
||||
// 重置
|
||||
saveCheckinLog({ version: '1.0.0', checkins: {} });
|
||||
|
||||
// checkin
|
||||
const ci = checkin('AG-TEST-001');
|
||||
assert(ci.agentId === 'AG-TEST-001', 'checkin 返回正确 agentId');
|
||||
assert(ci.status === 'running', 'checkin status = running');
|
||||
assert(typeof ci.timestamp === 'string', 'checkin 返回 timestamp');
|
||||
|
||||
// getLastCheckin
|
||||
const last = getLastCheckin('AG-TEST-001');
|
||||
assert(last !== null, 'getLastCheckin 返回记录');
|
||||
assert(last.status === 'running', 'getLastCheckin status = running');
|
||||
|
||||
// detectTimeout — 刚签到,不应超时
|
||||
const timeoutCheck = detectTimeout('AG-TEST-001', 600000);
|
||||
assert(timeoutCheck.timedOut === false, '刚签到 → 未超时');
|
||||
|
||||
// detectTimeout — 用极小 maxDuration 强制超时
|
||||
const shortTimeout = detectTimeout('AG-TEST-001', 0);
|
||||
assert(shortTimeout.timedOut === true, '0ms maxDuration → 超时');
|
||||
|
||||
// checkout
|
||||
const co = checkout('AG-TEST-001', 'success', { testsRun: 10 });
|
||||
assert(co.status === 'success', 'checkout status = success');
|
||||
assert(typeof co.duration === 'number', 'checkout 返回 duration');
|
||||
|
||||
// checkout 后不再检测为运行中
|
||||
const afterCo = detectTimeout('AG-TEST-001', 0);
|
||||
assert(afterCo.timedOut === false, 'checkout 后不超时');
|
||||
|
||||
// 不存在的 Agent
|
||||
const noAgent = getLastCheckin('AG-NONEXISTENT');
|
||||
assert(noAgent === null, '不存在的 Agent → null');
|
||||
|
||||
const noTimeout = detectTimeout('AG-NONEXISTENT');
|
||||
assert(noTimeout.timedOut === false, '不存在的 Agent → 不超时');
|
||||
|
||||
// 恢复
|
||||
if (checkinBackup) {
|
||||
fs.writeFileSync(checkinLogPath, checkinBackup, 'utf8');
|
||||
} else {
|
||||
saveCheckinLog({ version: '1.0.0', checkins: {} });
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 9: Twin Collector (不执行实际测试,只验证模块导出)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 9: Twin Collector 模块导出 ──');
|
||||
|
||||
const collector = require('../twin-collector');
|
||||
assert(typeof collector.collectMetrics === 'function', 'collectMetrics 是函数');
|
||||
assert(typeof collector.collectLeft === 'function', 'collectLeft 是函数');
|
||||
assert(typeof collector.collectRight === 'function', 'collectRight 是函数');
|
||||
assert(typeof collector.collectBoth === 'function', 'collectBoth 是函数');
|
||||
assert(typeof collector.WEIGHTS === 'object', 'WEIGHTS 已导出');
|
||||
assert(collector.WEIGHTS.taskCompletion === 0.4, 'taskCompletion 权重 = 0.4');
|
||||
assert(collector.WEIGHTS.testPassRate === 0.25, 'testPassRate 权重 = 0.25');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 测试 10: README Generator (SKINS 和布局)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log('\n── 测试 10: README Generator ──');
|
||||
|
||||
const {
|
||||
SKINS, selectSkin, generateLayout, renderDashboard,
|
||||
renderTwinBalance, renderBulletin, generateReadme
|
||||
} = require('../readme-generator');
|
||||
|
||||
assert(Object.keys(SKINS).length === 5, '5 种皮肤');
|
||||
assert(SKINS.CALM.badgeColor === 'blue', 'CALM badgeColor = blue');
|
||||
assert(SKINS.EMERGENCY.badgeColor === 'red', 'EMERGENCY badgeColor = red');
|
||||
|
||||
// selectSkin
|
||||
assert(selectSkin(null).name === SKINS.CALM.name, 'null state → CALM');
|
||||
assert(selectSkin({ alert: { level: 'high' } }).name === SKINS.EMERGENCY.name, 'high alert → EMERGENCY');
|
||||
assert(selectSkin({ alert: { level: 'medium' } }).name === SKINS.ATTENTION.name, 'medium alert → ATTENTION');
|
||||
assert(selectSkin({ building: true }).name === SKINS.BUILDING.name, 'building → BUILDING');
|
||||
|
||||
// generateLayout
|
||||
const emergencyLayout = generateLayout({ alert: { level: 'high' }, balanced: false });
|
||||
assert(emergencyLayout[0] === 'critical_alert', '紧急布局首项 = critical_alert');
|
||||
|
||||
const calmLayout = generateLayout({});
|
||||
assert(calmLayout.includes('dashboard'), '常规布局包含 dashboard');
|
||||
|
||||
// renderDashboard
|
||||
const dashMd = renderDashboard({});
|
||||
assert(dashMd.includes('系统状态'), 'dashboard 包含系统状态');
|
||||
|
||||
// renderTwinBalance
|
||||
const twinMd = renderTwinBalance({ metrics: { left: { composite: 0.9, dimensions: { taskCompletion: 0.9, testPassRate: 0.8, codeCoverage: 1.0, securityScore: 1.0 } }, right: { composite: 0.95, dimensions: { taskCompletion: 0.95, testPassRate: 0.9, codeCoverage: 1.0, securityScore: 1.0 } } }, balance: 0.95 });
|
||||
assert(twinMd.includes('双子天平'), 'twin balance 包含标题');
|
||||
assert(twinMd.includes('EXE-Engine'), 'twin balance 包含 EXE-Engine');
|
||||
|
||||
// renderBulletin
|
||||
const bulletinMd = renderBulletin([]);
|
||||
assert(bulletinMd.includes('暂无公告'), '空公告 → 暂无公告');
|
||||
|
||||
const bulletinMd2 = renderBulletin([{ timestamp: '2026-01-01T00:00:00Z', event: '测试', status: 'active', handler: 'auto' }]);
|
||||
assert(bulletinMd2.includes('测试'), '公告包含事件内容');
|
||||
|
||||
// generateReadme
|
||||
const readme = generateReadme({});
|
||||
assert(readme.includes('光湖'), 'README 包含光湖');
|
||||
assert(readme.includes('版权'), 'README 包含版权');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 结果汇总
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
console.log(`\n${'═'.repeat(60)}`);
|
||||
console.log(`🏛️ 治理系统测试结果: ${passed} passed, ${failed} failed`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\n❌ ${failed} 个测试失败`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\n✅ 全部 ${passed} 个测试通过`);
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// scripts/agents/twin-balance-workflow.js
|
||||
// Twin Balance Workflow · 双子天平主流程
|
||||
// ZY-P1-TWIN-005 · Phase 1 · Twin Balance Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const TIANYEN_DIR = path.join(ROOT, '.github/tianyen');
|
||||
const STATUS_PATH = path.join(TIANYEN_DIR, 'twin-status.json');
|
||||
|
||||
const { collectBoth } = require('./twin-collector');
|
||||
const { merge } = require('./twin-merge');
|
||||
const { check } = require('./balance-checker');
|
||||
const { diagnose, classify, repair, verify } = require('./auto-repair');
|
||||
|
||||
/**
|
||||
* 运行完整的双子天平检查流程
|
||||
* 采集 → 融合 → 校验 → 修复(如需要)
|
||||
* @returns {object} 完整结果
|
||||
*/
|
||||
function runPipeline() {
|
||||
console.log('⚖️ Twin Balance Check · 双子天平校验流程\n');
|
||||
|
||||
// Step 1: 采集双翼数据
|
||||
console.log('── Step 1: 数据采集 ──');
|
||||
const data = collectBoth();
|
||||
console.log(` 左翼 (EXE-Engine): tests=${data.left.details.totalPassed}/${data.left.details.totalTests}`);
|
||||
console.log(` 右翼 (Grid-DB): tests=${data.right.details.totalPassed}/${data.right.details.totalTests}`);
|
||||
|
||||
// Step 2: 融合分析
|
||||
console.log('\n── Step 2: 融合分析 ──');
|
||||
const merged = merge(data.left, data.right);
|
||||
console.log(` 左翼 composite: ${merged.left.composite}`);
|
||||
console.log(` 右翼 composite: ${merged.right.composite}`);
|
||||
console.log(` 平衡度: ${merged.balance}`);
|
||||
|
||||
// Step 3: 平衡校验
|
||||
console.log('\n── Step 3: 平衡校验 ──');
|
||||
const checkResult = check(merged);
|
||||
if (checkResult.balanced) {
|
||||
console.log(' ✅ 天平完全平衡');
|
||||
} else {
|
||||
console.log(` ⚠️ 偏差等级: ${checkResult.alert.level}`);
|
||||
console.log(` 📉 落后侧: ${checkResult.alert.laggingSide}`);
|
||||
console.log(` 📊 偏差值: ${checkResult.drift}`);
|
||||
}
|
||||
|
||||
// Step 4: 修复(如需要)
|
||||
let repairResult = null;
|
||||
let verifyResult = null;
|
||||
if (!checkResult.balanced && checkResult.alert) {
|
||||
console.log('\n── Step 4: 自动修复 ──');
|
||||
const diag = diagnose(checkResult.alert);
|
||||
const cls = classify(diag);
|
||||
console.log(` 诊断: ${diag.cause}`);
|
||||
console.log(` 分类: ${cls.level} - ${cls.name}`);
|
||||
|
||||
repairResult = repair(diag);
|
||||
console.log(` 修复: ${repairResult.action}`);
|
||||
|
||||
verifyResult = verify(repairResult);
|
||||
console.log(` 验证: ${verifyResult.message}`);
|
||||
}
|
||||
|
||||
// 汇总状态
|
||||
const status = {
|
||||
timestamp: new Date().toISOString(),
|
||||
metrics: {
|
||||
left: merged.left,
|
||||
right: merged.right
|
||||
},
|
||||
balance: merged.balance,
|
||||
balanced: checkResult.balanced,
|
||||
drift: checkResult.drift,
|
||||
alert: checkResult.alert,
|
||||
repair: repairResult,
|
||||
verification: verifyResult
|
||||
};
|
||||
|
||||
// 保存状态文件
|
||||
if (!fs.existsSync(TIANYEN_DIR)) {
|
||||
fs.mkdirSync(TIANYEN_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(STATUS_PATH, JSON.stringify(status, null, 2), 'utf8');
|
||||
|
||||
console.log(`\n✅ 状态已写入 ${STATUS_PATH}`);
|
||||
return status;
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
runPipeline();
|
||||
}
|
||||
|
||||
module.exports = { runPipeline };
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
// scripts/agents/twin-collector.js
|
||||
// Twin Data Collector · 双子数据采集器
|
||||
// ZY-P1-TWIN-001 · Phase 1 · Twin Balance Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const TIANYEN_DIR = path.join(ROOT, '.github/tianyen');
|
||||
const TWIN_DATA_PATH = path.join(TIANYEN_DIR, 'twin-data.json');
|
||||
|
||||
// ── 维度权重 · 四维天平 ─────────────────────────────────────────────────
|
||||
const WEIGHTS = {
|
||||
taskCompletion: 0.40,
|
||||
testPassRate: 0.25,
|
||||
codeCoverage: 0.20,
|
||||
securityScore: 0.15
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行测试命令,解析 ✅/❌ 计数
|
||||
* @param {string} cmd - 测试命令
|
||||
* @returns {{ passed: number, failed: number, total: number, output: string }}
|
||||
*/
|
||||
function runTests(cmd) {
|
||||
let output = '';
|
||||
try {
|
||||
output = execSync(cmd, {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
timeout: 60000,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
} catch (err) {
|
||||
// 测试失败时 execSync 会抛错,但 stderr/stdout 仍有数据
|
||||
output = (err.stdout || '') + '\n' + (err.stderr || '');
|
||||
}
|
||||
|
||||
const passCount = (output.match(/✅/g) || []).length;
|
||||
const failCount = (output.match(/❌/g) || []).length;
|
||||
const total = passCount + failCount;
|
||||
|
||||
return { passed: passCount, failed: failCount, total, output };
|
||||
}
|
||||
|
||||
/**
|
||||
* 采集单侧指标
|
||||
* @param {'exe-engine'|'grid-db'} side
|
||||
* @returns {{ taskCompletion: number, testPassRate: number, codeCoverage: number, securityScore: number, composite: number, details: object }}
|
||||
*/
|
||||
function collectMetrics(side) {
|
||||
const testCommands = {
|
||||
'exe-engine': [
|
||||
'node exe-engine/tests/smoke/exe-engine.test.js',
|
||||
'node exe-engine/tests/smoke/exe-engine-p1.test.js'
|
||||
],
|
||||
'grid-db': [
|
||||
'node grid-db/tests/smoke/grid-db.test.js'
|
||||
]
|
||||
};
|
||||
|
||||
const commands = testCommands[side] || [];
|
||||
let totalPassed = 0;
|
||||
let totalFailed = 0;
|
||||
let totalTests = 0;
|
||||
const testDetails = [];
|
||||
|
||||
for (const cmd of commands) {
|
||||
const result = runTests(cmd);
|
||||
totalPassed += result.passed;
|
||||
totalFailed += result.failed;
|
||||
totalTests += result.total;
|
||||
testDetails.push({
|
||||
command: cmd,
|
||||
passed: result.passed,
|
||||
failed: result.failed,
|
||||
total: result.total
|
||||
});
|
||||
}
|
||||
|
||||
// 测试通过率
|
||||
const testPassRate = totalTests > 0 ? totalPassed / totalTests : 1.0;
|
||||
|
||||
// 任务完成度 — 基于测试结果推导
|
||||
const taskCompletion = testPassRate;
|
||||
|
||||
// 代码覆盖率 — 无覆盖工具时默认 100%
|
||||
const codeCoverage = 1.0;
|
||||
|
||||
// 安全扫描 — 默认无告警 = 100%
|
||||
const securityScore = 1.0;
|
||||
|
||||
// 综合分数 = 加权求和
|
||||
const composite =
|
||||
WEIGHTS.taskCompletion * taskCompletion +
|
||||
WEIGHTS.testPassRate * testPassRate +
|
||||
WEIGHTS.codeCoverage * codeCoverage +
|
||||
WEIGHTS.securityScore * securityScore;
|
||||
|
||||
return {
|
||||
taskCompletion: Math.round(taskCompletion * 10000) / 10000,
|
||||
testPassRate: Math.round(testPassRate * 10000) / 10000,
|
||||
codeCoverage: Math.round(codeCoverage * 10000) / 10000,
|
||||
securityScore: Math.round(securityScore * 10000) / 10000,
|
||||
composite: Math.round(composite * 10000) / 10000,
|
||||
details: {
|
||||
totalPassed,
|
||||
totalFailed,
|
||||
totalTests,
|
||||
tests: testDetails
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 采集左翼 · EXE-Engine */
|
||||
function collectLeft() {
|
||||
return collectMetrics('exe-engine');
|
||||
}
|
||||
|
||||
/** 采集右翼 · Grid-DB */
|
||||
function collectRight() {
|
||||
return collectMetrics('grid-db');
|
||||
}
|
||||
|
||||
/** 采集双翼 · 同时采集左右 */
|
||||
function collectBoth() {
|
||||
const left = collectLeft();
|
||||
const right = collectRight();
|
||||
const result = {
|
||||
left,
|
||||
right,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(TIANYEN_DIR)) {
|
||||
fs.mkdirSync(TIANYEN_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(TWIN_DATA_PATH, JSON.stringify(result, null, 2), 'utf8');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('🔬 Twin Data Collector · 双子数据采集\n');
|
||||
const data = collectBoth();
|
||||
console.log(` 左翼 (EXE-Engine): composite = ${data.left.composite}`);
|
||||
console.log(` 右翼 (Grid-DB): composite = ${data.right.composite}`);
|
||||
console.log(`\n✅ 数据已写入 ${TWIN_DATA_PATH}`);
|
||||
}
|
||||
|
||||
module.exports = { collectMetrics, collectLeft, collectRight, collectBoth, WEIGHTS };
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// scripts/agents/twin-merge.js
|
||||
// Twin Merge Engine · 双子融合引擎
|
||||
// ZY-P1-TWIN-002 · Phase 1 · Twin Balance Agent
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const { WEIGHTS } = require('./twin-collector');
|
||||
|
||||
/**
|
||||
* 计算综合分数(加权求和)
|
||||
* @param {{ taskCompletion: number, testPassRate: number, codeCoverage: number, securityScore: number }} metrics
|
||||
* @returns {number}
|
||||
*/
|
||||
function computeComposite(metrics) {
|
||||
return Math.round((
|
||||
WEIGHTS.taskCompletion * metrics.taskCompletion +
|
||||
WEIGHTS.testPassRate * metrics.testPassRate +
|
||||
WEIGHTS.codeCoverage * metrics.codeCoverage +
|
||||
WEIGHTS.securityScore * metrics.securityScore
|
||||
) * 10000) / 10000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 融合左右两翼指标 · 天平合一
|
||||
* @param {object} leftMetrics - EXE-Engine 指标
|
||||
* @param {object} rightMetrics - Grid-DB 指标
|
||||
* @returns {{ left: object, right: object, balance: number, timestamp: string }}
|
||||
*/
|
||||
function merge(leftMetrics, rightMetrics) {
|
||||
const leftComposite = computeComposite(leftMetrics);
|
||||
const rightComposite = computeComposite(rightMetrics);
|
||||
|
||||
// 平衡度 = 1 - |左-右| — 完全一致时为 1.0
|
||||
const balance = Math.round((1 - Math.abs(leftComposite - rightComposite)) * 10000) / 10000;
|
||||
|
||||
return {
|
||||
left: {
|
||||
composite: leftComposite,
|
||||
dimensions: {
|
||||
taskCompletion: leftMetrics.taskCompletion,
|
||||
testPassRate: leftMetrics.testPassRate,
|
||||
codeCoverage: leftMetrics.codeCoverage,
|
||||
securityScore: leftMetrics.securityScore
|
||||
}
|
||||
},
|
||||
right: {
|
||||
composite: rightComposite,
|
||||
dimensions: {
|
||||
taskCompletion: rightMetrics.taskCompletion,
|
||||
testPassRate: rightMetrics.testPassRate,
|
||||
codeCoverage: rightMetrics.codeCoverage,
|
||||
securityScore: rightMetrics.securityScore
|
||||
}
|
||||
},
|
||||
balance,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const { collectBoth } = require('./twin-collector');
|
||||
console.log('⚖️ Twin Merge Engine · 双子融合\n');
|
||||
const data = collectBoth();
|
||||
const result = merge(data.left, data.right);
|
||||
console.log(` 左翼 composite: ${result.left.composite}`);
|
||||
console.log(` 右翼 composite: ${result.right.composite}`);
|
||||
console.log(` 平衡度: ${result.balance}`);
|
||||
}
|
||||
|
||||
module.exports = { merge, computeComposite };
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// scripts/tianyen/agent-checkin-module.js
|
||||
// Agent Checkin Module · 通用签到模块
|
||||
// ZY-SKD-004 · Phase 1 · TianYen Scheduling
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const CHECKIN_LOG_PATH = path.join(ROOT, '.github/tianyen/checkin-log.json');
|
||||
|
||||
/**
|
||||
* 读取签到记录
|
||||
* @returns {{ version: string, checkins: object }}
|
||||
*/
|
||||
function loadCheckinLog() {
|
||||
if (!fs.existsSync(CHECKIN_LOG_PATH)) {
|
||||
return { version: '1.0.0', checkins: {} };
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(CHECKIN_LOG_PATH, 'utf8'));
|
||||
if (!data.checkins) data.checkins = {};
|
||||
return data;
|
||||
} catch (_) {
|
||||
return { version: '1.0.0', checkins: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存签到记录
|
||||
* @param {object} log
|
||||
*/
|
||||
function saveCheckinLog(log) {
|
||||
const dir = path.dirname(CHECKIN_LOG_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CHECKIN_LOG_PATH, JSON.stringify(log, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到 · Agent 报到
|
||||
* @param {string} agentId
|
||||
* @returns {{ agentId: string, timestamp: string, status: string }}
|
||||
*/
|
||||
function checkin(agentId) {
|
||||
const log = loadCheckinLog();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
log.checkins[agentId] = {
|
||||
timestamp,
|
||||
status: 'running',
|
||||
checkin_at: timestamp,
|
||||
checkout_at: null,
|
||||
metrics: {}
|
||||
};
|
||||
|
||||
saveCheckinLog(log);
|
||||
|
||||
return { agentId, timestamp, status: 'running' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 签退 · Agent 收工
|
||||
* @param {string} agentId
|
||||
* @param {string} status - 'success' | 'failure' | 'cancelled'
|
||||
* @param {object} metrics
|
||||
* @returns {{ agentId: string, timestamp: string, status: string, duration: number|null }}
|
||||
*/
|
||||
function checkout(agentId, status, metrics) {
|
||||
const log = loadCheckinLog();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
let duration = null;
|
||||
if (log.checkins[agentId] && log.checkins[agentId].checkin_at) {
|
||||
const start = new Date(log.checkins[agentId].checkin_at).getTime();
|
||||
duration = Date.now() - start;
|
||||
}
|
||||
|
||||
if (!log.checkins[agentId]) {
|
||||
log.checkins[agentId] = {};
|
||||
}
|
||||
|
||||
log.checkins[agentId].status = status || 'success';
|
||||
log.checkins[agentId].checkout_at = timestamp;
|
||||
log.checkins[agentId].timestamp = timestamp;
|
||||
log.checkins[agentId].duration = duration;
|
||||
log.checkins[agentId].metrics = metrics || {};
|
||||
|
||||
saveCheckinLog(log);
|
||||
|
||||
return { agentId, timestamp, status: status || 'success', duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一次签到记录
|
||||
* @param {string} agentId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function getLastCheckin(agentId) {
|
||||
const log = loadCheckinLog();
|
||||
return log.checkins[agentId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测超时 · Agent 是否卡住了
|
||||
* @param {string} agentId
|
||||
* @param {number} maxDurationMs - 最大允许运行时间(默认 10 分钟)
|
||||
* @returns {{ timedOut: boolean, agentId: string, elapsed: number|null }}
|
||||
*/
|
||||
function detectTimeout(agentId, maxDurationMs) {
|
||||
const maxMs = (maxDurationMs !== undefined && maxDurationMs !== null) ? maxDurationMs : 600000;
|
||||
const log = loadCheckinLog();
|
||||
const record = log.checkins[agentId];
|
||||
|
||||
if (!record || !record.checkin_at) {
|
||||
return { timedOut: false, agentId, elapsed: null };
|
||||
}
|
||||
|
||||
// 只检查正在运行的 Agent
|
||||
if (record.status !== 'running') {
|
||||
return { timedOut: false, agentId, elapsed: null };
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - new Date(record.checkin_at).getTime();
|
||||
return {
|
||||
timedOut: elapsed >= maxMs,
|
||||
agentId,
|
||||
elapsed
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const action = args[0];
|
||||
const agentId = args[1];
|
||||
|
||||
if (!action || !agentId) {
|
||||
console.log('用法: node agent-checkin-module.js <checkin|checkout> <agentId> [status]');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (action === 'checkin') {
|
||||
const result = checkin(agentId);
|
||||
console.log(`📋 ${agentId} 签到: ${result.timestamp}`);
|
||||
} else if (action === 'checkout') {
|
||||
const status = args[2] || 'success';
|
||||
const result = checkout(agentId, status);
|
||||
console.log(`📋 ${agentId} 签退: ${result.status} (耗时 ${result.duration}ms)`);
|
||||
} else {
|
||||
console.log(`未知操作: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkin, checkout, getLastCheckin, detectTimeout, loadCheckinLog, saveCheckinLog };
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// scripts/tianyen/bulletin-dispatcher.js
|
||||
// Bulletin Dispatcher · 公告分发器
|
||||
// ZY-SKD-006 · Phase 1 · TianYen Scheduling
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const TIANYEN_DIR = path.join(ROOT, '.github/tianyen');
|
||||
const DISPATCH_PATH = path.join(TIANYEN_DIR, 'bulletin-dispatch.json');
|
||||
|
||||
/**
|
||||
* 安全读取 JSON 文件
|
||||
* @param {string} filePath
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function safeReadJSON(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
// JSON 损坏,跳过该数据源
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 采集所有 Agent 状态
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function collectAllAgentStatus() {
|
||||
const statuses = [];
|
||||
|
||||
// 签到记录
|
||||
const log = safeReadJSON(path.join(TIANYEN_DIR, 'checkin-log.json'));
|
||||
if (log) {
|
||||
for (const [agentId, record] of Object.entries(log.checkins || {})) {
|
||||
statuses.push({
|
||||
agentId,
|
||||
status: record.status || 'unknown',
|
||||
lastSeen: record.timestamp || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 调度配置
|
||||
const schedule = safeReadJSON(path.join(TIANYEN_DIR, 'agent-schedule.json'));
|
||||
if (schedule) {
|
||||
for (const [agentId, config] of Object.entries(schedule.agents || {})) {
|
||||
const existing = statuses.find(s => s.agentId === agentId);
|
||||
if (existing) {
|
||||
existing.schedule = config;
|
||||
} else {
|
||||
statuses.push({ agentId, status: 'configured', lastSeen: null, schedule: config });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估需要唤醒的 Agent
|
||||
* @param {object} globalState - 包含 statuses 数组
|
||||
* @returns {string[]} 需要唤醒的 Agent ID 列表
|
||||
*/
|
||||
function evaluateWakeTargets(globalState) {
|
||||
const targets = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (const agent of (globalState.statuses || [])) {
|
||||
// 事件驱动型不主动唤醒
|
||||
if (agent.schedule && agent.schedule.mode === 'event_driven') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 超过最大间隔未活动 → 唤醒
|
||||
if (agent.lastSeen) {
|
||||
const lastSeen = new Date(agent.lastSeen).getTime();
|
||||
const maxInterval = 24 * 60 * 60 * 1000; // 默认 24h
|
||||
if (now - lastSeen > maxInterval) {
|
||||
targets.push(agent.agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成公告条目
|
||||
* @param {string} agentId
|
||||
* @param {number} priority - 1(紧急) ~ 5(常规)
|
||||
* @param {string} context
|
||||
* @returns {object}
|
||||
*/
|
||||
function generateBulletinEntry(agentId, priority, context) {
|
||||
return {
|
||||
id: `DISP-${Date.now()}-${agentId}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
agentId,
|
||||
priority: priority || 3,
|
||||
context: context || '天眼调度唤醒',
|
||||
status: 'pending'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整分发流程
|
||||
* @returns {object}
|
||||
*/
|
||||
function dispatch() {
|
||||
const statuses = collectAllAgentStatus();
|
||||
const wakeTargets = evaluateWakeTargets({ statuses });
|
||||
const entries = [];
|
||||
|
||||
for (const agentId of wakeTargets) {
|
||||
entries.push(generateBulletinEntry(agentId, 2, '超时唤醒'));
|
||||
}
|
||||
|
||||
// 加载现有分发数据
|
||||
let dispatchData = { version: '1.0.0', bulletin: [] };
|
||||
if (fs.existsSync(DISPATCH_PATH)) {
|
||||
try {
|
||||
dispatchData = JSON.parse(fs.readFileSync(DISPATCH_PATH, 'utf8'));
|
||||
if (!Array.isArray(dispatchData.bulletin)) {
|
||||
dispatchData.bulletin = [];
|
||||
}
|
||||
} catch (_) {
|
||||
dispatchData = { version: '1.0.0', bulletin: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// 追加新条目
|
||||
dispatchData.bulletin.push(...entries);
|
||||
|
||||
// 保留最近 100 条
|
||||
if (dispatchData.bulletin.length > 100) {
|
||||
dispatchData.bulletin = dispatchData.bulletin.slice(-100);
|
||||
}
|
||||
|
||||
// 保存
|
||||
if (!fs.existsSync(TIANYEN_DIR)) {
|
||||
fs.mkdirSync(TIANYEN_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(DISPATCH_PATH, JSON.stringify(dispatchData, null, 2), 'utf8');
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
agentCount: statuses.length,
|
||||
wakeTargets,
|
||||
dispatched: entries.length
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('👁️ TianYen Bulletin Dispatcher · 公告分发器\n');
|
||||
|
||||
const result = dispatch();
|
||||
console.log(` Agent 总数: ${result.agentCount}`);
|
||||
console.log(` 唤醒目标: ${result.wakeTargets.length}`);
|
||||
console.log(` 已分发: ${result.dispatched}`);
|
||||
console.log('\n✅ 分发完成');
|
||||
}
|
||||
|
||||
module.exports = { collectAllAgentStatus, evaluateWakeTargets, generateBulletinEntry, dispatch };
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// scripts/tianyen/context-injector.js
|
||||
// Context Injector · 上下文注入器
|
||||
// ZY-SKD-002 · Phase 1 · TianYen Scheduling
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const TIANYEN_DIR = path.join(ROOT, '.github/tianyen');
|
||||
|
||||
/**
|
||||
* 安全读取 JSON 文件,解析失败返回 null
|
||||
* @param {string} filePath 文件路径
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function safeReadJSON(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
// 文件存在但 JSON 格式损坏,跳过该数据源继续采集其他状态
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 采集全局状态 · 汇聚各路信号
|
||||
* @returns {object} 所有 Agent 的状态汇总
|
||||
*/
|
||||
function collectGlobalState() {
|
||||
const state = {
|
||||
timestamp: new Date().toISOString(),
|
||||
twin: safeReadJSON(path.join(TIANYEN_DIR, 'twin-data.json')),
|
||||
bulletin: safeReadJSON(path.join(TIANYEN_DIR, 'bulletin-data.json')),
|
||||
schedule: safeReadJSON(path.join(TIANYEN_DIR, 'agent-schedule.json')),
|
||||
checkins: safeReadJSON(path.join(TIANYEN_DIR, 'checkin-log.json'))
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为特定 Agent 生成上下文
|
||||
* @param {string} agentId
|
||||
* @param {object} globalState
|
||||
* @returns {{ agentId: string, context: string, data: object }}
|
||||
*/
|
||||
function generateContext(agentId, globalState) {
|
||||
const data = {
|
||||
agentId,
|
||||
timestamp: globalState.timestamp,
|
||||
twinBalance: null,
|
||||
activeBulletins: 0,
|
||||
scheduleInfo: null,
|
||||
lastCheckin: null
|
||||
};
|
||||
|
||||
// 注入天平数据
|
||||
if (globalState.twin) {
|
||||
data.twinBalance = {
|
||||
leftComposite: globalState.twin.left ? globalState.twin.left.composite : null,
|
||||
rightComposite: globalState.twin.right ? globalState.twin.right.composite : null
|
||||
};
|
||||
}
|
||||
|
||||
// 注入公告数量
|
||||
if (globalState.bulletin && Array.isArray(globalState.bulletin.events)) {
|
||||
data.activeBulletins = globalState.bulletin.events.filter(e => e.status !== 'resolved').length;
|
||||
}
|
||||
|
||||
// 注入调度信息
|
||||
if (globalState.schedule && globalState.schedule.agents && globalState.schedule.agents[agentId]) {
|
||||
data.scheduleInfo = globalState.schedule.agents[agentId];
|
||||
}
|
||||
|
||||
// 注入签到信息
|
||||
if (globalState.checkins && globalState.checkins.checkins && globalState.checkins.checkins[agentId]) {
|
||||
data.lastCheckin = globalState.checkins.checkins[agentId];
|
||||
}
|
||||
|
||||
// 生成上下文文本
|
||||
const lines = [
|
||||
`[天眼上下文 · ${agentId}]`,
|
||||
`时间: ${data.timestamp}`,
|
||||
data.twinBalance ? `天平: L=${data.twinBalance.leftComposite} R=${data.twinBalance.rightComposite}` : '天平: 无数据',
|
||||
`活跃公告: ${data.activeBulletins}`,
|
||||
data.scheduleInfo ? `调度: ${data.scheduleInfo.cron} (${data.scheduleInfo.reason})` : '调度: 未配置',
|
||||
data.lastCheckin ? `上次签到: ${data.lastCheckin.timestamp}` : '上次签到: 无记录'
|
||||
];
|
||||
|
||||
return {
|
||||
agentId,
|
||||
context: lines.join('\n'),
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整注入流程 · 采集 → 生成 → 返回
|
||||
* @param {string} agentId
|
||||
* @returns {{ agentId: string, context: string, data: object }}
|
||||
*/
|
||||
function injectContext(agentId) {
|
||||
const globalState = collectGlobalState();
|
||||
return generateContext(agentId, globalState);
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const agentId = process.argv[2] || 'AG-ZY-README';
|
||||
console.log('💉 Context Injector · 上下文注入器\n');
|
||||
|
||||
const result = injectContext(agentId);
|
||||
console.log(result.context);
|
||||
}
|
||||
|
||||
module.exports = { collectGlobalState, generateContext, injectContext };
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// scripts/tianyen/scheduler.js
|
||||
// TianYen Scheduler · 天眼调度器
|
||||
// ZY-SKD-001 · Phase 1 · TianYen Scheduling
|
||||
// 版权:国作登字-2026-A-00037559
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const SCHEDULE_PATH = path.join(ROOT, '.github/tianyen/agent-schedule.json');
|
||||
|
||||
// ── 频率级别 · 五档心跳 ─────────────────────────────────────────────────
|
||||
const FREQUENCY_LEVELS = {
|
||||
max: { cron: '*/30 * * * *', name: '最高频', description: '每30分钟' },
|
||||
high: { cron: '0 */2 * * *', name: '高频', description: '每2小时' },
|
||||
medium: { cron: '0 */4 * * *', name: '中频', description: '每4小时' },
|
||||
default: { cron: '0 */6 * * *', name: '默认', description: '每6小时' },
|
||||
low: { cron: '0 9 * * *', name: '低频', description: '每天9点' }
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载调度配置
|
||||
* @returns {object}
|
||||
*/
|
||||
function loadSchedule() {
|
||||
if (!fs.existsSync(SCHEDULE_PATH)) {
|
||||
return { version: '1.0.0', last_evaluation: null, agents: {} };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(SCHEDULE_PATH, 'utf8'));
|
||||
} catch (_) {
|
||||
return { version: '1.0.0', last_evaluation: null, agents: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存调度配置
|
||||
* @param {object} schedule
|
||||
*/
|
||||
function saveSchedule(schedule) {
|
||||
const dir = path.dirname(SCHEDULE_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(SCHEDULE_PATH, JSON.stringify(schedule, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估最优调度频率 · 天眼的第六感
|
||||
* @param {string} agentId
|
||||
* @param {{ balance: number, drift: number, recentChanges: number }} metrics
|
||||
* @returns {{ level: string, cron: string, reason: string }}
|
||||
*/
|
||||
function evaluateSchedule(agentId, metrics) {
|
||||
if (!metrics) {
|
||||
return { level: 'default', ...FREQUENCY_LEVELS.default, reason: '无指标数据,使用默认频率' };
|
||||
}
|
||||
|
||||
// 高偏差 → 提高频率
|
||||
if (metrics.drift && metrics.drift >= 0.15) {
|
||||
return { level: 'max', ...FREQUENCY_LEVELS.max, reason: '严重偏差,需要最高频率监控' };
|
||||
}
|
||||
|
||||
if (metrics.drift && metrics.drift >= 0.05) {
|
||||
return { level: 'high', ...FREQUENCY_LEVELS.high, reason: '中等偏差,提高监控频率' };
|
||||
}
|
||||
|
||||
// 活跃变更 → 中高频率
|
||||
if (metrics.recentChanges && metrics.recentChanges > 10) {
|
||||
return { level: 'high', ...FREQUENCY_LEVELS.high, reason: '大量变更活动,提高频率' };
|
||||
}
|
||||
|
||||
if (metrics.recentChanges && metrics.recentChanges > 3) {
|
||||
return { level: 'medium', ...FREQUENCY_LEVELS.medium, reason: '中等变更活动' };
|
||||
}
|
||||
|
||||
// 完全平衡且安静 → 低频率
|
||||
if (metrics.balance && metrics.balance >= 1.0) {
|
||||
return { level: 'low', ...FREQUENCY_LEVELS.low, reason: '系统平静,降低频率节省资源' };
|
||||
}
|
||||
|
||||
return { level: 'default', ...FREQUENCY_LEVELS.default, reason: '常规状态' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Agent 调度频率
|
||||
* @param {string} agentId
|
||||
* @param {string} level
|
||||
* @param {string} reason
|
||||
* @returns {{ agentId: string, cron: string, level: string, reason: string }}
|
||||
*/
|
||||
function setFrequency(agentId, level, reason) {
|
||||
const freq = FREQUENCY_LEVELS[level] || FREQUENCY_LEVELS.default;
|
||||
const schedule = loadSchedule();
|
||||
|
||||
if (!schedule.agents[agentId]) {
|
||||
schedule.agents[agentId] = {};
|
||||
}
|
||||
|
||||
schedule.agents[agentId].cron = freq.cron;
|
||||
schedule.agents[agentId].mode = 'scheduled';
|
||||
schedule.agents[agentId].reason = reason || freq.description;
|
||||
schedule.agents[agentId].updated_at = new Date().toISOString();
|
||||
|
||||
schedule.last_evaluation = new Date().toISOString();
|
||||
saveSchedule(schedule);
|
||||
|
||||
return { agentId, cron: freq.cron, level, reason: reason || freq.description };
|
||||
}
|
||||
|
||||
// ── CLI 入口 ─────────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
console.log('👁️ TianYen Scheduler · 天眼调度器\n');
|
||||
|
||||
const schedule = loadSchedule();
|
||||
console.log(` 当前 Agent 数量: ${Object.keys(schedule.agents).length}`);
|
||||
console.log(` 上次评估: ${schedule.last_evaluation || '从未'}`);
|
||||
|
||||
// 如果有天平数据,基于它评估
|
||||
const statusPath = path.join(ROOT, '.github/tianyen/twin-status.json');
|
||||
let metrics = {};
|
||||
if (fs.existsSync(statusPath)) {
|
||||
try {
|
||||
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
||||
metrics = { balance: status.balance, drift: status.drift, recentChanges: 0 };
|
||||
} catch {
|
||||
// twin-status.json 损坏或不可读,使用默认空指标继续评估
|
||||
}
|
||||
}
|
||||
|
||||
const evaluation = evaluateSchedule('AG-ZY-TWIN', metrics);
|
||||
console.log(`\n AG-ZY-TWIN 评估结果:`);
|
||||
console.log(` 频率: ${evaluation.level} (${evaluation.cron})`);
|
||||
console.log(` 原因: ${evaluation.reason}`);
|
||||
|
||||
// 更新调度配置
|
||||
schedule.last_evaluation = new Date().toISOString();
|
||||
saveSchedule(schedule);
|
||||
console.log(`\n✅ 调度配置已更新`);
|
||||
}
|
||||
|
||||
module.exports = { evaluateSchedule, setFrequency, loadSchedule, saveSchedule, FREQUENCY_LEVELS };
|
||||
Loading…
Reference in New Issue