maestro/src/bridge/subtask-activity-api.ts
2026-06-03 05:08:00 +00:00

95 lines
3.6 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { type Repository, type Job, localTaskRepoName } from '../db/repository.js';
import { logger } from '../logger.js';
import { canViewTask } from './local-api-helpers.js';
const MAX_ACTIVITY_LOG_CHARS = 4000;
function readActivityLog(worktreePath: string | null, maxChars: number = 0): string {
if (!worktreePath) return '';
const logPath = join(worktreePath, 'logs', 'activity.log');
if (!existsSync(logPath)) return '';
try {
const content = readFileSync(logPath, 'utf-8');
return maxChars > 0 && content.length > maxChars
? content.slice(-maxChars)
: content;
} catch {
return '';
}
}
export function createSubtaskActivityRouter(repo: Repository): Router {
const router = Router();
// GET /:id/subtasks/activities — bulk fetch all subtask activities (includes nested subtasks)
router.get('/:id/subtasks/activities', async (req: Request, res: Response) => {
try {
const taskId = Number(req.params.id);
const viewer = req.user as Express.User | undefined;
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
if (!canViewTask(req, res, task)) return;
const latestJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);
if (!latestJob) { res.status(404).json({ error: 'No job found' }); return; }
// 再帰的に全サブジョブ(孫含む)を収集
const collectAllSubJobs = async (parentId: string): Promise<Job[]> => {
const jobs = await repo.getSubJobs(parentId);
const result = [...jobs];
for (const job of jobs) {
if (job.status === 'waiting_subtasks') {
result.push(...await collectAllSubJobs(job.id));
}
}
return result;
};
const allJobs = await collectAllSubJobs(latestJob.id);
const subtasks = allJobs.map(job => ({
jobId: job.id,
issueNumber: job.issueNumber,
status: job.status,
currentMovement: job.currentMovement ?? null,
currentActivity: job.currentActivity ?? null,
activityLog: readActivityLog(job.worktreePath, MAX_ACTIVITY_LOG_CHARS),
}));
res.json({ subtasks });
} catch (err) {
logger.error(`Subtask activities API error: ${err}`);
res.status(500).json({ error: 'Failed to fetch subtask activities' });
}
});
// GET /:id/subtasks/:jobId/activity — individual subtask activity (supports nested subtasks)
router.get('/:id/subtasks/:jobId/activity', async (req: Request, res: Response) => {
try {
const taskId = Number(req.params.id);
const jobId = req.params.jobId;
const viewer = (req.user as Express.User | undefined) ?? undefined;
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
if (!canViewTask(req, res, task)) return;
// jobId で直接取得(孫タスクにも対応)
const job = await repo.getJob(jobId, viewer ? { viewer } : undefined);
if (!job || !job.worktreePath) { res.status(404).json({ error: 'Subtask not found' }); return; }
// タスクのワークスペース配下であることを確認
if (task!.workspacePath && !job.worktreePath.startsWith(task!.workspacePath)) {
res.status(404).json({ error: 'Subtask not found' }); return;
}
res.json({ activityLog: readActivityLog(job.worktreePath) });
} catch (err) {
logger.error(`Subtask activity API error: ${err}`);
res.status(500).json({ error: 'Failed to fetch subtask activity' });
}
});
return router;
}