dsh-novel 0.1.0 → 0.1.1
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/CHANGELOG.md +13 -0
- package/lib/client.js +101 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +31 -2
- package/lib/index.js.map +1 -1
- package/lib/types/novel/outline-labels.d.ts +4 -0
- package/package.json +11 -9
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 — 2026-08-18
|
|
4
|
+
|
|
5
|
+
- 大纲与正文身份只收序号 + 短名,界面显示「第N卷/章 + 名称」;写入时剥掉 name 里已有的「第1章 / 第一章」等前缀,避免标题重复。
|
|
6
|
+
- 章节详情标题行左侧为章名,右侧为去空白字数和复制正文。
|
|
7
|
+
- 正文开头若再写一遍章名,展示时隐藏;复制仍是完整原文。关联大纲只显示所属卷,不再重复「第N章」。
|
|
8
|
+
|
|
9
|
+
## 0.1.0 — 2026-08-18
|
|
10
|
+
|
|
11
|
+
- 首个 npm 发布:DeepSeek Harness 标准小说插件,一个工作区目录一本小说。
|
|
12
|
+
- 会话「内容」页可浏览概述、世界、人物、生物、物品、等级、时间线、大纲、章节和技能。
|
|
13
|
+
- `/novel-create` 开书设定;`/novel-outline`、`/novel-chapter` 由用户显式写大纲和正文。
|
package/lib/client.js
CHANGED
|
@@ -79,14 +79,46 @@ window.__ModuleLoader__.load({
|
|
|
79
79
|
if (explicit !== void 0) return explicit;
|
|
80
80
|
return typeof data.parentId === "string" && data.parentId.length > 0 || typeof data.parentName === "string" && data.parentName.length > 0 ? "chapter_segment" : "volume";
|
|
81
81
|
}
|
|
82
|
+
/** Drop a leading 第N卷/章 or 第N卷 so the UI prefix is not duplicated. */
|
|
83
|
+
function stripVolumeChapterPrefix(name) {
|
|
84
|
+
const re = /^第\s*(?:[0-9]+|[零〇一二三四五六七八九十百千两]+)\s*[章节卷]\s*/u;
|
|
85
|
+
let next = name.trim();
|
|
86
|
+
for (let i = 0; i < 3; i++) {
|
|
87
|
+
const stripped = next.replace(re, "").trim();
|
|
88
|
+
if (stripped === next) break;
|
|
89
|
+
next = stripped;
|
|
90
|
+
}
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
82
93
|
function volumeChapterLabel(ordinal, unit, name) {
|
|
83
|
-
const
|
|
94
|
+
const stripped = stripVolumeChapterPrefix(name);
|
|
95
|
+
const title = stripped.length > 0 ? stripped : "未命名";
|
|
84
96
|
const n = asOrdinal(ordinal);
|
|
85
97
|
if (n === void 0) return title;
|
|
86
98
|
return `第${n}${unit} ${title}`;
|
|
87
99
|
}
|
|
88
100
|
function shortName(data, fallback) {
|
|
89
|
-
|
|
101
|
+
const stripped = stripVolumeChapterPrefix(typeof data.name === "string" && data.name.trim().length > 0 ? data.name.trim() : fallback);
|
|
102
|
+
if (stripped.length > 0) return stripped;
|
|
103
|
+
const fallbackStripped = stripVolumeChapterPrefix(fallback);
|
|
104
|
+
return fallbackStripped.length > 0 ? fallbackStripped : fallback;
|
|
105
|
+
}
|
|
106
|
+
/** Character count with whitespace removed, matching chapter review. */
|
|
107
|
+
function countChapterChars(content) {
|
|
108
|
+
if (typeof content !== "string") return 0;
|
|
109
|
+
return content.replace(/\s/gu, "").length;
|
|
110
|
+
}
|
|
111
|
+
/** Drop a first line that repeats the chapter heading (display only). */
|
|
112
|
+
function stripLeadingChapterHeading(content, title) {
|
|
113
|
+
const lines = content.split("\n");
|
|
114
|
+
const first = (lines[0] ?? "").trim().replace(/^#+\s*/u, "");
|
|
115
|
+
if (first.length === 0) return content;
|
|
116
|
+
const a = stripVolumeChapterPrefix(first);
|
|
117
|
+
const b = stripVolumeChapterPrefix(title);
|
|
118
|
+
if (first !== title && a !== b) return content;
|
|
119
|
+
let i = 1;
|
|
120
|
+
while (i < lines.length && (lines[i] ?? "").trim() === "") i++;
|
|
121
|
+
return lines.slice(i).join("\n");
|
|
90
122
|
}
|
|
91
123
|
function outlineNodeLabel(data, fallback) {
|
|
92
124
|
const unit = outlineKindOf(data) === "chapter_segment" ? "章" : "卷";
|
|
@@ -1240,14 +1272,42 @@ window.__ModuleLoader__.load({
|
|
|
1240
1272
|
name: item.name ?? data.name
|
|
1241
1273
|
};
|
|
1242
1274
|
}
|
|
1275
|
+
function CopyIcon() {
|
|
1276
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1277
|
+
width: 16,
|
|
1278
|
+
height: 16,
|
|
1279
|
+
viewBox: "0 0 16 16",
|
|
1280
|
+
fill: "none",
|
|
1281
|
+
"aria-hidden": "true",
|
|
1282
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1283
|
+
x: "5.5",
|
|
1284
|
+
y: "5.5",
|
|
1285
|
+
width: "8",
|
|
1286
|
+
height: "8",
|
|
1287
|
+
rx: "1.2",
|
|
1288
|
+
stroke: "currentColor",
|
|
1289
|
+
strokeWidth: "1.2"
|
|
1290
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1291
|
+
d: "M10.5 5.5V3.7A1.2 1.2 0 0 0 9.3 2.5H3.7A1.2 1.2 0 0 0 2.5 3.7v5.6A1.2 1.2 0 0 0 3.7 10.5H5.5",
|
|
1292
|
+
stroke: "currentColor",
|
|
1293
|
+
strokeWidth: "1.2"
|
|
1294
|
+
})]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1243
1297
|
function ChapterSection({ path }) {
|
|
1244
1298
|
const [items, setItems] = (0, react.useState)([]);
|
|
1245
1299
|
const [outlines, setOutlines] = (0, react.useState)([]);
|
|
1246
1300
|
const [selectedId, setSelectedId] = (0, react.useState)();
|
|
1247
1301
|
const selected = items.find((item) => item.id === selectedId);
|
|
1248
1302
|
const data = viewData(selected);
|
|
1303
|
+
const content = typeof data.content === "string" ? data.content : "";
|
|
1304
|
+
const title = chapterNodeLabel(data, String(data.name ?? data.title ?? "未命名"));
|
|
1305
|
+
const displayed = stripLeadingChapterHeading(content, title);
|
|
1306
|
+
const wordCount = countChapterChars(content);
|
|
1249
1307
|
const related = outlines.find((item) => item.id === data.relatedOutlineId);
|
|
1250
|
-
const
|
|
1308
|
+
const relatedVolume = related === void 0 ? void 0 : outlineKindOf(labelData(related)) === "chapter_segment" ? outlines.find((item) => item.id === dataOf(related).parentId) ?? related : related;
|
|
1309
|
+
const volumeLabel = relatedVolume === void 0 ? void 0 : outlineNodeLabel(labelData(relatedVolume), relatedVolume.name ?? relatedVolume.id);
|
|
1310
|
+
const meta = volumeLabel !== void 0 && volumeLabel !== title ? volumeLabel : void 0;
|
|
1251
1311
|
const reload = (0, react.useCallback)(async () => {
|
|
1252
1312
|
const [rows, outlineRows] = await Promise.all([listEntities(path, "chapter"), listEntities(path, "outline")]);
|
|
1253
1313
|
const sorted = sortByOrdinalName(rows, (item) => dataOf(item).ordinal ?? dataOf(item).chapterNo);
|
|
@@ -1259,6 +1319,10 @@ window.__ModuleLoader__.load({
|
|
|
1259
1319
|
reload();
|
|
1260
1320
|
}, [reload]);
|
|
1261
1321
|
useLiveReload(reload);
|
|
1322
|
+
const copyContent = (0, react.useCallback)(() => {
|
|
1323
|
+
if (content.length === 0) return;
|
|
1324
|
+
navigator.clipboard.writeText(content);
|
|
1325
|
+
}, [content]);
|
|
1262
1326
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntitySidebar, {
|
|
1263
1327
|
items,
|
|
1264
1328
|
selectedId,
|
|
@@ -1275,17 +1339,37 @@ window.__ModuleLoader__.load({
|
|
|
1275
1339
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1276
1340
|
className: "dshNovelViewFormBody",
|
|
1277
1341
|
children: [
|
|
1278
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1279
|
-
className: "
|
|
1280
|
-
children:
|
|
1342
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1343
|
+
className: "dshNovelTitleRow",
|
|
1344
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1345
|
+
className: "dshNovelTitle",
|
|
1346
|
+
children: title
|
|
1347
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1348
|
+
className: "dshNovelViewFormHeadActions",
|
|
1349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1350
|
+
className: "dshNovelWordCount",
|
|
1351
|
+
children: [wordCount, " 字"]
|
|
1352
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
1353
|
+
label: "复制正文",
|
|
1354
|
+
side: "bottom",
|
|
1355
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1356
|
+
type: "button",
|
|
1357
|
+
className: "dshNovelIconBtn",
|
|
1358
|
+
"aria-label": "复制正文",
|
|
1359
|
+
disabled: content.length === 0,
|
|
1360
|
+
onClick: copyContent,
|
|
1361
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyIcon, {})
|
|
1362
|
+
})
|
|
1363
|
+
})]
|
|
1364
|
+
})]
|
|
1281
1365
|
}),
|
|
1282
|
-
|
|
1366
|
+
meta !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1283
1367
|
className: "dshNovelMeta",
|
|
1284
|
-
children:
|
|
1368
|
+
children: meta
|
|
1285
1369
|
}),
|
|
1286
1370
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1287
1371
|
className: "dshNovelProse",
|
|
1288
|
-
children:
|
|
1372
|
+
children: displayed || "—"
|
|
1289
1373
|
})
|
|
1290
1374
|
]
|
|
1291
1375
|
})
|
|
@@ -1501,7 +1585,10 @@ textarea.dshNovelControl { min-height: 88px; resize: vertical; }
|
|
|
1501
1585
|
.dshNovelEntryTitle { font-size: 14px; font-weight: 500; line-height: 22px; }
|
|
1502
1586
|
.dshNovelTier { display: flex; flex-direction: column; gap: 8px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; }
|
|
1503
1587
|
.dshNovelPills { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1504
|
-
.dshNovelTitle { margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; }
|
|
1588
|
+
.dshNovelTitle { margin: 0; font-size: 20px; font-weight: 600; line-height: 28px; min-width: 0; }
|
|
1589
|
+
.dshNovelTitleRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
|
|
1590
|
+
.dshNovelTitleRow .dshNovelTitle { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1591
|
+
.dshNovelTitleRow .dshNovelViewFormHeadActions { padding-bottom: 0; }
|
|
1505
1592
|
.dshNovelMeta { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }
|
|
1506
1593
|
.dshNovelProse { margin: 0; white-space: pre-wrap; font-size: 14px; line-height: 24px; }
|
|
1507
1594
|
.dshNovelReadValue { white-space: pre-wrap; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary, inherit); }
|
|
@@ -1521,6 +1608,10 @@ textarea.dshNovelControl { min-height: 88px; resize: vertical; }
|
|
|
1521
1608
|
.dshNovelViewFormHead { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; padding: 4px 16px 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l2); min-width: 0; }
|
|
1522
1609
|
.dshNovelViewFormHeadTabs { min-width: 0; overflow: hidden; }
|
|
1523
1610
|
.dshNovelViewFormHeadActions { display: flex; flex: none; align-items: center; gap: 8px; margin-left: auto; padding-bottom: 6px; }
|
|
1611
|
+
.dshNovelWordCount { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary, inherit); white-space: nowrap; }
|
|
1612
|
+
.dshNovelIconBtn { appearance: none; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; }
|
|
1613
|
+
.dshNovelIconBtn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary, inherit); }
|
|
1614
|
+
.dshNovelIconBtn:disabled { opacity: 0.4; cursor: default; }
|
|
1524
1615
|
.dshNovelViewFormBody { flex: 1; min-height: 0; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 12px; }
|
|
1525
1616
|
.dshNovelViewField { display: flex; flex-direction: column; gap: 4px; }
|
|
1526
1617
|
.dshNovelViewField label { font-size: 12px; color: var(--dsw-alias-label-secondary, inherit); }
|