feat: 補齊工具一(最新章節狀態、作品用語、語言欄位、貼上章節、台詞引號檢查)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8c25728206
commit
fb04b0fa23
@@ -33,6 +33,7 @@ claude plugin install jsc-anima
|
|||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
- `/jsc-anima:anima-extract <章節檔案路徑>`:從輕小說章節萃取作品與角色,依序確認代號與變更摘要後,寫入 `characters/<作品代號>/` 並對預設分支建立 PR(不自動合併)。
|
- `/jsc-anima:anima-extract <章節檔案路徑>`:從輕小說章節萃取作品與角色,依序確認代號與變更摘要後,寫入 `characters/<作品代號>/` 並對預設分支建立 PR(不自動合併)。
|
||||||
|
- `/jsc-anima:anima-extract paste <章節名稱>`(下一行起貼上章節內容):不想存檔時直接貼上,之後流程相同。
|
||||||
|
|
||||||
## 更新
|
## 更新
|
||||||
|
|
||||||
|
|||||||
@@ -73,3 +73,24 @@ test('角色包轉成可啟動角色:代號、年齡與原作戀人', () => {
|
|||||||
registerCharacters([c, c])
|
registerCharacters([c, c])
|
||||||
expect(CHARACTERS.filter(x => x.slug === 'demo-rin').length).toBe(1)
|
expect(CHARACTERS.filter(x => x.slug === 'demo-rin').length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('角色包:最新章節狀態、作品用語與語言設定進入 persona', () => {
|
||||||
|
const pack: PackLike = {
|
||||||
|
slug: 'mio', name: '澪', work: 'demo2',
|
||||||
|
persona: { selfRef: '我', address: '你', tone: '冷靜', catchphrase: '嗯', values: '誠實', taboos: '不說謊' },
|
||||||
|
axes: CHARACTERS[0]!.axes,
|
||||||
|
anime: { moe: 'm', speech: 's', emotions: 'e', kaomoji: 'k', themeColor: 'red' },
|
||||||
|
language: '繁體中文,不使用口癖',
|
||||||
|
timeline: [
|
||||||
|
{ chapter: '第1章', mood: '緊張', dialogueStyle: '客氣', monologue: '想變強', place: '學院', age: 16, relationships: [] },
|
||||||
|
{ chapter: '第2章', mood: '安心', dialogueStyle: '放鬆', monologue: '想守護', place: '街上', age: 16, relationships: [] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const c = packToCharacter(pack, '示範二', [{ term: '魔核', definition: '力量來源' }])
|
||||||
|
const text = personaSection(c, c.axes)
|
||||||
|
expect(text).toContain('最新章節(第2章)')
|
||||||
|
expect(text).toContain('安心')
|
||||||
|
expect(text).not.toContain('緊張')
|
||||||
|
expect(text).toContain('魔核=力量來源')
|
||||||
|
expect(text).toContain('不使用口癖')
|
||||||
|
})
|
||||||
|
|||||||
+15
-3
@@ -20,6 +20,9 @@ export type Character = {
|
|||||||
persona: string
|
persona: string
|
||||||
axes: Axes
|
axes: Axes
|
||||||
anime?: Anime
|
anime?: Anime
|
||||||
|
language?: string
|
||||||
|
latest?: { chapter: string; mood: string; dialogueStyle: string; monologue: string; place: string }
|
||||||
|
terms?: readonly { term: string; definition: string }[]
|
||||||
timeline: readonly Timeline[]
|
timeline: readonly Timeline[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +73,8 @@ export type PackLike = {
|
|||||||
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
|
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
|
||||||
axes: Axes
|
axes: Axes
|
||||||
anime: Anime
|
anime: Anime
|
||||||
timeline: { chapter: string; age: number | null; relationships: { with: string; kind: string }[] }[]
|
language?: string
|
||||||
|
timeline: { chapter: string; mood?: string; dialogueStyle?: string; monologue?: string; place?: string; age: number | null; relationships: { with: string; kind: string }[] }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapterNo = (s: string, i: number): number => {
|
const chapterNo = (s: string, i: number): number => {
|
||||||
@@ -78,13 +82,19 @@ const chapterNo = (s: string, i: number): number => {
|
|||||||
return n ? Number(n[0]) : i + 1
|
return n ? Number(n[0]) : i + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export const packToCharacter = (p: PackLike, workName: string): Character => ({
|
export const packToCharacter = (p: PackLike, workName: string, terms: Character['terms'] = []): Character => ({
|
||||||
slug: `${p.work}-${p.slug}`,
|
slug: `${p.work}-${p.slug}`,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
work: workName,
|
work: workName,
|
||||||
persona: `自稱「${p.persona.selfRef}」,稱呼使用者為「${p.persona.address}」。語氣:${p.persona.tone}。口頭禪:${p.persona.catchphrase}。價值觀:${p.persona.values}。禁忌:${p.persona.taboos}。`,
|
persona: `自稱「${p.persona.selfRef}」,稱呼使用者為「${p.persona.address}」。語氣:${p.persona.tone}。口頭禪:${p.persona.catchphrase}。價值觀:${p.persona.values}。禁忌:${p.persona.taboos}。`,
|
||||||
axes: p.axes,
|
axes: p.axes,
|
||||||
anime: p.anime,
|
anime: p.anime,
|
||||||
|
language: p.language,
|
||||||
|
terms,
|
||||||
|
// 永遠採最新已處理章節的狀態。
|
||||||
|
latest: p.timeline.length
|
||||||
|
? (t => ({ chapter: t.chapter, mood: t.mood ?? '', dialogueStyle: t.dialogueStyle ?? '', monologue: t.monologue ?? '', place: t.place ?? '' }))(p.timeline[p.timeline.length - 1]!)
|
||||||
|
: undefined,
|
||||||
timeline: p.timeline.map((t, i) => {
|
timeline: p.timeline.map((t, i) => {
|
||||||
const lover = t.relationships.find(r => r.kind === 'romantic')
|
const lover = t.relationships.find(r => r.kind === 'romantic')
|
||||||
return { chapter: chapterNo(t.chapter, i), age: t.age ?? 0, ...(lover ? { romance: lover.with } : {}) }
|
return { chapter: chapterNo(t.chapter, i), age: t.age ?? 0, ...(lover ? { romance: lover.with } : {}) }
|
||||||
@@ -127,6 +137,8 @@ export const personaSection = (c: Character, axes: Axes): string =>
|
|||||||
...(c.anime
|
...(c.anime
|
||||||
? [`動漫刻畫:萌屬性——${c.anime.moe};說話規格——${c.anime.speech};情緒反應——${c.anime.emotions};顏文字傾向——${c.anime.kaomoji}。`]
|
? [`動漫刻畫:萌屬性——${c.anime.moe};說話規格——${c.anime.speech};情緒反應——${c.anime.emotions};顏文字傾向——${c.anime.kaomoji}。`]
|
||||||
: []),
|
: []),
|
||||||
|
...(c.latest ? [`最新章節(${c.latest.chapter})的狀態:心情——${c.latest.mood};說話方式——${c.latest.dialogueStyle};內心獨白傾向——${c.latest.monologue};所在——${c.latest.place}。`] : []),
|
||||||
|
...(c.terms && c.terms.length ? [`作品用語:${c.terms.slice(0, 30).map(t => `${t.term}=${t.definition}`).join(';')}。`] : []),
|
||||||
`個性數值(0-10):${(Object.keys(AXIS_LABEL) as (keyof Axes)[]).map(k => `${AXIS_LABEL[k]} ${axes[k]}`).join('、')}。`,
|
`個性數值(0-10):${(Object.keys(AXIS_LABEL) as (keyof Axes)[]).map(k => `${AXIS_LABEL[k]} ${axes[k]}`).join('、')}。`,
|
||||||
'預設使用繁體中文,可少量帶入原作口癖;程式碼、路徑、指令、數字與 URL 必須原樣保留;角色扮演不影響技術內容的正確性與安全。',
|
`${c.language ? `回覆語言與口癖:${c.language}。` : '預設使用繁體中文,可少量帶入原作口癖。'}程式碼、路徑、指令、數字與 URL 必須原樣保留;角色扮演不影響技術內容的正確性與安全。`,
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
@@ -102,3 +102,27 @@ test('schema 驗證失敗不寫檔、不建 PR;cancel 清除待確認', async
|
|||||||
expect([...s.files.keys()].some(k => k.includes('characters'))).toBe(false)
|
expect([...s.files.keys()].some(k => k.includes('characters'))).toBe(false)
|
||||||
expect(await run($ as never, 'cancel')).toContain('已取消')
|
expect(await run($ as never, 'cancel')).toContain('已取消')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('貼上章節內容:paste <章節名稱> 加下一行起的內容,並寫入語言欄位與 index.json', async ($, on) => {
|
||||||
|
const s = setup(on as never, [reply(char({ language: '繁體中文,少量原作口癖' }))])
|
||||||
|
await $.session.start({ source: 'startup', cwd: '/repo' } as never)
|
||||||
|
expect(await run($ as never, 'paste')).toContain('貼上章節的用法')
|
||||||
|
const t1 = await run($ as never, 'paste 第一章\n這是貼上的章節內容。')
|
||||||
|
expect(t1).toContain('aoi')
|
||||||
|
expect(s.prompts[0]).toContain('這是貼上的章節內容。')
|
||||||
|
await run($ as never, 'confirm')
|
||||||
|
await run($ as never, 'confirm')
|
||||||
|
const pack = JSON.parse(s.files.get('/repo/characters/ts/aoi.json')!)
|
||||||
|
expect(pack.timeline[0].chapter).toBe('第一章')
|
||||||
|
expect(pack.language).toBe('繁體中文,少量原作口癖')
|
||||||
|
expect(JSON.parse(s.files.get('/repo/characters/index.json')!).packs[0]).toEqual({ work: 'ts', workName: '測試物語', slug: 'aoi' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('欄位含長段引號內容視為抄錄原作台詞,不寫檔', async ($, on) => {
|
||||||
|
const quoted = char({ persona: { selfRef: '我', address: '你', tone: '「這是一段很長很長的原作台詞內容,不應該出現在角色包裡面」', catchphrase: '加油', values: '誠實', taboos: '背叛' } })
|
||||||
|
const s = setup(on as never, [reply(quoted)], new Map([['/repo/ch1.txt', 'a']]))
|
||||||
|
await $.session.start({ source: 'startup', cwd: '/repo' } as never)
|
||||||
|
await run($ as never, 'ch1.txt')
|
||||||
|
expect(await run($ as never, 'confirm')).toContain('疑似抄錄原作台詞')
|
||||||
|
expect([...s.files.keys()].some(k => k.includes('characters'))).toBe(false)
|
||||||
|
})
|
||||||
|
|||||||
+11
-2
@@ -19,6 +19,7 @@ export type CharacterPack = {
|
|||||||
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
|
persona: { selfRef: string; address: string; tone: string; catchphrase: string; values: string; taboos: string }
|
||||||
axes: Axes
|
axes: Axes
|
||||||
anime: { moe: string; speech: string; emotions: string; kaomoji: string; themeColor: string }
|
anime: { moe: string; speech: string; emotions: string; kaomoji: string; themeColor: string }
|
||||||
|
language?: string
|
||||||
baseChapter: string
|
baseChapter: string
|
||||||
timeline: StateEntry[]
|
timeline: StateEntry[]
|
||||||
conflicts: Conflict[]
|
conflicts: Conflict[]
|
||||||
@@ -42,6 +43,7 @@ export type Extracted = {
|
|||||||
persona: CharacterPack['persona']
|
persona: CharacterPack['persona']
|
||||||
axes: Axes
|
axes: Axes
|
||||||
anime: CharacterPack['anime']
|
anime: CharacterPack['anime']
|
||||||
|
language?: string
|
||||||
state: Omit<StateEntry, 'chapter'>
|
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":""},',
|
'"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},',
|
'"axes":{"gentle":0,"tsundere":0,"lively":0,"formal":0,"talkative":0,"humor":0,"proactive":0,"expressive":0},',
|
||||||
'"anime":{"moe":"","speech":"","emotions":"","kaomoji":"","themeColor":""},',
|
'"anime":{"moe":"","speech":"","emotions":"","kaomoji":"","themeColor":""},',
|
||||||
|
'"language":"回覆語言與口癖使用方式,例如:繁體中文,少量原作口癖",',
|
||||||
'"state":{"mood":"","dialogueStyle":"","monologue":"","relationships":[{"with":"","kind":"friend|romantic|family|other","note":""}],"age":null,"place":""}}]}',
|
'"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()] }
|
return { work, characters: [...chars.values()] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 引號內長句多半是原作台詞:角色包只放自己撰寫的概括。
|
||||||
|
const QUOTED = /[「『“"][^」』”"]{20,}[」』”"]/
|
||||||
|
|
||||||
export const validateCharacter = (c: CharacterPack): string[] => {
|
export const validateCharacter = (c: CharacterPack): string[] => {
|
||||||
const errs: 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 (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(c.slug)) errs.push(`${c.name}:角色代號格式不合法`)
|
||||||
if (!c.name) errs.push('缺少角色名稱')
|
if (!c.name) errs.push('缺少角色名稱')
|
||||||
for (const k of PERSONA_KEYS) {
|
for (const k of PERSONA_KEYS) {
|
||||||
@@ -139,6 +147,7 @@ export const validateCharacter = (c: CharacterPack): string[] => {
|
|||||||
const v = c.axes?.[k]
|
const v = c.axes?.[k]
|
||||||
if (!Number.isInteger(v) || v < 0 || v > 10) errs.push(`${c.name}:axes.${k} 必須是 0-10 的整數`)
|
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}:時間線不可為空`)
|
if (!Array.isArray(c.timeline) || c.timeline.length === 0) errs.push(`${c.name}:時間線不可為空`)
|
||||||
for (const t of c.timeline ?? []) {
|
for (const t of c.timeline ?? []) {
|
||||||
if (!t.chapter) errs.push(`${c.name}:時間線缺少來源章節`)
|
if (!t.chapter) errs.push(`${c.name}:時間線缺少來源章節`)
|
||||||
@@ -172,7 +181,7 @@ export const mergeCharacter = (
|
|||||||
action: '新增',
|
action: '新增',
|
||||||
detail: `時間線 ${chapter}`,
|
detail: `時間線 ${chapter}`,
|
||||||
conflicts: [],
|
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)
|
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)
|
const conflicts: Conflict[] = existing.conflicts.filter(c => c.chapter !== chapter)
|
||||||
let base = existing
|
let base = existing
|
||||||
if (overwriteBase) {
|
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 {
|
} else {
|
||||||
const diffs: [string, string, string][] = []
|
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]])
|
for (const k of PERSONA_KEYS) if (existing.persona[k] !== incoming.persona[k]) diffs.push([`persona.${k}`, existing.persona[k], incoming.persona[k]])
|
||||||
|
|||||||
+15
-4
@@ -5,7 +5,7 @@ import type { CharacterPack, Conflict, Extracted, WorkPack } from './extract'
|
|||||||
|
|
||||||
export const EXTRACT_COMMAND = 'anima-extract'
|
export const EXTRACT_COMMAND = 'anima-extract'
|
||||||
const PENDING_KEY = 'extract:pending'
|
const PENDING_KEY = 'extract:pending'
|
||||||
const USAGE = `用法:/jsc-anima:${EXTRACT_COMMAND} <章節檔案路徑>,之後用 /jsc-anima:${EXTRACT_COMMAND} confirm 逐步確認,cancel 取消。`
|
const USAGE = `用法:/jsc-anima:${EXTRACT_COMMAND} <章節檔案路徑>(或 paste <章節名稱> 並於下一行起貼上內容),之後用 /jsc-anima:${EXTRACT_COMMAND} confirm 逐步確認,cancel 取消。`
|
||||||
|
|
||||||
type Plan = {
|
type Plan = {
|
||||||
files: { path: string; json: unknown }[]
|
files: { path: string; json: unknown }[]
|
||||||
@@ -38,13 +38,17 @@ async function readJson<T>($: Engine, path: string): Promise<T | undefined> {
|
|||||||
|
|
||||||
const slugKey = (c: Extracted['characters'][number]) => slugify(c.slug || c.nameEn || c.name) || c.name
|
const slugKey = (c: Extracted['characters'][number]) => slugify(c.slug || c.nameEn || c.name) || c.name
|
||||||
|
|
||||||
async function stage($: Engine, path: string): Promise<string> {
|
async function stageFile($: Engine, path: string): Promise<string> {
|
||||||
const cwd = await $.session.cwd().catch(() => '')
|
const cwd = await $.session.cwd().catch(() => '')
|
||||||
const full = path.startsWith('/') ? path : `${cwd}/${path}`
|
const full = path.startsWith('/') ? path : `${cwd}/${path}`
|
||||||
if (!(await $.fs.exists(full).catch(() => false))) return `找不到檔案:${path}`
|
if (!(await $.fs.exists(full).catch(() => false))) return `找不到檔案:${path}`
|
||||||
const text = String(await $.fs.read(full))
|
const text = String(await $.fs.read(full))
|
||||||
if (!text.trim()) return `檔案是空的:${path}`
|
if (!text.trim()) return `檔案是空的:${path}`
|
||||||
|
return stage($, (path.split('/').pop() ?? path).replace(/\.[^.]+$/, ''), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stage($: Engine, chapter: string, text: string): Promise<string> {
|
||||||
|
const cwd = await $.session.cwd().catch(() => '')
|
||||||
const chunks = chunkText(text)
|
const chunks = chunkText(text)
|
||||||
const parts: Extracted[] = []
|
const parts: Extracted[] = []
|
||||||
for (const [i, chunk] of chunks.entries()) {
|
for (const [i, chunk] of chunks.entries()) {
|
||||||
@@ -55,7 +59,6 @@ async function stage($: Engine, path: string): Promise<string> {
|
|||||||
parts.push(parsed)
|
parts.push(parsed)
|
||||||
}
|
}
|
||||||
const extracted = mergeChunks(parts)
|
const extracted = mergeChunks(parts)
|
||||||
const chapter = (path.split('/').pop() ?? path).replace(/\.[^.]+$/, '')
|
|
||||||
const workSlug = slugify(extracted.work.slug || extracted.work.nameEn || extracted.work.name)
|
const workSlug = slugify(extracted.work.slug || extracted.work.nameEn || extracted.work.name)
|
||||||
if (!workSlug) return '無法產生作品代號,請在章節中提供作品的英文名。'
|
if (!workSlug) return '無法產生作品代號,請在章節中提供作品的英文名。'
|
||||||
const slugs: Record<string, string> = {}
|
const slugs: Record<string, string> = {}
|
||||||
@@ -172,7 +175,15 @@ export async function runExtract($: Engine, args: string): Promise<string> {
|
|||||||
await $.store.delete(PENDING_KEY)
|
await $.store.delete(PENDING_KEY)
|
||||||
return '已取消,沒有寫入任何檔案。'
|
return '已取消,沒有寫入任何檔案。'
|
||||||
}
|
}
|
||||||
if (head !== 'confirm') return stage($, args.trim())
|
if (head === 'paste') {
|
||||||
|
// 貼上:第一行 paste <章節名稱>,下一行起為章節內容。
|
||||||
|
const [first = '', ...body] = args.trim().split('\n')
|
||||||
|
const name = first.replace(/^paste\s*/, '').trim()
|
||||||
|
const text = body.join('\n')
|
||||||
|
if (!name || !text.trim()) return `貼上章節的用法:/jsc-anima:${EXTRACT_COMMAND} paste <章節名稱>,下一行起貼上章節內容。`
|
||||||
|
return stage($, name, text)
|
||||||
|
}
|
||||||
|
if (head !== 'confirm') return stageFile($, args.trim())
|
||||||
|
|
||||||
const p = (await $.store.get(PENDING_KEY)) as Pending | undefined
|
const p = (await $.store.get(PENDING_KEY)) as Pending | undefined
|
||||||
if (!p) return `沒有待確認的萃取,請先執行 /jsc-anima:${EXTRACT_COMMAND} <章節檔案路徑>。`
|
if (!p) return `沒有待確認的萃取,請先執行 /jsc-anima:${EXTRACT_COMMAND} <章節檔案路徑>。`
|
||||||
|
|||||||
+2
-1
@@ -255,7 +255,8 @@ async function loadPacks($: Engine): Promise<void> {
|
|||||||
const index = JSON.parse(await $.fs.read(`${dir}index.json`)) as { packs: { work: string; workName: string; slug: string }[] }
|
const index = JSON.parse(await $.fs.read(`${dir}index.json`)) as { packs: { work: string; workName: string; slug: string }[] }
|
||||||
for (const i of index.packs) {
|
for (const i of index.packs) {
|
||||||
const pack = JSON.parse(await $.fs.read(`${dir}${i.work}/${i.slug}.json`)) as PackLike
|
const pack = JSON.parse(await $.fs.read(`${dir}${i.work}/${i.slug}.json`)) as PackLike
|
||||||
registerCharacters([packToCharacter(pack, i.workName)])
|
const work = (await $.fs.exists(`${dir}${i.work}/work.json`)) ? (JSON.parse(await $.fs.read(`${dir}${i.work}/work.json`)) as { terms?: { term: string; definition: string }[] }) : undefined
|
||||||
|
registerCharacters([packToCharacter(pack, i.workName, work?.terms ?? [])])
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 角色包損壞不影響內建角色。
|
// 角色包損壞不影響內建角色。
|
||||||
|
|||||||
Reference in New Issue
Block a user