commit 8e08f8f13d786f560ba213de48fbbcfd13e85aa9 Author: WuYongJie Date: Mon Mar 16 15:22:39 2026 +0800 初始化 Agents Team 工作台 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aec2d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.npm-cache/ +.agents-team/ +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ced243 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# Agents Team 项目级 Lead 工作台 v1 + +这是一个本地优先的项目工作台。 + +它的核心不是“全局固定团队”,而是: + +- 每个项目都有自己的 `lead` +- 用户默认只和这个项目的 `lead` 对话 +- 团队先讨论出草案,再确认创建真实成员 +- 正式团队变更会先给出新增 / 移出 / 调整清单,再单独确认 +- 团队内正式成员可以互相直连发消息 +- 默认不自动 `commit` / `push` + +## 这版已经能做什么 + +- 首页查看项目列表,手动接入本地项目目录 +- 新建项目时先创建一个真实 `lead` 运行会话 +- 在项目页和 `lead` 对话,生成项目专属团队草案 +- 确认团队后创建真实成员线程和运行态 +- 任务派发、送审、打回、复审、lead 汇总 +- 成员之间通过 mailbox 直接协作 +- 维护共享记忆、任务活动流、人工接管记录 +- 展示 Codex CLI 默认模型,并支持按会话覆盖模型 +- 支持按角色同步恢复入口、重建角色会话 +- 一键打开角色对应的原生 Codex 接管口 + +## Codex CLI 集成现状 + +当前已经接入真实 Codex CLI,用在这些环节: + +- lead 主对话回复 +- 团队草案生成 +- 成员线程回复 +- 任务启动回执 +- reviewer 审查接单回执 +- reviewer 审查通过 / 打回结论 +- 执行成员返工后的复审回执 + +默认模型跟随本机 Codex CLI 配置。 +Web 端可以对单个角色会话临时覆盖模型,也可以切回继承默认模型。 +可选模型列表现在优先通过 Codex app-server 的 `model/list` 动态获取,只有失败时才退回配置文件兜底。 + +## 本地存储现在放在哪里 + +全局索引只保留在: + +```text +data/db.json +``` + +这个文件现在只保存项目索引,不再塞整份项目运行状态。 + +每个项目自己的工作台数据会放到该项目目录下: + +```text +<项目目录>\.agents-team\workbench\ +``` + +目前会生成这些文件: + +```text +project-state.json 项目主状态 +mailbox.json 队内消息 +shared-memory.json 结构化共享记忆 +shared-memory.md 可直接阅读的共享记忆摘要 +task-activity.json 任务活动流 +``` + +这样项目级的“乱七八糟文件”都会被收口到项目自己的隐藏目录里,不会把用户项目根目录弄乱。 + +另外,mailbox 现在已经有服务端摘要分组: + +- lead 先看摘要卡 +- 原始成员往来默认折叠 +- 同任务或同成员对话会自动收进同一组 + +## 还没做完的部分 + +- 固定角色已经支持按 thread 恢复,但还没完全做到“所有长期身份都只靠原生会话驱动” +- 页内还没有嵌入原生终端 +- mailbox 目前已经可用,但“长对话自动折叠成更强摘要卡”还可以继续加强 +- README 之外的设计文档还会继续补齐 + +## 运行方式 + +安装依赖: + +```bash +npm install +``` + +开发模式: + +```bash +npm run dev +``` + +构建: + +```bash +npm run build +``` + +启动构建产物: + +```bash +npm run start +``` + +默认地址: + +```text +http://localhost:8787 +``` + +## 当前目录结构 + +```text +server/ 本地后端 API +web/ 前端工作台 +data/ 全局项目索引 +docs/ 设计与交接文档 +``` + +## 本轮已验证 + +已经实际验证过: + +- `npm run build` +- 旧 `data/db.json` 自动迁移到项目自己的 `.agents-team/workbench` +- `/api/health` +- `/api/projects` +- `/api/projects/:projectId` + +如果你正在推进这个仓库本身,直接打开当前仓库目录就能看到: + +```text +D:\AI Code\Agents Team\.agents-team\workbench +``` diff --git a/docs/CODEX_APP_NEW_CHAT_PROMPT.md b/docs/CODEX_APP_NEW_CHAT_PROMPT.md new file mode 100644 index 0000000..98204a0 --- /dev/null +++ b/docs/CODEX_APP_NEW_CHAT_PROMPT.md @@ -0,0 +1,53 @@ +# 给新 Codex App 对话的启动提示词 + +把下面这段直接发给新的 Codex app 对话即可: + +--- + +先不要假设这是个空白仓库,也不要把它理解成只有 mock 的 MVP。 + +请先完整阅读这些文件: + +1. `D:\AI Code\Agents Team\README.md` +2. `D:\AI Code\Agents Team\docs\design.md` +3. `D:\AI Code\Agents Team\docs\CODEX_APP_PRODUCT_HANDOFF.md` +4. `D:\AI Code\Agents Team\docs\OLD_MAIN_20260314_PLAN_SUMMARY.md` + +然后按下面顺序执行: + +1. 先检查当前仓库的真实状态,至少查看这些文件: + - `D:\AI Code\Agents Team\package.json` + - `D:\AI Code\Agents Team\web\src\pages\HomePage.tsx` + - `D:\AI Code\Agents Team\web\src\pages\ProjectPage.tsx` + - `D:\AI Code\Agents Team\web\src\services\api.ts` + - `D:\AI Code\Agents Team\server\src\index.ts` + - `D:\AI Code\Agents Team\server\src\store.ts` + - `D:\AI Code\Agents Team\server\src\types.ts` + - `D:\AI Code\Agents Team\server\src\codexConfig.ts` + - `D:\AI Code\Agents Team\server\src\runtime.ts` +2. 先用中文总结你现在的理解: + - 产品定位 + - 已锁定的产品约束 + - 当前仓库已经实现到什么程度 + - 当前最关键的工作流 + - 当前还没做完的尾项 + - 哪些地方最容易跑偏 +3. 再告诉我: + - 你认为接下来最值得优先推进的 3 到 5 个具体动作 + - 每个动作为什么优先 + - 验收标准是什么 +4. 然后直接开始动手,不要没有必要地停下来等确认 + +额外要求: + +- 默认中文沟通,面向非程序员表达。 +- 不要把产品做成“全局固定团队系统”。 +- 不要做成后台管理表单页。 +- 核心必须围绕“项目级 lead + 项目级团队 + lead 对话驱动工作台”。 +- 项目摸底必须用户主动确认触发,不能自动偷偷跑。 +- 默认不自动 commit,不自动 push。 +- 项目级协作状态要放在项目自己的 `.agents-team\workbench`。 +- 能继续推进就继续推进,不要频繁停下来问。 +- 优先以当前代码和当前文档为准,不要被旧聊天误导。 + +如果你读完后发现有不确定的地方,先列出来;如果没有阻塞,就直接继续做。 diff --git a/docs/CODEX_APP_PRODUCT_HANDOFF.md b/docs/CODEX_APP_PRODUCT_HANDOFF.md new file mode 100644 index 0000000..eae1242 --- /dev/null +++ b/docs/CODEX_APP_PRODUCT_HANDOFF.md @@ -0,0 +1,191 @@ +# Codex App 产品交接文档 + +更新时间:2026-03-14 + +## 1. 先看什么 + +如果后面在 Codex app 里开新对话,先按这个顺序读: + +1. `D:\AI Code\Agents Team\README.md` +2. `D:\AI Code\Agents Team\docs\design.md` +3. `D:\AI Code\Agents Team\docs\OLD_MAIN_20260314_PLAN_SUMMARY.md` +4. `C:\Users\54972\.openclaw\agents\main\sessions\2b99b736-9bb5-4444-a72e-36fe796ccf3a.jsonl` + +优先级也按这个顺序理解: + +1. 当前仓库真实代码和文档 +2. 本文件 +3. 旧总结 +4. 旧原始会话 + +## 2. 当前仓库已经实现到哪里 + +当前仓库不是空壳,也不再只是“mock MVP”。 + +现在已经有: + +- 项目级 `lead` +- 项目级团队草案 +- 正式团队确认 +- 团队变更确认 +- lead 主对话 +- 成员线程 +- mailbox +- 共享记忆 +- 任务推进 +- reviewer 审查闭环 +- 角色运行态 +- 原生接管口 +- 会话恢复 / 重建 +- 动态模型列表 + +而且已经接入真实 Codex CLI,用在这些环节: + +- lead 回复 +- 团队草案生成 +- 成员线程回复 +- 任务启动回执 +- reviewer 接单回执 +- reviewer 审查结论 +- 成员返工后复审回执 + +## 3. 现在这个产品到底是什么 + +一句话: + +> 这是一个本地优先、项目级 lead 驱动、面向非程序员的多-agent 工作台。 + +再拆开说: + +- 不是全局固定团队系统 +- 不是后台管理表单页 +- 不是点开就自动扫描项目的工具 +- 不是一堆 agent 名字平铺展示的假看板 + +它的核心一直是: + +- 项目级 `lead` +- 项目级团队 +- `lead` 对话驱动工作台 + +## 4. 已经锁死的产品原则 + +后面新对话不要再把这些重新讨论一遍: + +- 用户默认只和当前项目的 `lead` 聊 +- 团队先讨论出草案,再确认正式创建 +- 正式团队变更要先出变更清单,再单独确认 +- 正式成员之间允许直接互相通讯 +- mailbox 默认“任务优先,但允许自由消息” +- 共享记忆默认由 lead 整理入库 +- 默认不自动 `commit` +- 默认不自动 `push` +- 项目摸底必须显式确认 +- 不改 Codex CLI 本体,只做外层薄适配 +- 项目里的协作状态放在项目自己的 `.agents-team\workbench` + +## 5. 当前 UI 方向 + +当前项目页已经是下面这个结构: + +- 左侧:像素办公室 / 成员状态 +- 中间:大面积主对话区 +- 右侧:收纳区 + +收纳区现在已经有: + +- 团队草案 +- 团队变更确认清单 +- 任务推进 +- Mailbox +- 共享记忆 +- 项目摸底 +- 运行态 / 接管口 +- 人工接管 +- Git 状态 + +当前风格方向: + +- 默认深色工作台 +- 中文白话文案 +- 面向非程序员 +- lead 主窗口始终是默认入口 + +## 6. 当前架构要点 + +### 6.1 存储 + +全局索引: + +- `D:\AI Code\Agents Team\data\db.json` + +项目状态: + +- `<项目目录>\.agents-team\workbench\project-state.json` +- `<项目目录>\.agents-team\workbench\mailbox.json` +- `<项目目录>\.agents-team\workbench\shared-memory.json` +- `<项目目录>\.agents-team\workbench\shared-memory.md` +- `<项目目录>\.agents-team\workbench\task-activity.json` + +### 6.2 角色会话 + +每个正式角色都有独立运行态。 + +当前已经支持: + +- `lastThreadId` +- `resumeCommand` +- `takeoverCommand` +- 身份状态 +- 模型覆盖 +- 同步恢复入口 +- 重建角色会话 + +### 6.3 模型来源 + +当前模型能力已经升级成: + +1. 优先走 Codex app-server `model/list` +2. 失败时退回配置文件 + +所以现在页面里拿到的模型,不只是字符串列表,而是带元数据的真实可用模型集合。 + +## 7. 当前最值得继续推进的尾项 + +如果后面继续推进,优先顺序建议是: + +1. 继续把角色长期身份往“更彻底的原生会话连续性”收口 +2. 继续增强 mailbox 的超长对话摘要和冲突压缩 +3. 评估页内原生终端 / 更顺手接管体验 +4. 同步剩余历史文档 + +## 8. 最容易跑偏的地方 + +后面最容易犯的错还是这几个: + +- 把它做成全局固定团队系统 +- 把它做成后台管理表单页 +- 先做一堆视觉炫技,主工作流反而变假 +- 偷偷自动做项目摸底 +- 过早把产品做成黑盒自动驾驶 +- 把整个产品焊死在某个 CLI 私有实现上 + +## 9. 给后续 Codex app 的执行要求 + +如果后面继续用 Codex app 做: + +- 默认中文输出 +- 面向非程序员表达 +- 没有阻塞就继续推进,不要频繁停下来问 +- 优先看当前代码,不要只看历史聊天 +- 修改后一定要自己验证 + +如果要做长任务或隐藏执行: + +- 要有日志 / 输出监控 +- 要能看到文件变化 +- 要做阶段性验收 + +核心提醒: + +> 每一步都要反问自己,这一步是不是让它更像一个真正的“项目级 lead 工作台”。 diff --git a/docs/OLD_MAIN_20260314_PLAN_SUMMARY.md b/docs/OLD_MAIN_20260314_PLAN_SUMMARY.md new file mode 100644 index 0000000..9de8e2f --- /dev/null +++ b/docs/OLD_MAIN_20260314_PLAN_SUMMARY.md @@ -0,0 +1,276 @@ +# 旧主会话方案整理(main_old_20260314) + +来源: +- 当前整理文档位置:`D:\AI Code\Agents Team\docs\OLD_MAIN_20260314_PLAN_SUMMARY.md` +- 旧主会话键:`agent:main:main_old_20260314` +- 原始会话文件(档案):`C:\Users\54972\.openclaw\agents\main\sessions\2b99b736-9bb5-4444-a72e-36fe796ccf3a.jsonl` + +这份文档只保留当时讨论里真正重要的产品方向、执行优先级和工作规则,方便后续继续做项目时直接参考。 + +补充说明: +- 这份文档是“历史方案与方向总结”,不是当前仓库实现状态说明。 +- 后续如果和 `D:\AI Code\Agents Team` 里的当前代码、`README.md`、`docs/design.md` 冲突,应优先以当前仓库现状为准。 + +## 一、项目核心定位 + +- 这是一个“项目级 lead + 项目级团队”的系统,不是全局固定团队。 +- 每个项目都有自己独立的 lead,用户默认只和该项目的 lead 对话。 +- 团队不是全局写死,而是用户先和项目 lead 讨论,再由 lead 产出团队草案,用户确认后创建。 +- 同岗位允许多个成员,但应优先复用该项目里的老成员。 +- reviewer 默认有较强否决权,但允许 lead 或用户 override。 +- 允许重建项目 lead,但新 lead 上线前需要前一个 lead 做交接。 +- 默认不自动 commit、不自动 push。 +- 第一阶段先锁定“当前项目目录”范围,后续再考虑全局访问。 +- 整体目标是本地优先:本地网页端 + 本地后台服务,不是云端产品。 + +## 二、MVP 骨架阶段结论 + +当时已经确认的第一版 MVP 骨架方向: + +- 首页 / 项目列表页 +- 添加项目(手动输入本地路径) +- 项目工作台页 +- lead 对话区 +- 团队草案区 +- 任务状态区 +- 项目状态 / Git 状态区 +- 人工接管入口区 +- 本地 JSON 持久化 +- 中文 README +- 中文设计说明 + +同时明确: +- “项目摸底”必须由用户主动确认触发,不能点开项目就自动跑。 +- 第一版先可用,但要面向非程序员,尽量全中文、清晰、好理解。 + +## 三、从参考项目里得出的产品判断 + +### 1. 对 oh my opencode 的判断 + +值得借的点: +- 规划和执行分离 +- 主调度者 / lead 的思路 +- 后台并行成员 +- 把前面学到的约定和坑往后传 +- 强调“别半路摆烂”,完成前要验证 +- 先判断任务类型,再决定怎么做 + +不适合直接照搬的点: +- 太偏终端极客工作流 +- 角色命名过于神话化,不适合产品化 +- 更像全局总控,不是项目级团队 +- 一键全自动太黑盒,不适合当前产品方向 + +### 2. 对 Pixel Agents 的判断 + +结论: +- 值得借的是“施工可视化层” +- 不值得直接照搬的是“业务逻辑层” + +适合借的内容: +- 办公室 / 工位 / 小人这类可视化表达 +- 把抽象的 agent 工作状态变成一眼能看懂的画面 +- 不改 CLI 本体,走外层观察和可视化适配 + +不适合直接搬的内容: +- VS Code 扩展壳子 +- Claude Code 专用监听逻辑 +- 终端和 transcript 的专用适配 + +后续正确姿势: +- 先做我们自己的业务骨架 +- 再把 Pixel Agents 风格的可视化层接到我们自己的状态源上 + +## 四、UI 与工作台方向 + +后续 UI 方向后来被明确修正为: + +- 页面中心必须是 lead 对话区 +- lead 对话区要占最大面积 +- 参考 Codex app 的“主对话驱动”感受,但不能照抄 +- 次级功能要收纳,不再平铺一地 +- 在 lead 对话区下沿放成员岗位标签 / 线程切换 +- 点击不同岗位标签后,看到对应成员线程或工作内容 + +明确的视觉偏好: +- 默认做成深色工作台风格 +- 页面要更像真正的工作台,不像表单后台 +- 中文文案尽量白话 + +## 五、整晚冲刺版目标(后来确认的最终版) + +后来目标从“只改页面”升级为“整晚尽量把项目推进成像真产品”。 + +最终优先级被确认成: + +### P0:完整多-agent 编排内核 + +至少要把一条真正能跑通的主链路做出来: + +- 项目级 lead +- 项目级团队配置 +- lead 创建团队 +- 成员线程 / 会话结构 +- 任务拆分 +- 任务派发 +- reviewer 打回 +- coder 修复 +- 再审 +- 结果回到 lead + +要求: +- 做成真工作流 +- 不是只做假壳子 + +### P1:像素办公室 / 施工可视化 + +要求: +- 不是 VS Code 插件 +- 是网页里的一个模块 +- 先接我们自己的状态源 +- 至少能看到: + - lead + - reviewer + - 实现成员 + - 测试 / 验收成员 + - 当前状态(工作中 / 等待 / 需要确认 / 完成 / 打回) + +### P2:lead 对话工作台彻底重做 + +要求: +- 大面积主对话区 +- 下沿成员线程 tags +- 次级功能收纳 +- 更像主对话驱动的工作台体验 + +## 六、当时制定的执行路线 + +### 第一阶段:重构数据模型 + +计划中的核心对象: +- Project +- Lead +- TeamConfig +- Member +- Thread +- Task +- Review +- Handover +- ManualAction +- Discovery + +目标: +- 后面接真实内核和像素办公室时,不需要推翻重来 + +### 第二阶段:把项目摸底做真 + +目标: +- 真实读取项目目录 +- 识别技术栈 +- 读取 Git 基本状态 +- 给出中文项目摘要 +- 给 lead 一个可用的摸底结果 + +### 第三阶段:把 lead 变成“真 lead” + +重点: +- 用户意图识别 +- 是否需要摸底 +- 是否需要讨论团队 +- 是否需要开始执行 +- 是否需要 review +- 是否需要打回 +- 是否需要重建 lead + +也就是把 lead workflow 做成状态机 / 规则流。 + +### 第四阶段:把多-agent 内核接起来 + +计划推进内容: +- 成员线程结构 +- 任务路由 +- review 流 +- 返工流 +- 结果汇总 +- 项目内老成员优先复用逻辑 + +### 第五阶段:做像素办公室 v0 + +条件: +- 前面的状态流先成型 + +优先内容: +- 嵌在网页里的施工可视化区 +- 成员卡 / 小人 / 工位类视觉表达 +- 状态变化驱动 UI +- 至少做到“能一眼看懂谁在干嘛” + +### 第六阶段:整体验收和打磨 + +要自己验: +- build +- start +- 页面实际打开 +- 项目页工作流 +- 摸底链路 +- 团队链路 +- 成员线程 +- 像素办公室入口 / 模块 +- 中文文案 +- 视觉细节 + +## 七、后来补充确认的偏好 + +- 深色工作台风格 +- 像素办公室默认放在对话框左侧 +- 最好支持左右互换布局 + +## 八、后台调用 Codex 的硬规则 + +后来用户明确允许: +- 下班后可用隐藏后台模式调用 Codex + +但附带强约束: +- 必须带监控 +- 不能无监控放养 + +最终确认的三层监控规则: + +### 1. 实时输出监控 +- 持续写日志 +- 可以实时 tail +- 一旦报错或停滞,第一时间发现 + +### 2. 文件变化监控 +- 盯改了哪些文件 +- 看有没有异常大改 +- 看 git 状态变化 +- 看关键产物是否落地 + +### 3. 阶段性验收 +- 不管 Codex 说没说完成,都要自己验 +- 至少验: + - build + - start + - 页面 + - API + - 关键流程 + +这条规则的核心表述是: + +> 后台模式可以用,但必须带监控。 +> 继续由人来当 lead / 监工 / 验收人,Codex 只是施工队之一。 + +## 九、这份文档怎么用 + +以后如果你继续做这个项目,优先看这几个部分: + +- 想看产品定位:看“项目核心定位” +- 想看 UI 方向:看“UI 与工作台方向” +- 想看最终冲刺目标:看“整晚冲刺版目标” +- 想看执行分期:看“执行路线” +- 想看后台 Codex 规则:看“后台调用 Codex 的硬规则” + +如果后面需要,我还可以继续把旧主会话里的内容再拆成两份: +- 一份“产品方案版” +- 一份“执行路线版” diff --git a/docs/UI_VISUAL_STANDARDS.md b/docs/UI_VISUAL_STANDARDS.md new file mode 100644 index 0000000..6c039f8 --- /dev/null +++ b/docs/UI_VISUAL_STANDARDS.md @@ -0,0 +1,795 @@ +# Agents Team - UI 视觉标准文档 + +> 版本: v1.0 +> 日期: 2026-03-14 +> 适用范围: Web 前端界面开发 + +--- + +## 1. 设计原则 + +### 1.1 核心理念 +- **简洁优先**: 去除冗余装饰,聚焦内容展示 +- **深色主题**: 采用专业深色配色,减少视觉疲劳 +- **信息层级**: 通过颜色和间距清晰区分信息层级 +- **流畅交互**: 平滑的过渡动画提升用户体验 + +### 1.2 布局架构 +采用经典的三栏式布局: +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ [左侧边栏] [中间主区域] [右侧面板 - 像素办公室] │ +│ ───────── ───────────── ─────────────────── │ +│ 可折叠 对话/内容区 Agent详情/像素办公室 │ +│ 项目列表 输入框(底部) 420px 宽度 │ +│ 设置入口 480px 宽度 │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**布局尺寸规范:** +- 左侧边栏: 260px (可折叠至 60px) +- 中间对话区域: 最大 480px,居中显示 +- 右侧像素办公室: 420px 固定宽度 +- 输入框区域: 最大 640px,与对话区域协调 + +--- + +## 2. 色彩系统 + +### 2.1 基础色彩变量 + +```css +:root { + /* 背景色 */ + --bg-primary: #0d0d0d; /* 主背景 - 纯黑 */ + --bg-secondary: #141414; /* 次级背景 - 侧边栏 */ + --bg-tertiary: #1a1a1a; /* 三级背景 */ + --bg-elevated: #242424; /* elevated 背景 - 卡片/输入框 */ + --bg-hover: #2a2a2a; /* 悬停状态 */ + + /* 文字色 */ + --text-primary: #ffffff; /* 主文字 */ + --text-secondary: #a1a1a1; /* 次级文字 */ + --text-tertiary: #6e6e6e; /* 三级文字 - 提示/禁用 */ + + /* 边框 */ + --border-color: #2a2a2a; /* 默认边框 */ + --border-hover: #3a3a3a; /* 悬停边框 */ +} +``` + +### 2.2 强调色 + +```css +:root { + /* 主强调色 - 青绿色系 */ + --accent-primary: #10a37f; /* 主按钮/高亮 */ + --accent-primary-hover: #0d8c6d; /* 悬停状态 */ + --accent-secondary: #19c59f; /* 渐变/装饰 */ + + /* Agent 角色色 */ + --agent-lead: #f59e0b; /* Lead - 琥珀色 */ + --agent-worker: #3b82f6; /* Worker - 蓝色 */ + --agent-reviewer: #8b5cf6; /* Reviewer - 紫色 */ + + /* 状态色 */ + --status-success: #10a37f; /* 成功/运行中 */ + --status-warning: #f59e0b; /* 警告/等待 */ + --status-error: #ef4444; /* 错误 */ + --status-neutral: #6b7280; /* 中性/默认 */ +} +``` + +### 2.3 色彩使用规范 + +| 场景 | 颜色变量 | 示例 | +|------|----------|------| +| 页面背景 | `--bg-primary` | #0d0d0d | +| 侧边栏背景 | `--bg-secondary` | #141414 | +| 卡片/输入框 | `--bg-elevated` | #242424 | +| 主要按钮 | `--accent-primary` | #10a37f | +| 主要文字 | `--text-primary` | #ffffff | +| 次要说明 | `--text-secondary` | #a1a1a1 | +| 边框 | `--border-color` | #2a2a2a | + +--- + +## 3. 间距系统 + +### 3.1 间距变量 + +```css +:root { + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 24px; + --spacing-2xl: 32px; +} +``` + +### 3.2 使用规范 + +| 场景 | 间距值 | 用途 | +|------|--------|------| +| 图标与文字 | `--spacing-xs` (4px) | 内联图标间距 | +| 紧凑元素 | `--spacing-sm` (8px) | 小按钮、标签 | +| 标准间距 | `--spacing-md` (12px) | 卡片内边距、表单字段 | +| 区块间距 | `--spacing-lg` (16px) | 区块分隔 | +| 大区块 | `--spacing-xl` (24px) | 页面边距、大区块 | +| 超大间距 | `--spacing-2xl` (32px) | Hero 区域、特殊区块 | + +--- + +## 4. 圆角系统 + +```css +:root { + --radius-sm: 6px; /* 小按钮、标签 */ + --radius-md: 8px; /* 标准按钮、输入框 */ + --radius-lg: 12px; /* 卡片、面板 */ + --radius-xl: 16px; /* 大卡片、模态框 */ + --radius-full: 9999px; /* 胶囊形、圆形 */ +} +``` + +### 使用规范 + +| 组件 | 圆角值 | +|------|--------| +| 小按钮/标签 | `--radius-md` (8px) | +| 输入框 | `--radius-lg` (12px) | +| 卡片/面板 | `--radius-lg` (12px) | +| 大卡片 | `--radius-xl` (16px) | +| 状态标签 | `--radius-full` (胶囊形) | +| 头像 | `--radius-lg` (12px) 或圆形 | + +--- + +## 5. 字体规范 + +### 5.1 字体栈 + +```css +font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; +``` + +### 5.2 字号层级 + +| 层级 | 大小 | 字重 | 用途 | +|------|------|------|------| +| 页面标题 | 32px | 700 | 首页 Hero | +| 区块标题 | 18px | 600 | 表单标题 | +| 卡片标题 | 16px | 600 | 面板标题 | +| 正文 | 14px | 400 | 普通文字 | +| 小字 | 13px | 400 | 次要信息 | +| 标签 | 12px | 500 | 标签、时间戳 | +| 微字 | 11px | 500 | 状态标签 uppercase | + +--- + +## 6. 组件规范 + +### 6.1 按钮 + +#### 主要按钮 (Primary) +```css +.primary-button { + padding: 12px 16px; + background: var(--accent-primary); + color: white; + border-radius: var(--radius-lg); + font-size: 14px; + font-weight: 500; + transition: background 150ms ease; +} + +.primary-button:hover { + background: var(--accent-primary-hover); +} +``` + +#### 次要按钮 (Secondary) +```css +.secondary-button { + padding: 12px 16px; + background: var(--bg-elevated); + color: var(--text-secondary); + border-radius: var(--radius-lg); + font-size: 14px; + font-weight: 500; + transition: all 150ms ease; +} + +.secondary-button:hover { + background: var(--bg-hover); + color: var(--text-primary); +} +``` + +#### 图标按钮 +```css +.icon-button { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + transition: all 150ms ease; +} + +.icon-button:hover { + background: var(--bg-hover); + color: var(--text-primary); +} +``` + +### 6.2 输入框 + +#### 标准输入框 +```css +.input-field { + width: 100%; + padding: 12px 16px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + color: var(--text-primary); + font-size: 14px; + transition: all 150ms ease; +} + +.input-field:focus { + outline: none; + border-color: var(--accent-primary); + background: var(--bg-hover); +} + +.input-field::placeholder { + color: var(--text-tertiary); +} +``` + +#### Kimi 风格聊天输入框 +```css +.chat-input-container { + display: flex; + align-items: flex-end; + gap: 8px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: 24px; /* 大圆角,胶囊形状 */ + padding: 8px 8px 8px 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 150ms ease; +} + +.chat-input-container:focus-within { + border-color: var(--accent-primary); + box-shadow: 0 2px 12px rgba(16, 163, 127, 0.15); +} + +/* 左侧工具按钮区 */ +.chat-input-tools { + display: flex; + align-items: center; + gap: 4px; + padding-right: 8px; + border-right: 1px solid var(--border-color); +} + +.input-tool-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: var(--text-secondary); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.input-tool-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +/* 发送按钮 - 圆形 */ +.input-action-btn.send { + width: 32px; + height: 32px; + border: none; + border-radius: 50%; /* 圆形 */ + background: var(--accent-primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.input-action-btn.send:hover:not(:disabled) { + background: var(--accent-primary-hover); + transform: scale(1.05); +} +``` + +### 6.3 卡片/面板 + +```css +.card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 16px; +} + +.card-elevated { + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + padding: 24px; +} +``` + +### 6.4 状态标签 (Status Badge) + +```css +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + border-radius: 9999px; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* 状态变体 */ +.status-badge.success { + background: rgba(16, 163, 127, 0.15); + color: #34d399; + border: 1px solid rgba(16, 163, 127, 0.3); +} + +.status-badge.warning { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.status-badge.neutral { + background: rgba(100, 116, 139, 0.15); + color: #94a3b8; + border: 1px solid rgba(100, 116, 139, 0.3); +} +``` + +### 6.5 消息气泡 + +```css +/* Agent 消息 */ +.message-bubble { + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + max-width: 80%; +} + +/* 用户消息 */ +.message-bubble.user { + background: var(--bg-elevated); + border-color: var(--accent-primary); + margin-left: auto; +} +``` + +### 6.6 像素办公室 (Pixel Office) + +像素办公室是右侧面板的核心功能,以像素风格展示项目中的 Agents 状态。 + +#### 办公室布局 +```css +.pixel-office-panel { + width: 420px; + height: 100%; + background: var(--bg-secondary); + border-left: 1px solid var(--border-color); + display: flex; + flex-direction: column; +} + +.office-room { + position: relative; + background: var(--bg-tertiary); + border-radius: 12px; + padding: 24px; + min-height: 320px; +} + +/* 地板网格 */ +.office-floor { + position: absolute; + display: flex; + flex-direction: column; + gap: 8px; +} + +.office-floor-tile { + flex: 1; + background: var(--bg-hover); + border-radius: 6px; + opacity: 0.5; +} +``` + +#### 像素角色 +```css +.pixel-character { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + transition: transform 150ms ease; +} + +.pixel-character:hover { + transform: translateY(-2px); +} + +.pixel-avatar { + position: relative; + width: 44px; + height: 44px; + background: var(--bg-elevated); + border: 2px solid var(--border-color); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +/* 角色徽章 */ +.pixel-role-badge { + position: absolute; + top: -6px; + right: -6px; + width: 18px; + height: 18px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; +} + +/* 状态指示点 */ +.pixel-status-dot { + position: absolute; + bottom: -2px; + left: 50%; + transform: translateX(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + border: 2px solid var(--bg-secondary); +} + +.pixel-status-dot.typing { + background: #10a37f; + animation: pulse 1.5s ease-in-out infinite; +} + +.pixel-status-dot.reading { background: #3b82f6; } +.pixel-status-dot.waiting { background: #f59e0b; } +.pixel-status-dot.idle { background: #6b7280; } +``` + +#### 角色图标映射 +| 角色 | 图标 | +|------|------| +| Lead | 👑 | +| Developer | 💻 | +| Reviewer | 👀 | +| Architect | 🏗️ | +| Tester | 🧪 | +| 其他 | 🤖 | + +--- + +## 7. 布局规范 + +### 7.1 三栏布局 + +```css +:root { + /* 布局尺寸变量 */ + --sidebar-width: 260px; + --sidebar-collapsed-width: 60px; + --agent-panel-width: 420px; + --chat-max-width: 480px; + --input-max-width: 640px; +} + +.app-layout { + display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +/* 左侧边栏 */ +.sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border-color); + flex-shrink: 0; +} + +.sidebar-collapsed { + width: var(--sidebar-collapsed-width); +} + +/* 中间主区域 */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + background: var(--bg-primary); +} + +/* 对话消息区域 - 收窄居中 */ +.messages-container { + max-width: var(--chat-max-width); + margin: 0 auto; + width: 100%; +} + +/* 输入框区域 - Kimi 风格 */ +.chat-input-wrapper { + max-width: var(--input-max-width); + margin: 0 auto; + width: 100%; +} + +/* 右侧面板 - 像素办公室 */ +.agent-panel { + width: var(--agent-panel-width); + background: var(--bg-secondary); + border-left: 1px solid var(--border-color); + flex-shrink: 0; +} +``` + +### 7.2 内容区域结构 + +``` +中间主区域: +├── header (60px 高度) +│ ├── 标题 + 状态标签 +│ └── 操作按钮 +├── agent-selector (可滚动) +│ └── Agent 切换标签 +├── chat-area (flex: 1, 可滚动) +│ └── 消息列表 +└── chat-input-container + ├── toolbar (模型/上下文) + └── input-area (输入框+发送) +``` + +--- + +## 8. 交互规范 + +### 8.1 过渡动画 + +```css +:root { + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; +} + +/* 通用过渡 */ +.interactive { + transition: all var(--transition-fast); +} + +/* 悬停效果 */ +.hover-lift:hover { + transform: translateY(-1px); +} + +/* 按钮点击 */ +.button:active { + transform: scale(0.98); +} +``` + +### 8.2 滚动条样式 + +```css +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-hover); +} +``` + +--- + +## 9. 图标规范 + +### 9.1 图标尺寸 + +| 场景 | 尺寸 | +|------|------| +| 小图标 (内联) | 16px | +| 标准图标 | 18px | +| 大图标 | 20px | +| 按钮图标 | 与文字搭配 | + +### 9.2 图标风格 +- 使用 SVG 图标 +- 线条粗细: 2px +- 风格: 线性图标 (outline) +- 颜色: 继承父元素颜色 + +### 9.3 常用图标 + +```tsx +// 发送 + + + + +// 暂停 + + + + + +// 设置 + + + + +``` + +--- + +## 10. 响应式设计 + +### 10.1 断点 + +```css +/* 大屏幕 - 三栏完整显示 */ +@media (min-width: 1201px) { + /* 默认样式 */ +} + +/* 中等屏幕 - 右侧面板可收起 */ +@media (max-width: 1200px) { + .agent-panel { + position: fixed; + right: 0; + transform: translateX(100%); + } + .agent-panel.open { + transform: translateX(0); + } +} + +/* 小屏幕 - 左侧边栏可收起 */ +@media (max-width: 768px) { + .sidebar { + position: fixed; + left: 0; + transform: translateX(-100%); + } + .sidebar.open { + transform: translateX(0); + } +} +``` + +### 10.2 移动端适配 +- 侧边栏变为抽屉式 +- 输入框变为垂直布局 +- 触摸目标最小 44px + +--- + +## 11. 代码示例 + +### 11.1 完整组件示例 + +```tsx +// StatusBadge 组件 +function StatusBadge({ + tone, + children +}: { + tone: "success" | "warning" | "neutral" | "error"; + children: React.ReactNode; +}) { + const toneClasses = { + success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", + warning: "bg-amber-500/15 text-amber-400 border-amber-500/30", + neutral: "bg-slate-500/15 text-slate-400 border-slate-500/30", + error: "bg-rose-500/15 text-rose-400 border-rose-500/30", + }; + + return ( + + {children} + + ); +} +``` + +### 11.2 CSS 类名命名规范 + +使用语义化命名: +- `.sidebar` - 侧边栏 +- `.main-content` - 主内容区 +- `.chat-input-container` - 聊天输入容器 +- `.agent-panel` - Agent 面板 +- `.message-bubble` - 消息气泡 + +避免使用: +- 无意义的缩写 +- 颜色相关的类名 (如 `.red-button`) +- 具体尺寸 (如 `.width-200`) + +--- + +## 12. 检查清单 + +在提交代码前,请确认: + +- [ ] 使用了正确的色彩变量 +- [ ] 间距符合规范 +- [ ] 圆角一致 +- [ ] 字体层级清晰 +- [ ] 过渡动画流畅 +- [ ] 响应式适配完成 +- [ ] 暗色主题下对比度足够 +- [ ] 交互状态完整 (hover, active, disabled) + +--- + +## 附录: 参考资源 + +- 设计灵感: Kimi (kimi.moonshot.cn), Claude (claude.ai) +- 色彩工具: https://colorhunt.co/ +- 图标库: Lucide Icons (https://lucide.dev/) +- CSS 变量: 现代浏览器均支持 + +--- + +**文档维护**: 如有更新需求,请同步修改本文档。 diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..f2ad2ba --- /dev/null +++ b/docs/design.md @@ -0,0 +1,261 @@ +# 设计说明 + +更新时间:2026-03-14 + +## 1. 当前产品定义 + +这不是“全局固定团队系统”,而是: + +- 项目级 `lead` +- 项目级团队 +- `lead` 对话驱动工作台 + +用户默认只和当前项目的 `lead` 聊。 +团队由 `lead` 先提出草案,用户确认后才创建正式成员。 +团队内正式成员之间允许直接互相发消息,不限制成“执行成员只能找 reviewer”。 + +## 2. 当前已经成立的主工作流 + +当前仓库已经不只是 MVP 骨架,而是已经有一条真实可跑的主链路: + +1. 创建项目 +2. 生成项目专属团队草案 +3. 确认团队 +4. lead 派发任务 +5. 成员执行 +6. reviewer 接单审查 +7. 打回 / 通过 +8. 成员返工后复审 +9. 结果回到 lead 汇总 + +其中 lead 回复、团队草案生成、成员线程回复、任务启动回执、reviewer 接单、reviewer 审查结论、成员复审回执,已经接入真实 Codex CLI。 + +## 3. 当前核心数据结构 + +服务端类型定义在: + +- `D:\AI Code\Agents Team\server\src\types.ts` + +当前比较关键的对象有: + +- `Project` +- `LeadProfile` +- `TeamProposal` +- `TeamState` +- `TeamChangeState` +- `ProjectMember` +- `RuntimeSession` +- `MailboxMessage` +- `MailboxDigest` +- `SharedMemoryEntry` +- `TaskItem` +- `ReviewItem` +- `TaskActivityEntry` + +### 3.1 阶段状态 + +项目阶段已经固定为: + +- `lead_only` +- `discovery_pending` +- `team_drafting` +- `team_pending_confirmation` +- `team_active` +- `executing` +- `reviewing` +- `blocked` +- `done` + +### 3.2 团队变更确认 + +当正式团队已经存在时,再改团队草案,不会直接覆盖正式团队。 + +当前会先生成: + +- 新增项 +- 移出项 +- 调整项 + +然后再单独确认团队变更。 + +### 3.3 运行态 / 会话身份 + +每个正式角色都会保存: + +- 当前模型 +- 模型来源 +- 最近 thread id +- 恢复命令 +- 接管命令 +- 最近输出 +- 错误状态 +- 身份状态 + +身份状态当前分为: + +- `fresh` +- `resume_ready` +- `needs_rebuild` + +## 4. Codex CLI 接法 + +当前策略是“薄适配层”,不修改 Codex CLI 本体。 + +核心文件: + +- `D:\AI Code\Agents Team\server\src\runtime.ts` +- `D:\AI Code\Agents Team\server\src\codexConfig.ts` + +### 4.1 已经做成的事情 + +- 真实调用 Codex CLI 生成 lead / 成员 / reviewer 关键回复 +- 角色 thread 建立后,后续优先走 `codex exec resume` +- 原生接管口使用 `codex resume ...` +- Web 端可以对单个角色覆盖模型 +- 模型列表优先通过 Codex app-server 的 `model/list` 动态获取 + +### 4.2 当前真实模型能力 + +Web 端现在看到的可用模型,不再主要靠 `config.toml` 猜,而是: + +1. 优先调用 Codex app-server `model/list` +2. 失败时退回配置文件兜底 + +所以页面里已经能看到: + +- 模型显示名 +- 模型说明 +- 默认推理档位 +- 支持的推理档位 +- 是否支持 personality +- 是否是当前默认模型 + +## 5. 存储策略 + +### 5.1 全局索引 + +全局只保留项目索引: + +- `D:\AI Code\Agents Team\data\db.json` + +这个文件现在只保存: + +- `id` +- `name` +- `path` +- `description` +- `createdAt` +- `updatedAt` + +### 5.2 项目级状态 + +每个项目自己的真实状态保存在: + +- `<项目目录>\.agents-team\workbench\` + +当前已经落地的文件: + +- `project-state.json` +- `mailbox.json` +- `shared-memory.json` +- `shared-memory.md` +- `task-activity.json` + +这样项目的协作状态会跟着项目走,不会继续把所有项目状态堆在全局一个 JSON 里。 + +## 6. 当前页面结构 + +前端主页面在: + +- `D:\AI Code\Agents Team\web\src\pages\HomePage.tsx` +- `D:\AI Code\Agents Team\web\src\pages\ProjectPage.tsx` + +### 6.1 项目页 + +项目页当前已经是: + +- 左侧像素办公室 / 成员状态区 +- 中间大面积 lead / 成员对话区 +- 右侧收纳区 + +收纳区当前已经包含: + +- 团队草案 +- 团队变更确认清单 +- 任务推进 +- Mailbox +- 共享记忆 +- 项目摸底 +- 运行态 / 接管口 +- 人工接管 +- Git 状态 + +### 6.2 Mailbox 展示 + +Mailbox 现在不是简单平铺消息。 + +当前已经有: + +- 服务端摘要分组 +- lead 汇总视角 +- 按任务 / 成员往来折叠 +- 参与人信息 +- 建议下一步 +- 高亮重点 + +### 6.3 运行态展示 + +运行态面板当前已经支持: + +- 当前模型切换 +- 切回继承默认模型 +- 同步恢复入口 +- 重建角色会话 +- 打开原生接管口 +- 复制恢复命令 +- 复制接管命令 + +## 7. 当前 API + +后端入口: + +- `D:\AI Code\Agents Team\server\src\index.ts` + +当前主要 API 已包括: + +- 项目列表 / 项目详情 +- 创建项目 +- 保存团队草案 +- 确认团队 +- 确认团队变更 +- 发起 / 确认项目摸底 +- lead / 成员消息发送 +- 创建任务 +- 送审 +- 复审 +- reviewer 审查 +- mailbox 发送 / 状态更新 +- 写入共享记忆 +- 会话模型切换 +- 会话恢复 +- 会话重建 +- 打开原生接管口 +- 人工接管记录 + +## 8. 现在还没做完的部分 + +还有这些尾项没有完全收口: + +- 固定角色虽然已经支持按 thread 恢复,但还没做到“所有长期身份都只靠原生会话驱动” +- 页内还没有嵌入原生终端 +- mailbox 摘要卡还能继续增强 +- `docs/` 里其它历史文档还没完全同步 + +## 9. 接下来最合理的方向 + +按当前进度,后面最值得继续推的是: + +1. 继续收口角色长期身份,让更多长期动作都复用同一原生会话 +2. 继续加强 mailbox 的长对话摘要和冲突压缩 +3. 评估是否要做页内原生终端 / 更顺手的接管体验 +4. 同步剩余设计 / 交接文档 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..232b0d6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3580 @@ +{ + "name": "agents-team-workbench", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agents-team-workbench", + "version": "0.1.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^6.30.1" + }, + "devDependencies": { + "@types/cors": "^2.8.18", + "@types/express": "^5.0.1", + "@types/node": "^24.0.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.1.2", + "tsx": "^4.19.3", + "typescript": "^5.8.2", + "vite": "^5.4.14" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.7", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad2ffb2 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "agents-team-workbench", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "concurrently \"npm:dev:server\" \"npm:dev:web\"", + "dev:server": "tsx watch server/src/index.ts", + "dev:web": "vite --config web/vite.config.ts", + "build": "npm run build:web && npm run build:server", + "build:web": "vite build --config web/vite.config.ts", + "build:server": "tsc -p server/tsconfig.json", + "start": "node dist/server/index.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^6.30.1" + }, + "devDependencies": { + "@types/cors": "^2.8.18", + "@types/express": "^5.0.1", + "@types/node": "^24.0.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.1.2", + "tsx": "^4.19.3", + "typescript": "^5.8.2", + "vite": "^5.4.14" + } +} diff --git a/server/src/codexConfig.ts b/server/src/codexConfig.ts new file mode 100644 index 0000000..1690585 --- /dev/null +++ b/server/src/codexConfig.ts @@ -0,0 +1,337 @@ +import os from "node:os"; +import path from "node:path"; +import { existsSync, readFileSync } from "node:fs"; +import { spawn } from "node:child_process"; +import type { CodexCliInfo, CodexModelOption, RuntimeSession } from "./types.js"; +import { buildCodexExecCommand, buildCodexTakeoverCommand, resolveCodexInvocation } from "./runtime.js"; + +const fallbackModels = ["gpt-5.4", "gpt-5.2", "gpt-5.2-codex"]; +const CODEX_INFO_CACHE_TTL_MS = 5 * 60 * 1000; +let codexCliInfoCache: { value: CodexCliInfo; fetchedAt: number } | null = null; + +const stripQuotes = (value: string): string => value.replace(/^['"]|['"]$/g, ""); +const quoteForDisplay = (value: string): string => `"${value.replace(/"/g, '\\"')}"`; + +const createConfigModelOption = (model: string, isDefault = false): CodexModelOption => ({ + model, + displayName: model, + description: "从本机 Codex 配置读取到的模型。", + defaultReasoningEffort: "medium", + supportedReasoningEfforts: [], + supportsPersonality: true, + isDefault, + hidden: false, + source: "config" +}); + +const mergeModelOptions = ( + preferred: CodexModelOption[], + fallback: CodexModelOption[], + defaultModel: string +): CodexModelOption[] => { + const merged = new Map(); + + for (const item of [...fallback, ...preferred]) { + merged.set(item.model, { + ...item, + isDefault: item.model === defaultModel + }); + } + + if (!merged.has(defaultModel)) { + merged.set(defaultModel, createConfigModelOption(defaultModel, true)); + } + + return [...merged.values()].sort((left, right) => { + if (left.model === defaultModel) { + return -1; + } + if (right.model === defaultModel) { + return 1; + } + if (left.isDefault && !right.isDefault) { + return -1; + } + if (!left.isDefault && right.isDefault) { + return 1; + } + return left.model.localeCompare(right.model); + }); +}; + +const readCodexCliInfoFromConfig = (): CodexCliInfo => { + const configPath = path.join(os.homedir(), ".codex", "config.toml"); + const info: CodexCliInfo = { + defaultModel: "gpt-5.4", + activeProfile: "", + provider: "", + contextWindow: null, + autoCompactTokenLimit: null, + suggestedModels: [...fallbackModels], + availableModels: fallbackModels.map((model, index) => createConfigModelOption(model, index === 0)) + }; + + if (!existsSync(configPath)) { + return info; + } + + const raw = readFileSync(configPath, "utf8"); + let currentSection = ""; + const discoveredModels = new Set(fallbackModels); + + for (const originalLine of raw.split(/\r?\n/)) { + const line = originalLine.replace(/\s+#.*$/, "").trim(); + if (!line) { + continue; + } + + const sectionMatch = line.match(/^\[(.+)\]$/); + if (sectionMatch) { + currentSection = sectionMatch[1]; + continue; + } + + const kvMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/); + if (!kvMatch) { + continue; + } + + const key = kvMatch[1]; + const value = stripQuotes(kvMatch[2].trim()); + + if (!currentSection) { + if (key === "model") { + info.defaultModel = value; + discoveredModels.add(value); + } + if (key === "profile") { + info.activeProfile = value; + } + if (key === "model_provider") { + info.provider = value; + } + if (key === "model_context_window") { + const parsed = Number(value); + info.contextWindow = Number.isFinite(parsed) ? parsed : info.contextWindow; + } + if (key === "model_auto_compact_token_limit") { + const parsed = Number(value); + info.autoCompactTokenLimit = Number.isFinite(parsed) ? parsed : info.autoCompactTokenLimit; + } + } + + if (key === "model") { + discoveredModels.add(value); + } + + if (currentSection === "notice.model_migrations") { + discoveredModels.add(stripQuotes(key)); + discoveredModels.add(value); + } + } + + info.availableModels = mergeModelOptions( + [], + Array.from(new Set([info.defaultModel, ...discoveredModels])).map((model) => createConfigModelOption(model, model === info.defaultModel)), + info.defaultModel + ); + info.suggestedModels = info.availableModels.map((item) => item.model); + return info; +}; + +type AppServerRpcResponse = + | { id: number; result?: unknown; error?: { code?: number; message?: string } } + | { method: string; params?: unknown }; + +const parseJsonLines = (buffer: string): AppServerRpcResponse[] => + buffer + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + try { + return JSON.parse(line) as AppServerRpcResponse; + } catch { + return null; + } + }) + .filter((item): item is AppServerRpcResponse => item !== null); + +const readModelsFromAppServer = async (): Promise => + new Promise((resolve, reject) => { + const invocation = resolveCodexInvocation(); + const child = spawn(invocation.command, [...invocation.prefixArgs, "app-server"], { + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true + }); + + let stdout = ""; + let stderr = ""; + let settled = false; + let modelListRequested = false; + const timeoutHandle = setTimeout(() => { + finish(new Error(stderr.trim() || "读取 Codex app-server 模型列表超时")); + }, 15000); + + const finish = (value: CodexModelOption[] | Error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeoutHandle); + child.kill(); + if (value instanceof Error) { + reject(value); + } else { + resolve(value); + } + }; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + const messages = parseJsonLines(stdout); + const initializeAck = messages.find((message) => "id" in message && message.id === 1); + if (initializeAck && !modelListRequested) { + modelListRequested = true; + child.stdin.write( + `${JSON.stringify({ + jsonrpc: "2.0", + id: 2, + method: "model/list", + params: { limit: 100, includeHidden: false } + })}\n` + ); + } + + const modelList = messages.find((message) => "id" in message && message.id === 2); + if (modelList && "result" in modelList) { + const data = Array.isArray((modelList.result as { data?: unknown[] })?.data) + ? ((modelList.result as { + data: Array<{ + model?: string; + displayName?: string; + description?: string; + defaultReasoningEffort?: string; + supportedReasoningEfforts?: Array<{ reasoningEffort?: string; description?: string }>; + supportsPersonality?: boolean; + isDefault?: boolean; + hidden?: boolean; + }>; + }).data || []) + : []; + const models: CodexModelOption[] = []; + for (const item of data) { + const model = typeof item.model === "string" ? item.model : ""; + if (!model) { + continue; + } + + models.push({ + model, + displayName: typeof item.displayName === "string" && item.displayName ? item.displayName : model, + description: typeof item.description === "string" ? item.description : "", + defaultReasoningEffort: + typeof item.defaultReasoningEffort === "string" && item.defaultReasoningEffort ? item.defaultReasoningEffort : "medium", + supportedReasoningEfforts: Array.isArray(item.supportedReasoningEfforts) + ? item.supportedReasoningEfforts + .map((effort) => ({ + reasoningEffort: + typeof effort?.reasoningEffort === "string" && effort.reasoningEffort ? effort.reasoningEffort : "", + description: typeof effort?.description === "string" ? effort.description : "" + })) + .filter((effort) => effort.reasoningEffort) + : [], + supportsPersonality: Boolean(item.supportsPersonality), + isDefault: Boolean(item.isDefault), + hidden: Boolean(item.hidden), + source: "app-server" + }); + } + + finish(models); + } + if (modelList && "error" in modelList && modelList.error) { + finish(new Error(modelList.error.message || "Codex app-server model/list 失败")); + } + }); + + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + child.on("error", (error) => finish(error)); + child.on("exit", (code) => { + if (!settled && code !== 0) { + finish(new Error(stderr.trim() || `Codex app-server 退出码 ${code}`)); + } + }); + + child.stdin.write( + `${JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + clientInfo: { + name: "agents-team-workbench", + title: "Agents Team Workbench", + version: "0.1.0" + }, + capabilities: { + experimentalApi: true + } + } + })}\n` + ); + }); + +export const getCodexCliInfo = (): CodexCliInfo => codexCliInfoCache?.value || readCodexCliInfoFromConfig(); + +export const refreshCodexCliInfo = async (force = false): Promise => { + if (!force && codexCliInfoCache && Date.now() - codexCliInfoCache.fetchedAt < CODEX_INFO_CACHE_TTL_MS) { + return codexCliInfoCache.value; + } + + const base = readCodexCliInfoFromConfig(); + + try { + const appServerModels = await readModelsFromAppServer(); + const availableModels = mergeModelOptions(appServerModels, base.availableModels, base.defaultModel); + const merged: CodexCliInfo = { + ...base, + availableModels, + suggestedModels: availableModels.map((item) => item.model) + }; + codexCliInfoCache = { value: merged, fetchedAt: Date.now() }; + return merged; + } catch { + codexCliInfoCache = { value: base, fetchedAt: Date.now() }; + return base; + } +}; + +export const applyCodexConfigToSession = ( + session: RuntimeSession, + codex: CodexCliInfo, + projectPath: string +): RuntimeSession => { + const effectiveModel = session.modelSource === "session-override" && session.model ? session.model : codex.defaultModel; + const resumeCommand = session.lastThreadId + ? `codex resume ${quoteForDisplay(session.lastThreadId)} -m ${quoteForDisplay(effectiveModel)} -C ${quoteForDisplay(projectPath)}` + : session.resumeCommand; + const identityState = + session.lastThreadId + ? "resume_ready" + : session.status === "blocked" || session.status === "offline" + ? "needs_rebuild" + : "fresh"; + + return { + ...session, + identityState, + model: effectiveModel, + lastCommand: buildCodexExecCommand(projectPath, `以 ${session.roleLabel} 身份接手当前项目上下文`, effectiveModel), + takeoverCommand: buildCodexTakeoverCommand(projectPath, effectiveModel), + resumeCommand + }; +}; diff --git a/server/src/defaults.ts b/server/src/defaults.ts new file mode 100644 index 0000000..cdbb447 --- /dev/null +++ b/server/src/defaults.ts @@ -0,0 +1,461 @@ +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import type { + ChatMessage, + CodexCliInfo, + ConversationThread, + GitSnapshot, + LeadProfile, + MailboxKind, + MailboxMessage, + ManualAction, + MemberCapabilityHint, + MemberLifecycle, + MemberRuntimeType, + NativeSessionRef, + Project, + ProjectCreateInput, + ProjectMember, + ProjectStorageInfo, + ProposalRole, + RuntimeSession, + SharedMemoryEntry, + TaskItem, + TaskActivityEntry, + TeamProposal, + TeamState, + ThreadStatus +} from "./types.js"; +import { buildCodexExecCommand, createRuntimeSession, updateRuntimeSession } from "./runtime.js"; + +const createId = (prefix: string): string => `${prefix}_${randomUUID()}`; +export const now = (): string => new Date().toISOString(); + +const trimPath = (value: string): string => value.replace(/[\\/]+$/, ""); + +export const createProjectStorageInfo = (projectPath: string): ProjectStorageInfo => { + const agentsDir = path.join(projectPath, ".agents-team"); + const workbenchDir = path.join(agentsDir, "workbench"); + + return { + agentsDir, + workbenchDir, + projectStateFile: path.join(workbenchDir, "project-state.json"), + mailboxFile: path.join(workbenchDir, "mailbox.json"), + sharedMemoryFile: path.join(workbenchDir, "shared-memory.json"), + sharedMemoryMarkdownFile: path.join(workbenchDir, "shared-memory.md"), + taskActivityFile: path.join(workbenchDir, "task-activity.json") + }; +}; + +export const inferProjectName = (projectPath: string): string => { + const baseName = path.basename(trimPath(projectPath)); + return baseName || "未命名项目"; +}; + +export const createPlaceholderGitStatus = (): GitSnapshot => ({ + mode: "placeholder", + branch: "未读取", + summary: "还没有读取 Git 状态。", + dirty: false, + trackedChanges: 0, + lastCheckedAt: null, + raw: "" +}); + +export const createSystemMessage = (content: string): ChatMessage => ({ + id: createId("msg"), + sender: "system", + content, + createdAt: now(), + senderName: "系统" +}); + +export const createMailboxMessage = (input: { + fromMemberId: string | null; + fromLabel: string; + toMemberId: string | null; + toLabel: string; + kind: MailboxKind; + relatedTaskId?: string | null; + summary: string; + body: string; +}): MailboxMessage => ({ + id: createId("mailbox"), + fromMemberId: input.fromMemberId, + fromLabel: input.fromLabel, + toMemberId: input.toMemberId, + toLabel: input.toLabel, + kind: input.kind, + relatedTaskId: input.relatedTaskId || null, + summary: input.summary, + body: input.body, + status: "open", + createdAt: now() +}); + +export const createSharedMemoryEntry = (input: { + title: string; + body: string; + source: SharedMemoryEntry["source"]; +}): SharedMemoryEntry => ({ + id: createId("memory"), + title: input.title, + body: input.body, + source: input.source, + createdAt: now() +}); + +export const createTaskActivityEntry = (input: { + taskId?: string | null; + kind: TaskActivityEntry["kind"]; + actorId?: string | null; + actorLabel: string; + summary: string; + detail: string; + relatedMailboxId?: string | null; +}): TaskActivityEntry => ({ + id: createId("activity"), + taskId: input.taskId || null, + kind: input.kind, + actorId: input.actorId || null, + actorLabel: input.actorLabel, + summary: input.summary, + detail: input.detail, + relatedMailboxId: input.relatedMailboxId || null, + createdAt: now() +}); + +export const createLeadMessage = (content: string, leadName = "项目 lead"): ChatMessage => ({ + id: createId("msg"), + sender: "lead", + content, + createdAt: now(), + senderName: leadName +}); + +export const createMemberMessage = (content: string, senderName: string): ChatMessage => ({ + id: createId("msg"), + sender: "member", + content, + createdAt: now(), + senderName +}); + +export const inferRuntimeType = (text: string): MemberRuntimeType => + /review|审|复审/i.test(text) ? "reviewer" : "worker"; + +export const inferCapabilityHint = (text: string): MemberCapabilityHint => { + if (/review|审|复审/i.test(text)) { + return "review"; + } + + if (/front|ui|页面|交互|样式|react|vue/i.test(text)) { + return "frontend"; + } + + if (/back|api|服务|后端|node|express|server/i.test(text)) { + return "backend"; + } + + if (/qa|test|验收|测试/i.test(text)) { + return "qa"; + } + + return "custom"; +}; + +export const createProposalRole = (input: { + displayName: string; + title: string; + runtimeType: MemberRuntimeType; + capabilityHint?: MemberCapabilityHint; + responsibility: string; + reason: string; + source?: "existing" | "new"; +}): ProposalRole => ({ + id: createId("proposal_role"), + displayName: input.displayName, + title: input.title, + runtimeType: input.runtimeType, + capabilityHint: input.capabilityHint || inferCapabilityHint(`${input.displayName} ${input.title} ${input.responsibility}`), + responsibility: input.responsibility, + reason: input.reason, + source: input.source || "new" +}); + +export const ensureReviewerRole = (roles: ProposalRole[]): ProposalRole[] => { + if (roles.some((role) => role.runtimeType === "reviewer")) { + return roles; + } + + return [ + createProposalRole({ + displayName: "项目 reviewer", + title: "风险审查成员", + runtimeType: "reviewer", + capabilityHint: "review", + responsibility: "负责把关风险、提出打回意见、给出复审结论。", + reason: "reviewer 是 v1 固定核心岗位,用来保住主工作流的审查闭环。" + }), + ...roles + ]; +}; + +const lifecycleToSessionStatus = (lifecycle: MemberLifecycle): RuntimeSession["status"] => { + switch (lifecycle) { + case "working": + return "running"; + case "waiting_review": + return "waiting"; + case "blocked": + case "paused": + return "blocked"; + case "done": + return "done"; + default: + return "idle"; + } +}; + +const lifecycleToThreadStatus = (lifecycle: MemberLifecycle): ThreadStatus => { + switch (lifecycle) { + case "working": + return "active"; + case "waiting_review": + return "standby"; + case "blocked": + case "paused": + return "blocked"; + case "done": + return "done"; + default: + return "standby"; + } +}; + +export const createDefaultTeamProposal = (createdAt = now()): TeamProposal => ({ + summary: "先由项目 lead 理解目标,再产出项目专属团队草案,确认后再正式建队。", + naturalLanguageBrief: + "你只需要先跟项目 lead 说清楚目标、风险边界和节奏。lead 会先给你一版团队草案,再等你确认后创建成员。", + status: "draft", + changeMode: "initial", + reuseFirst: true, + reviewerRule: "reviewer 默认有较强否决权,发现明显风险时可以打回。", + leadOverrideRule: "lead 可以说明原因后申请 override,但必须留痕。", + userOverrideRule: "你可以最终拍板 override,系统会保留人工干预记录。", + roles: [], + archivedMembers: [], + updatedAt: createdAt +}); + +export const createDefaultTeamState = (createdAt = now()): TeamState => ({ + status: "inactive", + members: [], + archivedMembers: [], + confirmedAt: null, + updatedAt: createdAt, + lastChangeSummary: "项目刚创建,还没有正式团队。" +}); + +const createLeadProfile = (projectPath: string, leadName: string, defaultModel: string): LeadProfile => { + const session = updateRuntimeSession(createRuntimeSession(leadName, projectPath, defaultModel, ["Lead 会在这里保留最近输出。"]), { + appendOutput: ["项目刚创建,Lead 正在等待你说清楚目标。"] + }); + + return { + id: createId("lead"), + name: leadName, + version: 1, + status: "active", + handoverNote: "如需重建 lead,新 lead 上线前必须由上一任 lead 先做交接。", + session + }; +}; + +export const promoteProposalRoleToMember = (role: ProposalRole, projectPath: string, defaultModel: string): ProjectMember => { + const createdAt = now(); + const threadId = `thread_member_${createId("member_thread")}`; + + return { + id: createId("member"), + displayName: role.displayName, + title: role.title, + runtimeType: role.runtimeType, + capabilityHint: role.capabilityHint, + responsibility: role.responsibility, + lifecycle: "idle", + source: role.source, + createdAt, + lastUsedAt: null, + threadId, + session: updateRuntimeSession(createRuntimeSession(role.displayName, projectPath, defaultModel, [`${role.displayName} 的运行日志会出现在这里。`]), { + status: "waiting", + appendOutput: [`已根据团队草案创建 ${role.displayName}。等待 lead 派发任务。`] + }) + }; +}; + +export const createTaskItem = ( + title: string, + summary: string, + ownerMemberId: string | null, + ownerName: string, + createdBy: string +): TaskItem => ({ + id: createId("task"), + title, + summary, + status: "in_progress", + ownerMemberId, + ownerName, + createdBy, + reviewState: "not_requested", + currentReviewId: null, + updatedAt: now() +}); + +export const createManualAction = ( + action: string, + note: string, + targetMemberId: string | null, + targetMemberName: string | null +): ManualAction => ({ + id: createId("manual"), + action, + operator: "用户人工接管", + targetMemberId, + targetMemberName, + note, + createdAt: now() +}); + +const buildLeadThread = (lead: LeadProfile, existing?: ConversationThread, seedMessages?: ChatMessage[]): ConversationThread => ({ + id: "thread_lead", + kind: "lead", + runtimeType: "lead", + label: lead.name, + subtitle: "主对话窗口。你默认只需要和项目 lead 说目标、边界和拍板。", + status: lead.status === "handover_pending" ? "blocked" : "active", + actorId: lead.id, + messages: existing?.messages.length ? existing.messages : seedMessages || [createLeadMessage("Lead 已就位,你可以直接开始说项目目标。", lead.name)] +}); + +const buildMemberThread = (member: ProjectMember, existing?: ConversationThread): ConversationThread => ({ + id: member.threadId, + kind: "member", + runtimeType: member.runtimeType, + label: member.displayName, + subtitle: member.title, + status: lifecycleToThreadStatus(member.lifecycle), + actorId: member.id, + messages: + existing?.messages.length + ? existing.messages + : [ + createSystemMessage( + member.runtimeType === "reviewer" + ? `${member.displayName} 已挂到 reviewer 线程,后面会在这里给出打回与复审结论。` + : `${member.displayName} 已创建完成,后面会在这里显示执行回执和最近输出。` + ) + ] +}); + +export const syncThreadsWithTeam = ( + existingThreads: ConversationThread[], + lead: LeadProfile, + members: ProjectMember[], + seedLeadMessages?: ChatMessage[] +): ConversationThread[] => { + const leadThread = buildLeadThread( + lead, + existingThreads.find((thread) => thread.id === "thread_lead"), + seedLeadMessages + ); + + const memberThreads = members.map((member) => + buildMemberThread( + { + ...member, + session: updateRuntimeSession(member.session, { status: lifecycleToSessionStatus(member.lifecycle) }) + }, + existingThreads.find((thread) => thread.id === member.threadId) + ) + ); + + return [leadThread, ...memberThreads]; +}; + +export const buildNativeSessions = (lead: LeadProfile, members: ProjectMember[]): NativeSessionRef[] => [ + { + actorId: lead.id, + actorLabel: lead.name, + runtimeType: "lead", + session: lead.session + }, + ...members.map((member) => ({ + actorId: member.id, + actorLabel: member.displayName, + runtimeType: member.runtimeType, + session: member.session + })) +]; + +export const createDefaultProject = (input: ProjectCreateInput, codex: CodexCliInfo): Project => { + const createdAt = now(); + const projectName = input.name?.trim() || inferProjectName(input.path); + const projectPath = input.path.trim(); + const leadName = input.leadName?.trim() || `${projectName} 项目 lead`; + const lead = createLeadProfile(projectPath, leadName, codex.defaultModel); + const storage = createProjectStorageInfo(projectPath); + const introMessages = [ + createSystemMessage("项目已接入。当前只锁定这个项目目录,不会自动摸底,也不会自动 commit / push。"), + createLeadMessage("我是这个项目的专属 lead。先和我聊目标,我会再给你项目专属团队草案。", leadName) + ]; + + return { + id: createId("project"), + name: projectName, + path: projectPath, + description: input.description?.trim() || "本地优先项目级 Lead 工作台", + createdAt, + updatedAt: createdAt, + stage: "lead_only", + codex, + lead, + teamProposal: createDefaultTeamProposal(createdAt), + team: createDefaultTeamState(createdAt), + nativeSessions: buildNativeSessions(lead, []), + discovery: { + status: "idle", + requiresUserConfirmation: true, + requestNote: "", + requestedAt: null, + confirmedAt: null, + lastBrief: "", + facts: [], + stack: [], + topLevelEntries: [] + }, + teamChange: { + pendingConfirmation: false, + summary: "当前没有待确认的团队变更。", + entries: [], + updatedAt: null + }, + messages: introMessages, + threads: syncThreadsWithTeam([], lead, [], introMessages), + tasks: [], + reviews: [], + mailbox: [], + mailboxSummaries: [], + sharedProjectMemory: [], + taskActivity: [], + git: createPlaceholderGitStatus(), + manualActions: [], + storage + }; +}; + +export const createLeadSummaryCommand = (projectPath: string, goal: string): string => + buildCodexExecCommand(projectPath, `以项目 lead 的身份整理目标并决定下一步:${goal}`); diff --git a/server/src/discovery.ts b/server/src/discovery.ts new file mode 100644 index 0000000..b62ae65 --- /dev/null +++ b/server/src/discovery.ts @@ -0,0 +1,202 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import path from "node:path"; +import type { DiscoveryResult, MemberCapabilityHint, MemberRuntimeType, ProposalRole, SuggestedTask } from "./types.js"; + +const createSuggestedRole = ( + displayName: string, + title: string, + runtimeType: MemberRuntimeType, + responsibility: string, + reason: string, + capabilityHint: MemberCapabilityHint +): ProposalRole => ({ + id: `proposal_role_${randomUUID()}`, + displayName, + title, + runtimeType, + capabilityHint, + responsibility, + reason, + source: "new" +}); + +const createSuggestedTask = (title: string, summary: string): SuggestedTask => ({ title, summary }); + +const safeReadJson = async (filePath: string): Promise => { + try { + const raw = await readFile(filePath, "utf8"); + return JSON.parse(raw) as T; + } catch { + return null; + } +}; + +const pushUnique = (target: string[], value: string | null | undefined) => { + if (!value || target.includes(value)) { + return; + } + + target.push(value); +}; + +const pushRole = (target: ProposalRole[], role: ProposalRole) => { + if (target.some((item) => item.title === role.title)) { + return; + } + + target.push(role); +}; + +export const analyzeProject = async (projectPath: string, leadName: string): Promise => { + const topLevelEntries = (await readdir(projectPath, { withFileTypes: true })) + .map((entry) => entry.name) + .sort((left, right) => left.localeCompare(right, "zh-CN")); + + const packageJsonPath = path.join(projectPath, "package.json"); + const packageJson = await safeReadJson<{ + name?: string; + scripts?: Record; + dependencies?: Record; + devDependencies?: Record; + }>(packageJsonPath); + + const dependencies = { + ...(packageJson?.dependencies || {}), + ...(packageJson?.devDependencies || {}) + }; + + const stack: string[] = []; + const facts: string[] = []; + + if (packageJson) { + pushUnique(stack, "Node.js 项目"); + facts.push(`检测到 package.json,项目名:${packageJson.name || "未写项目名"}。`); + } + + const hasTypeScript = topLevelEntries.includes("tsconfig.json") || Object.keys(dependencies).some((name) => name.includes("typescript")); + const hasReact = "react" in dependencies; + const hasVite = Object.keys(dependencies).some((name) => name.includes("vite")); + const hasExpress = "express" in dependencies; + const hasNext = "next" in dependencies; + const hasVue = "vue" in dependencies; + const hasTailwind = Object.keys(dependencies).some((name) => name.includes("tailwind")); + const hasServerDir = topLevelEntries.includes("server"); + const hasWebDir = topLevelEntries.includes("web") || topLevelEntries.includes("src"); + + if (hasTypeScript) pushUnique(stack, "TypeScript"); + if (hasReact) pushUnique(stack, "React"); + if (hasVite) pushUnique(stack, "Vite"); + if (hasExpress) pushUnique(stack, "Express"); + if (hasNext) pushUnique(stack, "Next.js"); + if (hasVue) pushUnique(stack, "Vue"); + if (hasTailwind) pushUnique(stack, "Tailwind CSS"); + + if (topLevelEntries.includes(".git")) { + facts.push("当前目录本身就是一个 Git 仓库。"); + } + + if (hasServerDir && hasWebDir) { + facts.push("目录里同时有前端和后端结构,适合按 lead 工作台 + 本地后台服务推进。"); + } else if (hasReact || hasVue || hasNext) { + facts.push("目录里有明显前端框架特征,界面和交互很可能是主战场。"); + } + + if (packageJson?.scripts) { + const scriptNames = Object.keys(packageJson.scripts); + facts.push(`可见脚本:${scriptNames.join("、") || "未配置脚本"}。`); + } + + const keyDirs: string[] = []; + for (const candidate of ["server", "web", "src", "app", "pages", "components"]) { + try { + const info = await stat(path.join(projectPath, candidate)); + if (info.isDirectory()) { + keyDirs.push(candidate); + } + } catch { + // ignore + } + } + + if (keyDirs.length > 0) { + facts.push(`关键目录:${keyDirs.join("、")}。`); + } + + const suggestedRoles: ProposalRole[] = []; + pushRole( + suggestedRoles, + createSuggestedRole( + "项目 reviewer", + "风险审查成员", + "reviewer", + "负责拦风险、给出打回意见、做最终复审。", + "reviewer 是 v1 固定核心岗位,先把主工作流的审查闭环稳住。", + "review" + ) + ); + + if (hasReact || hasVue || hasNext || hasWebDir) { + pushRole( + suggestedRoles, + createSuggestedRole( + "界面推进成员", + "界面推进成员", + "worker", + "负责主工作台、页面结构、交互和视觉细节。", + "当前目录里有明显前端特征,建议单独安排一个偏界面的执行成员。", + "frontend" + ) + ); + } + + if (hasExpress || hasServerDir) { + pushRole( + suggestedRoles, + createSuggestedRole( + "流程推进成员", + "流程推进成员", + "worker", + "负责 API、数据流、工作流状态和本地服务层。", + "当前目录里有后端或服务层结构,建议安排一个偏流程实现的执行成员。", + "backend" + ) + ); + } + + if (suggestedRoles.filter((role) => role.runtimeType === "worker").length === 0) { + pushRole( + suggestedRoles, + createSuggestedRole( + "主实现成员", + "主实现成员", + "worker", + "负责把 lead 当前确定的第一批任务真正落下来。", + "当前项目还没识别出明确前后端分层,先用一个通用执行成员起步最稳。", + "custom" + ) + ); + } + + const suggestedTasks: SuggestedTask[] = [ + createSuggestedTask("先确认第一批主链路", `${leadName} 先锁定一条最能体现产品方向的真实工作流。`), + createSuggestedTask("根据真实目录调整团队草案", "优先复用项目内历史成员,没有合适的再补新成员。"), + createSuggestedTask("给 reviewer 留出清晰的打回口", "不要把审查做成一句装饰性文案,要让返工链路能看见。") + ]; + + const briefParts = [ + "我已经看过当前项目目录了。", + stack.length > 0 ? `这更像一个 ${stack.join(" + ")} 的本地项目。` : "暂时还没有识别出特别明确的技术栈标签。", + keyDirs.length > 0 ? `关键目录主要有 ${keyDirs.join("、")}。` : "目录结构目前还比较轻。", + "按现在的情况,最适合先走“项目级 lead + reviewer 固定 + 自定义执行成员”的路线。" + ]; + + return { + brief: briefParts.join(""), + facts, + stack, + topLevelEntries, + suggestedRoles, + suggestedTasks + }; +}; diff --git a/server/src/git.ts b/server/src/git.ts new file mode 100644 index 0000000..7ac49c6 --- /dev/null +++ b/server/src/git.ts @@ -0,0 +1,57 @@ +import { access } from "node:fs/promises"; +import { constants } from "node:fs"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import type { GitSnapshot } from "./types.js"; +import { createPlaceholderGitStatus } from "./defaults.js"; + +const execFileAsync = promisify(execFile); + +const parseGitStatus = (output: string): GitSnapshot => { + const lines = output.split(/\r?\n/).filter(Boolean); + const header = lines[0]?.replace(/^##\s*/, "") || "未命名分支"; + const changes = lines.slice(1); + const dirty = changes.length > 0; + + return { + mode: "live", + branch: header, + summary: dirty ? `检测到 ${changes.length} 项工作区变更。` : "工作区干净,可以继续安排任务。", + dirty, + trackedChanges: changes.length, + lastCheckedAt: new Date().toISOString(), + raw: output.trim() + }; +}; + +export const readGitStatus = async (projectPath: string): Promise => { + if (!projectPath.trim()) { + return { + ...createPlaceholderGitStatus(), + summary: "还没有填写项目路径,暂时无法读取 Git 状态。" + }; + } + + try { + await access(projectPath, constants.R_OK); + } catch { + return { + ...createPlaceholderGitStatus(), + summary: "项目路径暂时不可读,请先确认本地路径是否正确。" + }; + } + + try { + const { stdout } = await execFileAsync("git", ["-C", projectPath, "status", "--short", "--branch"], { + timeout: 3000, + windowsHide: true + }); + + return parseGitStatus(stdout); + } catch { + return { + ...createPlaceholderGitStatus(), + summary: "当前目录还没有可读取的 Git 状态,或 Git 暂时不可用。" + }; + } +}; diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..1551fcb --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,484 @@ +import path from "node:path"; +import { existsSync } from "node:fs"; +import express from "express"; +import cors from "cors"; +import { + addSharedMemoryEntry, + confirmDiscovery, + confirmTeamChange, + confirmTeamProposal, + createProject, + createTask, + ensureStore, + getProject, + getProjectOfficeState, + getProjectMessages, + listProjects, + requestDiscovery, + rebuildSession, + restoreSession, + resubmitTask, + reviewTask, + saveManualAction, + sendMailboxMessage, + saveTeamProposal, + saveUserMessage, + streamUserMessage, + submitTask, + updateMailboxStatus, + updateSessionModel, + triggerSessionTakeover +} from "./store.js"; +import { getPixelAgentsAssetFilePath, getPixelAgentsAssetsManifest } from "./pixelAgents.js"; +import type { + MailboxMessageCreateInput, + MailboxStatusUpdateInput, + ReviewDecisionInput, + SessionModelUpdateInput, + TaskCreateInput, + TeamProposalUpdateInput +} from "./types.js"; + +const app = express(); +const port = Number(process.env.PORT || 8787); +const webDist = path.resolve(process.cwd(), "dist", "web"); + +app.use(cors()); +app.use(express.json({ limit: "1mb" })); + +app.get("/api/health", (_request, response) => { + response.json({ + ok: true, + service: "agents-team-workbench-local-api", + time: new Date().toISOString() + }); +}); + +app.get("/api/projects", async (_request, response) => { + response.json(await listProjects()); +}); + +app.post("/api/projects", async (request, response) => { + const projectPath = String(request.body?.path || "").trim(); + + if (!projectPath) { + response.status(400).json({ message: "请先填写本地项目路径。" }); + return; + } + + const project = await createProject({ + name: String(request.body?.name || "").trim(), + path: projectPath, + leadName: String(request.body?.leadName || "").trim(), + description: String(request.body?.description || "").trim() + }); + + response.status(201).json(project); +}); + +app.get("/api/projects/:projectId", async (request, response) => { + const project = await getProject(request.params.projectId); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.get("/api/projects/:projectId/messages", async (request, response) => { + const messages = await getProjectMessages(request.params.projectId); + + if (!messages) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(messages); +}); + +app.get("/api/projects/:projectId/office", async (request, response) => { + const office = await getProjectOfficeState(request.params.projectId, String(request.query.threadId || "").trim() || null); + + if (!office) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(office); +}); + +const handleSaveProposal = async (request: express.Request, response: express.Response) => { + const payload = request.body as TeamProposalUpdateInput; + const projectId = String(request.params.projectId); + + if (!payload?.summary || !payload?.naturalLanguageBrief) { + response.status(400).json({ message: "团队草案至少需要一句总结和一段自然语言说明。" }); + return; + } + + const project = await saveTeamProposal(projectId, payload); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}; + +app.put("/api/projects/:projectId/team-proposal", handleSaveProposal); +app.put("/api/projects/:projectId/team-draft", handleSaveProposal); + +app.post("/api/projects/:projectId/team-proposal/confirm", async (request, response) => { + const project = await confirmTeamProposal(request.params.projectId); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/team-change/confirm", async (request, response) => { + const project = await confirmTeamChange(request.params.projectId); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/discovery/request", async (request, response) => { + const project = await requestDiscovery(request.params.projectId, String(request.body?.note || "").trim()); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/discovery/confirm", async (request, response) => { + const existingProject = await getProject(request.params.projectId); + + if (!existingProject) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + if (existingProject.discovery.status !== "pending_confirmation") { + response.status(409).json({ message: "当前没有待确认的项目摸底请求。" }); + return; + } + + response.json(await confirmDiscovery(request.params.projectId)); +}); + +app.post("/api/projects/:projectId/messages", async (request, response) => { + const content = String(request.body?.content || "").trim(); + const threadId = String(request.body?.threadId || "").trim() || null; + + if (!content) { + response.status(400).json({ message: "消息内容不能为空。" }); + return; + } + + const project = await saveUserMessage(request.params.projectId, content, threadId); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/messages/stream", async (request, response) => { + const content = String(request.body?.content || "").trim(); + const threadId = String(request.body?.threadId || "").trim() || null; + + if (!content) { + response.status(400).json({ message: "消息内容不能为空。" }); + return; + } + + response.status(200); + response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8"); + response.setHeader("Cache-Control", "no-cache, no-transform"); + response.setHeader("Connection", "keep-alive"); + response.flushHeaders(); + + const writeEvent = (event: unknown) => { + if (!response.writableEnded) { + response.write(`${JSON.stringify(event)}\n`); + } + }; + + try { + const project = await streamUserMessage(request.params.projectId, content, threadId, writeEvent); + if (!project && !response.writableEnded) { + writeEvent({ + type: "error", + threadId, + actorId: null, + actorLabel: "系统", + message: "没有找到这个项目。" + }); + } + } catch (error) { + writeEvent({ + type: "error", + threadId, + actorId: null, + actorLabel: "系统", + message: error instanceof Error ? error.message : "流式消息处理失败。" + }); + } finally { + response.end(); + } +}); + +app.post("/api/projects/:projectId/tasks", async (request, response) => { + const payload = request.body as TaskCreateInput; + + if (!payload?.title?.trim()) { + response.status(400).json({ message: "请先给这条任务一个标题。" }); + return; + } + + const project = await createTask(request.params.projectId, { + title: payload.title.trim(), + summary: String(payload.summary || "").trim() || "还没有任务说明。", + ownerMemberId: payload.ownerMemberId || null + }); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/tasks/:taskId/submit", async (request, response) => { + const project = await submitTask(request.params.projectId, request.params.taskId, String(request.body?.note || "").trim() || "成员已提交当前结果,请 reviewer 审查。"); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或任务。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/tasks/:taskId/resubmit", async (request, response) => { + const project = await resubmitTask(request.params.projectId, request.params.taskId, String(request.body?.note || "").trim() || "已按 reviewer 意见修复,重新提交审查。"); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或任务。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/tasks/:taskId/review", async (request, response) => { + const payload = request.body as ReviewDecisionInput; + + if (payload?.decision !== "approve" && payload?.decision !== "changes_requested") { + response.status(400).json({ message: "review 结果只能是 approve 或 changes_requested。" }); + return; + } + + const project = await reviewTask(request.params.projectId, request.params.taskId, { + decision: payload.decision, + note: String(payload.note || "").trim() || (payload.decision === "approve" ? "这条任务通过了。" : "这条任务需要继续修复。") + }); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或任务。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/manual-actions", async (request, response) => { + const action = String(request.body?.action || "").trim(); + const note = String(request.body?.note || "").trim(); + const targetMemberId = String(request.body?.targetMemberId || "").trim() || null; + + if (!action) { + response.status(400).json({ message: "请先选择人工接管动作。" }); + return; + } + + const project = await saveManualAction(request.params.projectId, action, note, targetMemberId); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/mailbox", async (request, response) => { + const payload = request.body as MailboxMessageCreateInput; + + if (!payload?.fromMemberId || !payload?.toMemberId || !payload?.summary?.trim()) { + response.status(400).json({ message: "mailbox 消息至少需要发送人、接收人和一句摘要。" }); + return; + } + + const project = await sendMailboxMessage(String(request.params.projectId), { + fromMemberId: String(payload.fromMemberId), + toMemberId: String(payload.toMemberId), + kind: payload.kind || "question", + relatedTaskId: payload.relatedTaskId || null, + summary: String(payload.summary).trim(), + body: String(payload.body || "").trim() + }); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或成员。" }); + return; + } + + response.json(project); +}); + +app.put("/api/projects/:projectId/mailbox/:mailboxId/status", async (request, response) => { + const payload = request.body as MailboxStatusUpdateInput; + + if (payload?.status !== "open" && payload?.status !== "acknowledged" && payload?.status !== "resolved") { + response.status(400).json({ message: "mailbox 状态只能是 open、acknowledged 或 resolved。" }); + return; + } + + const project = await updateMailboxStatus(String(request.params.projectId), String(request.params.mailboxId), payload.status); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或消息。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/shared-memory", async (request, response) => { + const title = String(request.body?.title || "").trim(); + const body = String(request.body?.body || "").trim(); + + if (!title || !body) { + response.status(400).json({ message: "共享记忆至少需要标题和正文。" }); + return; + } + + const project = await addSharedMemoryEntry(String(request.params.projectId), title, body); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目。" }); + return; + } + + response.json(project); +}); + +app.put("/api/projects/:projectId/sessions/:sessionId/model", async (request, response) => { + const payload = request.body as SessionModelUpdateInput; + + if (payload?.mode !== "inherit" && payload?.mode !== "override") { + response.status(400).json({ message: "模型模式只能是 inherit 或 override。" }); + return; + } + + if (payload.mode === "override" && !String(payload.model || "").trim()) { + response.status(400).json({ message: "自定义模型名不能为空。" }); + return; + } + + const project = await updateSessionModel(String(request.params.projectId), String(request.params.sessionId), { + mode: payload.mode, + model: String(payload.model || "").trim() + }); + + if (!project) { + response.status(404).json({ message: "没有找到这个项目或会话。" }); + return; + } + + response.json(project); +}); + +app.post("/api/projects/:projectId/sessions/:sessionId/takeover", async (request, response) => { + const result = await triggerSessionTakeover(request.params.projectId, request.params.sessionId); + + if (!result.project) { + response.status(404).json({ message: result.message }); + return; + } + + response.json({ ok: true, message: result.message }); +}); + +app.post("/api/projects/:projectId/sessions/:sessionId/restore", async (request, response) => { + const result = await restoreSession(request.params.projectId, request.params.sessionId); + + if (!result.project) { + response.status(404).json({ message: result.message }); + return; + } + + response.json({ ok: true, message: result.message, project: result.project }); +}); + +app.post("/api/projects/:projectId/sessions/:sessionId/rebuild", async (request, response) => { + const result = await rebuildSession(request.params.projectId, request.params.sessionId); + + if (!result.project) { + response.status(404).json({ message: result.message }); + return; + } + + response.json({ ok: true, message: result.message, project: result.project }); +}); + +app.get("/api/pixel-agents-assets/*", (request, response) => { + const requestedAssetPath = decodeURIComponent(request.path.replace(/^\/api\/pixel-agents-assets\/+/, "")).trim(); + const assetFilePath = getPixelAgentsAssetFilePath(requestedAssetPath); + + if (!assetFilePath) { + response.status(404).type("text/plain").send("没有找到对应的 Pixel Agents 资源。"); + return; + } + + response.sendFile(assetFilePath); +}); + +app.get("/api/pixel-agents-manifest", (_request, response) => { + response.json(getPixelAgentsAssetsManifest()); +}); + +if (existsSync(path.join(webDist, "index.html"))) { + app.use(express.static(webDist)); + app.get("*", (_request, response) => { + response.sendFile(path.join(webDist, "index.html")); + }); +} else { + app.get("/", (_request, response) => { + response.type("text/plain").send("前端还没有 build。开发模式请运行 npm run dev。"); + }); +} + +await ensureStore(); + +app.listen(port, () => { + console.log(`Agents Team 本地服务已启动: http://localhost:${port}`); +}); diff --git a/server/src/pixelAgents.ts b/server/src/pixelAgents.ts new file mode 100644 index 0000000..8134756 --- /dev/null +++ b/server/src/pixelAgents.ts @@ -0,0 +1,215 @@ +import os from "node:os"; +import path from "node:path"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import type { OfficeAssetsManifest } from "./types.js"; + +const extensionBaseDirs = [path.join(os.homedir(), ".vscode", "extensions"), path.join(os.homedir(), ".vscode-insiders", "extensions")]; +const extensionPrefix = "pablodelucca.pixel-agents-"; +const assetBaseUrl = "/api/pixel-agents-assets"; + +interface PixelAgentsInstallation { + extensionPath: string; + version: string | null; + assetsDir: string; +} + +interface PixelFurnitureCatalogAsset { + id: string; + label?: string; + isDesk?: boolean; + footprintW?: number; + footprintH?: number; +} + +interface PixelFurnitureCatalog { + assets?: PixelFurnitureCatalogAsset[]; +} + +interface PixelLayoutFurnitureItem { + uid?: string; + type?: string; + col?: number; + row?: number; +} + +interface PixelDefaultLayout { + cols?: number; + rows?: number; + furniture?: PixelLayoutFurnitureItem[]; +} + +export interface PixelDeskSlotTemplate { + id: string; + label: string; + assetId: string; + col: number; + row: number; + width: number; + height: number; +} + +export interface PixelOfficeLayoutTemplate { + cols: number | null; + rows: number | null; + deskSlots: PixelDeskSlotTemplate[]; +} + +const compareExtensionNames = (left: string, right: string): number => + right.localeCompare(left, undefined, { numeric: true, sensitivity: "base" }); + +const listInstalledPixelAgentsExtensions = (): PixelAgentsInstallation[] => + extensionBaseDirs.flatMap((baseDir) => { + if (!existsSync(baseDir)) { + return []; + } + + return readdirSync(baseDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && entry.name.startsWith(extensionPrefix)) + .sort((left, right) => compareExtensionNames(left.name, right.name)) + .map((entry) => { + const extensionPath = path.join(baseDir, entry.name); + return { + extensionPath, + version: entry.name.slice(extensionPrefix.length) || null, + assetsDir: path.join(extensionPath, "dist", "webview", "assets") + }; + }) + .filter((entry) => existsSync(entry.assetsDir)); + }); + +const getLatestPixelAgentsInstallation = (): PixelAgentsInstallation | null => listInstalledPixelAgentsExtensions()[0] || null; + +const buildAssetUrl = (relativePath: string): string => `${assetBaseUrl}/${relativePath.replace(/\\/g, "/").replace(/^\/+/, "")}`; + +const readJsonFile = (filePath: string): T | null => { + if (!existsSync(filePath)) { + return null; + } + + try { + return JSON.parse(readFileSync(filePath, "utf8")) as T; + } catch { + return null; + } +}; + +const listCharacterUrls = (assetsDir: string): string[] => { + const charactersDir = path.join(assetsDir, "characters"); + if (!existsSync(charactersDir)) { + return []; + } + + return readdirSync(charactersDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".png")) + .sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" })) + .map((entry) => buildAssetUrl(path.posix.join("characters", entry.name))); +}; + +export const getPixelAgentsOfficeLayoutTemplate = (): PixelOfficeLayoutTemplate => { + const installation = getLatestPixelAgentsInstallation(); + if (!installation) { + return { + cols: null, + rows: null, + deskSlots: [] + }; + } + + const layout = readJsonFile(path.join(installation.assetsDir, "default-layout.json")); + const catalog = readJsonFile(path.join(installation.assetsDir, "furniture", "furniture-catalog.json")); + const catalogAssets = catalog?.assets || []; + const catalogById = new Map(catalogAssets.map((asset) => [asset.id, asset])); + const deskSlots = (layout?.furniture || []) + .filter( + (item): item is Required> & PixelLayoutFurnitureItem => + typeof item.type === "string" && typeof item.col === "number" && typeof item.row === "number" + ) + .map((item) => { + const asset = catalogById.get(item.type); + return { + item, + asset + }; + }) + .filter((entry) => Boolean(entry.asset?.isDesk)) + .sort((left, right) => (left.item.row - right.item.row) || (left.item.col - right.item.col)) + .map((entry, index) => ({ + id: entry.item.uid || `desk-slot-${index + 1}`, + label: entry.asset?.label || `Desk ${index + 1}`, + assetId: entry.item.type, + col: entry.item.col, + row: entry.item.row, + width: Math.max(1, Math.round(entry.asset?.footprintW || 1)), + height: Math.max(1, Math.round(entry.asset?.footprintH || 1)) + })); + + return { + cols: typeof layout?.cols === "number" ? layout.cols : null, + rows: typeof layout?.rows === "number" ? layout.rows : null, + deskSlots + }; +}; + +export const getPixelAgentsExtensionPath = (): string | null => getLatestPixelAgentsInstallation()?.extensionPath || null; + +export const getPixelAgentsAssetFilePath = (relativePath: string): string | null => { + const installation = getLatestPixelAgentsInstallation(); + if (!installation) { + return null; + } + + const normalizedPath = relativePath.replace(/\\/g, "/").replace(/^\/+/, ""); + if (!normalizedPath) { + return null; + } + + const resolvedPath = path.resolve(installation.assetsDir, normalizedPath); + const relativeToAssets = path.relative(installation.assetsDir, resolvedPath); + if (relativeToAssets.startsWith("..") || path.isAbsolute(relativeToAssets)) { + return null; + } + + return existsSync(resolvedPath) ? resolvedPath : null; +}; + +export const getPixelAgentsAssetsManifest = (): OfficeAssetsManifest => { + const installation = getLatestPixelAgentsInstallation(); + if (!installation) { + return { + available: false, + source: "none", + extensionPath: null, + extensionVersion: null, + assetBaseUrl: null, + defaultLayoutUrl: null, + floorsUrl: null, + wallsUrl: null, + officeTilesetUrl: null, + furnitureCatalogUrl: null, + furnitureBaseUrl: null, + characterUrls: [], + notes: ["本机还没有检测到已安装的 Pixel Agents VS Code 扩展。"] + }; + } + + const characterUrls = listCharacterUrls(installation.assetsDir); + + return { + available: true, + source: "vscode-extension", + extensionPath: installation.extensionPath, + extensionVersion: installation.version, + assetBaseUrl, + defaultLayoutUrl: buildAssetUrl("default-layout.json"), + floorsUrl: buildAssetUrl("floors.png"), + wallsUrl: buildAssetUrl("walls.png"), + officeTilesetUrl: buildAssetUrl("office_tileset_16x16.png"), + furnitureCatalogUrl: buildAssetUrl("furniture/furniture-catalog.json"), + furnitureBaseUrl: buildAssetUrl("furniture"), + characterUrls, + notes: [ + `已检测到 Pixel Agents ${installation.version || "unknown"} 扩展资源。`, + "当前已直接暴露默认布局、地板、墙体、办公室 tileset、家具目录和角色素材。" + ] + }; +}; diff --git a/server/src/runtime.ts b/server/src/runtime.ts new file mode 100644 index 0000000..827b352 --- /dev/null +++ b/server/src/runtime.ts @@ -0,0 +1,594 @@ +import { randomUUID } from "node:crypto"; +import { spawn } from "node:child_process"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { existsSync } from "node:fs"; +import { writeFile, unlink } from "node:fs/promises"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import os from "node:os"; +import type { RuntimeSession, SessionStatus } from "./types.js"; + +type JsonRecord = Record; + +export type CodexPromptStreamEvent = + | { + type: "thread_started"; + threadId: string; + } + | { + type: "assistant_delta"; + delta: string; + } + | { + type: "reasoning_delta"; + delta: string; + } + | { + type: "tool_event"; + toolCallId: string; + toolName: string; + summary: string; + state: "started" | "completed"; + } + | { + type: "command_output"; + toolCallId: string | null; + chunk: string; + }; + +const now = (): string => new Date().toISOString(); +const execFileAsync = promisify(execFile); +let codexInvocationCache: { command: string; prefixArgs: string[] } | null = null; + +const quoteForDisplay = (value: string): string => `"${value.replace(/"/g, '\\"')}"`; + +const psEscape = (value: string): string => value.replace(/'/g, "''"); +const isRecord = (value: unknown): value is JsonRecord => Boolean(value) && typeof value === "object" && !Array.isArray(value); +const knownNoisyWarnings = new Set([ + "unknown feature key in config: plan_tool", + "unknown feature key in config: rmcp_client", + "unknown feature key in config: streamable_shell", + "unknown feature key in config: view_image_tool", + "Failed to create shell snapshot for powershell" +]); + +const firstString = (...values: unknown[]): string => { + for (const value of values) { + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + + return ""; +}; + +const decodeBase64Chunk = (value: unknown): string => { + if (typeof value !== "string" || !value.trim()) { + return ""; + } + + try { + return Buffer.from(value, "base64").toString("utf8"); + } catch { + return ""; + } +}; + +const eventPayload = (raw: JsonRecord): JsonRecord => { + const payload = raw.params; + return isRecord(payload) ? payload : raw; +}; + +const eventName = (raw: JsonRecord): string => { + const name = firstString(raw.type, raw.method, raw.event, eventPayload(raw).type, eventPayload(raw).method, eventPayload(raw).event); + return name.replace(/\//g, ".").replace(/\s+/g, "").toLowerCase(); +}; + +const itemRecord = (raw: JsonRecord): JsonRecord => { + const payload = eventPayload(raw); + if (isRecord(raw.item)) { + return raw.item; + } + + if (isRecord(payload.item)) { + return payload.item; + } + + return payload; +}; + +const itemTypeName = (item: JsonRecord): string => + firstString(item.type, item.kind, item.name, item.toolName, item.tool_name, item.commandType, item.command_type).replace(/\//g, ".").toLowerCase(); + +const itemText = (item: JsonRecord, payload?: JsonRecord): string => + firstString( + item.text, + item.delta, + item.content, + item.summary, + item.title, + payload?.text, + payload?.delta, + payload?.content, + payload?.summary, + payload?.title + ); + +const itemId = (item: JsonRecord, payload?: JsonRecord): string => + firstString(item.id, item.callId, item.call_id, item.toolCallId, item.tool_call_id, payload?.id, payload?.callId, payload?.call_id); + +const itemToolName = (item: JsonRecord, payload?: JsonRecord): string => { + const explicit = firstString(item.name, item.toolName, item.tool_name, item.command, payload?.name, payload?.toolName, payload?.tool_name, payload?.command); + if (explicit) { + return explicit; + } + + const type = itemTypeName(item); + if (!type) { + return "工具调用"; + } + + if (type.includes("apply_patch")) { + return "apply_patch"; + } + + if (type.includes("commandexecution") || type.includes("local_shell_call")) { + return "命令执行"; + } + + if (type.includes("web_search")) { + return "网页搜索"; + } + + if (type.includes("function_call")) { + return "函数调用"; + } + + return explicit || type; +}; + +const classifyCodexStreamEvent = (raw: JsonRecord): CodexPromptStreamEvent[] => { + const name = eventName(raw); + const payload = eventPayload(raw); + const item = itemRecord(raw); + const normalizedItemType = itemTypeName(item); + const events: CodexPromptStreamEvent[] = []; + + if (name === "thread.started") { + const threadId = firstString(raw.thread_id, raw.threadId, payload.thread_id, payload.threadId); + if (threadId) { + events.push({ type: "thread_started", threadId }); + } + return events; + } + + if (name.includes("agentmessage") && name.includes("delta")) { + const delta = itemText(item, payload); + if (delta) { + events.push({ type: "assistant_delta", delta }); + } + return events; + } + + if (name.includes("reasoning") && (name.includes("delta") || name.includes("summarypartadded"))) { + const delta = itemText(item, payload); + if (delta) { + events.push({ type: "reasoning_delta", delta }); + } + return events; + } + + if (name.includes("command.exec.outputdelta") || name.includes("commandexecution.outputdelta")) { + const chunk = + firstString(payload.chunk, payload.text, payload.delta, payload.output, payload.stdout, payload.stderr) || + decodeBase64Chunk(payload.deltaBase64); + if (chunk) { + events.push({ + type: "command_output", + toolCallId: firstString(payload.toolCallId, payload.tool_call_id, payload.callId, payload.call_id) || null, + chunk + }); + } + return events; + } + + if (name === "item.started" || name === "item.completed") { + const state = name === "item.started" ? "started" : "completed"; + + if (normalizedItemType === "agent_message" || normalizedItemType === "agentmessage") { + const text = itemText(item, payload); + if (state === "completed" && text) { + events.push({ type: "assistant_delta", delta: text }); + } + return events; + } + + const isToolLike = + normalizedItemType.includes("tool") || + normalizedItemType.includes("function_call") || + normalizedItemType.includes("shell") || + normalizedItemType.includes("commandexecution") || + normalizedItemType.includes("web_search") || + normalizedItemType.includes("apply_patch"); + + if (isToolLike) { + const toolCallId = itemId(item, payload) || `${itemToolName(item, payload)}-${state}`; + const summary = + firstString(item.summary, item.title, payload.summary, payload.title, payload.command, item.command) || + `${state === "started" ? "开始" : "完成"} ${itemToolName(item, payload)}`; + events.push({ + type: "tool_event", + toolCallId, + toolName: itemToolName(item, payload), + summary, + state + }); + } + } + + return events; +}; + +export const buildCodexTakeoverCommand = (projectPath: string, model?: string): string => + model + ? `codex --cd ${quoteForDisplay(projectPath)} -m ${quoteForDisplay(model)}` + : `codex --cd ${quoteForDisplay(projectPath)}`; + +export const buildCodexExecCommand = (projectPath: string, prompt: string, model?: string): string => + model + ? `codex exec --cd ${quoteForDisplay(projectPath)} -m ${quoteForDisplay(model)} ${quoteForDisplay(prompt)}` + : `codex exec --cd ${quoteForDisplay(projectPath)} ${quoteForDisplay(prompt)}`; + +export const createRuntimeSession = ( + roleLabel: string, + projectPath: string, + model: string, + seedOutput: string[] = [] +): RuntimeSession => ({ + id: `session_${randomUUID()}`, + adapter: "codex-cli", + roleLabel, + status: "idle", + identityState: "fresh", + model, + modelSource: "codex-config", + estimatedContextTokens: 0, + contextUsagePercent: 0, + lastThreadId: null, + recentOutput: seedOutput.slice(-6), + lastUpdatedAt: now(), + lastCommand: buildCodexExecCommand(projectPath, `以 ${roleLabel} 身份接手当前项目上下文`, model), + takeoverCommand: buildCodexTakeoverCommand(projectPath, model), + resumeCommand: "codex resume --last", + lastError: "" +}); + +export const updateRuntimeSession = ( + session: RuntimeSession, + patch: Partial & { status?: SessionStatus; appendOutput?: string[] } +): RuntimeSession => { + const { appendOutput, ...rest } = patch; + const mergedOutput = patch.appendOutput ? [...session.recentOutput, ...patch.appendOutput].slice(-6) : session.recentOutput; + + return { + ...session, + ...rest, + recentOutput: mergedOutput, + lastUpdatedAt: now() + }; +}; + +export const launchCodexTakeover = async ( + roleLabel: string, + projectPath: string, + model?: string, + resumeCommand?: string +): Promise => { + const takeoverCommand = resumeCommand && resumeCommand !== "codex resume --last" + ? resumeCommand + : model + ? `codex --cd '${psEscape(projectPath)}' -m '${psEscape(model)}'` + : `codex --cd '${psEscape(projectPath)}'`; + const script = [ + `$Host.UI.RawUI.WindowTitle = 'Agents Team - ${psEscape(roleLabel)}'`, + `Set-Location -LiteralPath '${psEscape(projectPath)}'`, + `Write-Host '已打开 ${psEscape(roleLabel)} 的原生 Codex 接管窗口。' -ForegroundColor Cyan`, + "Write-Host '你现在可以继续用 Codex CLI 原生命令操作。' -ForegroundColor DarkGray", + takeoverCommand + ].join("; "); + + const child = spawn("powershell.exe", ["-NoExit", "-Command", script], { + detached: true, + stdio: "ignore" + }); + + child.unref(); +}; + +export const resolveCodexInvocation = (): { command: string; prefixArgs: string[] } => { + if (codexInvocationCache) { + return codexInvocationCache; + } + + if (process.platform === "win32") { + const lookup = spawnSync("where", ["codex.cmd"], { encoding: "utf8", windowsHide: true }); + const cmdPath = lookup.stdout?.split(/\r?\n/).map((line) => line.trim()).find(Boolean); + if (cmdPath) { + const binScript = path.join(path.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js"); + if (existsSync(binScript)) { + codexInvocationCache = { + command: process.execPath, + prefixArgs: [binScript] + }; + return codexInvocationCache; + } + } + } + + codexInvocationCache = { + command: "codex", + prefixArgs: [] + }; + return codexInvocationCache; +}; + +export const runCodexReadOnlyPrompt = async (input: { + projectPath: string; + model: string; + prompt: string; + resumeSessionId?: string; + timeoutMs?: number; +}): Promise<{ reply: string; threadId: string; logLines: string[]; errorLines: string[] }> => { + return runCodexReadOnlyPromptStreaming(input); +}; + +export const runCodexReadOnlyPromptStreaming = async (input: { + projectPath: string; + model: string; + prompt: string; + resumeSessionId?: string; + timeoutMs?: number; + onEvent?: (event: CodexPromptStreamEvent) => void; +}): Promise<{ reply: string; threadId: string; logLines: string[]; errorLines: string[] }> => { + const args = input.resumeSessionId + ? [ + "-s", + "read-only", + "-a", + "never", + "exec", + "resume", + "--json", + "--skip-git-repo-check", + "-m", + input.model, + input.resumeSessionId, + input.prompt + ] + : [ + "-s", + "read-only", + "-a", + "never", + "exec", + "--json", + "--skip-git-repo-check", + "-C", + input.projectPath, + "-m", + input.model, + input.prompt + ]; + + const invocation = resolveCodexInvocation(); + + return new Promise((resolve, reject) => { + const child = spawn(invocation.command, [...invocation.prefixArgs, ...args], { + cwd: input.projectPath, + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"] + }); + + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("Codex CLI 调用超时。")); + }, input.timeoutMs || 120000); + + let stdoutBuffer = ""; + let stderrBuffer = ""; + let threadId = ""; + let assistantReply = ""; + let sawAssistantDelta = false; + const assistantSnapshots: string[] = []; + const errorLines: string[] = []; + + const handleStdoutLine = (rawLine: string) => { + const line = rawLine.trim(); + if (!line) { + return; + } + + try { + const rawEvent = JSON.parse(line) as JsonRecord; + for (const streamEvent of classifyCodexStreamEvent(rawEvent)) { + if (streamEvent.type === "thread_started") { + threadId = streamEvent.threadId; + } else if (streamEvent.type === "assistant_delta") { + sawAssistantDelta = true; + assistantReply += streamEvent.delta; + } + + input.onEvent?.(streamEvent); + } + + const event = rawEvent; + if (event.type === "thread.started" && typeof event.thread_id === "string") { + threadId = event.thread_id; + } + if (event.type === "item.completed") { + const item = isRecord(event.item) ? event.item : undefined; + if (item?.type === "agent_message" && typeof item.text === "string" && item.text.trim()) { + assistantSnapshots.push(item.text.trim()); + if (!sawAssistantDelta) { + assistantReply = item.text.trim(); + } + } + } + } catch { + // ignore non-json stdout lines + } + }; + + const handleStderrLine = (rawLine: string) => { + const line = rawLine.trim(); + if (!line) { + return; + } + + if (!Array.from(knownNoisyWarnings).some((noise) => line.includes(noise))) { + errorLines.push(line); + } + }; + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdoutBuffer += chunk; + const lines = stdoutBuffer.split(/\r?\n/); + stdoutBuffer = lines.pop() || ""; + for (const line of lines) { + handleStdoutLine(line); + } + }); + + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk: string) => { + stderrBuffer += chunk; + const lines = stderrBuffer.split(/\r?\n/); + stderrBuffer = lines.pop() || ""; + for (const line of lines) { + handleStderrLine(line); + } + }); + + child.on("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + + child.on("close", (code) => { + clearTimeout(timeout); + + if (stdoutBuffer.trim()) { + handleStdoutLine(stdoutBuffer); + } + if (stderrBuffer.trim()) { + handleStderrLine(stderrBuffer); + } + + const reply = assistantReply.trim() || assistantSnapshots.join("\n").trim(); + + if (code && code !== 0 && !reply) { + reject(new Error(errorLines.at(-1) || `Codex CLI 退出码异常:${code}`)); + return; + } + + resolve({ + reply, + threadId, + logLines: [`模型:${input.model}`, threadId ? `Thread:${threadId}` : "Thread:未返回", ...(reply ? [reply] : [])], + errorLines + }); + }); + }); +}; + +export const runCodexReadOnlyStructuredPrompt = async (input: { + projectPath: string; + model: string; + prompt: string; + schema: Record; + resumeSessionId?: string; + timeoutMs?: number; +}): Promise<{ data: T; threadId: string; logLines: string[]; errorLines: string[] }> => { + const schemaPath = path.join(os.tmpdir(), `codex-schema-${randomUUID()}.json`); + await writeFile(schemaPath, JSON.stringify(input.schema, null, 2), "utf8"); + + try { + const invocation = resolveCodexInvocation(); + const args = input.resumeSessionId + ? [ + "-s", + "read-only", + "-a", + "never", + "exec", + "resume", + "--json", + "--skip-git-repo-check", + "-m", + input.model, + "--output-schema", + schemaPath, + input.resumeSessionId, + input.prompt + ] + : [ + "-s", + "read-only", + "-a", + "never", + "exec", + "--json", + "--skip-git-repo-check", + "-C", + input.projectPath, + "-m", + input.model, + "--output-schema", + schemaPath, + input.prompt + ]; + + const { stdout, stderr } = await execFileAsync(invocation.command, [...invocation.prefixArgs, ...args], { + cwd: input.projectPath, + timeout: input.timeoutMs || 120000, + windowsHide: true, + maxBuffer: 1024 * 1024 * 8 + }); + + let replyText = ""; + let threadId = ""; + for (const rawLine of stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)) { + try { + const event = JSON.parse(rawLine) as Record; + if (event.type === "thread.started" && typeof event.thread_id === "string") { + threadId = event.thread_id; + } + if (event.type === "item.completed") { + const item = event.item as Record | undefined; + if (item?.type === "agent_message" && typeof item.text === "string") { + replyText = item.text.trim(); + } + } + } catch { + // ignore non-json stdout lines + } + } + + const noisyWarnings = ["unknown feature key in config", "Failed to create shell snapshot for powershell"]; + const errorLines = stderr + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !noisyWarnings.some((noise) => line.includes(noise))); + + return { + data: JSON.parse(replyText) as T, + threadId, + logLines: [`模型:${input.model}`, threadId ? `Thread:${threadId}` : "Thread:未返回", "已收到结构化结果"], + errorLines + }; + } finally { + await unlink(schemaPath).catch(() => undefined); + } +}; diff --git a/server/src/store.ts b/server/src/store.ts new file mode 100644 index 0000000..1c9f8d6 --- /dev/null +++ b/server/src/store.ts @@ -0,0 +1,3748 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { + ChatMessage, + ChatStreamEvent, + ChatStreamStage, + ConversationThread, + Database, + DiscoveryState, + GitSnapshot, + MailboxDigest, + MailboxMessage, + MailboxMessageCreateInput, + MailboxStatusUpdateInput, + OfficeAgentState, + OfficeState, + Project, + ProjectCreateInput, + ProjectIndexEntry, + ProjectMember, + ProjectStage, + ProjectStorageInfo, + ProjectSummary, + ProposalRole, + ReviewDecisionInput, + ReviewItem, + RuntimeSession, + SessionOperationResult, + SessionModelUpdateInput, + SharedMemoryEntry, + SummaryTeamStatus, + TaskCreateInput, + TaskActivityEntry, + TaskItem, + TeamChangeEntry, + TeamChangeState, + TeamProposal, + TeamProposalUpdateInput, + TeamState +} from "./types.js"; +import { + buildNativeSessions, + createMailboxMessage, + createSharedMemoryEntry, + createTaskActivityEntry, + createDefaultProject, + createDefaultTeamProposal, + createDefaultTeamState, + createLeadMessage, + createManualAction, + createMemberMessage, + createProposalRole, + createProjectStorageInfo, + createSystemMessage, + ensureReviewerRole, + inferCapabilityHint, + inferProjectName, + inferRuntimeType, + now, + promoteProposalRoleToMember, + syncThreadsWithTeam +} from "./defaults.js"; +import { applyCodexConfigToSession, getCodexCliInfo, refreshCodexCliInfo } from "./codexConfig.js"; +import { readGitStatus } from "./git.js"; +import { analyzeProject } from "./discovery.js"; +import { getPixelAgentsAssetsManifest, getPixelAgentsOfficeLayoutTemplate } from "./pixelAgents.js"; +import { + createRuntimeSession, + type CodexPromptStreamEvent, + launchCodexTakeover, + runCodexReadOnlyPrompt, + runCodexReadOnlyStructuredPrompt, + runCodexReadOnlyPromptStreaming, + updateRuntimeSession +} from "./runtime.js"; + +const DATA_FILE = path.resolve(process.cwd(), "data", "db.json"); +const DATA_DIR = path.dirname(DATA_FILE); +const EMPTY_DB: Database = { projects: [] }; +type PersistedProjectRecord = Omit & { + mailbox?: MailboxMessage[]; + sharedProjectMemory?: SharedMemoryEntry[]; + taskActivity?: TaskActivityEntry[]; +}; + +let writeChain = Promise.resolve(); + +const withWriteLock = async (task: () => Promise): Promise => { + const next = writeChain.then(task, task); + writeChain = next.then( + () => undefined, + () => undefined + ); + return next; +}; + +const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; + +const asString = (value: unknown, fallback = ""): string => (typeof value === "string" ? value : fallback); + +const asNullableString = (value: unknown): string | null => (typeof value === "string" && value ? value : null); + +const asBoolean = (value: unknown, fallback = false): boolean => (typeof value === "boolean" ? value : fallback); + +const asArray = (value: unknown): unknown[] => (Array.isArray(value) ? value : []); + +const nowPlus = (seconds = 0): string => new Date(Date.now() + seconds * 1000).toISOString(); + +const legacyBootstrapTaskFingerprints = new Set([ + "确认要不要做项目摸底::这一步必须由你主动点确认,打开项目不会自动跑。", + "和 lead 讨论第一版团队草案::团队岗位不是固定模板,先围绕当前项目目标商量。", + "锁定当前目录下的第一个任务::第一版只在当前项目目录范围内工作。" +]); + +const isLegacyProjectRecord = (value: unknown): boolean => + isRecord(value) && + ["lead", "threads", "teamProposal", "teamDraft", "mailbox", "sharedProjectMemory", "taskActivity"].some((key) => + Object.prototype.hasOwnProperty.call(value, key) + ); + +const createProjectIndex = (project: Project): ProjectIndexEntry => ({ + id: project.id, + name: project.name, + path: project.path, + description: project.description, + createdAt: project.createdAt, + updatedAt: project.updatedAt +}); + +const normalizeProjectIndex = (value: unknown): ProjectIndexEntry => { + const record = isRecord(value) ? value : {}; + const projectPath = asString(record.path, process.cwd()); + const projectName = asString(record.name, inferProjectName(projectPath)); + const createdAt = asString(record.createdAt, now()); + const updatedAt = asString(record.updatedAt, createdAt); + + return { + id: asString(record.id, `project_index_${createdAt}`), + name: projectName, + path: projectPath, + description: asString(record.description, "本地优先项目级 Lead 工作台"), + createdAt, + updatedAt + }; +}; + +const projectStorageForPath = (projectPath: string): ProjectStorageInfo => createProjectStorageInfo(projectPath); + +const sessionStatusFromLifecycle = (lifecycle: ProjectMember["lifecycle"]): RuntimeSession["status"] => { + switch (lifecycle) { + case "working": + return "running"; + case "waiting_review": + return "waiting"; + case "blocked": + case "paused": + return "blocked"; + case "done": + return "done"; + default: + return "idle"; + } +}; + +const appendUniqueLine = (session: RuntimeSession, line: string, status?: RuntimeSession["status"]): RuntimeSession => + updateRuntimeSession(session, { + status, + appendOutput: session.recentOutput.at(-1) === line ? [] : [line] + }); + +const normalizeChatMessage = (value: unknown, fallbackName?: string): ChatMessage => { + const record = isRecord(value) ? value : {}; + const sender = asString(record.sender, fallbackName ? "member" : "system"); + const createdAt = asString(record.createdAt, now()); + const senderName = typeof record.senderName === "string" ? record.senderName : fallbackName; + + return { + id: asString(record.id, `msg_${createdAt}`), + sender: sender === "lead" || sender === "member" || sender === "user" || sender === "system" ? sender : "system", + content: asString(record.content, "没有消息内容。"), + createdAt, + ...(senderName ? { senderName } : {}) + }; +}; + +const normalizeRuntimeSession = ( + value: unknown, + projectPath: string, + roleLabel: string, + defaultModel: string +): RuntimeSession => { + const seed = createRuntimeSession(roleLabel, projectPath, defaultModel); + const record = isRecord(value) ? value : {}; + + return { + ...seed, + id: asString(record.id, seed.id), + adapter: "codex-cli", + roleLabel, + status: + asString(record.status, seed.status) === "running" || + asString(record.status, seed.status) === "waiting" || + asString(record.status, seed.status) === "needs_confirmation" || + asString(record.status, seed.status) === "blocked" || + asString(record.status, seed.status) === "done" || + asString(record.status, seed.status) === "offline" + ? (asString(record.status, seed.status) as RuntimeSession["status"]) + : "idle", + identityState: + asString(record.identityState, seed.identityState) === "resume_ready" || + asString(record.identityState, seed.identityState) === "needs_rebuild" + ? (asString(record.identityState, seed.identityState) as RuntimeSession["identityState"]) + : "fresh", + model: asString(record.model, seed.model), + modelSource: asString(record.modelSource, "codex-config") === "session-override" ? "session-override" : "codex-config", + estimatedContextTokens: + typeof record.estimatedContextTokens === "number" && Number.isFinite(record.estimatedContextTokens) + ? Math.max(0, Math.round(record.estimatedContextTokens)) + : 0, + contextUsagePercent: + typeof record.contextUsagePercent === "number" && Number.isFinite(record.contextUsagePercent) + ? Math.max(0, record.contextUsagePercent) + : 0, + lastThreadId: asNullableString(record.lastThreadId), + recentOutput: asArray(record.recentOutput).map((item) => asString(item)).filter(Boolean).slice(-6), + lastUpdatedAt: asString(record.lastUpdatedAt, seed.lastUpdatedAt), + lastCommand: asString(record.lastCommand, seed.lastCommand), + takeoverCommand: asString(record.takeoverCommand, seed.takeoverCommand), + resumeCommand: asString(record.resumeCommand, seed.resumeCommand), + lastError: asString(record.lastError, "") + }; +}; + +const normalizeProposalRole = (value: unknown): ProposalRole => { + const record = isRecord(value) ? value : {}; + const displayName = asString(record.displayName, asString(record.name, "新成员")); + const title = asString(record.title, asString(record.role, "执行成员")); + const responsibility = asString(record.responsibility, asString(record.notes, "负责承接项目当前阶段的工作。")); + const runtimeTypeSource = asString(record.runtimeType, asString(record.type, "")); + const runtimeType = + runtimeTypeSource === "reviewer" || runtimeTypeSource === "worker" + ? (runtimeTypeSource as ProposalRole["runtimeType"]) + : inferRuntimeType(`${displayName} ${title} ${responsibility}`); + const capabilityHintSource = asString(record.capabilityHint, ""); + + return { + id: asString(record.id, `proposal_${now()}`), + displayName, + title, + runtimeType, + capabilityHint: + capabilityHintSource === "review" || + capabilityHintSource === "frontend" || + capabilityHintSource === "backend" || + capabilityHintSource === "qa" || + capabilityHintSource === "custom" + ? (capabilityHintSource as ProposalRole["capabilityHint"]) + : inferCapabilityHint(`${displayName} ${title} ${responsibility}`), + responsibility, + reason: asString(record.reason, "这是当前项目主链路需要的岗位。"), + source: asString(record.source, "new") === "existing" ? "existing" : "new" + }; +}; + +const normalizeProjectMember = (value: unknown, projectPath: string, defaultModel: string): ProjectMember => { + const record = isRecord(value) ? value : {}; + const displayName = asString(record.displayName, asString(record.name, "项目成员")); + const title = asString(record.title, asString(record.role, "执行成员")); + const responsibility = asString(record.responsibility, asString(record.notes, "负责承接当前任务。")); + const runtimeTypeSource = asString(record.runtimeType, asString(record.type, "")); + const runtimeType = + runtimeTypeSource === "reviewer" || runtimeTypeSource === "worker" + ? (runtimeTypeSource as ProjectMember["runtimeType"]) + : inferRuntimeType(`${displayName} ${title} ${responsibility}`); + const capabilityHintSource = asString(record.capabilityHint, ""); + const lifecycleSource = asString(record.lifecycle, asString(record.status, "idle")); + const lifecycle: ProjectMember["lifecycle"] = + lifecycleSource === "working" || + lifecycleSource === "waiting_review" || + lifecycleSource === "blocked" || + lifecycleSource === "paused" || + lifecycleSource === "done" + ? (lifecycleSource as ProjectMember["lifecycle"]) + : "idle"; + + return { + id: asString(record.id, `member_${now()}`), + displayName, + title, + runtimeType, + capabilityHint: + capabilityHintSource === "review" || + capabilityHintSource === "frontend" || + capabilityHintSource === "backend" || + capabilityHintSource === "qa" || + capabilityHintSource === "custom" + ? (capabilityHintSource as ProjectMember["capabilityHint"]) + : inferCapabilityHint(`${displayName} ${title} ${responsibility}`), + responsibility, + lifecycle, + source: asString(record.source, "new") === "existing" ? "existing" : "new", + createdAt: asString(record.createdAt, now()), + lastUsedAt: asNullableString(record.lastUsedAt), + threadId: asString(record.threadId, `thread_member_${asString(record.id, now())}`), + session: appendUniqueLine( + normalizeRuntimeSession(record.session, projectPath, displayName, defaultModel), + asString(record.sessionLog, `${displayName} 已加入项目。`), + sessionStatusFromLifecycle(lifecycle) + ) + }; +}; + +const normalizeTeamProposal = (value: unknown, projectPath: string, defaultModel: string): TeamProposal => { + const base = createDefaultTeamProposal(); + const record = isRecord(value) ? value : {}; + const roles = ensureReviewerRole(asArray(record.roles).map((item) => normalizeProposalRole(item))); + const statusSource = asString(record.status, roles.length > 0 ? "pending_confirmation" : "draft"); + + return { + ...base, + summary: asString(record.summary, base.summary), + naturalLanguageBrief: asString(record.naturalLanguageBrief, base.naturalLanguageBrief), + status: + statusSource === "pending_confirmation" || statusSource === "confirmed" + ? (statusSource as TeamProposal["status"]) + : roles.length > 0 + ? "pending_confirmation" + : "draft", + changeMode: asString(record.changeMode, base.changeMode) === "update" ? "update" : "initial", + reuseFirst: asBoolean(record.reuseFirst, base.reuseFirst), + reviewerRule: asString(record.reviewerRule, base.reviewerRule), + leadOverrideRule: asString(record.leadOverrideRule, base.leadOverrideRule), + userOverrideRule: asString(record.userOverrideRule, base.userOverrideRule), + roles, + archivedMembers: asArray(record.archivedMembers).map((item) => normalizeProjectMember(item, projectPath, defaultModel)), + updatedAt: asString(record.updatedAt, base.updatedAt) + }; +}; + +const materializeMember = ( + role: ProposalRole, + projectPath: string, + archivedPool: ProjectMember[], + defaultModel: string +): ProjectMember => { + const matched = archivedPool.find( + (member) => member.displayName === role.displayName || (member.title === role.title && member.runtimeType === role.runtimeType) + ); + + if (!matched) { + return promoteProposalRoleToMember(role, projectPath, defaultModel); + } + + return { + ...matched, + responsibility: role.responsibility, + title: role.title, + capabilityHint: role.capabilityHint, + lifecycle: "idle", + source: "existing", + lastUsedAt: now(), + session: appendUniqueLine(matched.session, `${matched.displayName} 已从项目历史成员池中复用。`, "waiting") + }; +}; + +const normalizeTeamState = (value: unknown, projectPath: string, proposal: TeamProposal, defaultModel: string): TeamState => { + const base = createDefaultTeamState(); + const record = isRecord(value) ? value : {}; + const membersFromTeam = asArray(record.members).map((item) => normalizeProjectMember(item, projectPath, defaultModel)); + const legacyConfirmedMembers = + membersFromTeam.length > 0 + ? membersFromTeam + : asString(record.status) === "confirmed" + ? ensureReviewerRole(proposal.roles).map((role) => promoteProposalRoleToMember(role, projectPath, defaultModel)) + : []; + + return { + ...base, + status: legacyConfirmedMembers.length > 0 ? "active" : "inactive", + members: legacyConfirmedMembers, + archivedMembers: asArray(record.archivedMembers).map((item) => normalizeProjectMember(item, projectPath, defaultModel)), + confirmedAt: asNullableString(record.confirmedAt), + updatedAt: asString(record.updatedAt, base.updatedAt), + lastChangeSummary: asString(record.lastChangeSummary, base.lastChangeSummary) + }; +}; + +const normalizeDiscovery = (value: unknown): DiscoveryState => { + const record = isRecord(value) ? value : {}; + const status = asString(record.status, "idle"); + + return { + status: status === "pending_confirmation" || status === "completed" ? status : "idle", + requiresUserConfirmation: asBoolean(record.requiresUserConfirmation, true), + requestNote: asString(record.requestNote, ""), + requestedAt: asNullableString(record.requestedAt), + confirmedAt: asNullableString(record.confirmedAt), + lastBrief: asString(record.lastBrief, ""), + facts: asArray(record.facts).map((item) => asString(item)).filter(Boolean), + stack: asArray(record.stack).map((item) => asString(item)).filter(Boolean), + topLevelEntries: asArray(record.topLevelEntries).map((item) => asString(item)).filter(Boolean) + }; +}; + +const normalizeGit = (value: unknown): GitSnapshot => { + const record = isRecord(value) ? value : {}; + + return { + mode: asString(record.mode, "placeholder") === "live" ? "live" : "placeholder", + branch: asString(record.branch, "未读取"), + summary: asString(record.summary, "还没有读取 Git 状态。"), + dirty: asBoolean(record.dirty, false), + trackedChanges: typeof record.trackedChanges === "number" ? record.trackedChanges : 0, + lastCheckedAt: asNullableString(record.lastCheckedAt), + raw: asString(record.raw, "") + }; +}; + +const normalizeTasks = (value: unknown): TaskItem[] => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + const updatedAt = asString(record.updatedAt, now()); + const status = asString(record.status, "todo"); + + return { + id: asString(record.id, `task_${updatedAt}`), + title: asString(record.title, "未命名任务"), + summary: asString(record.summary, asString(record.note, "还没有任务说明。")), + status: + status === "in_progress" || + status === "in_review" || + status === "changes_requested" || + status === "blocked" || + status === "done" + ? status + : status === "review" + ? "in_review" + : "todo", + ownerMemberId: asNullableString(record.ownerMemberId), + ownerName: asString(record.ownerName, asString(record.owner, "项目 lead")), + createdBy: asString(record.createdBy, "项目 lead"), + reviewState: + asString(record.reviewState, "not_requested") === "requested" || + asString(record.reviewState, "not_requested") === "changes_requested" || + asString(record.reviewState, "not_requested") === "approved" + ? (asString(record.reviewState) as TaskItem["reviewState"]) + : "not_requested", + currentReviewId: asNullableString(record.currentReviewId), + updatedAt + }; + }); + +const normalizeReviews = (value: unknown): ReviewItem[] => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + const status = asString(record.status, "requested"); + + return { + id: asString(record.id, `review_${now()}`), + taskId: asString(record.taskId), + reviewerMemberId: asString(record.reviewerMemberId), + status: status === "approved" || status === "changes_requested" ? status : "requested", + note: asString(record.note, "等待 reviewer 结论。"), + createdAt: asString(record.createdAt, now()) + }; + }); + +const normalizeManualActions = (value: unknown) => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + return { + id: asString(record.id, `manual_${now()}`), + action: asString(record.action, "manual"), + operator: asString(record.operator, "用户人工接管"), + targetMemberId: asNullableString(record.targetMemberId), + targetMemberName: asNullableString(record.targetMemberName), + note: asString(record.note, ""), + createdAt: asString(record.createdAt, now()) + }; + }); + +const normalizeMailbox = (value: unknown): MailboxMessage[] => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + const kind = asString(record.kind, "question"); + const status = asString(record.status, "open"); + + return { + id: asString(record.id, `mailbox_${now()}`), + fromMemberId: asNullableString(record.fromMemberId), + fromLabel: asString(record.fromLabel, "未知成员"), + toMemberId: asNullableString(record.toMemberId), + toLabel: asString(record.toLabel, "未指定"), + kind: + kind === "dependency_request" || + kind === "blocker" || + kind === "handoff" || + kind === "review_feedback" || + kind === "resubmit_request" || + kind === "broadcast" + ? kind + : "question", + relatedTaskId: asNullableString(record.relatedTaskId), + summary: asString(record.summary, "协作消息"), + body: asString(record.body, ""), + status: status === "acknowledged" || status === "resolved" ? status : "open", + createdAt: asString(record.createdAt, now()) + }; + }); + +const normalizeSharedMemory = (value: unknown): SharedMemoryEntry[] => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + return { + id: asString(record.id, `memory_${now()}`), + title: asString(record.title, "共享记忆"), + body: asString(record.body, ""), + source: asString(record.source, "system") === "lead" ? "lead" : "system", + createdAt: asString(record.createdAt, now()) + }; + }); + +const normalizeTaskActivity = (value: unknown): TaskActivityEntry[] => + asArray(value).map((item) => { + const record = isRecord(item) ? item : {}; + const kind = asString(record.kind, "task_created"); + return { + id: asString(record.id, `activity_${now()}`), + taskId: asNullableString(record.taskId), + kind: + kind === "task_assigned" || + kind === "task_started" || + kind === "task_submitted" || + kind === "task_reviewed" || + kind === "task_blocked" || + kind === "task_resumed" || + kind === "task_done" || + kind === "mailbox_linked" + ? kind + : "task_created", + actorId: asNullableString(record.actorId), + actorLabel: asString(record.actorLabel, "系统"), + summary: asString(record.summary, "任务活动"), + detail: asString(record.detail, ""), + relatedMailboxId: asNullableString(record.relatedMailboxId), + createdAt: asString(record.createdAt, now()) + }; + }); + +const normalizeLead = (value: unknown, projectPath: string, fallbackName: string, defaultModel: string): Project["lead"] => { + const record = isRecord(value) ? value : {}; + const name = asString(record.name, fallbackName); + + return { + id: asString(record.id, `lead_${now()}`), + name, + version: typeof record.version === "number" ? record.version : 1, + status: asString(record.status, "active") === "handover_pending" ? "handover_pending" : "active", + handoverNote: asString(record.handoverNote, "如需重建 lead,新 lead 上线前必须由上一任 lead 先做交接。"), + session: appendUniqueLine(normalizeRuntimeSession(record.session, projectPath, name, defaultModel), "Lead 运行态已恢复。") + }; +}; + +const deriveSummaryTeamStatus = (project: Project): SummaryTeamStatus => { + if (project.team.status === "active") { + return "active"; + } + + if (project.teamProposal.status === "pending_confirmation") { + return "pending_confirmation"; + } + + if (project.teamProposal.roles.length > 0) { + return "drafting"; + } + + return "lead_only"; +}; + +const deriveProjectStage = (project: Project): ProjectStage => { + const workflowTasks = + project.team.status === "active" + ? project.tasks + : project.tasks.filter((task) => task.ownerMemberId !== null || task.currentReviewId !== null); + + if (project.lead.status === "handover_pending") { + return "blocked"; + } + + if (workflowTasks.length > 0 && workflowTasks.every((task) => task.status === "done")) { + return "done"; + } + + if (workflowTasks.some((task) => task.status === "changes_requested" || task.status === "blocked")) { + return "blocked"; + } + + if (workflowTasks.some((task) => task.status === "in_review")) { + return "reviewing"; + } + + if (workflowTasks.some((task) => task.status === "in_progress")) { + return "executing"; + } + + if (project.team.status === "active") { + return "team_active"; + } + + if (project.teamProposal.status === "pending_confirmation") { + return "team_pending_confirmation"; + } + + if (project.teamProposal.roles.length > 0) { + return "team_drafting"; + } + + if (project.discovery.status === "pending_confirmation") { + return "discovery_pending"; + } + + return "lead_only"; +}; + +const stripLegacyBootstrapTasks = (project: Project): Project => { + if (project.tasks.length === 0) { + return project; + } + + const filteredTasks = project.tasks.filter((task) => { + const fingerprint = `${task.title}::${task.summary}`; + const isLegacyBootstrapTask = + legacyBootstrapTaskFingerprints.has(fingerprint) && + task.ownerMemberId === null && + task.currentReviewId === null && + task.createdBy === "项目 lead"; + + return !isLegacyBootstrapTask; + }); + + if (filteredTasks.length === project.tasks.length) { + return project; + } + + return { + ...project, + tasks: filteredTasks + }; +}; + +const approximateTokenCount = (text: string): number => { + const trimmed = text.trim(); + if (!trimmed) { + return 0; + } + + // 粗略估算:中文、路径、命令、英文混合时,按 1.2 字符约 1 token 取整。 + return Math.max(1, Math.ceil(trimmed.length / 1.2)); +}; + +const estimateSessionContext = ( + project: Project, + session: RuntimeSession, + thread: ConversationThread | undefined, + actorId: string | null +): Pick => { + const threadText = thread?.messages.map((message) => `${message.sender}:${message.content}`).join("\n") || ""; + const mailboxText = project.mailbox + .filter((message) => (actorId ? message.fromMemberId === actorId || message.toMemberId === actorId : true)) + .slice(0, 6) + .map((message) => `${message.summary}\n${message.body}`) + .join("\n"); + const taskText = project.tasks + .filter((task) => (actorId ? task.ownerMemberId === actorId : true)) + .slice(0, 4) + .map((task) => `${task.title}\n${task.summary}`) + .join("\n"); + const memoryText = project.sharedProjectMemory + .slice(0, 4) + .map((entry) => `${entry.title}\n${entry.body}`) + .join("\n"); + const runtimeText = session.recentOutput.join("\n"); + const sourceText = [threadText, mailboxText, taskText, memoryText, runtimeText].filter(Boolean).join("\n"); + const estimatedContextTokens = approximateTokenCount(sourceText); + const contextWindow = project.codex.contextWindow || project.codex.autoCompactTokenLimit || null; + const rawPercent = contextWindow ? (estimatedContextTokens / contextWindow) * 100 : 0; + const contextUsagePercent = contextWindow + ? Number(Math.min(99.9, Math.max(estimatedContextTokens > 0 ? 0.1 : 0, rawPercent)).toFixed(1)) + : 0; + + return { + estimatedContextTokens, + contextUsagePercent + }; +}; + +const teamChangeKindRank: Record = { + add: 0, + remove: 1, + update: 2, + keep: 3 +}; + +const buildTeamChangeState = (team: TeamState, proposal: TeamProposal): TeamChangeState => { + if (team.status !== "active" || proposal.changeMode !== "update") { + return { + pendingConfirmation: false, + summary: "当前没有待确认的团队变更。", + entries: [], + updatedAt: null + }; + } + + const matchedMemberIds = new Set(); + const entries: TeamChangeEntry[] = []; + + for (const role of ensureReviewerRole(proposal.roles)) { + const currentMember = team.members.find( + (member) => + !matchedMemberIds.has(member.id) && + (member.displayName === role.displayName || (member.title === role.title && member.runtimeType === role.runtimeType)) + ); + + if (!currentMember) { + entries.push({ + kind: "add", + currentMemberId: null, + proposalRoleId: role.id, + displayName: role.displayName, + title: role.title, + summary: `会新增正式成员,职责是:${role.responsibility}` + }); + continue; + } + + matchedMemberIds.add(currentMember.id); + const changedFields: string[] = []; + + if (currentMember.displayName !== role.displayName) { + changedFields.push("显示名称"); + } + if (currentMember.title !== role.title) { + changedFields.push("岗位标题"); + } + if (currentMember.responsibility !== role.responsibility) { + changedFields.push("职责说明"); + } + if (currentMember.capabilityHint !== role.capabilityHint) { + changedFields.push("能力提示"); + } + if (currentMember.runtimeType !== role.runtimeType) { + changedFields.push("角色类型"); + } + + entries.push({ + kind: changedFields.length > 0 ? "update" : "keep", + currentMemberId: currentMember.id, + proposalRoleId: role.id, + displayName: role.displayName, + title: role.title, + summary: + changedFields.length > 0 + ? `会更新这些内容:${changedFields.join("、")}` + : "保留当前正式成员,不需要额外调整。" + }); + } + + for (const member of team.members) { + if (matchedMemberIds.has(member.id)) { + continue; + } + + entries.push({ + kind: "remove", + currentMemberId: member.id, + proposalRoleId: null, + displayName: member.displayName, + title: member.title, + summary: "会从当前正式团队移出,并归档到本项目历史成员池。" + }); + } + + const sortedEntries = [...entries].sort((left, right) => teamChangeKindRank[left.kind] - teamChangeKindRank[right.kind]); + const addCount = sortedEntries.filter((entry) => entry.kind === "add").length; + const removeCount = sortedEntries.filter((entry) => entry.kind === "remove").length; + const updateCount = sortedEntries.filter((entry) => entry.kind === "update").length; + const actionableEntries = sortedEntries.filter((entry) => entry.kind !== "keep"); + const pendingConfirmation = proposal.status === "pending_confirmation" && actionableEntries.length > 0; + + return { + pendingConfirmation, + summary: + pendingConfirmation + ? `这次团队变更里:新增 ${addCount} 个、移出 ${removeCount} 个、调整 ${updateCount} 个。确认后才会真正改动正式团队。` + : "当前没有待确认的团队变更。", + entries: pendingConfirmation ? actionableEntries : [], + updatedAt: pendingConfirmation ? proposal.updatedAt : null + }; +}; + +const mailboxPriority = (message: MailboxMessage): number => { + if (message.status === "open" && message.kind === "blocker") { + return 0; + } + if (message.status === "open" && message.kind === "dependency_request") { + return 1; + } + if (message.status === "open" && message.kind === "review_feedback") { + return 2; + } + if (message.status === "open" && message.kind === "resubmit_request") { + return 3; + } + if (message.status === "open") { + return 4; + } + return 5; +}; + +const dominantMailboxMessage = (messages: MailboxMessage[]): MailboxMessage | null => { + if (messages.length === 0) { + return null; + } + + return [...messages].sort((left, right) => { + const priorityDiff = mailboxPriority(left) - mailboxPriority(right); + if (priorityDiff !== 0) { + return priorityDiff; + } + return right.createdAt.localeCompare(left.createdAt); + })[0]; +}; + +const mailboxNextAction = (message: MailboxMessage | null): string => { + if (!message) { + return "当前没有需要推进的队内动作。"; + } + + if (message.status !== "open") { + return "这组消息当前都已经收口。"; + } + + switch (message.kind) { + case "blocker": + return `建议 ${message.toLabel} 先处理这个阻塞。`; + case "dependency_request": + return `建议 ${message.toLabel} 先回应依赖请求。`; + case "review_feedback": + return `建议 ${message.toLabel} 先按 reviewer 意见返工。`; + case "resubmit_request": + return `建议 ${message.toLabel} 尽快给出复审结论。`; + case "handoff": + return `建议 ${message.toLabel} 先接住这条交接。`; + case "broadcast": + return "建议 lead 先看这条广播是否需要同步到主摘要。"; + default: + return `建议 ${message.toLabel} 先回复这组沟通。`; + } +}; + +const mailboxHighlights = (messages: MailboxMessage[]): string[] => + [...messages] + .sort((left, right) => { + const priorityDiff = mailboxPriority(left) - mailboxPriority(right); + if (priorityDiff !== 0) { + return priorityDiff; + } + return right.createdAt.localeCompare(left.createdAt); + }) + .slice(0, 2) + .map((message) => + message.status === "open" + ? `${mailboxKindLabelForLead(message.kind)}:${message.summary}` + : `最近收口:${message.summary}` + ); + +const buildMailboxDigestSummary = (messages: MailboxMessage[], title: string): string => { + const latest = messages[0]; + const important = dominantMailboxMessage(messages) || latest; + + if (!latest) { + return `${title} 当前还没有原始消息。`; + } + + if (messages.length === 1) { + return latest.summary; + } + + const base = `${title} 目前累计往来 ${messages.length} 条`; + if (important.kind === "blocker") { + return `${base},当前主要阻塞是:${important.summary}`; + } + if (important.kind === "dependency_request") { + return `${base},当前主要依赖是:${important.summary}`; + } + if (important.kind === "review_feedback") { + return `${base},当前审查意见是:${important.summary}`; + } + if (important.kind === "resubmit_request") { + return `${base},当前最新复审动作是:${important.summary}`; + } + + return `${base},最近进展是:${latest.summary}`; +}; + +const buildMailboxSummaries = (project: Project): MailboxDigest[] => { + const grouped = new Map< + string, + { + title: string; + subtitle: string; + relatedTaskId: string | null; + messages: MailboxMessage[]; + } + >(); + + const sortedMessages = [...project.mailbox].sort((left, right) => right.createdAt.localeCompare(left.createdAt)); + + for (const message of sortedMessages) { + const relatedTask = project.tasks.find((task) => task.id === message.relatedTaskId) || null; + const key = relatedTask + ? `task:${relatedTask.id}` + : message.kind === "broadcast" + ? `broadcast:${message.fromMemberId || message.fromLabel}` + : `route:${[message.fromMemberId || message.fromLabel, message.toMemberId || message.toLabel].sort().join("::")}`; + + if (!grouped.has(key)) { + grouped.set(key, { + title: relatedTask ? `任务协作:${relatedTask.title}` : message.kind === "broadcast" ? `广播消息:${message.fromLabel}` : `${message.fromLabel} ⇄ ${message.toLabel}`, + subtitle: relatedTask + ? "同一任务下的原始往来默认折叠展示。" + : message.kind === "broadcast" + ? "非任务型广播默认折叠展示。" + : "成员之间的原始往来默认折叠展示。", + relatedTaskId: relatedTask?.id || null, + messages: [] + }); + } + + grouped.get(key)?.messages.push(message); + } + + return [...grouped.entries()] + .map(([id, group]) => { + const dominant = dominantMailboxMessage(group.messages) || group.messages[0]; + const participants = Array.from(new Set(group.messages.flatMap((message) => [message.fromLabel, message.toLabel]).filter(Boolean))); + + return { + id, + title: group.title, + summary: buildMailboxDigestSummary(group.messages, group.title), + subtitle: group.subtitle, + primaryKind: dominant?.kind || "question", + participants, + nextAction: mailboxNextAction(dominant), + highlights: mailboxHighlights(group.messages), + latestAt: group.messages[0]?.createdAt || now(), + unresolvedCount: group.messages.filter((message) => message.status === "open").length, + messageCount: group.messages.length, + relatedTaskId: group.relatedTaskId, + messageIds: group.messages.map((message) => message.id) + }; + }) + .sort((left, right) => right.latestAt.localeCompare(left.latestAt)); +}; + +const syncProject = (project: Project): Project => { + const cleanedProject = stripLegacyBootstrapTasks(project); + const codex = getCodexCliInfo(); + const storage = projectStorageForPath(cleanedProject.path); + const syncedLead = { + ...cleanedProject.lead, + session: applyCodexConfigToSession( + updateRuntimeSession(cleanedProject.lead.session, { + status: cleanedProject.lead.status === "handover_pending" ? "blocked" : cleanedProject.lead.session.status + }), + codex, + cleanedProject.path + ) + }; + + const syncedMembers = cleanedProject.team.members.map((member) => ({ + ...member, + session: applyCodexConfigToSession( + updateRuntimeSession(member.session, { + status: sessionStatusFromLifecycle(member.lifecycle) + }), + codex, + cleanedProject.path + ) + })); + + const threads = syncThreadsWithTeam(cleanedProject.threads, syncedLead, syncedMembers, cleanedProject.messages); + const leadThread = threads.find((thread) => thread.id === "thread_lead"); + const leadContext = estimateSessionContext(cleanedProject, syncedLead.session, leadThread, cleanedProject.lead.id); + const membersWithContext = syncedMembers.map((member) => { + const thread = threads.find((item) => item.actorId === member.id); + return { + ...member, + session: updateRuntimeSession(member.session, estimateSessionContext(cleanedProject, member.session, thread, member.id)) + }; + }); + const syncedProject: Project = { + ...cleanedProject, + codex, + storage, + lead: syncedLead, + team: { + ...cleanedProject.team, + members: membersWithContext + }, + nativeSessions: buildNativeSessions( + { + ...syncedLead, + session: updateRuntimeSession(syncedLead.session, leadContext) + }, + membersWithContext + ), + teamChange: buildTeamChangeState( + { + ...cleanedProject.team, + members: membersWithContext + }, + cleanedProject.teamProposal + ), + threads, + messages: leadThread?.messages || cleanedProject.messages + }; + + return { + ...syncedProject, + lead: { + ...syncedProject.lead, + session: updateRuntimeSession(syncedProject.lead.session, leadContext) + }, + mailboxSummaries: buildMailboxSummaries(syncedProject), + stage: deriveProjectStage(syncedProject) + }; +}; + +const currentTaskLabel = (project: Project): string => { + const taskPool = + project.team.status === "active" + ? project.tasks + : project.tasks.filter((task) => task.ownerMemberId !== null || task.currentReviewId !== null); + const activeTask = taskPool.find((task) => task.status !== "done"); + if (activeTask) { + return activeTask.title; + } + + switch (project.stage) { + case "discovery_pending": + return "等你确认是否开始项目摸底"; + case "team_pending_confirmation": + return "确认项目专属团队"; + case "team_active": + return "让 lead 派发第一条任务"; + case "reviewing": + return "等待 reviewer 给出结论"; + case "done": + return "当前主链路已走完"; + default: + return "先和 lead 讨论目标"; + } +}; + +const toSummary = (project: Project): ProjectSummary => ({ + id: project.id, + name: project.name, + path: project.path, + updatedAt: project.updatedAt, + leadName: project.lead.name, + stage: project.stage, + discoveryStatus: project.discovery.status, + teamStatus: deriveSummaryTeamStatus(project), + currentTask: currentTaskLabel(project), + lastMessagePreview: project.messages.at(-1)?.content || "还没有对话" +}); + +const withLiveGit = async (project: Project): Promise => ({ + ...project, + git: await readGitStatus(project.path) +}); + +const replaceThread = (threads: ConversationThread[], nextThread: ConversationThread): ConversationThread[] => + threads.map((thread) => (thread.id === nextThread.id ? nextThread : thread)); + +const normalizeProject = (value: unknown): Project => { + const record = isRecord(value) ? value : {}; + const projectPath = asString(record.path, process.cwd()); + const storage = projectStorageForPath(projectPath); + const codex = getCodexCliInfo(); + const leadName = asString(isRecord(record.lead) ? record.lead.name : undefined, `${asString(record.name, "未命名项目")} 项目 lead`); + const base = createDefaultProject({ + name: asString(record.name, "未命名项目"), + path: projectPath, + leadName, + description: asString(record.description, "本地优先项目级 Lead 工作台") + }, codex); + + const messages = asArray(record.messages).map((item) => normalizeChatMessage(item, leadName)); + const teamProposal = normalizeTeamProposal(record.teamProposal ?? record.teamDraft, projectPath, codex.defaultModel); + const team = normalizeTeamState(record.team ?? record.teamDraft, projectPath, teamProposal, codex.defaultModel); + const lead = normalizeLead(record.lead, projectPath, leadName, codex.defaultModel); + + const threads = syncThreadsWithTeam( + asArray(record.threads).map((item) => { + const threadRecord = isRecord(item) ? item : {}; + const rawStatus = asString(threadRecord.status, "standby"); + return { + id: asString(threadRecord.id, "thread_unknown"), + kind: asString(threadRecord.kind, "member") === "lead" ? "lead" : "member", + runtimeType: + asString(threadRecord.runtimeType, asString(threadRecord.kind, "member")) === "reviewer" + ? "reviewer" + : asString(threadRecord.kind) === "lead" + ? "lead" + : "worker", + label: asString(threadRecord.label, "线程"), + subtitle: asString(threadRecord.subtitle, ""), + status: rawStatus === "active" || rawStatus === "blocked" || rawStatus === "done" ? rawStatus : "standby", + actorId: asNullableString(threadRecord.actorId ?? threadRecord.memberId), + messages: asArray(threadRecord.messages).map((message) => normalizeChatMessage(message)) + } satisfies ConversationThread; + }), + lead, + team.members, + messages.length > 0 ? messages : base.messages + ); + + return syncProject({ + ...base, + id: asString(record.id, base.id), + description: asString(record.description, base.description), + createdAt: asString(record.createdAt, base.createdAt), + updatedAt: asString(record.updatedAt, base.updatedAt), + codex, + lead, + teamProposal, + team, + nativeSessions: buildNativeSessions(lead, team.members), + discovery: normalizeDiscovery(record.discovery), + messages: messages.length > 0 ? messages : base.messages, + threads, + tasks: normalizeTasks(record.tasks), + reviews: normalizeReviews(record.reviews), + mailbox: normalizeMailbox(record.mailbox), + sharedProjectMemory: normalizeSharedMemory(record.sharedProjectMemory), + taskActivity: normalizeTaskActivity(record.taskActivity), + git: normalizeGit(record.git), + manualActions: normalizeManualActions(record.manualActions), + storage + }); +}; + +const serializeProjectCore = (project: Project): PersistedProjectRecord => { + const { storage: _storage, mailbox, sharedProjectMemory, taskActivity, mailboxSummaries: _mailboxSummaries, teamChange: _teamChange, ...core } = project; + return core; +}; + +const renderSharedMemoryMarkdown = (project: Project): string => { + const lines = [ + "# Shared Project Memory", + "", + `- 项目:${project.name}`, + `- 路径:${project.path}`, + `- 最近更新:${project.updatedAt}`, + "" + ]; + + if (project.sharedProjectMemory.length === 0) { + lines.push("当前还没有正式共享记忆。", ""); + return lines.join("\n"); + } + + for (const entry of project.sharedProjectMemory) { + lines.push(`## ${entry.title}`, "", `- 来源:${entry.source}`, `- 时间:${entry.createdAt}`, "", entry.body || "暂无正文。", ""); + } + + return lines.join("\n"); +}; + +const ensureProjectWorkbench = async (storage: ProjectStorageInfo): Promise => { + await mkdir(storage.workbenchDir, { recursive: true }); +}; + +const writeJsonFile = async (filePath: string, value: unknown): Promise => { + await writeFile(filePath, JSON.stringify(value, null, 2), "utf8"); +}; + +const readJsonFile = async (filePath: string): Promise => { + try { + return JSON.parse(await readFile(filePath, "utf8")) as unknown; + } catch { + return null; + } +}; + +const writeProjectStorage = async (project: Project): Promise => { + const syncedProject = syncProject(project); + await ensureProjectWorkbench(syncedProject.storage); + await writeJsonFile(syncedProject.storage.projectStateFile, serializeProjectCore(syncedProject)); + await writeJsonFile(syncedProject.storage.mailboxFile, syncedProject.mailbox); + await writeJsonFile(syncedProject.storage.sharedMemoryFile, syncedProject.sharedProjectMemory); + await writeJsonFile(syncedProject.storage.taskActivityFile, syncedProject.taskActivity); + await writeFile(syncedProject.storage.sharedMemoryMarkdownFile, renderSharedMemoryMarkdown(syncedProject), "utf8"); + return syncedProject; +}; + +const fallbackSummaryFromIndex = (entry: ProjectIndexEntry): ProjectSummary => ({ + id: entry.id, + name: entry.name, + path: entry.path, + updatedAt: entry.updatedAt, + leadName: `${entry.name} 项目 lead`, + stage: "lead_only", + discoveryStatus: "idle", + teamStatus: "lead_only", + currentTask: "项目状态文件还没准备好", + lastMessagePreview: "还没有读取到本地工作台状态。" +}); + +const readProjectStorage = async (entry: ProjectIndexEntry): Promise => { + const storage = projectStorageForPath(entry.path); + const coreValue = await readJsonFile(storage.projectStateFile); + + if (!coreValue || !isRecord(coreValue)) { + return null; + } + + const mailboxValue = await readJsonFile(storage.mailboxFile); + const sharedMemoryValue = await readJsonFile(storage.sharedMemoryFile); + const taskActivityValue = await readJsonFile(storage.taskActivityFile); + + return normalizeProject({ + ...coreValue, + id: entry.id, + name: entry.name, + path: entry.path, + description: entry.description || asString(coreValue.description, "本地优先项目级 Lead 工作台"), + createdAt: entry.createdAt || asString(coreValue.createdAt, now()), + updatedAt: asString(coreValue.updatedAt, entry.updatedAt), + mailbox: mailboxValue ?? coreValue.mailbox ?? [], + sharedProjectMemory: sharedMemoryValue ?? coreValue.sharedProjectMemory ?? [], + taskActivity: taskActivityValue ?? coreValue.taskActivity ?? [] + }); +}; + +const migrateLegacyDatabase = async (projects: unknown[]): Promise => { + const migrated: Database = { projects: [] }; + + for (const item of projects) { + if (isLegacyProjectRecord(item)) { + const project = await writeProjectStorage(normalizeProject(item)); + migrated.projects.push(createProjectIndex(project)); + continue; + } + + migrated.projects.push(normalizeProjectIndex(item)); + } + + await writeDatabase(migrated); + return migrated; +}; + +const updateLeadThread = (project: Project, messages: ChatMessage[]): Project => { + const leadThread = + project.threads.find((thread) => thread.id === "thread_lead") || syncThreadsWithTeam([], project.lead, project.team.members, messages)[0]; + + return syncProject({ + ...project, + updatedAt: messages.at(-1)?.createdAt || now(), + threads: replaceThread(project.threads.length > 0 ? project.threads : [leadThread], { + ...leadThread, + messages + }), + messages + }); +}; + +const appendLeadMessages = (project: Project, nextMessages: ChatMessage[], runtimeLine?: string): Project => { + const messages = [...project.messages, ...nextMessages]; + const lead = runtimeLine ? { ...project.lead, session: appendUniqueLine(project.lead.session, runtimeLine) } : project.lead; + + return updateLeadThread({ ...project, lead }, messages); +}; + +const appendToThread = (project: Project, threadId: string, nextMessages: ChatMessage[], runtimeLine?: string): Project => { + const thread = project.threads.find((item) => item.id === threadId); + if (!thread) { + return project; + } + + const threads = replaceThread(project.threads, { + ...thread, + status: runtimeLine ? "active" : thread.status, + messages: [...thread.messages, ...nextMessages] + }); + const nextProject = syncProject({ ...project, threads, updatedAt: nextMessages.at(-1)?.createdAt || now() }); + + if (thread.actorId && thread.runtimeType !== "lead") { + return { + ...nextProject, + team: { + ...nextProject.team, + members: nextProject.team.members.map((member) => + member.id === thread.actorId && runtimeLine + ? { ...member, session: appendUniqueLine(member.session, runtimeLine, member.session.status) } + : member + ) + } + }; + } + + return nextProject; +}; + +const updateMemberById = (project: Project, memberId: string, updater: (member: ProjectMember) => ProjectMember): Project => ({ + ...project, + team: { + ...project.team, + members: project.team.members.map((member) => (member.id === memberId ? updater(member) : member)) + } +}); + +const updateSessionById = (project: Project, sessionId: string, updater: (session: RuntimeSession) => RuntimeSession): Project => { + if (project.lead.session.id === sessionId) { + return { + ...project, + lead: { + ...project.lead, + session: updater(project.lead.session) + } + }; + } + + const member = project.team.members.find((item) => item.session.id === sessionId); + if (!member) { + return project; + } + + return updateMemberById(project, member.id, (current) => ({ + ...current, + session: updater(current.session) + })); +}; + +const sessionLabelById = (project: Project, sessionId: string): string => + project.lead.session.id === sessionId + ? project.lead.name + : project.team.members.find((member) => member.session.id === sessionId)?.displayName || "未知角色"; + +const officePresenceFromSession = (session: RuntimeSession): OfficeAgentState["presence"] => { + if (session.status === "running") { + const latest = session.recentOutput.at(-1) || ""; + if (/审|读|检查|摸底|分析|依赖|字段|边界/i.test(latest)) { + return "reading"; + } + return "typing"; + } + + if (session.status === "waiting" || session.status === "needs_confirmation") { + return "waiting"; + } + + if (session.status === "blocked" || session.identityState === "needs_rebuild") { + return "blocked"; + } + + if (session.status === "done") { + return "done"; + } + + return "idle"; +}; + +const leadRuleSummary = (project: Project): string => + [ + "负责理解需求、组队、派工和汇总。", + "重大变更需要回问用户。", + "默认不自动 commit / push。" + ].join(" "); + +const memberRuleSummary = (project: Project, member: ProjectMember): string => + member.runtimeType === "reviewer" + ? `${member.responsibility} reviewer 负责给出通过 / 打回结论,并保持审查闭环。` + : `${member.responsibility} 执行成员可直接和其他正式成员通过 mailbox 协作。`; + +interface OfficeAgentPlacement { + characterIndex: number | null; + characterUrl: string | null; + officeSlotId: string | null; + positionCol: number | null; + positionRow: number | null; +} + +const getOfficeActorId = (project: Project, thread: ConversationThread): string => + thread.kind === "lead" ? project.lead.id : thread.actorId || thread.id; + +const createOverflowOfficePosition = ( + overflowIndex: number, + cols: number | null, + rows: number | null +): Pick => { + const colSpan = 4; + const colOffset = overflowIndex % colSpan; + const rowOffset = Math.floor(overflowIndex / colSpan); + + if (cols && rows) { + return { + positionCol: Math.max(1, cols - 2 - colOffset * 2), + positionRow: Math.max(1, rows - 2 - rowOffset * 2) + }; + } + + return { + positionCol: 2 + colOffset * 2, + positionRow: 2 + rowOffset * 2 + }; +}; + +const buildProjectOfficeLayoutState = (project: Project, characterUrls: string[]) => { + const layoutTemplate = getPixelAgentsOfficeLayoutTemplate(); + const placementByActorId = new Map(); + const orderedActorIds = project.threads.map((thread) => getOfficeActorId(project, thread)); + + const deskSlots = layoutTemplate.deskSlots.map((slot, index) => { + const assignedActorId = orderedActorIds[index] || null; + + if (assignedActorId) { + placementByActorId.set(assignedActorId, { + characterIndex: characterUrls.length ? index % characterUrls.length : null, + characterUrl: characterUrls.length ? characterUrls[index % characterUrls.length] : null, + officeSlotId: slot.id, + positionCol: slot.col + Math.floor((slot.width - 1) / 2), + positionRow: slot.row + slot.height - 1 + }); + } + + return { + ...slot, + assignedActorId + }; + }); + + orderedActorIds.forEach((actorId, index) => { + if (placementByActorId.has(actorId)) { + return; + } + + const overflowIndex = Math.max(0, index - layoutTemplate.deskSlots.length); + const overflowPosition = createOverflowOfficePosition(overflowIndex, layoutTemplate.cols, layoutTemplate.rows); + placementByActorId.set(actorId, { + characterIndex: characterUrls.length ? index % characterUrls.length : null, + characterUrl: characterUrls.length ? characterUrls[index % characterUrls.length] : null, + officeSlotId: null, + positionCol: overflowPosition.positionCol, + positionRow: overflowPosition.positionRow + }); + }); + + return { + layout: { + cols: layoutTemplate.cols, + rows: layoutTemplate.rows, + deskSlots + }, + placementByActorId + }; +}; + +const buildOfficeAgentState = ( + project: Project, + thread: ConversationThread, + activeActorId: string | null, + placementByActorId: Map +): OfficeAgentState => { + if (thread.kind === "lead") { + const leadMailbox = project.mailbox.filter((message) => message.fromMemberId === project.lead.id || message.toMemberId === project.lead.id); + const leadTask = project.tasks.find((task) => task.status !== "done") || null; + const placement = placementByActorId.get(project.lead.id); + return { + actorId: project.lead.id, + threadId: thread.id, + label: project.lead.name, + title: "项目 lead", + runtimeType: "lead", + badge: "👑", + presence: officePresenceFromSession(project.lead.session), + sessionStatus: project.lead.session.status, + identityState: project.lead.session.identityState, + currentModel: project.lead.session.model, + contextUsagePercent: project.lead.session.contextUsagePercent, + estimatedContextTokens: project.lead.session.estimatedContextTokens, + characterIndex: placement?.characterIndex ?? null, + characterUrl: placement?.characterUrl ?? null, + officeSlotId: placement?.officeSlotId ?? null, + positionCol: placement?.positionCol ?? null, + positionRow: placement?.positionRow ?? null, + unresolvedMailboxCount: leadMailbox.filter((message) => message.status === "open").length, + activeTaskTitle: leadTask?.title || null, + latestMailboxSummary: leadMailbox[0]?.summary || null, + lastOutput: project.lead.session.recentOutput.at(-1) || "Lead 最近还没有新的运行输出。", + ruleSummary: leadRuleSummary(project), + detailSummary: "默认主窗口,负责对外汇总和推进主工作流。" + }; + } + + const member = project.team.members.find((item) => item.id === thread.actorId); + if (!member) { + const placeholderActorId = thread.actorId || thread.id; + const placement = placementByActorId.get(placeholderActorId); + return { + actorId: placeholderActorId, + threadId: thread.id, + label: thread.label, + title: thread.subtitle, + runtimeType: "worker", + badge: "🤖", + presence: "idle", + sessionStatus: "idle", + identityState: "fresh", + currentModel: project.codex.defaultModel, + contextUsagePercent: 0, + estimatedContextTokens: 0, + characterIndex: placement?.characterIndex ?? null, + characterUrl: placement?.characterUrl ?? null, + officeSlotId: placement?.officeSlotId ?? null, + positionCol: placement?.positionCol ?? null, + positionRow: placement?.positionRow ?? null, + unresolvedMailboxCount: 0, + activeTaskTitle: null, + latestMailboxSummary: null, + lastOutput: "这个工位当前还没有接上正式成员会话。", + ruleSummary: "当前还是占位工位。", + detailSummary: "正式团队确认后,这里才会进入真实成员详情。" + }; + } + + const mailbox = project.mailbox.filter((message) => message.fromMemberId === member.id || message.toMemberId === member.id); + const activeTask = project.tasks.find((task) => task.ownerMemberId === member.id && task.status !== "done") || null; + const placement = placementByActorId.get(member.id); + return { + actorId: member.id, + threadId: thread.id, + label: member.displayName, + title: member.title, + runtimeType: member.runtimeType, + badge: member.runtimeType === "reviewer" ? "👀" : member.capabilityHint === "frontend" ? "🎨" : member.capabilityHint === "backend" ? "💻" : member.capabilityHint === "qa" ? "🧪" : "🤖", + presence: officePresenceFromSession(member.session), + sessionStatus: member.session.status, + identityState: member.session.identityState, + currentModel: member.session.model, + contextUsagePercent: member.session.contextUsagePercent, + estimatedContextTokens: member.session.estimatedContextTokens, + characterIndex: placement?.characterIndex ?? null, + characterUrl: placement?.characterUrl ?? null, + officeSlotId: placement?.officeSlotId ?? null, + positionCol: placement?.positionCol ?? null, + positionRow: placement?.positionRow ?? null, + unresolvedMailboxCount: mailbox.filter((message) => message.status === "open").length, + activeTaskTitle: activeTask?.title || null, + latestMailboxSummary: mailbox[0]?.summary || null, + lastOutput: member.session.recentOutput.at(-1) || `${member.displayName} 最近还没有新的运行输出。`, + ruleSummary: memberRuleSummary(project, member), + detailSummary: activeActorId === member.id ? "当前正在查看这个角色的线程和运行细节。" : "点击这个工位后,前端可以切到该角色的详细线程。" + }; +}; + +const rebuildRoleSession = (project: Project, sessionId: string): Project => { + const roleLabel = sessionLabelById(project, sessionId); + const currentSession = + project.lead.session.id === sessionId + ? project.lead.session + : project.team.members.find((member) => member.session.id === sessionId)?.session; + + if (!currentSession) { + return project; + } + + return syncProject( + updateSessionById(project, sessionId, () => + updateRuntimeSession(createRuntimeSession(roleLabel, project.path, currentSession.model, [`${roleLabel} 的运行日志会出现在这里。`]), { + model: currentSession.model, + modelSource: currentSession.modelSource, + appendOutput: [`已重建 ${roleLabel} 的角色会话身份,下一次真实交互后会建立新的原生 thread。`] + }) + ) + ); +}; + +const restoreRoleSession = (project: Project, sessionId: string): Project => { + const roleLabel = sessionLabelById(project, sessionId); + const currentSession = + project.lead.session.id === sessionId + ? project.lead.session + : project.team.members.find((member) => member.session.id === sessionId)?.session; + + if (!currentSession) { + return project; + } + + return syncProject( + updateSessionById(project, sessionId, (session) => + updateRuntimeSession(session, { + status: session.status, + appendOutput: [ + session.lastThreadId + ? `已按最近 thread 恢复 ${roleLabel} 的原生接管入口。` + : `${roleLabel} 当前还没有可恢复的原生 thread,下一次真实交互后会自动建立。` + ] + }) + ) + ); +}; + +const appendMailbox = (project: Project, messages: MailboxMessage[]): Project => ({ + ...project, + mailbox: [...messages, ...project.mailbox] +}); + +const appendTaskActivity = (project: Project, entries: TaskActivityEntry[]): Project => ({ + ...project, + taskActivity: [...entries, ...project.taskActivity] +}); + +const appendSharedMemory = (project: Project, entries: SharedMemoryEntry[]): Project => ({ + ...project, + sharedProjectMemory: [...entries, ...project.sharedProjectMemory].slice(0, 12) +}); + +const memberLabelById = (project: Project, memberId: string | null): string => { + if (!memberId) { + return "系统"; + } + + if (memberId === project.lead.id) { + return project.lead.name; + } + + return project.team.members.find((member) => member.id === memberId)?.displayName || "未知成员"; +}; + +const mailboxKindLabelForLead = (kind: MailboxMessageCreateInput["kind"]): string => { + switch (kind) { + case "dependency_request": + return "依赖请求"; + case "blocker": + return "阻塞提醒"; + case "review_feedback": + return "审查反馈"; + case "resubmit_request": + return "复审请求"; + case "handoff": + return "任务交接"; + case "broadcast": + return "广播"; + default: + return "队内消息"; + } +}; + +const findReviewer = (project: Project): ProjectMember | undefined => project.team.members.find((member) => member.runtimeType === "reviewer"); + +const findDefaultWorker = (project: Project): ProjectMember | undefined => + project.team.members.find((member) => member.runtimeType === "worker"); + +const buildTeamProposalFallback = (project: Project, content: string): TeamProposal => { + const roles: ProposalRole[] = []; + + roles.push( + createProposalRole({ + displayName: "项目 reviewer", + title: "风险审查成员", + runtimeType: "reviewer", + capabilityHint: "review", + responsibility: "负责把关风险、提出打回意见、给出复审结论。", + reason: "reviewer 是当前 v1 固定核心岗位,用来保证主工作流有真审查。" + }) + ); + + if (/前端|页面|ui|界面|react|vue/i.test(content) || project.discovery.stack.some((item) => /React|Vue|Next/.test(item))) { + roles.push( + createProposalRole({ + displayName: "界面推进成员", + title: "界面推进成员", + runtimeType: "worker", + capabilityHint: "frontend", + responsibility: "负责主工作台、页面结构、交互和视觉细节。", + reason: "当前讨论里明显提到了界面或前端工作。" + }) + ); + } + + if (/后端|api|服务|server|node|express/i.test(content) || project.discovery.stack.some((item) => /Express|Node/.test(item))) { + roles.push( + createProposalRole({ + displayName: "流程推进成员", + title: "流程推进成员", + runtimeType: "worker", + capabilityHint: "backend", + responsibility: "负责 API、数据流、工作流状态和本地服务层。", + reason: "当前讨论里有后端或流程实现诉求。" + }) + ); + } + + if (/测试|验收|qa/i.test(content)) { + roles.push( + createProposalRole({ + displayName: "验收协同成员", + title: "验收协同成员", + runtimeType: "worker", + capabilityHint: "qa", + responsibility: "负责冒烟验证、回归检查和交付前确认。", + reason: "当前讨论里希望把验收工作前置进团队草案。" + }) + ); + } + + if (roles.filter((role) => role.runtimeType === "worker").length === 0) { + roles.push( + createProposalRole({ + displayName: "主实现成员", + title: "主实现成员", + runtimeType: "worker", + capabilityHint: inferCapabilityHint(content), + responsibility: "负责把 lead 当前确认的第一批任务真正落下来。", + reason: "先用一个通用执行成员把主链路跑通最稳。" + }) + ); + } + + return { + ...project.teamProposal, + summary: "这是 lead 根据当前项目和你刚刚的要求整理出来的项目专属团队草案。", + naturalLanguageBrief: + "我先把 reviewer 固定下来,其它岗位按当前项目的真实需要来组。你确认后,才会真正创建这些成员会话。", + status: roles.length > 0 ? "pending_confirmation" : "draft", + changeMode: project.team.status === "active" ? "update" : "initial", + roles: ensureReviewerRole(roles), + updatedAt: now() + }; +}; + +const teamProposalSchema = { + type: "object", + properties: { + summary: { type: "string" }, + naturalLanguageBrief: { type: "string" }, + roles: { + type: "array", + items: { + type: "object", + properties: { + displayName: { type: "string" }, + title: { type: "string" }, + runtimeType: { type: "string", enum: ["reviewer", "worker"] }, + capabilityHint: { type: "string", enum: ["review", "frontend", "backend", "qa", "custom"] }, + responsibility: { type: "string" }, + reason: { type: "string" } + }, + required: ["displayName", "title", "runtimeType", "capabilityHint", "responsibility", "reason"], + additionalProperties: false + } + } + }, + required: ["summary", "naturalLanguageBrief", "roles"], + additionalProperties: false +} as const; + +const buildTeamProposalPrompt = (project: Project, content: string): string => ` +你是一个“项目级 lead 工作台”里的项目 lead。 +请直接根据下面上下文,输出一份团队草案 JSON。 + +硬要求: +1. 团队是项目级的,不是全局固定团队。 +2. reviewer 是固定核心岗位,必须保留一个 reviewer。 +3. 其他岗位走半自由岗位,但 v1 先单岗单人,不要同类岗位重复堆很多人。 +4. 默认优先少而精,通常 2 到 4 个岗位就够。 +5. 面向非程序员,summary 和 naturalLanguageBrief 必须是中文白话。 +6. 不要输出 markdown,不要输出解释,只输出符合 schema 的 JSON。 +7. 如果用户说得还很宽泛,也至少给出“reviewer + 1 个最合适的执行成员”。 + +项目上下文: +${projectContextSummary(project)} + +用户这次关于组队的表达: +${content} + +请输出团队草案 JSON: +`.trim(); + +const buildTeamProposalFromCodex = async (project: Project, content: string): Promise => { + const fallback = buildTeamProposalFallback(project, content); + + try { + const result = await runCodexReadOnlyStructuredPrompt<{ + summary: string; + naturalLanguageBrief: string; + roles: Array<{ + displayName: string; + title: string; + runtimeType: "reviewer" | "worker"; + capabilityHint: ProposalRole["capabilityHint"]; + responsibility: string; + reason: string; + }>; + }>({ + projectPath: project.path, + model: project.lead.session.model, + resumeSessionId: project.lead.session.lastThreadId || undefined, + prompt: buildTeamProposalPrompt(project, content), + schema: teamProposalSchema + }); + + const roles = ensureReviewerRole( + result.data.roles.map((role) => + createProposalRole({ + displayName: role.displayName, + title: role.title, + runtimeType: role.runtimeType, + capabilityHint: role.capabilityHint, + responsibility: role.responsibility, + reason: role.reason + }) + ) + ); + + return { + ...project.teamProposal, + summary: result.data.summary || fallback.summary, + naturalLanguageBrief: result.data.naturalLanguageBrief || fallback.naturalLanguageBrief, + status: roles.length > 0 ? "pending_confirmation" : "draft", + changeMode: project.team.status === "active" ? "update" : "initial", + roles, + updatedAt: now() + }; + } catch { + return fallback; + } +}; + +const saveTeamProposalState = (project: Project, payload: TeamProposalUpdateInput): Project => { + const updatedAt = now(); + const nextChangeMode = project.team.status === "active" ? "update" : "initial"; + const nextProposal: TeamProposal = { + ...project.teamProposal, + summary: payload.summary, + naturalLanguageBrief: payload.naturalLanguageBrief, + reuseFirst: payload.reuseFirst, + reviewerRule: payload.reviewerRule, + leadOverrideRule: payload.leadOverrideRule, + userOverrideRule: payload.userOverrideRule, + roles: ensureReviewerRole(payload.roles.map((role) => normalizeProposalRole(role))), + status: payload.roles.length > 0 ? "pending_confirmation" : "draft", + changeMode: nextChangeMode, + updatedAt + }; + + const leadMessage = createLeadMessage( + nextProposal.roles.length > 0 + ? nextChangeMode === "update" + ? "我已经把这次团队变更草案整理好了。你确认后,才会真正调整正式团队和角色会话。" + : "我已经把团队草案整理好了。你看完后确认团队,就会真正创建这些成员会话。" + : "我先把团队草案收回到讨论态了。你可以继续告诉我需要哪些岗位。", + project.lead.name + ); + + return appendLeadMessages( + { + ...project, + updatedAt, + teamProposal: nextProposal + }, + [leadMessage], + "Lead 已更新团队草案。" + ); +}; + +const confirmTeamChangeState = (project: Project): Project => { + if (project.team.status !== "active") { + return confirmTeamProposalState(project); + } + + if (!project.teamChange.pendingConfirmation) { + return appendLeadMessages( + project, + [createLeadMessage("当前没有待确认的团队变更。我会继续沿用现在这支正式团队。", project.lead.name)], + "Lead 检查了团队变更状态。" + ); + } + + return confirmTeamProposalState(project); +}; + +const confirmTeamProposalState = (project: Project): Project => { + const archivedPool = [...project.team.archivedMembers, ...project.teamProposal.archivedMembers]; + const roles = ensureReviewerRole(project.teamProposal.roles); + const nextMembers = roles.map((role) => materializeMember(role, project.path, archivedPool, project.codex.defaultModel)); + const archivedMembers = [ + ...project.team.archivedMembers, + ...project.team.members.filter((member) => !nextMembers.some((nextMember) => nextMember.id === member.id)) + ]; + const confirmedAt = now(); + + const nextProject = syncProject({ + ...project, + updatedAt: confirmedAt, + teamProposal: { + ...project.teamProposal, + status: "confirmed", + updatedAt: confirmedAt + }, + team: { + status: "active", + members: nextMembers, + archivedMembers, + confirmedAt, + updatedAt: confirmedAt, + lastChangeSummary: + project.team.status === "active" ? "已按新的项目草案完成团队调整。" : "已按团队草案正式创建项目专属团队。" + } + }); + + const withMemory = appendSharedMemory(nextProject, [ + createSharedMemoryEntry({ + title: "正式团队已创建", + body: `当前项目团队已确认,共有 ${nextMembers.length} 个正式角色:${nextMembers.map((member) => member.displayName).join("、")}。`, + source: "lead" + }) + ]); + + return appendLeadMessages( + appendTaskActivity(withMemory, [ + createTaskActivityEntry({ + kind: "task_started", + actorId: project.lead.id, + actorLabel: project.lead.name, + summary: "团队已确认并建队", + detail: "真实成员线程、原生会话和运行态已经挂到项目工作台。" + }) + ]), + [ + createSystemMessage("团队已确认,真实成员线程和运行态已经挂到项目工作台。"), + createLeadMessage("团队已经建好了。接下来你可以继续让我先摸底,或者直接让我派发第一条任务。", project.lead.name) + ], + "团队确认完成,成员会话已创建。" + ); +}; + +const requestDiscoveryState = (project: Project, note: string): Project => { + const requestedAt = now(); + + return appendLeadMessages( + { + ...project, + updatedAt: requestedAt, + discovery: { + ...project.discovery, + status: "pending_confirmation", + requestNote: note, + requestedAt + } + }, + [createSystemMessage("已创建项目摸底请求,只有你确认后才会真正开始。")], + "Lead 已准备好项目摸底请求,等待用户确认。" + ); +}; + +const applyDiscoveryResultState = (project: Project, analysis: Awaited>): Project => { + const confirmedAt = now(); + const proposal = + project.team.status === "active" + ? project.teamProposal + : { + ...project.teamProposal, + summary: "这是基于真实目录摸底后整理的一版项目专属团队草案。", + naturalLanguageBrief: + "我已经按真实目录信息补全了一版更贴近项目现状的团队建议。你确认后,才会真正创建这些成员会话。", + status: "pending_confirmation" as const, + changeMode: "initial" as const, + roles: ensureReviewerRole(analysis.suggestedRoles), + updatedAt: confirmedAt + }; + + const discoveredProject = syncProject({ + ...project, + updatedAt: confirmedAt, + discovery: { + status: "completed", + requiresUserConfirmation: true, + requestNote: project.discovery.requestNote, + requestedAt: project.discovery.requestedAt || confirmedAt, + confirmedAt, + lastBrief: analysis.brief, + facts: analysis.facts, + stack: analysis.stack, + topLevelEntries: analysis.topLevelEntries + }, + teamProposal: proposal + }); + + return appendLeadMessages( + appendSharedMemory(discoveredProject, [ + createSharedMemoryEntry({ + title: "项目摸底摘要", + body: analysis.brief, + source: "lead" + }) + ]), + [createLeadMessage(`${analysis.brief} 我已经顺手给出一版更贴近当前项目的团队建议。`, project.lead.name)], + "Lead 已完成项目摸底,并更新了团队建议。" + ); +}; + +const createTaskState = async (project: Project, input: TaskCreateInput): Promise => { + const owner = project.team.members.find((member) => member.id === input.ownerMemberId) || findDefaultWorker(project) || findReviewer(project); + if (!owner) { + return appendLeadMessages(project, [createLeadMessage("现在还没有可用成员,先确认团队后我才能派任务。", project.lead.name)]); + } + + const createdAt = now(); + const task: TaskItem = { + id: `task_${createdAt}`, + title: input.title, + summary: input.summary, + status: "in_progress", + ownerMemberId: owner.id, + ownerName: owner.displayName, + createdBy: project.lead.name, + reviewState: "not_requested", + currentReviewId: null, + updatedAt: createdAt + }; + + const nextProject = updateMemberById( + { + ...project, + updatedAt: createdAt, + tasks: [task, ...project.tasks] + }, + owner.id, + (member) => ({ + ...member, + lifecycle: "working", + lastUsedAt: createdAt, + session: appendUniqueLine(member.session, `${task.title} 已派发到 ${member.displayName}。`, "running") + }) + ); + + const mailbox = createMailboxMessage({ + fromMemberId: project.lead.id, + fromLabel: project.lead.name, + toMemberId: owner.id, + toLabel: owner.displayName, + kind: "handoff", + relatedTaskId: task.id, + summary: `Lead 已把「${task.title}」派给 ${owner.displayName}`, + body: task.summary + }); + + const kickoff = await generateWorkerKickoffWithCodex(nextProject, owner, task); + + return appendLeadMessages( + appendTaskActivity( + appendMailbox( + appendToThread( + updateMemberById(nextProject, owner.id, (member) => (member.id === owner.id ? { ...member, session: kickoff.session } : member)), + owner.threadId, + [createMemberMessage(kickoff.message, owner.displayName)], + `${owner.displayName} 已开始执行 ${task.title}。` + ), + [mailbox] + ), + [ + createTaskActivityEntry({ + taskId: task.id, + kind: "task_created", + actorId: project.lead.id, + actorLabel: project.lead.name, + summary: `创建任务:${task.title}`, + detail: task.summary + }), + createTaskActivityEntry({ + taskId: task.id, + kind: "task_assigned", + actorId: owner.id, + actorLabel: owner.displayName, + summary: `任务已分派给 ${owner.displayName}`, + detail: task.summary, + relatedMailboxId: mailbox.id + }), + createTaskActivityEntry({ + taskId: task.id, + kind: "task_started", + actorId: owner.id, + actorLabel: owner.displayName, + summary: `${owner.displayName} 已开始执行`, + detail: `开始推进 ${task.title}` + }) + ] + ), + [createLeadMessage(`我已经把「${task.title}」派给 ${owner.displayName} 了。等它回执后,我会安排 reviewer 介入。`, project.lead.name)], + `Lead 已派发任务:${task.title}` + ); +}; + +const submitTaskForReviewState = async (project: Project, taskId: string, note: string): Promise => { + const task = project.tasks.find((item) => item.id === taskId); + if (!task) { + return project; + } + + const reviewer = findReviewer(project); + if (!reviewer) { + return appendLeadMessages(project, [createLeadMessage("当前团队里还没有 reviewer,我没法把这条任务送审。", project.lead.name)]); + } + + const submittedAt = now(); + const review: ReviewItem = { + id: `review_${submittedAt}`, + taskId, + reviewerMemberId: reviewer.id, + status: "requested", + note, + createdAt: submittedAt + }; + + let nextProject = { + ...project, + updatedAt: submittedAt, + tasks: project.tasks.map((item) => + item.id === taskId + ? { + ...item, + status: "in_review" as const, + reviewState: "requested" as const, + currentReviewId: review.id, + updatedAt: submittedAt + } + : item + ), + reviews: [review, ...project.reviews] + }; + + if (task.ownerMemberId) { + nextProject = updateMemberById(nextProject, task.ownerMemberId, (member) => ({ + ...member, + lifecycle: "waiting_review", + lastUsedAt: submittedAt, + session: appendUniqueLine(member.session, `${task.title} 已提交 reviewer 审查。`, "waiting") + })); + } + + nextProject = updateMemberById(nextProject, reviewer.id, (member) => ({ + ...member, + lifecycle: "working", + lastUsedAt: submittedAt, + session: appendUniqueLine(member.session, `${task.title} 已进入 reviewer 审查队列。`, "running") + })); + + const reviewerQueue = await generateReviewerQueueWithCodex(nextProject, reviewer, task, note); + nextProject = appendToThread( + updateMemberById(nextProject, reviewer.id, (member) => (member.id === reviewer.id ? { ...member, session: reviewerQueue.session } : member)), + reviewer.threadId, + [createMemberMessage(reviewerQueue.message, reviewer.displayName)], + `${reviewer.displayName} 正在审查 ${task.title}。` + ); + + const ownerLabel = task.ownerMemberId ? memberLabelById(project, task.ownerMemberId) : project.lead.name; + const mailbox = createMailboxMessage({ + fromMemberId: task.ownerMemberId, + fromLabel: ownerLabel, + toMemberId: reviewer.id, + toLabel: reviewer.displayName, + kind: "resubmit_request", + relatedTaskId: task.id, + summary: `${ownerLabel} 已提交「${task.title}」请 reviewer 审查`, + body: note + }); + + return appendLeadMessages( + appendTaskActivity( + appendMailbox(nextProject, [mailbox]), + [ + createTaskActivityEntry({ + taskId: task.id, + kind: "task_submitted", + actorId: task.ownerMemberId, + actorLabel: ownerLabel, + summary: `${task.title} 已送审`, + detail: note, + relatedMailboxId: mailbox.id + }) + ] + ), + [createLeadMessage(`我已经把「${task.title}」送给 reviewer 了。下一步要么通过,要么打回。`, project.lead.name)], + `Lead 已把任务送审:${task.title}` + ); +}; + +const reviewTaskState = async (project: Project, taskId: string, input: ReviewDecisionInput): Promise => { + const task = project.tasks.find((item) => item.id === taskId); + if (!task) { + return project; + } + + const reviewer = findReviewer(project); + const reviewedAt = now(); + const decisionStatus = input.decision === "approve" ? "approved" : "changes_requested"; + + let nextProject: Project = { + ...project, + updatedAt: reviewedAt, + tasks: project.tasks.map((item) => + item.id === taskId + ? { + ...item, + status: (input.decision === "approve" ? "done" : "changes_requested") as TaskItem["status"], + reviewState: decisionStatus as TaskItem["reviewState"], + updatedAt: reviewedAt + } + : item + ), + reviews: project.reviews.map((review) => + review.id === task.currentReviewId + ? { + ...review, + status: decisionStatus, + note: input.note, + createdAt: reviewedAt + } + : review + ) + }; + + if (reviewer) { + const reviewerDecision = await generateReviewerDecisionWithCodex(nextProject, reviewer, task, input); + nextProject = updateMemberById(nextProject, reviewer.id, (member) => ({ + ...member, + lifecycle: "idle", + lastUsedAt: reviewedAt, + session: reviewerDecision.session + })); + + nextProject = appendToThread( + nextProject, + reviewer.threadId, + [ + createMemberMessage( + reviewerDecision.message, + reviewer.displayName + ) + ], + input.decision === "approve" ? `${reviewer.displayName} 已通过 ${task.title}。` : `${reviewer.displayName} 已打回 ${task.title}。` + ); + } + + if (task.ownerMemberId) { + nextProject = updateMemberById(nextProject, task.ownerMemberId, (member) => ({ + ...member, + lifecycle: input.decision === "approve" ? "done" : "blocked", + lastUsedAt: reviewedAt, + session: appendUniqueLine( + member.session, + input.decision === "approve" ? `${task.title} 已通过 reviewer 审查。` : `${task.title} 被 reviewer 打回,需要修复。`, + input.decision === "approve" ? "done" : "blocked" + ) + })); + } + + const ownerLabel = task.ownerMemberId ? memberLabelById(project, task.ownerMemberId) : "未指定成员"; + const mailbox = task.ownerMemberId + ? createMailboxMessage({ + fromMemberId: reviewer?.id || null, + fromLabel: reviewer?.displayName || "项目 reviewer", + toMemberId: task.ownerMemberId, + toLabel: ownerLabel, + kind: "review_feedback", + relatedTaskId: task.id, + summary: + input.decision === "approve" + ? `reviewer 已通过「${task.title}」` + : `reviewer 已打回「${task.title}」`, + body: input.note + }) + : null; + + return appendLeadMessages( + appendTaskActivity( + mailbox ? appendMailbox(nextProject, [mailbox]) : nextProject, + [ + createTaskActivityEntry({ + taskId: task.id, + kind: input.decision === "approve" ? "task_done" : "task_blocked", + actorId: reviewer?.id || null, + actorLabel: reviewer?.displayName || "项目 reviewer", + summary: input.decision === "approve" ? `${task.title} 已通过审查` : `${task.title} 已被打回`, + detail: input.note, + relatedMailboxId: mailbox?.id || null + }) + ] + ), + [ + createLeadMessage( + input.decision === "approve" + ? `reviewer 已通过「${task.title}」。如果没有下一条任务,这轮主链路就算走完了。` + : `reviewer 已打回「${task.title}」。我会让对应成员修复后再送审。`, + project.lead.name + ) + ], + input.decision === "approve" ? `Lead 已收到任务通过:${task.title}` : `Lead 已收到任务打回:${task.title}` + ); +}; + +const resubmitTaskState = async (project: Project, taskId: string, note: string): Promise => { + const task = project.tasks.find((item) => item.id === taskId); + if (!task) { + return project; + } + + const owner = project.team.members.find((member) => member.id === task.ownerMemberId); + let nextProject = project; + + if (owner) { + const resubmission = await generateWorkerResubmitWithCodex(project, owner, task, note); + + nextProject = updateMemberById(project, owner.id, (member) => ({ + ...member, + lifecycle: "working", + lastUsedAt: now(), + session: resubmission.session + })); + + nextProject = appendToThread( + nextProject, + owner.threadId, + [createMemberMessage(resubmission.message, owner.displayName)], + `${owner.displayName} 正在返工 ${task.title}。` + ); + } + nextProject = appendTaskActivity(nextProject, [ + createTaskActivityEntry({ + taskId: task.id, + kind: "task_resumed", + actorId: owner?.id || null, + actorLabel: owner?.displayName || "项目成员", + summary: `${task.title} 已重新进入返工`, + detail: note + }) + ]); + + return submitTaskForReviewState(nextProject, taskId, note); +}; + +const executionIntentPattern = /流程|闭环|跑通|自测|测试|验证|冒烟|试一下|试一轮|检查能不能|先自己做|联调/i; +const activeTeamChangeIntentPattern = /重组团队|调整团队|团队变更|改团队|重新组队|补(一个|一下)?(成员|岗位|角色)|新增(成员|岗位|角色)|增加(成员|岗位|角色)|移除(成员|岗位|角色)|删除(成员|岗位|角色)|换(成员|岗位|角色)|加个(review(er)?|成员|岗位|角色)/i; +const teamPlanningIntentPattern = /团队|组队|岗位|reviewer|成员|前端|后端/i; + +const buildWorkflowSmokeTestSummary = (project: Project, taskId: string): string => { + const task = project.tasks.find((item) => item.id === taskId); + const mailboxCount = project.mailbox.filter((message) => message.relatedTaskId === taskId).length; + const activityCount = project.taskActivity.filter((entry) => entry.taskId === taskId).length; + const owner = task?.ownerMemberId ? memberLabelById(project, task.ownerMemberId) : "未指定成员"; + + if (!task) { + return "我已经尝试跑主流程自测,但这条测试任务没有成功落进台账,还需要继续修。"; + } + + return [ + `我已经把「${task.title}」这条自测任务完整跑了一遍。`, + `负责人是 ${owner},最终状态是 ${task.status}。`, + `这一轮共记录了 ${mailboxCount} 条关联 mailbox、${activityCount} 条任务活动。`, + "也就是说,任务派发、reviewer 打回、成员复审、最终通过这条闭环现在已经真正落到台账里了。" + ].join(""); +}; + +const runWorkflowSmokeTestState = async (project: Project): Promise => { + if (project.team.status !== "active") { + return appendLeadMessages(project, [createLeadMessage("团队还没正式确认,我现在还不能跑完整协作闭环。先确认团队后我再自测。", project.lead.name)]); + } + + const worker = findDefaultWorker(project); + const reviewer = findReviewer(project); + + if (!worker || !reviewer) { + return appendLeadMessages( + project, + [createLeadMessage("当前团队角色还不够完整,至少要有 1 个执行成员和 1 个 reviewer,我才能跑完整闭环。", project.lead.name)], + "Lead 检查了自测前置条件。" + ); + } + + const existingTask = project.tasks.find((task) => task.title.includes("项目协作闭环自测") && task.status !== "done"); + if (existingTask) { + return appendLeadMessages( + project, + [createLeadMessage(`我已经有一条正在推进的闭环自测任务:「${existingTask.title}」。我会继续沿着它往下走。`, project.lead.name)], + "Lead 检查到已有闭环自测任务。" + ); + } + + const title = `项目协作闭环自测 ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}`; + const summary = "验证项目级 Lead 团队主流程是否真的跑通:Lead 派发任务,执行成员承接,reviewer 打回与复审,mailbox 留痕,最终结果回到 Lead。"; + + let nextProject = await createTaskState(project, { + title, + summary, + ownerMemberId: worker.id + }); + const createdTask = nextProject.tasks.find((task) => task.title === title); + + if (!createdTask) { + return appendLeadMessages( + nextProject, + [createLeadMessage("闭环自测任务没有成功创建,我会继续检查任务落库环节。", project.lead.name)], + "Lead 自测时发现任务创建未落库。" + ); + } + + nextProject = await submitTaskForReviewState(nextProject, createdTask.id, "第一轮自测结果已提交,请 reviewer 检查协作链路。"); + nextProject = await reviewTaskState(nextProject, createdTask.id, { + decision: "changes_requested", + note: "先模拟一次 reviewer 打回,验证返工、消息回传和复审请求链路。" + }); + nextProject = await resubmitTaskState(nextProject, createdTask.id, "已按 reviewer 意见补齐闭环自测记录,重新提交审查。"); + nextProject = await reviewTaskState(nextProject, createdTask.id, { + decision: "approve", + note: "复审通过,主流程自测闭环已完成。" + }); + + return appendLeadMessages( + nextProject, + [createLeadMessage(buildWorkflowSmokeTestSummary(nextProject, createdTask.id), project.lead.name)], + `Lead 已完成闭环自测:${createdTask.title}` + ); +}; + +const saveManualActionState = (project: Project, action: string, note: string, targetMemberId: string | null): Project => { + const targetMember = project.team.members.find((member) => member.id === targetMemberId); + const manualAction = createManualAction(action, note, targetMember?.id || null, targetMember?.displayName || null); + let nextProject: Project = { + ...project, + updatedAt: manualAction.createdAt, + manualActions: [manualAction, ...project.manualActions] + }; + + if (action === "pause_member" && targetMember) { + nextProject = updateMemberById(nextProject, targetMember.id, (member) => ({ + ...member, + lifecycle: "paused", + session: appendUniqueLine(member.session, "用户人工接管:已暂停该成员。", "blocked") + })); + } + + if (action === "retry_member" && targetMember) { + nextProject = updateMemberById(nextProject, targetMember.id, (member) => ({ + ...member, + lifecycle: "idle", + session: appendUniqueLine(member.session, "用户人工接管:已允许该成员重新推进。", "idle") + })); + } + + if (action === "kick_member" && targetMember) { + nextProject = syncProject({ + ...nextProject, + team: { + ...nextProject.team, + members: nextProject.team.members.filter((member) => member.id !== targetMember.id), + archivedMembers: [{ ...targetMember, lifecycle: "paused" }, ...nextProject.team.archivedMembers], + updatedAt: manualAction.createdAt, + lastChangeSummary: `${targetMember.displayName} 已被人工移出当前团队。` + }, + teamProposal: { + ...nextProject.teamProposal, + changeMode: "update", + status: "draft", + roles: nextProject.teamProposal.roles.filter((role) => role.displayName !== targetMember.displayName), + updatedAt: manualAction.createdAt + } + }); + } + + if (action === "request_reteam") { + nextProject = syncProject({ + ...nextProject, + teamProposal: { + ...nextProject.teamProposal, + changeMode: "update", + status: "draft", + roles: nextProject.team.members.map((member) => + createProposalRole({ + displayName: member.displayName, + title: member.title, + runtimeType: member.runtimeType, + capabilityHint: member.capabilityHint, + responsibility: member.responsibility, + reason: "这是从当前团队回填出来的变更草案。", + source: "existing" + }) + ), + updatedAt: manualAction.createdAt + } + }); + } + + if (action === "rebuild_lead") { + nextProject = syncProject({ + ...nextProject, + lead: { + ...nextProject.lead, + status: "handover_pending", + session: appendUniqueLine(nextProject.lead.session, "用户已发起 lead 重建,需要先做交接。", "blocked") + } + }); + } + + return appendLeadMessages( + appendTaskActivity(nextProject, [ + createTaskActivityEntry({ + kind: "mailbox_linked", + actorId: targetMemberId, + actorLabel: targetMember?.displayName || "系统", + summary: `已记录人工接管:${action}`, + detail: note || "暂无补充说明。" + }) + ]), + [createSystemMessage(`已记录人工接管:${action}。${note || "暂无补充说明。"}`)], + `已记录人工接管动作:${action}` + ); +}; + +const sendMailboxMessageState = (project: Project, payload: MailboxMessageCreateInput): Project => { + const fromMember = project.team.members.find((member) => member.id === payload.fromMemberId); + const toMember = project.team.members.find((member) => member.id === payload.toMemberId); + + if (!fromMember || !toMember) { + return project; + } + + const mailbox = createMailboxMessage({ + fromMemberId: fromMember.id, + fromLabel: fromMember.displayName, + toMemberId: toMember.id, + toLabel: toMember.displayName, + kind: payload.kind, + relatedTaskId: payload.relatedTaskId, + summary: payload.summary, + body: payload.body + }); + + const withMailbox = appendMailbox(project, [mailbox]); + const withActivity = appendTaskActivity(withMailbox, [ + createTaskActivityEntry({ + taskId: payload.relatedTaskId, + kind: "mailbox_linked", + actorId: fromMember.id, + actorLabel: fromMember.displayName, + summary: `${fromMember.displayName} -> ${toMember.displayName}`, + detail: payload.summary, + relatedMailboxId: mailbox.id + }) + ]); + + const fromThread = fromMember.threadId; + const toThread = toMember.threadId; + const messageBody = `${payload.summary}${payload.body ? `\n${payload.body}` : ""}`; + + const withThreads = appendToThread( + appendToThread( + withActivity, + fromThread, + [createMemberMessage(`我刚发给 ${toMember.displayName} 一条消息:${messageBody}`, fromMember.displayName)], + `${fromMember.displayName} 已向 ${toMember.displayName} 发送协作消息。` + ), + toThread, + [createMemberMessage(`${fromMember.displayName} 发来一条消息:${messageBody}`, toMember.displayName)], + `${toMember.displayName} 收到了来自 ${fromMember.displayName} 的协作消息。` + ); + + const shouldSummarize = + payload.kind === "dependency_request" || + payload.kind === "blocker" || + payload.kind === "review_feedback" || + payload.kind === "resubmit_request"; + + return shouldSummarize + ? appendLeadMessages( + withThreads, + [ + createLeadMessage( + `${fromMember.displayName} 刚刚给 ${toMember.displayName} 发了一条${mailboxKindLabelForLead(payload.kind)}:${payload.summary}`, + project.lead.name + ) + ], + "Lead 已汇总一条关键队内协作消息。" + ) + : withThreads; +}; + +const updateMailboxStatusState = (project: Project, mailboxId: string, status: MailboxStatusUpdateInput["status"]): Project => ({ + ...appendTaskActivity( + { + ...project, + mailbox: project.mailbox.map((message) => (message.id === mailboxId ? { ...message, status } : message)) + }, + [ + createTaskActivityEntry({ + taskId: project.mailbox.find((message) => message.id === mailboxId)?.relatedTaskId || null, + kind: "mailbox_linked", + actorId: null, + actorLabel: "系统", + summary: `mailbox 已更新为 ${status}`, + detail: mailboxId, + relatedMailboxId: mailboxId + }) + ] + ) +}); + +const addSharedMemoryEntryState = (project: Project, title: string, body: string): Project => + appendTaskActivity( + appendSharedMemory(project, [ + createSharedMemoryEntry({ + title, + body, + source: "lead" + }) + ]), + [ + createTaskActivityEntry({ + kind: "mailbox_linked", + actorId: project.lead.id, + actorLabel: project.lead.name, + summary: `已写入共享记忆:${title}`, + detail: body + }) + ] + ); + +const buildMemberReply = (project: Project, thread: ConversationThread, content: string): string => { + const member = project.team.members.find((item) => item.id === thread.actorId); + if (!member) { + return "这个成员当前还没有挂上真实运行态,你可以先回到 lead 主对话继续推进。"; + } + + if (member.runtimeType === "reviewer") { + return "我会继续盯风险、回归范围和打回结论。后面你也可以直接在任务区用 reviewer 按钮走完整闭环。"; + } + + const relatedTask = project.tasks.find((task) => task.ownerMemberId === member.id && task.status !== "done"); + if (!relatedTask) { + return "我这边已经准备好了,但还没接到新的任务。你可以让 lead 先派发下一条任务。"; + } + + if (relatedTask.status === "changes_requested") { + return `我已经知道「${relatedTask.title}」被打回了,会按 reviewer 的意见继续修。`; + } + + if (relatedTask.status === "in_review") { + return `我这边已经把「${relatedTask.title}」提交给 reviewer 了,正在等复审结果。`; + } + + return `我正在推进「${relatedTask.title}」。如果需要,我也可以继续补充当前卡点和回执。`; +}; + +type MessageStreamEmitter = ((event: ChatStreamEvent) => void) | undefined; + +const segmentReplyForStream = (value: string): string[] => + value + .split(/(?<=[,。!?;\n])/) + .map((part) => part.trim()) + .filter(Boolean); + +const emitStatusEvent = ( + emit: MessageStreamEmitter, + threadId: string, + actorId: string | null, + actorLabel: string, + stage: ChatStreamStage, + text: string +) => { + emit?.({ + type: "status", + threadId, + actorId, + actorLabel, + stage, + text + }); +}; + +const emitFallbackAssistantDeltas = ( + emit: MessageStreamEmitter, + threadId: string, + actorId: string | null, + actorLabel: string, + reply: string +) => { + for (const chunk of segmentReplyForStream(reply)) { + emit?.({ + type: "assistant_delta", + threadId, + actorId, + actorLabel, + delta: chunk + }); + } +}; + +const emitRuntimePromptEvent = ( + emit: MessageStreamEmitter, + threadId: string, + actorId: string | null, + actorLabel: string, + event: CodexPromptStreamEvent +) => { + if (!emit) { + return; + } + + switch (event.type) { + case "assistant_delta": + emit({ + type: "assistant_delta", + threadId, + actorId, + actorLabel, + delta: event.delta + }); + return; + case "reasoning_delta": + emit({ + type: "reasoning", + threadId, + actorId, + actorLabel, + text: event.delta + }); + return; + case "tool_event": + emit({ + type: "tool", + threadId, + actorId, + actorLabel, + toolCallId: event.toolCallId, + toolName: event.toolName, + summary: event.summary, + state: event.state + }); + return; + case "command_output": + emit({ + type: "command_output", + threadId, + actorId, + actorLabel, + toolCallId: event.toolCallId, + chunk: event.chunk + }); + return; + case "thread_started": + emitStatusEvent(emit, threadId, actorId, actorLabel, "codex", `Codex CLI 已建立线程 ${event.threadId}`); + return; + } +}; + +const projectContextSummary = (project: Project): string => { + const teamList = + project.team.members.length > 0 + ? project.team.members.map((member) => `${member.displayName}(${member.title}/${member.runtimeType}/${member.lifecycle})`).join(";") + : "当前还没有正式团队成员"; + + const proposalList = + project.teamProposal.roles.length > 0 + ? project.teamProposal.roles.map((role) => `${role.displayName}(${role.title})`).join(";") + : "当前还没有团队草案"; + + const taskList = + project.tasks.length > 0 + ? project.tasks.slice(0, 5).map((task) => `${task.title}[${task.status}]`).join(";") + : "当前没有执行任务"; + + const mailboxList = + project.mailbox.length > 0 + ? project.mailbox + .slice(0, 5) + .map((message) => `${message.fromLabel}->${message.toLabel}[${message.kind}/${message.status}] ${message.summary}`) + .join(";") + : "当前没有队内 mailbox"; + + const memoryList = + project.sharedProjectMemory.length > 0 + ? project.sharedProjectMemory.slice(0, 5).map((entry) => `${entry.title}:${entry.body}`).join(";") + : "当前没有正式共享记忆"; + + return [ + `项目:${project.name}`, + `阶段:${project.stage}`, + `lead:${project.lead.name}`, + `摸底:${project.discovery.status} / ${project.discovery.lastBrief || "暂无结果"}`, + `团队草案:${proposalList}`, + `正式团队:${teamList}`, + `任务:${taskList}`, + `mailbox:${mailboxList}`, + `共享记忆:${memoryList}` + ].join("\n"); +}; + +const buildLeadCodexPrompt = (project: Project, userContent: string, suggestedReply: string): string => ` +你是一个“项目级 lead 工作台”里的项目 lead。 +请基于下面的项目上下文,用中文直接回复用户。 + +硬要求: +1. 只回复给用户看的正文,不要加标题,不要加 markdown 列表外壳。 +2. 面向非程序员,说人话,简短但别空。 +3. 不要声称已经修改代码、运行成功、完成审查,除非上下文里已经明确发生。 +4. 团队未确认前,不要假装真实成员已经开始干活。 +5. 默认不自动 commit / push。 +6. 这是只读模式,不要要求自己去改文件,不要输出工具调用过程。 + +项目上下文: +${projectContextSummary(project)} + +用户刚刚说: +${userContent} + +系统建议回复: +${suggestedReply} + +请输出最终回复正文: +`.trim(); + +const buildMemberCodexPrompt = (project: Project, thread: ConversationThread, userContent: string, fallbackReply: string): string => ` +你正在扮演项目工作台里的一个成员线程。 +请根据上下文,直接用中文回复用户。 + +硬要求: +1. 只输出最终回复正文。 +2. 口吻像项目成员,不要冒充 lead。 +3. 面向非程序员,但要保留当前线程的职责边界。 +4. 不要声称已经改文件或跑完流程,除非上下文里已经明确发生。 +5. 这是只读模式,不要输出工具调用过程。 + +项目上下文: +${projectContextSummary(project)} + +当前线程: +${thread.label} / ${thread.subtitle} + +用户刚刚说: +${userContent} + +系统兜底回复: +${fallbackReply} + +请输出最终回复正文: +`.trim(); + +const refineLeadReplyWithCodex = async ( + project: Project, + userContent: string, + suggestedReply: string, + stream?: { + emit?: MessageStreamEmitter; + threadId?: string; + } +): Promise<{ reply: string; session: RuntimeSession }> => { + const threadId = stream?.threadId || "thread_lead"; + const runningSession = updateRuntimeSession(project.lead.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 lead 回复:${userContent.slice(0, 36)}`] + }); + let sawAssistantDelta = false; + emitStatusEvent(stream?.emit, threadId, project.lead.id, project.lead.name, "codex", "Lead 正在调用 Codex CLI 组织最终回复。"); + + try { + const result = await runCodexReadOnlyPromptStreaming({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: buildLeadCodexPrompt(project, userContent, suggestedReply), + onEvent: (event) => { + if (event.type === "assistant_delta") { + sawAssistantDelta = true; + } + emitRuntimePromptEvent(stream?.emit, threadId, project.lead.id, project.lead.name, event); + } + }); + + const reply = result.reply || suggestedReply; + if (!sawAssistantDelta && reply) { + emitFallbackAssistantDeltas(stream?.emit, threadId, project.lead.id, project.lead.name, reply); + } + return { + reply, + session: updateRuntimeSession(runningSession, { + status: "idle", + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI lead 回复失败"; + emitStatusEvent(stream?.emit, threadId, project.lead.id, project.lead.name, "codex", `Codex CLI 回复失败,已回退到规则回复:${message}`); + if (!sawAssistantDelta && suggestedReply) { + emitFallbackAssistantDeltas(stream?.emit, threadId, project.lead.id, project.lead.name, suggestedReply); + } + return { + reply: suggestedReply, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI lead 回复失败,已回退到规则回复:${message}`] + }) + }; + } +}; + +const refineMemberReplyWithCodex = async ( + project: Project, + thread: ConversationThread, + userContent: string, + fallbackReply: string, + stream?: { + emit?: MessageStreamEmitter; + } +): Promise<{ reply: string; session: RuntimeSession }> => { + const member = project.team.members.find((item) => item.id === thread.actorId); + if (!member) { + return { reply: fallbackReply, session: project.lead.session }; + } + + const runningSession = updateRuntimeSession(member.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 ${member.displayName} 的线程回复。`] + }); + let sawAssistantDelta = false; + emitStatusEvent(stream?.emit, thread.id, member.id, member.displayName, "codex", `${member.displayName} 正在调用 Codex CLI 生成线程回复。`); + + try { + const result = await runCodexReadOnlyPromptStreaming({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: buildMemberCodexPrompt(project, thread, userContent, fallbackReply), + onEvent: (event) => { + if (event.type === "assistant_delta") { + sawAssistantDelta = true; + } + emitRuntimePromptEvent(stream?.emit, thread.id, member.id, member.displayName, event); + } + }); + + if (!sawAssistantDelta && (result.reply || fallbackReply)) { + emitFallbackAssistantDeltas(stream?.emit, thread.id, member.id, member.displayName, result.reply || fallbackReply); + } + return { + reply: result.reply || fallbackReply, + session: updateRuntimeSession(runningSession, { + status: sessionStatusFromLifecycle(member.lifecycle), + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI 成员回复失败"; + emitStatusEvent(stream?.emit, thread.id, member.id, member.displayName, "codex", `Codex CLI 回复失败,已回退到规则回复:${message}`); + if (!sawAssistantDelta && fallbackReply) { + emitFallbackAssistantDeltas(stream?.emit, thread.id, member.id, member.displayName, fallbackReply); + } + return { + reply: fallbackReply, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI 成员回复失败,已回退到规则回复:${message}`] + }) + }; + } +}; + +const generateWorkerKickoffWithCodex = async ( + project: Project, + member: ProjectMember, + task: TaskItem +): Promise<{ message: string; session: RuntimeSession }> => { + const runningSession = updateRuntimeSession(member.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 ${member.displayName} 的任务启动回执。`] + }); + const fallback = `我收到 lead 派来的任务了:${task.title}。${task.summary}`; + + try { + const result = await runCodexReadOnlyPrompt({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: [ + `你现在扮演项目成员:${member.displayName} / ${member.title}。`, + "请针对下面这条新任务,用中文输出一段简短回执。", + "要求:只说你会先做什么、会重点盯什么,不要说已经改完代码。", + projectContextSummary(project), + `任务标题:${task.title}`, + `任务说明:${task.summary}` + ].join("\n") + }); + + return { + message: result.reply || fallback, + session: updateRuntimeSession(runningSession, { + status: "running", + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI 启动回执失败"; + return { + message: fallback, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI 启动回执失败,已回退:${message}`] + }) + }; + } +}; + +const generateReviewerQueueWithCodex = async ( + project: Project, + reviewer: ProjectMember, + task: TaskItem, + note: string +): Promise<{ message: string; session: RuntimeSession }> => { + const runningSession = updateRuntimeSession(reviewer.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 ${reviewer.displayName} 的审查接单回执。`] + }); + const fallback = `我收到新的审查请求:${task.title}。${note}`; + + try { + const result = await runCodexReadOnlyPrompt({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: [ + `你现在扮演 reviewer:${reviewer.displayName} / ${reviewer.title}。`, + "请针对下面这条送审任务,用中文输出一段简短接单回执。", + "要求:只说明你会重点审什么、可能盯哪些风险,不要直接给通过/打回结论。", + projectContextSummary(project), + `送审任务:${task.title}`, + `送审备注:${note}` + ].join("\n") + }); + + return { + message: result.reply || fallback, + session: updateRuntimeSession(runningSession, { + status: "running", + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI 审查接单失败"; + return { + message: fallback, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI 审查接单失败,已回退:${message}`] + }) + }; + } +}; + +const generateReviewerDecisionWithCodex = async ( + project: Project, + reviewer: ProjectMember, + task: TaskItem, + input: ReviewDecisionInput +): Promise<{ message: string; session: RuntimeSession }> => { + const runningSession = updateRuntimeSession(reviewer.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 ${reviewer.displayName} 的审查结论。`] + }); + const fallback = + input.decision === "approve" + ? `我这边通过了「${task.title}」。${input.note}` + : `我打回了「${task.title}」。${input.note}`; + + try { + const result = await runCodexReadOnlyPrompt({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: [ + `你现在扮演 reviewer:${reviewer.displayName} / ${reviewer.title}。`, + "请针对下面这条已经审完的任务,用中文输出一段简短结论,直接用于 reviewer 线程。", + "要求:如果通过,说明为什么可以过;如果打回,明确最关键的修复点。", + "不要冒充 lead,不要声称已经执行改动,只输出结论正文。", + projectContextSummary(project), + `任务标题:${task.title}`, + `当前决定:${input.decision === "approve" ? "通过" : "打回"}`, + `审查备注:${input.note}` + ].join("\n") + }); + + return { + message: result.reply || fallback, + session: updateRuntimeSession(runningSession, { + status: "idle", + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI 审查结论生成失败"; + return { + message: fallback, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI 审查结论生成失败,已回退:${message}`] + }) + }; + } +}; + +const generateWorkerResubmitWithCodex = async ( + project: Project, + member: ProjectMember, + task: TaskItem, + note: string +): Promise<{ message: string; session: RuntimeSession }> => { + const runningSession = updateRuntimeSession(member.session, { + status: "running", + appendOutput: [`正在调用 Codex CLI 生成 ${member.displayName} 的复审回执。`] + }); + const fallback = `我已经按 reviewer 的意见修完「${task.title}」,现在重新提交审查。${note}`; + + try { + const result = await runCodexReadOnlyPrompt({ + projectPath: project.path, + model: runningSession.model, + resumeSessionId: runningSession.lastThreadId || undefined, + prompt: [ + `你现在扮演项目成员:${member.displayName} / ${member.title}。`, + "请针对下面这条返工后重新送审的任务,用中文输出一段简短回执。", + "要求:说明你已经按 reviewer 意见处理并重新提交,不要提前说已经通过。", + projectContextSummary(project), + `任务标题:${task.title}`, + `复审备注:${note}` + ].join("\n") + }); + + return { + message: result.reply || fallback, + session: updateRuntimeSession(runningSession, { + status: "running", + lastThreadId: result.threadId || runningSession.lastThreadId, + appendOutput: [...result.logLines, ...result.errorLines] + }) + }; + } catch (error) { + const message = error instanceof Error ? error.message : "Codex CLI 复审回执生成失败"; + return { + message: fallback, + session: updateRuntimeSession(runningSession, { + status: "blocked", + lastError: message, + appendOutput: [`Codex CLI 复审回执生成失败,已回退:${message}`] + }) + }; + } +}; + +const sendLeadWorkflow = async (project: Project, content: string): Promise<{ project: Project; reply: string }> => { + const text = content.trim(); + if (!text) { + return { + project, + reply: "你可以直接用白话告诉我:现在最想推进什么,我会先判断该先摸底、先组队,还是直接开工。" + }; + } + + if (/重建\s*lead|换\s*lead|更换\s*lead/i.test(text)) { + return { + project: saveManualActionState(project, "rebuild_lead", "用户在主对话里提出重建 lead。", null), + reply: "我已经把重建 lead 的请求记下来了。新 lead 正式接手前,上一任 lead 需要先完成交接。" + }; + } + + if (/确认\s*团队|就按这版团队|建队|确认这版/i.test(text) && project.teamProposal.roles.length > 0 && project.team.status !== "active") { + return { + project: confirmTeamProposalState(project), + reply: "我已经按你的确认正式建队了。现在可以继续让我先摸底,或者直接派发第一条任务。" + }; + } + + if (/摸底|扫描|目录|技术栈/i.test(text)) { + if (project.discovery.status === "idle") { + return { + project: requestDiscoveryState(project, text), + reply: "我已经先替你创建好摸底请求了。真正开始前,还是要你再点一次确认。" + }; + } + + if (project.discovery.status === "pending_confirmation") { + return { + project, + reply: "摸底请求已经准备好了,差你最后一次确认。确认后我再基于真实目录补团队建议。" + }; + } + + return { + project, + reply: "这轮项目摸底已经完成了。我可以基于真实目录结果继续细化团队,或者直接往执行阶段推进。" + }; + } + + if (project.team.status === "active" && executionIntentPattern.test(text)) { + const testedProject = await runWorkflowSmokeTestState(project); + return { + project: testedProject, + reply: buildWorkflowSmokeTestSummary(testedProject, testedProject.tasks[0]?.id || "") + }; + } + + if ( + ( + project.team.status !== "active" && + (teamPlanningIntentPattern.test(text) || project.teamProposal.roles.length === 0) + ) || + (project.team.status === "active" && activeTeamChangeIntentPattern.test(text)) + ) { + const proposal = await buildTeamProposalFromCodex(project, text); + return { + project: saveTeamProposalState(project, { + summary: proposal.summary, + naturalLanguageBrief: proposal.naturalLanguageBrief, + reuseFirst: project.teamProposal.reuseFirst, + reviewerRule: project.teamProposal.reviewerRule, + leadOverrideRule: project.teamProposal.leadOverrideRule, + userOverrideRule: project.teamProposal.userOverrideRule, + roles: proposal.roles + }), + reply: "我已经按你刚刚的要求整理出一版项目专属团队草案了。你确认后,才会真正创建这些成员会话。" + }; + } + + if (project.team.status === "active" && /开工|开始|执行|推进|派工|干活/i.test(text)) { + if (project.tasks.some((task) => task.status === "in_progress" || task.status === "in_review")) { + return { + project, + reply: "当前已经有任务在推进了。我会继续盯着成员回执和 reviewer 结论往下走。" + }; + } + + return { + project: await createTaskState(project, { + title: "由 lead 派发的第一条任务", + summary: text, + ownerMemberId: findDefaultWorker(project)?.id || null + }), + reply: "我已经按你的意思把第一条任务派下去了。接下来我会盯成员执行、送审和返工。" + }; + } + + if (project.team.status !== "active" && project.teamProposal.roles.length > 0) { + return { + project, + reply: "团队草案已经有了。你可以继续让我改,或者直接确认团队,我再把真实成员会话建起来。" + }; + } + + if (project.team.status === "active") { + return { + project, + reply: "我已经记下你的补充了。现在我会继续按项目级团队方式推进,不会自动 commit,也不会自动 push。" + }; + } + + return { + project, + reply: "我先把你的目标记下来了。现在更稳的下一步,是先确定要不要摸底,或者让我先给你一版项目专属团队草案。" + }; +}; + +export const ensureStore = async (): Promise => { + await mkdir(DATA_DIR, { recursive: true }); + + try { + await readFile(DATA_FILE, "utf8"); + } catch { + await writeFile(DATA_FILE, JSON.stringify(EMPTY_DB, null, 2), "utf8"); + } +}; + +const readDatabase = async (): Promise => { + await refreshCodexCliInfo(); + await ensureStore(); + const raw = await readFile(DATA_FILE, "utf8"); + const parsed = JSON.parse(raw) as { projects?: unknown[] }; + const projects = asArray(parsed.projects); + + if (projects.some((project) => isLegacyProjectRecord(project))) { + return migrateLegacyDatabase(projects); + } + + return { + projects: projects.map((project) => normalizeProjectIndex(project)) + }; +}; + +const writeDatabase = async (database: Database): Promise => { + await writeFile(DATA_FILE, JSON.stringify(database, null, 2), "utf8"); +}; + +const updateProject = async ( + projectId: string, + updater: (project: Project) => Promise | Project +): Promise => + withWriteLock(async () => { + const database = await readDatabase(); + const index = database.projects.findIndex((item) => item.id === projectId); + + if (index < 0) { + return null; + } + + const currentProject = await readProjectStorage(database.projects[index]); + if (!currentProject) { + return null; + } + + const updatedProject = await writeProjectStorage(await updater(currentProject)); + database.projects[index] = createProjectIndex(updatedProject); + await writeDatabase(database); + return updatedProject; + }); + +export const listProjects = async (): Promise => { + const database = await readDatabase(); + const projects = await Promise.all( + database.projects.map(async (entry) => { + const project = await readProjectStorage(entry); + return project ? toSummary(project) : fallbackSummaryFromIndex(entry); + }) + ); + + return projects.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)); +}; + +export const createProject = async (input: ProjectCreateInput): Promise => + withWriteLock(async () => { + const database = await readDatabase(); + const project = await writeProjectStorage(await withLiveGit(syncProject(createDefaultProject(input, await refreshCodexCliInfo())))); + database.projects.unshift(createProjectIndex(project)); + await writeDatabase(database); + return project; + }); + +export const getProject = async (projectId: string): Promise => { + const database = await readDatabase(); + const projectIndex = database.projects.find((item) => item.id === projectId); + + if (!projectIndex) { + return null; + } + + const project = await readProjectStorage(projectIndex); + if (!project) { + return null; + } + + return withLiveGit(syncProject(project)); +}; + +export const getProjectMessages = async (projectId: string): Promise => { + const project = await getProject(projectId); + return project?.messages ?? null; +}; + +export const getProjectOfficeState = async ( + projectId: string, + activeThreadId?: string | null +): Promise => { + const project = await getProject(projectId); + if (!project) { + return null; + } + + const activeThread = project.threads.find((thread) => thread.id === activeThreadId) || project.threads[0] || null; + const activeActorId = activeThread ? getOfficeActorId(project, activeThread) : null; + const assets = getPixelAgentsAssetsManifest(); + const { layout, placementByActorId } = buildProjectOfficeLayoutState(project, assets.characterUrls); + + return { + projectId: project.id, + activeActorId, + assets, + layout, + agents: project.threads.map((thread) => buildOfficeAgentState(project, thread, activeActorId, placementByActorId)) + }; +}; + +export const saveTeamProposal = async ( + projectId: string, + payload: TeamProposalUpdateInput +): Promise => + updateProject(projectId, async (project) => withLiveGit(saveTeamProposalState(project, payload))); + +export const confirmTeamProposal = async (projectId: string): Promise => + updateProject(projectId, async (project) => withLiveGit(confirmTeamProposalState(project))); + +export const confirmTeamChange = async (projectId: string): Promise => + updateProject(projectId, async (project) => withLiveGit(confirmTeamChangeState(project))); + +export const requestDiscovery = async (projectId: string, note: string): Promise => + updateProject(projectId, async (project) => withLiveGit(requestDiscoveryState(project, note))); + +export const confirmDiscovery = async (projectId: string): Promise => + updateProject(projectId, async (project) => { + const analysis = await analyzeProject(project.path, project.lead.name); + return withLiveGit(applyDiscoveryResultState(project, analysis)); + }); + +const processUserMessageState = async ( + project: Project, + content: string, + threadId?: string | null, + emit?: MessageStreamEmitter +): Promise<{ project: Project; reply: string; replyMessageId: string; threadId: string; actorId: string | null; actorLabel: string }> => { + const createdAt = now(); + const targetThread = project.threads.find((thread) => thread.id === threadId) || project.threads[0]; + + if (!targetThread) { + return { + project, + reply: "", + replyMessageId: "", + threadId: threadId || "", + actorId: null, + actorLabel: "未知线程" + }; + } + + const actorId = targetThread.kind === "lead" ? project.lead.id : targetThread.actorId; + const actorLabel = targetThread.kind === "lead" ? project.lead.name : targetThread.label; + const userMessage: ChatMessage = { + id: `msg_user_${createdAt}`, + sender: "user", + content, + createdAt, + senderName: "你" + }; + + emit?.({ + type: "ack", + threadId: targetThread.id, + actorId, + actorLabel, + message: userMessage + }); + emitStatusEvent(emit, targetThread.id, actorId, actorLabel, "received", "已收到你的消息,开始处理。"); + + if (targetThread.kind === "lead") { + const projectWithUser = appendLeadMessages(project, [userMessage], "收到新的用户输入。"); + emitStatusEvent(emit, targetThread.id, project.lead.id, project.lead.name, "workflow", "Lead 正在判断该先摸底、组队,还是直接推进。"); + const workflow = await sendLeadWorkflow(projectWithUser, content); + emitStatusEvent(emit, targetThread.id, project.lead.id, project.lead.name, "workflow", "主流程判断完成,正在整理最终回复。"); + const refined = await refineLeadReplyWithCodex(workflow.project, content, workflow.reply, { + emit, + threadId: targetThread.id + }); + const replyMessage: ChatMessage = { + id: `msg_lead_${nowPlus(1)}`, + sender: "lead", + content: refined.reply, + createdAt: nowPlus(1), + senderName: workflow.project.lead.name + }; + + return { + project: appendLeadMessages( + { + ...workflow.project, + lead: { + ...workflow.project.lead, + session: refined.session + } + }, + [replyMessage], + "Lead 已给出新的下一步判断。" + ), + reply: refined.reply, + replyMessageId: replyMessage.id, + threadId: targetThread.id, + actorId: project.lead.id, + actorLabel: project.lead.name + }; + } + + const fallbackReply = buildMemberReply(project, targetThread, content); + emitStatusEvent(emit, targetThread.id, targetThread.actorId, targetThread.label, "workflow", `${targetThread.label} 正在整理线程上下文。`); + const refined = await refineMemberReplyWithCodex(project, targetThread, content, fallbackReply, { + emit + }); + const replyMessage: ChatMessage = { + id: `msg_member_${nowPlus(1)}`, + sender: "member", + content: refined.reply, + createdAt: nowPlus(1), + senderName: targetThread.label + }; + + return { + project: appendToThread( + updateMemberById(project, targetThread.actorId || "", (member) => + member.id === targetThread.actorId ? { ...member, session: refined.session } : member + ), + targetThread.id, + [userMessage, replyMessage], + `${targetThread.label} 已响应当前线程消息。` + ), + reply: refined.reply, + replyMessageId: replyMessage.id, + threadId: targetThread.id, + actorId: targetThread.actorId, + actorLabel: targetThread.label + }; +}; + +export const saveUserMessage = async ( + projectId: string, + content: string, + threadId?: string | null +): Promise => + updateProject(projectId, async (project) => withLiveGit((await processUserMessageState(project, content, threadId)).project)); + +export const streamUserMessage = async ( + projectId: string, + content: string, + threadId: string | null | undefined, + emit: (event: ChatStreamEvent) => void +): Promise => + withWriteLock(async () => { + const database = await readDatabase(); + const index = database.projects.findIndex((item) => item.id === projectId); + + if (index < 0) { + emit({ + type: "error", + threadId: threadId || null, + actorId: null, + actorLabel: "系统", + message: "没有找到这个项目。" + }); + return null; + } + + const currentProject = await readProjectStorage(database.projects[index]); + if (!currentProject) { + emit({ + type: "error", + threadId: threadId || null, + actorId: null, + actorLabel: "系统", + message: "没有找到这个项目。" + }); + return null; + } + + const processed = await processUserMessageState(currentProject, content, threadId, emit); + emitStatusEvent(emit, processed.threadId, processed.actorId, processed.actorLabel, "persisting", "正在写入最新项目状态。"); + const updatedProject = await writeProjectStorage(await withLiveGit(processed.project)); + database.projects[index] = createProjectIndex(updatedProject); + await writeDatabase(database); + emit({ + type: "final", + threadId: processed.threadId, + actorId: processed.actorId, + actorLabel: processed.actorLabel, + replyMessageId: processed.replyMessageId, + reply: processed.reply, + project: updatedProject + }); + return updatedProject; + }); + +export const createTask = async (projectId: string, payload: TaskCreateInput): Promise => + updateProject(projectId, async (project) => withLiveGit(await createTaskState(project, payload))); + +export const submitTask = async (projectId: string, taskId: string, note: string): Promise => + updateProject(projectId, async (project) => withLiveGit(await submitTaskForReviewState(project, taskId, note))); + +export const resubmitTask = async (projectId: string, taskId: string, note: string): Promise => + updateProject(projectId, async (project) => withLiveGit(await resubmitTaskState(project, taskId, note))); + +export const reviewTask = async ( + projectId: string, + taskId: string, + payload: ReviewDecisionInput +): Promise => + updateProject(projectId, async (project) => withLiveGit(await reviewTaskState(project, taskId, payload))); + +export const saveManualAction = async ( + projectId: string, + action: string, + note: string, + targetMemberId: string | null +): Promise => + updateProject(projectId, async (project) => withLiveGit(saveManualActionState(project, action, note, targetMemberId))); + +export const sendMailboxMessage = async ( + projectId: string, + payload: MailboxMessageCreateInput +): Promise => + updateProject(projectId, async (project) => withLiveGit(sendMailboxMessageState(project, payload))); + +export const updateMailboxStatus = async ( + projectId: string, + mailboxId: string, + status: MailboxStatusUpdateInput["status"] +): Promise => + updateProject(projectId, async (project) => withLiveGit(updateMailboxStatusState(project, mailboxId, status))); + +export const addSharedMemoryEntry = async ( + projectId: string, + title: string, + body: string +): Promise => + updateProject(projectId, async (project) => withLiveGit(addSharedMemoryEntryState(project, title, body))); + +export const updateSessionModel = async ( + projectId: string, + sessionId: string, + payload: SessionModelUpdateInput +): Promise => + updateProject(projectId, async (project) => { + const nextMode = payload.mode === "override" ? "session-override" : "codex-config"; + const nextModel = payload.mode === "override" ? payload.model.trim() : project.codex.defaultModel; + + if (project.lead.session.id === sessionId) { + return withLiveGit( + syncProject({ + ...project, + lead: { + ...project.lead, + session: updateRuntimeSession(project.lead.session, { + model: nextModel, + modelSource: nextMode, + appendOutput: [ + payload.mode === "override" + ? `Lead 已切换到模型 ${nextModel}。` + : `Lead 已切回跟随 Codex CLI 默认模型 ${project.codex.defaultModel}。` + ] + }) + } + }) + ); + } + + const member = project.team.members.find((item) => item.session.id === sessionId); + if (!member) { + return project; + } + + return withLiveGit( + syncProject( + updateMemberById(project, member.id, (current) => ({ + ...current, + session: updateRuntimeSession(current.session, { + model: nextModel, + modelSource: nextMode, + appendOutput: [ + payload.mode === "override" + ? `${current.displayName} 已切换到模型 ${nextModel}。` + : `${current.displayName} 已切回跟随 Codex CLI 默认模型 ${project.codex.defaultModel}。` + ] + }) + })) + ) + ); + }); + +export const restoreSession = async (projectId: string, sessionId: string): Promise => { + const existingProject = await getProject(projectId); + if (!existingProject) { + return { message: "没有找到这个项目或会话。", project: null }; + } + + const roleLabel = sessionLabelById(existingProject, sessionId); + const project = await updateProject(projectId, async (current) => withLiveGit(restoreRoleSession(current, sessionId))); + if (!project) { + return { message: "没有找到这个项目或会话。", project: null }; + } + + return { + message: `${roleLabel} 的恢复入口已经按当前项目状态同步好了。`, + project + }; +}; + +export const rebuildSession = async (projectId: string, sessionId: string): Promise => { + const existingProject = await getProject(projectId); + if (!existingProject) { + return { message: "没有找到这个项目或会话。", project: null }; + } + + const roleLabel = sessionLabelById(existingProject, sessionId); + const project = await updateProject(projectId, async (current) => withLiveGit(rebuildRoleSession(current, sessionId))); + if (!project) { + return { message: "没有找到这个项目或会话。", project: null }; + } + + return { + message: `${roleLabel} 的角色会话已经重建。`, + project + }; +}; + +export const triggerSessionTakeover = async ( + projectId: string, + sessionId: string +): Promise => { + const project = await getProject(projectId); + if (!project) { + return { message: "没有找到这个项目。", project: null }; + } + + if (project.lead.session.id === sessionId) { + await launchCodexTakeover(project.lead.name, project.path, project.lead.session.model, project.lead.session.resumeCommand); + return { message: `已尝试打开 ${project.lead.name} 的原生 Codex 接管窗口。`, project }; + } + + const member = project.team.members.find((item) => item.session.id === sessionId); + if (!member) { + return { message: "没有找到这个运行会话。", project }; + } + + await launchCodexTakeover(member.displayName, project.path, member.session.model, member.session.resumeCommand); + return { message: `已尝试打开 ${member.displayName} 的原生 Codex 接管窗口。`, project }; +}; diff --git a/server/src/types.ts b/server/src/types.ts new file mode 100644 index 0000000..dd7ea78 --- /dev/null +++ b/server/src/types.ts @@ -0,0 +1,562 @@ +export type ProjectStage = + | "lead_only" + | "discovery_pending" + | "team_drafting" + | "team_pending_confirmation" + | "team_active" + | "executing" + | "reviewing" + | "blocked" + | "done"; + +export type DiscoveryStatus = "idle" | "pending_confirmation" | "completed"; +export type TeamProposalStatus = "draft" | "pending_confirmation" | "confirmed"; +export type TeamStatus = "inactive" | "active"; +export type ProposalChangeMode = "initial" | "update"; +export type TaskStatus = "todo" | "in_progress" | "in_review" | "changes_requested" | "blocked" | "done"; +export type ReviewStatus = "requested" | "changes_requested" | "approved"; +export type MessageSender = "user" | "lead" | "system" | "member"; +export type MemberSource = "existing" | "new"; +export type MemberRuntimeType = "reviewer" | "worker"; +export type MemberCapabilityHint = "review" | "frontend" | "backend" | "qa" | "custom"; +export type MemberLifecycle = "idle" | "working" | "waiting_review" | "blocked" | "paused" | "done"; +export type SessionStatus = "idle" | "running" | "waiting" | "needs_confirmation" | "blocked" | "done" | "offline"; +export type SessionModelSource = "codex-config" | "session-override"; +export type ThreadKind = "lead" | "member"; +export type ThreadStatus = "active" | "standby" | "waiting" | "blocked" | "done"; +export type SummaryTeamStatus = "lead_only" | "drafting" | "pending_confirmation" | "active"; +export type MailboxKind = + | "question" + | "dependency_request" + | "blocker" + | "handoff" + | "review_feedback" + | "resubmit_request" + | "broadcast"; +export type MailboxStatus = "open" | "acknowledged" | "resolved"; +export type MemoryEntrySource = "lead" | "system"; +export type SessionIdentityState = "fresh" | "resume_ready" | "needs_rebuild"; +export type TeamChangeKind = "add" | "remove" | "update" | "keep"; +export type TaskActivityKind = + | "task_created" + | "task_assigned" + | "task_started" + | "task_submitted" + | "task_reviewed" + | "task_blocked" + | "task_resumed" + | "task_done" + | "mailbox_linked"; +export type ChatStreamStage = "received" | "workflow" | "codex" | "persisting" | "done"; +export type ChatToolCallState = "started" | "completed"; + +export interface ProjectStorageInfo { + agentsDir: string; + workbenchDir: string; + projectStateFile: string; + mailboxFile: string; + sharedMemoryFile: string; + sharedMemoryMarkdownFile: string; + taskActivityFile: string; +} + +export interface CodexReasoningEffortOption { + reasoningEffort: string; + description: string; +} + +export interface CodexModelOption { + model: string; + displayName: string; + description: string; + defaultReasoningEffort: string; + supportedReasoningEfforts: CodexReasoningEffortOption[]; + supportsPersonality: boolean; + isDefault: boolean; + hidden: boolean; + source: "app-server" | "config"; +} + +export interface CodexCliInfo { + defaultModel: string; + activeProfile: string; + provider: string; + contextWindow: number | null; + autoCompactTokenLimit: number | null; + suggestedModels: string[]; + availableModels: CodexModelOption[]; +} + +export interface RuntimeSession { + id: string; + adapter: "codex-cli"; + roleLabel: string; + status: SessionStatus; + identityState: SessionIdentityState; + model: string; + modelSource: SessionModelSource; + estimatedContextTokens: number; + contextUsagePercent: number; + lastThreadId: string | null; + recentOutput: string[]; + lastUpdatedAt: string; + lastCommand: string; + takeoverCommand: string; + resumeCommand: string; + lastError: string; +} + +export interface NativeSessionRef { + actorId: string; + actorLabel: string; + runtimeType: "lead" | MemberRuntimeType; + session: RuntimeSession; +} + +export interface ProposalRole { + id: string; + displayName: string; + title: string; + runtimeType: MemberRuntimeType; + capabilityHint: MemberCapabilityHint; + responsibility: string; + reason: string; + source: MemberSource; +} + +export interface ProjectMember { + id: string; + displayName: string; + title: string; + runtimeType: MemberRuntimeType; + capabilityHint: MemberCapabilityHint; + responsibility: string; + lifecycle: MemberLifecycle; + source: MemberSource; + createdAt: string; + lastUsedAt: string | null; + threadId: string; + session: RuntimeSession; +} + +export interface TeamProposal { + summary: string; + naturalLanguageBrief: string; + status: TeamProposalStatus; + changeMode: ProposalChangeMode; + reuseFirst: boolean; + reviewerRule: string; + leadOverrideRule: string; + userOverrideRule: string; + roles: ProposalRole[]; + archivedMembers: ProjectMember[]; + updatedAt: string; +} + +export interface TeamState { + status: TeamStatus; + members: ProjectMember[]; + archivedMembers: ProjectMember[]; + confirmedAt: string | null; + updatedAt: string; + lastChangeSummary: string; +} + +export interface TeamChangeEntry { + kind: TeamChangeKind; + currentMemberId: string | null; + proposalRoleId: string | null; + displayName: string; + title: string; + summary: string; +} + +export interface TeamChangeState { + pendingConfirmation: boolean; + summary: string; + entries: TeamChangeEntry[]; + updatedAt: string | null; +} + +export interface LeadProfile { + id: string; + name: string; + version: number; + status: "active" | "handover_pending"; + handoverNote: string; + session: RuntimeSession; +} + +export interface DiscoveryState { + status: DiscoveryStatus; + requiresUserConfirmation: boolean; + requestNote: string; + requestedAt: string | null; + confirmedAt: string | null; + lastBrief: string; + facts: string[]; + stack: string[]; + topLevelEntries: string[]; +} + +export interface ChatMessage { + id: string; + sender: MessageSender; + content: string; + createdAt: string; + senderName?: string; +} + +export type ChatStreamEvent = + | { + type: "ack"; + threadId: string; + actorId: string | null; + actorLabel: string; + message: ChatMessage; + } + | { + type: "status"; + threadId: string; + actorId: string | null; + actorLabel: string; + stage: ChatStreamStage; + text: string; + } + | { + type: "reasoning"; + threadId: string; + actorId: string | null; + actorLabel: string; + text: string; + } + | { + type: "tool"; + threadId: string; + actorId: string | null; + actorLabel: string; + toolCallId: string; + toolName: string; + summary: string; + state: ChatToolCallState; + } + | { + type: "command_output"; + threadId: string; + actorId: string | null; + actorLabel: string; + toolCallId: string | null; + chunk: string; + } + | { + type: "assistant_delta"; + threadId: string; + actorId: string | null; + actorLabel: string; + delta: string; + } + | { + type: "final"; + threadId: string; + actorId: string | null; + actorLabel: string; + replyMessageId: string; + reply: string; + project: Project; + } + | { + type: "error"; + threadId: string | null; + actorId: string | null; + actorLabel: string; + message: string; + }; + +export interface ConversationThread { + id: string; + kind: ThreadKind; + runtimeType: "lead" | MemberRuntimeType; + label: string; + subtitle: string; + status: ThreadStatus; + actorId: string | null; + messages: ChatMessage[]; +} + +export interface TaskItem { + id: string; + title: string; + summary: string; + status: TaskStatus; + ownerMemberId: string | null; + ownerName: string; + createdBy: string; + reviewState: "not_requested" | ReviewStatus; + currentReviewId: string | null; + updatedAt: string; +} + +export interface ReviewItem { + id: string; + taskId: string; + reviewerMemberId: string; + status: ReviewStatus; + note: string; + createdAt: string; +} + +export interface MailboxMessage { + id: string; + fromMemberId: string | null; + fromLabel: string; + toMemberId: string | null; + toLabel: string; + kind: MailboxKind; + relatedTaskId: string | null; + summary: string; + body: string; + status: MailboxStatus; + createdAt: string; +} + +export interface SharedMemoryEntry { + id: string; + title: string; + body: string; + source: MemoryEntrySource; + createdAt: string; +} + +export interface TaskActivityEntry { + id: string; + taskId: string | null; + kind: TaskActivityKind; + actorId: string | null; + actorLabel: string; + summary: string; + detail: string; + relatedMailboxId: string | null; + createdAt: string; +} + +export interface MailboxDigest { + id: string; + title: string; + summary: string; + subtitle: string; + primaryKind: MailboxKind; + participants: string[]; + nextAction: string; + highlights: string[]; + latestAt: string; + unresolvedCount: number; + messageCount: number; + relatedTaskId: string | null; + messageIds: string[]; +} + +export interface OfficeAssetsManifest { + available: boolean; + source: "vscode-extension" | "none"; + extensionPath: string | null; + extensionVersion: string | null; + assetBaseUrl: string | null; + defaultLayoutUrl: string | null; + floorsUrl: string | null; + wallsUrl: string | null; + officeTilesetUrl: string | null; + furnitureCatalogUrl: string | null; + furnitureBaseUrl: string | null; + characterUrls: string[]; + notes: string[]; +} + +export interface OfficeDeskSlot { + id: string; + label: string; + assetId: string; + col: number; + row: number; + width: number; + height: number; + assignedActorId: string | null; +} + +export interface OfficeLayoutState { + cols: number | null; + rows: number | null; + deskSlots: OfficeDeskSlot[]; +} + +export interface OfficeAgentState { + actorId: string; + threadId: string; + label: string; + title: string; + runtimeType: "lead" | MemberRuntimeType; + badge: string; + presence: "typing" | "reading" | "waiting" | "idle" | "blocked" | "done"; + sessionStatus: SessionStatus; + identityState: SessionIdentityState; + currentModel: string; + contextUsagePercent: number; + estimatedContextTokens: number; + characterIndex: number | null; + characterUrl: string | null; + officeSlotId: string | null; + positionCol: number | null; + positionRow: number | null; + unresolvedMailboxCount: number; + activeTaskTitle: string | null; + latestMailboxSummary: string | null; + lastOutput: string; + ruleSummary: string; + detailSummary: string; +} + +export interface OfficeState { + projectId: string; + activeActorId: string | null; + assets: OfficeAssetsManifest; + layout: OfficeLayoutState; + agents: OfficeAgentState[]; +} + +export interface GitSnapshot { + mode: "live" | "placeholder"; + branch: string; + summary: string; + dirty: boolean; + trackedChanges: number; + lastCheckedAt: string | null; + raw: string; +} + +export interface ManualAction { + id: string; + action: string; + operator: string; + targetMemberId: string | null; + targetMemberName: string | null; + note: string; + createdAt: string; +} + +export interface Project { + id: string; + name: string; + path: string; + description: string; + createdAt: string; + updatedAt: string; + stage: ProjectStage; + codex: CodexCliInfo; + lead: LeadProfile; + teamProposal: TeamProposal; + team: TeamState; + nativeSessions: NativeSessionRef[]; + discovery: DiscoveryState; + teamChange: TeamChangeState; + messages: ChatMessage[]; + threads: ConversationThread[]; + tasks: TaskItem[]; + reviews: ReviewItem[]; + mailbox: MailboxMessage[]; + mailboxSummaries: MailboxDigest[]; + sharedProjectMemory: SharedMemoryEntry[]; + taskActivity: TaskActivityEntry[]; + git: GitSnapshot; + manualActions: ManualAction[]; + storage: ProjectStorageInfo; +} + +export interface ProjectIndexEntry { + id: string; + name: string; + path: string; + description: string; + createdAt: string; + updatedAt: string; +} + +export interface Database { + projects: ProjectIndexEntry[]; +} + +export interface ProjectCreateInput { + name?: string; + path: string; + leadName?: string; + description?: string; +} + +export interface TeamProposalUpdateInput { + summary: string; + naturalLanguageBrief: string; + reuseFirst: boolean; + reviewerRule: string; + leadOverrideRule: string; + userOverrideRule: string; + roles: ProposalRole[]; +} + +export interface TaskCreateInput { + title: string; + summary: string; + ownerMemberId?: string | null; +} + +export interface SessionModelUpdateInput { + model: string; + mode: "inherit" | "override"; +} + +export interface SessionOperationResult { + message: string; + project: Project | null; +} + +export interface MailboxMessageCreateInput { + fromMemberId: string; + toMemberId: string; + kind: MailboxKind; + relatedTaskId?: string | null; + summary: string; + body: string; +} + +export interface MailboxStatusUpdateInput { + status: MailboxStatus; +} + +export interface ReviewDecisionInput { + decision: "approve" | "changes_requested"; + note: string; +} + +export interface ProjectSummary { + id: string; + name: string; + path: string; + updatedAt: string; + leadName: string; + stage: ProjectStage; + discoveryStatus: DiscoveryStatus; + teamStatus: SummaryTeamStatus; + currentTask: string; + lastMessagePreview: string; +} + +export interface SuggestedTask { + title: string; + summary: string; +} + +export interface DiscoveryResult { + brief: string; + facts: string[]; + stack: string[]; + topLevelEntries: string[]; + suggestedRoles: ProposalRole[]; + suggestedTasks: SuggestedTask[]; +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..f706cd6 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "../dist/server", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0050c0b --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Agents Team 本地开发工作台 + + +
+ + + diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..48e8bab --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,1940 @@ +import { Route, Routes, useParams, useNavigate, Link } from "react-router-dom"; +import { useState, useEffect, useMemo, useRef, createContext, useContext } from "react"; +import { api } from "./services/api"; +import { PixelAgentsOffice } from "./components/PixelAgentsOffice"; +import { ProjectControlConsole } from "./components/ProjectControlConsole"; +import type { + ChatStreamEvent, + CodexModelOption, + ChatMessage, + OfficeAgentState, + OfficeState, + Project, + ProjectSummary, + RuntimeSession +} from "./types"; +import { + stageLabel, + stageTone, + formatDateTime, + senderLabel, + sessionStatusLabel +} from "./viewHelpers"; + +// 主题上下文 +const ThemeContext = createContext<{ + theme: "dark" | "light"; + toggleTheme: () => void; +}>({ + theme: "dark", + toggleTheme: () => {}, +}); + +const PIXEL_TILE_SIZE = 16; +const CHARACTER_FRAME_WIDTH = 16; +const CHARACTER_FRAME_HEIGHT = 24; +const PIXEL_RENDER_SCALE = 1.6; +const OFFICE_PANEL_MIN_WIDTH = 520; +const OFFICE_PANEL_MAX_WIDTH = 880; +const OFFICE_PANEL_DEFAULT_WIDTH = 620; + +type PixelTileColor = { + h: number; + s: number; + b: number; + c: number; +} | null; + +type PixelLayoutFurnitureItem = { + uid?: string; + type?: string; + col?: number; + row?: number; +}; + +type PixelLayoutData = { + cols?: number; + rows?: number; + tiles?: number[]; + tileColors?: PixelTileColor[]; + furniture?: PixelLayoutFurnitureItem[]; +}; + +type PixelFurnitureAsset = { + id: string; + file: string; + label?: string; + width: number; + height: number; + footprintW?: number; + footprintH?: number; + isDesk?: boolean; +}; + +type PixelFurnitureCatalog = { + assets?: PixelFurnitureAsset[]; +}; + +// 图标组件 +const Icons = { + menu: () => ( + + + + ), + send: () => ( + + + + ), + pause: () => ( + + + + + ), + settings: () => ( + + + + + ), + chevronRight: () => ( + + + + ), + chevronLeft: () => ( + + + + ), + chevronDown: () => ( + + + + ), + plus: () => ( + + + + ), + agent: () => ( + + + + + ), + model: () => ( + + + + ), + sun: () => ( + + + + + ), + moon: () => ( + + + + ), + more: () => ( + + + + + + ), + computer: () => ( + + + + + ), + check: () => ( + + + + ), + grid: () => ( + + + + + + + ), + user: () => ( + + + + + ), +}; + +// 圆形进度条组件 +function CircularProgress({ + percentage, + estimatedTokens = 0, + contextWindow = null, + size = 18, + strokeWidth = 2 +}: { + percentage: number; + estimatedTokens?: number; + contextWindow?: number | null; + size?: number; + strokeWidth?: number; +}) { + const radius = (size - strokeWidth) / 2; + const circumference = radius * 2 * Math.PI; + const offset = circumference - (percentage / 100) * circumference; + + const getColor = () => { + if (percentage < 50) return "#10a37f"; + if (percentage < 80) return "#f59e0b"; + return "#ef4444"; + }; + + return ( +
+ + + + +
+
上下文使用
+
+
已使用: {percentage}%
+
总量: {contextWindow ? `${contextWindow.toLocaleString()} tokens` : "未识别"}
+
已用: {estimatedTokens.toLocaleString()} tokens
+
+
+
+ ); +} + +// 状态标签组件 +function StatusBadge({ tone, children }: { tone: "accent" | "warning" | "neutral" | "danger"; children: React.ReactNode }) { + const { theme } = useContext(ThemeContext); + const toneClasses = { + accent: theme === "dark" + ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" + : "bg-emerald-100 text-emerald-700 border-emerald-300", + warning: theme === "dark" + ? "bg-amber-500/15 text-amber-400 border-amber-500/30" + : "bg-amber-100 text-amber-700 border-amber-300", + neutral: theme === "dark" + ? "bg-slate-500/15 text-slate-400 border-slate-500/30" + : "bg-slate-200 text-slate-600 border-slate-300", + danger: theme === "dark" + ? "bg-rose-500/15 text-rose-400 border-rose-500/30" + : "bg-rose-100 text-rose-700 border-rose-300", + }; + return ( + + {children} + + ); +} + +const officePresenceLabel = (presence: OfficeAgentState["presence"]) => { + switch (presence) { + case "typing": + return "工作中"; + case "reading": + return "阅读中"; + case "waiting": + return "等待中"; + case "blocked": + return "阻塞"; + case "done": + return "完成"; + default: + return "空闲"; + } +}; + +const officePresenceColor = (presence: OfficeAgentState["presence"]) => { + switch (presence) { + case "typing": + return "#10a37f"; + case "reading": + return "#3b82f6"; + case "waiting": + return "#f59e0b"; + case "blocked": + return "#ef4444"; + case "done": + return "#8b5cf6"; + default: + return "#6b7280"; + } +}; + +const spriteFrameOffsetX = (presence: OfficeAgentState["presence"]) => { + if (presence === "typing") { + return CHARACTER_FRAME_WIDTH; + } + + if (presence === "reading") { + return CHARACTER_FRAME_WIDTH * 2; + } + + if (presence === "waiting") { + return CHARACTER_FRAME_WIDTH * 3; + } + + return 0; +}; + +const floorTileSpriteIndex = (tile: number): number => { + if (tile === 8) { + return 0; + } + + return Math.max(0, Math.min(6, tile)); +}; + +// 像素办公室画布组件 - 直接使用 Pixel Agents 资源 +function PixelOfficeCanvas({ + office, + selectedActorId, + onSelectAgent +}: { + office: OfficeState; + selectedActorId: string | null; + onSelectAgent: (id: string) => void; +}) { + const [layoutData, setLayoutData] = useState(null); + const [furnitureCatalog, setFurnitureCatalog] = useState(null); + + useEffect(() => { + let disposed = false; + + const loadAssets = async () => { + try { + const [layoutResponse, catalogResponse] = await Promise.all([ + office.assets.defaultLayoutUrl ? fetch(office.assets.defaultLayoutUrl) : Promise.resolve(null), + office.assets.furnitureCatalogUrl ? fetch(office.assets.furnitureCatalogUrl) : Promise.resolve(null) + ]); + + if (disposed) { + return; + } + + setLayoutData(layoutResponse && layoutResponse.ok ? ((await layoutResponse.json()) as PixelLayoutData) : null); + setFurnitureCatalog(catalogResponse && catalogResponse.ok ? ((await catalogResponse.json()) as PixelFurnitureCatalog) : null); + } catch { + if (!disposed) { + setLayoutData(null); + setFurnitureCatalog(null); + } + } + }; + + void loadAssets(); + + return () => { + disposed = true; + }; + }, [office.assets.defaultLayoutUrl, office.assets.furnitureCatalogUrl]); + + const cols = layoutData?.cols || office.layout.cols || 21; + const rows = layoutData?.rows || office.layout.rows || 21; + const sceneWidth = cols * PIXEL_TILE_SIZE * PIXEL_RENDER_SCALE; + const sceneHeight = rows * PIXEL_TILE_SIZE * PIXEL_RENDER_SCALE; + + const furnitureAssets = useMemo( + () => + new Map( + (furnitureCatalog?.assets || []).map((asset) => [ + asset.id, + { + ...asset, + fileUrl: office.assets.assetBaseUrl ? `${office.assets.assetBaseUrl}/${asset.file}` : asset.file + } + ]) + ), + [furnitureCatalog, office.assets.assetBaseUrl] + ); + + const tiles = useMemo(() => { + const total = cols * rows; + const values = layoutData?.tiles || []; + return Array.from({ length: total }, (_, index) => ({ + id: `tile-${index}`, + tile: values[index] ?? 0, + tileColor: layoutData?.tileColors?.[index] || null, + left: (index % cols) * PIXEL_TILE_SIZE * PIXEL_RENDER_SCALE, + top: Math.floor(index / cols) * PIXEL_TILE_SIZE * PIXEL_RENDER_SCALE + })); + }, [cols, rows, layoutData]); + + const furnitureItems = useMemo( + () => + (layoutData?.furniture || []) + .filter( + (item): item is Required> & PixelLayoutFurnitureItem => + typeof item.type === "string" && typeof item.col === "number" && typeof item.row === "number" + ) + .map((item) => { + const asset = furnitureAssets.get(item.type); + if (!asset) { + return null; + } + + const footprintHeight = Math.max(1, asset.footprintH || Math.max(1, Math.round(asset.height / PIXEL_TILE_SIZE))); + + return { + id: item.uid || `${item.type}-${item.col}-${item.row}`, + url: asset.fileUrl, + alt: asset.label || item.type, + width: asset.width * PIXEL_RENDER_SCALE, + height: asset.height * PIXEL_RENDER_SCALE, + left: item.col * PIXEL_TILE_SIZE * PIXEL_RENDER_SCALE, + top: + (item.row * PIXEL_TILE_SIZE - Math.max(0, asset.height - footprintHeight * PIXEL_TILE_SIZE)) * PIXEL_RENDER_SCALE + }; + }) + .filter((item): item is NonNullable => Boolean(item)), + [furnitureAssets, layoutData] + ); + + const wallTiles = useMemo(() => tiles.filter((tile) => tile.tile === 8), [tiles]); + + return ( +
+
+ Pixel Agents {office.assets.extensionVersion || "unknown"} 资源 + {office.agents.length} 位成员 +
+ +
+
+
+ {tiles.map((tile) => ( +
+ ))} +
+ +
+ {wallTiles.map((tile) => ( +
+ ))} +
+ +
+ {furnitureItems.map((item) => ( + {item.alt} + ))} +
+ +
+ {office.layout.deskSlots.map((slot) => ( +
+ +
+ {office.agents.map((agent) => ( + + ))} +
+
+
+ +
+
+ + 工作中 +
+
+ + 阅读中 +
+
+ + 等待中 +
+
+ + 空闲 +
+
+ + 阻塞 +
+
+
+ ); +} + +// 左侧边栏组件 +function Sidebar({ + projects, + currentProjectId, + isCollapsed, + onToggle +}: { + projects: ProjectSummary[]; + currentProjectId?: string; + isCollapsed: boolean; + onToggle: () => void; +}) { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState<"projects" | "settings">("projects"); + const { theme, toggleTheme } = useContext(ThemeContext); + + if (isCollapsed) { + return ( + + ); + } + + return ( + + ); +} + +// 模型选择器下拉菜单 +function ModelSelector({ + selectedModel, + onModelChange, + models, + disabled +}: { + selectedModel: string; + onModelChange: (model: string) => void; + models: CodexModelOption[]; + disabled?: boolean; +}) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+ {models.map((model) => ( + + ))} +
+ )} +
+ ); +} + +// 消息气泡组件 +function MessageBubble({ + message, + isUser, + agentName, + trace +}: { + message: ChatMessage; + isUser: boolean; + agentName?: string; + trace?: CompletedThreadTrace | null; +}) { + if (isUser) { + return ( +
+
+
{message.content}
+
+
+ ); + } + + return ( +
+
+ {agentName?.charAt(0) || "A"} +
+
+
+ {agentName || senderLabel(message.sender, message.senderName)} + {formatDateTime(message.createdAt)} +
+
+
{message.content}
+ {trace ? ( +
+ + 本次回复轨迹 + + {trace.statuses.length} 条状态 + {trace.tools.length > 0 ? ` · ${trace.tools.length} 个工具` : ""} + {trace.reasoning.length > 0 ? ` · ${trace.reasoning.length} 段过程` : ""} + + +
+ {trace.statuses.length > 0 ? ( +
+ {trace.statuses.map((status, index) => ( +
+ {status} +
+ ))} +
+ ) : null} + {trace.reasoning.length > 0 ? ( +
+
处理过程
+ {trace.reasoning.map((item, index) => ( +
+ {item} +
+ ))} +
+ ) : null} + {trace.tools.length > 0 ? ( +
+
工具调用
+ {trace.tools.map((tool) => ( +
+
+ {tool.toolName} + {tool.state === "completed" ? "已完成" : "进行中"} +
+

{tool.summary}

+ {tool.output.length > 0 ?
{tool.output.join("")}
: null} +
+ ))} +
+ ) : null} + {trace.error ?
{trace.error}
: null} +
+
+ ) : null} +
+
+
+ ); +} + +type LiveToolTrace = { + id: string; + toolName: string; + summary: string; + state: "started" | "completed"; + output: string[]; +}; + +type LiveThreadStream = { + threadId: string; + actorId: string | null; + actorLabel: string; + statuses: string[]; + reasoning: string[]; + tools: LiveToolTrace[]; + content: string; + error: string | null; +}; + +type CompletedThreadTrace = LiveThreadStream & { + id: string; + replyMessageId: string; + finishedAt: string; +}; + +function StreamTraceBubble({ + stream, + mode +}: { + stream: LiveThreadStream | CompletedThreadTrace; + mode: "live" | "completed"; +}) { + const visibleStatuses = mode === "live" ? stream.statuses.slice(-4) : stream.statuses; + const visibleReasoning = mode === "live" ? stream.reasoning.slice(-4) : stream.reasoning; + const visibleTools = mode === "live" ? stream.tools.slice(-4) : stream.tools; + const isLive = mode === "live"; + + const traceContent = ( + <> + {visibleStatuses.length > 0 ? ( +
+ {visibleStatuses.map((status, index) => ( +
+ {status} +
+ ))} +
+ ) : null} + + {visibleReasoning.length > 0 ? ( +
+
处理过程
+ {visibleReasoning.map((item, index) => ( +
+ {item} +
+ ))} +
+ ) : null} + + {visibleTools.length > 0 ? ( +
+
工具调用
+ {visibleTools.map((tool) => ( +
+
+ {tool.toolName} + {tool.state === "completed" ? "已完成" : "进行中"} +
+

{tool.summary}

+ {tool.output.length > 0 ? ( +
{tool.output.slice(-6).join("")}
+ ) : null} +
+ ))} +
+ ) : null} + + {isLive ? ( +
+ {stream.content || "正在生成回复..."} + +
+ ) : null} + + {stream.error ?
{stream.error}
: null} + + ); + + return ( +
+
{stream.actorLabel.charAt(0) || "A"}
+
+
+ {stream.actorLabel} + {isLive ? "流式处理中" : "本次回复轨迹"} +
+
{traceContent}
+
+
+ ); +} + +const createLiveThreadStream = (threadId: string, actorId: string | null, actorLabel: string): LiveThreadStream => ({ + threadId, + actorId, + actorLabel, + statuses: [], + reasoning: [], + tools: [], + content: "", + error: null +}); + +const createCompletedTrace = (stream: LiveThreadStream, replyMessageId: string): CompletedThreadTrace => ({ + ...stream, + tools: stream.tools.map((tool) => ({ + ...tool, + output: [...tool.output] + })), + statuses: [...stream.statuses], + reasoning: [...stream.reasoning], + id: `trace_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + replyMessageId, + finishedAt: new Date().toISOString() +}); + +const applyLiveStreamEvent = (current: LiveThreadStream | null, event: ChatStreamEvent): LiveThreadStream | null => { + if (!current) { + return current; + } + + if (event.threadId && current.threadId !== event.threadId) { + return current; + } + + switch (event.type) { + case "status": + return { + ...current, + actorId: event.actorId, + actorLabel: event.actorLabel, + statuses: [...current.statuses, event.text] + }; + case "reasoning": + return { + ...current, + actorId: event.actorId, + actorLabel: event.actorLabel, + reasoning: [...current.reasoning, event.text] + }; + case "assistant_delta": + return { + ...current, + actorId: event.actorId, + actorLabel: event.actorLabel, + content: `${current.content}${event.delta}` + }; + case "tool": { + const existing = current.tools.find((tool) => tool.id === event.toolCallId); + const nextTool: LiveToolTrace = existing + ? { + ...existing, + toolName: event.toolName, + summary: event.summary, + state: event.state + } + : { + id: event.toolCallId, + toolName: event.toolName, + summary: event.summary, + state: event.state, + output: [] + }; + + return { + ...current, + actorId: event.actorId, + actorLabel: event.actorLabel, + tools: existing ? current.tools.map((tool) => (tool.id === event.toolCallId ? nextTool : tool)) : [...current.tools, nextTool] + }; + } + case "command_output": + if (!event.toolCallId) { + return { + ...current, + statuses: [...current.statuses, event.chunk] + }; + } + + if (!current.tools.some((tool) => tool.id === event.toolCallId)) { + return { + ...current, + tools: [ + ...current.tools, + { + id: event.toolCallId, + toolName: "命令输出", + summary: "正在接收命令输出", + state: "started", + output: [event.chunk] + } + ] + }; + } + + return { + ...current, + tools: current.tools.map((tool) => + tool.id === event.toolCallId ? { ...tool, output: [...tool.output, event.chunk].slice(-12) } : tool + ) + }; + case "error": + return { + ...current, + error: event.message + }; + default: + return current; + } +}; + +// 输入框组件 - Kimi 风格 +function ChatInput({ + value, + onChange, + onSend, + onPause, + isLoading, + selectedModel, + onModelChange, + availableModels, + contextPercent, + estimatedTokens, + contextWindow, + modelBusy +}: { + value: string; + onChange: (v: string) => void; + onSend: () => void; + onPause: () => void; + isLoading: boolean; + selectedModel: string; + onModelChange: (m: string) => void; + availableModels: CodexModelOption[]; + contextPercent: number; + estimatedTokens: number; + contextWindow: number | null; + modelBusy?: boolean; +}) { + const textareaRef = useRef(null); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + onSend(); + } + }; + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + } + }, [value]); + + return ( +
+
+
+ +
+
+
+ + 上下文使用 {contextPercent}% {estimatedTokens > 0 ? `· ${estimatedTokens.toLocaleString()} tokens` : ""} +
+
+
+
+ {/* 左侧工具按钮 */} +
+ + +
+ +