|
|
@@ -21,6 +21,28 @@ const ROLE_PATTERN = /(男主|女主|男配|女配|反派|导师|盟友|配角|
|
|
|
const NON_CHARACTER_HEADING_PATTERN =
|
|
|
/^(?:人物设定|角色设定|人物小传|角色小传|人物关系|角色关系|群像|设定总览|世界观|卷纲|章纲|细纲|大纲|设定|背景|地图|力量体系|金手指|伏笔|组织|势力|时间线|剧情线|第.{1,12}卷(?:[::].*)?)$/
|
|
|
|
|
|
+function extractRoleFromTrailing(text: string): string | null {
|
|
|
+ // 支持 "张三(男主)"、"张三(男主)"、"张三 — 男主"、"张三 - 男主"、"张三 男主"
|
|
|
+ const parenMatch = text.match(/[((]\s*(男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*[))]/)
|
|
|
+ if (parenMatch) return parenMatch[1]
|
|
|
+ const tailMatch = text.match(/[\s—\-–]+\s*(男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*$/)
|
|
|
+ if (tailMatch) return tailMatch[1]
|
|
|
+ const endMatch = text.match(/\s+(男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*$/)
|
|
|
+ if (endMatch) return endMatch[1]
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+function extractNameFromHeading(rawTitle: string): string {
|
|
|
+ // 去掉前缀 "角色设定"、"角色"、"人物小传" 等
|
|
|
+ let name = rawTitle.replace(/^角色(?:设定|小传|简介)?[::\-\s]*/, "")
|
|
|
+ name = name.replace(/^人物(?:设定|小传|简介)?[::\-\s]*/, "")
|
|
|
+ // 去掉尾部 role 标注
|
|
|
+ name = name.replace(/[((]\s*(?:男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*[))]/, "")
|
|
|
+ name = name.replace(/[\s—\-–]+\s*(?:男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*$/, "")
|
|
|
+ name = name.replace(/\s+(?:男主|女主|男配|女配|反派|导师|盟友|配角|主角)\s*$/, "")
|
|
|
+ return cleanFieldValue(name)
|
|
|
+}
|
|
|
+
|
|
|
export function buildCharacterFileName(roleType: string, characterName: string): string {
|
|
|
const role = sanitizeOutlineFileNamePart(roleType) || "角色"
|
|
|
const name = sanitizeOutlineFileNamePart(characterName) || "未命名"
|
|
|
@@ -59,24 +81,47 @@ function createDraft(
|
|
|
|
|
|
function splitByCharacterHeadings(content: string): CharacterSaveDraft[] {
|
|
|
const lines = content.split(/\r?\n/)
|
|
|
- const ranges: Array<{ start: number; end: number; roleType: string; name: string }> = []
|
|
|
+ const ranges: Array<{ start: number; end: number; roleType: string; name: string; confidence: CharacterSaveConfidence }> = []
|
|
|
|
|
|
for (let index = 0; index < lines.length; index++) {
|
|
|
- const match = lines[index].match(/^#{1,4}\s*(?:(男主|女主|男配|女配|反派|导师|盟友|配角|主角)[::\-\s]+)?(.{1,24})\s*$/)
|
|
|
+ // 匹配 ## 标题,捕获标题原文
|
|
|
+ const match = lines[index].match(/^#{1,4}\s+(.{1,32})\s*$/)
|
|
|
if (!match) continue
|
|
|
|
|
|
- const name = cleanFieldValue(match[2].replace(/^角色[-::\s]*/, ""))
|
|
|
+ const rawTitle = match[1]
|
|
|
+ // 优先尝试从标题前缀提取 "男主:/女主:/..." 旧格式
|
|
|
+ const prefixRoleMatch = rawTitle.match(/^(男主|女主|男配|女配|反派|导师|盟友|配角|主角)[::\-\s]+(.{1,24})$/)
|
|
|
+
|
|
|
+ let name: string
|
|
|
+ let roleType: string | null
|
|
|
+
|
|
|
+ if (prefixRoleMatch) {
|
|
|
+ // 旧格式:"## 男主:张三"
|
|
|
+ name = extractNameFromHeading(prefixRoleMatch[2])
|
|
|
+ roleType = prefixRoleMatch[1]
|
|
|
+ } else {
|
|
|
+ // 新格式:"## 张三(男主)"、"## 张三 - 男主"、"## 角色设定:张三"、"## 张三"
|
|
|
+ name = extractNameFromHeading(rawTitle)
|
|
|
+ if (!name) continue
|
|
|
+ roleType = extractRoleFromTrailing(rawTitle)
|
|
|
+ }
|
|
|
+
|
|
|
if (!name || NON_CHARACTER_HEADING_PATTERN.test(name)) continue
|
|
|
|
|
|
+ // 从邻近 8 行查找 "角色定位:男主" 字段
|
|
|
const nearbyText = lines.slice(index, index + 8).join("\n")
|
|
|
const roleFromFields = nearbyText.match(/角色定位[::]\s*(男主|女主|男配|女配|反派|导师|盟友|配角|主角)/)?.[1]
|
|
|
- if (!match[1] && !roleFromFields) continue
|
|
|
+
|
|
|
+ const finalRole = roleType ?? roleFromFields
|
|
|
+ // 没有任何 role 信息时跳过纯名字标题,避免误识别 "## 第一章" 之类的章节
|
|
|
+ if (!finalRole) continue
|
|
|
|
|
|
ranges.push({
|
|
|
start: index,
|
|
|
end: lines.length,
|
|
|
- roleType: match[1] ?? roleFromFields ?? "角色",
|
|
|
+ roleType: finalRole,
|
|
|
name,
|
|
|
+ confidence: roleType ? "high" : "medium",
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -88,7 +133,7 @@ function splitByCharacterHeadings(content: string): CharacterSaveDraft[] {
|
|
|
range.roleType,
|
|
|
range.name,
|
|
|
lines.slice(range.start, range.end).join("\n"),
|
|
|
- "high",
|
|
|
+ range.confidence,
|
|
|
))
|
|
|
}
|
|
|
|