fix: anima-extract 改用 jsc-anima 專屬快取 clone,不論在哪個專案都對 jsc-anima 開 PR

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 09:09:14 +00:00
co-authored by Claude Sonnet 5.5
parent 30aedeae2d
commit 2a4d138cdc
4 changed files with 121 additions and 30 deletions
+49 -14
View File
@@ -1,9 +1,12 @@
import type { Engine } from 'claude-code'
import { teaToken } from './wiki'
import { chunkText, extractPrompt, mergeChunks, mergeCharacter, mergeWork, parseExtracted, parseRemote, slugify, validateCharacter } from './extract'
import type { CharacterPack, Conflict, Extracted, WorkPack } from './extract'
export const EXTRACT_COMMAND = 'anima-extract'
// PR 永遠開在 jsc-anima repo,不論 session 在哪個專案:改用專屬的快取 clone 讀寫角色包。
export const ANIMA_REPO_URL = 'https://gitea.jsc.idv.tw/jiantw83/jsc-anima.git'
const PENDING_KEY = 'extract:pending'
const USAGE = `用法:/jsc-anima:${EXTRACT_COMMAND} <章節檔案路徑>(或 paste <章節名稱> 並於下一行起貼上內容),之後用 /jsc-anima:${EXTRACT_COMMAND} confirm 逐步確認,cancel 取消。`
@@ -38,6 +41,40 @@ 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
type Repo = { dir: string; base: string; auth: string[] }
async function gitIn($: Engine, auth: string[], dir: string, a: string[]) {
const r = await $.process.run(['git', ...auth, ...a], { cwd: dir, timeoutMs: 120000 })
if (r.exitCode !== 0) throw new Error(`git ${a[0]} 失敗:${String(r.stderr).trim()}`)
return String(r.stdout).trim()
}
// 取得 jsc-anima 的快取 clone 並對齊 origin 預設分支(認證優先 tea 登入,其次 GITEA_TOKEN;token 只放在指令參數)。
async function syncRepo($: Engine): Promise<Repo | string> {
const home = (await $.env.get('HOME')) ?? ''
const dir = `${home}/.cache/jsc-anima/repo`
let token: string | undefined
try {
token = teaToken(await $.fs.read(`${home}/.config/tea/config.yml`), new URL(ANIMA_REPO_URL).host)
} catch {}
token ??= (await $.env.get('GITEA_TOKEN')) || undefined
const auth = token ? ['-c', `http.extraHeader=Authorization: token ${token}`] : []
try {
if (!(await $.fs.exists(`${dir}/.git`).catch(() => false))) {
await $.process.run(['mkdir', '-p', dir.replace(/\/[^/]+$/, '')], { timeoutMs: 5000 })
await gitIn($, auth, home || '/', ['clone', ANIMA_REPO_URL, dir])
}
await gitIn($, auth, dir, ['fetch', 'origin', '--prune'])
await gitIn($, auth, dir, ['remote', 'set-head', 'origin', '--auto'])
const base = (await gitIn($, auth, dir, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'])).replace(/^origin\//, '')
await gitIn($, auth, dir, ['checkout', '--detach', '--force', `origin/${base}`])
await gitIn($, auth, dir, ['clean', '-fd', 'characters'])
return { dir, base, auth }
} catch (err) {
return `無法取得 jsc-anima repo(${ANIMA_REPO_URL}):${(err as Error).message}`
}
}
async function stageFile($: Engine, path: string): Promise<string> {
const cwd = await $.session.cwd().catch(() => '')
const full = path.startsWith('/') ? path : `${cwd}/${path}`
@@ -48,7 +85,6 @@ async function stageFile($: Engine, path: string): Promise<string> {
}
async function stage($: Engine, chapter: string, text: string): Promise<string> {
const cwd = await $.session.cwd().catch(() => '')
const chunks = chunkText(text)
const parts: Extracted[] = []
for (const [i, chunk] of chunks.entries()) {
@@ -58,6 +94,9 @@ async function stage($: Engine, chapter: string, text: string): Promise<string>
if (!parsed) return `第 ${i + 1}/${chunks.length} 段的回覆不是有效的 JSON,尚未寫入任何檔案。`
parts.push(parsed)
}
const repo = await syncRepo($)
if (typeof repo === 'string') return repo
const cwd = repo.dir
const extracted = mergeChunks(parts)
const workSlug = slugify(extracted.work.slug || extracted.work.nameEn || extracted.work.name)
if (!workSlug) return '無法產生作品代號,請在章節中提供作品的英文名。'
@@ -115,23 +154,19 @@ function run($: Engine, argv: string[], cwd: string) {
}
async function openPr($: Engine, p: Pending, plan: Plan): Promise<string> {
const root = p.root
const git = async (...a: string[]) => {
const r = await run($, ['git', ...a], root)
if (r.exitCode !== 0) throw new Error(`git ${a[0]} 失敗:${String(r.stderr).trim()}`)
return String(r.stdout).trim()
}
const repo = await syncRepo($)
if (typeof repo === 'string') return repo
const root = repo.dir
const git = (...a: string[]) => gitIn($, repo.auth, root, a)
try {
const prev = await git('rev-parse', '--abbrev-ref', 'HEAD')
const base = (await git('symbolic-ref', '--short', 'refs/remotes/origin/HEAD')).replace(/^origin\//, '')
const base = repo.base
const branch = `feat/extract-${plan.workSlug}-${slugify(p.chapter) || 'chapter'}`
await git('fetch', 'origin', base)
await git('switch', '-c', branch, '--no-track', `origin/${base}`)
await git('checkout', '-B', branch, '--no-track', `origin/${base}`)
for (const f of plan.files) await $.fs.write(f.path, `${JSON.stringify(f.json, null, 2)}\n`)
await git('add', 'characters')
await git('commit', '-m', `feat: 萃取《${plan.workName}》${p.chapter} 的作品與角色包`)
await git('push', '-u', 'origin', branch)
await git('switch', prev)
await git('push', '-u', '--force-with-lease', 'origin', branch)
await git('checkout', '--detach', `origin/${base}`)
const title = `feat: 萃取《${plan.workName}》${p.chapter} 的角色包`
const body = [
@@ -177,7 +212,7 @@ export async function runExtract($: Engine, args: string): Promise<string> {
}
if (head === 'paste') {
// 貼上:第一行 paste <章節名稱>,下一行起為章節內容。
const [first = '', ...body] = args.trim().split('\n')
const [first = '', ...body] = args.trim().replace(/\r\n?/g, '\n').split('\n')
const name = first.replace(/^paste\s*/, '').trim()
const text = body.join('\n')
if (!name || !text.trim()) return `貼上章節的用法:/jsc-anima:${EXTRACT_COMMAND} paste <章節名稱>,下一行起貼上章節內容。`