diff --git a/CLAUDE.md b/CLAUDE.md index 5802b86..ca9c8b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,7 @@ npm run okr:migrate # goals → okr 存量迁移 | `/ws/claude` | Claude 实时对话(仅 `index.js`) | | `/api/health` `/api/plugins` | 健康检查 / 插件状态 | | `/api/devloop/projects` | DevLoop:创建项目 + 跑 PM/Architect(POST)、列项目 / 查状态(GET,见下) | +| `/api/dream/*` | 经验汇流(29-DREAM):`dream-service.js` 能力的 REST 封装,只读查询 + 触发 `dreamOnce`/`dreamSweep`,不新增业务逻辑,见 [`docs/todo/system-design/29-dream-design.md`](docs/todo/system-design/29-dream-design.md) | ### DevLoop 子系统 diff --git a/docs/README.md b/docs/README.md index 294705f..2c317ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,7 +45,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **只剩验收 9**。2026-08-20 第四次真跑(§六ter)验掉 2 与 8,验收 4 由零注入链路验掉,7 早已修好。验收 9 的准确状态是「产出端通了、消费端没等到那个形状」—— 需要一次 **org 层回退边指向 subgraph 节点**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | -| [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(只有口述、无设计)、任务态清理(源码树无限增长)、`source_kind`/`legacy_path` 删列、三项「已实施未验证」。§五 是**已立起来的守卫清单**(别重复造,也别删) | +| [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | +| [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | ## todo/future/(暂缓,非当前重心) diff --git a/docs/todo/system-design/28-followups.md b/docs/todo/system-design/28-followups.md index bdcb06d..c3f9661 100644 --- a/docs/todo/system-design/28-followups.md +++ b/docs/todo/system-design/28-followups.md @@ -20,23 +20,32 @@ ### 1.1 dream 机制 -**状态**:用户口述过要解决什么问题和大致形态,**设计尚未开始**。 - -已知的只有这些: -- 定时跑的另一套逻辑,整理**所有 agent 在所有公司**的 history 与 memory,**显式提炼**后沉淀到 skill memory -- 它是「经验汇流」的正解;错误版本是「每轮对话自动把原文写进共享 `SKILL.md`」—— - 方向对,但直接灌原始对话等于把 A 公司的机密喂给 B 公司 - (这个错误版本真的发生过,是 28 号第一轮验收挖出的最严重 blocker,已修) - -**尚未设计的**:触发时机与频率、提炼规则、去敏策略(什么算「不含公司特定信息」)、 -产物落点与格式、失败与重试、提炼质量怎么评。 +**状态(2026-08-20 更新)**:设计已成文并已实现,见 +[29-dream-design.md](./29-dream-design.md)。四条取舍(去敏判据 / 沉淀落点 / 触发时机 / +质量评估)均已拍板,五步推进顺序 + 定时挂载 + 定义文件写入均已落地, +`innovator-evolver` 上真 CLI 跑通一次(40 条原料 → 4 条抽象经验,8 条引用全部核对回原文)。 + +已实现的: +- 定时(每日一次,串行,挂在 `server/index.js` 的 `DISABLE_BACKGROUND_AGENTS` 早退之后) + 整理**所有 agent 在所有公司**的原始记录,**显式提炼**(`dream-distiller`)+ + 独立去敏判定(`dream-screener`)后沉淀 +- 落点四选四:本体 `memory/experience.md`(经验积累)+ `SKILL.md` / `soul.md` / `agent.md` + (受管区块,三道闸门缺一不写,见 29 号文档 §5.6) +- 引用真伪自动查(编造出处直接拒)+ `auditDistilled()` 把结论与原料原文并排列出, + 几秒内可回查 + +- REST(`/api/dream/*`)+ 控制台(`/dream`,导航栏 `🌙 Dream`):查总览/详情、手动跑单个本体(含 dry-run)、 + 触发并轮询全量扫描(2026-08-21) + +**仍未做的**:「结论对不对」的判据(机器判不了,只能靠人抽查,`auditDistilled` 的输出不是质量分)。 +「把 dream 套进 org/work graph 来跑」评估过、用户明确选了不做—— +org/work graph 是单公司作用域的,dream 是跨公司扫描,两者模型不匹配; +且 graph 引擎自带的验收机制(goal-contract benchmark/test-script)判的是「产物过没过验收脚本」, +对「经验提炼得对不对」这种自由文本判断没有对应机制,套不套 graph 都得单独设计 rubric。 **已经生效的约束**(这条是确定的,不随 dream 的设计变化): 公司态目录(`system/agents/{aid}/{cid}/` 下的 `memory/` / `conversations/`)是 dream 的唯一原料, -**不许被任何清理策略当垃圾扫掉**。详见主文档 §5.6b。 - -> 真要做时另开设计文档。**不要**照主文档 §5.6b 的措辞去写代码 —— -> 那一节的目的是锁住上面那条约束,不是给 dream 下定义。 +**不许被任何清理策略当垃圾扫掉**。详见主文档 §5.6b,任务态清理已按此约束实现(见下 §3.2)。 --- diff --git a/docs/todo/system-design/29-dream-design.md b/docs/todo/system-design/29-dream-design.md index b932ad5..8856d56 100644 --- a/docs/todo/system-design/29-dream-design.md +++ b/docs/todo/system-design/29-dream-design.md @@ -1,8 +1,9 @@ # 29-DREAM · 经验汇流(dream)—— 设计草案 > **所属**: [28 Agent 注册表统一](../../done/system-design/28-agent-registry-unification.md) §5.6b 锁住的那条约束的下游设计;未确认项来源 [28-followups §1.1](./28-followups.md)。 -> **日期**: 2026-08-20 -> **状态**: 📝 **四条取舍已由用户拍板(2026-08-20,见 §三),实现未开始。** +> **日期**: 2026-08-20 / REST 与控制台入口 2026-08-21 +> **状态**: ✅ **四条取舍已拍板并已实现(五步 + 定时挂载 + 定义文件写入 + REST/控制台入口,均已落地并真跑验证,见 §四/§五/§5.7)。** +> 仍未做的只剩「结论对不对」的判据——机器判不了,只能靠人抽查(决策 D,见上)。 > 标 ✅ 的是已生效的约束或有实测证据的事实;标 🔒 的是本轮拍板的决定。 --- @@ -319,13 +320,15 @@ mtime 会被任何一次无关写入推动(例如别处重写整份 `memory.md ### 5.4 还没做的(别当成 dream 上线了) -1. **定时挂载没做**(决策 C 说每日一次)。现在只有 `dreamOnce(agentId)` 这个可调用的单元, - 没有挂到任何调度上,也没有 REST / 控制台入口。空转保护已经在,挂之前先想清楚 - 「每天对哪些本体跑」——全量 76 个本体逐个跑,即使空转也要 76 次目录读。 -2. **还没写定义文件**。`writeDistilled` 目前只写本体根的 `memory/experience.md` - (纯经验积累,删掉无副作用)。`SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义 - 留给下一步;`definitionWritability()` 已经能报出「这三份现在能不能写、不能写是为什么」, - 闸门(§B.3)也已就位,缺的是「往里写什么、写成什么形状」这半边设计。 +1. ~~定时挂载没做~~ ✅ **已完成**(§5.6)—— `dream-service.startDaily()` 已挂在 + `server/index.js` 的 `DISABLE_BACKGROUND_AGENTS` 早退之后,每日一次、串行扫、 + 定时器 `unref`。~~仍然没有的是 REST / 控制台入口~~ ✅ **已完成**(§5.7)—— + `/api/dream/*` + `web/src/views/DreamConsole.vue`(导航栏 `🌙 Dream`)。 +2. ~~还没写定义文件~~ ✅ **已完成**(§5.6)—— `writeDefinitionFiles()` 已实现, + 落点是 `SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义文件, + 形状是受管区块(``,区块外不碰、整体重写幂等), + 写之前过三道闸门(definition-write-gate → agent.md 前 2000 字符自足性 → + 写完复核毕业闸门,不过立刻还原),任何一道不过就不写、原因逐文件返回。 3. ~~决策 D~~ ✅ 已落地(见上)—— 但「结论对不对」仍然只能靠人抽查。 --- @@ -386,3 +389,100 @@ mtime 会被任何一次无关写入推动(例如别处重写整份 `memory.md ### 空转保护实跑 第二次调用(原料没变):`{"skipped":"no-new-material"}`,**一次 agent 调用都没起**。 + +--- + +## 5.6 定时挂载(决策 C)+ 定义文件写入(决策 B)(2026-08-20,`e9f380a`) + +### 定时:每日一次,串行 + +`dreamSweep` 逐个本体扫,**不并发** —— 每轮可能起两次 LLM 调用,几十个本体并发 += 同时拉起几十个 claude 子进程,这个代价本仓库付过(2h43m 漏出 82 个进程 / 17.5 GB, +[28-followups §五第五条](./28-followups.md))。慢一点没有代价,空转保护已经把 +「读一遍全量本体」的成本压到「N 次目录读」。 + +挂点:`server/index.js` 里 `DISABLE_BACKGROUND_AGENTS` 早退之后,调 +`dream-service.startDaily()`。定时器 `unref`;只挂定时**不立刻跑一轮** +(启动那一刻跑没有额外收益,反而让每次重启都烧一遍)。一个本体抛异常不拖垮整轮, +但必须留下是谁抛的 —— 否则现场只看到「今天没沉淀」,查不出是没原料还是一直在抛。 + +**没做的**:REST / 控制台入口。现在触发只有两条路:等每日定时,或代码 / 脚本直接调 +`dreamOnce(agentId)`。人想立刻看一次提炼结果,目前没有 API/页面。 + +### 定义文件:受管区块 + 三道闸门 + +落点是 `SKILL.md` / `soul.md` / `agent.md`(决策 B 的另外两个落点,本体根 +`memory/experience.md` 在步骤 3 已经在写)。形状是一对 +`` 之间的**受管区块**,区块外一个字节不碰 —— 自由追加会让 +「哪些是人写的、哪些是 dream 写的」几轮之后再也分不开,分不开就没法分别回滚。 +区块整体重写还顺带**幂等**:同一批经验重复跑不会越写越长。 + +三道闸门,缺一不写: +1. **definition-write-gate**(§B.3):当前内容必须已在 git 历史里,且没有未提交改动; +2. **agent.md 的前 2000 字符必须仍然自足**:`graph-engine.js:915` 的「角色定义摘录」 + 只看这一段,改了等于悄悄换掉规划 agent 读到的画像; +3. **写完复核毕业闸门**(`missingBodyDefinition`),不过就**立刻还原** —— + `agent.md` 写坏会让本体退回「定义没填满」,雇不进任何公司。 + +任何一道不过:不写,原因逐文件返回(`definitions[]`)。**不写不该是静默的。** + +守卫:`dream-service.test.js` 新增 3 例,对真被 git 跟踪且干净的本体(`dream-screener`) +走真路径 —— 真写、验区块外字节不变 / 幂等 / 毕业闸门仍过,`finally` 还原并断言还原干净; +`agent.md` 那条验「不写 + 理由点名前 2000 字符 + 一个字节都没动」。`dreamSweep` 加了 +`agents` 参数,让错误隔离那条能真调而不是真扫全量本体。 + +全量:144 文件 / 1330 测试全绿。 + +--- + +## 5.7 REST / 控制台入口(2026-08-21) + +只做了这一件事,没有跟着做「dream 改成 org/work graph 来跑」—— +两者被单独评估过(见对话记录):org/work graph 的执行模型是**单公司作用域**的 +(org graph = 公司级 workflow,work graph = 公司内某任务的节点图),dream 却是 +**跨公司**扫描(按 agent 扫全部公司的记忆),套进去要么破坏这个作用域假设, +要么得新开一种「跨公司图」类型。而且 graph 引擎自带的「自动质量判断」 +(goal-contract 的 benchmark / test-script、triager 驳回重做)判的是「产物有没有 +过验收脚本」,对「这条提炼的经验对不对」这种自由文本判断没有对应机制—— +那道题不管包不包进 graph 都得单独设计一个 rubric,是同一件事,graph 化本身 +不会替你解决它。所以只做了用户点了头的这一半。 + +### REST(`server/routes/dream.js`,挂在 `/api/dream`) + +路由本身**没有新业务逻辑**,全部委托给已经测过的 `dream-service.js`: + +| 端点 | 做什么 | +|---|---| +| `GET /api/dream/agents` | 总览:每个可 dream 的本体,公司数、上次跑的时间、上次沉淀/拒绝了几条 | +| `GET /api/dream/agents/:agentId` | 详情:状态、定义文件(`agent.md`/`soul.md`/`SKILL.md`)现在能不能写及原因、`auditDistilled()` 的可回查列表 | +| `POST /api/dream/agents/:agentId/run` | 手动跑一次(`{dryRun}` 可选;`dryRun` 只看有没有新原料,不起 LLM 调用) | +| `POST /api/dream/sweep` | 手动触发全量扫描,202 立即返回,后台跑(串行扫全部本体可能要几分钟到几十分钟,不能塞进一次 HTTP 请求里等) | +| `GET /api/dream/sweep/status` | 轮询上面那个后台任务:`sweeping` / `results` / `error` | + +`sweep` 的并发锁是路由自己的状态(内存里一个 `sweepState`,不落库)—— +`dream-service.js` 没有「正在跑」这个概念,`dreamSweep()` 单纯是个 promise。 +两次并发 `POST /sweep` 第二次会拿到 409,防的是「人在控制台连点两下」而不是 +分布式场景(单进程部署,够用)。 + +守卫:`server/tests/unit/dream-routes.test.js`,monkeypatch 掉 +`dream-service` 导出对象上的六个函数(不走真文件系统,不会误扫真仓库的 +76 个本体),只验路由这一层独有的东西——六个端点接没接对、`sweep` 的并发锁 +状态机对不对。`dream-service.js` 自身的正确性由 `dream-service.test.js` 覆盖, +这里不重复验。 + +### 控制台(`web/src/views/DreamConsole.vue`,路由 `/dream`,导航栏 `🌙 Dream`) + +一个表格 + 两个弹窗,没有别的:本体列表(点「详情」看状态/定义文件可写性/ +经验审计,点「跑一次」或「dry-run」手动触发单个本体),页头一个「全量扫描」按钮 +(触发后台 sweep,15 秒轮询一次状态直到跑完)。 + +### 真跑验证(2026-08-21) + +起真服务器(`DISABLE_BACKGROUND_AGENTS=1 bash start.sh`),对真数据打过一轮: +`GET /api/dream/agents` 拿到全部 76 个本体真实的 `companies` / `lastRunAt`; +`GET /api/dream/agents/innovator-evolver` 的 `definitions` 如实报出 +`agent.md`/`soul.md` 可写、`SKILL.md` 不可写(没有 git 历史)及具体原因, +`experience` 把 §5.5 那次真跑沉淀的 4 条经验连同原料原文一起吐出来; +`dry-run` 对 `innovator-evolver` 正确报 `no-new-material`(材料已在上次真跑时被 +消费,本轮**没有起一次 LLM 调用**)。控制台页面在浏览器里过了一遍同样的路径 +(列表 → 详情弹窗 → dry-run 弹窗),数据与直接打 API 一致。 diff --git a/server/index.js b/server/index.js index b8d6759..3fbba99 100644 --- a/server/index.js +++ b/server/index.js @@ -96,6 +96,7 @@ app.use('/api/tasks', require('./routes/tasks')); app.use('/api/devloop', require('./routes/devloop')); app.use('/api/graph-runs', require('./routes/graph-run')); // P3 图执行(T6) app.use('/api/company', require('./routes/graph-run').companyRouter); // GET /:cid/graph-runs +app.use('/api/dream', require('./routes/dream')); // 29-DREAM 经验汇流 app.use('/public', require('express').static(require('path').join(__dirname, 'public'))); // ─── 系统端点 ─── diff --git a/server/routes/dream.js b/server/routes/dream.js new file mode 100644 index 0000000..4760266 --- /dev/null +++ b/server/routes/dream.js @@ -0,0 +1,75 @@ +/** + * dream —— 经验汇流的 REST 入口([29-DREAM](../../docs/todo/system-design/29-dream-design.md))。 + * + * 只做一件事:把 `dream-service.js` 已有的能力(`dreamOnce` / `dreamSweep` / + * `auditDistilled` / `definitionWritability`)从「只能靠脚本或定时器触发」 + * 变成「REST 可查可触发」。不新增业务逻辑。 + */ +const express = require('express'); +const router = express.Router(); +const dream = require('../services/dream-service'); + +// dreamSweep 串行跑几十个本体、每个可能起两次 LLM 调用,不能塞进一次 HTTP 请求里等。 +// 用最小的内存态跟踪「在跑没在跑」+「上一轮结果」,够控制台轮询用。 +let sweepState = { sweeping: false, startedAt: null, finishedAt: null, results: null, error: null }; + +// GET /api/dream/agents — 总览:每个可 dream 的本体,跑没跑过、上次沉淀了几条 +router.get('/agents', (req, res) => { + try { + const list = dream.listDreamableAgents().map((agentId) => { + const state = dream.readState(agentId); + return { + agentId, + companies: dream.companiesOf(agentId).length, + lastRunAt: state.lastRunAt || null, + lastAccepted: state.lastAccepted || 0, + lastRejected: state.lastRejected || 0, + }; + }); + res.json({ agents: list }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/dream/agents/:agentId — 详情:状态 + 定义文件能不能写 + 已沉淀经验的可回查列表 +router.get('/agents/:agentId', (req, res) => { + try { + const { agentId } = req.params; + res.json({ + agentId, + companies: dream.companiesOf(agentId), + state: dream.readState(agentId), + definitions: dream.definitionWritability(agentId), + experience: dream.auditDistilled(agentId), // 每条结论并排列出引用的原料原文,回查成本降到几秒 + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// POST /api/dream/agents/:agentId/run — 手动跑一次(真会起 LLM 调用,dryRun 只想看会不会有新原料时用) +router.post('/agents/:agentId/run', async (req, res) => { + try { + const dryRun = req.body && req.body.dryRun === true; + const result = await dream.dreamOnce(req.params.agentId, { dryRun }); + res.json(result); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// POST /api/dream/sweep — 手动触发全量扫描(后台跑,别等;串行跑完可能是几分钟到几十分钟) +router.post('/sweep', (req, res) => { + if (sweepState.sweeping) return res.status(409).json({ error: '上一轮扫描还没跑完', ...sweepState }); + sweepState = { sweeping: true, startedAt: new Date().toISOString(), finishedAt: null, results: null, error: null }; + dream.dreamSweep() + .then((results) => { sweepState = { ...sweepState, sweeping: false, finishedAt: new Date().toISOString(), results }; }) + .catch((err) => { sweepState = { ...sweepState, sweeping: false, finishedAt: new Date().toISOString(), error: err.message }; }); + res.status(202).json({ started: true, startedAt: sweepState.startedAt }); +}); + +// GET /api/dream/sweep/status — 轮询上面那个后台任务 +router.get('/sweep/status', (req, res) => res.json(sweepState)); + +module.exports = router; diff --git a/server/tests/unit/dream-routes.test.js b/server/tests/unit/dream-routes.test.js new file mode 100644 index 0000000..26a7e90 --- /dev/null +++ b/server/tests/unit/dream-routes.test.js @@ -0,0 +1,98 @@ +/** + * dream 的 REST 入口(29-DREAM,见 docs/todo/system-design/29-dream-design.md)。 + * + * 路由本身没有业务逻辑 —— 全部委托给已经测过的 `dream-service.js`。 + * 这份只验两件路由层独有的东西: + * ① 六个端点确实接到了对应的 service 函数上(不是「实现在、零调用方」); + * ② `POST /sweep` 的并发锁 —— 上一轮没跑完时第二次触发要拒(409), + * 跑完之后 `GET /sweep/status` 要能看到结果。这段状态机全部写在 route 里, + * `dream-service.test.js` 覆盖不到。 + * + * `dream-service` 的六个被测函数在这里整体替身(monkeypatch 掉导出对象上的属性, + * 用完在 afterAll 里还原)—— 不走真文件系统,也不会误扫真仓库里的 76 个本体。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; + +const express = require('express'); +const dream = require('../../services/dream-service'); +const dreamRouter = require('../../routes/dream'); + +const AGENT = 'zz-route-agent'; +const orig = {}; +function patch(name, fn) { orig[name] = dream[name]; dream[name] = fn; } +function restore(name) { dream[name] = orig[name]; } + +let server; +let base; +let resolveSweep; + +beforeAll(() => { + patch('listDreamableAgents', () => [AGENT]); + patch('readState', () => ({ lastRunAt: '2026-08-20T00:00:00.000Z', lastAccepted: 2, lastRejected: 1 })); + patch('companiesOf', () => ['zz-route-co']); + patch('definitionWritability', () => [{ file: 'agent.md', allowed: true, reason: '' }]); + patch('auditDistilled', () => [{ title: 'X', sources: [{ source: 'zz-route-co:memory.md#1', text: '原文' }] }]); + patch('dreamOnce', async (agentId, opts) => ({ agentId, skipped: null, dryRun: !!(opts && opts.dryRun), accepted: 1 })); + patch('dreamSweep', () => new Promise((resolve) => { resolveSweep = resolve; })); + + const app = express(); + app.use(express.json()); + app.use('/api/dream', dreamRouter); + server = app.listen(0); + base = `http://127.0.0.1:${server.address().port}/api/dream`; +}); + +afterAll(() => { + ['listDreamableAgents', 'readState', 'companiesOf', 'definitionWritability', 'auditDistilled', 'dreamOnce', 'dreamSweep'] + .forEach(restore); + server.close(); +}); + +describe('dream REST 入口', () => { + it('GET /agents 用 listDreamableAgents + readState + companiesOf 拼总览', async () => { + const res = await fetch(`${base}/agents`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.agents).toEqual([ + { agentId: AGENT, companies: 1, lastRunAt: '2026-08-20T00:00:00.000Z', lastAccepted: 2, lastRejected: 1 }, + ]); + }); + + it('GET /agents/:id 把状态、定义文件可写性、经验审计一起交出去', async () => { + const res = await fetch(`${base}/agents/${AGENT}`); + const body = await res.json(); + expect(body.agentId).toBe(AGENT); + expect(body.companies).toEqual(['zz-route-co']); + expect(body.definitions[0].file).toBe('agent.md'); + expect(body.experience[0].sources[0].text).toBe('原文'); + }); + + it('POST /agents/:id/run 把 body.dryRun 原样传给 dreamOnce', async () => { + const res = await fetch(`${base}/agents/${AGENT}/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dryRun: true }), + }); + const body = await res.json(); + expect(body.dryRun).toBe(true); + }); + + it('sweep 并发锁:跑着的时候第二次触发要 409,跑完 status 要能看到结果', async () => { + const start = await fetch(`${base}/sweep`, { method: 'POST' }); + expect(start.status).toBe(202); + + const mid = await fetch(`${base}/sweep/status`); + expect((await mid.json()).sweeping).toBe(true); + + const blocked = await fetch(`${base}/sweep`, { method: 'POST' }); + expect(blocked.status).toBe(409); + + resolveSweep([{ agentId: AGENT, accepted: 3 }]); + await new Promise((r) => setTimeout(r, 20)); // 让 .then() 回调跑完 + + const done = await fetch(`${base}/sweep/status`); + const doneBody = await done.json(); + expect(doneBody.sweeping).toBe(false); + expect(doneBody.results).toEqual([{ agentId: AGENT, accepted: 3 }]); + }); +}); diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index ba4b440..4e1ff30 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -21,6 +21,10 @@ 📊 Quota + + 🌙 Dream + +
diff --git a/web/src/router/index.js b/web/src/router/index.js index 3e2c4ef..dc296f1 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -80,6 +80,11 @@ const routes = [ name: 'QuotaMonitor', component: () => import('@/views/QuotaMonitor.vue'), }, + { + path: '/dream', + name: 'DreamConsole', + component: () => import('@/views/DreamConsole.vue'), + }, // P5 · DevLoop Projects 列表 + Dashboard + Ops { path: '/devloop', diff --git a/web/src/views/DreamConsole.vue b/web/src/views/DreamConsole.vue new file mode 100644 index 0000000..b068a2a --- /dev/null +++ b/web/src/views/DreamConsole.vue @@ -0,0 +1,290 @@ + +定时把所有 agent 在所有公司的记忆提炼成不含公司信息的经验,跨公司沉淀
+