3c9b65d982
- Nuxt 4 + Nuxt UI 4 + AI SDK 7 + Drizzle/SQLite - 主编规划大纲与章节简报、选派作家;作家流式写入章节文件并实时推送前端 - 逐章确认模式(默认)与自动模式;摘要与故事记忆 - 酒馆角色卡(PNG/JSON,V1-V3)与世界书导入 - 自定义最高优先级系统提示词、审稿/修改/多版本、任务日志与提示词检视 - 模拟 LLM 服务与端到端测试脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.8 KiB
TypeScript
122 lines
4.8 KiB
TypeScript
import type { InjectionKey } from 'vue'
|
|
import type { JobInfo, ProjectEvent } from '#shared/types'
|
|
|
|
/** 项目工作区的共享状态:数据 + SSE 实时事件 */
|
|
export function createProjectStore(projectId: string) {
|
|
const api = useApi()
|
|
const project = ref<ProjectDetail | null>(null)
|
|
const chapters = ref<ChapterListItem[]>([])
|
|
const agents = ref<AgentItem[]>([])
|
|
const models = ref<ModelItem[]>([])
|
|
const jobs = ref<JobInfo[]>([])
|
|
const running = ref(false)
|
|
const runningLabel = ref('')
|
|
const connected = ref(false)
|
|
/** 正在写作的章节实时文本:chapterId → { versionId, text } */
|
|
const live = reactive<Record<string, { versionId: string, text: string }>>({})
|
|
/** 章节变更计数,章节详情组件据此刷新 */
|
|
const chapterRev = reactive<Record<string, number>>({})
|
|
|
|
const refreshProject = async () => { project.value = await api.get<ProjectDetail>(`/api/projects/${projectId}`) }
|
|
const refreshChapters = async () => { chapters.value = await api.get<ChapterListItem[]>(`/api/projects/${projectId}/chapters`) }
|
|
const refreshAgents = async () => { agents.value = await api.get<AgentItem[]>(`/api/projects/${projectId}/agents`) }
|
|
const refreshModels = async () => { models.value = await api.get<ModelItem[]>('/api/models') }
|
|
const refreshJobs = async () => { jobs.value = await api.get<JobInfo[]>(`/api/projects/${projectId}/jobs?limit=100`) }
|
|
|
|
let chapterTimer: ReturnType<typeof setTimeout> | 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<string, unknown>) {
|
|
project.value = { ...project.value!, ...(await api.put<ProjectDetail>(`/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<typeof createProjectStore>
|
|
export const projectStoreKey: InjectionKey<ProjectStore> = Symbol('project-store')
|
|
|
|
export function useProjectStore() {
|
|
const store = inject(projectStoreKey)
|
|
if (!store) throw new Error('useProjectStore 必须在项目页面内使用')
|
|
return store
|
|
}
|