2026-08-14 14:34:27 +08:00

117 lines
3.7 KiB
TypeScript

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<readonly ProjectView[]> {
return Promise.all(
(await this.dependencies.projects.list()).map((project) =>
this.view(project),
),
);
}
async get(id: string): Promise<ProjectView> {
return this.view(await this.require(id));
}
async create(name: string, workspaceRoot: string): Promise<ProjectView> {
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<ProjectView> {
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<void> {
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<Project> {
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<Project> {
const project = await this.dependencies.projects.getById(id);
if (!project) throw new CoreError("PROJECT_NOT_FOUND", "项目不存在");
return project;
}
private async view(project: Project): Promise<ProjectView> {
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;
}