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.
- package/README.md +2 -1
- package/index.js +50 -10
- 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.
|
|
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
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
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)
|
|
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.
|
|
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",
|