commit 0ab83990b23eeec3c0809ec8068f77715580e22c Author: Codex Date: Mon Mar 23 12:43:39 2026 +0800 feat: bootstrap boss control plane prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f24404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.boss-data +npm-debug.log* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..713d66e --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Boss + +Boss 是一个面向多设备开发协作的 agent control plane。 + +它的目标是让用户通过对话入口或独立控制台,持续管理多台 Windows、Mac 上的编码代理,支持任务拆分、实时进度、需求变更、审批和审计。 + +## 当前状态 + +当前仓库已经完成第一轮产品设计文档: + +- [文档总览](./docs/README.md) +- [竞品对比](./docs/competitor-comparison.md) +- [系统架构](./docs/system-architecture.md) +- [MVP 功能清单](./docs/mvp-feature-plan.md) +- [技术选型](./docs/technical-selection.md) +- [消息协议与状态机](./docs/message-protocol-and-state-machine.md) +- [实施路线图](./docs/implementation-roadmap.md) + +并且已经补入首版可运行原型: + +- Fastify API +- 文件持久化状态存储 +- SSE 实时事件流 +- Web 控制台 +- `boss-worker` 模拟执行器 + +## 当前推荐方向 + +- 主控面:Web 为主,聊天入口为辅 +- Manager:Codex +- 设备侧 worker:Codex CLI + Claude Code +- 工具层:MCP +- 调度:持久队列或工作流引擎 + +## 下一步 + +建议直接开始: + +1. 建立 Web 控制台和后端骨架 +2. 实现 `boss-worker` 注册与心跳 +3. 打通会话、任务树和子任务分发 +4. 接入审批和中途重规划 + +## 本地运行 + +```bash +npm install +npm run dev +``` + +浏览器打开: + +```bash +http://127.0.0.1:43210 +``` + +另开终端启动 worker: + +```bash +npm run worker -- --name win-a --os windows --capability terminal --capability browser +npm run worker -- --name win-b --os windows --capability terminal --capability test +npm run worker -- --name mac-a --os macos --capability terminal --capability test --capability browser +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4926d33 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +# Boss 项目设计文档 + +更新日期:2026-03-23 + +## 文档导航 + +- [竞品对比](./competitor-comparison.md) +- [系统架构](./system-architecture.md) +- [MVP 功能清单](./mvp-feature-plan.md) +- [技术选型](./technical-selection.md) +- [消息协议与状态机](./message-protocol-and-state-machine.md) +- [实施路线图](./implementation-roadmap.md) + +## 项目一句话定义 + +`Boss` 是一个面向多设备开发协作的 agent control plane。 + +它让用户通过对话入口或独立 App,持续管理多台 Windows、Mac 上的编码代理,支持: + +- 协同开发 +- 独立开发 +- 子任务拆分 +- 实时进度播报 +- 中途变更需求 +- 审批高风险操作 +- 审计和会话恢复 + +## 当前建议的产品定位 + +- 不是单一编码助手 +- 不是单机 IDE 插件 +- 不是单纯云端沙箱代理 +- 而是“主控端 + 多设备 worker + 对话式任务系统” + +## 建议阅读顺序 + +1. [竞品对比](./competitor-comparison.md) +2. [系统架构](./system-architecture.md) +3. [MVP 功能清单](./mvp-feature-plan.md) +4. [技术选型](./technical-selection.md) +5. [消息协议与状态机](./message-protocol-and-state-machine.md) +6. [实施路线图](./implementation-roadmap.md) diff --git a/docs/competitor-comparison.md b/docs/competitor-comparison.md new file mode 100644 index 0000000..fd0ae01 --- /dev/null +++ b/docs/competitor-comparison.md @@ -0,0 +1,216 @@ +# 多设备编码代理主控平台竞品对比 + +更新日期:2026-03-23 + +## 目标定义 + +本对比只关注和下面目标相关的产品或框架: + +- 主控端统一管理多个编码代理 +- 支持多入口触发,如聊天工具、Web、IDE、CLI +- 能把任务拆成子任务并持续回传进度 +- 最好能兼容或借鉴本地设备 worker,而不只是云沙箱 + +不纳入重点范围的产品: + +- 只做本地单机结对编程 +- 只做代码补全 +- 只做通用办公 agent,不强调编码任务编排 + +## 一页结论 + +- 最接近企业级成品中控:Devin、Factory +- 最适合搬运和二次开发:OpenHands +- 最适合作为主账号大脑:OpenAI Codex +- 最适合作为设备侧 worker:Claude Code、Codex CLI、Aider、Cline、Roo Code +- 最适合作为协议层:MCP +- 最适合作为 agent-to-agent 通信层:A2A + +## 竞品对比表 + +| 产品/方案 | 类型 | 中控能力 | 子任务/多代理 | 多设备本地 worker 适配 | 聊天入口 | 自建/二开友好度 | 适合借鉴的点 | 主要短板 | 结论 | +|---|---|---|---|---|---|---|---|---|---| +| Devin Enterprise | 商业成品 | 很强 | 强 | 弱到中 | 强 | 低 | 企业级组织管理、RBAC、API、调度、外部集成 | 更偏云环境,不是为本地多设备编队设计 | 可参考“成品控制台” | +| OpenAI Codex | 商业成品 + 平台能力 | 强 | 强 | 中 | 强 | 中 | 主账号统一对话、多入口、云并行任务 | 不是现成的多物理设备 fleet 管理器 | 可作为 manager brain | +| Factory | 商业成品 | 很强 | 很强 | 中 | 很强 | 低到中 | Subagents、Missions、多入口控制 | 生态较新,闭源较重 | 值得重点借鉴交互与控制面 | +| OpenHands Cloud / SDK | 开源平台 + 托管服务 | 强 | 强 | 中到强 | 强 | 很高 | Cloud UI、SDK、远程沙箱、权限、可自建 | 仍需自己补产品化细节 | 最适合做底座 | +| GitHub Copilot coding agent | 商业成品 | 中到强 | 中 | 弱 | 中 | 低 | Issue/PR 驱动、GitHub 闭环、组织策略 | 太 GitHub-centric | 只适合借鉴局部流程 | +| Cursor Background Agents | 商业成品 | 中 | 中 | 中 | 强 | 低 | 远程 agent + Slack 入口 | 更偏账号级,不像团队总控台 | 值得借鉴轻量远程协作 | +| Claude Code | 本地 agent | 弱 | 中 | 很强 | 中 | 中 | 本地终端执行、开发体验强 | local-first,不是中控台 | 非常适合当 worker | +| Sourcegraph + Batch Changes + MCP | 平台底座 | 中 | 弱到中 | 弱 | 弱 | 中 | 批量改仓、代码搜索、分析能力 | 不是 agent control plane | 可作为代码情报层 | +| LangGraph | 编排框架 | 中到强 | 很强 | 中 | 弱 | 很高 | 状态机、工作流、持久状态 | UI、权限、执行层都要自建 | 适合完全自研时使用 | +| AutoGen | 多代理框架 | 中 | 很强 | 中 | 弱 | 高 | 多代理协作范式成熟 | 产品化要自己做 | 适合研究协作逻辑 | +| CrewAI | 工作流框架 | 中 | 很强 | 中 | 弱 | 中到高 | Flow、观测、流程编排 | 编码执行层不够强 | 可借鉴业务流程层 | +| MCP | 工具协议 | 不适用 | 不适用 | 强 | 不适用 | 很高 | 工具统一接入标准 | 不是 agent 调度协议 | 必须纳入架构 | +| A2A | agent 通信协议 | 不适用 | 强 | 强 | 不适用 | 高 | 跨 agent 通信和任务协作 | 生态仍在发展 | 建议前瞻纳入 | + +## 重点产品拆解 + +### 1. Devin Enterprise + +适合借鉴: + +- 企业级组织、权限、审计、API +- 从 Slack、Teams、GitHub、Linear、Jira 等入口发起任务 +- 定时 session 和共享工作流 + +不完全适合直接照搬的点: + +- 更偏云端 Devin machine +- 对本地 Windows/Mac 设备级 agent 编队支持不是它的核心卖点 + +适合你们的参考角色: + +- 作为“企业中控台形态”参考 + +来源: + +- https://docs.devin.ai/enterprise/get-started +- https://docs.devin.ai/release-notes + +### 2. OpenAI Codex + +适合借鉴: + +- 主账号统一发起和跟踪任务 +- 跨 App、CLI、IDE、GitHub、Slack、Linear 协作 +- 云端并行任务和账户级工作台体验 + +不完全适合直接照搬的点: + +- 更像围绕账户和任务的 agent 工作台 +- 不是专门面向“多台真实开发设备”的设备编排系统 + +适合你们的参考角色: + +- 作为 manager brain +- 作为主对话入口和任务拆分层 + +来源: + +- https://openai.com/index/codex-now-generally-available/ +- https://help.openai.com/en/articles/11369540-codex-in-chatgpt + +### 3. Factory + +适合借鉴: + +- Missions 和 Custom Droids 的任务拆解思路 +- Slack、Teams、GitHub App、IDE、CLI、Web 的多入口控制 +- 远程和本地结合的控制面叙事 + +不完全适合直接照搬的点: + +- 闭源较重 +- 需要较多逆向产品层理解,难直接搬运技术实现 + +适合你们的参考角色: + +- 作为“多入口 + 子代理协同”交互范式参考 + +来源: + +- https://docs.factory.ai/ +- https://docs.factory.ai/user-guides/droids/creating-custom-droids + +### 4. OpenHands Cloud / SDK + +适合借鉴: + +- OpenHands SDK 已经是 CLI 和 Cloud 的底层引擎 +- 有 Cloud UI、Cloud API、权限、预算、远程沙箱 +- 自建和二开空间大 + +不完全适合直接照搬的点: + +- 如果要做极强的多设备本地 worker 编排,仍需扩展 agent server 和 worker registry + +适合你们的参考角色: + +- 最优先底座候选 + +来源: + +- https://docs.openhands.dev/sdk +- https://docs.openhands.dev/usage/cloud/cloud-ui + +### 5. Claude Code + +适合借鉴: + +- 本地执行体验强 +- 非常适合挂在 Windows/Mac 上做 worker +- 终端型编码任务执行成熟 + +不完全适合直接照搬的点: + +- 官方定位是 local-first,不是统一云端中控 + +适合你们的参考角色: + +- 设备侧 worker 执行器 + +来源: + +- https://claude.com/product/claude-code + +## 我们的推荐路线 + +### 路线 A:最快做出产品 + +- 底座:OpenHands SDK +- Manager:Codex +- 设备侧 worker:Claude Code / Codex CLI +- 对话入口:Web 为主,Slack/Telegram 为辅 + +适合: + +- 先把真实可用的多设备协作跑起来 + +### 路线 B:完全自研控制面 + +- 编排:LangGraph 或 AutoGen +- Manager:Codex +- 工具层:MCP +- agent 协作层:A2A +- 设备侧 worker:你们自己的 daemon,内部再调 Claude Code / Codex CLI + +适合: + +- 你们希望最终拥有自己的协议、调度和产品层壁垒 + +### 路线 C:先验证,再重构 + +- 第一阶段用 OpenHands 风格快速拼装 MVP +- 第二阶段把设备注册、任务队列、审计、聊天入口收回自研 + +适合: + +- 你们既想快,又不想被单一底座绑死 + +## 对你这个产品最关键的判断 + +如果目标是: + +- 主账号持续对话 +- 实时调整需求 +- 控制多台真实 Windows/Mac +- 支持协同开发和独立开发两种模式 + +那么最好的产品结构不是“一个超强 agent”,而是: + +- 一个 control plane +- 一个 manager agent +- 多个设备侧 worker +- 一套可中断、可续跑、可审计的任务系统 + +也就是说,真正应该对标的不是“谁最会写代码”,而是“谁最会调度一群会写代码的代理”。 + +## 下一步建议 + +完成第 1 项后,建议继续做: + +1. 系统架构图 +2. MVP 功能清单 +3. 技术选型建议 +4. 消息协议与任务状态机 diff --git a/docs/implementation-roadmap.md b/docs/implementation-roadmap.md new file mode 100644 index 0000000..e74c8f8 --- /dev/null +++ b/docs/implementation-roadmap.md @@ -0,0 +1,174 @@ +# Boss 实施路线图 + +更新日期:2026-03-23 + +## 路线目标 + +把 Boss 从概念验证推进到可长期迭代的产品原型。 + +## 阶段划分 + +### Phase 0:项目底座 + +目标: + +- 建立基础仓库结构 +- 搭建前后端骨架 +- 建立数据库和基础实体 + +完成标准: + +- 有 Web 控制台壳子 +- 有后端 API 壳子 +- 有 Postgres 和 Redis +- 能创建 session + +### Phase 1:多设备接入 + +目标: + +- 让 2 台 Windows 和 1 台 Mac 成为可调度 worker + +完成标准: + +- worker 可以注册 +- worker 有心跳 +- 控制台能看到在线状态 +- 可以手工下发简单任务 + +### Phase 2:对话驱动任务拆分 + +目标: + +- 用户一句话创建任务 +- manager 生成任务树 + +完成标准: + +- 对话生成主任务和子任务 +- 子任务可以指派到不同 worker +- UI 能看到任务树 + +### Phase 3:执行、事件和审批 + +目标: + +- worker 真正执行开发动作 +- 中途回传结构化进度 +- 高风险动作需要审批 + +完成标准: + +- worker 能跑 git、terminal、测试 +- 进度能在 UI 和聊天入口显示 +- 审批可以打断并恢复流程 + +### Phase 4:中途变更需求和重规划 + +目标: + +- 支持用户实时改变需求 +- manager 能生成新计划 + +完成标准: + +- 用户可在原会话继续说话 +- 系统可 pause/cancel/replan +- 能展示计划差异 + +### Phase 5:协同开发增强 + +目标: + +- 支持研究、实现、测试分工协作 + +完成标准: + +- 一个任务可以拆成研究和实现链路 +- 子任务之间可引用共享上下文 +- manager 能输出阶段性总结 + +## 推荐开发顺序 + +1. 建库和实体 +2. worker daemon +3. Web 控制台基础页 +4. manager 集成 +5. 子任务调度 +6. 事件流和实时订阅 +7. 审批 +8. 聊天入口 +9. 需求变更与重规划 + +## 每阶段产物 + +### Phase 0 产物 + +- API skeleton +- DB schema +- 基础 UI + +### Phase 1 产物 + +- `boss-worker` +- worker registry +- 设备管理页 + +### Phase 2 产物 + +- manager planning adapter +- task tree UI +- assignment service + +### Phase 3 产物 + +- executor adapter +- event stream +- approval flow + +### Phase 4 产物 + +- plan diff engine +- pause/resume/cancel controls +- session replay + +### Phase 5 产物 + +- collaborative mode +- shared artifacts +- richer progress summaries + +## 里程碑定义 + +### 里程碑 A:可看见 + +- 能看到 3 台机器在线 +- 能创建任务 + +### 里程碑 B:可调度 + +- 能把任务分发给不同设备 +- 能看到执行状态 + +### 里程碑 C:可对话改需求 + +- 执行中可重规划 +- 任务不会失控 + +### 里程碑 D:可协同开发 + +- 多台设备能并行分工 +- 主控端能统一总结 + +## 当前最建议的首版交付 + +如果你现在就准备开工,建议首版目标定成: + +- Web 控制台 +- 3 台 worker +- manager 拆分最多 3 个子任务 +- worker 支持 git、terminal、test +- 支持审批 +- 支持需求变更后重规划 + +做到这里,这个产品就已经不是 demo,而是一个真正可试用的原型。 diff --git a/docs/message-protocol-and-state-machine.md b/docs/message-protocol-and-state-machine.md new file mode 100644 index 0000000..6932434 --- /dev/null +++ b/docs/message-protocol-and-state-machine.md @@ -0,0 +1,419 @@ +# Boss 消息协议与任务状态机 + +更新日期:2026-03-23 + +## 设计目标 + +这份文档定义 Boss 内部最小可用消息模型,确保系统支持: + +- 实时对话 +- 子任务拆分 +- 中途改需求 +- 暂停和恢复 +- 审批 +- 事件回放 + +## 核心原则 + +- 所有关键变化都事件化 +- 会话状态来自事件流和状态快照 +- 用户消息和系统动作统一进入同一条项目时间线 +- worker 回传必须结构化,不能只传纯文本 + +## 会话模型 + +### 层级 + +```text +Project Session + -> Conversation Thread + -> Task Tree + -> Worker Assignments + -> Approval Requests + -> Artifacts +``` + +### 对象定义 + +#### project_session + +```json +{ + "id": "ps_001", + "title": "修复登录与缓存问题", + "status": "active", + "active_objective": "先定位根因,再决定是否修改代码", + "created_at": "2026-03-23T12:00:00Z" +} +``` + +#### message + +```json +{ + "id": "msg_001", + "session_id": "ps_001", + "role": "user", + "channel": "web", + "content": "先排查登录失败,不要急着改代码", + "created_at": "2026-03-23T12:00:05Z" +} +``` + +#### task + +```json +{ + "id": "task_001", + "session_id": "ps_001", + "parent_task_id": null, + "title": "定位登录失败根因", + "kind": "investigation", + "status": "planning", + "priority": "high", + "risk_level": "medium" +} +``` + +#### worker_assignment + +```json +{ + "id": "wa_001", + "task_id": "task_002", + "worker_id": "worker_win_a", + "status": "assigned" +} +``` + +## 事件模型 + +### 统一事件格式 + +```json +{ + "id": "evt_001", + "session_id": "ps_001", + "task_id": "task_002", + "source": "worker", + "type": "task.step.started", + "timestamp": "2026-03-23T12:10:00Z", + "payload": { + "step": "run_tests", + "summary": "开始运行登录相关测试" + } +} +``` + +字段说明: + +- `source` 可取 `user`, `manager`, `system`, `worker` +- `type` 是事件名 +- `payload` 是结构化内容 + +### 推荐事件类型 + +#### 会话事件 + +- `session.created` +- `session.objective.updated` +- `session.message.added` + +#### 规划事件 + +- `plan.created` +- `plan.updated` +- `plan.diff.generated` + +#### 任务事件 + +- `task.created` +- `task.assigned` +- `task.started` +- `task.progress` +- `task.blocked` +- `task.paused` +- `task.resumed` +- `task.cancelled` +- `task.completed` +- `task.failed` + +#### worker 事件 + +- `worker.heartbeat` +- `worker.capabilities.updated` +- `worker.assignment.accepted` +- `worker.assignment.rejected` + +#### 执行步骤事件 + +- `task.step.started` +- `task.step.finished` +- `task.step.failed` +- `tool.call.requested` +- `tool.call.finished` + +#### 审批事件 + +- `approval.requested` +- `approval.approved` +- `approval.rejected` + +## 任务状态机 + +### 主状态机 + +```mermaid +stateDiagram-v2 + [*] --> planning + planning --> queued + queued --> assigned + assigned --> running + running --> blocked + running --> paused + running --> waiting_approval + running --> completed + running --> failed + running --> cancelled + blocked --> running + paused --> running + waiting_approval --> running + waiting_approval --> cancelled + failed --> queued +``` + +### 状态说明 + +| 状态 | 含义 | +|---|---| +| `planning` | manager 正在生成任务计划 | +| `queued` | 已创建,等待调度 | +| `assigned` | 已分配给某个 worker | +| `running` | worker 正在执行 | +| `blocked` | 因外部条件缺失而卡住 | +| `paused` | 被用户或系统暂停 | +| `waiting_approval` | 等待用户审批 | +| `completed` | 成功完成 | +| `failed` | 执行失败 | +| `cancelled` | 被取消 | + +## 需求变更协议 + +### 为什么需要单独协议 + +用户在对话里改需求,不应该等于简单新增一句聊天消息。系统需要知道这条消息是否会: + +- 改变当前目标 +- 废弃现有子任务 +- 新增任务 +- 触发审批 + +### 建议流程 + +1. 用户消息进入 session +2. manager 判断该消息是否属于 `objective change` +3. 如果是,生成 `plan.diff` +4. Task Service 根据 diff 执行状态迁移 + +### plan diff 示例 + +```json +{ + "session_id": "ps_001", + "change_reason": "用户要求先修接口重试,不处理缓存", + "cancel_tasks": ["task_cache_001"], + "pause_tasks": ["task_ui_003"], + "continue_tasks": ["task_api_002"], + "create_tasks": [ + { + "title": "修复接口重试逻辑", + "kind": "implementation", + "priority": "high" + } + ] +} +``` + +## 审批协议 + +### 审批请求格式 + +```json +{ + "id": "apr_001", + "session_id": "ps_001", + "task_id": "task_009", + "kind": "dangerous_command", + "summary": "准备执行 rm -rf build-cache", + "risk_level": "high", + "status": "pending" +} +``` + +### 审批动作 + +支持: + +- `approve` +- `reject` +- `approve_once` +- `approve_for_session` + +MVP 建议只做: + +- `approve` +- `reject` + +## Worker 协议 + +### worker 注册 + +```json +{ + "worker_id": "worker_mac_001", + "hostname": "mac-studio", + "os": "macos", + "shell": "zsh", + "capabilities": [ + "git", + "terminal", + "playwright", + "xcode" + ] +} +``` + +### heartbeat + +```json +{ + "worker_id": "worker_mac_001", + "status": "idle", + "current_task_id": null, + "load": 0.25, + "timestamp": "2026-03-23T12:15:00Z" +} +``` + +### 任务执行请求 + +```json +{ + "task_id": "task_102", + "session_id": "ps_001", + "workspace": { + "repo": "git@github.com:org/repo.git", + "branch": "boss/task-102", + "worktree_path": "/workers/worktrees/task-102" + }, + "execution_mode": "independent", + "goal": "排查登录失败,输出根因和修复建议", + "constraints": [ + "先不要改代码", + "优先跑测试和读日志" + ] +} +``` + +## 进度摘要协议 + +worker 和 manager 都不应该只回长文本。 + +推荐维护一份结构化摘要: + +```json +{ + "task_id": "task_102", + "summary": "已定位到接口重试在 401 时进入死循环", + "progress_percent": 60, + "current_step": "analyze_retry_logic", + "next_step": "补充最小修复方案并跑测试", + "risk": "medium" +} +``` + +这个结构化摘要可以直接喂给: + +- Web 控制台右侧面板 +- 聊天入口状态播报 +- manager 汇总器 + +## 协同开发协议 + +### 模式字段 + +任务可声明: + +- `independent` +- `collaborative` +- `research_only` + +### collaborative 模式下增加字段 + +```json +{ + "shared_context_refs": [ + "artifact_001", + "task_202.summary" + ], + "handoff_expected": true +} +``` + +用于支持: + +- 研究任务把结论交给实现任务 +- 后端任务把 API 变更交给前端任务 + +## UI 实时订阅模型 + +控制台建议订阅三个流: + +- `session_feed` +- `task_feed` +- `worker_feed` + +这样可以避免一个超大流承载所有内容。 + +## 失败处理策略 + +### 可重试失败 + +- 网络抖动 +- worker 短时离线 +- tool call 超时 + +### 不可重试失败 + +- 权限不足 +- 任务目标冲突 +- 用户明确取消 + +### 推荐策略 + +- 每个任务记录 `retry_count` +- 重试必须带原因 +- 多次失败要自动升级为 `blocked` + +## MVP 最小协议集 + +第一版只需要支持这些事件: + +- `session.message.added` +- `plan.created` +- `task.created` +- `task.assigned` +- `task.started` +- `task.progress` +- `task.paused` +- `task.cancelled` +- `task.completed` +- `task.failed` +- `worker.heartbeat` +- `approval.requested` +- `approval.approved` +- `approval.rejected` + +## 一句话总结 + +Boss 的协议核心不是“消息收发”,而是“让对话、任务、worker、审批都落在同一个可追踪状态机里”。 diff --git a/docs/mvp-feature-plan.md b/docs/mvp-feature-plan.md new file mode 100644 index 0000000..bc8ef0a --- /dev/null +++ b/docs/mvp-feature-plan.md @@ -0,0 +1,269 @@ +# Boss MVP 功能清单 + +更新日期:2026-03-23 + +## MVP 目标 + +用最小可用范围验证下面三件事: + +- 用户能持续通过对话管理一个项目 +- 主控端能把任务拆给多台设备并持续汇报进度 +- 用户能在中途改变需求,系统能安全重规划 + +MVP 不追求: + +- 多租户企业 SaaS +- 完整计费系统 +- 支持所有聊天平台 +- 自动解决所有冲突 + +## MVP 范围定义 + +### 必须有 + +- 单用户项目会话 +- 3 台设备接入 +- manager 拆任务 +- worker 在线心跳 +- 实时进度事件 +- 任务暂停、继续、取消 +- 高风险审批 +- Git worktree 隔离 +- 测试结果回传 +- 基础审计 + +### 可以延后 + +- 多组织管理 +- 向量记忆优化 +- 多模型自动路由 +- 自动 PR 审核 +- 自动成本优化 +- 丰富报表 + +## 用户故事 + +### 用户故事 1:发起项目任务 + +作为用户,我希望在一个对话里描述需求,让系统自动拆任务给不同设备执行。 + +验收标准: + +- 用户说一句自然语言需求即可创建 project session +- manager 能生成主任务和子任务 +- 子任务能分配到不同设备 + +### 用户故事 2:查看实时进度 + +作为用户,我希望随时看到每台设备当前做到了哪里,而不是只在结束时知道结果。 + +验收标准: + +- 每个子任务有状态、最近一步、最近日志摘要 +- UI 能看到设备在线状态 +- 聊天入口能返回汇总版进度 + +### 用户故事 3:中途改需求 + +作为用户,我希望在任务执行中直接说“改一下方向”,系统就能调整任务,而不是重新开一个新会话。 + +验收标准: + +- 新需求追加到同一个 session +- manager 能触发 replan +- 正在运行的子任务可安全暂停或取消 +- 用户能看到新旧计划差异 + +### 用户故事 4:审批危险操作 + +作为用户,我希望对删文件、强推分支、运行危险命令等行为进行确认。 + +验收标准: + +- worker 可发起审批请求 +- 控制台和聊天入口都能完成审批 +- 审批前任务挂起 +- 审批结果可审计 + +### 用户故事 5:协同开发 + +作为用户,我希望多台设备能分别做调研、编码和测试,并由主账号统一汇总。 + +验收标准: + +- 至少支持 2 个并行子任务 +- manager 可汇总结果 +- 同一项目下子任务之间可引用共享上下文 + +## MVP 模块清单 + +### 1. Web 控制台 + +必须页面: + +- 会话页 +- 任务树页 +- 设备页 +- 审批页 + +最小功能: + +- 发消息 +- 看任务树 +- 看设备在线状态 +- 审批和取消任务 + +### 2. 聊天入口 + +第一阶段建议只接一个平台: + +- Slack 或 Telegram 二选一 + +最小功能: + +- 新建任务 +- 查看状态 +- 审批 +- 取消任务 + +不建议第一阶段做: + +- 复杂文件浏览 +- 终端实时流 + +### 3. Session Service + +最小职责: + +- 创建会话 +- 保存消息 +- 返回会话历史 +- 标记当前 active objective + +### 4. Task Service + +最小职责: + +- 创建任务树 +- 更新任务状态 +- 管理依赖关系 +- 触发重规划 + +### 5. Scheduler + +最小职责: + +- 根据能力分配 worker +- 处理重试和超时 +- 维护 assignment 状态 + +### 6. Worker Daemon + +最小职责: + +- 注册设备 +- 心跳 +- 拉取任务 +- 执行命令 +- 回传结构化事件 + +### 7. 审批系统 + +最小职责: + +- 定义危险动作 +- 创建审批请求 +- 接收审批结果 +- 恢复或终止工作流 + +## MVP 页面草图 + +### 会话页 + +区域: + +- 左侧项目和会话列表 +- 中间对话流 +- 右侧任务树与设备执行摘要 + +### 任务页 + +区域: + +- 主任务卡片 +- 子任务列表 +- 当前负责设备 +- 状态与最近事件 + +### 设备页 + +区域: + +- 设备名称 +- OS +- 在线状态 +- 当前任务 +- 最近心跳 +- 工具能力 + +## MVP 指标 + +### 产品指标 + +- 任务可创建成功率 +- 子任务成功分配率 +- 需求变更后的重规划成功率 +- 审批往返耗时 + +### 系统指标 + +- worker 心跳在线率 +- 事件回传延迟 +- 任务平均完成时间 +- 失败重试成功率 + +### 体验指标 + +- 用户查看进度时的响应时间 +- 对话到任务树生成耗时 +- 需求变更到新计划生效耗时 + +## MVP 版本边界 + +### V0.1 + +- Web 控制台 +- 单聊天入口 +- 3 台设备 +- manager 拆 2 到 3 个子任务 +- 手动审批 + +### V0.2 + +- 任务模板 +- 更细的设备能力调度 +- GitHub PR 集成 +- 更丰富的任务摘要 + +### V0.3 + +- 协同开发模式增强 +- 共享上下文管理 +- 更细粒度权限 + +## MVP 不做清单 + +- 不做跨团队权限模型 +- 不做复杂订阅体系 +- 不做自动跨仓库大规模变更 +- 不做完整 IDE 插件矩阵 +- 不做长周期自主运行无需监督的全自动模式 + +## 开工优先级 + +1. 设备接入和心跳 +2. 对话到任务树 +3. 子任务分发 +4. worker 执行与事件回传 +5. 审批与中断恢复 +6. 聊天入口 diff --git a/docs/system-architecture.md b/docs/system-architecture.md new file mode 100644 index 0000000..63938c9 --- /dev/null +++ b/docs/system-architecture.md @@ -0,0 +1,342 @@ +# Boss 系统架构 + +更新日期:2026-03-23 + +## 架构目标 + +Boss 要解决的问题不是“如何让一个 agent 写代码”,而是“如何让一个主控端持续调度多个运行在不同设备上的编码 agent,并保持对话、状态和审计一致”。 + +核心目标: + +- 用户始终只和一个主账号对话 +- 主账号能把任务拆给多个 worker +- 每个 worker 可以运行在真实 Windows 或 Mac 设备上 +- 用户可以随时改变需求 +- 系统支持中断、续跑、审批、回滚、审计 + +## 总体架构图 + +```mermaid +flowchart LR + User["用户"] --> Chat["聊天入口
Slack / Telegram / 飞书 / 企业微信"] + User --> App["独立控制台
Web / Desktop"] + + Chat --> Gateway["API Gateway"] + App --> Gateway + + Gateway --> Session["Session Service"] + Gateway --> Task["Task Service"] + Gateway --> Notify["Notification Service"] + + Session --> Planner["Manager Agent
Codex"] + Task --> Planner + Planner --> Queue["Queue / Workflow Engine"] + Queue --> Scheduler["Scheduler"] + + Scheduler --> Worker1["Windows Worker A"] + Scheduler --> Worker2["Windows Worker B"] + Scheduler --> Worker3["Mac Worker"] + + Worker1 --> Runtime1["Agent Runtime
Codex CLI / Claude Code / MCP Tools"] + Worker2 --> Runtime2["Agent Runtime
Codex CLI / Claude Code / MCP Tools"] + Worker3 --> Runtime3["Agent Runtime
Codex CLI / Claude Code / MCP Tools"] + + Session --> Store["Postgres"] + Task --> Store + Queue --> Store + Worker1 --> EventBus["Event Bus"] + Worker2 --> EventBus + Worker3 --> EventBus + EventBus --> Notify + EventBus --> Store + + Planner --> Memory["Project Memory / Vector Store"] + Worker1 --> Git["Git Provider"] + Worker2 --> Browser["Browser / Playwright"] + Worker3 --> IDE["Local IDE / Terminal / Filesystem"] +``` + +## 分层设计 + +### 1. 入口层 + +职责: + +- 接收用户自然语言 +- 展示项目进度、任务树、审批请求 +- 承载多端对话入口 + +组成: + +- Web 控制台 +- Desktop 控制台 +- Slack / Telegram / 飞书 / 企业微信 Bot + +设计原则: + +- 独立 App 是主控制面 +- 聊天工具只做轻入口和通知审批 +- 所有入口共享同一个会话和任务状态 + +### 2. 会话与任务层 + +职责: + +- 维护项目级会话 +- 存储用户意图、任务树、依赖关系 +- 接收需求变更并触发重规划 + +核心对象: + +- `project_session` +- `task` +- `subtask` +- `message` +- `approval_request` +- `worker_assignment` + +设计原则: + +- 任何需求变更都不是覆盖旧状态,而是追加一条事件 +- 主控 agent 根据最新状态决定继续、暂停、取消还是重规划 + +### 3. Manager Agent 层 + +职责: + +- 读取当前上下文 +- 把用户自然语言翻译成任务树 +- 给不同 worker 分派工作 +- 汇总各 worker 回传结果 +- 把技术执行过程重新组织成面向用户的进度播报 + +建议实现: + +- 首选 Codex 作为 manager brain +- manager 不直接执行重活,主要负责规划、协调和总结 + +为什么 manager 要单独存在: + +- 用户说的是需求语言,不是底层执行语言 +- 设备 worker 报的是技术状态,不是业务状态 +- manager 负责在这两者之间做翻译 + +### 4. 调度与工作流层 + +职责: + +- 排队 +- 优先级控制 +- 并发控制 +- 超时重试 +- 中断恢复 +- 审批等待 + +设计原则: + +- 长任务必须通过工作流引擎或持久队列驱动 +- 不能靠单个进程中的内存状态维持任务 + +推荐能力: + +- 任务可挂起 +- 任务可人工恢复 +- 子任务状态独立 +- 支持依赖关系图 + +### 5. Worker 层 + +职责: + +- 在真实设备上执行具体开发动作 +- 拉代码、建 worktree、改文件、跑测试、创建 PR +- 上报结构化事件和心跳 + +部署方式: + +- 每台设备安装一个 `boss-worker` +- worker 持久连接到 control plane +- worker 内部再调用 Codex CLI、Claude Code、Playwright、MCP 工具等 + +为什么 worker 必须是 daemon: + +- 需要持续在线状态 +- 需要接收取消和暂停命令 +- 需要中途回传事件 +- 需要在主控端丢线后继续执行 + +### 6. 工具层 + +职责: + +- 提供标准化工具访问 +- 屏蔽不同设备的具体环境差异 + +建议接入: + +- Git +- Filesystem +- Terminal +- Browser automation +- Issue tracker +- Chat connector +- CI / Test runner + +建议协议: + +- 工具接入优先使用 MCP +- agent-to-agent 通信前瞻引入 A2A + +### 7. 存储与审计层 + +职责: + +- 保存消息、任务、事件、状态快照 +- 为用户提供会话恢复与审计追踪 + +必须保存的内容: + +- 用户消息 +- manager 规划结果 +- worker 事件流 +- 审批记录 +- 高风险工具调用 +- Git 变更摘要 + +## 关键交互路径 + +### 路径 1:用户发起新任务 + +1. 用户在 Web 或聊天入口发起需求 +2. Session Service 追加消息 +3. Manager 生成任务树 +4. Task Service 创建主任务和子任务 +5. Scheduler 根据设备能力分配 worker +6. worker 执行并回传事件 +7. manager 定期汇总进度给用户 + +### 路径 2:用户中途改需求 + +1. 用户说“先暂停 A,改成先修登录问题” +2. 新消息进入当前 project session +3. manager 读取活动中的任务树 +4. manager 标记哪些子任务失效,哪些继续保留 +5. Task Service 下发 `pause / cancel / replan` +6. worker 接收并安全停止当前步骤 +7. manager 输出新的计划和最新进度 + +### 路径 3:高风险审批 + +1. worker 申请执行高风险操作 +2. control plane 生成审批请求 +3. 用户在 App 或聊天入口确认 +4. 工作流恢复执行或终止 + +## 协同开发模式 + +### 模式 A:独立开发 + +- 一个子任务绑定一个 worker +- worker 自己完成从理解、编码到测试的闭环 +- manager 只做验收和汇总 + +适合: + +- 低耦合功能开发 +- 单一 bug 修复 + +### 模式 B:协同开发 + +- manager 先做任务拆解 +- 不同 worker 分别负责不同模块或阶段 +- 通过 shared context 和中间产物衔接 + +适合: + +- 横跨前后端的需求 +- 需要并行研究和实现 + +### 模式 C:研究先行 + +- 多个 worker 先做调研、复现、定位 +- manager 再决定由哪台设备进入实现 + +适合: + +- 不确定问题根因 +- 风险较高的问题 + +## 设备调度策略 + +推荐调度维度: + +- OS 能力 +- 当前负载 +- 工具能力 +- 项目亲和性 +- 上下文热度 +- 风险等级 + +示例: + +- 需要 Xcode 的任务优先给 Mac +- 需要 Windows 原生应用自动化的任务优先给 Windows +- 同一 repo 的后续任务优先分给最近处理过该 repo 的 worker + +## 为什么不建议一开始就做的东西 + +- 不建议一开始做跨团队多租户 SaaS +- 不建议一开始做复杂计费系统 +- 不建议一开始就做完整 A2A 联邦网络 +- 不建议一开始支持太多聊天平台 + +第一阶段只要把下面几件事跑通: + +- 主账号持续对话 +- 三台机器在线 +- 子任务拆分 +- 实时进度回传 +- 可暂停、可继续、可取消 +- 可审批高风险操作 + +## 当前推荐架构决策 + +- 主控面:Web 优先,后续补 Desktop +- Manager:Codex +- Worker runtime:Codex CLI + Claude Code 混合 +- 工具协议:MCP +- 任务编排:持久队列或工作流引擎 +- 存储:Postgres +- 事件流:Redis Streams 或消息队列 + +## 架构风险 + +### 风险 1:多个 worker 改同一仓库互相踩踏 + +规避方式: + +- 每个任务独立 worktree +- 每个任务独立分支 +- 合并前必须做 diff 汇总和冲突检查 + +### 风险 2:需求变化导致长任务上下文混乱 + +规避方式: + +- 所有需求变化事件化 +- manager 在重规划前必须读取当前状态快照 + +### 风险 3:聊天入口承载太多复杂交互 + +规避方式: + +- 长会话、文件树、终端流只放在独立 App +- 聊天入口只承担轻操作 + +### 风险 4:worker 执行权限过大 + +规避方式: + +- 把危险命令纳入审批 +- 工具分级授权 +- 关键操作记录审计日志 diff --git a/docs/technical-selection.md b/docs/technical-selection.md new file mode 100644 index 0000000..269893c --- /dev/null +++ b/docs/technical-selection.md @@ -0,0 +1,314 @@ +# Boss 技术选型建议 + +更新日期:2026-03-23 + +## 选型原则 + +- 先保证系统能跑,再追求最优雅 +- 持久状态优先于纯内存编排 +- 结构化事件优先于拼字符串日志 +- 尽量复用成熟 agent 运行时,而不是自己重写编码 agent + +## 总体选型建议 + +| 层 | 推荐 | 备选 | 原因 | +|---|---|---|---| +| 前端控制台 | Next.js | Remix, Nuxt | 适合快速做管理台和流式 UI | +| Desktop | Tauri | Electron | 后续需要时再补,Tauri 更轻 | +| 后端 API | NestJS 或 Fastify | Express, Go Fiber | 结构清晰,适合长期维护 | +| 工作流引擎 | Temporal | Inngest, BullMQ + 自建状态机 | 长任务、中断、审批、恢复是核心需求 | +| 队列/事件 | Redis Streams | NATS, Kafka | MVP 阶段足够轻,开发快 | +| 主数据库 | Postgres | MySQL | 事务、JSON、查询能力更适合控制平面 | +| 缓存 | Redis | KeyDB | 用于会话热数据、限流、事件流 | +| Manager Agent | Codex | Claude API, GPT-5.x Responses | 适合做规划、汇总、重规划 | +| Worker Runtime | Codex CLI + Claude Code | OpenHands agent runtime | 直接复用成熟编码 agent | +| 工具协议 | MCP | 自定义 tool API | 标准化工具接入 | +| 浏览器自动化 | Playwright | Puppeteer | 成熟稳定,适合 agent 使用 | +| 聊天入口 | Slack 或 Telegram | 飞书, 企业微信 | 文档、生态和迭代速度更合适 | +| 身份认证 | Clerk 或 Auth.js | 自建 OAuth | MVP 快速起步 | +| 监控 | OpenTelemetry + Grafana | Datadog, Sentry | 方便追踪任务链路 | + +## 为什么推荐这个技术组合 + +### 1. Next.js 作为控制台前端 + +原因: + +- 适合做 dashboard、对话界面、任务树、设备面板 +- 容易做流式更新和服务端渲染 +- 与 TypeScript 一体化 + +不建议现在单独起 React Native: + +- 当前核心不是移动端,而是 control plane + +### 2. NestJS 或 Fastify 作为后端 + +如果团队偏工程化: + +- 选 NestJS + +如果你想更轻更快: + +- 选 Fastify + +建议: + +- 先用 Fastify 或 NestJS 中你更熟的那个 +- 关键是把服务边界拆干净 + +### 3. Temporal 作为工作流引擎 + +这是当前最关键的选型之一。 + +Boss 不是短请求系统,而是长任务系统。你需要天然支持: + +- 几分钟到几小时的任务 +- 中途暂停 +- 审批等待 +- worker 断线重连 +- 任务恢复 + +如果不用工作流引擎,后面会自己补很多分布式状态机逻辑。 + +如果 MVP 阶段不想上 Temporal: + +- 可以先用 `BullMQ + Postgres 状态表` +- 但要尽快收敛到真正可恢复的工作流模型 + +### 4. Postgres 作为主数据库 + +Boss 的核心不是海量日志,而是: + +- 会话 +- 任务树 +- 审批 +- worker 注册 +- 状态快照 + +这些都非常适合 Postgres。 + +### 5. Codex 作为 manager + +原因: + +- 你希望主账号持续对话和重规划 +- manager 不需要最强本地执行,它更需要任务理解、拆解和汇总 +- Codex 在多入口和云任务语境里更契合 manager 角色 + +推荐职责: + +- 规划 +- 重规划 +- 汇总 +- 风险解释 + +不建议让 manager 干太多底层执行: + +- 容易把规划和执行耦合死 + +### 6. Claude Code / Codex CLI 作为 worker runtime + +原因: + +- 本地设备执行体验成熟 +- 适合接 terminal、filesystem、git、browser +- 真实开发环境兼容性更好 + +建议策略: + +- Mac 和 Windows worker 都先抽象成统一 `executor` +- 底层可配置使用 `codex` 或 `claude` + +## 服务拆分建议 + +### 第一阶段服务 + +- `api-gateway` +- `session-service` +- `task-service` +- `scheduler-service` +- `worker-gateway` +- `notification-service` + +### 第二阶段可拆 + +- `approval-service` +- `memory-service` +- `git-integration-service` +- `chat-connector-service` + +## 数据存储建议 + +### Postgres 表 + +建议至少有: + +- `users` +- `project_sessions` +- `messages` +- `tasks` +- `task_dependencies` +- `worker_nodes` +- `worker_capabilities` +- `worker_assignments` +- `task_events` +- `approval_requests` +- `artifacts` + +### Redis 用途 + +- worker 在线状态 +- 事件总线 +- UI 实时订阅 +- 节流和限流 + +## Worker 设计建议 + +### worker 进程结构 + +组成: + +- `boss-worker` 守护进程 +- `executor` 适配层 +- `tool adapters` +- `sandbox policy` +- `event reporter` + +### worker 本地目录策略 + +建议: + +- 一个统一工作根目录 +- 每个任务一个独立 workspace +- 每个任务一个独立 git worktree + +示例: + +```text +~/boss-worker/ + repos/ + worktrees/ + cache/ + artifacts/ +``` + +### worker 能力声明 + +每个 worker 启动时上报: + +- OS +- shell +- 是否支持浏览器自动化 +- 是否支持特定 IDE +- 是否有移动端模拟器 +- 是否具备 repo 热缓存 + +## 协议选型建议 + +### MCP + +用途: + +- 统一工具调用 + +适合接: + +- Git +- 文件系统 +- 浏览器 +- Issue 系统 +- CI + +### A2A + +用途: + +- 中长期用于 agent 之间通信 + +建议: + +- MVP 不强依赖 +- 但内部消息模型尽量向 agent-to-agent 协作靠拢 + +## 前后端通信建议 + +### 控制台实时能力 + +推荐: + +- WebSocket 或 Server-Sent Events + +用途: + +- 推送任务状态 +- 推送审批请求 +- 推送设备在线变化 + +### worker 连接方式 + +推荐: + +- WebSocket 长连接 + +原因: + +- 需要双向通信 +- 需要下发中断和恢复命令 + +## 安全建议 + +### 最小权限原则 + +- worker 不要默认拥有所有仓库权限 +- 每种工具调用都要有权限分级 + +### 高风险动作审批 + +必须纳入审批的动作: + +- 删除大量文件 +- 强推分支 +- 执行危险 shell +- 访问敏感路径 + +### 审计 + +必须记录: + +- 谁发起了什么 +- 哪个 worker 执行了什么 +- 哪个 agent 建议了什么 +- 审批前后状态 + +## 推荐的 MVP 技术栈 + +### MVP 最稳组合 + +- 前端:Next.js +- 后端:NestJS +- 数据库:Postgres +- 缓存和事件:Redis +- 工作流:BullMQ,后续升级 Temporal +- Manager:Codex +- Worker:Codex CLI + Claude Code +- 聊天入口:Telegram 或 Slack + +### 如果你要一步到位更稳 + +- 前端:Next.js +- 后端:NestJS +- 数据库:Postgres +- 缓存:Redis +- 工作流:Temporal +- Manager:Codex +- Worker:Codex CLI + Claude Code +- 监控:OpenTelemetry + +## 不推荐的坑 + +- 不要一开始就全微服务 +- 不要一开始就自研完整 agent runtime +- 不要把聊天平台当成主控制台 +- 不要让多个任务共用一个工作目录 +- 不要把审批做成“日志里提示一下”而不是显式状态 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d3c6ba4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1618 @@ +{ + "name": "boss", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "boss", + "version": "0.1.0", + "dependencies": { + "@fastify/static": "^8.2.0", + "fastify": "^5.6.1" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "tsx": "^4.20.6", + "typescript": "^5.9.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.2.tgz", + "integrity": "sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", + "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/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/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/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/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/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/safe-regex2": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz", + "integrity": "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c50908a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "boss", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "check": "tsc --noEmit -p tsconfig.json", + "worker": "tsx src/worker.ts" + }, + "dependencies": { + "@fastify/static": "^8.2.0", + "fastify": "^5.6.1" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "tsx": "^4.20.6", + "typescript": "^5.9.2" + } +} + diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..f57bf8d --- /dev/null +++ b/public/app.js @@ -0,0 +1,333 @@ +const state = { + sessions: [], + messages: [], + tasks: [], + approvals: [], + workers: [], + events: [], + selectedSessionId: null, +}; + +const elements = { + sessionList: document.querySelector("#session-list"), + workerList: document.querySelector("#worker-list"), + messageList: document.querySelector("#message-list"), + taskList: document.querySelector("#task-list"), + approvalList: document.querySelector("#approval-list"), + eventList: document.querySelector("#event-list"), + sessionTitleDisplay: document.querySelector("#session-title-display"), + sessionSummary: document.querySelector("#session-summary"), + createSessionForm: document.querySelector("#create-session-form"), + sessionTitleInput: document.querySelector("#session-title"), + messageForm: document.querySelector("#message-form"), + messageInput: document.querySelector("#message-input"), + resetDemo: document.querySelector("#reset-demo"), +}; + +function escapeHtml(input) { + return input + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +async function request(url, options = {}) { + const response = await fetch(url, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`); + } + + return response.json(); +} + +function selectedSession() { + return state.sessions.find((session) => session.id === state.selectedSessionId) ?? null; +} + +function tasksForSelectedSession() { + return state.tasks.filter((task) => task.sessionId === state.selectedSessionId); +} + +function messagesForSelectedSession() { + return state.messages.filter((message) => message.sessionId === state.selectedSessionId); +} + +function approvalsForSelectedSession() { + return state.approvals.filter((approval) => approval.sessionId === state.selectedSessionId); +} + +function eventsForSelectedSession() { + return state.events + .filter((event) => event.sessionId === null || event.sessionId === state.selectedSessionId) + .slice(-50) + .reverse(); +} + +function renderSessions() { + elements.sessionList.innerHTML = state.sessions + .map((session) => { + const active = session.id === state.selectedSessionId ? "active" : ""; + return ` + + `; + }) + .join(""); + + elements.sessionList.querySelectorAll("[data-session-id]").forEach((button) => { + button.addEventListener("click", async () => { + state.selectedSessionId = button.dataset.sessionId; + await loadSession(state.selectedSessionId); + render(); + }); + }); +} + +function renderWorkers() { + elements.workerList.innerHTML = state.workers + .map( + (worker) => ` +
+
+ ${escapeHtml(worker.name)} + ${escapeHtml(worker.status)} +
+
${escapeHtml(worker.os)}
+
+ ${worker.capabilities.map((capability) => `${escapeHtml(capability)}`).join("")} +
+
+ `, + ) + .join(""); +} + +function renderSessionHeader() { + const session = selectedSession(); + if (!session) { + elements.sessionTitleDisplay.textContent = "选择一个项目会话"; + elements.sessionSummary.textContent = "创建会话后,在这里持续对话并观察任务状态。"; + return; + } + + elements.sessionTitleDisplay.textContent = session.title; + elements.sessionSummary.textContent = + session.lastPlannerSummary || session.activeObjective || "等待用户输入。"; +} + +function renderMessages() { + const messages = messagesForSelectedSession(); + elements.messageList.innerHTML = messages.length + ? messages + .map( + (message) => ` +
+
+ ${escapeHtml(message.role)} + ${new Date(message.createdAt).toLocaleTimeString()} +
+

${escapeHtml(message.content)}

+
+ `, + ) + .join("") + : `

当前没有消息。

`; +} + +function renderTasks() { + const tasks = tasksForSelectedSession(); + elements.taskList.innerHTML = tasks.length + ? tasks + .map( + (task) => ` +
+
+ ${escapeHtml(task.title)} + ${escapeHtml(task.status)} +
+

${escapeHtml(task.description)}

+
worker: ${escapeHtml(task.assignedWorkerId || "未分配")}
+
progress: ${task.progressPercent}%
+
summary: ${escapeHtml(task.summary || "暂无")}
+
+ + +
+
+ `, + ) + .join("") + : `

当前没有任务。

`; + + elements.taskList.querySelectorAll("[data-action]").forEach((button) => { + button.addEventListener("click", async () => { + const taskId = button.dataset.taskId; + const action = button.dataset.action; + await request(`/api/tasks/${taskId}/${action}`, { method: "POST", body: "{}" }); + await loadSession(state.selectedSessionId); + render(); + }); + }); +} + +function renderApprovals() { + const approvals = approvalsForSelectedSession(); + elements.approvalList.innerHTML = approvals.length + ? approvals + .map( + (approval) => ` +
+
+ ${escapeHtml(approval.summary)} + ${escapeHtml(approval.status)} +
+
risk: ${escapeHtml(approval.riskLevel)}
+
+ + +
+
+ `, + ) + .join("") + : `

当前没有待审批项。

`; + + elements.approvalList.querySelectorAll("[data-approval-id]").forEach((button) => { + button.addEventListener("click", async () => { + const approvalId = button.dataset.approvalId; + const approved = button.dataset.approved === "true"; + await request(`/api/approvals/${approvalId}/respond`, { + method: "POST", + body: JSON.stringify({ approved, responder: "web-user" }), + }); + await loadSession(state.selectedSessionId); + render(); + }); + }); +} + +function renderEvents() { + const events = eventsForSelectedSession(); + elements.eventList.innerHTML = events.length + ? events + .map( + (event) => ` +
+
+ ${escapeHtml(event.type)} + ${new Date(event.timestamp).toLocaleTimeString()} +
+
${escapeHtml(JSON.stringify(event.payload, null, 2))}
+
+ `, + ) + .join("") + : `

当前没有事件。

`; +} + +function render() { + renderSessions(); + renderWorkers(); + renderSessionHeader(); + renderMessages(); + renderTasks(); + renderApprovals(); + renderEvents(); +} + +async function loadBootstrap() { + const bootstrap = await request("/api/bootstrap"); + state.sessions = bootstrap.sessions; + state.messages = bootstrap.messages; + state.tasks = bootstrap.tasks; + state.workers = bootstrap.workers; + state.approvals = bootstrap.approvals; + state.events = bootstrap.events; + if (!state.selectedSessionId && state.sessions[0]) { + state.selectedSessionId = state.sessions[0].id; + } +} + +async function loadSession(sessionId) { + if (!sessionId) return; + const details = await request(`/api/sessions/${sessionId}`); + state.sessions = state.sessions.map((session) => (session.id === sessionId ? details.session : session)); + state.messages = [ + ...state.messages.filter((message) => message.sessionId !== sessionId), + ...details.messages, + ]; + state.tasks = [ + ...state.tasks.filter((task) => task.sessionId !== sessionId), + ...details.tasks, + ]; + state.approvals = [ + ...state.approvals.filter((approval) => approval.sessionId !== sessionId), + ...details.approvals, + ]; +} + +elements.createSessionForm.addEventListener("submit", async (event) => { + event.preventDefault(); + const title = elements.sessionTitleInput.value.trim(); + const details = await request("/api/sessions", { + method: "POST", + body: JSON.stringify({ title }), + }); + state.sessions.unshift(details.session); + state.selectedSessionId = details.session.id; + await loadSession(details.session.id); + elements.sessionTitleInput.value = ""; + render(); +}); + +elements.messageForm.addEventListener("submit", async (event) => { + event.preventDefault(); + if (!state.selectedSessionId) return; + const content = elements.messageInput.value.trim(); + if (!content) return; + await request(`/api/sessions/${state.selectedSessionId}/messages`, { + method: "POST", + body: JSON.stringify({ content, channel: "web" }), + }); + elements.messageInput.value = ""; + await loadSession(state.selectedSessionId); + await loadBootstrap(); + render(); +}); + +elements.resetDemo.addEventListener("click", async () => { + await request("/api/demo/reset", { method: "POST", body: "{}" }); + state.sessions = []; + state.messages = []; + state.tasks = []; + state.workers = []; + state.approvals = []; + state.events = []; + state.selectedSessionId = null; + render(); +}); + +const stream = new EventSource("/api/events/stream"); +stream.onmessage = async (event) => { + const payload = JSON.parse(event.data); + state.events.push(payload); + if (payload.sessionId) { + await loadSession(payload.sessionId); + } + await loadBootstrap(); + render(); +}; + +loadBootstrap().then(render).catch((error) => { + console.error(error); + elements.sessionSummary.textContent = error.message; +}); + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..61a425f --- /dev/null +++ b/public/index.html @@ -0,0 +1,92 @@ + + + + + + Boss Control Plane + + + +
+ + +
+
+
+

选择一个项目会话

+

创建会话后,在这里持续对话并观察任务状态。

+
+
+ +
+
+
+

对话

+
+
+
+ + +
+
+ +
+
+

任务树

+
+
+
+
+ +
+
+
+

审批

+
+
+
+ +
+
+

事件流

+
+
+
+
+
+
+ + + + + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..d883df1 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,219 @@ +:root { + color-scheme: light; + --bg: #f3efe4; + --panel: rgba(255, 252, 245, 0.88); + --line: rgba(43, 40, 35, 0.12); + --text: #1d1c19; + --muted: #716c61; + --accent: #1f6feb; + --danger: #b42318; + --shadow: 0 18px 48px rgba(31, 28, 25, 0.08); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "IBM Plex Sans", "Source Han Sans SC", sans-serif; + background: + radial-gradient(circle at top left, rgba(31, 111, 235, 0.12), transparent 24rem), + linear-gradient(180deg, #f8f5ec 0%, var(--bg) 100%); + color: var(--text); +} + +button, +input, +textarea { + font: inherit; +} + +button { + border: 0; + border-radius: 12px; + background: var(--accent); + color: white; + padding: 0.75rem 1rem; + cursor: pointer; +} + +button.ghost { + background: transparent; + color: var(--text); + border: 1px solid var(--line); +} + +button.danger { + color: var(--danger); +} + +input, +textarea { + width: 100%; + border-radius: 12px; + border: 1px solid var(--line); + background: white; + padding: 0.75rem 0.875rem; +} + +.shell { + min-height: 100vh; + display: grid; + grid-template-columns: 320px 1fr; +} + +.sidebar, +.content { + padding: 1.5rem; +} + +.content { + display: grid; + gap: 1rem; +} + +.panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: var(--shadow); + padding: 1rem; + backdrop-filter: blur(12px); +} + +.panel-header, +.row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.between { + justify-content: space-between; +} + +.hero { + padding: 1.5rem; +} + +.grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.stack { + display: grid; + gap: 0.75rem; +} + +.list, +.timeline { + display: grid; + gap: 0.75rem; + max-height: 32rem; + overflow: auto; +} + +.timeline.compact { + max-height: 24rem; +} + +.session-item, +.card, +.message, +.event { + border: 1px solid var(--line); + border-radius: 18px; + background: rgba(255, 255, 255, 0.82); + padding: 0.875rem; +} + +.session-item { + text-align: left; + display: grid; + gap: 0.35rem; + width: 100%; + background: white; + color: var(--text); +} + +.session-item.active { + outline: 2px solid rgba(31, 111, 235, 0.2); +} + +.message header, +.event header { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + color: var(--muted); + margin-bottom: 0.5rem; +} + +.message.manager { + background: rgba(31, 111, 235, 0.08); +} + +.badge { + border-radius: 999px; + padding: 0.2rem 0.55rem; + font-size: 0.8rem; + border: 1px solid var(--line); + background: white; +} + +.badge.running, +.badge.busy { + color: var(--accent); +} + +.badge.failed, +.badge.cancelled, +.badge.rejected { + color: var(--danger); +} + +.badge.completed, +.badge.approved, +.badge.idle { + color: #0f7b55; +} + +.tags, +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.tags span { + border-radius: 999px; + padding: 0.18rem 0.5rem; + background: rgba(31, 111, 235, 0.1); + color: var(--accent); + font-size: 0.82rem; +} + +.caption, +.muted { + color: var(--muted); +} + +pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} + +@media (max-width: 1100px) { + .shell { + grid-template-columns: 1fr; + } + + .grid { + grid-template-columns: 1fr; + } +} diff --git a/src/engine.ts b/src/engine.ts new file mode 100644 index 0000000..d528709 --- /dev/null +++ b/src/engine.ts @@ -0,0 +1,656 @@ +import { resolve } from "node:path"; +import type { + ApprovalRequest, + AppState, + BossEvent, + Message, + Session, + SessionDetails, + Task, + WorkerNode, +} from "./types.js"; +import { EventBroker } from "./event-broker.js"; +import { createPlan, buildPlannerMessage, materializeTasks } from "./planner.js"; +import { chooseAssignmentCandidates } from "./scheduler.js"; +import { FileStore } from "./store.js"; +import { createId, now } from "./utils.js"; + +const DATA_FILE = resolve(process.cwd(), ".boss-data", "store.json"); + +function isActiveTask(task: Task): boolean { + return !["completed", "failed", "cancelled"].includes(task.status); +} + +export class BossEngine { + readonly store = new FileStore(DATA_FILE); + readonly events = new EventBroker(); + + getState(): AppState { + return this.store.snapshot; + } + + bootstrap(): AppState { + return this.getState(); + } + + createSession(title?: string): SessionDetails { + const timestamp = now(); + const session: Session = { + id: createId("session"), + title: title?.trim() || "未命名项目", + status: "active", + activeObjective: "", + lastPlannerSummary: "", + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.store.mutate((state) => { + state.sessions.unshift(session); + state.events.push( + this.makeEvent({ + sessionId: session.id, + taskId: null, + source: "system", + type: "session.created", + payload: { title: session.title }, + }), + ); + }); + + this.publishLatestEvent(); + return this.getSession(session.id); + } + + getSession(sessionId: string): SessionDetails { + const state = this.getState(); + const session = state.sessions.find((candidate) => candidate.id === sessionId); + if (!session) { + throw new Error(`Session not found: ${sessionId}`); + } + + return { + session, + messages: state.messages.filter((message) => message.sessionId === sessionId), + tasks: state.tasks.filter((task) => task.sessionId === sessionId), + approvals: state.approvals.filter((approval) => approval.sessionId === sessionId), + }; + } + + listSessions(): Session[] { + return this.getState().sessions; + } + + addMessage(sessionId: string, content: string, channel = "web"): SessionDetails { + const session = this.getSession(sessionId).session; + const message: Message = { + id: createId("msg"), + sessionId, + role: "user", + channel, + content: content.trim(), + createdAt: now(), + }; + + if (!message.content) { + throw new Error("Message content is required."); + } + + this.store.mutate((state) => { + const mutableSession = state.sessions.find((candidate) => candidate.id === sessionId); + if (!mutableSession) { + throw new Error(`Session not found: ${sessionId}`); + } + + mutableSession.activeObjective = message.content; + mutableSession.updatedAt = message.createdAt; + if (!mutableSession.title || mutableSession.title === "未命名项目") { + mutableSession.title = message.content.slice(0, 32); + } + state.messages.push(message); + state.events.push( + this.makeEvent({ + sessionId, + taskId: null, + source: "user", + type: "session.message.added", + payload: { + channel, + content: message.content, + }, + }), + ); + }); + + this.publishLatestEvent(); + this.applyPlan(session, message.content); + return this.getSession(sessionId); + } + + registerWorker(input: { + name: string; + os: WorkerNode["os"]; + capabilities: string[]; + }): WorkerNode { + const timestamp = now(); + const existing = this.getState().workers.find((worker) => worker.name === input.name); + if (existing) { + return this.updateWorker(existing.id, { + os: input.os, + capabilities: input.capabilities, + status: "idle", + load: 0, + }); + } + + const worker: WorkerNode = { + id: createId("worker"), + name: input.name, + os: input.os, + capabilities: Array.from(new Set(input.capabilities)), + status: "idle", + currentTaskId: null, + load: 0, + lastSeenAt: timestamp, + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.store.mutate((state) => { + state.workers.push(worker); + state.events.push( + this.makeEvent({ + sessionId: null, + taskId: null, + source: "system", + type: "worker.registered", + payload: { + workerId: worker.id, + name: worker.name, + os: worker.os, + capabilities: worker.capabilities, + }, + }), + ); + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return worker; + } + + updateWorker( + workerId: string, + input: Partial>, + ): WorkerNode { + let updated!: WorkerNode; + this.store.mutate((state) => { + const worker = state.workers.find((candidate) => candidate.id === workerId); + if (!worker) { + throw new Error(`Worker not found: ${workerId}`); + } + + if (input.os) worker.os = input.os; + if (input.capabilities) worker.capabilities = Array.from(new Set(input.capabilities)); + if (input.status) worker.status = input.status; + if (typeof input.load === "number") worker.load = input.load; + worker.updatedAt = now(); + worker.lastSeenAt = worker.updatedAt; + updated = { ...worker }; + }); + return updated; + } + + heartbeat(workerId: string, load = 0): WorkerNode { + let updated!: WorkerNode; + this.store.mutate((state) => { + const worker = state.workers.find((candidate) => candidate.id === workerId); + if (!worker) { + throw new Error(`Worker not found: ${workerId}`); + } + + worker.lastSeenAt = now(); + worker.updatedAt = worker.lastSeenAt; + worker.load = load; + if (!worker.currentTaskId && worker.status !== "offline") { + worker.status = "idle"; + } + + state.events.push( + this.makeEvent({ + sessionId: null, + taskId: worker.currentTaskId, + source: "worker", + type: "worker.heartbeat", + payload: { + workerId: worker.id, + status: worker.status, + load: worker.load, + }, + }), + ); + + updated = { ...worker }; + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return updated; + } + + claimNextTask(workerId: string): Task | null { + let claimedTask: Task | null = null; + + this.store.mutate((state) => { + const worker = state.workers.find((candidate) => candidate.id === workerId); + if (!worker) { + throw new Error(`Worker not found: ${workerId}`); + } + + const task = state.tasks.find( + (candidate) => candidate.assignedWorkerId === workerId && candidate.status === "assigned", + ); + if (!task) { + return; + } + + task.status = "running"; + task.currentStep = "start"; + task.summary = "任务已被 worker 接收。"; + task.updatedAt = now(); + worker.status = "busy"; + worker.currentTaskId = task.id; + worker.updatedAt = task.updatedAt; + worker.lastSeenAt = task.updatedAt; + + claimedTask = { ...task }; + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source: "worker", + type: "task.started", + payload: { + workerId, + title: task.title, + }, + }), + ); + }); + + if (claimedTask) { + this.publishLatestEvent(); + } + + return claimedTask; + } + + reportProgress( + taskId: string, + workerId: string, + input: { + progressPercent: number; + summary: string; + currentStep: string; + nextStep: string; + }, + ): Task { + let updated!: Task; + this.store.mutate((state) => { + const task = state.tasks.find((candidate) => candidate.id === taskId); + if (!task) { + throw new Error(`Task not found: ${taskId}`); + } + + task.progressPercent = Math.max(0, Math.min(100, input.progressPercent)); + task.summary = input.summary; + task.currentStep = input.currentStep; + task.nextStep = input.nextStep; + task.updatedAt = now(); + if (task.status === "assigned") { + task.status = "running"; + } + + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source: "worker", + type: "task.progress", + payload: { + workerId, + progressPercent: task.progressPercent, + summary: task.summary, + currentStep: task.currentStep, + nextStep: task.nextStep, + }, + }), + ); + updated = { ...task }; + }); + + this.publishLatestEvent(); + return updated; + } + + completeTask(taskId: string, workerId: string, summary: string): Task { + let updated!: Task; + + this.store.mutate((state) => { + const task = state.tasks.find((candidate) => candidate.id === taskId); + const worker = state.workers.find((candidate) => candidate.id === workerId); + if (!task) { + throw new Error(`Task not found: ${taskId}`); + } + if (!worker) { + throw new Error(`Worker not found: ${workerId}`); + } + + task.status = "completed"; + task.progressPercent = 100; + task.summary = summary; + task.currentStep = "done"; + task.nextStep = ""; + task.updatedAt = now(); + worker.status = "idle"; + worker.currentTaskId = null; + worker.updatedAt = task.updatedAt; + worker.lastSeenAt = task.updatedAt; + + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source: "worker", + type: "task.completed", + payload: { + workerId, + summary, + }, + }), + ); + updated = { ...task }; + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return updated; + } + + failTask(taskId: string, workerId: string, errorMessage: string): Task { + let updated!: Task; + + this.store.mutate((state) => { + const task = state.tasks.find((candidate) => candidate.id === taskId); + const worker = state.workers.find((candidate) => candidate.id === workerId); + if (!task) { + throw new Error(`Task not found: ${taskId}`); + } + if (!worker) { + throw new Error(`Worker not found: ${workerId}`); + } + + task.status = "failed"; + task.summary = errorMessage; + task.currentStep = "failed"; + task.nextStep = ""; + task.updatedAt = now(); + worker.status = "idle"; + worker.currentTaskId = null; + worker.updatedAt = task.updatedAt; + worker.lastSeenAt = task.updatedAt; + + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source: "worker", + type: "task.failed", + payload: { + workerId, + errorMessage, + }, + }), + ); + updated = { ...task }; + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return updated; + } + + pauseTask(taskId: string): Task { + return this.transitionTask(taskId, "paused", "system", "task.paused", { + summary: "任务已被暂停。", + }); + } + + cancelTask(taskId: string): Task { + return this.transitionTask(taskId, "cancelled", "system", "task.cancelled", { + summary: "任务已被取消。", + }); + } + + respondApproval(approvalId: string, approved: boolean, responder: string): ApprovalRequest { + let updatedApproval!: ApprovalRequest; + + this.store.mutate((state) => { + const approval = state.approvals.find((candidate) => candidate.id === approvalId); + if (!approval) { + throw new Error(`Approval not found: ${approvalId}`); + } + + const task = state.tasks.find((candidate) => candidate.id === approval.taskId); + if (!task) { + throw new Error(`Task not found for approval: ${approval.taskId}`); + } + + const timestamp = now(); + approval.status = approved ? "approved" : "rejected"; + approval.responder = responder; + approval.updatedAt = timestamp; + task.approvalStatus = approval.status; + task.updatedAt = timestamp; + task.status = approved ? "queued" : "cancelled"; + task.summary = approved ? "审批已通过,重新进入队列。" : "审批被拒绝,任务已取消。"; + + state.events.push( + this.makeEvent({ + sessionId: approval.sessionId, + taskId: approval.taskId, + source: "system", + type: approved ? "approval.approved" : "approval.rejected", + payload: { + approvalId, + responder, + }, + }), + ); + updatedApproval = { ...approval }; + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return updatedApproval; + } + + private applyPlan(session: Session, content: string): void { + const sessionDetails = this.getSession(session.id); + const result = createPlan(sessionDetails.session, content, sessionDetails.tasks.filter(isActiveTask)); + const tasks = materializeTasks(session.id, result); + const plannerMessage = buildPlannerMessage(result.summary); + const timestamp = now(); + + this.store.mutate((state) => { + const mutableSession = state.sessions.find((candidate) => candidate.id === session.id); + if (!mutableSession) { + throw new Error(`Session not found: ${session.id}`); + } + + mutableSession.activeObjective = content; + mutableSession.lastPlannerSummary = plannerMessage; + mutableSession.updatedAt = timestamp; + + if (result.pauseExistingTasks) { + for (const task of state.tasks.filter( + (candidate) => candidate.sessionId === session.id && isActiveTask(candidate), + )) { + if (["running", "assigned", "queued", "planning", "blocked"].includes(task.status)) { + task.status = "paused"; + task.updatedAt = timestamp; + } + } + } + + state.tasks.push(...tasks); + for (const task of tasks) { + if (task.approvalStatus === "pending") { + state.approvals.push({ + id: createId("approval"), + sessionId: session.id, + taskId: task.id, + kind: "dangerous_action", + summary: `任务 "${task.title}" 包含高风险关键词,需要审批后才能执行。`, + riskLevel: "high", + status: "pending", + requester: "manager", + responder: null, + createdAt: timestamp, + updatedAt: timestamp, + }); + } + } + + const managerMessage: Message = { + id: createId("msg"), + sessionId: session.id, + role: "manager", + channel: "system", + content: plannerMessage, + createdAt: timestamp, + }; + state.messages.push(managerMessage); + + state.events.push( + this.makeEvent({ + sessionId: session.id, + taskId: null, + source: "manager", + type: "plan.created", + payload: { + summary: result.summary, + taskIds: tasks.map((task) => task.id), + }, + }), + ); + }); + + this.publishLatestEvent(); + this.syncAssignments(); + } + + private transitionTask( + taskId: string, + status: Task["status"], + source: BossEvent["source"], + eventType: string, + payload: Record, + ): Task { + let updated!: Task; + this.store.mutate((state) => { + const task = state.tasks.find((candidate) => candidate.id === taskId); + if (!task) { + throw new Error(`Task not found: ${taskId}`); + } + + task.status = status; + task.updatedAt = now(); + task.summary = typeof payload.summary === "string" ? payload.summary : task.summary; + + if (task.assignedWorkerId) { + const worker = state.workers.find((candidate) => candidate.id === task.assignedWorkerId); + if (worker && worker.currentTaskId === task.id) { + worker.currentTaskId = null; + worker.status = "idle"; + worker.updatedAt = task.updatedAt; + worker.lastSeenAt = task.updatedAt; + } + } + + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source, + type: eventType, + payload, + }), + ); + updated = { ...task }; + }); + + this.publishLatestEvent(); + this.syncAssignments(); + return updated; + } + + private syncAssignments(): void { + const candidates = chooseAssignmentCandidates(this.getState()); + if (candidates.length === 0) { + return; + } + + this.store.mutate((state) => { + for (const candidate of candidates) { + const task = state.tasks.find((item) => item.id === candidate.taskId); + const worker = state.workers.find((item) => item.id === candidate.workerId); + if (!task || !worker || task.status !== "queued" || worker.status !== "idle") { + continue; + } + + const timestamp = now(); + task.status = "assigned"; + task.assignedWorkerId = worker.id; + task.updatedAt = timestamp; + task.summary = `已分配给 ${worker.name}`; + worker.status = "busy"; + worker.currentTaskId = task.id; + worker.updatedAt = timestamp; + worker.lastSeenAt = timestamp; + + state.events.push( + this.makeEvent({ + sessionId: task.sessionId, + taskId: task.id, + source: "system", + type: "task.assigned", + payload: { + workerId: worker.id, + workerName: worker.name, + }, + }), + ); + } + }); + + this.publishLatestEvent(); + } + + private publishLatestEvent(): void { + const state = this.getState(); + const latestEvent = state.events[state.events.length - 1]; + if (latestEvent) { + this.events.publish(latestEvent); + } + } + + private makeEvent(input: Omit): BossEvent { + return { + id: createId("evt"), + timestamp: now(), + ...input, + }; + } +} + diff --git a/src/event-broker.ts b/src/event-broker.ts new file mode 100644 index 0000000..84f122b --- /dev/null +++ b/src/event-broker.ts @@ -0,0 +1,19 @@ +import type { BossEvent } from "./types.js"; + +type Listener = (event: BossEvent) => void; + +export class EventBroker { + private readonly listeners = new Set(); + + subscribe(listener: Listener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + publish(event: BossEvent): void { + for (const listener of this.listeners) { + listener(event); + } + } +} + diff --git a/src/planner.ts b/src/planner.ts new file mode 100644 index 0000000..e22ed68 --- /dev/null +++ b/src/planner.ts @@ -0,0 +1,181 @@ +import type { Session, Task } from "./types.js"; +import { containsKeyword, createId, now } from "./utils.js"; + +interface DraftTask { + title: string; + description: string; + kind: string; + requiredOs: Task["requiredOs"]; + requiredCapabilities: string[]; + priority: Task["priority"]; + dependencyIndexes: number[]; + approvalStatus: Task["approvalStatus"]; +} + +export interface PlannerResult { + summary: string; + tasks: DraftTask[]; + pauseExistingTasks: boolean; +} + +function inferRequiredOs(content: string): Task["requiredOs"] { + if (containsKeyword(content, ["mac", "macos", "xcode", "ios", "swift"])) { + return "macos"; + } + + if (containsKeyword(content, ["windows", "win", "powershell", ".exe", "注册表"])) { + return "windows"; + } + + return "any"; +} + +function inferCapabilities(content: string): string[] { + const capabilities = ["terminal"]; + + if (containsKeyword(content, ["ui", "browser", "web", "playwright", "页面", "前端"])) { + capabilities.push("browser"); + } + + if (containsKeyword(content, ["test", "测试", "验证", "ci"])) { + capabilities.push("test"); + } + + return Array.from(new Set(capabilities)); +} + +function requiresApproval(content: string): boolean { + return containsKeyword(content, [ + "rm -rf", + "delete", + "删除", + "force push", + "强推", + "drop database", + ]); +} + +export function createPlan(session: Session, content: string, activeTasks: Task[]): PlannerResult { + const baseOs = inferRequiredOs(content); + const baseCapabilities = inferCapabilities(content); + const approvalStatus = requiresApproval(content) ? "pending" : "not_required"; + const pauseExistingTasks = activeTasks.some((task) => + ["planning", "queued", "assigned", "running", "paused", "blocked"].includes(task.status), + ); + const replan = pauseExistingTasks; + + if (containsKeyword(content, ["调研", "研究", "定位", "排查", "分析"])) { + return { + summary: replan + ? `已根据新要求重排调研任务:${content}` + : `已生成调研型任务树:${content}`, + pauseExistingTasks: replan, + tasks: [ + { + title: "并行收集线索与上下文", + description: `围绕目标开展初步调研:${content}`, + kind: "research", + requiredOs: "any", + requiredCapabilities: ["terminal"], + priority: "high", + dependencyIndexes: [], + approvalStatus, + }, + { + title: "定位问题根因或约束", + description: `在合适设备上并行定位问题:${content}`, + kind: "investigation", + requiredOs: baseOs, + requiredCapabilities: baseCapabilities, + priority: "high", + dependencyIndexes: [], + approvalStatus, + }, + { + title: "整理结论与执行建议", + description: "汇总两条并行调研结果,并输出下一步建议。", + kind: "summary", + requiredOs: "any", + requiredCapabilities: ["terminal"], + priority: "medium", + dependencyIndexes: [0, 1], + approvalStatus: "not_required", + }, + ], + }; + } + + return { + summary: replan + ? `已根据最新需求重排执行计划:${content}` + : `已生成执行型任务树:${content}`, + pauseExistingTasks: replan, + tasks: [ + { + title: "分析需求并准备执行环境", + description: `分析用户目标并确认执行边界:${content}`, + kind: "planning", + requiredOs: "any", + requiredCapabilities: ["terminal"], + priority: "high", + dependencyIndexes: [], + approvalStatus: "not_required", + }, + { + title: "实现核心变更", + description: `在匹配设备上执行主任务:${content}`, + kind: "implementation", + requiredOs: baseOs, + requiredCapabilities: baseCapabilities, + priority: "high", + dependencyIndexes: [0], + approvalStatus, + }, + { + title: "验证结果并整理回报", + description: "运行验证步骤,输出风险和下一步建议。", + kind: "validation", + requiredOs: "any", + requiredCapabilities: Array.from(new Set([...baseCapabilities, "test"])), + priority: "medium", + dependencyIndexes: [1], + approvalStatus: "not_required", + }, + ], + }; +} + +export function materializeTasks(sessionId: string, result: PlannerResult): Task[] { + const createdAt = now(); + const placeholders = result.tasks.map((draft) => ({ + id: createId("task"), + draft, + })); + + return placeholders.map(({ id, draft }) => ({ + id, + sessionId, + parentTaskId: null, + title: draft.title, + description: draft.description, + kind: draft.kind, + status: draft.approvalStatus === "pending" ? "waiting_approval" : "queued", + priority: draft.priority, + requiredOs: draft.requiredOs, + requiredCapabilities: draft.requiredCapabilities, + dependencyIds: draft.dependencyIndexes.map((index) => placeholders[index].id), + assignedWorkerId: null, + approvalStatus: draft.approvalStatus, + progressPercent: 0, + summary: "", + currentStep: "", + nextStep: "", + createdAt, + updatedAt: createdAt, + })); +} + +export function buildPlannerMessage(summary: string): string { + return `${summary}。系统会继续调度可执行子任务,并在需要审批时暂停。`; +} + diff --git a/src/scheduler.ts b/src/scheduler.ts new file mode 100644 index 0000000..6141205 --- /dev/null +++ b/src/scheduler.ts @@ -0,0 +1,64 @@ +import type { AppState, Task, WorkerNode } from "./types.js"; + +function dependenciesSatisfied(task: Task, state: AppState): boolean { + return task.dependencyIds.every((dependencyId) => { + const dependency = state.tasks.find((candidate) => candidate.id === dependencyId); + return dependency?.status === "completed"; + }); +} + +function workerIsIdle(worker: WorkerNode): boolean { + return worker.status === "idle" && !worker.currentTaskId; +} + +function scoreWorker(task: Task, worker: WorkerNode): number { + let score = 0; + + if (task.requiredOs === "any" || task.requiredOs === worker.os) { + score += 10; + } + + for (const capability of task.requiredCapabilities) { + if (worker.capabilities.includes(capability)) { + score += 4; + } + } + + return score - worker.load; +} + +export function chooseAssignmentCandidates(state: AppState): Array<{ + taskId: string; + workerId: string; +}> { + const tasks = state.tasks + .filter((task) => task.status === "queued" && dependenciesSatisfied(task, state)) + .sort((left, right) => left.createdAt.localeCompare(right.createdAt)); + const availableWorkers = new Map( + state.workers.filter(workerIsIdle).map((worker) => [worker.id, worker]), + ); + const assignments: Array<{ taskId: string; workerId: string }> = []; + + for (const task of tasks) { + let bestWorker: WorkerNode | null = null; + let bestScore = Number.NEGATIVE_INFINITY; + + for (const worker of availableWorkers.values()) { + const score = scoreWorker(task, worker); + if (score > bestScore) { + bestScore = score; + bestWorker = worker; + } + } + + if (!bestWorker || bestScore < 0) { + continue; + } + + assignments.push({ taskId: task.id, workerId: bestWorker.id }); + availableWorkers.delete(bestWorker.id); + } + + return assignments; +} + diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..819ce20 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,153 @@ +import path from "node:path"; +import Fastify from "fastify"; +import fastifyStatic from "@fastify/static"; +import { BossEngine } from "./engine.js"; + +const engine = new BossEngine(); +const app = Fastify({ logger: true }); + +await app.register(fastifyStatic, { + root: path.resolve(process.cwd(), "public"), + prefix: "/", +}); + +function ok() { + return { ok: true }; +} + +app.get("/api/health", async () => ({ + status: "ok", + sessions: engine.getState().sessions.length, + workers: engine.getState().workers.length, +})); + +app.get("/api/bootstrap", async () => engine.bootstrap()); + +app.get("/api/sessions", async () => engine.listSessions()); + +app.post("/api/sessions", async (request) => { + const body = (request.body ?? {}) as { title?: string }; + return engine.createSession(body.title); +}); + +app.get("/api/sessions/:sessionId", async (request) => { + const params = request.params as { sessionId: string }; + return engine.getSession(params.sessionId); +}); + +app.post("/api/sessions/:sessionId/messages", async (request) => { + const params = request.params as { sessionId: string }; + const body = (request.body ?? {}) as { content?: string; channel?: string }; + return engine.addMessage(params.sessionId, body.content ?? "", body.channel ?? "web"); +}); + +app.get("/api/events/stream", async (_request, reply) => { + reply.raw.setHeader("Content-Type", "text/event-stream"); + reply.raw.setHeader("Cache-Control", "no-cache"); + reply.raw.setHeader("Connection", "keep-alive"); + reply.raw.flushHeaders?.(); + + const unsubscribe = engine.events.subscribe((event) => { + reply.raw.write(`data: ${JSON.stringify(event)}\n\n`); + }); + + const interval = setInterval(() => { + reply.raw.write(": ping\n\n"); + }, 15_000); + + reply.raw.on("close", () => { + clearInterval(interval); + unsubscribe(); + reply.raw.end(); + }); +}); + +app.get("/api/workers", async () => engine.getState().workers); + +app.post("/api/workers/register", async (request) => { + const body = request.body as { + name?: string; + os?: "windows" | "macos" | "linux"; + capabilities?: string[]; + }; + return engine.registerWorker({ + name: body.name ?? "worker", + os: body.os ?? "linux", + capabilities: body.capabilities ?? ["terminal"], + }); +}); + +app.post("/api/workers/:workerId/heartbeat", async (request) => { + const params = request.params as { workerId: string }; + const body = (request.body ?? {}) as { load?: number }; + return engine.heartbeat(params.workerId, body.load ?? 0); +}); + +app.post("/api/workers/:workerId/claim-next", async (request) => { + const params = request.params as { workerId: string }; + return { + task: engine.claimNextTask(params.workerId), + }; +}); + +app.post("/api/tasks/:taskId/progress", async (request) => { + const params = request.params as { taskId: string }; + const body = request.body as { + workerId: string; + progressPercent: number; + summary: string; + currentStep: string; + nextStep: string; + }; + return engine.reportProgress(params.taskId, body.workerId, { + progressPercent: body.progressPercent, + summary: body.summary, + currentStep: body.currentStep, + nextStep: body.nextStep, + }); +}); + +app.post("/api/tasks/:taskId/complete", async (request) => { + const params = request.params as { taskId: string }; + const body = request.body as { + workerId: string; + summary: string; + }; + return engine.completeTask(params.taskId, body.workerId, body.summary); +}); + +app.post("/api/tasks/:taskId/fail", async (request) => { + const params = request.params as { taskId: string }; + const body = request.body as { + workerId: string; + errorMessage: string; + }; + return engine.failTask(params.taskId, body.workerId, body.errorMessage); +}); + +app.post("/api/tasks/:taskId/pause", async (request) => { + const params = request.params as { taskId: string }; + return engine.pauseTask(params.taskId); +}); + +app.post("/api/tasks/:taskId/cancel", async (request) => { + const params = request.params as { taskId: string }; + return engine.cancelTask(params.taskId); +}); + +app.post("/api/approvals/:approvalId/respond", async (request) => { + const params = request.params as { approvalId: string }; + const body = request.body as { + approved: boolean; + responder?: string; + }; + return engine.respondApproval(params.approvalId, body.approved, body.responder ?? "user"); +}); + +app.post("/api/demo/reset", async () => { + engine.store.reset(); + return ok(); +}); + +const port = Number(process.env.PORT ?? 43210); +await app.listen({ port, host: "0.0.0.0" }); diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..5919224 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,61 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; +import type { AppState } from "./types.js"; + +function defaultState(): AppState { + return { + sessions: [], + messages: [], + tasks: [], + workers: [], + approvals: [], + events: [], + }; +} + +export class FileStore { + private state: AppState; + + constructor(private readonly filePath: string) { + this.ensureDirectory(); + this.state = this.load(); + } + + get snapshot(): AppState { + return structuredClone(this.state); + } + + mutate(mutator: (state: AppState) => T): T { + const result = mutator(this.state); + this.save(); + return result; + } + + reset(): AppState { + this.state = defaultState(); + this.save(); + return this.snapshot; + } + + private ensureDirectory(): void { + mkdirSync(dirname(this.filePath), { recursive: true }); + } + + private load(): AppState { + if (!existsSync(this.filePath)) { + return defaultState(); + } + + try { + const raw = readFileSync(this.filePath, "utf8"); + return { ...defaultState(), ...(JSON.parse(raw) as AppState) }; + } catch { + return defaultState(); + } + } + + private save(): void { + writeFileSync(this.filePath, `${JSON.stringify(this.state, null, 2)}\n`, "utf8"); + } +} + diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b96587b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,110 @@ +export type SessionStatus = "active" | "archived"; +export type TaskStatus = + | "planning" + | "queued" + | "assigned" + | "running" + | "blocked" + | "paused" + | "waiting_approval" + | "completed" + | "failed" + | "cancelled"; +export type WorkerStatus = "idle" | "busy" | "offline"; +export type ApprovalStatus = "pending" | "approved" | "rejected"; +export type RiskLevel = "low" | "medium" | "high"; + +export interface Session { + id: string; + title: string; + status: SessionStatus; + activeObjective: string; + lastPlannerSummary: string; + createdAt: string; + updatedAt: string; +} + +export interface Message { + id: string; + sessionId: string; + role: "user" | "manager" | "system"; + channel: string; + content: string; + createdAt: string; +} + +export interface Task { + id: string; + sessionId: string; + parentTaskId: string | null; + title: string; + description: string; + kind: string; + status: TaskStatus; + priority: "low" | "medium" | "high"; + requiredOs: "any" | "windows" | "macos" | "linux"; + requiredCapabilities: string[]; + dependencyIds: string[]; + assignedWorkerId: string | null; + approvalStatus: "not_required" | ApprovalStatus; + progressPercent: number; + summary: string; + currentStep: string; + nextStep: string; + createdAt: string; + updatedAt: string; +} + +export interface WorkerNode { + id: string; + name: string; + os: "windows" | "macos" | "linux"; + capabilities: string[]; + status: WorkerStatus; + currentTaskId: string | null; + load: number; + lastSeenAt: string; + createdAt: string; + updatedAt: string; +} + +export interface ApprovalRequest { + id: string; + sessionId: string; + taskId: string; + kind: string; + summary: string; + riskLevel: RiskLevel; + status: ApprovalStatus; + requester: string; + responder: string | null; + createdAt: string; + updatedAt: string; +} + +export interface BossEvent { + id: string; + sessionId: string | null; + taskId: string | null; + source: "user" | "manager" | "system" | "worker"; + type: string; + timestamp: string; + payload: Record; +} + +export interface AppState { + sessions: Session[]; + messages: Message[]; + tasks: Task[]; + workers: WorkerNode[]; + approvals: ApprovalRequest[]; + events: BossEvent[]; +} + +export interface SessionDetails { + session: Session; + messages: Message[]; + tasks: Task[]; + approvals: ApprovalRequest[]; +} + diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6c2a4d6 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,15 @@ +import { randomUUID } from "node:crypto"; + +export function createId(prefix: string): string { + return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}`; +} + +export function now(): string { + return new Date().toISOString(); +} + +export function containsKeyword(input: string, keywords: string[]): boolean { + const normalized = input.toLowerCase(); + return keywords.some((keyword) => normalized.includes(keyword.toLowerCase())); +} + diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..88b5551 --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,130 @@ +import { setTimeout as delay } from "node:timers/promises"; + +interface Task { + id: string; + title: string; + description: string; + kind: string; +} + +function parseArgs(argv: string[]) { + const options = { + name: "", + os: "linux", + capabilities: ["terminal"], + server: "http://127.0.0.1:43210", + }; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === "--name") { + options.name = argv[index + 1] ?? ""; + index += 1; + } else if (token === "--os") { + options.os = argv[index + 1] ?? "linux"; + index += 1; + } else if (token === "--capability") { + options.capabilities.push(argv[index + 1] ?? "terminal"); + index += 1; + } else if (token === "--server") { + options.server = argv[index + 1] ?? options.server; + index += 1; + } + } + + if (!options.name) { + throw new Error("Usage: npm run worker -- --name [--os windows|macos|linux]"); + } + + options.capabilities = Array.from(new Set(options.capabilities)); + return options; +} + +async function postJson(url: string, body: unknown) { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +async function simulateTask(server: string, workerId: string, task: Task) { + const steps = [ + { + progressPercent: 20, + summary: `${task.title}: 收集上下文和工作目录`, + currentStep: "prepare", + nextStep: "inspect", + }, + { + progressPercent: 55, + summary: `${task.title}: 正在执行主要步骤`, + currentStep: "execute", + nextStep: "verify", + }, + { + progressPercent: 85, + summary: `${task.title}: 正在整理结果`, + currentStep: "summarize", + nextStep: "complete", + }, + ]; + + for (const step of steps) { + await delay(1_500); + await postJson(`${server}/api/tasks/${task.id}/progress`, { + workerId, + ...step, + }); + } + + await delay(1_000); + await postJson(`${server}/api/tasks/${task.id}/complete`, { + workerId, + summary: `${task.title} 已完成。`, + }); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const worker = (await postJson(`${options.server}/api/workers/register`, { + name: options.name, + os: options.os, + capabilities: options.capabilities, + })) as { id: string; name: string }; + + console.log(`worker ready: ${worker.name} (${worker.id})`); + + for (;;) { + await postJson(`${options.server}/api/workers/${worker.id}/heartbeat`, { load: 0 }); + const response = (await postJson(`${options.server}/api/workers/${worker.id}/claim-next`, {})) as { + task: Task | null; + }; + + if (response.task) { + console.log(`claimed task: ${response.task.title}`); + try { + await simulateTask(options.server, worker.id, response.task); + } catch (error) { + const message = error instanceof Error ? error.message : "unknown error"; + await postJson(`${options.server}/api/tasks/${response.task.id}/fail`, { + workerId: worker.id, + errorMessage: message, + }); + } + } else { + await delay(2_500); + } + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a747518 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} +