feat: 人工接管任务(takeover)+ 任务详情展示附件

B-③ takeover(⑧ ★ POST /api/tasks/:id/takeover):

后端:
- POST /api/tasks/:id/takeover:准备(或复用 result.worktree)任务 worktree,
  返回 {taskId, worktree, branch, console}——console 为用户在自己终端起交互式
  claude 的命令(daemon 无 TTY,不在进程内起交互会话);在途 run 时拒绝

前端:
- api.takeover;TakeoverModal(展示命令 + 一键复制 + 分支提示)
- TaskDetail 加「⌨ 人工接管」按钮(onTakeover 经 TaskSection→TaskRow 线程下来)
- 顺带:TaskDetail 展示任务附件(📎 文件名,读 task._raw.attachments)

验证:typecheck 干净;206 测试通过;前端 build + dev 渲染 0 错误。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 17:06:35 +08:00
parent a688b6977e
commit 638f2f64f0
4 changed files with 72 additions and 8 deletions
+19 -1
View File
@@ -9,7 +9,7 @@ import { syncProject, hasTodoJson } from '../sync/todo-sync.js';
import { resolvedExecutorModels } from '../executor/models.js';
import { classifyComplexity, type ClassifierFn } from '../executor/classify.js';
import { mergeBranch } from '../executor/merge.js';
import { git, removeWorktree } from '../executor/worktree.js';
import { git, removeWorktree, createWorktree } from '../executor/worktree.js';
import { cleanupTaskRunArtifacts } from '../executor/cleanup.js';
import { resolveLogo, LOGO_MIME } from './logo.js';
import { readTranscript, TranscriptError } from '../executor/transcript.js';
@@ -248,6 +248,24 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
return { attachments: store.addAttachments(id, saved) };
});
// 人工接管:准备(或复用)任务 worktree,返回用户在自己终端起交互式 claude 的命令。
// 适用卡住/需人工的任务——人接手手动改。不在 daemon 内起交互会话(无 TTY)。
app.post('/api/tasks/:id/takeover', async (req, reply) => {
const { id } = req.params as { id: string };
const task = store.getTask(id);
if (!task) return reply.code(404).send({ error: `任务不存在: ${id}` });
if (store.hasInflightRun(id)) return reply.code(400).send({ error: '任务有在途 run,无法接管' });
const project = store.getProject(task.projectId);
if (!project) return reply.code(404).send({ error: `项目不存在: ${task.projectId}` });
let worktree = task.result?.worktree ?? null;
let branch = task.result?.branch ?? null;
if (!worktree) {
const wt = await createWorktree(project.repoPath, id, project.defaultBranch);
worktree = wt.dir; branch = wt.branch;
}
return { taskId: id, worktree, branch, console: `cd ${worktree} && claude` };
});
// 部分更新任务(title/priority/complexitycomplexity 重置逻辑在 Store.patchTask
app.patch('/api/tasks/:id', (req) => {
const { id } = req.params as { id: string };