Files
jocay 3c9b65d982 AI 小说工坊:主编 + 作家多模型协作写作应用
- 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>
2026-10-18 02:04:40 +08:00

106 lines
4.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ROLE_LABEL } from '#shared/types'
const props = defineProps<{ agent: AgentItem, builtin: string }>()
const store = useProjectStore()
const api = useApi()
const form = reactive({
name: props.agent.name,
modelId: props.agent.modelId,
systemPrompt: props.agent.systemPrompt,
styleNotes: props.agent.styleNotes,
enabled: props.agent.enabled
})
watch(() => props.agent, (a) => {
Object.assign(form, { name: a.name, modelId: a.modelId, systemPrompt: a.systemPrompt, styleNotes: a.styleNotes, enabled: a.enabled })
})
const dirty = computed(() => form.name !== props.agent.name || form.modelId !== props.agent.modelId || form.systemPrompt !== props.agent.systemPrompt || form.styleNotes !== props.agent.styleNotes || form.enabled !== props.agent.enabled)
const saving = ref(false)
async function save() {
saving.value = true
try {
await api.put(`/api/agents/${props.agent.id}`, { ...form })
await store.refreshAgents()
api.toast.add({ title: `${form.name} 已保存`, color: 'success' })
}
finally {
saving.value = false
}
}
async function remove() {
if (!window.confirm(`删除作家「${props.agent.name}」?`)) return
await api.del(`/api/agents/${props.agent.id}`)
await store.refreshAgents()
}
const modelItems = computed(() => store.models.value.map(m => ({ label: `${m.providerName} / ${m.label}`, value: m.id })))
const showBuiltin = ref(false)
const ICON = { editor: 'i-lucide-crown', writer: 'i-lucide-pen-tool', summarizer: 'i-lucide-notebook-pen' } as const
const DESC = {
editor: '负责规划章节大纲、为每章撰写简报、选派作家、审稿。建议使用推理能力强的模型(如 Claude)。',
writer: '根据主编的简报写出章节正文。可以创建多位风格不同的作家,使用不同模型。',
summarizer: '章节确认后提炼摘要并更新人物状态、伏笔等故事记忆。可以用便宜快速的模型;未配置时使用主编的模型。'
} as const
</script>
<template>
<UCard :class="form.enabled ? '' : 'opacity-60'">
<template #header>
<div class="flex items-center gap-3">
<UIcon :name="ICON[agent.role]" class="size-5 text-primary" />
<UInput v-model="form.name" variant="ghost" class="font-semibold max-w-56" />
<UBadge variant="subtle" size="sm">
{{ ROLE_LABEL[agent.role] }}
</UBadge>
<UBadge v-if="agent.createdBy === 'editor'" color="info" variant="subtle" size="sm">
主编创建
</UBadge>
<div class="ms-auto flex items-center gap-2">
<USwitch v-if="agent.role === 'writer'" v-model="form.enabled" size="sm" label="启用" />
<UButton v-if="agent.role === 'writer'" icon="i-lucide-trash-2" size="xs" color="error" variant="ghost" @click="remove" />
</div>
</div>
</template>
<div class="space-y-4">
<p class="text-xs text-muted">
{{ DESC[agent.role] }}
</p>
<div class="grid sm:grid-cols-2 gap-4">
<UFormField label="模型" required>
<USelect
:model-value="form.modelId ?? undefined"
:items="modelItems"
placeholder="选择模型"
@update:model-value="(v: string) => (form.modelId = v)"
class="w-full"
/>
</UFormField>
<UFormField v-if="agent.role === 'writer'" label="擅长风格" hint="主编选派作家时参考">
<UInput v-model="form.styleNotes" placeholder="例如:细腻的情感描写、快节奏动作戏" class="w-full" />
</UFormField>
</div>
<UFormField label="自定义系统提示词" hint="最高优先级,与内置提示词冲突时以此为准">
<UTextarea
v-model="form.systemPrompt"
autoresize
:rows="4"
:maxrows="20"
class="w-full"
:placeholder="agent.role === 'writer' ? '例如:你的文字冷峻克制,擅长用白描和短句营造张力;对话不加多余的动作提示……' : agent.role === 'editor' ? '例如:每章必须有一个明确的小高潮;感情线推进要克制……' : '例如:摘要要保留关键对白原句……'"
/>
</UFormField>
<div>
<UButton :label="showBuiltin ? '收起内置提示词' : '查看内置提示词'" :icon="showBuiltin ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" size="xs" color="neutral" variant="link" class="px-0" @click="showBuiltin = !showBuiltin" />
<pre v-if="showBuiltin" class="mt-2 text-xs whitespace-pre-wrap rounded-md bg-elevated p-3 text-muted">{{ builtin }}</pre>
</div>
<div class="flex justify-end">
<UButton label="保存" icon="i-lucide-save" size="sm" :loading="saving" :disabled="!dirty" @click="save" />
</div>
</div>
</UCard>
</template>