diff --git a/CLAUDE.md b/CLAUDE.md index 39af433..5ed79b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,8 +28,6 @@ ClawWork is an OpenClaw desktop client inspired by Claude Cowork: three-panel la Desktop → Gateway (:18789): `chat.send` sends user messages (`deliver: false` — no external channel delivery), receives Agent streaming replies via `event:"chat"`, and receives tool-call events via `event:"agent"` + `caps:["tool-events"]`. -> Historical note: An earlier version used a Desktop↔Plugin dual-channel architecture, which was fully removed during the Gateway-Only refactor (G1-G9). The `packages/channel-plugin` code remains in the repo but is excluded from the workspace. - ## Monorepo Structure ``` @@ -39,61 +37,13 @@ Desktop → Gateway (:18789): `chat.send` sends user messages (`deliver: false` ├── tsconfig.base.json # ES2022, strict, bundler resolution ├── packages/ │ ├── shared/ # @clawwork/shared — zero-dependency type bridge -│ │ └── src/ -│ │ ├── types.ts # Task, Message, Artifact, ToolCall, ProgressStep -│ │ ├── protocol.ts # WsMessage union type + type guards -│ │ ├── gateway-protocol.ts # GatewayFrame types, GatewayConnectParams -│ │ ├── constants.ts # port numbers, buildSessionKey(), parseTaskIdFromSessionKey() -│ │ └── index.ts # barrel export +│ │ └── src/ # types.ts, protocol.ts, gateway-protocol.ts, constants.ts │ ├── channel-plugin/ # (excluded from workspace; code retained but not built) │ └── desktop/ # @clawwork/desktop — Electron app -│ ├── electron.vite.config.ts # React + Tailwind v4 vite plugin │ └── src/ -│ ├── main/ -│ │ ├── index.ts # Electron main process, hiddenInset titleBar -│ │ ├── ws/ -│ │ │ ├── gateway-client.ts # GatewayClient: challenge-response auth, heartbeat, reconnect -│ │ │ ├── device-identity.ts # Ed25519 keypair, device auth signing, device token persistence -│ │ │ ├── window-utils.ts # BrowserWindow helpers -│ │ │ └── index.ts # initWebSockets, getters, destroy -│ │ └── ipc/ -│ │ └── ws-handlers.ts # IPC handlers: send-message, chat-history, list-sessions, gateway-status -│ ├── preload/ -│ │ ├── index.ts # buildApi() factory, contextBridge -│ │ └── clawwork.d.ts # ClawWorkAPI interface -│ └── renderer/ -│ ├── index.html -│ ├── main.tsx # React entry -│ ├── App.tsx # Three-panel layout (260px | flex | 320px) -│ ├── stores/ -│ │ ├── taskStore.ts # Task CRUD, activeTaskId -│ │ ├── messageStore.ts # messagesByTask, streamingByTask -│ │ └── uiStore.ts # rightPanelOpen, unreadTaskIds -│ ├── styles/ -│ │ ├── theme.css # Tailwind v4 + CSS Variables + Inter/JetBrains Mono -│ │ └── design-tokens.ts # TS design tokens (colors, spacing, motion presets) -│ ├── lib/ -│ │ ├── utils.ts # cn(), formatRelativeTime(), formatFileSize() -│ │ └── session-sync.ts # Session state sync logic -│ ├── components/ -│ │ ├── ui/ # shadcn/ui base components (Button, ScrollArea, Tabs, etc.) -│ │ ├── ChatMessage.tsx # Markdown rendering + motion.div listItem -│ │ ├── ChatInput.tsx # Button + motion, Enter/Shift+Enter -│ │ ├── StreamingMessage.tsx # motion.div fadeIn + cursor animation -│ │ ├── ToolCallCard.tsx # Radix Collapsible + AnimatePresence -│ │ ├── ContextMenu.tsx # useTaskContextMenu hook (component removed) -│ │ ├── FileCard.tsx # motion.button file card -│ │ └── FilePreview.tsx # File preview panel -│ ├── hooks/ -│ │ ├── useGatewayDispatcher.ts # Gateway chat events → stores -│ │ └── useTheme.ts # Theme toggle hook -│ └── layouts/ -│ ├── LeftNav/ # TaskItem extraction, DropdownMenu context menu, Tooltip -│ ├── MainArea/ # AnimatePresence view switching, welcome screen -│ ├── RightPanel/ # Tabs (Progress/Artifacts/Git), ScrollArea -│ ├── FileBrowser/ # File browser + AnimatePresence preview -│ ├── Settings/ # Settings page -│ └── Setup/ # Initial setup wizard +│ ├── main/ # Electron main process, ws/, ipc/, db/, artifact/, workspace/ +│ ├── preload/ # contextBridge, ClawWorkAPI interface +│ └── renderer/ # React UI: components/, layouts/, stores/, hooks/, i18n/, styles/ ``` ### Inter-package Dependencies @@ -124,19 +74,11 @@ Desktop → Gateway (:18789): `chat.send` sends user messages (`deliver: false` ## Development Commands ```bash -# Install all dependencies -pnpm install - -# Dev Desktop App (Electron hot-reload) -pnpm --filter @clawwork/desktop dev - -# Type-check (note: tsc lives under desktop/node_modules; pnpm exec tsc won't work) -# Must build shared first (composite: true), then check desktop -packages/desktop/node_modules/.bin/tsc -b packages/shared/tsconfig.json -packages/desktop/node_modules/.bin/tsc --noEmit -p packages/desktop/tsconfig.json - -# Package -pnpm --filter @clawwork/desktop build +pnpm install # Install all dependencies +pnpm dev # Dev Desktop App (Electron hot-reload) +pnpm typecheck # Type-check shared + desktop +pnpm test # Run all tests +pnpm --filter @clawwork/desktop build # Package ``` ## Key Protocols @@ -170,9 +112,7 @@ Desktop communicates with Gateway (:18789) via a single WS connection: ### File Transfer -MVP assumes co-located deployment only: artifact files are copied directly to the workspace artifact directory via local paths. - -Note the `mediaLocalRoots` security check (v2026.3.2+). +MVP assumes co-located deployment only: artifact files are copied directly to the workspace artifact directory via local paths. Note the `mediaLocalRoots` security check (v2026.3.2+). ## Theme System @@ -182,193 +122,11 @@ Core accent: green `#0FFD0D` (dark) / `#0B8A0A` (light); background `#1C1C1C` / All colors are referenced via `var(--xxx)` — no hardcoded hex values. -## Current Progress - -### Phase 1 — Complete ✅ (commits `375154c`, `c882b4e`) - -- **T1-0** Monorepo scaffold (pnpm workspace, tsconfig, .gitignore, git init) -- **T1-1** Desktop package (Electron main, preload, renderer entry, theme CSS) -- **T1-2** ~~Channel Plugin~~ (removed in the Gateway-Only refactor; code retained but not built) -- **T1-3** Shared types (types.ts, protocol.ts, gateway-protocol.ts, constants.ts) — Drizzle ORM schema pending -- **T1-4** Three-panel layout (App.tsx: 260px LeftNav | flex MainArea | 320px RightPanel, right panel collapsible) -- **T1-5** LeftNav static structure (New Task button, search box, file manager entry, sample task list, settings) -- **T1-7** Electron main-process WS client: Gateway challenge-response auth, heartbeat, exponential-backoff reconnect -- **T1-8** Message sending: Electron → Gateway (chat.send) with idempotencyKey -- **T1-9** Message receiving: Gateway events forwarded to renderer via IPC; useAgentMessages hook routes by sessionKey - -**Phase 1 acceptance met: completed a round-trip conversation with Agent via `window.clawwork.sendMessage()` in Electron DevTools; events returned correctly.** - -### Phase 2 — Complete ✅ (commit `bc220ad`) - -- **T2-0** Install zustand, react-markdown, rehype-highlight -- **T2-1** New Task flow: taskStore.createTask() — creates a task and automatically sets it as active -- **T2-2** Task list rendering: reads taskStore dynamically, grouped by status (Active → Completed → Archived), reverse-chronological within groups -- **T2-3** Context menu: ContextMenu component + useTaskContextMenu hook, state transitions active→completed→archived -- **T2-4** ChatMessage component: Markdown rendering (react-markdown + rehype-highlight), role differentiation (user/assistant/system) -- **T2-5** ChatInput component: Shift+Enter for newline, Enter to send, auto-expanding textarea height -- **T2-6** StreamingMessage component: streaming response delta accumulation + blinking cursor animation -- **T2-7** ToolCallCard component: collapsible tool-call card showing arguments/result -- **T2-8** Progress panel: extracts `- [x]`/`- [ ]` patterns from AI messages, displays progress steps -- **T2-9** Artifacts panel: lists file artifacts from message artifacts field -- **Bug fix** Zustand selector infinite loop: removed getter methods from store, switched to direct state access + EMPTY_MESSAGES sentinel value -- **Bug fix** Gateway chat event parsing: content is at `payload.message.content[]`, not `payload.content[]` -- **Preload refactor** buildApi() factory function, fixed type errors - -### Deferred - -- **T2-10** Multi-task parallel verification: functionality is ready but not systematically tested - -### Phase 3 — Complete ✅ (commit `TBD`) - -- **T3-1** Workspace config persistence: `app.getPath('userData')/clawwork-config.json`, Setup wizard -- **T3-2** SQLite database initialization: `better-sqlite3`, tasks/messages/artifacts tables, DB file at `/.clawwork.db` -- **T3-3** Artifact persistence: artifact files copied to workspace, DB records created, Git auto-commit (simple-git) -- **T3-4** File browser UI: FileBrowser layout, FileCard component, search + filter + type grouping -- **T3-5** File preview panel: FilePreview component, code/text/image preview -- **T3-6** IPC layer: workspace/artifact/settings IPC handlers - -### Phase 3.5 — Complete ✅ (pending commit) - -Design System + full UI overhaul: shadcn/ui + Framer Motion + Inter/JetBrains Mono fonts - -- **T3.5-0** Install dependencies: framer-motion, cva, Radix UI suite, @fontsource-variable/* -- **T3.5-1** Design system definition: `design-system.md` spec + `design-tokens.ts` TS constants + shadcn/ui base components -- **T3.5-2** Foundation refactor: theme.css rewrite (font imports, @layer base, extended CSS variables) -- **T3.5-3** Component refactor: ChatMessage, ChatInput, StreamingMessage, ToolCallCard, FileCard, FilePreview — all rewritten with shadcn/ui + motion -- **T3.5-4** Layout refactor: LeftNav (TaskItem extraction + DropdownMenu), MainArea (AnimatePresence), RightPanel (Tabs), FileBrowser, Settings, Setup, App.tsx (TooltipProvider) -- **T3.5-5** Cleanup: removed useAgentMessages.ts dead code -- **T3.5-6** Verification passed: tsc --noEmit zero errors, dev server starts normally, UI screenshots confirm correct rendering - -### Phase 3.5 Visual Polish — Complete ✅ (pending commit) - -**Font/Size Bump (13 files):** -- **T3.5-7** Base font 13→14px, avatar/icon/button sizes increased, border radius unified, section labels text-xs, Button danger variant hex→CSS vars - -**Premium Depth Pass (10 items, all verified):** -- **T3.5-8** theme.css: 12 new premium CSS Variables (dark+light): `--accent-hover`, `--accent-soft`, `--accent-soft-hover`, `--bg-elevated`, `--ring-accent`, `--glow-accent`, `--shadow-elevated`, `--shadow-card`, `--border-subtle`, `--danger`, `--danger-bg` -- **T3.5-9** 3 CSS utility classes: `.surface-elevated`, `.glow-accent`, `.ring-accent-focus` -- **T3.5-10** button.tsx: new `soft` variant + all variants `active:scale-[0.98]` + focus ring `--ring-accent` -- **T3.5-11** ChatInput: `--bg-elevated` + `--shadow-elevated` + `ring-accent-focus`, send button → `soft` variant -- **T3.5-12** MainArea WelcomeScreen: radial glow + subtitle + typography hierarchy -- **T3.5-13** LeftNav "New Task" button: `default` → `soft` variant -- **T3.5-14** TaskItem: active left-side 3px accent bar + `whileHover={{ x: 2 }}` micro-interaction -- **T3.5-15** ToolCallCard: left status bar (running=pulse, done=accent, error=red) + shadow-card -- **T3.5-16** tabs.tsx: sizes increased, active uses `--bg-elevated` + `--shadow-card` -- **T3.5-17** dropdown-menu.tsx: hardcoded colors → CSS Variables, content uses `--bg-elevated` + `--shadow-elevated` -- **T3.5-18** Setup page: radial glow + elevated card form container - -### Upcoming Phases - -- **Phase 4** — Polish + packaging (T4-1 ~ T4-7): theme toggle, global search (FTS5), Settings, error handling, electron-builder dmg - -## Task Dependency Graph - -``` -Phase 1: - T1-0 → T1-1 ─────┐ - T1-3 ─────┘→ T1-7 → T1-8 → T1-9 - T1-4 ─┬→ (Phase 2 UI depends on these) - T1-5 ─┘ - -Phase 2: - [T2-1 → T2-2 → T2-3] Task CRUD chain (serial) - [T2-4 → T2-5 → T2-6 → T2-7] Chat flow components (serial) - [T2-8, T2-9] Right panel (parallelizable) - T2-10 Multi-task verification (after all Phase 2 tasks) - -Phase 3: - [T3-1 → T3-2 → T3-3] Artifact persistence (serial) - [T3-4 → T3-5 → T3-6 → T3-7] File browser (serial) - Both chains can run in parallel - -Phase 4: - [T4-1, T4-2, T4-3, T4-4] All parallelizable - T4-5 → T4-6 → T4-7 Packaging chain (serial) -``` - -## Technical Discoveries (Lessons Learned) - -### Gateway Protocol Key Details - -1. **Challenge-response auth**: Server sends `connect.challenge` (containing nonce) first; client must reply with a `connect` request (protocol=3, client.id=`gateway-client`, mode=`backend`) - - **`client.id` MUST be the literal string `"gateway-client"`**. The server schema validates this field against a constant/enum — any other value (e.g. dynamic IDs like `clawwork-`) will be rejected with `invalid connect params: at /client/id: must be equal to constant`. -2. **`chat.send` parameters**: `sessionKey` + `message` (not `text`) + `idempotencyKey` (UUID). Returns `{runId, status: "started"}`, non-blocking -3. **Chat event payload structure (major gotcha)**: Content is at `payload.message.content[]`, not `payload.content[]`. This was the root cause of messages not displaying in Phase 2 -4. **Preload path**: electron-vite outputs preload as `.mjs` (not `.js`); the main process load path must match -5. **`@clawwork/shared` cannot be externalized**: Must be bundled in the electron-vite config - -### Device Identity is MANDATORY for Scoped RPCs (Critical) - -**The #1 gateway auth pitfall**: connecting without a `device` field in the `connect` request causes the server to **silently clear all requested scopes to `[]`**. Every scope-protected RPC (`chat.send` needs `operator.write`, `sessions.list` needs `operator.read`) then fails with `missing scope`. - -Server logic (`gateway-cli-Bmg642Lj.js:22579`): -```javascript -if (!device && (!isControlUi || decision.kind !== "allow")) clearUnboundScopes(); -``` - -When `device` is null and the client is not Control UI, scopes are wiped. Token/password auth alone is NOT enough — the server distinguishes between "authenticated" and "authorized with scopes". - -**Required device auth flow:** -1. Generate Ed25519 keypair, persist to `userData/device-identity.json` (mode 0o600) -2. `deviceId` = SHA256 hex of raw 32-byte public key (strip SPKI prefix `302a300506032b6570032100`) -3. `publicKey` in connect params = raw public key bytes as base64url (no padding) -4. Build signature payload string (v3 format): `"v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily"` (pipe-separated, metadata ASCII-lowercased via `normalizeMetadataForAuth`) -5. Sign with Ed25519 private key, encode signature as base64url -6. `device` field in connect params: `{ id, publicKey, signature, signedAt, nonce }` where `nonce` comes from the `connect.challenge` event - -**Auto-pairing**: Local backend clients (`client.id === "gateway-client"`, `mode === "backend"`, local IP, no browser origin header, shared auth OK) auto-pair without user approval. - -**Device token persistence**: Server issues a `deviceToken` in the `hello-ok` response at `payload.auth.deviceToken`. Store it per-gateway in `userData/device-tokens.json` and send it back on subsequent connections via `auth.deviceToken`. This provides a secondary auth channel — if the shared gateway token changes, the device token still works (until explicitly revoked). Implementation: `device-identity.ts` has `saveDeviceToken()` / `loadDeviceToken()` / `removeDeviceToken()`. - -**Server source locations** (OpenClaw 2026.3.12, for future reverse-engineering): -- `gateway-cli-Bmg642Lj.js:22222` — `shouldSkipBackendSelfPairing()` -- `gateway-cli-Bmg642Lj.js:22505-22579` — device auth validation + `clearUnboundScopes` -- `gateway-cli-Bmg642Lj.js:22883-22907` — `hello-ok` payload construction (includes `auth.deviceToken`) -- `reply-BEN3KNDZ.js:58052` — `buildDeviceAuthPayloadV3()` reference implementation -- `reply-BEN3KNDZ.js:58017-58028` — `normalizeDeviceMetadataForAuth()` - -### Zustand Pitfall - -**Never call `get()` inside a selector.** `useStore((s) => s.someMethod())` where `someMethod` internally calls `get()` causes infinite re-renders (new object reference each time). Fix: access state fields directly + module-level `const EMPTY_ARRAY: T[] = []` sentinel to avoid empty-array reference changes. - -### `ws` Library `close()` on CONNECTING Socket - -The `ws` npm package (v8.x) throws `"WebSocket was closed before the connection was established"` if you call `ws.close()` on a socket in `CONNECTING` state before the HTTP upgrade request has been created (`_req` is undefined). This happens in `GatewayClient.cleanup()` when `destroy()` is called while a connection attempt is in flight (e.g. test-gateway timeout, or rapid add/remove of gateways). **Always wrap `ws.close()` in try-catch** inside cleanup paths. - -### Full Gateway Protocol Reference - -Detailed protocol documentation is stored under `~/.agents/memories/**/gateway-ws-protocol.md` (path may vary by machine; generate or locate via your agent runner’s memory directory), including: frame format, connection handshake, valid client ID/mode enums, RPC method list, event types, chat message structure, available Agent list. - -### Tailwind v4 `@layer` Specificity Pitfall - -Tailwind v4 emits all utility classes into `@layer utilities`. Per the CSS spec, **unlayered styles always have higher specificity than any `@layer` styles** — regardless of selector specificity. - -If you write unlayered global resets in `theme.css` (the file containing `@import "tailwindcss"`): - -```css -/* Wrong: this overrides ALL Tailwind padding/margin utilities */ -* { margin: 0; padding: 0; box-sizing: border-box; } -``` - -Then `pt-14`, `px-4`, `pb-3`, etc. — **all** padding/margin utilities — will be overridden to 0px, completely ineffective. - -**Fix:** Remove the reset. Tailwind v4 Preflight (`@layer base`) already includes `* { margin: 0; padding: 0; box-sizing: border-box }`. If you must write custom base styles, wrap them in `@layer base { ... }`. - -### Gateway Streaming Text is Cumulative Snapshots, Not Incremental Deltas - -Gateway `chat` event `state: 'delta'` frames may contain **cumulative snapshots rather than incremental chunks**. The same frame may also be sent repeatedly. If `messageStore.appendStreamDelta()` directly uses `+=` to concatenate, messages will display duplicated (e.g. "HelloHelloHello..."). - -**Fix:** Use `mergeGatewayStreamText(previous, incoming)` for smart merging (implemented in `@clawwork/shared/constants.ts`). Merge logic: -1. `incoming === previous` → ignore duplicate frame -2. `incoming.startsWith(previous)` → cumulative snapshot, replace directly with incoming -3. `previous.startsWith(incoming)` → old snapshot replay, ignore -4. Otherwise → real incremental chunk, normal concatenation - -Also, `state: 'final'` frames may carry trailing text that must be processed with `appendStreamDelta()` before `finalizeStream()`, otherwise the trailing content is lost. +## Current Status -### Electron Auto-Screenshot Troubleshooting +Phases 1–3.5 complete (monorepo, Gateway integration, full UI with shadcn/ui + Framer Motion, artifact persistence, file browser). Phase 4 (polish + packaging) is next. -In dev mode, `main/index.ts` automatically captures a screenshot to `/tmp/clawwork-screenshot.png` after `did-finish-load`, and also supports `Cmd+Shift+S` for manual capture. When screenshots appear unchanged, don't restart repeatedly — use `executeJavaScript` to inject a diagnostic script that dumps `getComputedStyle()` to `/tmp/clawwork-debug.json` to directly verify whether CSS is taking effect. +See memory files for detailed phase history and technical pitfalls discovered during development. ## Known Issues & Risks @@ -389,5 +147,5 @@ In dev mode, `main/index.ts` automatically captures a screenshot to `/tmp/clawwo ## Design Documents -- Full design doc: `docs/openclaw-desktop-design.md` (v0.2), covering data models, UI prototypes, ADRs, and complete descriptions + acceptance criteria for all 28 dev tasks. -- Design system spec: `design-system.md` in the repo, covering colors, fonts, spacing, border radius, shadows, animations, and component states. +- Full design doc: `docs/openclaw-desktop-design.md` (v0.2) +- Design system spec: `design-system.md` diff --git a/docs/plan/architecture-refactor.md b/docs/plan/architecture-refactor.md deleted file mode 100644 index a1c415b..0000000 --- a/docs/plan/architecture-refactor.md +++ /dev/null @@ -1,265 +0,0 @@ -# Architecture Refactor Plan — 桌面专属能力与通用能力分离 - -> Status: Draft -> Created: 2026-03-14 - -## 1. 目标 - -将 ClawWork 的代码按职责拆为两层: - -- **通用层 (`@clawwork/core`)** — 零平台依赖,可在 Electron/Tauri/浏览器中复用 -- **桌面专属层 (`@clawwork/desktop`)** — 仅含 Electron 胶水代码 - -最终使 renderer 侧代码可脱离 Electron 独立测试和运行。 - -## 2. 能力分类 - -| 通用能力 | 桌面专属能力 | -|---------|------------| -| 会话管理 (session CRUD) | 本地文件系统 (fs, path) | -| 消息收发 / 流式处理 | Electron IPC (contextBridge) | -| 搜索 (接口层面) | Git 本地仓库 (simple-git) | -| 任务进度追踪 | 原生窗口能力 (dialog, BrowserWindow) | -| Gateway 通信协议 (解析/帧格式/消息模型) | SQLite (better-sqlite3, Node 原生模块) | -| | Gateway 传输层 (ws 库 + IPC 事件转发) | - -## 3. 现状分析 - -### 3.1 耦合数据 - -``` -Renderer 侧: - 13 个文件 × 34 次 window.clawwork.xxx() 直接调用 - 使用 20/31 个 preload API 方法 - 6 个 preload 方法从未被调用 (死代码) - -Store 耦合率: - taskStore 5/6 方法调用 IPC (83%) - messageStore 2/8 (25%) - uiStore 1/11 (9%) - -God Hook: - useGatewayDispatcher ~279 行, 7 个职责, 4 个 IPC 调用 -``` - -### 3.2 依赖关系图 - -``` -┌─ Renderer ──────────────────────────────────────────────────┐ -│ │ -│ stores (taskStore, messageStore, uiStore) │ -│ └──► window.clawwork.persistXxx / loadXxx │ -│ │ -│ useGatewayDispatcher (God Hook, 7 职责) │ -│ ├──► window.clawwork.onGatewayEvent / onGatewayStatus │ -│ ├──► window.clawwork.gatewayStatus / listGateways │ -│ ├──► session-sync.ts │ -│ │ └──► window.clawwork.loadMessages / syncSessions │ -│ └──► stores │ -│ │ -│ Setup/Settings (11 个 IPC 调用) │ -│ └──► window.clawwork.addGateway / testGateway / ... │ -│ │ -│ ChatInput → window.clawwork.sendMessage │ -│ FileBrowser → window.clawwork.listArtifacts │ -│ FilePreview → window.clawwork.readArtifactFile │ -│ LeftNav → window.clawwork.globalSearch │ -│ │ -└───────────────────────┬──────────────────────────────────────┘ - │ contextBridge (preload, 31 方法) - ▼ -┌─ Main Process ──────────────────────────────────────────────┐ -│ ws-handlers (7 ch) → GatewayClient[] │ -│ data-handlers (5 ch) → Drizzle/SQLite │ -│ artifact-handlers (5 ch) → fs + git + SQLite │ -│ settings-handlers (7 ch) → config JSON + GatewayClient │ -│ workspace-handlers (5 ch) → fs + git init + dialog │ -│ search-handlers (1 ch) → SQLite FTS │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 3.3 已知问题清单 - -| # | 问题 | 位置 | -|---|------|------| -| 1 | Store 直接调用 `window.clawwork` — 无抽象层 | taskStore, messageStore, uiStore | -| 2 | `useGatewayDispatcher` 是 God Hook (7 职责 ~279 行) | hooks/useGatewayDispatcher.ts | -| 3 | Preload 类型独立声明,未从 `@clawwork/shared` 派生 | preload/clawwork.d.ts | -| 4 | 协议解析逻辑在 `ws-handlers.ts` 和 `useGatewayDispatcher.ts` 中重复 | 两个文件 | -| 5 | `protocol.ts` 旧 Plugin WS 协议类型疑似死代码 | shared/src/protocol.ts | -| 6 | 6 个 preload 方法未被调用 | preload/index.ts | - -## 4. 目标架构 - -### 4.1 包结构 - -``` -packages/ - shared/ # 不变 — 类型 + 协议 + 常量 - core/ # 新增 — 通用业务逻辑层 (零平台依赖) - desktop/ # 瘦身 — 只剩 Electron 胶水代码 -``` - -### 4.2 `@clawwork/core` 内部结构 - -``` -packages/core/ - src/ - ports/ # 接口定义 (依赖反转边界) - persistence.ts # loadTasks, persistTask, loadMessages, persistMessage - gateway-transport.ts # connect, sendReq, onEvent, onStatus - artifacts.ts # saveArtifact, listArtifacts, readFile - search.ts # globalSearch - settings.ts # getSettings, updateSettings - platform.ts # 聚合接口, re-export 上面所有 port - - stores/ # Zustand stores (通过 port 调用副作用) - task-store.ts - message-store.ts - ui-store.ts - - services/ # 业务编排 (纯逻辑) - gateway-dispatcher.ts # 事件路由主函数 - session-sync.ts # hydrateFromLocal, syncFromGateway - auto-title.ts # 自动标题逻辑 - - protocol/ # Gateway 协议解析 (纯函数) - parse-chat-event.ts # extractText, parseDelta/Final - parse-tool-event.ts # mapPhaseToStatus - merge-stream.ts # mergeGatewayStreamText -``` - -**硬性约束:`core/` 的任何文件禁止 import `electron`、`fs`、`path`、`better-sqlite3`、`simple-git`、`window.clawwork`。** - -### 4.3 `@clawwork/desktop` 变化 - -``` -packages/desktop/ - src/ - main/ - adapters/ # 新增 — Port 接口的 Electron 实现 - electron-persistence.ts # SQLite CRUD via Drizzle - electron-gateway.ts # GatewayClient 封装 - electron-artifacts.ts # fs + git - electron-settings.ts # config JSON + dialog - electron-search.ts # SQLite FTS - ws/ # 不变 — GatewayClient, device-identity - ipc/ # 瘦身 — 只做 IPC 注册, 逻辑委托给 adapters - db/ # 不变 — schema, migrations - - preload/ # 精简 — 删除死代码方法, API 可能缩减 - - renderer/ - platform/ # 新增 - electron-adapter.ts # IPlatformAdapter 的 renderer 侧实现 - # 内部调用 window.clawwork, 对外暴露 port 接口 - hooks/ # 瘦身 - useGatewayDispatcher.ts # → 只做 React 生命周期绑定, 委托 core - stores/ → 删除 # stores 移到 core - lib/session-sync.ts → 删除 # 移到 core -``` - -### 4.4 依赖注入方式 - -不引入 DI 框架。利用 Zustand 的工厂函数 + React Context: - -```typescript -// core/stores/task-store.ts -import type { PersistencePort } from '../ports/persistence' - -export function createTaskStore(persistence: PersistencePort) { - return create()((set, get) => ({ - tasks: [], - createTask(gatewayId?: string) { - const task = { /* ... */ } - set(s => ({ tasks: [...s.tasks, task] })) - persistence.persistTask(task) - }, - // ... - })) -} - -// desktop/renderer/main.tsx -import { createTaskStore } from '@clawwork/core' -import { electronPersistence } from './platform/electron-adapter' - -const useTaskStore = createTaskStore(electronPersistence) -``` - -### 4.5 `useGatewayDispatcher` 拆分方案 - -当前 7 个职责拆成独立 hook/函数: - -| 新 hook/函数 | 原职责 | 类型 | -|-------------|--------|------| -| `useChatEventRouter` | Chat 事件路由 + 流式文本分发 | core (纯逻辑) + 薄 hook 壳 | -| `useToolCallRouter` | Agent tool-call 事件处理 | core (纯逻辑) + 薄 hook 壳 | -| `useUnreadTracker` | 非活跃任务未读标记 | core (纯逻辑) | -| `useAutoTitle` | 首条消息自动标题 | core (纯逻辑) | -| `useSessionHydration` | 挂载时从 SQLite 加载数据 | core service | -| `useGatewayStatusTracker` | 连接状态监听 + 初始化 | core + platform adapter | -| `useGatewayBootstrap` | 组合以上,挂载到 React 生命周期 | desktop renderer | - -## 5. 实施步骤 - -按风险从低到高,增量推进,每步可独立验证: - -| 步骤 | 内容 | 改动量 | 风险 | 前置 | -|------|------|--------|------|------| -| **S0** | 清理死代码: 6 个未调用 preload 方法, `protocol.ts` 旧类型 | 删除 | 无 | 无 | -| **S1** | 创建 `packages/core/` 包, 定义 `ports/` 接口 | 新增 ~5 文件 | 无 | 无 | -| **S2** | 提取 `protocol/` 纯函数到 core (extractText, mergeStream 等) | 移动 + 新增 | 低 | S1 | -| **S3** | 移动 3 个 store 到 core, 改为工厂函数 + port 注入 | 改 3 文件 + 调整 import | 低 | S1 | -| **S4** | 在 renderer 写 `ElectronPlatformAdapter`, App 根注入 | 新增 1 文件, 改 2 文件 | 低 | S3 | -| **S5** | 移动 `session-sync.ts` 到 core, 接受 adapter 参数 | 改 1 文件 | 低 | S3, S4 | -| **S6** | 拆 `useGatewayDispatcher` 为 5-6 个独立 hook/函数 | 1 → 5-6 文件 | **中** | S2-S5 | -| **S7** | 提取 `ws-handlers.ts` 中的协议解析到 `core/protocol/` | 改 2-3 文件 | 低 | S2 | -| **S8** | Preload 类型改为从 `@clawwork/shared` / `core` 派生 | 改 1 文件 | 低 | S1 | - -### 依赖关系 - -``` -S0 (独立) -S1 ──► S2 (并行) - ├──► S3 ──► S4 ──► S5 - │ └──► S6 - └──► S7 (可与 S3-S5 并行) -S8 (可在任意时刻做) -``` - -### 每步验证标准 - -每一步完成后必须通过: -1. `tsc -b packages/shared/tsconfig.json` — shared 编译通过 -2. `tsc -b packages/core/tsconfig.json` — core 编译通过 (S1 起) -3. `tsc --noEmit -p packages/desktop/tsconfig.json` — desktop 类型检查通过 -4. `pnpm --filter @clawwork/desktop dev` — 应用正常启动 -5. 手动验证:能创建任务、发消息、收到 Agent 回复 - -## 6. 包依赖关系 - -### 当前 - -``` -@clawwork/shared ←── @clawwork/desktop -``` - -### 目标 - -``` -@clawwork/shared ←── @clawwork/core ←── @clawwork/desktop -``` - -`core` 依赖 `shared` 的类型定义,`desktop` 依赖 `core` 的 stores/services 和 `shared` 的类型。 - -## 7. 风险与注意事项 - -1. **Gateway 传输 vs 协议的边界** — 协议解析 (帧格式、content 提取) 是通用的,但 WebSocket 连接管理 (`ws` 库、reconnect、heartbeat) 和 IPC 事件转发是桌面专属的。拆分时注意不要把传输层拉进 core。 - -2. **Zustand store 工厂化的 HMR 影响** — `create()` 从模块顶层调用改为工厂函数调用,需确认 electron-vite HMR 仍能正确保持 store 状态。可能需要在 dev 模式下做 store 实例缓存。 - -3. **Store 间跨引用** — `taskStore.createTask` 读 `useUiStore.getState().defaultGatewayId`。移到 core 后这种 cross-store 依赖需要显式传参或通过统一的 store 容器解决。 - -4. **preload API 精简的时机** — 先完成 renderer 侧的 adapter 封装,确认哪些 IPC channel 确实不需要了,再精简 preload。不要提前删。 - -5. **不要在 S6 之前重构 Settings/Setup** — 这两个页面有 11 个 IPC 调用但都是直接的 UI→平台操作,不涉及 store 或业务逻辑。优先级低,留到最后或单独处理。 diff --git a/docs/plan/model-agent-thinking-research.md b/docs/plan/model-agent-thinking-research.md deleted file mode 100644 index e3214bb..0000000 --- a/docs/plan/model-agent-thinking-research.md +++ /dev/null @@ -1,355 +0,0 @@ -# 调研:消息/会话元数据 & 模型/Agent/思考深度切换 - -> Status: Completed -> Created: 2026-03-14 -> Source: OpenClaw Gateway v2026.3.12 逆向分析 - -## 1. 结论摘要 - -| 能力 | Gateway 是否支持 | ClawWork 当前状态 | -|------|----------------|-----------------| -| 每条消息显示模型名 | **支持** — session 级别记录 `model`、`modelProvider` | **未实现** — 完全忽略 | -| Token 用量 (↑input ↓output) | **支持** — session 级别 `inputTokens`/`outputTokens`/`totalTokens` | **未实现** — 完全忽略 | -| 上下文窗口百分比 (7% ctx) | **支持** — `contextTokens` + `totalTokens` 可算 | **未实现** | -| 思考深度 (thinking level) | **支持** — `thinkingLevel` 字段, `sessions.patch` 可修改 | **未实现** — 字段声明了但没读 | -| Reasoning tokens (R3.6k) | **支持** — `reasoningLevel` 字段控制推理输出 | **未实现** | -| Agent 身份 | **支持** — `agents.list` RPC, session key 中的 agentId | **硬编码 `main`** | -| 切换模型 | **支持** — `sessions.patch` 的 `model` 字段 | **未实现** | -| 切换 Agent | **支持** — 修改 session key 的 agent 前缀 | **未实现** — session key 硬编码 | -| 切换思考深度 | **支持** — `sessions.patch` 的 `thinkingLevel` 字段 | **未实现** | -| 列出可用模型 | **支持** — `models.list` RPC | **未实现** | -| 列出可用 Agent | **支持** — `agents.list` RPC | **未实现** | - -## 2. Gateway 协议详解 - -### 2.1 `sessions.list` 返回的 Session 元数据 - -Gateway 的 `sessions.list` RPC 返回每个 session 的完整元数据(`reply-BEN3KNDZ.js:85093` `listSessionsFromStore()`): - -```typescript -// 每个 session 条目包含以下字段: -{ - key: string, // session key, e.g. "agent:main:clawwork:task:xxx" - displayName?: string, // 显示名称 - label?: string, // 用户自定义标签 - updatedAt: number, // 最后更新时间戳 - - // === 模型信息 === - modelProvider: string, // 模型提供商, e.g. "anthropic", "openai" - model: string, // 模型 ID, e.g. "claude-opus-4-6", "gpt-5.4" - contextTokens?: number, // 上下文窗口大小, e.g. 200000 - - // === Token 用量 === - inputTokens?: number, // 累计输入 token 数 - outputTokens?: number, // 累计输出 token 数 - totalTokens?: number, // 衍生的总 token 数 - totalTokensFresh: boolean, // totalTokens 是否为最新计算 - responseUsage?: string, // 用量显示模式: "off" | "tokens" | "full" - - // === 思考 / 推理 === - thinkingLevel?: string, // 思考深度: 模型相关的级别值 - reasoningLevel?: string, // 推理输出: "on" | "off" | "stream" - elevatedLevel?: string, // 提升级别: "on" | "off" | "ask" | "full" - fastMode?: boolean, // 快速模式 - - // === Agent 信息 === - // agentId 嵌入在 session key 中: "agent::..." - // 可通过 parseAgentSessionKey(key) 提取 - - // === 其他 === - sessionId?: string, - verboseLevel?: string, - sendPolicy?: string, - chatType?: string, - channel?: string, - spawnedBy?: string, -} -``` - -### 2.2 `chat.history` 返回的元数据 - -`chat.history` 的 response payload(`gateway-cli-Bmg642Lj.js:10878-10887`)包含: - -```typescript -{ - sessionKey: string, - sessionId: string, - messages: ChatMessage[], // 历史消息数组 - thinkingLevel?: string, // 当前会话的思考深度 - fastMode?: boolean, // 快速模式状态 - verboseLevel?: string, // 详细级别 -} -``` - -**ClawWork 当前只提取了 `messages`,完全忽略了 `thinkingLevel`、`fastMode`、`verboseLevel`。** - -### 2.3 实时 Chat 事件的 Payload 结构 - -`chat` 事件(`gateway-cli-Bmg642Lj.js:2849-2960`)的 payload: - -```typescript -// delta 事件 -{ - runId: string, // 运行 ID (可用于关联) - sessionKey: string, - seq: number, - state: "delta", - message: { - role: "assistant", - content: [{ type: "text", text: string }], - timestamp: number, - } -} - -// final 事件 -{ - runId: string, - sessionKey: string, - seq: number, - state: "final", - stopReason?: string, // 停止原因 - message?: { // 有时为空 (silent reply) - role: "assistant", - content: [{ type: "text", text: string }], - timestamp: number, - } -} -``` - -**注意:实时 chat 事件中不直接包含 model/usage 信息。** 这些信息是在 run 完成后写入 session store 的(`gateway-cli-Bmg642Lj.js:4494-4530`)。想要获取 per-message 的 model/usage,需要在 `final` 事件后调用 `sessions.list` 或 `sessions.preview` 来刷新 session 元数据。 - -### 2.4 Run 完成后的 Usage 写入 - -在 Agent run 完成后(`gateway-cli-Bmg642Lj.js:4494-4540`),Gateway 会把以下信息写入 session entry: - -```typescript -// 从 finalRunResult.meta.agentMeta 提取 -const usage = finalRunResult.meta?.agentMeta?.usage; -// usage 结构: -{ - input?: number, // 输入 token - output?: number, // 输出 token - cacheRead?: number, // 缓存读取 - cacheWrite?: number, // 缓存写入 -} - -const modelUsed = finalRunResult.meta?.agentMeta?.model; // 实际使用的模型 -const providerUsed = finalRunResult.meta?.agentMeta?.provider; // 实际使用的提供商 - -// 写入 session entry -sessionEntry.inputTokens = input; -sessionEntry.outputTokens = output; -sessionEntry.contextTokens = contextTokens; // 上下文窗口大小 -sessionEntry.cacheRead = usage.cacheRead; -sessionEntry.cacheWrite = usage.cacheWrite; -setSessionRuntimeModel(sessionEntry, { provider, model }); -``` - -## 3. 可用的 Gateway RPC 方法 - -### 3.1 `agents.list` — 列出可用 Agent - -**Request:** `{ }` (无必需参数) - -**Response:** (`reply-BEN3KNDZ.js:84768`) -```typescript -{ - defaultId: string, // 默认 agent ID, 通常 "main" - mainKey: string, // 主 key - scope: string, // "per-sender" 等 - agents: [ - { - id: string, // e.g. "main", "research", "code-review" - name?: string, // 显示名称 - identity?: { - name?: string, - theme?: string, - emoji?: string, - avatar?: string, - avatarUrl?: string, // 头像 URL - } - } - ] -} -``` - -### 3.2 `models.list` — 列出可用模型 - -**Request:** `{ }` (无必需参数) - -**Response:** (`gateway-cli-Bmg642Lj.js:12669`) -```typescript -{ - models: ModelCatalogEntry[] // 完整的模型目录 -} -``` - -### 3.3 `sessions.patch` — 修改会话配置 - -**Request:** -```typescript -{ - sessionKey: string, // 目标 session - // 以下均为可选, 传 null 表示清除/重置 - thinkingLevel?: string | null, // 修改思考深度 - fastMode?: boolean | null, // 切换快速模式 - model?: string | null, // 切换模型 (e.g. "gpt-5.4", "claude-opus-4-6") - reasoningLevel?: string | null, // "on" | "off" | "stream" - elevatedLevel?: string | null, // "on" | "off" | "ask" | "full" - verboseLevel?: string | null, // 详细级别 - responseUsage?: string | null, // "off" | "tokens" | "full" - label?: string | null, // 自定义标签 - execHost?: string | null, // "sandbox" | "gateway" | "node" - execSecurity?: string | null, // "deny" | "allowlist" | "full" -} -``` - -### 3.4 `agents.create` / `agents.update` / `agents.delete` — Agent CRUD - -完整 CRUD 支持。`agents.create` 接受 `name`、`workspace`、`emoji`、`avatar` 等参数。 - -## 4. Session Key 与 Agent 的关系 - -当前 ClawWork 硬编码 session key 为: -``` -agent:main:clawwork:task: -``` - -其中 `main` 是 agent ID。要支持多 Agent,需要: -``` -agent::clawwork:task: -``` - -Server 端通过 `resolveSessionAgentId()` 从 session key 中提取 agentId,据此加载对应 agent 的配置、system prompt、workspace 等。 - -## 5. 当前 ClawWork 丢失的数据 - -### 5.1 `sessions.list` 响应中被忽略的字段 - -ClawWork 的 `ws:sync-sessions` handler (`ws-handlers.ts`) 只提取了: -- `s.key` — session key -- `s.updatedAt` — 时间戳 -- `s.derivedTitle` / `s.label` / `s.displayName` — 标题 - -**以下字段全部被丢弃:** -- `modelProvider`, `model` — 模型信息 -- `inputTokens`, `outputTokens`, `totalTokens`, `contextTokens` — token 用量 -- `thinkingLevel`, `reasoningLevel`, `fastMode` — 思考配置 -- `responseUsage` — 用量显示配置 -- `elevatedLevel`, `verboseLevel` — 运行配置 - -### 5.2 `chat.history` 响应中被忽略的字段 - -- `thinkingLevel` — 会话思考深度 -- `fastMode` — 快速模式 -- `verboseLevel` — 详细级别 - -### 5.3 实时 Chat 事件中被忽略的字段 - -- `runId` — 声明了但从未使用 -- `thinking` content blocks (`type: 'thinking'`) — 声明了但被 `extractText()` 过滤掉 - -### 5.4 Preload API 中未实现的方法 - -6 个声明但从未调用的 preload 方法:`chatHistory`, `listSessions`, `saveArtifact`, `getArtifact`, `getWorkspacePath`, `onArtifactSaved` - -## 6. 实现方案建议 - -### 6.1 展示层 — 消息/会话元数据显示 - -**Task/Session 级别展示(优先级高):** - -| 显示项 | 数据源 | 实现路径 | -|-------|--------|---------| -| 模型名 badge | `sessions.list` → `model` | session 列表同步时提取, 存入 taskStore | -| 当前 Agent 名 | `agents.list` → `agents[].name` + session key 中的 agentId | 新增 agentStore | -| 思考深度指示 | `sessions.list` → `thinkingLevel` | 存入 taskStore | - -**Message 级别展示(优先级中):** - -| 显示项 | 数据源 | 实现路径 | -|-------|--------|---------| -| ↑input / ↓output tokens | `sessions.list` → `inputTokens`/`outputTokens` (session 累计) | final 事件后刷新 session 数据 | -| R (reasoning tokens) | 需要扩展 — 当前 gateway 不在 chat 事件中直接传 | 从 session-level 数据推算或等 gateway 扩展 | -| ctx% | `totalTokens / contextTokens * 100` | 同上 | - -**注意:OpenClaw Web UI 的 per-message token 显示很可能来自 control-ui 内部的特殊渠道(control socket 有更多权限),而非标准 gateway 事件。标准 chat 事件中不包含 per-message usage。** 可行方案: -1. 在 `final` 事件后立即调用 `sessions.list` 刷新 session 级 token 数据,显示为 session 总量 -2. 或者使用 `sessions.preview` 获取更细粒度的数据 - -### 6.2 控制层 — 切换 Agent / 模型 / 思考深度 - -**切换模型(优先级高):** -1. 新增 IPC: `ws:models-list` → 调用 `GatewayClient.sendReq('models.list', {})` -2. 新增 IPC: `ws:session-patch` → 调用 `GatewayClient.sendReq('sessions.patch', { sessionKey, model })` -3. Preload 新增: `listModels(gatewayId)`, `patchSession(gatewayId, sessionKey, patch)` -4. UI: 在 ChatInput 区域或 RightPanel 添加模型选择器 - -**切换 Agent(优先级中):** -1. 新增 IPC: `ws:agents-list` → 调用 `GatewayClient.sendReq('agents.list', {})` -2. 修改 `buildSessionKey()` — 接受 `agentId` 参数(不再硬编码 `main`) -3. Task 创建时选择 Agent -4. UI: 新建任务 dialog 中添加 Agent 选择器 - -**切换思考深度(优先级中):** -1. 复用 `ws:session-patch` IPC -2. UI: 在 ChatInput 区域添加 thinking level 选择器 -3. 可选值取决于当前模型(不同模型支持不同的 thinking levels) - -### 6.3 数据模型变更 - -```typescript -// @clawwork/shared/types.ts — Task 扩展 -export interface Task { - // ... existing fields ... - agentId?: string; // 新增: 关联的 agent ID - model?: string; // 新增: 当前使用的模型 - modelProvider?: string; // 新增: 模型提供商 - thinkingLevel?: string; // 新增: 思考深度 - inputTokens?: number; // 新增: 累计输入 tokens - outputTokens?: number; // 新增: 累计输出 tokens - contextTokens?: number; // 新增: 上下文窗口大小 -} - -// @clawwork/shared/types.ts — Message 扩展 -export interface Message { - // ... existing fields ... - runId?: string; // 新增: 关联的 run ID - thinkingContent?: string; // 新增: thinking blocks 内容 -} - -// constants.ts — buildSessionKey 修改 -export function buildSessionKey(taskId: string, agentId: string = 'main'): string { - return `agent:${agentId}:clawwork:task:${taskId}`; -} -``` - -### 6.4 SQLite Schema 变更 - -```sql --- tasks 表新增列 -ALTER TABLE tasks ADD COLUMN agent_id TEXT DEFAULT 'main'; -ALTER TABLE tasks ADD COLUMN model TEXT; -ALTER TABLE tasks ADD COLUMN model_provider TEXT; -ALTER TABLE tasks ADD COLUMN thinking_level TEXT; -ALTER TABLE tasks ADD COLUMN input_tokens INTEGER DEFAULT 0; -ALTER TABLE tasks ADD COLUMN output_tokens INTEGER DEFAULT 0; -ALTER TABLE tasks ADD COLUMN context_tokens INTEGER; - --- messages 表新增列 -ALTER TABLE messages ADD COLUMN run_id TEXT; -ALTER TABLE messages ADD COLUMN thinking_content TEXT; -``` - -## 7. 实施优先级 - -| P | 任务 | 依赖 | -|---|------|------| -| P0 | 新增 `models.list`、`agents.list`、`sessions.patch` 的 IPC 通道 | 无 | -| P0 | 从 `sessions.list` 提取 model/token/thinking 元数据,存入 taskStore | P0 IPC | -| P1 | Task 卡片上显示模型 badge + token 概要 | P0 数据 | -| P1 | ChatInput 区域添加模型选择器 (dropdown) | P0 IPC | -| P1 | ChatInput 区域添加 thinking level 选择器 | P0 IPC | -| P2 | 新建任务时选择 Agent | P0 IPC + session key 改造 | -| P2 | 提取并显示 thinking content blocks | Message 类型扩展 | -| P3 | SQLite schema 迁移 + 持久化元数据 | 数据模型变更 | diff --git a/package.json b/package.json index 1633424..6774a8f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "postinstall": "node scripts/ensure-electron.mjs", "dev": "pnpm --filter @clawwork/desktop dev", "build": "pnpm -r build", + "typecheck": "packages/desktop/node_modules/.bin/tsc -b packages/shared/tsconfig.json && packages/desktop/node_modules/.bin/tsc --noEmit -p packages/desktop/tsconfig.json", "lint": "pnpm -r lint", "test": "pnpm -r test" },