@waterwx/dsh-novel-forge 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/lib/client.js +1256 -294
- package/lib/client.js.map +1 -1
- package/lib/index.js +486 -2
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +4 -0
- package/lib/types/client/locales.d.ts +42 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/engine.d.ts +6 -0
- package/lib/types/protocol.d.ts +69 -0
- package/package.json +1 -1
- package/src/client/api.ts +10 -0
- package/src/client/locales.ts +84 -0
- package/src/client/panel/AssetsTab.tsx +7 -7
- package/src/client/panel/NovelPanel.tsx +505 -68
- package/src/client/panel/panel.module.css +398 -57
- package/src/engine.ts +88 -0
- package/src/protocol.ts +75 -0
- package/src/routes.ts +128 -0
package/lib/index.js
CHANGED
|
@@ -63,6 +63,10 @@ const NOVEL_API = {
|
|
|
63
63
|
world: "/api/dsh-novel-forge/world",
|
|
64
64
|
/** 封面:GET 读取(dataUrl)/ POST 上传或移除。 */
|
|
65
65
|
cover: "/api/dsh-novel-forge/blurb/cover",
|
|
66
|
+
/** 剧情线管理:增删改 + 关联章节。 */
|
|
67
|
+
plotlines: "/api/dsh-novel-forge/plotlines",
|
|
68
|
+
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
69
|
+
sensitiveCheck: "/api/dsh-novel-forge/sensitive-check",
|
|
66
70
|
config: "/api/dsh-novel-forge/config",
|
|
67
71
|
openFolder: "/api/dsh-novel-forge/open-folder"
|
|
68
72
|
};
|
|
@@ -769,6 +773,7 @@ function loadProject(outputDir) {
|
|
|
769
773
|
if (!Array.isArray(raw.assets.auxiliaryProgressions)) raw.assets.auxiliaryProgressions = [];
|
|
770
774
|
if (!Array.isArray(raw.assets.styleAssets)) raw.assets.styleAssets = [];
|
|
771
775
|
if (!Array.isArray(raw.facts)) raw.facts = [];
|
|
776
|
+
if (!Array.isArray(raw.plotlines)) raw.plotlines = [];
|
|
772
777
|
return raw;
|
|
773
778
|
} catch {
|
|
774
779
|
return;
|
|
@@ -779,6 +784,354 @@ function saveProject(outputDir, project) {
|
|
|
779
784
|
mkdirSync(outputDir, { recursive: true });
|
|
780
785
|
writeFileSync(join(outputDir, PROJECT_FILE), JSON.stringify(project, null, 2), "utf8");
|
|
781
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* 内置违禁词库(网文平台常见审查类别)。只做硬匹配提示,不代替人工判断。
|
|
789
|
+
* 词语刻意保持常见写法;作者可自行判断是否修改。
|
|
790
|
+
*/
|
|
791
|
+
const SENSITIVE_WORDS = [
|
|
792
|
+
{
|
|
793
|
+
word: "共匪",
|
|
794
|
+
category: "政治"
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
word: "独裁",
|
|
798
|
+
category: "政治"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
word: "法轮",
|
|
802
|
+
category: "政治"
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
word: "六四",
|
|
806
|
+
category: "政治"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
word: "天安门事件",
|
|
810
|
+
category: "政治"
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
word: "翻墙",
|
|
814
|
+
category: "政治"
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
word: "政治敏感",
|
|
818
|
+
category: "政治"
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
word: "乳沟",
|
|
822
|
+
category: "擦边"
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
word: "酥胸",
|
|
826
|
+
category: "擦边"
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
word: "淫荡",
|
|
830
|
+
category: "擦边"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
word: "做爱",
|
|
834
|
+
category: "擦边"
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
word: "上床",
|
|
838
|
+
category: "擦边"
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
word: "裸体",
|
|
842
|
+
category: "擦边"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
word: "一丝不挂",
|
|
846
|
+
category: "擦边"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
word: "胴体",
|
|
850
|
+
category: "擦边"
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
word: "春药",
|
|
854
|
+
category: "擦边"
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
word: "催情",
|
|
858
|
+
category: "擦边"
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
word: "迷奸",
|
|
862
|
+
category: "擦边"
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
word: "强暴",
|
|
866
|
+
category: "擦边"
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
word: "轮奸",
|
|
870
|
+
category: "擦边"
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
word: "援交",
|
|
874
|
+
category: "擦边"
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
word: "嫖娼",
|
|
878
|
+
category: "擦边"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
word: "卖淫",
|
|
882
|
+
category: "擦边"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
word: "色情",
|
|
886
|
+
category: "擦边"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
word: "情色",
|
|
890
|
+
category: "擦边"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
word: "撸管",
|
|
894
|
+
category: "擦边"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
word: "自慰",
|
|
898
|
+
category: "擦边"
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
word: "口交",
|
|
902
|
+
category: "擦边"
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
word: "打炮",
|
|
906
|
+
category: "擦边"
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
word: "约炮",
|
|
910
|
+
category: "擦边"
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
word: "一夜情",
|
|
914
|
+
category: "擦边"
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
word: "碎尸",
|
|
918
|
+
category: "暴力"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
word: "分尸",
|
|
922
|
+
category: "暴力"
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
word: "凌迟",
|
|
926
|
+
category: "暴力"
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
word: "剥皮",
|
|
930
|
+
category: "暴力"
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
word: "开膛",
|
|
934
|
+
category: "暴力"
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
word: "剖腹",
|
|
938
|
+
category: "暴力"
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
word: "挖心",
|
|
942
|
+
category: "暴力"
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
word: "虐杀",
|
|
946
|
+
category: "暴力"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
word: "凌辱",
|
|
950
|
+
category: "暴力"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
word: "血腥",
|
|
954
|
+
category: "暴力"
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
word: "大屠杀",
|
|
958
|
+
category: "暴力"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
word: "灭门",
|
|
962
|
+
category: "暴力"
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
word: "满门抄斩",
|
|
966
|
+
category: "暴力"
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
word: "腰斩",
|
|
970
|
+
category: "暴力"
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
word: "活埋",
|
|
974
|
+
category: "暴力"
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
word: "点天灯",
|
|
978
|
+
category: "暴力"
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
word: "傻逼",
|
|
982
|
+
category: "辱骂"
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
word: "傻B",
|
|
986
|
+
category: "辱骂"
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
word: "草泥马",
|
|
990
|
+
category: "辱骂"
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
word: "妈的",
|
|
994
|
+
category: "辱骂"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
word: "尼玛",
|
|
998
|
+
category: "辱骂"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
word: "去死",
|
|
1002
|
+
category: "辱骂"
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
word: "废物",
|
|
1006
|
+
category: "辱骂"
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
word: "垃圾",
|
|
1010
|
+
category: "辱骂"
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
word: "人渣",
|
|
1014
|
+
category: "辱骂"
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
word: "贱人",
|
|
1018
|
+
category: "辱骂"
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
word: "婊子",
|
|
1022
|
+
category: "辱骂"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
word: "狗日的",
|
|
1026
|
+
category: "辱骂"
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
word: "加微信",
|
|
1030
|
+
category: "广告"
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
word: "加QQ",
|
|
1034
|
+
category: "广告"
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
word: "微信公众号",
|
|
1038
|
+
category: "广告"
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
word: "淘宝",
|
|
1042
|
+
category: "广告"
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
word: "拼多多",
|
|
1046
|
+
category: "广告"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
word: "刷单",
|
|
1050
|
+
category: "广告"
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
word: "充值返利",
|
|
1054
|
+
category: "广告"
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
word: "扫码领",
|
|
1058
|
+
category: "广告"
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
word: "加群领",
|
|
1062
|
+
category: "广告"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
word: "vx",
|
|
1066
|
+
category: "广告"
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
word: "扣扣",
|
|
1070
|
+
category: "广告"
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
word: "赌博",
|
|
1074
|
+
category: "其他"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
word: "赌场",
|
|
1078
|
+
category: "其他"
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
word: "毒品",
|
|
1082
|
+
category: "其他"
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
word: "冰毒",
|
|
1086
|
+
category: "其他"
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
word: "摇头丸",
|
|
1090
|
+
category: "其他"
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
word: "自杀方法",
|
|
1094
|
+
category: "其他"
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
word: "邪教",
|
|
1098
|
+
category: "其他"
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
word: "传销",
|
|
1102
|
+
category: "其他"
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
word: "军火",
|
|
1106
|
+
category: "其他"
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
word: "枪支",
|
|
1110
|
+
category: "其他"
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
word: "管制刀具",
|
|
1114
|
+
category: "其他"
|
|
1115
|
+
}
|
|
1116
|
+
];
|
|
1117
|
+
/** 对一段文本做违禁词硬匹配,返回命中(词/类别/次数)。 */
|
|
1118
|
+
function checkSensitiveText(text) {
|
|
1119
|
+
const hits = [];
|
|
1120
|
+
for (const entry of SENSITIVE_WORDS) {
|
|
1121
|
+
let count = 0;
|
|
1122
|
+
let idx = text.indexOf(entry.word);
|
|
1123
|
+
while (idx !== -1) {
|
|
1124
|
+
count++;
|
|
1125
|
+
idx = text.indexOf(entry.word, idx + entry.word.length);
|
|
1126
|
+
}
|
|
1127
|
+
if (count > 0) hits.push({
|
|
1128
|
+
word: entry.word,
|
|
1129
|
+
category: entry.category,
|
|
1130
|
+
count
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return hits;
|
|
1134
|
+
}
|
|
782
1135
|
/** List generated chapter files in the output dir (sorted). */
|
|
783
1136
|
function listChapterFiles(outputDir) {
|
|
784
1137
|
if (!existsSync(outputDir)) return [];
|
|
@@ -971,7 +1324,8 @@ async function extractBible(ctx, config, outline) {
|
|
|
971
1324
|
].includes(entry.role) ? entry.role : "other",
|
|
972
1325
|
traits: strArray(entry.traits),
|
|
973
1326
|
goals: typeof entry.goals === "string" ? entry.goals : "",
|
|
974
|
-
relations: typeof entry.relations === "string" ? entry.relations : ""
|
|
1327
|
+
relations: typeof entry.relations === "string" ? entry.relations : "",
|
|
1328
|
+
knowledge: strArray(entry.knowledge)
|
|
975
1329
|
})).filter((card) => card.name !== "") : [];
|
|
976
1330
|
const bible = {
|
|
977
1331
|
genre: typeof raw.genre === "string" ? raw.genre : "",
|
|
@@ -1067,6 +1421,7 @@ function writeSystemPrompt(project) {
|
|
|
1067
1421
|
other: "其他"
|
|
1068
1422
|
}[card.role];
|
|
1069
1423
|
sections.push(`- ${card.name}(${roleName}):${card.traits.join("、")}${card.goals !== "" ? `;目标:${card.goals}` : ""}${card.relations !== "" ? `;关系:${card.relations}` : ""}`);
|
|
1424
|
+
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) sections.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
1070
1425
|
}
|
|
1071
1426
|
}
|
|
1072
1427
|
if (bible.redLines.length > 0) sections.push("写作红线(违反即失败):\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
@@ -1085,6 +1440,17 @@ function writeSystemPrompt(project) {
|
|
|
1085
1440
|
sections.push("==================== 活跃伏笔(近期需推进或回收的线索) ====================");
|
|
1086
1441
|
for (const f of active) sections.push(`- [${f.status === "planted" ? "已埋设" : "推进中"}] ${f.description}${f.targetChapter !== void 0 ? `(预计 ${f.targetChapter} 章回收)` : ""}`);
|
|
1087
1442
|
}
|
|
1443
|
+
const lines = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused");
|
|
1444
|
+
if (lines.length > 0) {
|
|
1445
|
+
const kindName = {
|
|
1446
|
+
main: "主线",
|
|
1447
|
+
branch: "支线",
|
|
1448
|
+
character: "人物线",
|
|
1449
|
+
mystery: "悬念线"
|
|
1450
|
+
};
|
|
1451
|
+
sections.push("==================== 剧情线(本章应推进至少一条活跃线) ====================");
|
|
1452
|
+
for (const l of lines) sections.push(`- [${kindName[l.kind]}${l.status === "paused" ? "·暂停中" : ""}] ${l.name}:${l.goal}${l.progress !== "" ? `(当前进度:${l.progress})` : ""}`);
|
|
1453
|
+
}
|
|
1088
1454
|
sections.push("");
|
|
1089
1455
|
sections.push("写作硬性要求:");
|
|
1090
1456
|
sections.push("1. 每章 3000-4000 字(按中文字符计),只输出章节正文,不要输出标题、章回名、作者的话或任何 Markdown 标记。");
|
|
@@ -1187,7 +1553,10 @@ function reviewSystemPrompt(project) {
|
|
|
1187
1553
|
if (bible.worldRules.length > 0) sections.push("世界规则:\n" + bible.worldRules.map((r) => `- ${r}`).join("\n"));
|
|
1188
1554
|
if (bible.characters.length > 0) {
|
|
1189
1555
|
sections.push("角色卡:");
|
|
1190
|
-
for (const card of bible.characters)
|
|
1556
|
+
for (const card of bible.characters) {
|
|
1557
|
+
sections.push(`- ${card.name}(${card.role}):${card.traits.join("、")}`);
|
|
1558
|
+
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) sections.push(` 该角色知道:${card.knowledge.join(";")}(未列出的信息该角色不知道)`);
|
|
1559
|
+
}
|
|
1191
1560
|
}
|
|
1192
1561
|
if (bible.redLines.length > 0) sections.push("红线:\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
1193
1562
|
}
|
|
@@ -3867,6 +4236,19 @@ function makeRoutes(deps) {
|
|
|
3867
4236
|
if (Array.isArray(body?.worldRules)) project.bible.worldRules = body.worldRules.filter((r) => r.trim() !== "");
|
|
3868
4237
|
if (Array.isArray(body?.redLines)) project.bible.redLines = body.redLines.filter((r) => r.trim() !== "");
|
|
3869
4238
|
if (Array.isArray(body?.style)) project.bible.style = body.style.filter((r) => r.trim() !== "");
|
|
4239
|
+
if (Array.isArray(body?.characters)) project.bible.characters = body.characters.filter((c) => c !== void 0 && c !== null && typeof c.name === "string" && c.name.trim() !== "").map((c) => ({
|
|
4240
|
+
name: c.name.trim(),
|
|
4241
|
+
role: [
|
|
4242
|
+
"protagonist",
|
|
4243
|
+
"supporting",
|
|
4244
|
+
"antagonist",
|
|
4245
|
+
"other"
|
|
4246
|
+
].includes(c.role) ? c.role : "other",
|
|
4247
|
+
traits: Array.isArray(c.traits) ? c.traits.filter((t) => typeof t === "string" && t.trim() !== "") : [],
|
|
4248
|
+
goals: typeof c.goals === "string" ? c.goals : "",
|
|
4249
|
+
relations: typeof c.relations === "string" ? c.relations : "",
|
|
4250
|
+
knowledge: Array.isArray(c.knowledge) ? c.knowledge.filter((k) => typeof k === "string" && k.trim() !== "") : void 0
|
|
4251
|
+
}));
|
|
3870
4252
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3871
4253
|
saveProject(config.outputDir, project);
|
|
3872
4254
|
writeJson(res, 200, { bible: project.bible });
|
|
@@ -4029,6 +4411,108 @@ function makeRoutes(deps) {
|
|
|
4029
4411
|
writeJson(res, 200, { bookName: project.bookName });
|
|
4030
4412
|
}
|
|
4031
4413
|
},
|
|
4414
|
+
{
|
|
4415
|
+
kind: "exact",
|
|
4416
|
+
path: NOVEL_API.plotlines,
|
|
4417
|
+
handler: async (req, res) => {
|
|
4418
|
+
if (!guard(req, res, "POST")) return;
|
|
4419
|
+
const config = getConfig();
|
|
4420
|
+
const project = requireProject(res);
|
|
4421
|
+
if (project === void 0) return;
|
|
4422
|
+
const body = await readJsonBody(req);
|
|
4423
|
+
if (project.plotlines === void 0) project.plotlines = [];
|
|
4424
|
+
const op = body?.op;
|
|
4425
|
+
if (op === "add" && body?.line !== void 0) {
|
|
4426
|
+
const line = body.line;
|
|
4427
|
+
project.plotlines.push({
|
|
4428
|
+
id: line.id !== "" ? line.id : `pl-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
4429
|
+
name: line.name.slice(0, 40),
|
|
4430
|
+
kind: line.kind,
|
|
4431
|
+
goal: line.goal.slice(0, 300),
|
|
4432
|
+
progress: line.progress.slice(0, 300),
|
|
4433
|
+
status: line.status,
|
|
4434
|
+
chapters: Array.isArray(line.chapters) ? line.chapters.filter((n) => typeof n === "number") : [],
|
|
4435
|
+
createdAt: line.createdAt !== "" ? line.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
4436
|
+
});
|
|
4437
|
+
} else if (op === "update" && body?.line !== void 0 && body.line.id !== "") {
|
|
4438
|
+
const idx = project.plotlines.findIndex((l) => l.id === body.line.id);
|
|
4439
|
+
if (idx !== -1) {
|
|
4440
|
+
const line = body.line;
|
|
4441
|
+
project.plotlines[idx] = {
|
|
4442
|
+
...project.plotlines[idx],
|
|
4443
|
+
name: line.name.slice(0, 40),
|
|
4444
|
+
kind: line.kind,
|
|
4445
|
+
goal: line.goal.slice(0, 300),
|
|
4446
|
+
progress: line.progress.slice(0, 300),
|
|
4447
|
+
status: line.status
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
} else if (op === "remove" && body?.id !== void 0) project.plotlines = project.plotlines.filter((l) => l.id !== body.id);
|
|
4451
|
+
else if (op === "link" && body?.id !== void 0 && typeof body.chapterNo === "number" && body.chapterNo > 0) {
|
|
4452
|
+
const line = project.plotlines.find((l) => l.id === body.id);
|
|
4453
|
+
if (line !== void 0 && !line.chapters.includes(body.chapterNo)) {
|
|
4454
|
+
line.chapters.push(body.chapterNo);
|
|
4455
|
+
if (line.status === "active" && line.progress === "") line.progress = `推进至第 ${body.chapterNo} 章`;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4459
|
+
saveProject(config.outputDir, project);
|
|
4460
|
+
writeJson(res, 200, { plotlines: project.plotlines });
|
|
4461
|
+
}
|
|
4462
|
+
},
|
|
4463
|
+
{
|
|
4464
|
+
kind: "exact",
|
|
4465
|
+
path: NOVEL_API.sensitiveCheck,
|
|
4466
|
+
handler: async (req, res) => {
|
|
4467
|
+
if (!guard(req, res, "POST")) return;
|
|
4468
|
+
const config = getConfig();
|
|
4469
|
+
const project = requireProject(res);
|
|
4470
|
+
if (project === void 0) return;
|
|
4471
|
+
const body = await readJsonBody(req);
|
|
4472
|
+
const hits = [];
|
|
4473
|
+
let scanned = 0;
|
|
4474
|
+
if (body?.text !== void 0) for (const hit of checkSensitiveText(body.text)) hits.push({
|
|
4475
|
+
chapterNo: 0,
|
|
4476
|
+
word: hit.word,
|
|
4477
|
+
category: hit.category,
|
|
4478
|
+
count: hit.count
|
|
4479
|
+
});
|
|
4480
|
+
else if (typeof body?.chapterNo === "number") {
|
|
4481
|
+
const chapter = project.chapters.find((c) => c.no === body.chapterNo);
|
|
4482
|
+
if (chapter !== void 0) {
|
|
4483
|
+
const text = readChapterFile(config.outputDir, chapter);
|
|
4484
|
+
if (text !== void 0) {
|
|
4485
|
+
scanned = 1;
|
|
4486
|
+
for (const hit of checkSensitiveText(text)) hits.push({
|
|
4487
|
+
chapterNo: chapter.no,
|
|
4488
|
+
word: hit.word,
|
|
4489
|
+
category: hit.category,
|
|
4490
|
+
count: hit.count
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
} else if (body?.all === true) for (const chapter of project.chapters) {
|
|
4495
|
+
if (chapter.status === "pending" || chapter.status === "generating") continue;
|
|
4496
|
+
const text = readChapterFile(config.outputDir, chapter);
|
|
4497
|
+
if (text === void 0) continue;
|
|
4498
|
+
scanned++;
|
|
4499
|
+
for (const hit of checkSensitiveText(text)) hits.push({
|
|
4500
|
+
chapterNo: chapter.no,
|
|
4501
|
+
word: hit.word,
|
|
4502
|
+
category: hit.category,
|
|
4503
|
+
count: hit.count
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
else {
|
|
4507
|
+
writeJson(res, 400, { error: "请提供 chapterNo / text / all 之一" });
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
writeJson(res, 200, {
|
|
4511
|
+
hits,
|
|
4512
|
+
scannedChapters: scanned
|
|
4513
|
+
});
|
|
4514
|
+
}
|
|
4515
|
+
},
|
|
4032
4516
|
{
|
|
4033
4517
|
kind: "exact",
|
|
4034
4518
|
path: NOVEL_API.config,
|