dsh-novel-generator 0.1.12 → 0.1.13

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.js +50 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,8 @@ dsh plugin --profile web add dsh-novel-generator
38
38
  - **物品 / 事件**:快照里除了世界观、角色、势力、关系,还有物品(名称 + 下落/用途)和事件(故事天数 + 标题 + 一句话),事件按时间从新到旧排列。
39
39
  - **生成依据(统一规则)**:先找本章之前最近的那份世界快照。若「本章起始日 − 快照落点 > 当前间隔」,判定快照过旧,直接拒绝并提示先去时间长河更新快照;否则自动推演用「最近快照 + 快照落点之后、本章之前各章梗概」,写正文在此基础上再加一段「待生成的本章梗概」,并在提示里区分已发生与待生成两部分。
40
40
 
41
- ## 数据与接口
41
+ > 侧栏分组:DSH 的分组按目录归属,一个分组只收同目录下的会话,所以不同工作目录的作品会各有一个「小说工坊 · 目录名」分组;空分组与临时目录下的残留分组会被自动清理。
42
+
42
43
 
43
44
 
44
45
  - **索引文件**:`<DSH_HOME>/novel-studio/projects/<项目id>/project.json`(`DSH_HOME` 缺省为 `~/.dsh`,本机即 `C:\Users\Admin\.dsh\novel-studio`)。时间线、大纲、快照字段都记在这里;`dataDir` 配置可改这块的位置。
package/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { randomUUID } from 'node:crypto';
14
14
  import { mkdir, readFile, readdir, rename, rm, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
15
- import { homedir } from 'node:os';
15
+ import { homedir, tmpdir } from 'node:os';
16
16
  import { basename, join } from 'node:path';
17
17
 
18
18
  export const name = 'dsh-novel-generator';
@@ -859,7 +859,7 @@ async function rebindSession(ctx, root, project) {
859
859
  draft.sessionId = sessionId;
860
860
  });
861
861
  try {
862
- await ensureWorkspace(ctx, project);
862
+ await ensureWorkspace(ctx, root, project);
863
863
  } catch {
864
864
  // Grouping is cosmetic; the session works either way.
865
865
  }
@@ -1215,9 +1215,11 @@ async function bootstrap(ctx, root) {
1215
1215
  const projects = await listProjects(root);
1216
1216
  // Repair projects created before sessions were placed in the sidebar group,
1217
1217
  // and stamp works made before folders carried an ownership mark.
1218
+ const workspaceRegistry = ctx.get('workspaceRegistry');
1219
+ if (workspaceRegistry) tidyWorkspaceGroups(workspaceRegistry).catch(() => {});
1218
1220
  for (const project of projects) {
1219
1221
  try {
1220
- await ensureWorkspace(ctx, project);
1222
+ await ensureWorkspace(ctx, root, project);
1221
1223
  } catch {
1222
1224
  // A missing directory or an older harness simply leaves the session as is.
1223
1225
  }
@@ -1239,7 +1241,7 @@ async function bootstrap(ctx, root) {
1239
1241
  }
1240
1242
  }
1241
1243
  return {
1242
- revision: '2026-10-20.35',
1244
+ revision: '2026-10-20.39',
1243
1245
  projects,
1244
1246
  models: {
1245
1247
  default: modelCatalog.default || null,
@@ -1322,7 +1324,7 @@ async function createProject(ctx, root, body) {
1322
1324
  // Put the session in a sidebar workspace instead of leaving it ungrouped.
1323
1325
  let workspace = null;
1324
1326
  try {
1325
- workspace = await ensureWorkspace(ctx, project);
1327
+ workspace = await ensureWorkspace(ctx, root, project);
1326
1328
  } catch (error) {
1327
1329
  const message = `会话已创建,但没能放进「${WORKSPACE_TITLE}」分组:${error instanceof Error ? error.message : String(error)}`;
1328
1330
  warning = warning === null ? message : `${warning} ${message}`;
@@ -1489,14 +1491,18 @@ const WORKSPACE_TITLE = '小说工坊';
1489
1491
  const ensuredWorkspaces = new Set();
1490
1492
 
1491
1493
  /**
1492
- * Put a project's session under a sidebar workspace instead of 未分组: reuse the
1493
- * workspace that owns the project's directory, otherwise create one titled
1494
- * 小说工坊 for it. Idempotent, so bootstrap can repair older projects too.
1494
+ * Put a project's session under a sidebar workspace titled 小说工坊.
1495
+ *
1496
+ * DSH groups sessions by directory: a workspace owns one canonical path, and a
1497
+ * session only shows under the workspace matching its cwd. Works in different
1498
+ * directories therefore need different groups, so each group is titled with its
1499
+ * directory to keep them apart.
1495
1500
  */
1496
- async function ensureWorkspace(ctx, project) {
1501
+ async function ensureWorkspace(ctx, root, project) {
1497
1502
  const registry = ctx.get('workspaceRegistry');
1498
1503
  if (!registry || !project || !project.sessionId || !project.cwd) return null;
1499
1504
  if (ensuredWorkspaces.has(project.projectId)) return null;
1505
+
1500
1506
  const attached = registry.list()
1501
1507
  .some((workspace) => Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(project.sessionId));
1502
1508
  if (attached) {
@@ -1509,12 +1515,46 @@ async function ensureWorkspace(ctx, project) {
1509
1515
  } catch {
1510
1516
  workspace = undefined;
1511
1517
  }
1512
- if (!workspace) workspace = await registry.create(project.cwd, WORKSPACE_TITLE);
1518
+ if (!workspace) {
1519
+ const label = project.cwd.split(/[\\/]/).filter(Boolean).pop() || '根目录';
1520
+ workspace = await registry.create(project.cwd, `${WORKSPACE_TITLE} · ${label}`);
1521
+ }
1513
1522
  await workspace.attachSession(project.sessionId);
1514
1523
  ensuredWorkspaces.add(project.projectId);
1515
1524
  return workspace;
1516
1525
  }
1517
1526
 
1527
+ /** Remove empty groups this plugin leaves behind; never touches a used one. */
1528
+ async function tidyWorkspaceGroups(registry) {
1529
+ for (const workspace of registry.list()) {
1530
+ if (!workspace.title.startsWith(WORKSPACE_TITLE)) continue;
1531
+ const sessions = Array.isArray(workspace.sessionIds) ? workspace.sessionIds : [];
1532
+ if (sessions.length === 0) {
1533
+ try {
1534
+ await registry.delete(workspace.id);
1535
+ } catch {
1536
+ // Leaving one empty group is harmless.
1537
+ }
1538
+ continue;
1539
+ }
1540
+ // A group rooted in the OS temp directory can only be a leftover from a
1541
+ // throwaway test work: archive what it holds, then drop the group.
1542
+ if (!workspace.path.toLowerCase().startsWith(tmpdir().toLowerCase())) continue;
1543
+ for (const sessionId of sessions) {
1544
+ try {
1545
+ await registry.archiveSession(sessionId, { stopActivity: true });
1546
+ } catch {
1547
+ // An active session stays; the group stays with it.
1548
+ }
1549
+ }
1550
+ try {
1551
+ await registry.delete(workspace.id);
1552
+ } catch {
1553
+ // The group stays visible if it could not be removed.
1554
+ }
1555
+ }
1556
+ }
1557
+
1518
1558
  /**
1519
1559
  * The grid point a new chapter brings the story to, or null when nothing new is
1520
1560
  * due. A chapter spanning several points targets the last point it covers; the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Novel studio for DeepSeek Harness: a draggable story timeline, world snapshots, chapter outlines, and chapters written by the work's own agent.",
6
6
  "license": "MIT",