diff --git a/CLAUDE.md b/CLAUDE.md index b113201e..99bd0eaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,9 @@ npm run typecheck # Type-check without emitting npm run lint # ESLint on src/ ``` -No test framework is configured. +```bash +npx vitest run # Run all tests (vitest) +``` ## Architecture @@ -35,7 +37,12 @@ Feishu User → Feishu Platform → Bridge Server → Claude Agent SDK → Claud - **`src/feishu/client.ts`** — Feishu API wrapper using `@larksuiteoapi/node-sdk` for sending/updating messages and cards. - **`src/feishu/event-handler.ts`** — EventDispatcher handlers for incoming messages and card actions. Orchestrates the full flow: parse message → check allowlist → get/create session → enqueue task → execute → send result. - **`src/feishu/message-builder.ts`** — Constructs interactive Feishu card messages for progress and results. -- **`src/claude/executor.ts`** — Wraps `@anthropic-ai/claude-agent-sdk` `query()`. Streams SDKMessage async generator, extracts output text, tracks cost/duration. Supports session resumption via `resumeSessionId`. Uses `permissionMode: 'bypassPermissions'` (security delegated to Feishu layer). Budget: $5/query, max 50 turns. +- **`src/claude/executor.ts`** — Wraps `@anthropic-ai/claude-agent-sdk` `query()`. Streams SDKMessage async generator, extracts output text, tracks cost/duration. Supports session resumption via `resumeSessionId`. Uses `permissionMode: 'acceptEdits'` + `canUseTool` auto-allow (not `bypassPermissions` which fails under root). Budget: $5/query, max 50 turns. Injects MCP workspace tool via `createSdkMcpServer`. +- **`src/workspace/tool.ts`** — MCP tool `setup_workspace` for creating isolated workspaces. Each query gets its own MCP server instance via closure to avoid concurrency issues. +- **`src/workspace/manager.ts`** — Git clone + workspace isolation. Supports remote URL (via bare cache) and local path modes. URL normalization handles SSH shorthand. +- **`src/workspace/cache.ts`** — Bare clone cache layer for fast repeated clones. +- **`src/pipeline/orchestrator.ts`** — State-machine-driven multi-step dev pipeline (plan → review → implement → review → push). Uses parallel multi-agent review. +- **`src/pipeline/reviewer.ts`** — Parallel review with 3 agents (correctness/security/architecture). - **`src/session/manager.ts`** — In-memory session store keyed by `chatId:userId`. Maps each chat to a working directory. Auto-cleans sessions idle >2 hours. - **`src/session/queue.ts`** — Per-chat FIFO task queue ensuring one Claude query runs at a time per chat. - **`src/utils/security.ts`** — User allowlist check and dangerous command regex detection (`rm -rf /`, `mkfs`, `dd if=`, etc.). @@ -48,12 +55,20 @@ Feishu User → Feishu Platform → Bridge Server → Claude Agent SDK → Claud - **Two-phase messaging** — Send a progress card first, then update it with the final result card. - **Session isolation** — Each Feishu chat gets its own working directory and serialized task queue. +### Agent SDK Gotchas + +- **`canUseTool` must return `updatedInput`** — `{ behavior: 'allow' }` alone causes SDK internal Zod validation failure. MCP tool handlers silently won't execute. Must return `{ behavior: 'allow', updatedInput: inputObj }`. +- **`bypassPermissions` fails under root** — Use `permissionMode: 'acceptEdits'` + `canUseTool` callback instead. +- **`settingSources: ['project']`** loads `.claude/settings.local.json` from cwd, including `permissions.allow` whitelist. This whitelist is checked *before* `canUseTool`, so unlisted tools (including MCP) get blocked. Currently `canUseTool` with proper `updatedInput` overrides this. +- **Feishu rich text breaks URLs** — `github.com:user/repo` gets auto-linked by Feishu as `[github.com:](http://github.com/)user/repo`. The `workspace/manager.ts` `normalizeRepoUrl()` handles SSH shorthand normalization. + ## Configuration Environment variables loaded via dotenv (see `.env.example`): - **Required**: `FEISHU_APP_ID`, `FEISHU_APP_SECRET` - **Claude**: `ANTHROPIC_API_KEY`, `DEFAULT_WORK_DIR` (default: `/home/ubuntu/projects`), `CLAUDE_TIMEOUT` (default: 300s) +- **Workspace**: `REPO_CACHE_DIR` (bare clone cache), `WORKSPACE_BASE_DIR` (writable workspaces), `WORKSPACE_BRANCH_PREFIX` - **Event mode**: `FEISHU_EVENT_MODE` (`websocket` | `webhook`), `FEISHU_ENCRYPT_KEY`, `FEISHU_VERIFY_TOKEN` (webhook only) - **Security**: `ALLOWED_USER_IDS` (comma-separated, empty = allow all) - **Server**: `PORT` (default: 3000), `NODE_ENV`, `LOG_LEVEL` diff --git a/package-lock.json b/package-lock.json index 170f3b2d..2f5c5577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.42", + "@anthropic-ai/claude-agent-sdk": "^0.2.45", "@larksuiteoapi/node-sdk": "^1.40.0", "better-sqlite3": "^12.6.2", "dotenv": "^16.4.0", @@ -32,9 +32,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.42", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.42.tgz", - "integrity": "sha512-/CugP7AjP57Dqtl2sbsDtxdbpQoPKIhjyF5WrTViGu4NHQdM+UikrRs4MhZ2jeotiC5R7iK9ZUN9SiBgcZ8oLw==", + "version": "0.2.45", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.45.tgz", + "integrity": "sha512-AKH2hKoJNyjLf9ThAttKqbmCjUFg7qs/8+LR/UTVX20fCLn359YH9WrQc6dAiAfi8RYNA+mWwrNYCAq+Sdo5Ag==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index 67cf2c6c..f293a1a2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "node": ">=18.0.0" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.42", + "@anthropic-ai/claude-agent-sdk": "^0.2.45", "@larksuiteoapi/node-sdk": "^1.40.0", "better-sqlite3": "^12.6.2", "dotenv": "^16.4.0", diff --git a/src/feishu/event-handler.ts b/src/feishu/event-handler.ts index 55e6a67d..e7dfc835 100644 --- a/src/feishu/event-handler.ts +++ b/src/feishu/event-handler.ts @@ -631,12 +631,13 @@ async function executeClaudeTask( progressMsgId, threadRootMsgId, chatId, ); - // 保存会话摘要(取输出末尾 500 字符作为摘要) + // 保存会话摘要(用户 prompt + 输出末尾) if (result.success && result.output && result.output.length > 100) { try { const date = new Date().toISOString().slice(0, 10); - const tail = result.output.slice(-500).trim(); - const summary = `[${date}] dir: ${session.workingDir} | ${tail}`; + const promptSnippet = prompt.length > 200 ? prompt.slice(0, 200) + '...' : prompt; + const tail = result.output.slice(-300).trim(); + const summary = `[${date}] dir: ${session.workingDir} | 用户: ${promptSnippet} | 回复: ${tail}`; sessionManager.saveSummary(chatId, userId, session.workingDir, summary); } catch (err) { logger.warn({ err }, 'Failed to save session summary'); diff --git a/src/pipeline/runner.ts b/src/pipeline/runner.ts index e5c22560..f1f4901a 100644 --- a/src/pipeline/runner.ts +++ b/src/pipeline/runner.ts @@ -209,12 +209,13 @@ export async function startPipeline(pipelineId: string): Promise { } } - // 保存摘要 + // 保存摘要(含用户原始 prompt) if (pipelineResult.summary.length > 100) { try { const date = new Date().toISOString().slice(0, 10); - const tail = pipelineResult.summary.slice(-500).trim(); - const summary = `[${date}] [pipeline] dir: ${workingDir} | ${tail}`; + const promptSnippet = prompt.length > 200 ? prompt.slice(0, 200) + '...' : prompt; + const tail = pipelineResult.summary.slice(-300).trim(); + const summary = `[${date}] [pipeline] dir: ${workingDir} | 用户: ${promptSnippet} | 回复: ${tail}`; sessionManager.saveSummary(chatId, userId, workingDir, summary); } catch (err) { logger.warn({ err }, 'Failed to save pipeline summary');