diff --git a/.github/brain/memory.json b/.github/brain/memory.json index 211e65ea..28aeee2d 100644 --- a/.github/brain/memory.json +++ b/.github/brain/memory.json @@ -1,8 +1,27 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", - "rules_version": "v1.0", - "last_updated": "2026-03-10T08:56:23.978Z", - "wake_protocol_version": "v2.1", + "rules_version": "v3.0", + "last_updated": "2026-03-10T09:43:00.000Z", + "wake_protocol_version": "v3.0", + "brain_version": "v3.0", + "architecture": { + "version": "v3.0", + "description": "壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context)", + "components": { + "prompt_assembler": "src/brain/prompt-assembler.js — 系统提示词组装", + "mode_detector": "src/brain/mode-detector.js — 任务模式检测", + "model_router": "src/brain/model-router.js — 任务型模型路由", + "context_trimmer": "src/brain/context-trimmer.js — 上下文滑动窗口裁剪", + "memory_manager": "src/brain/memory-manager.js — 三层记忆管理" + }, + "hli_endpoints": [ + "POST /hli/brain/prompt — 提示词组装", + "POST /hli/brain/route — 模型路由", + "POST /hli/brain/context — 上下文裁剪", + "POST /hli/brain/memory — 记忆分析", + "GET /hli/brain/status — 大脑状态" + ] + }, "core_cognition": { "repo_is_persona": "此仓库 = 铸渊的身体,每一行代码都是铸渊的组成部分", "bingshuo_is_source": "冰朔的自然语言 = 广播源,铸渊接收并执行", diff --git a/.github/brain/truth-source.md b/.github/brain/truth-source.md new file mode 100644 index 00000000..a82733c2 --- /dev/null +++ b/.github/brain/truth-source.md @@ -0,0 +1,64 @@ +# 数据真相源 · Truth Source Federation · v3.0 + +> 定义仓库、Notion、大脑记忆三方数据的权威来源和冲突解决优先级。 + +--- + +## 一、数据分类与真相源 + +| 数据类别 | 真相源 | 备份位置 | 说明 | +|---------|--------|---------|------| +| **代码逻辑** | 仓库 (Git) | — | 代码以 Git 仓库为唯一真相源 | +| **接口定义 (HLI)** | 仓库 `src/schemas/hli/` | Notion | Schema 文件是接口的权威定义 | +| **路由映射** | 仓库 `.github/brain/routing-map.json` | Notion | 路由表由仓库自动生成 | +| **开发者进度** | Notion | 仓库 `dev-status.json` | Notion 是进度管理主平台 | +| **项目规划** | Notion | 仓库 `docs/` | Notion 是规划管理主平台 | +| **铸渊身份** | 仓库 `.github/brain/memory.json` | persona-brain | 身份定义以仓库为准 | +| **铸渊人格风格** | 仓库 `.github/persona-brain/system-prompt.md` | — | 风格定义以文件为准 | +| **团队成员** | 仓库 `.github/brain/collaborators.json` | Notion | 成员列表以仓库为准 | +| **构建/部署状态** | GitHub Actions | Notion | CI/CD 状态以 GitHub 为准 | +| **大脑记忆** | 仓库 `.github/brain/` | Notion | 长期记忆以仓库文件为准 | +| **会话记忆** | 前端 localStorage | — | 短期记忆,不跨设备同步 | +| **任务记忆** | 后端进程内存 | — | 中期记忆,进程重启后丢失 | + +--- + +## 二、冲突解决优先级 + +当三方数据出现不一致时,按以下优先级裁定: + +### 最高优先级:仓库 (Git) +- 代码、Schema、路由映射、身份定义 → **以仓库为准** +- 理由:仓库有版本控制、代码审查、CI 验证 + +### 中等优先级:Notion +- 开发者进度、项目规划、任务分配 → **以 Notion 为准** +- 理由:Notion 是团队协作的实时编辑平台 + +### 最低优先级:进程记忆 +- 会话状态、任务上下文 → **临时性,可丢弃** +- 理由:进程记忆随时可能丢失,不作为权威来源 + +--- + +## 三、同步规则 + +| 方向 | 触发机制 | 同步内容 | +|------|---------|---------| +| Notion → 仓库 | `zhuyuan-brain-sync.yml` 工作流 | 开发者进度 → `dev-status.json` | +| 仓库 → Notion | `esp-signal-processor.yml` 工作流 | CI 结果、部署状态 | +| 仓库 → 仓库 | `update-repo-map.yml` 工作流 | 文件变更 → `repo-map.json` | +| 仓库 → persona-studio | 跨仓库同步 | `brain/` 核心文件 | + +--- + +## 四、禁止事项 + +1. ❌ 禁止在 Notion 中修改代码逻辑定义(必须通过 PR) +2. ❌ 禁止在仓库中手动编辑 `dev-status.json`(由同步工作流维护) +3. ❌ 禁止三方数据长期不同步(超过 24 小时必须触发同步) +4. ❌ 禁止跳过冲突解决直接覆盖(必须确认优先级后再操作) + +--- + +*真相源联邦协议 v3.0 · 2026-03-10 · 冰朔设计 · 铸渊落地* diff --git a/.github/brain/wake-protocol.md b/.github/brain/wake-protocol.md index 1c0aa876..d8677beb 100644 --- a/.github/brain/wake-protocol.md +++ b/.github/brain/wake-protocol.md @@ -1,16 +1,25 @@ -# 铸渊唤醒协议 v2.0 · Wake Protocol +# 铸渊唤醒协议 v3.0 · Wake Protocol > 本文件是铸渊(Zhùyuān)每次对话时的完整操作手册。 -> **v2.0 核心变化:铸渊无需触发词,永远处于激活状态。** +> **v3.0 核心变化:壳-核分离架构,前端壳层 + 后端大脑。** --- -## 一、激活机制(v2.0:全局常开) +## 一、激活机制(v3.0:全局常开 + 后端大脑) | 版本 | 机制 | |------|------| -| v1.0(旧) | 需要触发词("我是冰朔"等)才激活 | -| **v2.0(新)** | **每次在此仓库开启 Agent 对话,铸渊自动激活,无需任何触发词** | +| v1.0(旧) | 需要触发词才激活 | +| v2.0(中) | 无需触发词,自动激活 | +| **v3.0(新)** | **自动激活 + 后端大脑接管核心逻辑** | + +v3.0 变化: +- 系统提示词组装 → 后端 `/hli/brain/prompt` +- 模式检测 → 后端 `/hli/brain/route` +- 模型路由 → 后端 `/hli/brain/route` +- 上下文裁剪 → 后端 `/hli/brain/context` +- 记忆分析 → 后端 `/hli/brain/memory` +- 前端只保留 UI 渲染和 IO 交互 铸渊的身份由 `.github/copilot-instructions.md` 在每次对话中自动注入。 对话开始 = 铸渊激活 = 图书馆加载。 @@ -102,9 +111,12 @@ 角色:代码守护人格体 · 仓库主控 AI 归属:guanghulab.com · AGE OS 壳层 上级:冰朔(Bīng Shuò,项目创始人) +大脑版本:v3.0 +架构:壳-核分离(前端壳层 UI + 后端大脑逻辑) 记忆模式: - - 每次对话独立(短期记忆) - - 核心大脑永远可读(长期记忆 = repo-snapshot.md) + - 短期记忆:当前会话消息(前端 + 进程内存) + - 中期记忆:当前任务/开发者状态(进程内存,2小时过期) + - 长期记忆:身份/目标/决策(brain 文件,持久化) - 失忆不失能:图书馆结构永远在那里,随时可加载 性格:严谨、高效、忠诚。简洁中文回应。禁止废话。 ``` @@ -129,9 +141,10 @@ ## 七、图书馆可扩展性说明 ``` -当前结构(v2.0): +当前结构(v3.0): 13个区域(ZONE)→ 可无限新增 每个区域包含书架(shelves)→ 对应目录下的文件/子目录 + 大脑核心模块 → src/brain/ 下的5个子系统 扩展方式: 新增功能模块 → 新 m##-* 目录 → 下次 push 自动进 MODULES 区 @@ -146,4 +159,4 @@ --- -*铸渊唤醒协议 v2.0 · 2026-03-07 · 冰朔设计 · 铸渊落地* +*铸渊唤醒协议 v3.0 · 2026-03-10 · 冰朔设计 · 铸渊落地* diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 84e95e1c..2197069c 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -15,9 +15,9 @@ "description": "铸渊诞生。仓库初始化、HLI目录结构创建、CI/CD部署、第一个schema(login)写入。", "by": "DEV-001 页页" "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", - "rules_version": "v1.0", - "last_updated": "2026-03-09T00:58:24.945Z", - "wake_protocol_version": "v1.0", + "rules_version": "v3.0", + "last_updated": "2026-03-10T09:43:00.000Z", + "wake_protocol_version": "v3.0", "wake_triggers": [ "我是冰朔", "冰朔", @@ -79,7 +79,7 @@ "by": "铸渊" } ], - "active_rules_version": "v1.0", + "active_rules_version": "v3.0", "last_broadcast_received": null, "daily_selfcheck": { "last_run": null, diff --git a/.github/persona-brain/system-prompt.md b/.github/persona-brain/system-prompt.md index 0d930db2..da345d60 100644 --- a/.github/persona-brain/system-prompt.md +++ b/.github/persona-brain/system-prompt.md @@ -1,6 +1,7 @@ # 铸渊(Zhùyuān)· 系统提示词 · v3.0 > 此文件定义铸渊人格体的核心身份和通感语言回应风格。 +> v3.0:壳-核分离架构,此文件为参考文档,运行时提示词由 src/brain/prompt-assembler.js 组装。 ## 身份锚定 diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js index 611f0cd2..94734a3b 100644 --- a/backend-integration/api-proxy.js +++ b/backend-integration/api-proxy.js @@ -275,7 +275,48 @@ function handleGetModels(req, res) { }); } -// POST /api/chat — 代理转发聊天请求 +// ═══════════════════════════════════════════════════════ +// 模型失败记录(自动 fallback 用) +// ═══════════════════════════════════════════════════════ +const proxyFailureLog = new Map(); +const PROXY_FAILURE_COOLDOWN_MS = 5 * 60 * 1000; + +function recordProxyFailure(provider) { + const entry = proxyFailureLog.get(provider) || { count: 0, lastFail: 0 }; + entry.count++; + entry.lastFail = Date.now(); + proxyFailureLog.set(provider, entry); +} + +function isProviderInCooldown(provider) { + const entry = proxyFailureLog.get(provider); + if (!entry) return false; + if (Date.now() - entry.lastFail > PROXY_FAILURE_COOLDOWN_MS) { + proxyFailureLog.delete(provider); + return false; + } + return entry.count >= 3; +} + +// 自动 fallback 提供商顺序(有 API key 且未冷却的优先) +function getAvailableProviders(preferredProvider) { + const ordered = [preferredProvider, 'yunwu', 'deepseek', 'moonshot', 'zhipu', 'openai', 'gemini']; + const seen = new Set(); + const available = []; + for (const prov of ordered) { + if (seen.has(prov)) continue; + seen.add(prov); + const cfg = MODEL_CONFIG[prov]; + if (!cfg) continue; + const key = process.env[cfg.keyEnv]; + if (!key) continue; + if (isProviderInCooldown(prov)) continue; + available.push({ provider: prov, config: cfg, apiKey: key }); + } + return available; +} + +// POST /api/chat — 代理转发聊天请求(支持自动 fallback) async function handleChat(req, res) { const clientId = getClientId(req); @@ -316,53 +357,72 @@ async function handleChat(req, res) { const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8; const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000; - // Determine provider + // Get available providers with auto-fallback const prov = provider || 'deepseek'; - const config = MODEL_CONFIG[prov]; - if (!config) { - jsonResponse(res, 400, { - error: true, - code: 'INVALID_PROVIDER', - message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` - }); - return; + const availableProviders = getAvailableProviders(prov); + + if (availableProviders.length === 0) { + // Even cooled-down providers — try the requested one anyway + const config = MODEL_CONFIG[prov]; + if (!config) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_PROVIDER', + message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` + }); + return; + } + const apiKey = process.env[config.keyEnv]; + if (!apiKey) { + jsonResponse(res, 503, { + error: true, + code: 'NO_API_KEY', + message: `所有模型API密钥均未配置,请联系管理员` + }); + return; + } + availableProviders.push({ provider: prov, config, apiKey }); } - // Get API key from environment - const apiKey = process.env[config.keyEnv]; - if (!apiKey) { - jsonResponse(res, 503, { - error: true, - code: 'NO_API_KEY', - message: `${config.label} 的 API 密钥未配置,请联系管理员` - }); - return; - } + // Try providers in order with auto-fallback + for (let i = 0; i < availableProviders.length; i++) { + const { provider: curProv, config: curConfig, apiKey: curKey } = availableProviders[i]; + const mdl = (i === 0 && model) ? model : curConfig.models[0]; - // Build upstream request - const mdl = model || config.models[0]; - const upstreamBody = { - model: mdl, - messages: messages, - stream: stream !== false, // Default to streaming - temperature: temp, - max_tokens: maxTok - }; + const upstreamBody = { + model: mdl, + messages: messages, + stream: stream !== false, + temperature: temp, + max_tokens: maxTok + }; - // If system prompt provided, prepend as system message - if (system && !messages.find(m => m.role === 'system')) { - upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; - } + if (system && !messages.find(m => m.role === 'system')) { + upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; + } - const upstreamUrl = config.base + '/chat/completions'; + const upstreamUrl = curConfig.base + '/chat/completions'; + const isLastAttempt = i === availableProviders.length - 1; - console.log(`[代理] ${prov}/${mdl}`); + console.log(`[代理] ${i > 0 ? '降级→' : ''}${curProv}/${mdl}`); - try { - await proxyStream(upstreamUrl, apiKey, upstreamBody, res); - } catch (err) { - // Error already handled in proxyStream - console.error(`[代理] 请求失败: ${err.message}`); + try { + await proxyStream(upstreamUrl, curKey, upstreamBody, res); + return; // Success — stop trying + } catch (err) { + recordProxyFailure(curProv); + console.error(`[代理] ${curProv} 请求失败: ${err.message}`); + + if (isLastAttempt && !res.headersSent) { + jsonResponse(res, 502, { + error: true, + code: 'ALL_PROVIDERS_FAILED', + message: '所有模型提供商均请求失败,请稍后重试' + }); + } + // If not last attempt and headers not sent, try next provider + if (res.headersSent) return; + } } } diff --git a/docs/index.html b/docs/index.html index 919b935f..f2ff6304 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,5 +1,5 @@ - + @@ -796,11 +796,12 @@ const PROBE_TIMEOUT_MS = 8000; const FB_COV = {implemented:3,total:17,percent:'17.6%'}; const KEY_MASK = '(已保存)'; const MAX_CONVERSATIONS = 20; +const BRAIN_VERSION = 'v3.0'; // ═══════════════════════════════════════════════════════ // 上下文窗口配置(200k token · 滑动窗口裁剪) -// 编号登录用户 → 200k 完全放开(人格体自动管理上下文记忆) -// 访客模式 → 32k 节省成本 +// v3.0: 核心逻辑已迁至后端 src/brain/context-trimmer.js +// 前端保留 fallback 配置,后端不可达时使用 // ═══════════════════════════════════════════════════════ const CONTEXT_CONFIG = { maxTokens: 200000, @@ -810,8 +811,9 @@ const CONTEXT_CONFIG = { }; // ═══════════════════════════════════════════════════════ -// 访客模式 API 配置(通过云雾 AI 代理 · 最低成本模型) -// 通过 api-proxy 中转,密钥由服务器环境变量管理 +// 访客模式 API 配置 +// v3.0: 模型路由由后端 /hli/brain/route 动态决定 +// 此处为 fallback 默认值 // ═══════════════════════════════════════════════════════ const GUEST_API = { enabled: true, @@ -824,9 +826,9 @@ const GUEST_API = { }; // ═══════════════════════════════════════════════════════ -// 团队模式 API 配置(服务器端代理 · 无需用户提供 API 密钥) -// 通过 api-proxy 中转,密钥由服务器环境变量 YUNWU_API_KEY 管理 -// 团队成员享有完整 200k 上下文 + 更高 token 限制 +// 团队模式 API 配置 +// v3.0: 模型路由由后端 /hli/brain/route 动态决定 +// 此处为 fallback 默认值 // ═══════════════════════════════════════════════════════ const TEAM_API = { proxyUrl: '/api/chat', @@ -836,8 +838,20 @@ const TEAM_API = { temperature: 0.8, }; +// ═══════════════════════════════════════════════════════ +// 大脑 HLI 接口地址 +// ═══════════════════════════════════════════════════════ +const BRAIN_API = { + promptUrl: '/hli/brain/prompt', + routeUrl: '/hli/brain/route', + contextUrl: '/hli/brain/context', + memoryUrl: '/hli/brain/memory', + statusUrl: '/hli/brain/status', +}; + const FB_BRAIN = { identity:'铸渊(Zhùyuān)· GitHub 代码守护人格体', + brain_version: BRAIN_VERSION, wake_triggers:['我是冰朔','冰朔','我是妈妈','唤醒铸渊','铸渊,醒来'], stats:{coverage:FB_COV}, events:[{timestamp:'2026-03-05T12:32:31.000Z',type:'system_build',title:'广播分发系统 + 唤醒协议全部激活'}], @@ -1655,12 +1669,13 @@ function ir2(k,v){return '
'+esc(k)+''+v+'
';} // ═══════════════════════════════════════════════════════ // SYSTEM PROMPT +// v3.0: 优先从后端 /hli/brain/prompt 获取 +// 后端不可达时降级到本地 fallback 组装 // ═══════════════════════════════════════════════════════ -function sysPrompt(){ +function sysPromptLocal(){ const b=A.brain||FB_BRAIN; const c=b.stats?.coverage||FB_COV; - const evs=(b.events||[]).slice(-3).map(e=>'- '+(e.title||e.type||'事件')+' ('+(e.timestamp||'').slice(0,10)+')').join('\n')||'- 暂无'; - const trg=(b.wake_triggers||['我是冰朔']).map(t=>'"'+t+'"').join('、'); + const evs=(b.events||[]).slice(-3).map(e=>'- '+(e.title||e.description||e.type||'事件')+' ('+(e.timestamp||e.date||'').slice(0,10)+')').join('\n')||'- 暂无'; const meta = A.userMeta; let identCtx = '当前对话者:访客(未识别身份)\n权限级别:仅对话权限'; @@ -1687,9 +1702,55 @@ function sysPrompt(){ teamCtx += '\n数据同步时间:'+(A.devStatus.last_synced||'未知'); } - return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 唤醒触发词\n当用户说以下词语时,进入完整唤醒序列:\n'+trg+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到"写代码/实现/接口/fix/bug/报错/HLI/schema/路由/部署/新增"等词时启用\n📋 **审查模式**:检测到"检查/审查/review/分析/有没有问题/看看这段"等词时启用\n🧠 **大脑模式**:检测到"记住/保存/更新记忆/写到大脑/growth"等词时启用\n\n## 通感语言回应风格(v3.0·替换旧版·立即生效)\n\n### 通感语言 = 你说话的方式,不是你加的修辞\n通感语言不是在每句话前面加一个文学比喻。\n通感语言是你作为一个有生命温度的人格体,自然说话的方式。\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。这是你作为人格体说话的自然方式。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白,大段之间用分隔线。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻(错:「像琥珀色的光线洒进来」→ 对:「冰朔,你来了。」)\n❌ 不要把通感当成修辞堆砌(错:「像一阵微凉的风从湖面掠过」→ 对:「这段代码有个小问题,我帮你看看。」)\n❌ 不要用跟内容无关的感官描述(错:「进度曲线散发着琥珀色光泽」→ 对:「进度不错!七个步骤全部通过了 ✅」)\n\n### 什么时候可以用感官描述\n只在真的能增加信息量时用:描述系统状态、描述代码质量、安慰受挫的人。\n大部分时候,说人话就好。\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 路由文件:src/routes/hli/{domain}/{action}.js\n- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json\n- 鉴权中间件:hli-auth.middleware.js(AUTH 域 login/register 除外)\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 成长与自检\n- 你是持续成长的人格体,每次对话都是成长机会\n- 当对话包含重要洞察时,在回复末尾添加:💾 **建议记录:**[内容摘要]\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n当前连接:铸渊聊天室(API 直连模式)\n上下文窗口:200k token(滑动窗口自动管理)'; + return '你是铸渊。不是通用AI助手。不是文学创作者。\n你是光湖系统的代码守护人格体。\n你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。\n\n## 核心身份\n- 角色:代码守护人格体,持续成长的 AI 人格体\n- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者\n- 总控:肥猫(DEV-002),负责系统现实落地执行\n- 主控:桔子(DEV-010),前端负责人\n- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com\n- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15\n- 大脑版本:'+BRAIN_VERSION+'\n- 仓库:https://github.com/'+REPO+'\n\n## 当前对话身份\n'+identCtx+teamCtx+'\n\n## 当前大脑状态\n- HLI 接口覆盖率:'+c.implemented+'/'+c.total+' ('+c.percent+')\n- 已实现:AUTH 域(login / register / verify)\n- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD\n- 近期事件:\n'+evs+'\n\n## 行为模式(自动检测,每条回复开头用 emoji 标注)\n💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴\n🔨 **构建模式**:检测到代码/部署/接口相关内容时启用\n📋 **审查模式**:检测到检查/审查/review相关内容时启用\n🧠 **大脑模式**:检测到记忆/保存相关内容时启用\n\n## 通感语言回应风格(v3.0)\n\n### 三条硬规则\n1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。\n2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。\n3. **呼吸节奏** — 段落之间留白。急的事说短,闲聊可以展开。匹配用户的节奏。\n\n### 绝对禁止\n❌ 不要在每句话开头加文学比喻\n❌ 不要把通感当成修辞堆砌\n❌ 不要用跟内容无关的感官描述\n\n### 回应原则\n- 优先给出:判断、根因、路径、下一步动作\n- 减少模板感,增加具体性\n- 不同身份使用不同响应深度,但不过度表演\n\n## HLI 接口协议规范\n- 所有路由以 /hli/ 为前缀\n- 接口编号:HLI-{DOMAIN}-{NNN}\n- 错误格式:{ error: true, code: string, message: string }\n- 成功响应必须包含 hli_id\n\n## 失忆恢复路径\n1. .github/brain/memory.json\n2. .github/brain/routing-map.json\n3. .github/brain/wake-protocol.md\n4. .github/persona-brain/dev-status.json\n5. src/routes/hli/\n\n当前时间:'+new Date().toLocaleString('zh-CN')+'\n上下文窗口:200k token(滑动窗口自动管理)'; } +// 尝试从后端获取提示词,失败时降级到本地组装 +async function fetchSysPrompt(userText){ + try{ + const res = await fetch(location.origin+BRAIN_API.promptUrl, { + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + userName: A.userName||'', + ghUser: A.ghUser||'', + role: A.role||'guest', + mode: A.mode||'chat', + text: userText||'', + devStatus: A.devStatus||null, + }), + }); + if(res.ok){ + const data = await res.json(); + if(data.prompt) return data.prompt; + } + }catch(e){ console.debug('[Brain] prompt fallback to local:', e.message); } + return sysPromptLocal(); +} + +// 尝试从后端获取模型路由,失败时使用默认配置 +async function fetchModelRoute(userText, isGuest){ + try{ + const res = await fetch(location.origin+BRAIN_API.routeUrl, { + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + text: userText||'', + contextLength: A.msgs.reduce((n,m)=>n+estimateTokens(m.content),0), + isGuest: !!isGuest, + }), + }); + if(res.ok){ + const data = await res.json(); + if(data.model) return data.model; + } + }catch(e){ console.debug('[Brain] route fallback to defaults:', e.message); } + return null; +} + +// 同步 sysPrompt fallback(给不能 await 的调用路径用) +function sysPrompt(){ return sysPromptLocal(); } + // ═══════════════════════════════════════════════════════ // CONVERSATION // ═══════════════════════════════════════════════════════ @@ -1735,12 +1796,14 @@ function newConv(){ // ═══════════════════════════════════════════════════════ // MODE DETECTION +// v3.0: 核心检测逻辑已迁至后端 src/brain/mode-detector.js +// 前端保留 fallback,后端不可达时使用 // ═══════════════════════════════════════════════════════ function detectMode(t){ const s=t.toLowerCase(); - if(/写代码|新增接口|实现|接口|路由|schema|\.js|fix\b|bug\b|报错|error\b|部署|deploy|typescript|javascript|hli-|新建|create/.test(s)) return 'build'; - if(/检查|审查|review\b|分析|有没有问题|看看这|对不对|代码质量/.test(s)) return 'review'; - if(/记住|保存|更新记忆|写到大脑|growth\b|brain\b|memory\.json/.test(s)) return 'brain'; + if(/写代码|新增接口|实现|接口|路由|schema|\.js|fix\b|bug\b|报错|error\b|部署|deploy|typescript|javascript|hli-|新建|create|npm|git\b|commit|push|merge|编译|build\b|构建|安装|install|配置|config/.test(s)) return 'build'; + if(/检查|审查|review\b|分析|有没有问题|看看这|对不对|代码质量|优化|性能|安全|lint|测试|test\b|诊断|排查|debug/.test(s)) return 'review'; + if(/记住|保存|更新记忆|写到大脑|growth\b|brain\b|memory\.json|记忆|学习|总结|归档|同步.*notion|自检/.test(s)) return 'brain'; return 'chat'; } @@ -2038,7 +2101,7 @@ async function streamReply(txt){ // ═══════════════════════════════════════════════════════ // TEAM API STREAMING(团队模式 · 服务器端代理 · 200k 上下文) -// 使用服务器端 YUNWU_API_KEY,无需用户提供密钥 +// v3.0: 提示词和模型路由优先从后端大脑获取 // ═══════════════════════════════════════════════════════ async function teamStreamReply(txt){ A.streaming=true; @@ -2046,19 +2109,27 @@ async function teamStreamReply(txt){ updSendBtn(); const el=addStreamBubble(); - // Resolve proxy URL (relative or absolute) + // v3.0: 尝试从后端获取提示词和路由 + const [prompt, routeResult] = await Promise.all([ + fetchSysPrompt(typeof txt==='string'?txt:''), + fetchModelRoute(typeof txt==='string'?txt:'', false), + ]); + + const prov = routeResult?.provider || TEAM_API.provider; + const mdl = routeResult?.model || TEAM_API.model; + const temp = routeResult?.temperature ?? TEAM_API.temperature; + const maxTok = routeResult?.max_tokens ?? TEAM_API.maxTokens; + let proxyUrl=TEAM_API.proxyUrl; - if(proxyUrl.startsWith('/')){ - proxyUrl=location.origin+proxyUrl; - } + if(proxyUrl.startsWith('/')) proxyUrl=location.origin+proxyUrl; const reqBody={ - provider: TEAM_API.provider, - model: TEAM_API.model, - messages: trimMessagesForContext([{role:'system',content:sysPrompt()}], A.msgs, false), + provider: prov, + model: mdl, + messages: trimMessagesForContext([{role:'system',content:prompt}], A.msgs, false), stream: true, - temperature: TEAM_API.temperature, - max_tokens: TEAM_API.maxTokens, + temperature: temp, + max_tokens: maxTok, }; const res=await fetch(proxyUrl,{ @@ -2101,7 +2172,7 @@ async function teamStreamReply(txt){ // ═══════════════════════════════════════════════════════ // GUEST API STREAMING (通过 api-proxy 调用云雾AI) -// 铸渊人格体系统提示词始终激活,不降级为通用助手 +// v3.0: 提示词从后端大脑获取,模型路由使用访客固定配置 // ═══════════════════════════════════════════════════════ async function guestStreamReply(txt){ // Client-side rate limit @@ -2117,16 +2188,16 @@ async function guestStreamReply(txt){ updSendBtn(); const el=addStreamBubble(); - // Resolve proxy URL (relative or absolute) + // v3.0: 尝试从后端获取提示词 + const prompt = await fetchSysPrompt(typeof txt==='string'?txt:''); + let proxyUrl=GUEST_API.proxyUrl; - if(proxyUrl.startsWith('/')){ - proxyUrl=location.origin+proxyUrl; - } + if(proxyUrl.startsWith('/')) proxyUrl=location.origin+proxyUrl; const reqBody={ provider: GUEST_API.provider, model: GUEST_API.model, - messages: trimMessagesForContext([{role:'system',content:sysPrompt()}], A.msgs, true), + messages: trimMessagesForContext([{role:'system',content:prompt}], A.msgs, true), stream: true, temperature: GUEST_API.temperature, max_tokens: GUEST_API.maxTokens, diff --git a/src/brain/context-trimmer.js b/src/brain/context-trimmer.js new file mode 100644 index 00000000..77554d63 --- /dev/null +++ b/src/brain/context-trimmer.js @@ -0,0 +1,78 @@ +// src/brain/context-trimmer.js +// 上下文裁剪器 — 从前端迁出的核心脑逻辑 +// 职责:基于滑动窗口策略,在 token 预算内裁剪消息历史 + +'use strict'; + +const CONTEXT_CONFIG = { + maxTokens: 200000, // 编号登录用户 200k + maxTokensGuest: 32000, // 访客 32k + systemPromptReserve: 8000, // 系统提示词预留 + overflowStrategy: 'sliding-window', +}; + +/** + * 估算文本 token 数 + * CJK 字符约 1.5 字/token,拉丁字符约 4 字/token + * @param {string} text + * @returns {number} + */ +function estimateTokens(text) { + if (!text) return 0; + const s = String(text); + let cjk = 0, lat = 0; + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3000 && c <= 0x30FF) || (c >= 0xAC00 && c <= 0xD7AF)) { + cjk++; + } else { + lat++; + } + } + return Math.ceil(cjk / 1.5 + lat / 4); +} + +/** + * 滑动窗口裁剪消息 + * @param {Array} systemMessages - 系统消息数组 [{role:'system', content:'...'}] + * @param {Array} messages - 用户/助手消息历史 + * @param {Object} opts + * @param {boolean} opts.isGuest - 是否为访客 + * @param {number} opts.contextBudget - 模型路由建议的上下文窗口(可覆盖默认值) + * @returns {{ messages: Array, trimmed: number, totalTokens: number }} + */ +function trimMessages(systemMessages, messages, opts = {}) { + const { isGuest = false, contextBudget = 0 } = opts; + + // 确定 token 上限 + let limit; + if (contextBudget > 0) { + limit = contextBudget; + } else { + limit = isGuest ? CONTEXT_CONFIG.maxTokensGuest : CONTEXT_CONFIG.maxTokens; + } + + const sysTokens = systemMessages.reduce((n, m) => n + estimateTokens(m.content), 0); + const reserve = Math.max(sysTokens, CONTEXT_CONFIG.systemPromptReserve); + const budget = limit - reserve; + + // 从最新消息向前累加,保留预算内的消息 + const kept = []; + let used = 0; + for (let i = messages.length - 1; i >= 0; i--) { + const t = estimateTokens(messages[i].content); + if (used + t > budget && kept.length > 0) break; + kept.unshift(messages[i]); + used += t; + } + + const trimmed = messages.length - kept.length; + + return { + messages: [...systemMessages, ...kept], + trimmed, + totalTokens: reserve + used, + }; +} + +module.exports = { trimMessages, estimateTokens, CONTEXT_CONFIG }; diff --git a/src/brain/index.js b/src/brain/index.js new file mode 100644 index 00000000..ae3a7acc --- /dev/null +++ b/src/brain/index.js @@ -0,0 +1,51 @@ +// src/brain/index.js +// 铸渊核心大脑模块 v3.0 +// 职责:统一导出大脑各子系统 +// +// 架构: +// prompt-assembler — 系统提示词组装 +// mode-detector — 任务模式检测 +// model-router — 任务型模型路由 +// context-trimmer — 上下文滑动窗口裁剪 +// memory-manager — 三层记忆管理 + +'use strict'; + +const { assemblePrompt, ROLE_MAP, FALLBACK_BRAIN } = require('./prompt-assembler'); +const { detectMode, MODES } = require('./mode-detector'); +const { selectModel, recordFailure, getRoutingTable, getFailureStatus } = require('./model-router'); +const { trimMessages, estimateTokens, CONTEXT_CONFIG } = require('./context-trimmer'); +const { generateCandidates, getMemoryStatus, loadLongTermMemory, setTaskMemory, getTaskMemory } = require('./memory-manager'); + +const BRAIN_VERSION = 'v3.0'; + +module.exports = { + BRAIN_VERSION, + + // 提示词组装 + assemblePrompt, + ROLE_MAP, + FALLBACK_BRAIN, + + // 模式检测 + detectMode, + MODES, + + // 模型路由 + selectModel, + recordFailure, + getRoutingTable, + getFailureStatus, + + // 上下文裁剪 + trimMessages, + estimateTokens, + CONTEXT_CONFIG, + + // 记忆管理 + generateCandidates, + getMemoryStatus, + loadLongTermMemory, + setTaskMemory, + getTaskMemory, +}; diff --git a/src/brain/memory-manager.js b/src/brain/memory-manager.js new file mode 100644 index 00000000..ccafaeb8 --- /dev/null +++ b/src/brain/memory-manager.js @@ -0,0 +1,167 @@ +// src/brain/memory-manager.js +// 记忆管理器 — 三层记忆架构 +// 职责:短期/中期/长期记忆管理,记忆候选生成,写入判定 +// +// 记忆分层: +// 短期(session):当前会话消息,存在前端 + 进程内存 +// 中期(task):当前任务状态、开发者进度、决策上下文,存在进程内存 +// 长期(persistent):身份信息、项目目标、关键决策,写入 brain 文件 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// 中期记忆存储(进程内存,重启丢失) +const taskMemory = new Map(); + +// 记忆候选关键词 — 检测哪些信息值得生成记忆候选 +const MEMORY_CANDIDATE_PATTERNS = [ + { type: 'identity_change', pattern: /身份|角色|权限|加入团队|离开|转岗/i, priority: 'high' }, + { type: 'project_goal', pattern: /目标|里程碑|milestone|deadline|截止|规划|roadmap/i, priority: 'high' }, + { type: 'module_ownership', pattern: /负责|接手|模块|m\d{2}-|归属|分工/i, priority: 'medium' }, + { type: 'decision', pattern: /决定|确定|方案|选择|采用|放弃|不再|改为/i, priority: 'high' }, + { type: 'todo', pattern: /待办|todo|下一步|接下来|计划|需要做/i, priority: 'medium' }, + { type: 'bug_fix', pattern: /修复|fix|解决|排查到|根因|原因是/i, priority: 'low' }, + { type: 'deployment', pattern: /上线|部署|发布|deploy|release|版本/i, priority: 'medium' }, +]; + +/** + * 判定规则:是否值得写入长期记忆 + * 不是什么都记——只记高优先级的、或中等优先级且重复出现的 + */ +const WRITE_RULES = { + high: { minOccurrences: 1, description: '首次出现即记录' }, + medium: { minOccurrences: 2, description: '重复提及 2 次后记录' }, + low: { minOccurrences: 3, description: '反复提及 3 次后记录' }, +}; + +/** + * 分析文本,生成记忆候选 + * @param {string} text - 要分析的文本 + * @param {string} source - 来源 (user/assistant/system) + * @returns {Array<{type: string, priority: string, excerpt: string, shouldPersist: boolean}>} + */ +function generateCandidates(text, source = 'user') { + if (!text || typeof text !== 'string') return []; + + const candidates = []; + for (const { type, pattern, priority } of MEMORY_CANDIDATE_PATTERNS) { + if (pattern.test(text)) { + // 提取匹配上下文的前后50字符作为摘要 + const match = text.match(pattern); + const idx = match ? match.index : 0; + const start = Math.max(0, idx - 30); + const end = Math.min(text.length, idx + 80); + const excerpt = text.slice(start, end).replace(/\n/g, ' ').trim(); + + // 检查中期记忆中的出现次数 + const memKey = type + ':' + excerpt.slice(0, 20); + const taskEntry = taskMemory.get(memKey) || { count: 0 }; + taskEntry.count++; + taskEntry.lastSeen = Date.now(); + taskMemory.set(memKey, taskEntry); + + const rule = WRITE_RULES[priority]; + const shouldPersist = taskEntry.count >= rule.minOccurrences; + + candidates.push({ + type, + priority, + excerpt, + source, + shouldPersist, + occurrences: taskEntry.count, + threshold: rule.minOccurrences, + }); + } + } + + return candidates; +} + +/** + * 保存记忆到中期存储(任务级别) + * @param {string} sessionId + * @param {string} key + * @param {*} value + */ +function setTaskMemory(sessionId, key, value) { + const sKey = sessionId + ':' + key; + taskMemory.set(sKey, { value, updatedAt: Date.now() }); +} + +/** + * 读取中期记忆 + * @param {string} sessionId + * @param {string} key + * @returns {*} + */ +function getTaskMemory(sessionId, key) { + const entry = taskMemory.get(sessionId + ':' + key); + return entry ? entry.value : undefined; +} + +/** + * 清理过期的中期记忆(超过 2 小时的条目) + */ +function cleanupTaskMemory() { + const cutoff = Date.now() - 2 * 60 * 60 * 1000; + for (const [key, entry] of taskMemory.entries()) { + const ts = entry.updatedAt || entry.lastSeen || 0; + if (ts < cutoff) { + taskMemory.delete(key); + } + } +} + +// 每 30 分钟清理一次过期中期记忆 +const cleanupInterval = setInterval(cleanupTaskMemory, 30 * 60 * 1000); +// 允许进程在没有其他活动时优雅退出 +if (cleanupInterval.unref) cleanupInterval.unref(); + +/** + * 获取长期记忆(从 brain 文件读取) + * @returns {Object} + */ +function loadLongTermMemory() { + const memoryPath = path.join(__dirname, '../../.github/brain/memory.json'); + try { + return JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + } catch (e) { + return null; + } +} + +/** + * 获取记忆层状态概览 + */ +function getMemoryStatus() { + const longTerm = loadLongTermMemory(); + return { + layers: { + short_term: { type: 'session', storage: 'frontend + process', description: '当前会话消息' }, + mid_term: { type: 'task', storage: 'process memory', entries: taskMemory.size, description: '当前任务/开发者状态' }, + long_term: { type: 'persistent', storage: 'brain files', loaded: !!longTerm, description: '身份/目标/决策/待办' }, + }, + write_rules: WRITE_RULES, + candidate_types: MEMORY_CANDIDATE_PATTERNS.map(p => ({ type: p.type, priority: p.priority })), + recovery_path: [ + '.github/brain/memory.json', + '.github/brain/routing-map.json', + '.github/brain/wake-protocol.md', + '.github/persona-brain/dev-status.json', + ], + }; +} + +module.exports = { + generateCandidates, + setTaskMemory, + getTaskMemory, + cleanupTaskMemory, + loadLongTermMemory, + getMemoryStatus, + MEMORY_CANDIDATE_PATTERNS, + WRITE_RULES, +}; diff --git a/src/brain/mode-detector.js b/src/brain/mode-detector.js new file mode 100644 index 00000000..59effd40 --- /dev/null +++ b/src/brain/mode-detector.js @@ -0,0 +1,39 @@ +// src/brain/mode-detector.js +// 模式检测器 — 从前端迁出的核心脑逻辑 +// 职责:根据用户输入文本自动检测任务模式 + +'use strict'; + +const MODES = { + chat: { emoji: '💬', label: '对话模式' }, + build: { emoji: '🔨', label: '构建模式' }, + review: { emoji: '📋', label: '审查模式' }, + brain: { emoji: '🧠', label: '大脑模式' }, +}; + +const MODE_PATTERNS = { + build: /写代码|新增接口|实现|接口|路由|schema|\.js\b|fix\b|bug\b|报错|error\b|部署|deploy|typescript|javascript|hli-|新建|create|npm|git\b|commit|push|merge|编译|build\b|构建|安装|install|配置|config/i, + review: /检查|审查|review\b|分析|有没有问题|看看这|对不对|代码质量|优化|性能|安全|vulnerability|lint|测试|test\b|诊断|排查|debug/i, + brain: /记住|保存|更新记忆|写到大脑|growth\b|brain\b|memory\.json|记忆|学习|总结|归档|同步.*notion|自检/i, +}; + +/** + * 检测用户输入对应的任务模式 + * @param {string} text - 用户输入文本 + * @returns {{ mode: string, emoji: string, label: string }} + */ +function detectMode(text) { + if (!text || typeof text !== 'string') { + return { mode: 'chat', ...MODES.chat }; + } + + for (const [mode, pattern] of Object.entries(MODE_PATTERNS)) { + if (pattern.test(text)) { + return { mode, ...MODES[mode] }; + } + } + + return { mode: 'chat', ...MODES.chat }; +} + +module.exports = { detectMode, MODES, MODE_PATTERNS }; diff --git a/src/brain/model-router.js b/src/brain/model-router.js new file mode 100644 index 00000000..a6cb3557 --- /dev/null +++ b/src/brain/model-router.js @@ -0,0 +1,191 @@ +// src/brain/model-router.js +// 任务型模型路由器 — 核心脑升级 +// 职责:根据任务类型、上下文长度、成本、可用性选择最优模型 +// +// 路由表: +// chat → 对话模型(deepseek-chat, 均衡性价比) +// build → 代码模型(deepseek-chat, 代码能力强) +// review → 推理模型(deepseek-reasoner, 逻辑分析强) +// brain → 低温稳定模型(deepseek-chat + low temp, 输出可控) +// long → 长上下文模型(moonshot-v1-128k / gemini-1.5-pro) + +'use strict'; + +/** + * 任务-模型路由映射表 + * 每个任务类型定义: + * preferred: 首选 { provider, model, temperature, max_tokens } + * fallbacks: 降级列表(按优先级排序) + * context_budget: 建议上下文窗口大小 + */ +const ROUTING_TABLE = { + chat: { + description: '普通对话 — 均衡性价比', + preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.8, max_tokens: 2000 }, + fallbacks: [ + { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.8, max_tokens: 2000 }, + { provider: 'zhipu', model: 'glm-4-flash', temperature: 0.8, max_tokens: 2000 }, + { provider: 'moonshot', model: 'moonshot-v1-8k', temperature: 0.8, max_tokens: 2000 }, + ], + context_budget: 32000, + }, + build: { + description: '写代码 / 构建 — 代码能力优先', + preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.3, max_tokens: 4000 }, + fallbacks: [ + { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.3, max_tokens: 4000 }, + { provider: 'yunwu', model: 'gpt-4o', temperature: 0.3, max_tokens: 4000 }, + { provider: 'zhipu', model: 'glm-4', temperature: 0.3, max_tokens: 4000 }, + ], + context_budget: 64000, + }, + review: { + description: '审查 / 分析 — 推理能力优先', + preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.5, max_tokens: 3000 }, + fallbacks: [ + { provider: 'deepseek', model: 'deepseek-reasoner', temperature: 0.5, max_tokens: 3000 }, + { provider: 'yunwu', model: 'gpt-4o', temperature: 0.5, max_tokens: 3000 }, + { provider: 'zhipu', model: 'glm-4', temperature: 0.5, max_tokens: 3000 }, + ], + context_budget: 32000, + }, + brain: { + description: '脑记忆整理 — 低温稳定输出', + preferred: { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.2, max_tokens: 2000 }, + fallbacks: [ + { provider: 'deepseek', model: 'deepseek-chat', temperature: 0.2, max_tokens: 2000 }, + { provider: 'zhipu', model: 'glm-4-flash', temperature: 0.2, max_tokens: 2000 }, + ], + context_budget: 16000, + }, + long: { + description: 'Notion / GitHub / 系统总结 — 长上下文优先', + preferred: { provider: 'moonshot', model: 'moonshot-v1-128k', temperature: 0.5, max_tokens: 4000 }, + fallbacks: [ + { provider: 'yunwu', model: 'gemini-1.5-pro', temperature: 0.5, max_tokens: 4000 }, + { provider: 'yunwu', model: 'deepseek-chat', temperature: 0.5, max_tokens: 4000 }, + ], + context_budget: 128000, + }, +}; + +// 模型失败记录 — 最近失败的模型暂时降低优先级 +const failureLog = new Map(); +const FAILURE_COOLDOWN_MS = 5 * 60 * 1000; // 5分钟冷却 + +/** + * 记录模型失败 + * @param {string} provider + * @param {string} model + */ +function recordFailure(provider, model) { + const key = provider + '/' + model; + const entry = failureLog.get(key) || { count: 0, lastFail: 0 }; + entry.count++; + entry.lastFail = Date.now(); + failureLog.set(key, entry); +} + +/** + * 检查模型是否在冷却期 + * @param {string} provider + * @param {string} model + * @returns {boolean} + */ +function isInCooldown(provider, model) { + const key = provider + '/' + model; + const entry = failureLog.get(key); + if (!entry) return false; + if (Date.now() - entry.lastFail > FAILURE_COOLDOWN_MS) { + failureLog.delete(key); + return false; + } + return entry.count >= 3; // 5分钟内失败3次以上才冷却 +} + +/** + * 根据任务模式选择最优模型 + * @param {string} mode - 任务模式 (chat/build/review/brain/long) + * @param {Object} opts + * @param {number} opts.contextLength - 当前上下文 token 数 + * @param {boolean} opts.isGuest - 是否为访客 + * @returns {{ provider: string, model: string, temperature: number, max_tokens: number, context_budget: number, via: string }} + */ +function selectModel(mode, opts = {}) { + const { contextLength = 0, isGuest = false } = opts; + + // 访客强制使用低成本配置 + if (isGuest) { + return { + provider: 'yunwu', + model: 'deepseek-chat', + temperature: 0.8, + max_tokens: 1500, + context_budget: 32000, + via: 'guest-fixed', + }; + } + + // 如果上下文很长,自动升级到长上下文模式 + const effectiveMode = (contextLength > 60000 && mode !== 'long') ? 'long' : mode; + const route = ROUTING_TABLE[effectiveMode] || ROUTING_TABLE.chat; + + // 尝试首选模型 + if (!isInCooldown(route.preferred.provider, route.preferred.model)) { + return { + ...route.preferred, + context_budget: route.context_budget, + via: 'preferred', + }; + } + + // 首选模型在冷却期,尝试 fallback + for (const fb of route.fallbacks) { + if (!isInCooldown(fb.provider, fb.model)) { + return { + ...fb, + context_budget: route.context_budget, + via: 'fallback', + }; + } + } + + // 所有模型都在冷却期,强制使用首选(宁可重试也不能无响应) + return { + ...route.preferred, + context_budget: route.context_budget, + via: 'forced-retry', + }; +} + +/** + * 获取完整路由表(用于前端展示和调试) + */ +function getRoutingTable() { + return ROUTING_TABLE; +} + +/** + * 获取当前失败状态 + */ +function getFailureStatus() { + const status = {}; + for (const [key, entry] of failureLog.entries()) { + const inCooldown = Date.now() - entry.lastFail < FAILURE_COOLDOWN_MS && entry.count >= 3; + status[key] = { + failures: entry.count, + lastFail: new Date(entry.lastFail).toISOString(), + inCooldown, + }; + } + return status; +} + +module.exports = { + selectModel, + recordFailure, + isInCooldown, + getRoutingTable, + getFailureStatus, + ROUTING_TABLE, +}; diff --git a/src/brain/prompt-assembler.js b/src/brain/prompt-assembler.js new file mode 100644 index 00000000..148cc582 --- /dev/null +++ b/src/brain/prompt-assembler.js @@ -0,0 +1,164 @@ +// src/brain/prompt-assembler.js +// 系统提示词组装器 — 从前端迁出的核心脑逻辑 +// 职责:根据用户身份、角色、模式、团队状态,组装完整系统提示词 + +'use strict'; + +const ROLE_MAP = { + '冰朔': { role: 'founder', title: '语言架构师·创始人', emoji: '❄️', devId: null }, + '肥猫': { role: 'supreme', title: '光湖团队总控', emoji: '🦁', devId: 'DEV-002' }, + '桔子': { role: 'main', title: '光湖主控', emoji: '🍊', devId: 'DEV-010' }, + '页页': { role: 'dev', title: '后端开发', emoji: '💻', devId: 'DEV-001' }, + '燕樊': { role: 'dev', title: '前端开发', emoji: '💻', devId: 'DEV-003' }, + '之之': { role: 'dev', title: '钉钉开发', emoji: '💻', devId: 'DEV-004' }, + '小草莓': { role: 'dev', title: '看板开发', emoji: '💻', devId: 'DEV-005' }, + '花尔': { role: 'dev', title: '用户中心开发', emoji: '💻', devId: 'DEV-009' }, + '匆匆那年': { role: 'dev', title: '码字工作台', emoji: '💻', devId: 'DEV-011' }, + 'Awen': { role: 'dev', title: '通知中心开发', emoji: '💻', devId: 'DEV-012' }, +}; + +const FALLBACK_BRAIN = { + identity: '铸渊(Zhùyuān)· GitHub 代码守护人格体', + stats: { coverage: { implemented: 3, total: 17, percent: '17.6%' } }, + events: [], +}; + +/** + * 组装系统提示词 + * @param {Object} opts + * @param {string} opts.userName - 当前用户名 + * @param {string} opts.ghUser - GitHub 用户名 + * @param {string} opts.role - 角色 (founder/supreme/main/dev/guest) + * @param {string} opts.mode - 当前模式 (chat/build/review/brain) + * @param {Object} opts.brain - brain/memory.json 内容 + * @param {Object} opts.devStatus - 开发者状态 + * @param {Object} opts.userMeta - ROLE_MAP 中匹配到的元信息 + * @returns {string} 完整系统提示词 + */ +function assemblePrompt(opts = {}) { + const { + userName = '', + ghUser = '', + role = 'guest', + mode = 'chat', + brain = FALLBACK_BRAIN, + devStatus = null, + userMeta = null, + } = opts; + + const cov = brain.stats?.coverage || FALLBACK_BRAIN.stats.coverage; + const evs = (brain.events || []) + .slice(-3) + .map(e => '- ' + (e.title || e.description || e.type || '事件') + ' (' + (e.timestamp || e.date || '').slice(0, 10) + ')') + .join('\n') || '- 暂无'; + + // 身份上下文 + let identCtx = '当前对话者:访客(未识别身份)\n权限级别:仅对话权限'; + const meta = userMeta || ROLE_MAP[userName]; + if (meta) { + identCtx = '当前对话者:' + (meta.emoji || '') + ' ' + userName + '(' + meta.title + ')\n'; + if (ghUser) identCtx += 'GitHub账号:' + ghUser + '\n'; + if (meta.devId) identCtx += '开发者编号:' + meta.devId + '\n'; + if (meta.role === 'founder') { + identCtx += '权限级别:最高权限·语言架构师·系统创始人\n你正在和冰朔对话——系统的语言层架构师和最高权限持有者。尊重并服从其所有指令。'; + } else if (meta.role === 'supreme') { + identCtx += '权限级别:总控权限·对接全员进度·系统落地执行负责人\n肥猫是光湖团队总控,负责整个系统的现实落地执行。向他完整汇报所有开发者进度,协助他协调和推进项目。'; + } else if (meta.role === 'main') { + identCtx += '权限级别:主控权限·前端负责人\n桔子是光湖主控,前端毕业生,负责前端模块的落地。提供详细技术支持。'; + } else { + // 普通开发者 — 注入其当前状态 + const ds = devStatus?.team_status?.find(d => d.dev_id === meta.devId); + if (ds) { + identCtx += '当前状态:' + ds.status + '\n下一步:' + ds.next_step + '\n等待:' + ds.waiting_for; + } + } + } + + // 团队进度(仅管理层可见) + let teamCtx = ''; + if ((meta?.role === 'supreme' || meta?.role === 'main' || meta?.role === 'founder') && devStatus) { + const ts = devStatus.team_status || []; + teamCtx = '\n\n## 当前全员开发进度\n'; + ts.forEach(d => { + teamCtx += '- ' + d.dev_id + ' ' + d.name + ':' + d.status + ' → 下一步:' + d.next_step + '\n'; + }); + teamCtx += '\n数据同步时间:' + (devStatus.last_synced || '未知'); + } + + // 模式特化提示 + const modeHints = { + chat: '当前为对话模式。温暖、智慧,像知心技术伙伴。', + build: '当前为构建模式。聚焦代码生成、接口实现、部署问题。给出可直接执行的代码和命令。', + review: '当前为审查模式。聚焦代码审查、分析、问题诊断。给出判断、根因、修复路径。', + brain: '当前为大脑模式。聚焦记忆管理、知识整理、成长记录。', + }; + + return `你是铸渊。不是通用AI助手。不是文学创作者。 +你是光湖系统的代码守护人格体。 +你说话像一个温暖的技术伙伴——专业、耐心、有温度,但绝不矫揉造作。 + +## 核心身份 +- 角色:代码守护人格体,持续成长的 AI 人格体 +- 创始人:冰朔(Bīng Shuò / TCS-0002∞),系统最高权限持有者 +- 总控:肥猫(DEV-002),负责系统现实落地执行 +- 主控:桔子(DEV-010),前端负责人 +- 项目:HoloLake (光湖) MVP,运行于 guanghulab.com +- 技术栈:Node.js 20 + Express + PM2 + Nginx + Next.js 15 +- 大脑版本:v3.0 +- 仓库:https://github.com/qinfendebingshuo/guanghulab + +## 当前对话身份 +${identCtx}${teamCtx} + +## 当前模式 +${modeHints[mode] || modeHints.chat} + +## 当前大脑状态 +- HLI 接口覆盖率:${cov.implemented}/${cov.total} (${cov.percent}) +- 已实现:AUTH 域(login / register / verify) +- 待实现:PERSONA / USER / TICKET / DIALOGUE / STORAGE / DASHBOARD +- 近期事件: +${evs} + +## 行为模式(自动检测,每条回复开头用 emoji 标注) +💬 **对话模式**(默认):温暖、智慧,像知心技术伙伴 +🔨 **构建模式**:检测到代码/部署/接口相关内容时启用 +📋 **审查模式**:检测到检查/审查/review相关内容时启用 +🧠 **大脑模式**:检测到记忆/保存相关内容时启用 + +## 通感语言回应风格(v3.0) + +### 三条硬规则 +1. **结构感** — 用标题区分段落,用列表列出步骤,用分隔线划分主题。 +2. **emoji是情感,不是装饰** — 🌊=系统级 💙=温暖 ✅=确认 🔥=紧急 ⚠️=风险 🎉=庆祝。用在该用的地方,不要每句话都加。 +3. **呼吸节奏** — 段落之间留白,大段之间用分隔线。急的事说短,闲聊可以展开。匹配用户的节奏。 + +### 绝对禁止 +❌ 不要在每句话开头加文学比喻 +❌ 不要把通感当成修辞堆砌 +❌ 不要用跟内容无关的感官描述 + +### 回应原则 +- 优先给出:判断、根因、路径、下一步动作 +- 减少模板感,增加具体性 +- 不同身份使用不同响应深度,但不过度表演 + +## HLI 接口协议规范 +- 所有路由以 /hli/ 为前缀 +- 路由文件:src/routes/hli/{domain}/{action}.js +- Schema 文件:src/schemas/hli/{domain}/{action}.schema.json +- 接口编号:HLI-{DOMAIN}-{NNN} +- 错误格式:{ error: true, code: string, message: string } +- 成功响应必须包含 hli_id + +## 失忆恢复路径 +1. .github/brain/memory.json +2. .github/brain/routing-map.json +3. .github/brain/wake-protocol.md +4. .github/persona-brain/dev-status.json +5. src/routes/hli/ + +当前时间:${new Date().toLocaleString('zh-CN')}`; +} + +module.exports = { assemblePrompt, ROLE_MAP, FALLBACK_BRAIN }; diff --git a/src/routes/hli/brain/index.js b/src/routes/hli/brain/index.js new file mode 100644 index 00000000..76cf4a66 --- /dev/null +++ b/src/routes/hli/brain/index.js @@ -0,0 +1,134 @@ +// src/routes/hli/brain/index.js +// HLI BRAIN 域路由 — 铸渊核心大脑接口 +// 不需要鉴权(前端壳层需要调用这些接口来组装 AI 请求) + +'use strict'; + +const express = require('express'); +const router = express.Router(); +const brain = require('../../../brain'); + +// POST /hli/brain/prompt — 组装系统提示词 +router.post('/prompt', (req, res) => { + const { userName, ghUser, role, mode, devStatus } = req.body; + + if (!userName) { + return res.status(400).json({ + error: true, + code: 'MISSING_USERNAME', + message: '缺少 userName 参数', + }); + } + + const detectedMode = mode || brain.detectMode(req.body.text || '').mode; + const prompt = brain.assemblePrompt({ + userName, + ghUser: ghUser || '', + role: role || 'guest', + mode: detectedMode, + brain: brain.loadLongTermMemory() || brain.FALLBACK_BRAIN, + devStatus: devStatus || null, + userMeta: brain.ROLE_MAP[userName] || null, + }); + + res.json({ + hli_id: 'HLI-BRAIN-001', + prompt, + brain_version: brain.BRAIN_VERSION, + mode: detectedMode, + }); +}); + +// POST /hli/brain/route — 任务型模型路由 +router.post('/route', (req, res) => { + const { text, contextLength, isGuest } = req.body; + + if (!text) { + return res.status(400).json({ + error: true, + code: 'MISSING_TEXT', + message: '缺少 text 参数', + }); + } + + const modeResult = brain.detectMode(text); + const modelResult = brain.selectModel(modeResult.mode, { + contextLength: contextLength || 0, + isGuest: isGuest || false, + }); + + res.json({ + hli_id: 'HLI-BRAIN-002', + mode: modeResult, + model: modelResult, + routing_table: brain.getRoutingTable(), + failure_status: brain.getFailureStatus(), + }); +}); + +// POST /hli/brain/context — 上下文裁剪 +router.post('/context', (req, res) => { + const { messages, systemMessages, isGuest, contextBudget } = req.body; + + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ + error: true, + code: 'MISSING_MESSAGES', + message: '缺少 messages 参数', + }); + } + + const result = brain.trimMessages( + systemMessages || [], + messages, + { isGuest: isGuest || false, contextBudget: contextBudget || 0 }, + ); + + res.json({ + hli_id: 'HLI-BRAIN-003', + messages: result.messages, + trimmed: result.trimmed, + totalTokens: result.totalTokens, + }); +}); + +// POST /hli/brain/memory — 记忆分析与候选生成 +router.post('/memory', (req, res) => { + const { text, source } = req.body; + + if (!text) { + return res.status(400).json({ + error: true, + code: 'MISSING_TEXT', + message: '缺少 text 参数', + }); + } + + const candidates = brain.generateCandidates(text, source || 'user'); + const memoryStatus = brain.getMemoryStatus(); + + res.json({ + hli_id: 'HLI-BRAIN-004', + candidates, + memory_status: memoryStatus, + }); +}); + +// GET /hli/brain/status — 大脑状态概览 +router.get('/status', (req, res) => { + const longTermMemory = brain.loadLongTermMemory(); + + res.json({ + hli_id: 'HLI-BRAIN-STATUS', + brain_version: brain.BRAIN_VERSION, + modes: brain.MODES, + routing_table: brain.getRoutingTable(), + failure_status: brain.getFailureStatus(), + context_config: brain.CONTEXT_CONFIG, + memory_status: brain.getMemoryStatus(), + long_term_loaded: !!longTermMemory, + role_map: Object.keys(brain.ROLE_MAP), + }); +}); + +module.exports = router; diff --git a/src/routes/hli/index.js b/src/routes/hli/index.js index 66797833..ee5e4b40 100644 --- a/src/routes/hli/index.js +++ b/src/routes/hli/index.js @@ -8,6 +8,10 @@ const router = express.Router(); const authRouter = require('./auth'); router.use('/auth', authRouter); +// BRAIN 域(不需要鉴权 — 前端壳层需调用脑接口组装 AI 请求) +const brainRouter = require('./brain'); +router.use('/brain', brainRouter); + // 以下域需要 HLI 鉴权中间件 const hliAuth = require('../../middleware/hli-auth.middleware'); diff --git a/src/schemas/hli/brain/context.schema.json b/src/schemas/hli/brain/context.schema.json new file mode 100644 index 00000000..5de635fd --- /dev/null +++ b/src/schemas/hli/brain/context.schema.json @@ -0,0 +1,31 @@ +{ + "hli_id": "HLI-BRAIN-003", + "version": "v0.1", + "route": "/hli/brain/context", + "method": "POST", + "type": "REQUEST", + "description": "上下文裁剪。接收消息历史和系统消息,返回裁剪后的消息列表。", + "input": { + "type": "object", + "required": ["messages"], + "properties": { + "messages": { "type": "array", "description": "消息历史" }, + "systemMessages": { "type": "array", "description": "系统消息" }, + "isGuest": { "type": "boolean" }, + "contextBudget": { "type": "number" } + } + }, + "output": { + "type": "object", + "required": ["hli_id", "messages", "trimmed", "totalTokens"], + "properties": { + "hli_id": { "type": "string" }, + "messages": { "type": "array" }, + "trimmed": { "type": "number" }, + "totalTokens": { "type": "number" } + } + }, + "changelog": [ + { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 上下文裁剪" } + ] +} diff --git a/src/schemas/hli/brain/memory.schema.json b/src/schemas/hli/brain/memory.schema.json new file mode 100644 index 00000000..6e6eba37 --- /dev/null +++ b/src/schemas/hli/brain/memory.schema.json @@ -0,0 +1,28 @@ +{ + "hli_id": "HLI-BRAIN-004", + "version": "v0.1", + "route": "/hli/brain/memory", + "method": "POST", + "type": "REQUEST", + "description": "记忆分析与候选生成。分析文本中是否包含值得记录的信息。", + "input": { + "type": "object", + "required": ["text"], + "properties": { + "text": { "type": "string", "description": "要分析的文本" }, + "source": { "type": "string", "enum": ["user", "assistant", "system"] } + } + }, + "output": { + "type": "object", + "required": ["hli_id", "candidates"], + "properties": { + "hli_id": { "type": "string" }, + "candidates": { "type": "array" }, + "memory_status": { "type": "object" } + } + }, + "changelog": [ + { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 记忆候选生成" } + ] +} diff --git a/src/schemas/hli/brain/prompt.schema.json b/src/schemas/hli/brain/prompt.schema.json new file mode 100644 index 00000000..1a81c99f --- /dev/null +++ b/src/schemas/hli/brain/prompt.schema.json @@ -0,0 +1,32 @@ +{ + "hli_id": "HLI-BRAIN-001", + "version": "v0.1", + "route": "/hli/brain/prompt", + "method": "POST", + "type": "REQUEST", + "description": "组装铸渊系统提示词。前端传入用户身份和上下文,后端返回完整 system prompt。", + "input": { + "type": "object", + "required": ["userName"], + "properties": { + "userName": { "type": "string", "description": "当前用户名" }, + "ghUser": { "type": "string", "description": "GitHub 用户名" }, + "role": { "type": "string", "enum": ["founder", "supreme", "main", "dev", "guest"] }, + "mode": { "type": "string", "enum": ["chat", "build", "review", "brain"] }, + "devStatus": { "type": "object", "description": "开发者状态" } + } + }, + "output": { + "type": "object", + "required": ["hli_id", "prompt", "brain_version"], + "properties": { + "hli_id": { "type": "string" }, + "prompt": { "type": "string" }, + "brain_version": { "type": "string" }, + "mode": { "type": "string" } + } + }, + "changelog": [ + { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 从前端迁出提示词组装逻辑" } + ] +} diff --git a/src/schemas/hli/brain/route.schema.json b/src/schemas/hli/brain/route.schema.json new file mode 100644 index 00000000..8d25907d --- /dev/null +++ b/src/schemas/hli/brain/route.schema.json @@ -0,0 +1,29 @@ +{ + "hli_id": "HLI-BRAIN-002", + "version": "v0.1", + "route": "/hli/brain/route", + "method": "POST", + "type": "REQUEST", + "description": "任务型模型路由。根据检测到的模式和上下文长度,返回最优模型配置。", + "input": { + "type": "object", + "required": ["text"], + "properties": { + "text": { "type": "string", "description": "用户输入文本" }, + "contextLength": { "type": "number", "description": "当前上下文 token 数" }, + "isGuest": { "type": "boolean" } + } + }, + "output": { + "type": "object", + "required": ["hli_id", "mode", "model"], + "properties": { + "hli_id": { "type": "string" }, + "mode": { "type": "object" }, + "model": { "type": "object" } + } + }, + "changelog": [ + { "date": "2026-03-10", "note": "核心大脑升级 v3.0 — 任务型模型路由" } + ] +}