117 lines
3.7 KiB
TypeScript
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;
|
|
}
|