cc-haha/src/server/services/taskService.ts
程序员阿江(Relakkes) d059a8b487 feat: add Provider management UI, CLI Tasks page, and enhance NewTaskModal
- Settings: new Providers tab with full CRUD, activation, and connectivity
  test; Model tab shows active provider name; General tab simplified
- Tasks: new CLI Tasks page displaying task lists from ~/.claude/tasks/
  with status, owner, and dependency (blocks/blockedBy) visualization
- NewTaskModal: add Advanced options (model, permission mode, working dir)
- Backend: fix TaskService to parse CLI V2 task format; extend /api/tasks
  with /lists endpoint for grouped queries
- Fix ModelInfo.context type from number to string

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:05:50 +08:00

147 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TaskService — CLI Task V2 的读取与查询
*
* 任务信息存储在 ~/.claude/tasks/<task_list_id>/ 目录下,每个任务一个 JSON 文件。
* Task list ID 可以是 session ID、team name 等。
*/
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
export type TaskStatus = 'pending' | 'in_progress' | 'completed'
export type TaskInfo = {
id: string
subject: string
description: string
activeForm?: string
owner?: string
status: TaskStatus
blocks: string[]
blockedBy: string[]
metadata?: Record<string, unknown>
taskListId: string
}
export type TaskListSummary = {
id: string
taskCount: number
completedCount: number
inProgressCount: number
pendingCount: number
}
export class TaskService {
private getConfigDir(): string {
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
}
private getTasksDir(): string {
return path.join(this.getConfigDir(), 'tasks')
}
/** 列出所有 task list (目录) */
async listTaskLists(): Promise<TaskListSummary[]> {
const tasksDir = this.getTasksDir()
try {
const entries = await fs.readdir(tasksDir, { withFileTypes: true })
const results: TaskListSummary[] = []
for (const entry of entries) {
if (!entry.isDirectory()) continue
const tasks = await this.getTasksForList(entry.name)
if (tasks.length === 0) continue
results.push({
id: entry.name,
taskCount: tasks.length,
completedCount: tasks.filter((t) => t.status === 'completed').length,
inProgressCount: tasks.filter((t) => t.status === 'in_progress').length,
pendingCount: tasks.filter((t) => t.status === 'pending').length,
})
}
return results
} catch (err: any) {
if (err.code === 'ENOENT') return []
throw err
}
}
/** 获取指定 task list 的所有任务 */
async getTasksForList(taskListId: string): Promise<TaskInfo[]> {
const listDir = path.join(this.getTasksDir(), taskListId)
try {
const entries = await fs.readdir(listDir)
const tasks: TaskInfo[] = []
for (const filename of entries) {
if (!filename.endsWith('.json')) continue
try {
const raw = await fs.readFile(path.join(listDir, filename), 'utf-8')
const data = JSON.parse(raw)
const task = this.parseTaskFile(data, taskListId)
if (task) tasks.push(task)
} catch {
// skip unparseable files
}
}
// Sort by numeric ID
return tasks.sort((a, b) => {
const numA = parseInt(a.id, 10)
const numB = parseInt(b.id, 10)
if (!isNaN(numA) && !isNaN(numB)) return numA - numB
return a.id.localeCompare(b.id)
})
} catch (err: any) {
if (err.code === 'ENOENT') return []
throw err
}
}
/** 列出所有任务(跨所有 task list */
async listTasks(): Promise<TaskInfo[]> {
const taskLists = await this.listTaskLists()
const allTasks: TaskInfo[] = []
for (const list of taskLists) {
const tasks = await this.getTasksForList(list.id)
allTasks.push(...tasks)
}
return allTasks
}
/** 获取单个任务详情 */
async getTask(taskListId: string, taskId: string): Promise<TaskInfo | null> {
const tasks = await this.getTasksForList(taskListId)
return tasks.find((t) => t.id === taskId) || null
}
/** 解析单个任务文件 — 匹配 CLI V2 Task 格式 */
private parseTaskFile(data: any, taskListId: string): TaskInfo | null {
if (!data || typeof data !== 'object') return null
if (!data.id || !data.subject) return null
// Skip internal tasks
if (data.metadata?._internal) return null
return {
id: String(data.id),
subject: data.subject || '',
description: data.description || '',
activeForm: data.activeForm,
owner: data.owner,
status: (['pending', 'in_progress', 'completed'].includes(data.status)
? data.status
: 'pending') as TaskStatus,
blocks: Array.isArray(data.blocks) ? data.blocks : [],
blockedBy: Array.isArray(data.blockedBy) ? data.blockedBy : [],
metadata: data.metadata,
taskListId,
}
}
}
export const taskService = new TaskService()