diff --git a/src/claude/__tests__/executor.test.ts b/src/claude/__tests__/executor.test.ts index 70f4917..9209ba5 100644 --- a/src/claude/__tests__/executor.test.ts +++ b/src/claude/__tests__/executor.test.ts @@ -69,7 +69,7 @@ vi.mock('../../cron/init.js', () => ({ getCronScheduler: vi.fn(() => null), })); -import { ClaudeExecutor } from '../executor.js'; +import { ClaudeExecutor, buildWorkspaceSystemPrompt } from '../executor.js'; // ============================================================ // Helpers @@ -581,4 +581,21 @@ describe('ClaudeExecutor', () => { expect(opts.env).toBeUndefined(); }); }); + + describe('buildWorkspaceSystemPrompt — platform constraints', () => { + it('includes platform execution constraints section', () => { + const prompt = buildWorkspaceSystemPrompt('/tmp/work'); + expect(prompt).toContain('平台执行约束'); + expect(prompt).toContain('子进程生命周期'); + expect(prompt).toContain('不要用 Monitor 等待超过 5 分钟的后台任务结果'); + expect(prompt).toContain('权限交互'); + }); + + it('omits cron guidance when cron is disabled', () => { + // config mock has cron.enabled = false + const prompt = buildWorkspaceSystemPrompt('/tmp/work'); + expect(prompt).not.toContain('manage_cron'); + expect(prompt).toContain('建议用户稍后手动追问检查结果'); + }); + }); }); diff --git a/src/claude/executor.ts b/src/claude/executor.ts index ab94cd1..a958654 100644 --- a/src/claude/executor.ts +++ b/src/claude/executor.ts @@ -322,7 +322,7 @@ export function listKnownOrgs(cacheDir: string): string[] { } /** 构建工作区管理系统提示词(注入实际目录路径 + 可用项目列表) */ -function buildWorkspaceSystemPrompt(workingDir?: string, options?: { isRestart?: boolean }): string { +export function buildWorkspaceSystemPrompt(workingDir?: string, options?: { isRestart?: boolean }): string { const projectsDir = config.claude.defaultWorkDir; const cacheDir = config.repoCache.dir; const workspacesDir = config.workspace.baseDir; @@ -465,7 +465,30 @@ gh search issues --repo owner/repo -- "keyword" # 错误 ✗ — 不要 cd 到主仓库或其他目录执行 gh cd /some/other/dir && gh pr view 123 -\`\`\``; +\`\`\` + +## 平台执行约束 + +你运行在 Feishu Bot 环境中,通过 Claude Agent SDK 的 \`query()\` API 执行。以下约束源于该执行模型,与目标仓库无关: + +### 子进程生命周期 + +每次对话是一次独立的 query,返回结果后子进程(包括所有后台任务)会被回收。这意味着: + +- **不要用 Monitor 等待超过 5 分钟的后台任务结果** — 你返回 result 后 Monitor 会随子进程一起终止 +- **不要期望"返回后继续工作"** — 一旦你给出最终回复,本次执行即结束 + +### 长时间任务的正确模式 + +对于需要 5 分钟以上才能完成的后台任务(如跑批、大规模测试、编译): + +1. 用 \`nohup ... &\` 或 \`... &\` 启动任务,确保脱离子进程 +${config.cron.enabled ? `2. 用 cron 工具(\`manage_cron\`)设一个一次性定时检查(\`schedule_kind: "at"\`),在预计完成时间触发 +3. 立即返回结果,告知用户任务已启动、预计完成时间、以及会自动回来检查` : `2. 立即返回结果,告知用户任务已启动、预计完成时间,建议用户稍后手动追问检查结果`} + +### 权限交互 + +Bot 场景无法弹窗授权。所有需要的命令权限必须通过 \`.claude/settings.json\` 或 \`.claude/settings.local.json\` 白名单预配置。如果遇到权限拒绝,不要重试,告知用户需要更新白名单。`; const selfRepoGuide = (workingDir && isServiceOwnRepo(workingDir)) ? (() => { const rt = detectRuntime(); @@ -946,7 +969,8 @@ export class ClaudeExecutor { : { type: 'preset', preset: 'claude_code', append: promptAppend }, // 加载项目设置 (CLAUDE.md 等);路由 agent 传 [] 避免加载 - settingSources: settingSourcesOverride ?? ['user', 'project'], + // 'local' 加载 .claude/settings.local.json(优先级最高,覆盖 project) + settingSources: settingSourcesOverride ?? ['user', 'project', 'local'], // MCP 服务器:工作区管理工具 + 飞书工具 (空对象等同于无 MCP 服务器) mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, @@ -984,7 +1008,8 @@ export class ClaudeExecutor { try { // 遍历 SDK 流式消息 - for await (const message of q) { + // label 用于在收到 result 后跳出循环(result 是 SDK 协议的终止消息) + messageLoop: for await (const message of q) { // 每收到消息重置 idle 计时器 resetIdleTimer(`msg:${message.type}${'subtype' in message ? ':' + (message as Record).subtype : ''}`); @@ -1123,7 +1148,7 @@ export class ClaudeExecutor { case 'result': resultMessage = message; - break; + break messageLoop; default: // tool_progress, stream_event 等其他消息类型 — 记录以便诊断 idle timeout 间隙 @@ -1132,6 +1157,10 @@ export class ClaudeExecutor { break; } } + + // 提前 break(收到 result)时子进程可能仍在运行(如有 background task), + // 主动 close 确保不泄漏;正常迭代结束时 close 是幂等的 no-op + q.close(); } catch (err) { clearTimeout(idleTimer); if (hardTimer) clearTimeout(hardTimer);