Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.).
Expand All @@ -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`
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions src/feishu/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
7 changes: 4 additions & 3 deletions src/pipeline/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,13 @@ export async function startPipeline(pipelineId: string): Promise<void> {
}
}

// 保存摘要
// 保存摘要(含用户原始 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');
Expand Down