dsh-cuigengji-plugin 0.4.3 → 0.4.4

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 (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1 -1
  3. package/cordis.patch.yml +3 -3
  4. package/docs/CHANGELOG.md +7 -0
  5. package/docs/agent-installation.md +19 -12
  6. package/docs/architecture.md +74 -74
  7. package/docs/desktop-migration-20260930.md +26 -26
  8. package/docs/development.md +7 -3
  9. package/docs/getting-started.md +17 -7
  10. package/docs/layout-progress.md +31 -31
  11. package/docs/market-listing.md +5 -3
  12. package/docs/npm-publishing.md +28 -12
  13. package/docs/package-name-migration.md +3 -3
  14. package/docs/planning-board.md +44 -44
  15. package/docs/presets.md +24 -24
  16. package/docs/release-process.md +15 -11
  17. package/docs/tavern-compatibility-audit-20261001.md +109 -109
  18. package/docs/test-report.md +139 -129
  19. package/docs/upgrade.md +3 -1
  20. package/docs/usage-review.md +13 -13
  21. package/lib/client.js +18 -14
  22. package/package.json +18 -14
  23. package/skills/cuigengji-maintenance/SKILL.md +2 -2
  24. package/src/adapters/dsh/prompt.js +36 -36
  25. package/src/adapters/dsh/rpc.js +96 -96
  26. package/src/adapters/dsh/skills.js +17 -17
  27. package/src/adapters/mcp/actions.js +6 -6
  28. package/src/adapters/mcp/client.js +172 -172
  29. package/src/adapters/mcp/server.js +40 -40
  30. package/src/application/bindings/session.js +72 -72
  31. package/src/application/queries/context.js +15 -15
  32. package/src/application/store.js +412 -412
  33. package/src/client/features/chapters/clipboard.ts +27 -27
  34. package/src/client/features/chapters/history.ts +11 -11
  35. package/src/client/features/chapters/index.jsx +118 -118
  36. package/src/client/features/chapters/sync.ts +155 -155
  37. package/src/client/features/chapters/useChapterSync.ts +39 -39
  38. package/src/client/features/context/index.jsx +22 -22
  39. package/src/client/features/memory/index.jsx +73 -73
  40. package/src/client/features/planning/Decorations.tsx +1 -1
  41. package/src/client/features/planning/Pages.tsx +11 -11
  42. package/src/client/features/planning/data.js +16 -16
  43. package/src/client/index.tsx +130 -130
  44. package/src/client/launcher.tsx +78 -78
  45. package/src/client/shared/creation.tsx +34 -34
  46. package/src/client/shared/detail-page.tsx +61 -61
  47. package/src/client/shared/drafts.js +17 -17
  48. package/src/client/shared/evidence.jsx +11 -11
  49. package/src/client/shared/groups.tsx +34 -34
  50. package/src/client/shared/preferences.tsx +104 -104
  51. package/src/client/shared/select-style.ts +25 -25
  52. package/src/client/shared/state.js +95 -95
  53. package/src/client/shared/ui.jsx +19 -19
  54. package/src/client/shared/workspace.tsx +11 -11
  55. package/src/client/style.ts +274 -274
  56. package/src/client/visual.ts +190 -190
  57. package/src/contracts/actions.js +19 -19
  58. package/src/contracts/planning.ts +52 -52
  59. package/src/domain/manuscript.ts +4 -4
  60. package/src/domain/planning/index.ts +113 -113
  61. package/src/domain/planning/model.ts +59 -59
  62. package/src/domain/preset/index.js +89 -89
  63. package/src/index.js +91 -91
  64. package/src/infrastructure/migrations/legacy.js +96 -96
  65. package/src/infrastructure/migrations/tavern.js +78 -78
  66. package/src/infrastructure/persistence/atomic-store.ts +80 -80
  67. package/src/infrastructure/persistence/project-layout.ts +1 -1
@@ -1,40 +1,40 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
- import { MEMORY_ACTIONS, toolName } from './actions.js';
5
-
6
- // This process owns no project files. Its capability is one session-bound token.
7
- const endpoint = process.env.CUIGENGJI_MEMORY_ENDPOINT;
8
- const token = process.env.CUIGENGJI_MEMORY_TOKEN;
9
- if (!endpoint || !token) throw new Error('缺少 cuigengji 宿主连接信息');
10
- const address = new URL(endpoint);
11
- if (address.protocol !== 'http:' || address.hostname !== '127.0.0.1') {
12
- throw new Error('记忆服务必须为本机 loopback 地址');
13
- }
14
-
15
- const server = new Server({ name: 'cuigengji-memory', version: '0.1.0' }, { capabilities: { tools: {} } });
16
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
- tools: MEMORY_ACTIONS.map(action => ({
18
- name: toolName(action),
19
- description: `当前小说记忆操作 ${action}。小说由会话绑定;修改须携带读取到的版本。`,
20
- inputSchema: { type: 'object', additionalProperties: true },
21
- })),
22
- }));
23
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
24
- const action = MEMORY_ACTIONS.find(item => toolName(item) === request.params.name);
25
- if (!action) return { isError: true, content: [{ type: 'text', text: '未知记忆工具' }] };
26
- try {
27
- const response = await fetch(endpoint, {
28
- method: 'POST',
29
- headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
30
- body: JSON.stringify({ action, args: request.params.arguments ?? {} }),
31
- signal: AbortSignal.any([extra.signal, AbortSignal.timeout(30_000)]),
32
- });
33
- const body = await response.json();
34
- if (!response.ok) return { isError: true, content: [{ type: 'text', text: body.error ?? '记忆操作失败' }] };
35
- return { content: [{ type: 'text', text: JSON.stringify(body.result ?? null) }] };
36
- } catch (error) {
37
- return { isError: true, content: [{ type: 'text', text: error.message }] };
38
- }
39
- });
40
- await server.connect(new StdioServerTransport());
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { MEMORY_ACTIONS, toolName } from './actions.js';
5
+
6
+ // This process owns no project files. Its capability is one session-bound token.
7
+ const endpoint = process.env.CUIGENGJI_MEMORY_ENDPOINT;
8
+ const token = process.env.CUIGENGJI_MEMORY_TOKEN;
9
+ if (!endpoint || !token) throw new Error('缺少 cuigengji 宿主连接信息');
10
+ const address = new URL(endpoint);
11
+ if (address.protocol !== 'http:' || address.hostname !== '127.0.0.1') {
12
+ throw new Error('记忆服务必须为本机 loopback 地址');
13
+ }
14
+
15
+ const server = new Server({ name: 'cuigengji-memory', version: '0.1.0' }, { capabilities: { tools: {} } });
16
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
+ tools: MEMORY_ACTIONS.map(action => ({
18
+ name: toolName(action),
19
+ description: `当前小说记忆操作 ${action}。小说由会话绑定;修改须携带读取到的版本。`,
20
+ inputSchema: { type: 'object', additionalProperties: true },
21
+ })),
22
+ }));
23
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
24
+ const action = MEMORY_ACTIONS.find(item => toolName(item) === request.params.name);
25
+ if (!action) return { isError: true, content: [{ type: 'text', text: '未知记忆工具' }] };
26
+ try {
27
+ const response = await fetch(endpoint, {
28
+ method: 'POST',
29
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
30
+ body: JSON.stringify({ action, args: request.params.arguments ?? {} }),
31
+ signal: AbortSignal.any([extra.signal, AbortSignal.timeout(30_000)]),
32
+ });
33
+ const body = await response.json();
34
+ if (!response.ok) return { isError: true, content: [{ type: 'text', text: body.error ?? '记忆操作失败' }] };
35
+ return { content: [{ type: 'text', text: JSON.stringify(body.result ?? null) }] };
36
+ } catch (error) {
37
+ return { isError: true, content: [{ type: 'text', text: error.message }] };
38
+ }
39
+ });
40
+ await server.connect(new StdioServerTransport());
@@ -1,72 +1,72 @@
1
- import { isReadAction } from '../../contracts/actions.js';
2
- import {novelLifecycleActions, recyclesNovel} from '../novels/lifecycle.ts';
3
- import {createHash} from 'node:crypto';
4
- const sessionKey = id => createHash('sha256').update(id || '').digest('hex');
5
-
6
- export function createSessionPolicy({ store, audit, ready = async () => undefined }) {
7
- const active = new Set();
8
- const resolveBinding = async sessionId => {
9
- await ready();
10
- return store.dispatch('binding.get', {}, { kind: 'agent', sessionId });
11
- };
12
-
13
- async function dispatch(action, args = {}, actor = { kind: 'agent' }) {
14
- await ready();
15
- if (action === 'binding.set' && active.has(actor.sessionId)) throw new Error('会话正在运行,请停止后切换小说或章节');
16
- if (novelLifecycleActions.includes(action) && actor.kind !== 'human') throw new Error('整部作品的回收、恢复与永久删除需要作者操作');
17
- if (actor.kind === 'agent') {
18
- const binding = await resolveBinding(actor.sessionId);
19
- // Inspecting an unbound session is valid and lets the agent explain how
20
- // to begin; all novel data operations still require an author binding.
21
- if (action === 'binding.get') return binding;
22
- if (!binding) throw new Error('请先在催更姬面板绑定小说');
23
- if (args.novelId && args.novelId !== binding.novelId) throw new Error('不能访问另一部小说');
24
- args = { ...args, novelId: binding.novelId };
25
-
26
- }
27
- const guard = action === 'binding.set' || recyclesNovel(action, args) ? state => {
28
- if (action === 'binding.set' && active.has(actor.sessionId)) throw new Error('会话正在运行,请停止后切换小说或章节');
29
- if (recyclesNovel(action, args)) {
30
- const novelId = args.novelId || state.bindings[sessionKey(actor.sessionId)]?.novelId;
31
- for (const sessionId of active) if (novelId && state.bindings[sessionKey(sessionId)]?.novelId === novelId) {
32
- throw Object.assign(new Error('这部作品仍有 AI 写作任务在运行,请先停止相关会话,再移入回收站'), {code: 'NOVEL_BUSY'});
33
- }
34
- }
35
- } : undefined;
36
- const result = await store.dispatch(action, args, actor, guard);
37
- if (!isReadAction(action)) await audit(actor.sessionId, {
38
- action, actor: actor.kind, entityId: result?.id, revision: result?.revision, reason: args.reason || '',
39
- });
40
- return result;
41
- }
42
-
43
- function register(ctx, memory) {
44
- const disconnect = sessionId => {
45
- // Status/disposal are emit events; do not leave an unhandled rejection.
46
- void memory.disconnect(sessionId).catch(error => ctx.logger?.warn?.(`cuigengji memory cleanup: ${error.message}`));
47
- };
48
- ctx.on('agent/pre-step', async ({ agent }, next) => {
49
- const sessionId = String(agent.id);
50
- // DSH only emits pre-step for running agents. Record this before any I/O
51
- // so a simultaneous recycle sees the active session inside the lock.
52
- active.add(sessionId);
53
- try {await resolveBinding(sessionId);}
54
- catch (error) {if (error.code !== 'UPGRADE_REQUIRED') throw error;}
55
- return next();
56
- });
57
- ctx.on('agent/status', ({ agent, status }) => {
58
- const sessionId = String(agent.id);
59
- if (status === 'running') active.add(sessionId);
60
- else {
61
- active.delete(sessionId);
62
- disconnect(sessionId);
63
- }
64
- });
65
- ctx.on('agent/disposed', ({ agent }) => {
66
- const sessionId = String(agent.id);
67
- active.delete(sessionId);
68
- disconnect(sessionId);
69
- });
70
- }
71
- return { dispatch, resolveBinding, register };
72
- }
1
+ import { isReadAction } from '../../contracts/actions.js';
2
+ import {novelLifecycleActions, recyclesNovel} from '../novels/lifecycle.ts';
3
+ import {createHash} from 'node:crypto';
4
+ const sessionKey = id => createHash('sha256').update(id || '').digest('hex');
5
+
6
+ export function createSessionPolicy({ store, audit, ready = async () => undefined }) {
7
+ const active = new Set();
8
+ const resolveBinding = async sessionId => {
9
+ await ready();
10
+ return store.dispatch('binding.get', {}, { kind: 'agent', sessionId });
11
+ };
12
+
13
+ async function dispatch(action, args = {}, actor = { kind: 'agent' }) {
14
+ await ready();
15
+ if (action === 'binding.set' && active.has(actor.sessionId)) throw new Error('会话正在运行,请停止后切换小说或章节');
16
+ if (novelLifecycleActions.includes(action) && actor.kind !== 'human') throw new Error('整部作品的回收、恢复与永久删除需要作者操作');
17
+ if (actor.kind === 'agent') {
18
+ const binding = await resolveBinding(actor.sessionId);
19
+ // Inspecting an unbound session is valid and lets the agent explain how
20
+ // to begin; all novel data operations still require an author binding.
21
+ if (action === 'binding.get') return binding;
22
+ if (!binding) throw new Error('请先在催更姬面板绑定小说');
23
+ if (args.novelId && args.novelId !== binding.novelId) throw new Error('不能访问另一部小说');
24
+ args = { ...args, novelId: binding.novelId };
25
+
26
+ }
27
+ const guard = action === 'binding.set' || recyclesNovel(action, args) ? state => {
28
+ if (action === 'binding.set' && active.has(actor.sessionId)) throw new Error('会话正在运行,请停止后切换小说或章节');
29
+ if (recyclesNovel(action, args)) {
30
+ const novelId = args.novelId || state.bindings[sessionKey(actor.sessionId)]?.novelId;
31
+ for (const sessionId of active) if (novelId && state.bindings[sessionKey(sessionId)]?.novelId === novelId) {
32
+ throw Object.assign(new Error('这部作品仍有 AI 写作任务在运行,请先停止相关会话,再移入回收站'), {code: 'NOVEL_BUSY'});
33
+ }
34
+ }
35
+ } : undefined;
36
+ const result = await store.dispatch(action, args, actor, guard);
37
+ if (!isReadAction(action)) await audit(actor.sessionId, {
38
+ action, actor: actor.kind, entityId: result?.id, revision: result?.revision, reason: args.reason || '',
39
+ });
40
+ return result;
41
+ }
42
+
43
+ function register(ctx, memory) {
44
+ const disconnect = sessionId => {
45
+ // Status/disposal are emit events; do not leave an unhandled rejection.
46
+ void memory.disconnect(sessionId).catch(error => ctx.logger?.warn?.(`cuigengji memory cleanup: ${error.message}`));
47
+ };
48
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
49
+ const sessionId = String(agent.id);
50
+ // DSH only emits pre-step for running agents. Record this before any I/O
51
+ // so a simultaneous recycle sees the active session inside the lock.
52
+ active.add(sessionId);
53
+ try {await resolveBinding(sessionId);}
54
+ catch (error) {if (error.code !== 'UPGRADE_REQUIRED') throw error;}
55
+ return next();
56
+ });
57
+ ctx.on('agent/status', ({ agent, status }) => {
58
+ const sessionId = String(agent.id);
59
+ if (status === 'running') active.add(sessionId);
60
+ else {
61
+ active.delete(sessionId);
62
+ disconnect(sessionId);
63
+ }
64
+ });
65
+ ctx.on('agent/disposed', ({ agent }) => {
66
+ const sessionId = String(agent.id);
67
+ active.delete(sessionId);
68
+ disconnect(sessionId);
69
+ });
70
+ }
71
+ return { dispatch, resolveBinding, register };
72
+ }
@@ -1,18 +1,18 @@
1
- // Select complete sections; the plugin does not impose a character budget.
2
- export function selectReference(novel, chapterId) {
3
- const current = chapterId ? novel.chapters[chapterId] : null;
4
- const previous = Object.values(novel.chapters).filter(c=>!c.deleted && c.id!==current?.id && (!current || c.order<current.order))
5
- .sort((a,b)=>b.order-a.order || a.id.localeCompare(b.id)).slice(0,2).reverse();
6
- const items=[];
7
- const add=(kind,entity)=>{
8
- if (!entity?.content || entity.deleted) return;
9
- items.push({kind,id:entity.id,revision:entity.revision,title:entity.title||'情节规划',content:entity.content,truncated:false});
10
- };
11
- for(const chapter of previous)add('previous_chapter',chapter);
12
- add('current_chapter',current);
13
- return {items,omitted:[],usedChars:items.reduce((sum,i)=>sum+i.content.length,0)};
14
- }
15
-
1
+ // Select complete sections; the plugin does not impose a character budget.
2
+ export function selectReference(novel, chapterId) {
3
+ const current = chapterId ? novel.chapters[chapterId] : null;
4
+ const previous = Object.values(novel.chapters).filter(c=>!c.deleted && c.id!==current?.id && (!current || c.order<current.order))
5
+ .sort((a,b)=>b.order-a.order || a.id.localeCompare(b.id)).slice(0,2).reverse();
6
+ const items=[];
7
+ const add=(kind,entity)=>{
8
+ if (!entity?.content || entity.deleted) return;
9
+ items.push({kind,id:entity.id,revision:entity.revision,title:entity.title||'情节规划',content:entity.content,truncated:false});
10
+ };
11
+ for(const chapter of previous)add('previous_chapter',chapter);
12
+ add('current_chapter',current);
13
+ return {items,omitted:[],usedChars:items.reduce((sum,i)=>sum+i.content.length,0)};
14
+ }
15
+
16
16
  export function renderReference(material) {
17
17
  const names={previous_chapter:'前文章节',current_chapter:'当前参考章节',memory:'按需读取的设定'};
18
18
  const sections=material.items.map(item=>`## ${names[item.kind]||'参考资料'}:${item.title}\n[ID: ${item.id} | 版本: ${item.revision}${item.truncated?` | 已截断,仅含${item.position||'部分'}片段,可用工具读取完整内容`:''}]\n\n${item.content}`);