ソースを参照

chore(character-save): 移除未使用 monaco 依赖并增加段落兜底拆分

1. 从 package.json 移除未使用的 @monaco-editor/react 和 monaco-editor 依赖。2. 在 extractCharacterSaveDrafts 中增加 splitByParagraphs 兜底:无标题无字段时按空行或 --- 分割段落,必须包含角色关键字且能提取名字才生成草稿,默认选中让用户确认。
Mochocyang 3 ヶ月 前
親
コミット
f9a25f19a4

+ 0 - 62
package-lock.json

@@ -18,7 +18,6 @@
         "@milkdown/plugin-math": "^7.5.9",
         "@milkdown/react": "^7.21.2",
         "@milkdown/theme-nord": "^7.21.2",
-        "@monaco-editor/react": "^4.7.0",
         "@react-sigma/core": "^5.0.6",
         "@sigma/edge-curve": "^3.1.0",
         "@tailwindcss/vite": "^4.3.2",
@@ -39,7 +38,6 @@
         "katex": "^0.16.47",
         "lucide-react": "^1.23.0",
         "mermaid": "^11.16.0",
-        "monaco-editor": "^0.56.0",
         "opencc-js": "^1.4.0",
         "pinyin-pro": "^3.28.1",
         "react": "^19.2.7",
@@ -2269,29 +2267,6 @@
         }
       }
     },
-    "node_modules/@monaco-editor/loader": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
-      "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
-      "license": "MIT",
-      "dependencies": {
-        "state-local": "^1.0.6"
-      }
-    },
-    "node_modules/@monaco-editor/react": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
-      "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
-      "license": "MIT",
-      "dependencies": {
-        "@monaco-editor/loader": "^1.5.0"
-      },
-      "peerDependencies": {
-        "monaco-editor": ">= 0.25.0 < 1",
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
-      }
-    },
     "node_modules/@napi-rs/wasm-runtime": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
@@ -8332,37 +8307,6 @@
         "obliterator": "^2.0.1"
       }
     },
-    "node_modules/monaco-editor": {
-      "version": "0.56.0",
-      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz",
-      "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==",
-      "license": "MIT",
-      "dependencies": {
-        "dompurify": "3.4.8",
-        "marked": "14.0.0"
-      }
-    },
-    "node_modules/monaco-editor/node_modules/dompurify": {
-      "version": "3.4.8",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
-      "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
-      "license": "(MPL-2.0 OR Apache-2.0)",
-      "optionalDependencies": {
-        "@types/trusted-types": "^2.0.7"
-      }
-    },
-    "node_modules/monaco-editor/node_modules/marked": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
-      "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
-      "license": "MIT",
-      "bin": {
-        "marked": "bin/marked.js"
-      },
-      "engines": {
-        "node": ">= 18"
-      }
-    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -10003,12 +9947,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/state-local": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
-      "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
-      "license": "MIT"
-    },
     "node_modules/statuses": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",

+ 0 - 2
package.json

@@ -27,7 +27,6 @@
     "@milkdown/plugin-math": "^7.5.9",
     "@milkdown/react": "^7.21.2",
     "@milkdown/theme-nord": "^7.21.2",
-    "@monaco-editor/react": "^4.7.0",
     "@react-sigma/core": "^5.0.6",
     "@sigma/edge-curve": "^3.1.0",
     "@tailwindcss/vite": "^4.3.2",
@@ -48,7 +47,6 @@
     "katex": "^0.16.47",
     "lucide-react": "^1.23.0",
     "mermaid": "^11.16.0",
-    "monaco-editor": "^0.56.0",
     "opencc-js": "^1.4.0",
     "pinyin-pro": "^3.28.1",
     "react": "^19.2.7",

+ 32 - 0
src/lib/novel/character-save-extractor.spec.ts

@@ -180,4 +180,36 @@ describe("character-save-extractor", () => {
   it("清理角色文件名中的非法字符", () => {
     expect(buildCharacterFileName("男主", "林:辰/一号")).toBe("角色-男主-林-辰-一号.md")
   })
+
+  it("无标题无字段时按段落兜底拆分多角色", () => {
+    const result = extractCharacterSaveDrafts([
+      "张三,男主,京城世家子弟,性格沉稳。",
+      "",
+      "苏晚,女主,江南书香门第,聪慧机敏。",
+    ].join("\n"))
+
+    expect(result.drafts).toHaveLength(2)
+    expect(result.drafts[0]).toMatchObject({
+      characterName: "张三",
+      roleType: "男主",
+      selected: true,
+    })
+    expect(result.drafts[1]).toMatchObject({
+      characterName: "苏晚",
+      roleType: "女主",
+      selected: true,
+    })
+    expect(result.errors[0]).toContain("已按段落自动拆分")
+  })
+
+  it("段落兜底跳过不含角色关键字的段落", () => {
+    const result = extractCharacterSaveDrafts([
+      "张三,京城世家子弟,性格沉稳。",
+      "",
+      "苏晚,女主,江南书香门第,聪慧机敏。",
+    ].join("\n"))
+
+    expect(result.drafts).toHaveLength(1)
+    expect(result.drafts[0].characterName).toBe("苏晚")
+  })
 })

+ 53 - 0
src/lib/novel/character-save-extractor.ts

@@ -144,6 +144,50 @@ function extractSingleByFields(content: string): CharacterSaveDraft | null {
   return createDraft(role, name, content, role ? "medium" : "low")
 }
 
+const PARAGRAPH_ROLE_PATTERN = /(男主|女主|男配|女配|反派|导师|盟友|配角|主角)/
+const PARAGRAPH_NAME_PATTERNS = [
+  /(?:姓名|角色名|名字)[::]\s*([^\n,,。;;]{1,24})/,
+  /^([^\n,,。;::\-—\s]{1,24})[,,]/,
+  /^([^\n,,。;::\-—\s]{1,24})\s*[,,]/,
+]
+
+function splitByParagraphs(content: string): CharacterSaveDraft[] {
+  // 按空行或 --- 分割段落
+  const paragraphs = content
+    .split(/\n\s*\n|\n---\s*\n/)
+    .map((p) => p.trim())
+    .filter((p) => p.length > 0)
+
+  if (paragraphs.length <= 1) return []
+
+  const drafts: CharacterSaveDraft[] = []
+  for (const para of paragraphs) {
+    // 必须包含角色关键字才认为是角色描述
+    const roleMatch = para.match(PARAGRAPH_ROLE_PATTERN)
+    if (!roleMatch) continue
+
+    // 尝试从段落首行提取名字
+    const firstLine = para.split(/\r?\n/)[0] ?? para
+    let name: string | null = null
+    for (const pattern of PARAGRAPH_NAME_PATTERNS) {
+      const m = firstLine.match(pattern)
+      if (m && m[1]) {
+        name = cleanFieldValue(m[1])
+        if (name) break
+      }
+    }
+    // 没有明确名字时跳过,避免误识别
+    if (!name) continue
+
+    const draft = createDraft(roleMatch[1], name, para, "low")
+    // 段落兜底默认选中,让用户在弹窗中确认
+    draft.selected = true
+    drafts.push(draft)
+  }
+
+  return drafts
+}
+
 export function extractCharacterSaveDrafts(content: string): CharacterSaveExtractionResult {
   const headingDrafts = splitByCharacterHeadings(content)
   if (headingDrafts.length > 0) return { drafts: headingDrafts, errors: [] }
@@ -151,6 +195,15 @@ export function extractCharacterSaveDrafts(content: string): CharacterSaveExtrac
   const fieldDraft = extractSingleByFields(content)
   if (fieldDraft) return { drafts: [fieldDraft], errors: [] }
 
+  // 段落兜底:无标题无字段时按段落分割
+  const paragraphDrafts = splitByParagraphs(content)
+  if (paragraphDrafts.length > 0) {
+    return {
+      drafts: paragraphDrafts,
+      errors: [`已按段落自动拆分 ${paragraphDrafts.length} 个角色,请检查命名和角色定位是否准确。`],
+    }
+  }
+
   return {
     drafts: [],
     errors: ["未识别到可单独保存的角色,请手动选择保存范围或让 AI 按一人一档重新输出。"],