import type { Project, ProjectView } from "../domain/project"; import { CoreError } from "../errors/core-error"; import type { ConversationRepository } from "../ports/conversation-repository"; import type { ProjectRepository } from "../ports/project-repository"; import type { RunRepository } from "../ports/run-repository"; import type { ClockPort, IdPort, WorkspaceResolverPort, } from "../ports/system-ports"; export type ProjectServiceDependencies = Readonly<{ projects: ProjectRepository; conversations: ConversationRepository; runs: RunRepository; workspaces: WorkspaceResolverPort; clock: ClockPort; ids: IdPort; }>; export class ProjectService { constructor(private readonly dependencies: ProjectServiceDependencies) {} async list(): Promise { return Promise.all( (await this.dependencies.projects.list()).map((project) => this.view(project), ), ); } async get(id: string): Promise { return this.view(await this.require(id)); } async create(name: string, workspaceRoot: string): Promise { const normalizedName = normalizeName(name); const resolvedRoot = await this.dependencies.workspaces.resolveForCreation(workspaceRoot); const timestamp = this.dependencies.clock.now().toISOString(); const project: Project = { id: this.dependencies.ids.create(), name: normalizedName, workspaceRoot: resolvedRoot, createdAt: timestamp, updatedAt: timestamp, }; await this.dependencies.projects.create(project); return this.view(project); } async rename(id: string, name: string): Promise { const project = await this.require(id); const updated = { ...project, name: normalizeName(name), updatedAt: this.dependencies.clock.now().toISOString(), }; await this.dependencies.projects.update(updated); return this.view(updated); } async delete(id: string): Promise { await this.require(id); const conversations = await this.dependencies.conversations.listByProject(id); const ids = conversations.map((conversation) => conversation.id); if (await this.dependencies.runs.hasActiveForConversations(ids)) throw new CoreError( "PROJECT_RUN_ACTIVE", "项目中仍有任务正在运行,暂时无法删除", ); await this.dependencies.runs.deleteByConversations(ids); await this.dependencies.conversations.deleteByProject(id); await this.dependencies.projects.delete(id); } async requireAvailable(id: string): Promise { const project = await this.require(id); if ( !(await this.dependencies.workspaces.isAvailable(project.workspaceRoot)) ) throw new CoreError( "PROJECT_WORKSPACE_UNAVAILABLE", "项目目录当前不可用,无法开始新任务", ); return project; } private async require(id: string): Promise { const project = await this.dependencies.projects.getById(id); if (!project) throw new CoreError("PROJECT_NOT_FOUND", "项目不存在"); return project; } private async view(project: Project): Promise { const conversations = await this.dependencies.conversations.listByProject( project.id, ); return { ...project, workspaceAvailable: await this.dependencies.workspaces.isAvailable( project.workspaceRoot, ), conversationCount: conversations.length, }; } } function normalizeName(name: string): string { const normalized = name.trim(); if (!normalized) throw new CoreError("PROJECT_NAME_EMPTY", "请输入项目名称"); if (normalized.length > 80) throw new CoreError("PROJECT_NAME_TOO_LONG", "项目名称过长"); return normalized; }