diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..af7c582 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +.boss-data + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..88c40b2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-bookworm-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM deps AS build +COPY tsconfig.json ./ +COPY src ./src +COPY public ./public +COPY docs ./docs +COPY README.md ./ +RUN npm run build + +FROM node:22-bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/public ./public +COPY package.json package-lock.json README.md ./ +EXPOSE 43210 +CMD ["node", "dist/server.js"] + diff --git a/README.md b/README.md index 713d66e..1fa87d7 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Boss 是一个面向多设备开发协作的 agent control plane。 - SSE 实时事件流 - Web 控制台 - `boss-worker` 模拟执行器 +- `npm run smoke` 自动跑端到端验证 +- `Dockerfile` + `compose.yaml` 支持容器启动 ## 当前推荐方向 @@ -45,7 +47,7 @@ Boss 是一个面向多设备开发协作的 agent control plane。 ```bash npm install -npm run dev +npm run demo ``` 浏览器打开: @@ -54,10 +56,50 @@ npm run dev http://127.0.0.1:43210 ``` -另开终端启动 worker: +如果你只想单独启动服务端: + +```bash +npm run dev +``` + +如果你要手工启动 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 ``` + +一键本地 demo: + +```bash +npm install +npm run demo +``` + +这会拉起: + +- Web/API 服务 +- 2 台 Windows 模拟 worker +- 1 台 Mac 模拟 worker + +自动 smoke test: + +```bash +npm run smoke +``` + +容器启动: + +```bash +docker compose up --build +``` + +## 当前 v1 能力 + +- 创建项目会话并持续对话 +- 自动生成任务树并调度到不同 worker +- worker 心跳、掉线回收、任务重排 +- 审批、暂停、恢复、取消、重排 +- SSE 实时事件流和 Web 控制台 +- 一键 demo 启动 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..ee2d894 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,28 @@ +services: + boss: + build: . + ports: + - "43210:43210" + volumes: + - boss-data:/app/.boss-data + + worker-win-a: + build: . + depends_on: + - boss + command: ["node", "dist/worker.js", "--name", "win-a", "--os", "windows", "--capability", "terminal", "--capability", "browser", "--server", "http://boss:43210"] + + worker-win-b: + build: . + depends_on: + - boss + command: ["node", "dist/worker.js", "--name", "win-b", "--os", "windows", "--capability", "terminal", "--capability", "test", "--server", "http://boss:43210"] + + worker-mac-a: + build: . + depends_on: + - boss + command: ["node", "dist/worker.js", "--name", "mac-a", "--os", "macos", "--capability", "terminal", "--capability", "test", "--capability", "browser", "--server", "http://boss:43210"] + +volumes: + boss-data: diff --git a/package.json b/package.json index c50908a..4ba1c50 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "build": "tsc -p tsconfig.json", "start": "node dist/server.js", "check": "tsc --noEmit -p tsconfig.json", - "worker": "tsx src/worker.ts" + "worker": "tsx src/worker.ts", + "demo": "tsx src/demo.ts", + "smoke": "tsx src/smoke.ts" }, "dependencies": { "@fastify/static": "^8.2.0", @@ -20,4 +22,3 @@ "typescript": "^5.9.2" } } - diff --git a/public/app.js b/public/app.js index f57bf8d..35244d9 100644 --- a/public/app.js +++ b/public/app.js @@ -15,17 +15,23 @@ const elements = { taskList: document.querySelector("#task-list"), approvalList: document.querySelector("#approval-list"), eventList: document.querySelector("#event-list"), + planHint: document.querySelector("#plan-hint"), sessionTitleDisplay: document.querySelector("#session-title-display"), sessionSummary: document.querySelector("#session-summary"), createSessionForm: document.querySelector("#create-session-form"), sessionTitleInput: document.querySelector("#session-title"), + createWorkerForm: document.querySelector("#create-worker-form"), + workerName: document.querySelector("#worker-name"), + workerOs: document.querySelector("#worker-os"), + workerCapabilities: document.querySelector("#worker-capabilities"), messageForm: document.querySelector("#message-form"), messageInput: document.querySelector("#message-input"), resetDemo: document.querySelector("#reset-demo"), + archiveSession: document.querySelector("#archive-session"), }; function escapeHtml(input) { - return input + return String(input) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") @@ -39,11 +45,18 @@ async function request(url, options = {}) { ...options, }); - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); + let payload = {}; + try { + payload = await response.json(); + } catch { + payload = {}; } - return response.json(); + if (!response.ok) { + throw new Error(payload.error || `${response.status} ${response.statusText}`); + } + + return payload; } function selectedSession() { @@ -70,13 +83,20 @@ function eventsForSelectedSession() { } function renderSessions() { + if (state.sessions.length === 0) { + elements.sessionList.innerHTML = `
先创建一个项目会话。
`; + return; + } + elements.sessionList.innerHTML = state.sessions .map((session) => { const active = session.id === state.selectedSessionId ? "active" : ""; + const archived = session.status === "archived" ? "archived" : ""; return ` -