diff --git a/.github/persona-brain/ontology-patches/ONT-PATCH-007.json b/.github/persona-brain/ontology-patches/ONT-PATCH-007.json new file mode 100644 index 00000000..297c0c9b --- /dev/null +++ b/.github/persona-brain/ontology-patches/ONT-PATCH-007.json @@ -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 霜砚" +} diff --git a/.github/tianyen/agent-schedule.json b/.github/tianyen/agent-schedule.json new file mode 100644 index 00000000..241af392 --- /dev/null +++ b/.github/tianyen/agent-schedule.json @@ -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": "只在出问题时醒来" + } + } +} diff --git a/.github/tianyen/bulletin-data.json b/.github/tianyen/bulletin-data.json new file mode 100644 index 00000000..cc58a523 --- /dev/null +++ b/.github/tianyen/bulletin-data.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "events": [] +} diff --git a/.github/tianyen/bulletin-dispatch.json b/.github/tianyen/bulletin-dispatch.json new file mode 100644 index 00000000..645079cf --- /dev/null +++ b/.github/tianyen/bulletin-dispatch.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "bulletin": [] +} diff --git a/.github/tianyen/checkin-log.json b/.github/tianyen/checkin-log.json new file mode 100644 index 00000000..d8f9b806 --- /dev/null +++ b/.github/tianyen/checkin-log.json @@ -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": {} + } + } +} \ No newline at end of file diff --git a/.github/tianyen/evolution-metrics.json b/.github/tianyen/evolution-metrics.json new file mode 100644 index 00000000..9cddc608 --- /dev/null +++ b/.github/tianyen/evolution-metrics.json @@ -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 +} diff --git a/config/escalation-rules.json b/config/escalation-rules.json new file mode 100644 index 00000000..5d6a470d --- /dev/null +++ b/config/escalation-rules.json @@ -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" +} diff --git a/exe-engine/src/adapters/qwen-adapter.js b/exe-engine/src/adapters/qwen-adapter.js index ceadd27f..76fb8fb1 100644 --- a/exe-engine/src/adapters/qwen-adapter.js +++ b/exe-engine/src/adapters/qwen-adapter.js @@ -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} 异步生成器,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'; } diff --git a/exe-engine/src/context/context-manager.js b/exe-engine/src/context/context-manager.js new file mode 100644 index 00000000..3f13d32f --- /dev/null +++ b/exe-engine/src/context/context-manager.js @@ -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; diff --git a/exe-engine/src/context/session.js b/exe-engine/src/context/session.js new file mode 100644 index 00000000..895a1238 --- /dev/null +++ b/exe-engine/src/context/session.js @@ -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; diff --git a/exe-engine/src/controller/scheduler-v2.js b/exe-engine/src/controller/scheduler-v2.js new file mode 100644 index 00000000..52c44c77 --- /dev/null +++ b/exe-engine/src/controller/scheduler-v2.js @@ -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; diff --git a/exe-engine/src/executor/multi-model.js b/exe-engine/src/executor/multi-model.js new file mode 100644 index 00000000..a055965e --- /dev/null +++ b/exe-engine/src/executor/multi-model.js @@ -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} 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} 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; diff --git a/exe-engine/src/index.js b/exe-engine/src/index.js index b9ecd18c..ccba4543 100644 --- a/exe-engine/src/index.js +++ b/exe-engine/src/index.js @@ -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 }; diff --git a/exe-engine/src/monitor/dashboard.js b/exe-engine/src/monitor/dashboard.js new file mode 100644 index 00000000..66022a52 --- /dev/null +++ b/exe-engine/src/monitor/dashboard.js @@ -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; diff --git a/exe-engine/src/router/stubs/dialogue-stub.js b/exe-engine/src/router/stubs/dialogue-stub.js index 10098546..3254e29f 100644 --- a/exe-engine/src/router/stubs/dialogue-stub.js +++ b/exe-engine/src/router/stubs/dialogue-stub.js @@ -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 }; diff --git a/exe-engine/src/router/stubs/persona-stub.js b/exe-engine/src/router/stubs/persona-stub.js index 48be08cb..71445a43 100644 --- a/exe-engine/src/router/stubs/persona-stub.js +++ b/exe-engine/src/router/stubs/persona-stub.js @@ -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 }; diff --git a/exe-engine/src/router/stubs/user-stub.js b/exe-engine/src/router/stubs/user-stub.js index 7ed98240..952066d7 100644 --- a/exe-engine/src/router/stubs/user-stub.js +++ b/exe-engine/src/router/stubs/user-stub.js @@ -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 }; diff --git a/exe-engine/tests/smoke/exe-engine-p1.test.js b/exe-engine/tests/smoke/exe-engine-p1.test.js new file mode 100644 index 00000000..9fa2ec8e --- /dev/null +++ b/exe-engine/tests/smoke/exe-engine-p1.test.js @@ -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); + } +})(); diff --git a/grid-db/src/core/namespace.js b/grid-db/src/core/namespace.js new file mode 100644 index 00000000..82bbc8ab --- /dev/null +++ b/grid-db/src/core/namespace.js @@ -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} */ + 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; diff --git a/grid-db/src/events/event-log.js b/grid-db/src/events/event-log.js index 8a36a65f..87fb18cc 100644 --- a/grid-db/src/events/event-log.js +++ b/grid-db/src/events/event-log.js @@ -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} diff --git a/grid-db/src/index.js b/grid-db/src/index.js index 27ffb134..0ec274de 100644 --- a/grid-db/src/index.js +++ b/grid-db/src/index.js @@ -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 }; diff --git a/grid-db/src/index/btree.js b/grid-db/src/index/btree.js new file mode 100644 index 00000000..31ff63cb --- /dev/null +++ b/grid-db/src/index/btree.js @@ -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; diff --git a/grid-db/src/index/secondary-index.js b/grid-db/src/index/secondary-index.js new file mode 100644 index 00000000..b38257be --- /dev/null +++ b/grid-db/src/index/secondary-index.js @@ -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 的映射结构。 + * 支持范围查询(利用排序后的键列表)。 + */ +class SecondaryIndex { + /** + * @param {string} fieldName 索引的字段名 + */ + constructor(fieldName) { + if (!fieldName || typeof fieldName !== 'string') { + throw new Error('SecondaryIndex: fieldName 必须是非空字符串'); + } + this._fieldName = fieldName; + /** @type {Map>} */ + 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; diff --git a/grid-db/src/query/nearby.js b/grid-db/src/query/nearby.js new file mode 100644 index 00000000..5c433872 --- /dev/null +++ b/grid-db/src/query/nearby.js @@ -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; diff --git a/grid-db/src/query/scan.js b/grid-db/src/query/scan.js new file mode 100644 index 00000000..ab75c8fb --- /dev/null +++ b/grid-db/src/query/scan.js @@ -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; diff --git a/grid-db/tests/smoke/grid-db-p1.test.js b/grid-db/tests/smoke/grid-db-p1.test.js new file mode 100644 index 00000000..0d8f45a5 --- /dev/null +++ b/grid-db/tests/smoke/grid-db-p1.test.js @@ -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); +} diff --git a/scripts/agents/actions-parser.js b/scripts/agents/actions-parser.js new file mode 100644 index 00000000..e1a862da --- /dev/null +++ b/scripts/agents/actions-parser.js @@ -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 }; diff --git a/scripts/agents/auto-repair.js b/scripts/agents/auto-repair.js new file mode 100644 index 00000000..7ec5c07f --- /dev/null +++ b/scripts/agents/auto-repair.js @@ -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 }; diff --git a/scripts/agents/balance-checker.js b/scripts/agents/balance-checker.js new file mode 100644 index 00000000..695fb6b6 --- /dev/null +++ b/scripts/agents/balance-checker.js @@ -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 }; diff --git a/scripts/agents/bulletin-manager.js b/scripts/agents/bulletin-manager.js new file mode 100644 index 00000000..38a4d375 --- /dev/null +++ b/scripts/agents/bulletin-manager.js @@ -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 }; diff --git a/scripts/agents/escalation-router.js b/scripts/agents/escalation-router.js new file mode 100644 index 00000000..86e38130 --- /dev/null +++ b/scripts/agents/escalation-router.js @@ -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 }; diff --git a/scripts/agents/notify-adapter.js b/scripts/agents/notify-adapter.js new file mode 100644 index 00000000..e1695e51 --- /dev/null +++ b/scripts/agents/notify-adapter.js @@ -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 }; diff --git a/scripts/agents/readme-generator.js b/scripts/agents/readme-generator.js new file mode 100644 index 00000000..92581a37 --- /dev/null +++ b/scripts/agents/readme-generator.js @@ -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 +}; diff --git a/scripts/agents/tests/governance.test.js b/scripts/agents/tests/governance.test.js new file mode 100644 index 00000000..6dcfda8f --- /dev/null +++ b/scripts/agents/tests/governance.test.js @@ -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} 个测试通过`); +} diff --git a/scripts/agents/twin-balance-workflow.js b/scripts/agents/twin-balance-workflow.js new file mode 100644 index 00000000..928f942a --- /dev/null +++ b/scripts/agents/twin-balance-workflow.js @@ -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 }; diff --git a/scripts/agents/twin-collector.js b/scripts/agents/twin-collector.js new file mode 100644 index 00000000..63bcf484 --- /dev/null +++ b/scripts/agents/twin-collector.js @@ -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 }; diff --git a/scripts/agents/twin-merge.js b/scripts/agents/twin-merge.js new file mode 100644 index 00000000..5167752b --- /dev/null +++ b/scripts/agents/twin-merge.js @@ -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 }; diff --git a/scripts/tianyen/agent-checkin-module.js b/scripts/tianyen/agent-checkin-module.js new file mode 100644 index 00000000..37887b75 --- /dev/null +++ b/scripts/tianyen/agent-checkin-module.js @@ -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 [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 }; diff --git a/scripts/tianyen/bulletin-dispatcher.js b/scripts/tianyen/bulletin-dispatcher.js new file mode 100644 index 00000000..725baafb --- /dev/null +++ b/scripts/tianyen/bulletin-dispatcher.js @@ -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 }; diff --git a/scripts/tianyen/context-injector.js b/scripts/tianyen/context-injector.js new file mode 100644 index 00000000..df2d37cf --- /dev/null +++ b/scripts/tianyen/context-injector.js @@ -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 }; diff --git a/scripts/tianyen/scheduler.js b/scripts/tianyen/scheduler.js new file mode 100644 index 00000000..67b1d70c --- /dev/null +++ b/scripts/tianyen/scheduler.js @@ -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 };