import type { InjectionKey } from 'vue' import type { JobInfo, ProjectEvent } from '#shared/types' /** 项目工作区的共享状态:数据 + SSE 实时事件 */ export function createProjectStore(projectId: string) { const api = useApi() const project = ref(null) const chapters = ref([]) const agents = ref([]) const models = ref([]) const jobs = ref([]) const running = ref(false) const runningLabel = ref('') const connected = ref(false) /** 正在写作的章节实时文本:chapterId → { versionId, text } */ const live = reactive>({}) /** 章节变更计数,章节详情组件据此刷新 */ const chapterRev = reactive>({}) const refreshProject = async () => { project.value = await api.get(`/api/projects/${projectId}`) } const refreshChapters = async () => { chapters.value = await api.get(`/api/projects/${projectId}/chapters`) } const refreshAgents = async () => { agents.value = await api.get(`/api/projects/${projectId}/agents`) } const refreshModels = async () => { models.value = await api.get('/api/models') } const refreshJobs = async () => { jobs.value = await api.get(`/api/projects/${projectId}/jobs?limit=100`) } let chapterTimer: ReturnType | null = null const refreshChaptersSoon = () => { if (chapterTimer) clearTimeout(chapterTimer) chapterTimer = setTimeout(() => { refreshChapters().catch(() => {}) }, 150) } function handle(e: ProjectEvent) { switch (e.type) { case 'job': { const i = jobs.value.findIndex(j => j.id === e.job.id) if (i >= 0) jobs.value[i] = e.job else jobs.value = [e.job, ...jobs.value].slice(0, 200) if (e.job.status === 'error') { api.toast.add({ title: `${e.job.agentName || '任务'}出错`, description: e.job.error.slice(0, 300), color: 'error', icon: 'i-lucide-circle-alert' }) } break } case 'delta': { const cur = live[e.chapterId] if (!cur || cur.versionId !== e.versionId) live[e.chapterId] = { versionId: e.versionId, text: e.text } else cur.text += e.text break } case 'chapter': chapterRev[e.chapterId] = (chapterRev[e.chapterId] ?? 0) + 1 refreshChaptersSoon() break case 'chapters': refreshChaptersSoon() break case 'agents': refreshAgents().catch(() => {}) break case 'pipeline': running.value = e.running runningLabel.value = e.message ?? '' if (!e.running) { refreshProject().catch(() => {}) refreshChaptersSoon() } break case 'log': api.toast.add({ title: e.level === 'error' ? '出错了' : '提示', description: e.message.slice(0, 400), color: e.level === 'error' ? 'error' : e.level === 'warn' ? 'warning' : 'info' }) break } } let source: EventSource | null = null function connect() { if (source) return source = new EventSource(`/api/projects/${projectId}/events`) source.onopen = () => { connected.value = true // 断线重连后补齐可能错过的状态 refreshChapters().catch(() => {}) refreshJobs().catch(() => {}) } source.onerror = () => { connected.value = false } source.onmessage = (msg) => { try { handle(JSON.parse(msg.data) as ProjectEvent) } catch (err) { console.error('bad event', err) } } } function disconnect() { source?.close() source = null connected.value = false } async function load() { await Promise.all([refreshProject(), refreshChapters(), refreshAgents(), refreshModels(), refreshJobs()]) running.value = Boolean(project.value?.running) runningLabel.value = project.value?.running?.label ?? '' } async function saveProject(patch: Record) { project.value = { ...project.value!, ...(await api.put(`/api/projects/${projectId}`, patch)), running: project.value?.running ?? null } } async function run() { await api.post(`/api/projects/${projectId}/run`) } async function stop() { await api.post(`/api/projects/${projectId}/stop`) } return { projectId, project, chapters, agents, models, jobs, running, runningLabel, connected, live, chapterRev, load, connect, disconnect, refreshProject, refreshChapters, refreshAgents, refreshModels, refreshJobs, saveProject, run, stop } } export type ProjectStore = ReturnType export const projectStoreKey: InjectionKey = Symbol('project-store') export function useProjectStore() { const store = inject(projectStoreKey) if (!store) throw new Error('useProjectStore 必须在项目页面内使用') return store }