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 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 title = name.trim().length > 0 ? name.trim() : "未命名";
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
- return typeof data.name === "string" && data.name.trim().length > 0 ? data.name.trim() : fallback;
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 outlineName = related === void 0 ? void 0 : outlineNodeLabel(labelData(related), related.name ?? related.id);
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.jsx)("h2", {
1279
- className: "dshNovelTitle",
1280
- children: chapterNodeLabel(data, String(data.name ?? data.title ?? "未命名"))
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
- outlineName !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1366
+ meta !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1283
1367
  className: "dshNovelMeta",
1284
- children: outlineName
1368
+ children: meta
1285
1369
  }),
1286
1370
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1287
1371
  className: "dshNovelProse",
1288
- children: String(data.content ?? "") || "—"
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); }