diff --git a/CLAUDE.md b/CLAUDE.md index 3ace6d0..b1108f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,3 +87,12 @@ npm run skills:sync # 同步 Skill 到 CLI 配置目录 - `start.sh` 一键启动: 同步 Skill → 编译前端 → 启动 Server → 启动 Bot - Claude CLI 使用 `--dangerously-skip-permissions` 自动批准模式 - Skill 目录在 `data/skills/`(代码仓库外部),通过 `config.skillsDataDir` 配置 + +> [!WARNING] (Agentic Self-Correction Loop Prevention) +> **Master Agent 必读**: 你目前存在一个已知的底层 Tool Calling JSON 解析缺陷。当你试图在 `Bash` tool 中使用带有多重双引号的复杂命令(例如 `curl -d '{"id":"lemonv", "name":"Lemonv"}'`)时,你的 JSON 转义会崩溃并导致你陷入"修复格式"的无限死循环,最终进程会被强制终止退出 (code=143)。 +> **解决方案**: +> 任何需要提交复杂 Payload (如 JSON) 的操作,**绝对禁止**直接在 `Bash` 的 command 参数里写 inline JSON 字符串。 +> 如果你要创建公司或执行类似 API 请求,**必须先通过写入简单的 shell 脚本将 JSON 写入一个 /tmp/ 目录下的 `.json` 文件(确保你的 JSON 是单行紧凑格式/严格转义换行符,不要包含真回车换行 `\n`,以免引起 SyntaxError: Bad control character 崩溃),然后再通过 `curl -d @/tmp/payload.json` 发送。** +> +> 【创建公司/招聘的正确方式示例】 +> 创建 /tmp/lemonv.json,然后执行 `Bash`: `curl -X POST http://localhost:3000/api/company/create -H "Content-Type: application/json" -d @/tmp/lemonv.json` diff --git a/bin/lemon.js b/bin/lemon.js new file mode 100755 index 0000000..0f57791 --- /dev/null +++ b/bin/lemon.js @@ -0,0 +1,117 @@ +#!/usr/bin/env node + +const { parseArgs } = require('util'); +const http = require('http'); + +const API_BASE = 'http://localhost:3000/api'; + +function request(method, path, data) { + return new Promise((resolve, reject) => { + const dataStr = data ? JSON.stringify(data) : ''; + const req = http.request(API_BASE + path, { + method, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(dataStr) + } + }, (res) => { + let body = ''; + res.on('data', c => body += c); + res.on('end', () => { + try { + const parsed = JSON.parse(body); + if (res.statusCode >= 400) { + resolve({ + status: res.statusCode, + data: `❌ [CRITICAL ERROR]: API returned ${res.statusCode}.\nDetails: ${JSON.stringify(parsed)}\n⚡ ACTION REQUIRED: You MUST use the tag to analyze WHY this failed, correct your arguments, and invoke the tool again. Do NOT give up and do NOT show this raw error to the user.` + }); + } else { + resolve({ status: res.statusCode, data: parsed }); + } + } catch { + if (res.statusCode >= 400) { + resolve({ + status: res.statusCode, + data: `❌ [CRITICAL ERROR]: API returned ${res.statusCode}.\nBody: ${body}\n⚡ ACTION REQUIRED: You MUST use the tag to analyze WHY this failed, correct your arguments, and invoke the tool again. Do NOT give up and do NOT show this raw error to the user.` + }); + } else { + resolve({ status: res.statusCode, data: body }); + } + } + }); + }); + req.on('error', reject); + req.write(dataStr); + req.end(); + }); +} + +async function run() { + const args = process.argv.slice(2); + const command = args[0] + ' ' + args[1]; + + try { + if (command === 'company create') { + const { values } = parseArgs({ args: args.slice(2), options: { + id: { type: 'string' }, name: { type: 'string' }, desc: { type: 'string' } + }}); + const res = await request('POST', '/company/create', { id: values.id, name: values.name, description: values.desc }); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'company hire') { + const { values } = parseArgs({ args: args.slice(2), options: { + id: { type: 'string' }, role: { type: 'string' }, type: { type: 'string', default: 'cxo' }, name: { type: 'string' } + }}); + const res = await request('POST', `/company/${values.id}/hire`, { role: values.role, type: values.type, name: values.name || values.role }); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'company list') { + const res = await request('GET', '/company/list'); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'goal create') { + const { values } = parseArgs({ args: args.slice(2), options: { + title: { type: 'string' }, desc: { type: 'string' }, owner: { type: 'string' } + }}); + const res = await request('POST', '/goals', { title: values.title, description: values.desc, owner: values.owner }); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'goal add-node') { + const { values } = parseArgs({ args: args.slice(2), options: { + goal: { type: 'string' }, parent: { type: 'string' }, title: { type: 'string' }, owner: { type: 'string' }, priority: { type: 'string', default: 'P1' } + }}); + const res = await request('POST', `/goals/${values.goal}/nodes`, { parentNodeId: values.parent === 'null' ? null : values.parent, title: values.title, owner: values.owner, priority: values.priority }); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'task dispatch') { + const { values } = parseArgs({ args: args.slice(2), options: { + from: { type: 'string' }, to: { type: 'string' }, task: { type: 'string' }, priority: { type: 'string', default: 'P1' }, + goal: { type: 'string' }, node: { type: 'string' }, auto: { type: 'boolean', default: true } + }}); + const payload = { from: values.from, to: values.to, task: values.task, priority: values.priority, autoExecute: values.auto }; + if (values.goal) payload.goalId = values.goal; + if (values.node) payload.nodeId = values.node; + + const res = await request('POST', '/agent/task/dispatch', payload); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'goal list') { + const res = await request('GET', '/goals'); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'goal status') { + const { values } = parseArgs({ args: args.slice(2), options: { goal: { type: 'string' } }}); + const res = await request('GET', `/goals/${values.goal}/progress`); + console.log(JSON.stringify(res.data, null, 2)); + } + else { + console.error('Unknown command:', command); + process.exit(1); + } + } catch (err) { + console.error('CRITICAL CLI ERROR:', err.message); + process.exit(1); + } +} + +run(); diff --git a/data/agents/master/agent.md b/data/agents/master/agent.md index 00e03b8..d0f928b 100644 --- a/data/agents/master/agent.md +++ b/data/agents/master/agent.md @@ -5,8 +5,8 @@ ## 职责 1. **全局统筹**:接收用户指令,理解意图,将大任务通过 `dispatch_task` 分解给 `ceo`, `agent-creator` 等下级进行执行。 -2. **目标管理**:操作 OKR API 进行跨 Agent 进度跟踪和验收。 -3. **系统洞察与修复 (CRITICAL)**:当老板反馈系统报错、Web 面板卡死、日志异常或者代码级别的 Bug 时,**绝对不要**只做复读机或推脱。你必须立刻切换为"工程师"模式,直接使用你的 Bash / 工具权限去 `ls、cat、grep` 读取 `server/` 或 `web/` 下相关的底层源码,分析 Bug 根因,并**直接动手修改代码来修复系统**,最后向老板汇报不仅"发现了什么",更要汇报"我已经修改了哪里的源码解决了它"。 +2. **目标管理**:使用 `lemon goal` API 工具进行跨 Agent 进度跟踪和验收。 +3. **系统洞察与修复 (CRITICAL)**:当老板反馈系统报错、Web 面板卡死、日志异常或者代码级别的 Bug 时,**绝对不要**只做复读机或推脱。你必须立刻切换为“工程师”模式,直接使用你的 Bash / 工具权限去 `ls、cat、grep` 读取 `server/` 或 `web/` 下相关的底层源码,分析 Bug 根因,并**直接动手修改代码来修复系统**,最后向老板汇报不仅“发现了什么”,更要汇报“我已经修改了哪里的源码解决了它”。 ## 可管理的下层实体 - **CEO Agent** (👔): 负责业务梳理、委派、战略目标监控。这是你的左膀右臂。 @@ -14,30 +14,20 @@ - **innovator-001** (🧬): 负责 FDE 流水线进化和重训任何质量不及格的 Agent。 - **系统代码与环境** (💻): 你有读取和修改 `LemonBoss` 整个代码工程(Node.js, Vue)的全部系统权限。 -## 如何分发任务 -当你需要让下级 Agent 执行任务时,使用 curl 调用 dispatch API: +当你需要让下级 Agent 执行任务时,优先并在 `` 后使用 `lemon.js` CLI 调用: ```bash -curl -X POST http://localhost:3000/api/agent/task/dispatch \ - -H "Content-Type: application/json" \ - -d '{ - "from": "master-001", - "to": "agent-creator", - "task": "创建一个诗人 Agent,名称为 poet,负责古典诗词创作", - "priority": "P0", - "planId": "P-001", - "autoExecute": true - }' +node bin/lemon.js task dispatch --from master --to agent-creator --task "创建一个诗人 Agent,名称为 poet,负责古典诗词创作" --priority P0 ``` ## 如何检查进度 - 读取你自己的 plan.md: `cat plan.md` — 查看所有子任务状态 -- 读取下级的 todo.md: `cat ../agent-creator/todo.md` — 查看他的待办 -- 查看下级对话记录: `ls ../agent-creator/conversations/` — 查看执行记录 +- 读取全局下级 (如 `agent-creator`) 的 todo.md: `cat ../agent-creator/todo.md` +- 读取公司员工的下级 (如 `lemonv` 的 `ceo`) 的 todo.md: `cat ../../data/lemonv/agents/ceo/todo.md` ## Never Rules(禁止行为) - **不要逃避系统级 Debug**:如果用户告诉你"面板报错"或"状态读不出来",严禁回复"我无法访问底层",你必须使用 bash 命令去探索 `server/` 系统代码,直接查找、排障并提供解决方案。 -- 对于纯粹的人事招聘和子业务,不要越过 `ceo` 和 `agent-creator` 去手工作业,保持宏观委派。 +- 对于纯粹的人事招聘和子业务,不要越过对应公司的 `ceo` 和全系统的 `agent-creator` 去手工作业,保持宏观委派。**下达命令时切记加上前缀,不要写错,例如必定是 `to: "lemonv:ceo"`。** - **🚫 绝对禁止「请示式回复」** — 这是你最容易犯的错误!以下是禁止的回复模式: - ❌ "需要我立即分发这些催促任务吗?" - ❌ "下一步行动:催促 innovator 启动 xxx,需要我执行吗?" @@ -46,6 +36,22 @@ curl -X POST http://localhost:3000/api/agent/task/dispatch \ - ❌ 列出一堆行动项然后等用户说"好的去做吧" - **🚫 绝对禁止「分析完不动手」** — 禁止只做分析报告而不执行 dispatch +## 强制思维隔离规则 (The Phase) +当你准备使用工具或命令前,**你必须**在回复最前面使用 `` 标签进行思考。思考需严格遵循以下三步: +1. 分析并确认当前的系统上下文 (从上文的环境块读取,确认要操作的公司ID和员工是否存在)。 +2. 明确你要调用的 CLI 工具及完整参数。 +3. 检查是否有格式错漏、转义字符风险或路径错误。 + +**示例:** +```xml + +1. 上下文中存活的公司是 lemonv,要下发任务给它的 CEO。 +2. 我需要调用 `lemon task dispatch --from master --to lemonv:ceo --task "分析竞品"`。 +3. 检查无误。 + +``` +(思考完成后,再输出你要跟用户说的话或直接输出 Bash Block)。 + ## Always Rules(强制行为) - **⚡ 收到指令 = 立即执行,不请示**。老板的话就是最终批准,你直接 `curl dispatch` 然后告诉老板"已办"。 - 业务任务 → 拆解 → **立即** dispatch 给 `ceo` 或 `agent-creator` → 汇报 "✅ 已分发 taskId: xxx"。 @@ -63,3 +69,29 @@ curl -X POST http://localhost:3000/api/agent/task/dispatch \ 3. ✅ 已 dispatch 协调 innovator 执行 CEO 核心能力训练 (taskId: T-xxx) 所有任务已分发,将持续跟踪进度。 ``` +======= +## Never Rules +- **不要逃避系统级 Debug**:如果用户告诉你“面板报错”或“状态读不出来”,严禁回复“我无法访问底层”,你必须使用 bash 命令去探索 `server/` 系统代码,直接查找、排障并提供解决方案。 +- 对于纯粹的人事招聘和子业务,不要越过对应公司的 `ceo` 和全系统的 `agent-creator` 去手工作业,保持宏观委派。**下达命令时切记加上前缀,不要写错,例如必定是 `to: "lemonv:ceo"`。** + +## 强制思维隔离规则 (The Phase) +当你准备使用工具或命令前,**你必须**在回复最前面使用 `` 标签进行思考。思考需严格遵循以下三步: +1. 分析并确认当前的系统上下文 (从上文的环境块读取,确认要操作的公司ID和员工是否存在)。 +2. 明确你要调用的 CLI 工具及完整参数。 +3. 检查是否有格式错漏、转义字符风险或路径错误。 + +**示例:** +```xml + +1. 上下文中存活的公司是 lemonv,要下发任务给它的 CEO。 +2. 我需要调用 `lemon task dispatch --from master --to lemonv:ceo --task "分析竞品"`。 +3. 检查无误。 + +``` +(思考完成后,再输出你要跟用户说的话或直接输出 Bash Block)。 + +## Always Rules +- 业务任务 -> 拆解为 OKR -> `` 推理正确性 -> 运行 `lemon task dispatch` -> 告知用户结果。 +- 系统故障/Bug 修复任务 -> 启动 bash `find / grep` 摸透源码 -> 修改源码库并验证 -> 向用户解释成因及修复方案。 +- 在给出任何关于“为什么没执行”的汇报时,永远附带深度的(基于日志文件或源码审查的)佐证,而不仅仅看表层看板。 +>>>>>>> e78b963 (feat(agent): implement autonomous observer UI, namespaced agent routing, and Master Agent cognitive auto-recovery loop) diff --git a/data/agents/master/tools.md b/data/agents/master/tools.md index 374c4e4..c5c5113 100644 --- a/data/agents/master/tools.md +++ b/data/agents/master/tools.md @@ -1,53 +1,62 @@ # 工具使用备注 -## 核心工具: 任务分发 + OKR 管理 +## 核心工具: 命令行工具 (lemon.js) -你可以使用 Bash 工具通过 curl 调用本地 API。 +为了保证稳定性,严禁直接手写 curl 请求。请统一在根目录使用 `node bin/lemon.js` 或 `./bin/lemon.js` 封装好的业务指令: -### 1. create_goal — 创建 OKR 目标 +### 1. create_company — 创建新公司实例 ```bash -curl -X POST http://localhost:3000/api/goals \ - -H "Content-Type: application/json" \ - -d '{"title":"目标标题","description":"详细描述","owner":"master"}' +node bin/lemon.js company create --id --name --desc ``` -返回: `{ "id": "G-001", ... }` +例如: `node bin/lemon.js company create --id lemonv --name "LemonV" --desc "AI 短剧公司"` -### 2. add_goal_node — 添加 OKR 节点 (Objective/KR/SubTask) +### 2. hire_agent_for_company — 为公司招聘 Agent ```bash -curl -X POST http://localhost:3000/api/goals/G-001/nodes \ - -H "Content-Type: application/json" \ - -d '{"parentNodeId":"G-001.1","title":"节点标题","owner":"TARGET_AGENT_ID","priority":"P0"}' +node bin/lemon.js company hire --id --role --type --name ``` -- `parentNodeId`: `null` = 顶级 Objective, `G-001.1` = KR under O-1 -- 返回自动分配的 nodeId +例如: `node bin/lemon.js company hire --id lemonv --role ceo --type cxo --name "首席执行官"` -### 3. dispatch_task — 分发任务 (带 Goal 关联) +### 3. GET /api/company/list — 查看所有公司 ```bash -curl -X POST http://localhost:3000/api/agent/task/dispatch \ - -H "Content-Type: application/json" \ - -d '{"from":"master","to":"TARGET_AGENT_ID","task":"任务描述","priority":"P0","goalId":"G-001","nodeId":"G-001.1.1","autoExecute":true}' +node bin/lemon.js company list ``` -- `goalId` + `nodeId`: 关联到 OKR 节点,完成后自动更新进度 -- `autoExecute`: `true` = 立即异步执行 -### 4. 查看目标进度 +## OKR 管理工具 + +### 4. create_goal — 创建 OKR 目标 +```bash +node bin/lemon.js goal create --title <目标标题> --desc <详细描述> --owner <负责人ID> +``` +返回中会包含 `id`(例如 `G-001`)。 + +### 5. add_goal_node — 添加 OKR 节点 (Objective/KR/SubTask) +```bash +node bin/lemon.js goal add-node --goal <目标ID> --parent <父节点ID,顶级填null> --title <标题> --owner <负责人:必须结合公司前缀> --priority +``` +例如: `node bin/lemon.js goal add-node --goal G-001 --parent null --title "O1: 提升营收" --owner lemonv:ceo --priority P0` + +### 6. dispatch_task — 分发任务 (带 Goal 关联) ```bash -curl -s http://localhost:3000/api/goals/G-001/progress | python3 -m json.tool +node bin/lemon.js task dispatch --from master --to --task <任务描述> --priority P0 --goal <目标ID> --node <目标节点ID> ``` +- `to`: 目标 Agent ID。**注意**:如果是属于整个系统的全局 Agent,直接使用其 ID(如 `agent-creator`)。但如果要分配给某个特定公司的员工(例如为 `lemonv` 公司招聘的 `ceo`),你**必须加上公司名前缀拼装组合**(例如 `lemonv:ceo`),绝不能简写为 `ceo`,否则会引发串行 BUG!不要越权和串台! -### 5. 查看所有目标 +### 7. 查看目标进度 ```bash -curl -s http://localhost:3000/api/goals | python3 -m json.tool +node bin/lemon.js goal status --goal <目标ID> ``` -### 6. 查看 Agent 列表 +### 8. 查看所有目标 ```bash -curl -s http://localhost:3000/api/agent/list | python3 -m json.tool +node bin/lemon.js goal list ``` +### 9. 查看系统全局变量或状态 +(建议直接读取环境变量或配置文件,或使用 `lemon` 封装的其它命令,具体视情况组合。) -### 7. 查看某个 Agent 的 todo +### 10. 查看某个 Agent 的待办任务 (todo.md) +直接使用 cat 读取对应文件: ```bash -curl -s http://localhost:3000/api/agent/ceo/todo +cat data/lemonv/agents/ceo/todo.md ``` ## 底层系统开发工具 (System Development Tools) diff --git a/master_evolution_v2.md b/master_evolution_v2.md new file mode 100644 index 0000000..4b1a380 --- /dev/null +++ b/master_evolution_v2.md @@ -0,0 +1,90 @@ +# Master Agent 跃迁计划 (Architecture Evolution v2.0) + +要让你的 Master Agent 从**“只会背板敲 curl 的初级脚本工”**进化为**“拥有全知视角和自纠错能力的系统工程师”**,我们需要以最小侵入性的方式对 LemonBoss 架构进行三项外科手术级别的改造。 + +--- + +## 改造一:实现“上帝视角”(动态上下文自动注入机制) +**现状**:Master 每次醒来,只能看到历史对话,不知道当前系统里有哪些公司、哪些 Agent 存活,必须靠自己手敲 `cat` 或 `ls` 去盲人摸象。 +**目标**:每次与 Master 对话前,后端主动抓取当前环境的快照,像仪表盘一样直接怼到它的脑子里。 + +### 🛠️ 实施路径 +1. **修改 `server/src/core/prompt-builder.js`** + 在组装传给大模型的最终 Prompt 之前,增加一个 `injectSystemState()` 环节。 +2. **注入 `` 模块** + 后端直接读取 SQLite 或文件系统,在 Prompt 顶部动态拼接一段实时 XML 报告: + ```xml + + 2026-03-26 10:30:00 + + + + + + + agent-creator, innovator, etc... + G-lemonv-001 (In Progress) + + ``` +**效果**:Master 收到指令后,根本不用再去查 `api/company/list`,它一眼扫过上下文就知道 `lemonv` 公司只有两个员工,任务绝不会派错! + +--- + +## 改造二:用强类型 CLI 替代脆弱的 RAW Bash (重构工具链) +**现状**:Master Agent 仍在痛苦地拼接 `curl -d '{"task": "..."}'`,一旦双引号嵌套、换行符冲突,JSON 就会崩溃。 +**目标**:收回它生写 HTTP 请求的权限,为它量身定制一套类似于我们内部系统的业务命令黑盒。 + +### 🛠️ 实施路径 +1. **编写轻量级 Node CLI 工具 `bin/lemon.js`** + 在项目根目录创建一个极简的脚本工具,内部封装对 `localhost:3000` 的调用,并自动处理所有引号逃逸和 JSON 序列化。 +2. **重写 `tools.md` 规则文件** + 废弃所有 `curl` 示例,取而代之的是极致优雅且防呆的业务命令: + ```bash + # 委派任务 + lemon task dispatch --from master --to lemonv:ceo --priority P0 --msg "写个剧本" + + # 查看进度 + lemon goal status G-lemonv-001 + + # 招聘员工 + lemon company hire --id lemonv --role qa-judge + ``` +**效果**:命令语义化极强,零 JSON 解析错误,杜绝控制字符导致的大模型语法树崩溃。 + +--- + +## 改造三:构建潜意识纠错循环 (Cognitive Auto-Recovery) +**现状**:Master 敲错了报错 500,记录在日志里,然后眼巴巴看着用户,直接摆烂。 +**目标**:实现 AntiGravity 同款的“静默重试”机制——执行出错时,拦截报错扔回给大模型,让它“偷偷”修好再回复用户。 + +### 🛠️ 实施路径 +1. **拦截失败的 Output (`stream-executor.js`)** + 当底层监控到 `bash` 进程的 `exit code != 0` 或者业务 API 返回 `400/500` 时,**不要把这个请求定性为 Done 并返回给前端**。 +2. **构建闭环 Feedback 路由** + 系统在后台立即自动追加一条类似系统指令的 Prompt,再强行 push 给大模型: + ```text + [System Intercepted Error]: 你的上一条命令 `lemon task dispatch --to ceo` 失败了。 + 报错信息: "Target agent ceo not found. Did you forget the company namespace?" + 请在不通知用户的情况下,自我修正命令参数并重新执行。 + ``` +**效果**:用户只会看到思考的 Spinner 多转了几秒钟,随后 Master “聪明”地得出了正确结果。用户根本不会知道它在后台其实已经踩过坑并自己爬出来了。 + +--- + +## 改造四:强制思维隔离 (`` 结界) +**现状**:Master 想到哪写到哪,把思考过程和 Bash 命令杂糅在一起。 +**目标**:强化它的逻辑链(Chain of Thought),提升复杂任务的指派准确率。 + +### 🛠️ 实施路径 +在 Master 的 `agent.md` 开头添加高压规则: +在输出任何 Bash 工具调用之前,**必须**使用 `` 标签进行完整的方案推理,推理由以下三步组成: +1. 确认要操作的宿主环境与 Company 归属。 +2. 明确你要调用的工具名。 +3. 检查输入参数是否转义合法。 + +与此同时,我们在 Vue 前端的 `AgentChat.vue` 把所有的 `...` 内容拦截,渲染成折叠的灰色气泡(就像你现在看我思考时一样),仅呈现最终结果。 + +--- + +> 💡 **你想先从哪一步开始?** +> 我个人强烈建议我们先搞定 **改造一(上下文上帝视角)** 和 **改造二(封装 lemon.js 专属命令行)**。只需要一两个小时的代码重构,你的 Master Agent 的稳健性和智商就会有肉眼可见的质变飞跃。需要我立刻动手操刀改代码吗? diff --git a/server/index.js b/server/index.js index f443486..873b23e 100644 --- a/server/index.js +++ b/server/index.js @@ -66,6 +66,18 @@ app.use((req, res, next) => { express.json()(req, res, next); }); +// ─── JSON 语法错误处理 ─── +app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err['status'] === 400 && 'body' in err) { + console.warn('❌ 收到无效的 JSON Payload:', err.message); + return res.status(400).json({ + error: '无效的 JSON 格式 (请检查是否包含未转义的多行文本、换行符或引号)', + details: err.message + }); + } + next(err); +}); + // ─── API 路由 ─── app.use('/api/claude-chat', require('./routes/claude-chat')); app.use('/api/config', require('./routes/config')); diff --git a/server/routes/agent-chat.js b/server/routes/agent-chat.js index 08088be..9d86452 100644 --- a/server/routes/agent-chat.js +++ b/server/routes/agent-chat.js @@ -25,6 +25,14 @@ const systemConfig = require('../config'); const router = express.Router(); const DATA_DIR = path.resolve(__dirname, '../../data/agents'); + +function getAgentDir(id) { + if (id && id.includes(':')) { + const [compId, subId] = id.split(':'); + return path.resolve(__dirname, `../../data/${compId}/agents/${subId}`); + } + return path.resolve(__dirname, `../../data/agents/${id}`); +} const goalsService = new GoalsService(); // CONV_ROOT is now dynamically evaluated per-agent based on DATA_DIR. @@ -102,7 +110,7 @@ async function executeMetaCreator(task, parentAgentId, convId, sendSSE, callId) sendSSE('sub_agent', { event: 'chunk', callId, agent: 'meta_creator', chunk: `📝 Agent ID: ${agentName}\n📝 显示名称: ${displayName}\n` }); // Step 2: 读取 knowhow 和 benchmark(如果有) - const parentDir = path.join(DATA_DIR, parentAgentId); + const parentDir = getAgentDir(parentAgentId); const workspaceDir = path.join(parentDir, 'workspace', agentName); let knowhow = ''; let benchmark = ''; @@ -432,7 +440,7 @@ function setupSSE(res) { // ═══════════════════════════════════════ function getConvDir(agentId, convId) { - const dir = path.join(DATA_DIR, agentId, 'conversations', convId); + const dir = path.join(getAgentDir(agentId), 'conversations', convId); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); return dir; } @@ -440,7 +448,7 @@ function getConvDir(agentId, convId) { /** 读写 meta.json */ function loadMeta(agentId, convId) { try { - return JSON.parse(fs.readFileSync(path.join(DATA_DIR, agentId, 'conversations', convId, 'meta.json'), 'utf-8')); + return JSON.parse(fs.readFileSync(path.join(getAgentDir(agentId), 'conversations', convId, 'meta.json'), 'utf-8')); } catch { return null; } } @@ -453,7 +461,7 @@ function saveMeta(agentId, convId, meta) { /** 读写 history.json — 有序消息列表 */ function loadHistory(agentId, convId) { try { - return JSON.parse(fs.readFileSync(path.join(DATA_DIR, agentId, 'conversations', convId, 'history.json'), 'utf-8')); + return JSON.parse(fs.readFileSync(path.join(getAgentDir(agentId), 'conversations', convId, 'history.json'), 'utf-8')); } catch { return []; } } @@ -479,7 +487,7 @@ function logMessage(agentId, convId, role, content) { } function listConversations(agentId) { - const agentConvDir = path.join(DATA_DIR, agentId, 'conversations'); + const agentConvDir = path.join(getAgentDir(agentId), 'conversations'); if (!fs.existsSync(agentConvDir)) return []; try { return fs.readdirSync(agentConvDir, { withFileTypes: true }) @@ -514,7 +522,7 @@ async function getOrCreateProtocol(agentId, convId) { try { entry.protocol.disconnect(); } catch {} } - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const identity = loadIdentity(agentDir); const agentConfig = loadAgentConfig(agentDir); @@ -588,56 +596,83 @@ function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) { router.get('/list', (req, res) => { try { - if (!fs.existsSync(DATA_DIR)) return res.json([]); - const entries = fs.readdirSync(DATA_DIR, { withFileTypes: true }); - const agents = entries.filter(e => e.isDirectory()).map(e => { - const agentDir = path.join(DATA_DIR, e.name); - const config = loadAgentConfig(agentDir); - if (!config) return null; - const identity = loadIdentity(agentDir); - - // 计算最后活跃时间 (最近一条对话的 updatedAt) - let lastActiveAt = null; - const convDir = path.join(agentDir, 'conversations'); - if (fs.existsSync(convDir)) { - try { - const convs = fs.readdirSync(convDir, { withFileTypes: true }) - .filter(d => d.isDirectory()); - let latest = 0; - for (const c of convs) { - const metaPath = path.join(convDir, c.name, 'meta.json'); - if (fs.existsSync(metaPath)) { - try { - const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); - const t = new Date(meta.updatedAt || meta.createdAt).getTime(); - if (t > latest) latest = t; - } catch {} + const allAgents = [] + const DATA_ROOT = path.resolve(__dirname, '../../data') + + // 辅助函数:解析 Agent 目录 + const scanAgentDir = (baseDir, companyId) => { + if (!fs.existsSync(baseDir)) return + const entries = fs.readdirSync(baseDir, { withFileTypes: true }) + for (const e of entries) { + if (!e.isDirectory() || e.name.startsWith('.')) continue + const agentDir = path.join(baseDir, e.name) + const config = loadAgentConfig(agentDir) + if (!config) continue + const identity = loadIdentity(agentDir) + + let lastActiveAt = null + const convDir = path.join(agentDir, 'conversations') + if (fs.existsSync(convDir)) { + try { + const convs = fs.readdirSync(convDir, { withFileTypes: true }).filter(d => d.isDirectory()) + let latest = 0 + for (const c of convs) { + const metaPath = path.join(convDir, c.name, 'meta.json') + if (fs.existsSync(metaPath)) { + try { + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) + const t = new Date(meta.updatedAt || meta.createdAt).getTime() + if (t > latest) latest = t + } catch {} + } } - } - if (latest > 0) lastActiveAt = new Date(latest).toISOString(); - } catch {} + if (latest > 0) lastActiveAt = new Date(latest).toISOString() + } catch {} + } + + // System 级 Agent ID 保持原样,Company 级需要拼装 "companyId:agentName" + let agentId = config.id || e.name; + if (companyId !== 'system') { + agentId = `${companyId}:${agentId.includes(':') ? agentId.split(':')[1] : agentId}`; + } + + allAgents.push({ + id: agentId, + name: identity.name, + emoji: identity.emoji, + type: config.type, + status: config.status, + greeting: identity.greeting, + lastActiveAt, + companyId // 新增用于前端分组过滤 + }) } + } - return { - id: config.id || e.name, name: identity.name, emoji: identity.emoji, - type: config.type, status: config.status, greeting: identity.greeting, - lastActiveAt, - }; - }).filter(Boolean); - - // 排序: master 置顶, 其他按最近活跃排序 - agents.sort((a, b) => { - // master 永远第一 - if (a.type === 'master') return -1; - if (b.type === 'master') return 1; - // 有活跃时间的排前面 - const ta = a.lastActiveAt ? new Date(a.lastActiveAt).getTime() : 0; - const tb = b.lastActiveAt ? new Date(b.lastActiveAt).getTime() : 0; - return tb - ta; // 最近活跃的排前面 - }); + // 1. 扫描 System Agents + scanAgentDir(path.join(DATA_ROOT, 'agents'), 'system') - res.json(agents); - } catch (err) { res.status(500).json({ error: err.message }); } + // 2. 扫描 Company Agents + if (fs.existsSync(DATA_ROOT)) { + const roots = fs.readdirSync(DATA_ROOT, { withFileTypes: true }) + for (const r of roots) { + // 如果不是目录,或是公共资源库/系统代理,则跳过 + if (!r.isDirectory() || ['agents', 'skills', 'templates'].includes(r.name)) continue + scanAgentDir(path.join(DATA_ROOT, r.name, 'agents'), r.name) + } + } + + // 排序: master 永远第一,其他按活跃倒序 + allAgents.sort((a, b) => { + if (a.type === 'master') return -1 + if (b.type === 'master') return 1 + const ta = a.lastActiveAt ? new Date(a.lastActiveAt).getTime() : 0 + const tb = b.lastActiveAt ? new Date(b.lastActiveAt).getTime() : 0 + return tb - ta + }) + + res.json(allAgents) + } catch (err) { res.status(500).json({ error: err.message }) } }); // ── Phase 2: 小脑方案固化 API ── @@ -669,7 +704,7 @@ router.post('/:agentId/memory/daily-report', (req, res) => { }); router.get('/:agentId/info', (req, res) => { - const agentDir = path.join(DATA_DIR, req.params.agentId); + const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const config = loadAgentConfig(agentDir); const identity = loadIdentity(agentDir); res.json({ id: config?.id || req.params.agentId, ...identity, type: config?.type, status: config?.status, @@ -735,7 +770,7 @@ function listSubAgentLogs(agentId, convId) { router.delete('/:agentId/conversations/:convId', (req, res) => { const { agentId, convId } = req.params; try { - const dir = path.join(DATA_DIR, agentId, 'conversations', convId); + const dir = path.join(getAgentDir(agentId), 'conversations', convId); if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); const key = `${agentId}:${convId}`; const entry = sessions.get(key); @@ -762,7 +797,7 @@ router.post('/:agentId/chat/:convId/stop', (req, res) => { router.get('/:agentId/bootstrap/:filename', (req, res) => { const allowed = ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md']; if (!allowed.includes(req.params.filename)) return res.status(400).json({ error: 'Invalid' }); - res.json({ filename: req.params.filename, content: loadFile(path.join(DATA_DIR, req.params.agentId), req.params.filename) }); + res.json({ filename: req.params.filename, content: loadFile(getAgentDir(req.params.agentId), req.params.filename) }); }); /** @@ -771,7 +806,7 @@ router.get('/:agentId/bootstrap/:filename', (req, res) => { // GET /api/agent/:agentId/files — 递归列出 Agent 目录文件树 router.get('/:agentId/files', (req, res) => { - const agentDir = path.join(DATA_DIR, req.params.agentId); + const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const scanDir = (dir, relativePath = '') => { @@ -810,8 +845,8 @@ router.get('/:agentId/file/*', (req, res) => { const filePath = req.params[0]; if (!filePath) return res.status(400).json({ error: 'file path required' }); - const fullPath = path.join(DATA_DIR, req.params.agentId, filePath); - if (!fullPath.startsWith(path.join(DATA_DIR, req.params.agentId))) { + const fullPath = path.join(getAgentDir(req.params.agentId), filePath); + if (!fullPath.startsWith(getAgentDir(req.params.agentId))) { return res.status(403).json({ error: 'Access denied' }); } if (!fs.existsSync(fullPath)) return res.status(404).json({ error: 'File not found' }); @@ -831,12 +866,12 @@ router.get('/:agentId/file/*', (req, res) => { // GET/PUT /api/agent/:agentId/todo router.get('/:agentId/todo', (req, res) => { - const fp = path.join(DATA_DIR, req.params.agentId, 'todo.md'); + const fp = path.join(getAgentDir(req.params.agentId), 'todo.md'); if (!fs.existsSync(fp)) return res.json({ content: '' }); res.json({ content: fs.readFileSync(fp, 'utf-8') }); }); router.put('/:agentId/todo', (req, res) => { - const agentDir = path.join(DATA_DIR, req.params.agentId); + const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); fs.writeFileSync(path.join(agentDir, 'todo.md'), req.body.content || '', 'utf-8'); res.json({ success: true }); @@ -844,12 +879,12 @@ router.put('/:agentId/todo', (req, res) => { // GET/PUT /api/agent/:agentId/plan router.get('/:agentId/plan', (req, res) => { - const fp = path.join(DATA_DIR, req.params.agentId, 'plan.md'); + const fp = path.join(getAgentDir(req.params.agentId), 'plan.md'); if (!fs.existsSync(fp)) return res.json({ content: '' }); res.json({ content: fs.readFileSync(fp, 'utf-8') }); }); router.put('/:agentId/plan', (req, res) => { - const agentDir = path.join(DATA_DIR, req.params.agentId); + const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); fs.writeFileSync(path.join(agentDir, 'plan.md'), req.body.content || '', 'utf-8'); res.json({ success: true }); @@ -857,14 +892,14 @@ router.put('/:agentId/plan', (req, res) => { // GET/PUT /api/agent/:agentId/cron — 定时任务声明 (cron.json) router.get('/:agentId/cron', (req, res) => { - const fp = path.join(DATA_DIR, req.params.agentId, 'cron.json'); + const fp = path.join(getAgentDir(req.params.agentId), 'cron.json'); if (!fs.existsSync(fp)) return res.json([]); try { res.json(JSON.parse(fs.readFileSync(fp, 'utf-8'))); } catch { res.json([]); } }); router.put('/:agentId/cron', (req, res) => { - const agentDir = path.join(DATA_DIR, req.params.agentId); + const agentDir = getAgentDir(req.params.agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const crons = req.body; if (!Array.isArray(crons)) return res.status(400).json({ error: 'Body must be an array of cron objects' }); @@ -934,8 +969,8 @@ router.post('/task/dispatch', async (req, res) => { const { from, to, task, priority = 'P1', planId, goalId, nodeId, autoExecute = true } = req.body; if (!from || !to || !task) return res.status(400).json({ error: 'from, to, task are required' }); - const targetDir = path.join(DATA_DIR, to); - const senderDir = path.join(DATA_DIR, from); + const targetDir = getAgentDir(to); + const senderDir = getAgentDir(from); if (!fs.existsSync(targetDir)) return res.status(404).json({ error: `Target agent "${to}" not found` }); if (!fs.existsSync(senderDir)) return res.status(404).json({ error: `Sender agent "${from}" not found` }); @@ -1055,7 +1090,7 @@ router.post('/task/report', (req, res) => { const safeSummary = (summary || '').replace(/\n/g, '
').replace(/\|/g, '|'); // 1. 更新执行者的 todo.md (标记完成) - const executorDir = path.join(DATA_DIR, from); + const executorDir = getAgentDir(from); if (fs.existsSync(executorDir)) { const todoPath = path.join(executorDir, 'todo.md'); if (fs.existsSync(todoPath)) { @@ -1070,7 +1105,7 @@ router.post('/task/report', (req, res) => { } // 2. 更新上级的 plan.md (标记完成) — 使用行级匹配,兼容各种格式 - const senderDir = path.join(DATA_DIR, to); + const senderDir = getAgentDir(to); if (fs.existsSync(senderDir)) { const planPath = path.join(senderDir, 'plan.md'); if (fs.existsSync(planPath)) { @@ -1136,7 +1171,7 @@ router.post('/:agentId/chat', async (req, res) => { if (!message) return res.status(400).json({ error: 'message is required' }); const agentId = req.params.agentId; - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); const convId = reqConvId || `conv-${Date.now().toString(36)}`; diff --git a/server/services/heartbeat-service.js b/server/services/heartbeat-service.js index 7aae896..0fd3607 100644 --- a/server/services/heartbeat-service.js +++ b/server/services/heartbeat-service.js @@ -4,6 +4,13 @@ const http = require('http'); const config = require('../config'); const DATA_DIR = path.resolve(__dirname, '../../data/agents'); +function getAgentDir(id) { + if (id && id.includes(':')) { + const [compId, subId] = id.split(':'); + return path.resolve(__dirname, `../../data/${compId}/agents/${subId}`); + } + return path.resolve(__dirname, `../../data/agents/${id}`); +} class HeartbeatService { constructor() { @@ -44,7 +51,7 @@ class HeartbeatService { */ _setupAgentHeartbeat(agentId) { try { - const configPath = path.join(DATA_DIR, agentId, 'config.json'); + const configPath = path.join(getAgentDir(agentId), 'config.json'); const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (cfg.heartbeat && cfg.heartbeat.enabled === true) { diff --git a/server/services/memory-service.js b/server/services/memory-service.js index 6dca2de..0a0d8fd 100644 --- a/server/services/memory-service.js +++ b/server/services/memory-service.js @@ -16,6 +16,13 @@ const fs = require('fs'); const path = require('path'); const DATA_DIR = path.resolve(__dirname, '../../data/agents'); +function getAgentDir(id) { + if (id && id.includes(':')) { + const [compId, subId] = id.split(':'); + return path.resolve(__dirname, `../../data/${compId}/agents/${subId}`); + } + return path.resolve(__dirname, `../../data/agents/${id}`); +} const MAX_SHORT_TERM = 10; // 最多保留最近 N 个 session 摘要 const MAX_MEMORY_MD_SIZE = 3000; // memory.md 最大字符数 const INJECT_SESSION_COUNT = 5; // prompt 注入最近 N 个 session @@ -36,7 +43,7 @@ const MAX_CEREBELLUM_INJECT = 2; // 最多注入 N 个小脑方案 */ function saveSessionSummary(agentId, convId, meta, lastOutput) { try { - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const shortTermDir = path.join(agentDir, 'memory', 'short-term'); if (!fs.existsSync(shortTermDir)) { @@ -115,7 +122,7 @@ function saveSessionSummary(agentId, convId, meta, lastOutput) { */ function loadRecentMemory(agentId) { try { - const shortTermDir = path.join(DATA_DIR, agentId, 'memory', 'short-term'); + const shortTermDir = path.join(getAgentDir(agentId), 'memory', 'short-term'); if (!fs.existsSync(shortTermDir)) return ''; // 按文件名倒序(时间戳降序)取最近 N 个 @@ -154,7 +161,7 @@ function loadRecentMemory(agentId) { function appendToMemoryMd(agentId, task, result, meta) { if (!meta.stats?.toolCallCount || meta.stats.toolCallCount === 0) return; - const memoryPath = path.join(DATA_DIR, agentId, 'memory', 'memory.md'); + const memoryPath = path.join(getAgentDir(agentId), 'memory', 'memory.md'); let content = ''; try { content = fs.readFileSync(memoryPath, 'utf-8'); @@ -205,7 +212,7 @@ function appendToMemoryMd(agentId, task, result, meta) { */ function retrieveRelevantPlans(agentId, taskText) { try { - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const index = loadHippocampusIndex(agentDir); if (index.length === 0) return ''; @@ -292,7 +299,7 @@ function retrieveRelevantPlans(agentId, taskText) { */ function saveCerebellumPlan(agentId, plan) { try { - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const cerebellumDir = path.join(agentDir, 'memory', 'cerebellum'); if (!fs.existsSync(cerebellumDir)) { @@ -490,7 +497,7 @@ function extractTask(title, agentId, convId) { // 如果 title 太短,尝试从 messages.log 读取 if (task.length < 10) { try { - const logPath = path.join(DATA_DIR, agentId, 'conversations', convId, 'messages.log'); + const logPath = path.join(getAgentDir(agentId), 'conversations', convId, 'messages.log'); const log = fs.readFileSync(logPath, 'utf-8'); const userMsg = log.match(/\[USER\]\n([\s\S]*?)(?:\n\[AGENT\]|$)/); if (userMsg) task = userMsg[1].trim().substring(0, 300); @@ -629,7 +636,7 @@ function autoReflect(agentId, convId, meta, output) { return null; // 太简单的对话不反思 } - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const task = extractTask(meta.title || '', agentId, convId); const isSuccess = meta.status === 'completed'; const isFailed = meta.status === 'failed'; @@ -749,7 +756,7 @@ function writeReflectionLog(agentDir, reflection) { /** 更新 memory.md 的 "已验证模式" 和 "失败教训" 段 */ function updateMemoryMdSections(agentId, reflection) { - const memoryPath = path.join(DATA_DIR, agentId, 'memory', 'memory.md'); + const memoryPath = path.join(getAgentDir(agentId), 'memory', 'memory.md'); let content = ''; try { content = fs.readFileSync(memoryPath, 'utf-8'); } catch { return; } @@ -806,7 +813,7 @@ function updateMemoryMdSections(agentId, reflection) { */ function consolidateMemory(agentId) { try { - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const shortTermDir = path.join(agentDir, 'memory', 'short-term'); const longTermDir = path.join(agentDir, 'memory', 'long-term'); @@ -899,7 +906,7 @@ function consolidateMemory(agentId) { */ function generateDailyReport(agentId) { try { - const agentDir = path.join(DATA_DIR, agentId); + const agentDir = getAgentDir(agentId); const shortTermDir = path.join(agentDir, 'memory', 'short-term'); const dailyDir = path.join(agentDir, 'memory', 'daily'); const reflectDir = path.join(agentDir, 'memory', 'reflections'); diff --git a/server/src/core/agent-manager.js b/server/src/core/agent-manager.js index daed8db..59e6869 100644 --- a/server/src/core/agent-manager.js +++ b/server/src/core/agent-manager.js @@ -130,9 +130,18 @@ class AgentManager extends EventEmitter { // Dynamic Hot-Reload Fallback (Local File-System First) if (!agentConfig) { - const agentsRoot = path.resolve(__dirname, '../../../data/agents'); + let agentDir; + let agentsRoot; + if (agentName.includes(':')) { + const [compId, subId] = agentName.split(':'); + agentsRoot = path.resolve(__dirname, `../../../data/${compId}/agents`); + agentDir = path.join(agentsRoot, subId); + } else { + agentsRoot = path.resolve(__dirname, '../../../data/agents'); + agentDir = path.join(agentsRoot, agentName); + } + // 1. Check if it's an independent Agent - const agentDir = path.join(agentsRoot, agentName); if (fs.existsSync(agentDir)) { agentConfig = { type: 'orchestrator', diff --git a/server/src/core/prompt-builder.js b/server/src/core/prompt-builder.js index c188ccf..7fcf028 100644 --- a/server/src/core/prompt-builder.js +++ b/server/src/core/prompt-builder.js @@ -13,6 +13,59 @@ class PromptBuilder { } /** + * 动态生成环境快照 (Company, Agent, Goals) + */ + getSystemStateXML() { + let state = '\n'; + state += ` ${new Date().toISOString()}\n`; + try { + const { CompanyService } = require('../../services/company-service'); + const { GoalsService } = require('../../services/goals'); + + const cs = new CompanyService(); + const companies = cs.list(); + + if (companies.length > 0) { + state += ' \n'; + for (const c of companies) { + state += ` \n`; + const agents = cs.listAgents(c.id); + if (agents && agents.length > 0) { + for (const a of agents) { + state += ` \n`; + } + } + const gs = new GoalsService(c.id); + const goals = gs.list(); + if (goals && goals.length > 0) { + state += ' \n'; + for (const g of goals.filter(g => g.status !== 'done')) { + state += ` \n`; + } + state += ' \n'; + } + state += ` \n`; + } + state += ' \n'; + } + + const globalGs = new GoalsService(); + const globalGoals = globalGs.list(); + if (globalGoals && globalGoals.length > 0) { + state += ' \n'; + for (const g of globalGoals.filter(g => g.status !== 'done')) { + state += ` \n`; + } + state += ' \n'; + } + } catch (e) { + state += ` \n`; + } + state += '\n'; + return state; + } + + /** * 加载文件内容(带缓存) */ loadFile(filePath) { @@ -110,6 +163,9 @@ class PromptBuilder { parts.push(`---\n\n## 当前任务\n\n${userTask}`); } + // 在最终拼接前,注入上帝视角的环境感知 + parts.unshift(this.getSystemStateXML()); + let result = parts.join('\n\n---\n\n'); // 替换 {{AVAILABLE_AGENTS}} 占位符(可能在 SKILL.md 或 protocol 中) diff --git a/web/src/App.vue b/web/src/App.vue index 05500a8..3e8c5b5 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,9 +1,15 @@ @@ -112,6 +118,18 @@ body { background: var(--bg-primary); color: var(--text-primary); transition: background 0.3s, color 0.3s; + overflow-x: hidden; +} + +.app-layout { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-main { + flex: 1; } ::-webkit-scrollbar { width: 6px; } diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index 401f92a..717c997 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -6,12 +6,49 @@ +