feat: 補齊工具一(最新章節狀態、作品用語、語言欄位、貼上章節、台詞引號檢查)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-10-15 08:34:36 +00:00
co-authored by Claude Sonnet 5.5
parent 8c25728206
commit fb04b0fa23
7 changed files with 89 additions and 10 deletions
+11 -2
View File
@@ -19,6 +19,7 @@ export type CharacterPack = {
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
axes: Axes
anime: { moe: string; speech: string; emotions: string; kaomoji: string; themeColor: string }
language?: string
baseChapter: string
timeline: StateEntry[]
conflicts: Conflict[]
@@ -42,6 +43,7 @@ export type Extracted = {
persona: CharacterPack['persona']
axes: Axes
anime: CharacterPack['anime']
language?: string
state: Omit<StateEntry, 'chapter'>
}[]
}
@@ -88,6 +90,7 @@ export const extractPrompt = (chunk: string, index: number, total: number): stri
'"characters":[{"name":"","nameEn":"","slug":"","persona":{"selfRef":"","address":"","tone":"","catchphrase":"","values":"","taboos":""},',
'"axes":{"gentle":0,"tsundere":0,"lively":0,"formal":0,"talkative":0,"humor":0,"proactive":0,"expressive":0},',
'"anime":{"moe":"","speech":"","emotions":"","kaomoji":"","themeColor":""},',
'"language":"回覆語言與口癖使用方式,例如:繁體中文,少量原作口癖",',
'"state":{"mood":"","dialogueStyle":"","monologue":"","relationships":[{"with":"","kind":"friend|romantic|family|other","note":""}],"age":null,"place":""}}]}',
'',
'章節內容:',
@@ -121,8 +124,13 @@ export const mergeChunks = (parts: Extracted[]): Extracted => {
return { work, characters: [...chars.values()] }
}
// 引號內長句多半是原作台詞:角色包只放自己撰寫的概括。
const QUOTED = /[「『“"][^」』”"]{20,}[」』”"]/
export const validateCharacter = (c: CharacterPack): string[] => {
const errs: string[] = []
const texts = [...Object.values(c.persona ?? {}), ...Object.values(c.anime ?? {}), ...(c.timeline ?? []).flatMap(t => [t.mood, t.dialogueStyle, t.monologue])]
if (texts.some(t => typeof t === 'string' && QUOTED.test(t))) errs.push(`${c.name}:欄位含長段引號內容(疑似抄錄原作台詞)`)
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(c.slug)) errs.push(`${c.name}:角色代號格式不合法`)
if (!c.name) errs.push('缺少角色名稱')
for (const k of PERSONA_KEYS) {
@@ -139,6 +147,7 @@ export const validateCharacter = (c: CharacterPack): string[] => {
const v = c.axes?.[k]
if (!Number.isInteger(v) || v < 0 || v > 10) errs.push(`${c.name}:axes.${k} 必須是 0-10 的整數`)
}
if (c.language !== undefined && (typeof c.language !== 'string' || c.language.length > MAX_FIELD)) errs.push(`${c.name}:language 必須是簡短字串`)
if (!Array.isArray(c.timeline) || c.timeline.length === 0) errs.push(`${c.name}:時間線不可為空`)
for (const t of c.timeline ?? []) {
if (!t.chapter) errs.push(`${c.name}:時間線缺少來源章節`)
@@ -172,7 +181,7 @@ export const mergeCharacter = (
action: '新增',
detail: `時間線 ${chapter}`,
conflicts: [],
pack: { slug, name: incoming.name, work: workSlug, persona: incoming.persona, axes: incoming.axes, anime: incoming.anime, baseChapter: chapter, timeline: [entry], conflicts: [] },
pack: { slug, name: incoming.name, work: workSlug, persona: incoming.persona, axes: incoming.axes, anime: incoming.anime, language: incoming.language, baseChapter: chapter, timeline: [entry], conflicts: [] },
}
}
const sameChapter = existing.timeline.some(t => t.chapter === chapter)
@@ -184,7 +193,7 @@ export const mergeCharacter = (
const conflicts: Conflict[] = existing.conflicts.filter(c => c.chapter !== chapter)
let base = existing
if (overwriteBase) {
base = { ...existing, name: incoming.name, persona: incoming.persona, axes: incoming.axes, anime: incoming.anime }
base = { ...existing, name: incoming.name, persona: incoming.persona, axes: incoming.axes, anime: incoming.anime, language: incoming.language ?? existing.language }
} else {
const diffs: [string, string, string][] = []
for (const k of PERSONA_KEYS) if (existing.persona[k] !== incoming.persona[k]) diffs.push([`persona.${k}`, existing.persona[k], incoming.persona[k]])