diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..980b3ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.git +data +*.md +.env +.env.* diff --git a/.env.example b/.env.example index 69b5994..e88ad9f 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ -ANTHROPIC_API_KEY= -OPENAI_API_KEY= +export OPENAI_API_KEY=your_key +export OPENAI_MODEL= +export OPENAI_API_KEY= diff --git a/.gitignore b/.gitignore index 42efec5..ffc520e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ data/* !data/.gitkeep .env *.tsbuildinfo +.worktrees/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c63f649 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM node:22-slim AS base +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* +RUN corepack enable pnpm + +WORKDIR /app + +# 克隆 stello SDK(公开仓库) +ARG STELLO_REF=main +RUN git clone --depth 1 --branch ${STELLO_REF} https://github.com/stello-agent/stello.git stello + +# 复制依赖定义 + 根 tsconfig +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./ +COPY packages/server/package.json packages/server/ +COPY packages/web/package.json packages/web/ + +# 安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源码 +COPY packages/ packages/ +COPY market/ market/ + +# 构建(顺序:session → core → server + web) +RUN pnpm --filter @stello-ai/session run build && \ + pnpm --filter @stello-ai/core run build && \ + pnpm --filter @mindkit/server run build && \ + pnpm --filter @mindkit/web run build + +# 确保 data 目录存在 +RUN mkdir -p data/spaces + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +EXPOSE 3000 + +CMD ["node", "packages/server/dist/index.js"] diff --git "a/docs/MindKit \344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243\357\274\210PRD\357\274\211.pdf" "b/docs/MindKit \344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243\357\274\210PRD\357\274\211.pdf" new file mode 100644 index 0000000..04c6e17 Binary files /dev/null and "b/docs/MindKit \344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243\357\274\210PRD\357\274\211.pdf" differ diff --git a/docs/superpowers/plans/2026-04-08-frontend-backend-integration.md b/docs/superpowers/plans/2026-04-08-frontend-backend-integration.md new file mode 100644 index 0000000..0a8cce1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-frontend-backend-integration.md @@ -0,0 +1,949 @@ +# 前端接入真实后端 + Session 分裂跑通 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 前端从 localStorage 切换到真实后端 API,丰富创建 Kit 配置,跑通 session 分裂 e2e + +**Architecture:** 后端补两个点(session records 端点 + EventBus 接入 Engine hooks + POST /spaces 传 options),前端删除全局 store,新建 api.ts 封装后端调用,各页面组件自行管理数据。对话走同步 REST POST(仿 devtools),WS 只推系统事件。 + +**Tech Stack:** TypeScript · Hono · React · Vite (proxy already configured) · @stello-ai/core + +**Spec:** `docs/superpowers/specs/2026-04-08-frontend-backend-integration-design.md` + +--- + +## File Structure + +| 文件 | 操作 | 职责 | +|------|------|------| +| `server/src/api/routes.ts` | Modify | 新增 GET sessions/:sid/records 端点 + POST /spaces 传 options | +| `server/src/space/space-factory.ts` | Modify | Engine hooks 接入 EventBus | +| `server/src/space/space-manager.ts` | Modify | createSpaceAgent 传 EventBus | +| `web/src/lib/api.ts` | Create | 后端 API 客户端 | +| `web/src/lib/store.tsx` | Delete | 删除 localStorage store | +| `web/src/lib/types.ts` | Modify | 对齐后端类型 | +| `web/src/App.tsx` | Modify | 删除 StoreProvider 包裹 | +| `web/src/pages/SpaceList.tsx` | Modify | 接入真实 API | +| `web/src/pages/KitWorkspace.tsx` | Modify | 接入 topology API + WS | +| `web/src/components/ChatPanel.tsx` | Modify | 接入 REST chat + records | + +--- + +## Task 1: 后端 — Session Records 端点 + POST /spaces options 透传 + +**Files:** +- Modify: `packages/server/src/api/routes.ts` + +- [ ] **Step 1: 新增 GET /spaces/:id/sessions/:sid/records 端点** + +在 routes.ts 的 events 端点后、return 之前添加: + +```typescript +/** GET /spaces/:id/sessions/:sid/records — 获取 session 对话记录 */ +app.get('/spaces/:id/sessions/:sid/records', async (c) => { + const id = c.req.param('id') + const sid = c.req.param('sid') + const meta = await spaceManager.getSpace(id) + if (!meta) return c.json({ error: 'Space not found' }, 404) + const agent = spaceManager.getAgent(id, meta) + try { + const records = await agent.memory.readRecords(sid) + return c.json({ + records: records + .filter((r) => r.role === 'user' || r.role === 'assistant') + .map((r) => ({ role: r.role, content: r.content, timestamp: r.timestamp })), + }) + } catch { + return c.json({ records: [] }) + } +}) +``` + +- [ ] **Step 2: 更新 POST /spaces 端点透传 options** + +修改 POST /spaces 端点,将 body 中的扩展字段传给 createSpace: + +```typescript +app.post('/spaces', async (c) => { + const body = await c.req.json<{ + name?: string + presetDirName?: string + emoji?: string + color?: string + description?: string + mode?: 'AUTO' | 'PRO' + expectedArtifacts?: string + presetSessions?: SpaceMeta['presetSessions'] + skills?: SpaceMeta['skills'] + }>() + if (!body.name || !body.presetDirName) { + return c.json({ error: 'name and presetDirName are required' }, 400) + } + try { + const { name, presetDirName, ...options } = body + const meta = await spaceManager.createSpace(name, presetDirName, options) + return c.json(meta, 201) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return c.json({ error: msg }, 400) + } +}) +``` + +需要在 import 行添加 `SpaceMeta`(已有 `SpaceUpdatePatch`,加上 `SpaceMeta`)。 + +- [ ] **Step 3: Typecheck + 全量测试** + +Run: `cd /home/i/Code/MindKit && pnpm --filter @mindkit/server exec tsc --noEmit && pnpm --filter @mindkit/server test -- --run` +Expected: 通过 + +- [ ] **Step 4: 提交** + +```bash +cd /home/i/Code/MindKit +git add packages/server/src/api/routes.ts +git commit -m "feat(server): 新增 session records 端点 + POST /spaces 透传 options" +``` + +--- + +## Task 2: 后端 — EventBus 接入 Engine Hooks + +SpaceFactory 创建 Agent 时,通过 Engine hooks 将 fork 事件写入 EventBus。 + +**Files:** +- Modify: `packages/server/src/space/space-factory.ts` +- Modify: `packages/server/src/space/space-manager.ts` + +- [ ] **Step 1: SpaceFactoryContext 新增 eventBus 字段** + +在 `space-factory.ts` 的 `SpaceFactoryContext` 中添加: + +```typescript +import type { EventBus } from '../events/event-bus' + +export interface SpaceFactoryContext { + dataDir: string + config: PresetConfig + env: Record + spaceMeta?: SpaceMeta + eventBus?: EventBus // 新增 +} +``` + +- [ ] **Step 2: 在 createSpaceAgent 中接入 hooks** + +在 `orchestration.hooks` 中添加 `onSessionFork` hook。找到现有的 `hooks: { onRoundEnd(...) { ... } }` 块,扩展为: + +```typescript +hooks: { + onRoundEnd({ sessionId, input, turn }) { + // ... 现有逻辑不变 + }, + onSessionFork({ parentId, child }) { + if (ctx.eventBus) { + ctx.eventBus.emit({ + id: crypto.randomUUID(), + at: new Date().toISOString(), + kind: 'node_forked', + payload: { nodeId: child.id, label: child.label, parentId }, + }) + } + }, +}, +``` + +需要在文件顶部添加 `import * as crypto from 'node:crypto'`(如果尚未导入)。 + +- [ ] **Step 3: SpaceManager.getAgent 传入 eventBus** + +在 `space-manager.ts` 的 `getAgent` 方法中,传入 eventBus: + +```typescript +const agent = createSpaceAgent({ + dataDir: path.join(this.spacesDir, id), + config: preset, + env: this.env, + spaceMeta: meta, + eventBus: this.getEventBus(id), // 新增 +}) +``` + +- [ ] **Step 4: Typecheck + 全量测试** + +Run: `cd /home/i/Code/MindKit && pnpm --filter @mindkit/server exec tsc --noEmit && pnpm --filter @mindkit/server test -- --run` +Expected: 通过 + +- [ ] **Step 5: 提交** + +```bash +cd /home/i/Code/MindKit +git add packages/server/src/space/space-factory.ts packages/server/src/space/space-manager.ts +git commit -m "feat(server): EventBus 接入 Engine hooks — onSessionFork 推送 node_forked 事件" +``` + +--- + +## Task 3: 前端 — API 客户端 + 删除 Store + +**Files:** +- Create: `packages/web/src/lib/api.ts` +- Delete: `packages/web/src/lib/store.tsx` +- Modify: `packages/web/src/lib/types.ts` +- Modify: `packages/web/src/App.tsx` + +- [ ] **Step 1: 重写 types.ts 对齐后端** + +```typescript +// packages/web/src/lib/types.ts + +/** Space 元数据(对应后端 SpaceMeta) */ +export interface SpaceMeta { + id: string + name: string + presetDirName: string + createdAt: string + emoji: string + color: string + description?: string + mode: 'AUTO' | 'PRO' + expectedArtifacts?: string +} + +/** Preset 摘要 */ +export interface PresetSummary { + dirName: string + name: string + description: string +} + +/** 递归拓扑树节点(core SessionTreeNode) */ +export interface SessionTreeNode { + id: string + label: string + sourceSessionId?: string + status: 'active' | 'archived' + turnCount: number + children: SessionTreeNode[] +} + +/** 对话记录 */ +export interface ChatRecord { + role: 'user' | 'assistant' + content: string + timestamp: string +} + +/** WS 服务端消息 */ +export type WsMessage = + | { type: 'chunk'; content: string } + | { type: 'done'; sessionId: string } + | { type: 'error'; message: string } + | { type: 'space_event'; event: SpaceEvent } + +/** 系统事件 */ +export interface SpaceEvent { + id: string + at: string + kind: string + payload: Record +} +``` + +- [ ] **Step 2: 创建 api.ts** + +```typescript +// packages/web/src/lib/api.ts +import type { SpaceMeta, PresetSummary, SessionTreeNode, ChatRecord, SpaceEvent } from './types' + +const BASE = '/api' + +/** 通用 fetch 封装 */ +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...init, + }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`) + } + return res.json() as Promise +} + +/* ── Presets ── */ + +export function fetchPresets(): Promise { + return request('/presets') +} + +/* ── Spaces ── */ + +export function fetchSpaces(): Promise { + return request('/spaces') +} + +export function createSpace(body: { + name: string + presetDirName: string + emoji?: string + color?: string + description?: string + mode?: 'AUTO' | 'PRO' + expectedArtifacts?: string + [key: string]: unknown +}): Promise { + return request('/spaces', { method: 'POST', body: JSON.stringify(body) }) +} + +export function deleteSpace(id: string): Promise { + return request(`/spaces/${id}`, { method: 'DELETE' }) +} + +/* ── Topology ── */ + +export function fetchTopology(spaceId: string): Promise { + return request(`/spaces/${spaceId}/topology`) +} + +/* ── Chat ── */ + +export function sendTurn( + spaceId: string, + sessionId: string, + message: string, +): Promise<{ content: string; sessionId: string }> { + return request(`/spaces/${spaceId}/chat`, { + method: 'POST', + body: JSON.stringify({ sessionId, message }), + }) +} + +/* ── Session Records ── */ + +export function fetchRecords( + spaceId: string, + sessionId: string, +): Promise<{ records: ChatRecord[] }> { + return request(`/spaces/${spaceId}/sessions/${sessionId}/records`) +} + +/* ── Events ── */ + +export function fetchEvents( + spaceId: string, + limit = 100, +): Promise<{ events: SpaceEvent[] }> { + return request(`/spaces/${spaceId}/events?limit=${limit}`) +} +``` + +- [ ] **Step 3: 删除 store.tsx,更新 App.tsx** + +删除 `packages/web/src/lib/store.tsx`。 + +更新 `App.tsx`,删除 StoreProvider: + +```typescript +// packages/web/src/App.tsx +import { Routes, Route, Navigate } from 'react-router-dom' +import { SpaceList } from '@/pages/SpaceList' +import { KitWorkspace } from '@/pages/KitWorkspace' + +/** 主应用 */ +export function App() { + return ( + + } /> + } /> + } /> + + ) +} +``` + +- [ ] **Step 4: 确认 web typecheck** + +Run: `cd /home/i/Code/MindKit && pnpm --filter @mindkit/web exec tsc --noEmit` +Expected: 会有 SpaceList/KitWorkspace/ChatPanel 的引用错误(它们还引用旧 store)——这是预期的,后续 task 修复。 + +- [ ] **Step 5: 提交** + +```bash +cd /home/i/Code/MindKit +git add packages/web/src/lib/api.ts packages/web/src/lib/types.ts packages/web/src/App.tsx +git rm packages/web/src/lib/store.tsx +git commit -m "feat(web): 新建 API 客户端 + 删除 localStorage store" +``` + +--- + +## Task 4: 前端 — SpaceList 接入真实 API + +**Files:** +- Modify: `packages/web/src/pages/SpaceList.tsx` + +- [ ] **Step 1: 重写 SpaceList** + +完整重写,从后端拉数据,创建 Kit 时传完整 options(含 preset 选择 + JSON 高级配置): + +```typescript +// packages/web/src/pages/SpaceList.tsx +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { Plus, Trash2, GitBranch } from 'lucide-react' +import { cn } from '@/lib/utils' +import { fetchSpaces, fetchPresets, createSpace, deleteSpace } from '@/lib/api' +import type { SpaceMeta, PresetSummary } from '@/lib/types' + +/** 创建 Kit 弹窗 */ +function CreateKitModal({ + presets, + onClose, + onCreated, +}: { + presets: PresetSummary[] + onClose: () => void + onCreated: (meta: SpaceMeta) => void +}) { + const [name, setName] = useState('') + const [presetDirName, setPresetDirName] = useState(presets[0]?.dirName ?? '') + const [mode, setMode] = useState<'AUTO' | 'PRO'>('AUTO') + const [advancedJson, setAdvancedJson] = useState('{}') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const handleCreate = async () => { + if (!name.trim() || !presetDirName) return + setLoading(true) + setError('') + try { + let extra: Record = {} + try { extra = JSON.parse(advancedJson) } catch { setError('JSON 格式错误'); setLoading(false); return } + const meta = await createSpace({ name: name.trim(), presetDirName, mode, ...extra }) + onCreated(meta) + onClose() + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setLoading(false) + } + } + + return ( +
+
e.stopPropagation()}> +

创建新空间

+
+
+ + +
+
+ + setName(e.target.value)} + placeholder="如:黑客松项目规划" + className="w-full px-3 py-2 rounded-lg border border-border bg-surface text-text placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30" + autoFocus onKeyDown={(e) => e.key === 'Enter' && handleCreate()} + /> +
+
+ +
+ {(['AUTO', 'PRO'] as const).map((m) => ( + + ))} +
+
+
+ +