dsh-cuigengji-plugin 0.4.2 → 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.
- package/LICENSE +21 -21
- package/README.md +1 -1
- package/cordis.patch.yml +3 -3
- package/docs/CHANGELOG.md +15 -0
- package/docs/agent-installation.md +19 -12
- package/docs/architecture.md +74 -74
- package/docs/desktop-migration-20260930.md +26 -26
- package/docs/development.md +7 -3
- package/docs/getting-started.md +17 -7
- package/docs/layout-progress.md +31 -31
- package/docs/market-listing.md +5 -3
- package/docs/npm-publishing.md +35 -11
- package/docs/package-name-migration.md +3 -3
- package/docs/planning-board.md +44 -44
- package/docs/presets.md +24 -24
- package/docs/release-process.md +45 -10
- package/docs/tavern-compatibility-audit-20261001.md +109 -109
- package/docs/test-report.md +139 -129
- package/docs/upgrade.md +3 -1
- package/docs/usage-review.md +13 -13
- package/lib/client.js +19 -14
- package/package.json +19 -14
- package/skills/cuigengji-maintenance/SKILL.md +2 -2
- package/src/adapters/dsh/prompt.js +36 -36
- package/src/adapters/dsh/rpc.js +96 -96
- package/src/adapters/dsh/skills.js +17 -17
- package/src/adapters/mcp/actions.js +6 -6
- package/src/adapters/mcp/client.js +172 -172
- package/src/adapters/mcp/server.js +40 -40
- package/src/application/bindings/session.js +72 -72
- package/src/application/queries/context.js +15 -15
- package/src/application/store.js +412 -412
- package/src/client/features/chapters/clipboard.ts +27 -27
- package/src/client/features/chapters/history.ts +11 -11
- package/src/client/features/chapters/index.jsx +118 -118
- package/src/client/features/chapters/sync.ts +155 -155
- package/src/client/features/chapters/useChapterSync.ts +39 -39
- package/src/client/features/context/index.jsx +22 -22
- package/src/client/features/memory/index.jsx +73 -73
- package/src/client/features/planning/Decorations.tsx +1 -1
- package/src/client/features/planning/Pages.tsx +11 -11
- package/src/client/features/planning/data.js +16 -16
- package/src/client/index.tsx +130 -130
- package/src/client/launcher.tsx +78 -78
- package/src/client/shared/creation.tsx +34 -34
- package/src/client/shared/detail-page.tsx +61 -61
- package/src/client/shared/drafts.js +17 -17
- package/src/client/shared/evidence.jsx +11 -11
- package/src/client/shared/groups.tsx +34 -34
- package/src/client/shared/preferences.tsx +104 -104
- package/src/client/shared/select-style.ts +25 -25
- package/src/client/shared/state.js +95 -95
- package/src/client/shared/ui.jsx +19 -19
- package/src/client/shared/workspace.tsx +11 -11
- package/src/client/style.ts +274 -274
- package/src/client/visual.ts +190 -190
- package/src/contracts/actions.js +19 -19
- package/src/contracts/planning.ts +52 -52
- package/src/domain/manuscript.ts +4 -4
- package/src/domain/planning/index.ts +113 -113
- package/src/domain/planning/model.ts +59 -59
- package/src/domain/preset/index.js +89 -89
- package/src/index.js +91 -91
- package/src/infrastructure/migrations/legacy.js +96 -96
- package/src/infrastructure/migrations/tavern.js +78 -78
- package/src/infrastructure/persistence/atomic-store.ts +80 -80
- package/src/infrastructure/persistence/project-layout.ts +1 -1
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import type { Board, Collection, Change, Decoration, PlanningEntity, PlanningNode, PlanningNovel, PlanningArgs, Operation } from '../../contracts/planning.ts';
|
|
1
|
+
import type { Board, Collection, Change, Decoration, PlanningEntity, PlanningNode, PlanningNovel, PlanningArgs, Operation } from '../../contracts/planning.ts';
|
|
2
2
|
import { ensurePlanning, validatePlanning } from './model.ts';
|
|
3
3
|
import { normalizePlanningOperations } from './operations.ts';
|
|
4
|
-
export { ensurePlanning, validatePlanning } from './model.ts';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
-
const fail=(code:string,message:string):never=>{throw Object.assign(new Error(message),{code});};
|
|
7
|
-
const copy=<T>(v:T):T=>structuredClone(v);
|
|
8
|
-
const own=(map:object,id:string)=>Object.prototype.hasOwnProperty.call(map,id);
|
|
9
|
-
const entity=<T>(map:Record<string,T>,id:string|undefined):T=>{if(!id||!own(map,id))fail('NOT_FOUND','规划对象不存在');return map[id!];};
|
|
10
|
-
const check=(item:{revision:number},revision:number|undefined)=>{if(item.revision!==revision)fail('CONFLICT',`规划版本已变化,当前版本 ${item.revision}`);};
|
|
11
|
-
const page=<T>(values:T[],args:PlanningArgs)=>{const offset=args.offset??0,limit=args.limit??50;if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>200)fail('INVALID_INPUT','分页参数无效');return {items:values.slice(offset,offset+limit),total:values.length,nextOffset:offset+limit<values.length?offset+limit:null};};
|
|
12
|
-
export function planningAction(novel:PlanningNovel,action:string,args:PlanningArgs,actor:{kind?:string;sessionId?:string}): unknown {
|
|
13
|
-
const board=ensurePlanning(novel);
|
|
14
|
-
const inPage=(node:{pageId?:string|null})=>args.pageId===undefined||(node.pageId??null)===args.pageId;
|
|
15
|
-
if(args.pageId!==undefined&&args.pageId!==null&&(!own(board.pages||{},args.pageId)||board.pages[args.pageId].deleted))fail('NOT_FOUND','规划页面不存在');
|
|
16
|
-
if(action==='planning.pages')return [{id:null,name:'主线规划',summary:'原有规划与默认页面',revision:0},...Object.values(board.pages||{}).filter(p=>!p.deleted)].map(p=>({...p,nodeCount:Object.values(board.nodes).filter(n=>!n.deleted&&(n.pageId??null)===p.id).length,decorationCount:Object.values(board.decorations||{}).filter(d=>!d.deleted&&d.pageId===p.id).length}));
|
|
17
|
-
if(action==='planning.decorations')return {...page(Object.values(board.decorations||{}).filter(d=>(args.includeDeleted||!d.deleted)&&inPage(d)),args),sequence:board.sequence};
|
|
18
|
-
if(action.startsWith('planning.page.')){
|
|
19
|
-
const op=action.slice('planning.'.length) as 'page.create'|'page.update'|'page.delete';
|
|
20
|
-
const result=planningAction(novel,'planning.apply',{...args,operations:[{op,id:args.pageId,expectedRevision:args.expectedRevision,confirm:args.confirm,ref:'page',value:{name:args.name,summary:args.summary}} as Operation]},actor) as {mapping:Record<string,string>;sequence:number;transactionId:string};
|
|
21
|
-
return {...novel.planning!.pages[result.mapping.page||args.pageId||''],sequence:result.sequence,transactionId:result.transactionId};
|
|
22
|
-
}
|
|
4
|
+
export { ensurePlanning, validatePlanning } from './model.ts';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
const fail=(code:string,message:string):never=>{throw Object.assign(new Error(message),{code});};
|
|
7
|
+
const copy=<T>(v:T):T=>structuredClone(v);
|
|
8
|
+
const own=(map:object,id:string)=>Object.prototype.hasOwnProperty.call(map,id);
|
|
9
|
+
const entity=<T>(map:Record<string,T>,id:string|undefined):T=>{if(!id||!own(map,id))fail('NOT_FOUND','规划对象不存在');return map[id!];};
|
|
10
|
+
const check=(item:{revision:number},revision:number|undefined)=>{if(item.revision!==revision)fail('CONFLICT',`规划版本已变化,当前版本 ${item.revision}`);};
|
|
11
|
+
const page=<T>(values:T[],args:PlanningArgs)=>{const offset=args.offset??0,limit=args.limit??50;if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>200)fail('INVALID_INPUT','分页参数无效');return {items:values.slice(offset,offset+limit),total:values.length,nextOffset:offset+limit<values.length?offset+limit:null};};
|
|
12
|
+
export function planningAction(novel:PlanningNovel,action:string,args:PlanningArgs,actor:{kind?:string;sessionId?:string}): unknown {
|
|
13
|
+
const board=ensurePlanning(novel);
|
|
14
|
+
const inPage=(node:{pageId?:string|null})=>args.pageId===undefined||(node.pageId??null)===args.pageId;
|
|
15
|
+
if(args.pageId!==undefined&&args.pageId!==null&&(!own(board.pages||{},args.pageId)||board.pages[args.pageId].deleted))fail('NOT_FOUND','规划页面不存在');
|
|
16
|
+
if(action==='planning.pages')return [{id:null,name:'主线规划',summary:'原有规划与默认页面',revision:0},...Object.values(board.pages||{}).filter(p=>!p.deleted)].map(p=>({...p,nodeCount:Object.values(board.nodes).filter(n=>!n.deleted&&(n.pageId??null)===p.id).length,decorationCount:Object.values(board.decorations||{}).filter(d=>!d.deleted&&d.pageId===p.id).length}));
|
|
17
|
+
if(action==='planning.decorations')return {...page(Object.values(board.decorations||{}).filter(d=>(args.includeDeleted||!d.deleted)&&inPage(d)),args),sequence:board.sequence};
|
|
18
|
+
if(action.startsWith('planning.page.')){
|
|
19
|
+
const op=action.slice('planning.'.length) as 'page.create'|'page.update'|'page.delete';
|
|
20
|
+
const result=planningAction(novel,'planning.apply',{...args,operations:[{op,id:args.pageId,expectedRevision:args.expectedRevision,confirm:args.confirm,ref:'page',value:{name:args.name,summary:args.summary}} as Operation]},actor) as {mapping:Record<string,string>;sequence:number;transactionId:string};
|
|
21
|
+
return {...novel.planning!.pages[result.mapping.page||args.pageId||''],sequence:result.sequence,transactionId:result.transactionId};
|
|
22
|
+
}
|
|
23
23
|
if(action==='planning.continue'){
|
|
24
|
-
const source=entity(board.nodes,args.nodeId);check(source,args.expectedRevision);
|
|
24
|
+
const source=entity(board.nodes,args.nodeId);check(source,args.expectedRevision);
|
|
25
25
|
if(source.deleted)fail('DELETED','起点已删除');
|
|
26
26
|
if(args.side!==undefined&&!['in','out'].includes(args.side))fail('INVALID_INPUT','连接方向无效');
|
|
27
|
-
if(typeof args.value?.title!=='string'||!args.value.title.trim())fail('INVALID_INPUT','请填写下一步情节标题');
|
|
28
|
-
const result=planningAction(novel,'planning.apply',{...args,reason:args.reason||`从「${source.title}」继续推进`,operations:[
|
|
29
|
-
{op:'node.create',ref:'continuation',value:{...args.value,pageId:source.pageId??null,groupId:args.value?.groupId===undefined?source.groupId??null:args.value.groupId}},
|
|
27
|
+
if(typeof args.value?.title!=='string'||!args.value.title.trim())fail('INVALID_INPUT','请填写下一步情节标题');
|
|
28
|
+
const result=planningAction(novel,'planning.apply',{...args,reason:args.reason||`从「${source.title}」继续推进`,operations:[
|
|
29
|
+
{op:'node.create',ref:'continuation',value:{...args.value,pageId:source.pageId??null,groupId:args.value?.groupId===undefined?source.groupId??null:args.value.groupId}},
|
|
30
30
|
{op:'edge.create',value:{from:args.side==='in'?'continuation':source.id,to:args.side==='in'?source.id:'continuation',type:'next'}},
|
|
31
|
-
]},actor) as {mapping:Record<string,string>;transactionId:string;sequence:number};
|
|
32
|
-
const {content,...node}=novel.planning!.nodes[result.mapping.continuation];
|
|
33
|
-
return {...result,node,from:source.id};
|
|
34
|
-
}
|
|
35
|
-
if(action==='planning.groups') return Object.values(board.groups).filter(g=>!g.deleted);
|
|
36
|
-
if(action.startsWith('planning.group.')) {
|
|
37
|
-
const op=action.slice('planning.'.length) as 'group.create'|'group.update'|'group.delete';
|
|
38
|
-
const result=planningAction(novel,'planning.apply',{...args,operations:[{op,id:args.groupId,expectedRevision:args.expectedRevision,confirm:args.confirm,ref:'group',value:{name:args.name,summary:args.summary}} as Operation]},actor) as {mapping:Record<string,string>;sequence:number;transactionId:string};
|
|
39
|
-
return {...novel.planning!.groups[result.mapping.group||args.groupId||''],sequence:result.sequence,transactionId:result.transactionId};
|
|
40
|
-
}
|
|
41
|
-
if(action==='planning.list'||action==='planning.search'){
|
|
42
|
-
const q=(args.query||'').toLowerCase();
|
|
43
|
-
const rows=Object.values(board.nodes).filter(n=>inPage(n)&&(args.includeDeleted||!n.deleted)&&(args.parentId===undefined||n.parentId===args.parentId)&&(args.groupId===undefined||(n.groupId??null)===args.groupId)&&(!args.status||n.status===args.status)&&(!args.thread||n.threads.includes(args.thread))&&(!q||`${n.title}\n${n.summary}`.toLowerCase().includes(q))).map(({content,...n})=>n);
|
|
44
|
-
return {...page(rows,args),edges:Object.values(board.edges).filter(e=>!e.deleted&&inPage(board.nodes[e.from])),sequence:board.sequence};
|
|
45
|
-
}
|
|
46
|
-
if(action==='planning.get'){const node=entity(board.nodes,args.nodeId),edges=Object.values(board.edges).filter(e=>!e.deleted&&(e.from===node.id||e.to===node.id));const neighbors=Object.values(board.nodes).filter(n=>!n.deleted&&n.id!==node.id&&edges.some(e=>e.from===n.id||e.to===n.id)).map(({content,...n})=>n);return {node,edges,neighbors,flow:{previous:neighbors.filter(n=>edges.some(e=>e.from===n.id&&e.to===node.id)),next:neighbors.filter(n=>edges.some(e=>e.from===node.id&&e.to===n.id))},children:Object.values(board.nodes).filter(n=>!n.deleted&&n.parentId===node.id).map(({content,...n})=>n),sequence:board.sequence};}
|
|
47
|
-
if(action==='planning.changes'||action==='planning.history'){
|
|
48
|
-
const values=board.transactions.filter(t=>(args.after===undefined||t.sequence>args.after)&&(!args.nodeId||t.changes.some(c=>c.id===args.nodeId)));
|
|
49
|
-
return {...page(action==='planning.history'?values.reverse():values,args),sequence:board.sequence};
|
|
50
|
-
}
|
|
51
|
-
if(!['planning.apply','planning.revert'].includes(action))fail('UNKNOWN_ACTION',action);
|
|
52
|
-
if(typeof args.requestId!=='string'||!args.requestId)fail('INVALID_INPUT','规划修改必须提供 requestId');
|
|
53
|
-
if(args.expectedSequence!==undefined&&args.expectedSequence!==board.sequence)fail('CONFLICT','规划结构在确认期间已变化,请重新核对删除范围');
|
|
54
|
-
const draft=copy(board), touched=new Map<string,Omit<Change,'after'>>(),mapping:Record<string,string>={};
|
|
55
|
-
draft.pages??={};draft.decorations??={};
|
|
56
|
-
const remember=(collection:Collection,id:string)=>{const key=`${collection}:${id}`;if(!touched.has(key))touched.set(key,{collection,id,before:copy(draft[collection][id]??null)});};
|
|
57
|
-
const stamp=()=>({lastSequence:board.sequence+1,updatedAt:new Date().toISOString(),actor:{kind:actor.kind==='human'?'human':'agent',sessionId:actor.sessionId||null}});
|
|
58
|
-
const update=<K extends Collection>(collection:K,id:string,patch:Partial<Board[K][string]>)=>{remember(collection,id);const previous=draft[collection][id];(draft[collection] as Record<string,PlanningEntity>)[id]={...previous,...patch,id,revision:(previous?.revision||0)+1,...stamp()} as PlanningEntity;};
|
|
59
|
-
const resolve=(id:string|null|undefined):string=>id&&(own(mapping,id)?mapping[id]:id)||'';
|
|
60
|
-
if(action==='planning.revert'){
|
|
61
|
-
const transaction=board.transactions.find(t=>t.id===args.transactionId);if(!transaction)fail('NOT_FOUND','变更记录不存在');
|
|
62
|
-
for(const change of transaction!.changes){const current=entity<PlanningEntity>(draft[change.collection],change.id);check(current,change.after.revision);update(change.collection,change.id,change.before||{...current,deleted:true});}
|
|
63
|
-
}else{
|
|
31
|
+
]},actor) as {mapping:Record<string,string>;transactionId:string;sequence:number};
|
|
32
|
+
const {content,...node}=novel.planning!.nodes[result.mapping.continuation];
|
|
33
|
+
return {...result,node,from:source.id};
|
|
34
|
+
}
|
|
35
|
+
if(action==='planning.groups') return Object.values(board.groups).filter(g=>!g.deleted);
|
|
36
|
+
if(action.startsWith('planning.group.')) {
|
|
37
|
+
const op=action.slice('planning.'.length) as 'group.create'|'group.update'|'group.delete';
|
|
38
|
+
const result=planningAction(novel,'planning.apply',{...args,operations:[{op,id:args.groupId,expectedRevision:args.expectedRevision,confirm:args.confirm,ref:'group',value:{name:args.name,summary:args.summary}} as Operation]},actor) as {mapping:Record<string,string>;sequence:number;transactionId:string};
|
|
39
|
+
return {...novel.planning!.groups[result.mapping.group||args.groupId||''],sequence:result.sequence,transactionId:result.transactionId};
|
|
40
|
+
}
|
|
41
|
+
if(action==='planning.list'||action==='planning.search'){
|
|
42
|
+
const q=(args.query||'').toLowerCase();
|
|
43
|
+
const rows=Object.values(board.nodes).filter(n=>inPage(n)&&(args.includeDeleted||!n.deleted)&&(args.parentId===undefined||n.parentId===args.parentId)&&(args.groupId===undefined||(n.groupId??null)===args.groupId)&&(!args.status||n.status===args.status)&&(!args.thread||n.threads.includes(args.thread))&&(!q||`${n.title}\n${n.summary}`.toLowerCase().includes(q))).map(({content,...n})=>n);
|
|
44
|
+
return {...page(rows,args),edges:Object.values(board.edges).filter(e=>!e.deleted&&inPage(board.nodes[e.from])),sequence:board.sequence};
|
|
45
|
+
}
|
|
46
|
+
if(action==='planning.get'){const node=entity(board.nodes,args.nodeId),edges=Object.values(board.edges).filter(e=>!e.deleted&&(e.from===node.id||e.to===node.id));const neighbors=Object.values(board.nodes).filter(n=>!n.deleted&&n.id!==node.id&&edges.some(e=>e.from===n.id||e.to===n.id)).map(({content,...n})=>n);return {node,edges,neighbors,flow:{previous:neighbors.filter(n=>edges.some(e=>e.from===n.id&&e.to===node.id)),next:neighbors.filter(n=>edges.some(e=>e.from===node.id&&e.to===n.id))},children:Object.values(board.nodes).filter(n=>!n.deleted&&n.parentId===node.id).map(({content,...n})=>n),sequence:board.sequence};}
|
|
47
|
+
if(action==='planning.changes'||action==='planning.history'){
|
|
48
|
+
const values=board.transactions.filter(t=>(args.after===undefined||t.sequence>args.after)&&(!args.nodeId||t.changes.some(c=>c.id===args.nodeId)));
|
|
49
|
+
return {...page(action==='planning.history'?values.reverse():values,args),sequence:board.sequence};
|
|
50
|
+
}
|
|
51
|
+
if(!['planning.apply','planning.revert'].includes(action))fail('UNKNOWN_ACTION',action);
|
|
52
|
+
if(typeof args.requestId!=='string'||!args.requestId)fail('INVALID_INPUT','规划修改必须提供 requestId');
|
|
53
|
+
if(args.expectedSequence!==undefined&&args.expectedSequence!==board.sequence)fail('CONFLICT','规划结构在确认期间已变化,请重新核对删除范围');
|
|
54
|
+
const draft=copy(board), touched=new Map<string,Omit<Change,'after'>>(),mapping:Record<string,string>={};
|
|
55
|
+
draft.pages??={};draft.decorations??={};
|
|
56
|
+
const remember=(collection:Collection,id:string)=>{const key=`${collection}:${id}`;if(!touched.has(key))touched.set(key,{collection,id,before:copy(draft[collection][id]??null)});};
|
|
57
|
+
const stamp=()=>({lastSequence:board.sequence+1,updatedAt:new Date().toISOString(),actor:{kind:actor.kind==='human'?'human':'agent',sessionId:actor.sessionId||null}});
|
|
58
|
+
const update=<K extends Collection>(collection:K,id:string,patch:Partial<Board[K][string]>)=>{remember(collection,id);const previous=draft[collection][id];(draft[collection] as Record<string,PlanningEntity>)[id]={...previous,...patch,id,revision:(previous?.revision||0)+1,...stamp()} as PlanningEntity;};
|
|
59
|
+
const resolve=(id:string|null|undefined):string=>id&&(own(mapping,id)?mapping[id]:id)||'';
|
|
60
|
+
if(action==='planning.revert'){
|
|
61
|
+
const transaction=board.transactions.find(t=>t.id===args.transactionId);if(!transaction)fail('NOT_FOUND','变更记录不存在');
|
|
62
|
+
for(const change of transaction!.changes){const current=entity<PlanningEntity>(draft[change.collection],change.id);check(current,change.after.revision);update(change.collection,change.id,change.before||{...current,deleted:true});}
|
|
63
|
+
}else{
|
|
64
64
|
for(const op of normalizePlanningOperations(args.operations)){
|
|
65
|
-
if(op.op==='page.create'){
|
|
66
|
-
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
67
|
-
update('pages',id,{name:op.value?.name,summary:op.value?.summary??'',deleted:false});
|
|
68
|
-
}else if(op.op==='page.update'||op.op==='page.delete'){
|
|
69
|
-
const id=resolve(op.id),p=entity(draft.pages,id);check(p,op.expectedRevision);if(p.deleted)fail('DELETED','页面已删除');
|
|
70
|
-
if(op.op==='page.delete'){
|
|
71
|
-
if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除页面需要确认');
|
|
72
|
-
if([...Object.values(draft.nodes),...Object.values(draft.decorations)].some(n=>!n.deleted&&n.pageId===id))fail('PAGE_NOT_EMPTY','请先移出情节并移除批注,再删除空页面');
|
|
73
|
-
update('pages',id,{deleted:true});
|
|
74
|
-
}else update('pages',id,Object.fromEntries((['name','summary'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,op.value![k]])));
|
|
75
|
-
}else if(op.op==='decoration.create'){
|
|
76
|
-
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
77
|
-
update('decorations',id,{kind:'note',title:'',content:'',position:{x:40,y:60},width:280,height:160,color:'sand',fontSize:18,fontFamily:'sans',...pickDecoration(op.value),pageId:resolve(op.value?.pageId)||null,deleted:false});
|
|
78
|
-
}else if(op.op==='decoration.update'||op.op==='decoration.delete'){
|
|
79
|
-
const id=resolve(op.id),d=entity(draft.decorations,id);check(d,op.expectedRevision);if(d.deleted)fail('DELETED','批注已删除');
|
|
80
|
-
if(op.op==='decoration.delete'){if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除批注需要确认');update('decorations',id,{deleted:true});}
|
|
81
|
-
else {const value=pickDecoration(op.value);if(value.pageId)value.pageId=resolve(value.pageId);update('decorations',id,value);}
|
|
82
|
-
}else if(op.op==='group.create'){
|
|
83
|
-
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
84
|
-
update('groups',id,{name:op.value?.name,summary:op.value?.summary??'',deleted:false});
|
|
85
|
-
}else if(op.op==='group.update'||op.op==='group.delete'){
|
|
86
|
-
const id=resolve(op.id),group=entity(draft.groups,id);check(group,op.expectedRevision);if(group.deleted)fail('DELETED','分组已删除');
|
|
87
|
-
if(op.op==='group.delete'){
|
|
88
|
-
if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除分组需要确认');
|
|
89
|
-
for(const node of Object.values(draft.nodes))if(node.groupId===id)update('nodes',node.id,{groupId:null});
|
|
90
|
-
update('groups',id,{deleted:true});
|
|
91
|
-
}else update('groups',id,Object.fromEntries((['name','summary'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,op.value![k]])));
|
|
92
|
-
}else if(op.op==='node.create'){
|
|
93
|
-
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
94
|
-
update('nodes',id,{title:'新规划',summary:'',content:'',scope:'unspecified',status:'idea',threads:[],chapterRefs:[],memoryRefs:[],...pickNode(op.value),pageId:resolve(op.value?.pageId)||null,groupId:resolve(op.value?.groupId)||null,parentId:resolve(op.value?.parentId)||null,deleted:false});
|
|
95
|
-
}else if(op.op==='node.update'){
|
|
96
|
-
const id=resolve(op.id),node=entity(draft.nodes,id);check(node,op.expectedRevision);if(node.deleted)fail('DELETED','规划已删除');const value=pickNode(op.value);if(value.parentId)value.parentId=resolve(value.parentId);if(value.groupId)value.groupId=resolve(value.groupId);if(value.pageId)value.pageId=resolve(value.pageId);update('nodes',id,value);
|
|
97
|
-
}else if(op.op==='node.delete'){
|
|
98
|
-
const id=resolve(op.id),node=entity(draft.nodes,id);check(node,op.expectedRevision);if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除需要确认');
|
|
99
|
-
const children=Object.values(draft.nodes).filter(n=>!n.deleted&&n.parentId===id);
|
|
100
|
-
if(children.length&&!['detach','subtree'].includes(op.childPolicy||''))fail('CONFIRM_REQUIRED','请选择移出子节点或删除子树');
|
|
101
|
-
const ids=new Set([id]);if(op.childPolicy==='subtree'){let changed=true;while(changed){changed=false;for(const n of Object.values(draft.nodes))if(!n.deleted&&n.parentId!==null&&ids.has(n.parentId)&&!ids.has(n.id)){ids.add(n.id);changed=true;}}}
|
|
102
|
-
else for(const child of children)update('nodes',child.id,{parentId:node.parentId});
|
|
103
|
-
for(const target of ids)update('nodes',target,{deleted:true});
|
|
104
|
-
for(const edge of Object.values(draft.edges))if(!edge.deleted&&(ids.has(edge.from)||ids.has(edge.to)))update('edges',edge.id,{deleted:true});
|
|
105
|
-
}else if(op.op==='edge.create'){
|
|
106
|
-
update('edges',randomUUID(),{from:resolve(op.value?.from),to:resolve(op.value?.to),type:op.value?.type||'next',label:op.value?.label||'',deleted:false});
|
|
107
|
-
}else if(op.op==='edge.update'){
|
|
108
|
-
const id=resolve(op.id),edge=entity(draft.edges,id);check(edge,op.expectedRevision);if(edge.deleted)fail('DELETED','关系已删除');
|
|
109
|
-
update('edges',id,Object.fromEntries((['from','to','type','label'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,k==='from'||k==='to'?resolve(op.value![k]):op.value![k]])));
|
|
110
|
-
}else if(op.op==='edge.delete'){
|
|
111
|
-
const id=resolve(op.id);check(entity(draft.edges,id),op.expectedRevision);if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除关系需要确认');update('edges',id,{deleted:true});
|
|
112
|
-
}else fail('INVALID_INPUT',`不支持的规划操作`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
validatePlanning(draft,novel);
|
|
116
|
-
const transaction={id:randomUUID(),sequence:board.sequence+1,...stamp(),reason:typeof args.reason==='string'?args.reason:'调整规划',changes:[...touched.values()].map(c=>({...c,after:copy(draft[c.collection][c.id])}))};
|
|
117
|
-
draft.sequence=transaction.sequence;draft.transactions.push(transaction);novel.planning=draft;
|
|
118
|
-
return {transactionId:transaction.id,sequence:draft.sequence,mapping,changed:transaction.changes.map(c=>({id:c.id,collection:c.collection,revision:c.after.revision}))};
|
|
119
|
-
}
|
|
120
|
-
function pickNode(value:Partial<PlanningNode>={}):Partial<PlanningNode> {return Object.fromEntries((['title','summary','content','scope','status','parentId','groupId','pageId','position','threads','chapterRefs','memoryRefs'] as const).filter(k=>value[k]!==undefined).map(k=>[k,value[k]]));}
|
|
65
|
+
if(op.op==='page.create'){
|
|
66
|
+
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
67
|
+
update('pages',id,{name:op.value?.name,summary:op.value?.summary??'',deleted:false});
|
|
68
|
+
}else if(op.op==='page.update'||op.op==='page.delete'){
|
|
69
|
+
const id=resolve(op.id),p=entity(draft.pages,id);check(p,op.expectedRevision);if(p.deleted)fail('DELETED','页面已删除');
|
|
70
|
+
if(op.op==='page.delete'){
|
|
71
|
+
if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除页面需要确认');
|
|
72
|
+
if([...Object.values(draft.nodes),...Object.values(draft.decorations)].some(n=>!n.deleted&&n.pageId===id))fail('PAGE_NOT_EMPTY','请先移出情节并移除批注,再删除空页面');
|
|
73
|
+
update('pages',id,{deleted:true});
|
|
74
|
+
}else update('pages',id,Object.fromEntries((['name','summary'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,op.value![k]])));
|
|
75
|
+
}else if(op.op==='decoration.create'){
|
|
76
|
+
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
77
|
+
update('decorations',id,{kind:'note',title:'',content:'',position:{x:40,y:60},width:280,height:160,color:'sand',fontSize:18,fontFamily:'sans',...pickDecoration(op.value),pageId:resolve(op.value?.pageId)||null,deleted:false});
|
|
78
|
+
}else if(op.op==='decoration.update'||op.op==='decoration.delete'){
|
|
79
|
+
const id=resolve(op.id),d=entity(draft.decorations,id);check(d,op.expectedRevision);if(d.deleted)fail('DELETED','批注已删除');
|
|
80
|
+
if(op.op==='decoration.delete'){if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除批注需要确认');update('decorations',id,{deleted:true});}
|
|
81
|
+
else {const value=pickDecoration(op.value);if(value.pageId)value.pageId=resolve(value.pageId);update('decorations',id,value);}
|
|
82
|
+
}else if(op.op==='group.create'){
|
|
83
|
+
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
84
|
+
update('groups',id,{name:op.value?.name,summary:op.value?.summary??'',deleted:false});
|
|
85
|
+
}else if(op.op==='group.update'||op.op==='group.delete'){
|
|
86
|
+
const id=resolve(op.id),group=entity(draft.groups,id);check(group,op.expectedRevision);if(group.deleted)fail('DELETED','分组已删除');
|
|
87
|
+
if(op.op==='group.delete'){
|
|
88
|
+
if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除分组需要确认');
|
|
89
|
+
for(const node of Object.values(draft.nodes))if(node.groupId===id)update('nodes',node.id,{groupId:null});
|
|
90
|
+
update('groups',id,{deleted:true});
|
|
91
|
+
}else update('groups',id,Object.fromEntries((['name','summary'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,op.value![k]])));
|
|
92
|
+
}else if(op.op==='node.create'){
|
|
93
|
+
const id=randomUUID();if(op.ref){if(own(mapping,op.ref))fail('INVALID_INPUT','临时引用重复');Object.defineProperty(mapping,op.ref,{value:id,enumerable:true});}
|
|
94
|
+
update('nodes',id,{title:'新规划',summary:'',content:'',scope:'unspecified',status:'idea',threads:[],chapterRefs:[],memoryRefs:[],...pickNode(op.value),pageId:resolve(op.value?.pageId)||null,groupId:resolve(op.value?.groupId)||null,parentId:resolve(op.value?.parentId)||null,deleted:false});
|
|
95
|
+
}else if(op.op==='node.update'){
|
|
96
|
+
const id=resolve(op.id),node=entity(draft.nodes,id);check(node,op.expectedRevision);if(node.deleted)fail('DELETED','规划已删除');const value=pickNode(op.value);if(value.parentId)value.parentId=resolve(value.parentId);if(value.groupId)value.groupId=resolve(value.groupId);if(value.pageId)value.pageId=resolve(value.pageId);update('nodes',id,value);
|
|
97
|
+
}else if(op.op==='node.delete'){
|
|
98
|
+
const id=resolve(op.id),node=entity(draft.nodes,id);check(node,op.expectedRevision);if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除需要确认');
|
|
99
|
+
const children=Object.values(draft.nodes).filter(n=>!n.deleted&&n.parentId===id);
|
|
100
|
+
if(children.length&&!['detach','subtree'].includes(op.childPolicy||''))fail('CONFIRM_REQUIRED','请选择移出子节点或删除子树');
|
|
101
|
+
const ids=new Set([id]);if(op.childPolicy==='subtree'){let changed=true;while(changed){changed=false;for(const n of Object.values(draft.nodes))if(!n.deleted&&n.parentId!==null&&ids.has(n.parentId)&&!ids.has(n.id)){ids.add(n.id);changed=true;}}}
|
|
102
|
+
else for(const child of children)update('nodes',child.id,{parentId:node.parentId});
|
|
103
|
+
for(const target of ids)update('nodes',target,{deleted:true});
|
|
104
|
+
for(const edge of Object.values(draft.edges))if(!edge.deleted&&(ids.has(edge.from)||ids.has(edge.to)))update('edges',edge.id,{deleted:true});
|
|
105
|
+
}else if(op.op==='edge.create'){
|
|
106
|
+
update('edges',randomUUID(),{from:resolve(op.value?.from),to:resolve(op.value?.to),type:op.value?.type||'next',label:op.value?.label||'',deleted:false});
|
|
107
|
+
}else if(op.op==='edge.update'){
|
|
108
|
+
const id=resolve(op.id),edge=entity(draft.edges,id);check(edge,op.expectedRevision);if(edge.deleted)fail('DELETED','关系已删除');
|
|
109
|
+
update('edges',id,Object.fromEntries((['from','to','type','label'] as const).filter(k=>op.value?.[k]!==undefined).map(k=>[k,k==='from'||k==='to'?resolve(op.value![k]):op.value![k]])));
|
|
110
|
+
}else if(op.op==='edge.delete'){
|
|
111
|
+
const id=resolve(op.id);check(entity(draft.edges,id),op.expectedRevision);if(op.confirm!==true)fail('CONFIRM_REQUIRED','删除关系需要确认');update('edges',id,{deleted:true});
|
|
112
|
+
}else fail('INVALID_INPUT',`不支持的规划操作`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
validatePlanning(draft,novel);
|
|
116
|
+
const transaction={id:randomUUID(),sequence:board.sequence+1,...stamp(),reason:typeof args.reason==='string'?args.reason:'调整规划',changes:[...touched.values()].map(c=>({...c,after:copy(draft[c.collection][c.id])}))};
|
|
117
|
+
draft.sequence=transaction.sequence;draft.transactions.push(transaction);novel.planning=draft;
|
|
118
|
+
return {transactionId:transaction.id,sequence:draft.sequence,mapping,changed:transaction.changes.map(c=>({id:c.id,collection:c.collection,revision:c.after.revision}))};
|
|
119
|
+
}
|
|
120
|
+
function pickNode(value:Partial<PlanningNode>={}):Partial<PlanningNode> {return Object.fromEntries((['title','summary','content','scope','status','parentId','groupId','pageId','position','threads','chapterRefs','memoryRefs'] as const).filter(k=>value[k]!==undefined).map(k=>[k,value[k]]));}
|
|
121
121
|
function pickDecoration(value:Partial<Decoration>={}):Partial<Decoration>{return Object.fromEntries((['pageId','kind','title','content','position','width','height','color','fontSize','fontFamily','moveContents'] as const).filter(k=>value[k]!==undefined).map(k=>[k,value[k]]));}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import type { Board, PlanningNovel } from '../../contracts/planning.ts';
|
|
2
|
-
const fail=(code:string,message:string):never=>{throw Object.assign(new Error(message),{code});};
|
|
3
|
-
const copy=<T>(v:T):T=>structuredClone(v);
|
|
4
|
-
const record=(v:unknown):v is Record<string,unknown>=>v!==null&&typeof v==='object'&&!Array.isArray(v);
|
|
5
|
-
const own=(map:object,id:string)=>Object.prototype.hasOwnProperty.call(map,id);
|
|
6
|
-
export function ensurePlanning(novel:PlanningNovel):Board {
|
|
7
|
-
if(novel.planning){
|
|
8
|
-
novel.planning.groups ??= {};
|
|
9
|
-
return novel.planning;
|
|
10
|
-
}
|
|
11
|
-
const board:Board={formatVersion:2,nodes:{},edges:{},groups:{},pages:{},decorations:{},sequence:0,transactions:[]};
|
|
12
|
-
if(novel.plan){const id=`legacy-plan-${novel.plan.id}`;board.nodes[id]={id,revision:1,title:'原有规划',summary:'从旧规划完整迁移',content:novel.plan.content,scope:'unspecified',status:'idea',parentId:null,threads:[],chapterRefs:[],memoryRefs:[],deleted:false,updatedAt:novel.plan.updatedAt||new Date().toISOString(),actor:{kind:'migration'}};board.legacy=copy(novel.plan);}
|
|
13
|
-
novel.planning=board;return board;
|
|
14
|
-
}
|
|
15
|
-
function cycle(nodes:{id:string}[],edges:[string,string][]) {
|
|
16
|
-
const adjacency=new Map<string,string[]>();for(const [from,to] of edges){if(!adjacency.has(from))adjacency.set(from,[]);adjacency.get(from)!.push(to);}
|
|
17
|
-
const done=new Set(),active=new Set();
|
|
18
|
-
const visit=(id:string)=>{if(active.has(id))fail('INVALID_PLANNING','层级或关系不能形成循环');if(done.has(id))return;active.add(id);for(const next of adjacency.get(id)||[])visit(next);active.delete(id);done.add(id);};
|
|
19
|
-
for(const node of nodes)visit(node.id);
|
|
20
|
-
}
|
|
21
|
-
export function validatePlanning(board:Board,novel:PlanningNovel):void {
|
|
22
|
-
if(!board||![1,2].includes(board.formatVersion)||!record(board.nodes)||!record(board.edges)||!Array.isArray(board.transactions)||(!Number.isSafeInteger(board.sequence)||board.sequence<0))fail('INVALID_PLANNING','规划数据格式无效');
|
|
23
|
-
board.groups ??= {};
|
|
24
|
-
const pages=board.pages??{},decorations=board.decorations??{};
|
|
25
|
-
if(!record(pages)||!record(decorations))fail('INVALID_PLANNING','页面或批注格式无效');
|
|
26
|
-
const validPage=(id:unknown)=>id===undefined||id===null||(typeof id==='string'&&own(pages,id)&&!pages[id].deleted);
|
|
27
|
-
for(const [id,p] of Object.entries(pages))if(!record(p)||id!==p.id||typeof p.name!=='string'||!p.name.trim()||typeof p.summary!=='string'||typeof p.deleted!=='boolean'||!Number.isSafeInteger(p.revision)||p.revision<1)fail('INVALID_PLANNING','规划页面无效');
|
|
28
|
-
for(const [id,d] of Object.entries(decorations)){
|
|
29
|
-
if(!record(d)||id!==d.id||!Number.isSafeInteger(d.revision)||d.revision<1||typeof d.deleted!=='boolean'||!['note','frame'].includes(d.kind)||typeof d.title!=='string'||typeof d.content!=='string'||(!d.deleted&&!validPage(d.pageId)))fail('INVALID_PLANNING','画布批注无效');
|
|
1
|
+
import type { Board, PlanningNovel } from '../../contracts/planning.ts';
|
|
2
|
+
const fail=(code:string,message:string):never=>{throw Object.assign(new Error(message),{code});};
|
|
3
|
+
const copy=<T>(v:T):T=>structuredClone(v);
|
|
4
|
+
const record=(v:unknown):v is Record<string,unknown>=>v!==null&&typeof v==='object'&&!Array.isArray(v);
|
|
5
|
+
const own=(map:object,id:string)=>Object.prototype.hasOwnProperty.call(map,id);
|
|
6
|
+
export function ensurePlanning(novel:PlanningNovel):Board {
|
|
7
|
+
if(novel.planning){
|
|
8
|
+
novel.planning.groups ??= {};
|
|
9
|
+
return novel.planning;
|
|
10
|
+
}
|
|
11
|
+
const board:Board={formatVersion:2,nodes:{},edges:{},groups:{},pages:{},decorations:{},sequence:0,transactions:[]};
|
|
12
|
+
if(novel.plan){const id=`legacy-plan-${novel.plan.id}`;board.nodes[id]={id,revision:1,title:'原有规划',summary:'从旧规划完整迁移',content:novel.plan.content,scope:'unspecified',status:'idea',parentId:null,threads:[],chapterRefs:[],memoryRefs:[],deleted:false,updatedAt:novel.plan.updatedAt||new Date().toISOString(),actor:{kind:'migration'}};board.legacy=copy(novel.plan);}
|
|
13
|
+
novel.planning=board;return board;
|
|
14
|
+
}
|
|
15
|
+
function cycle(nodes:{id:string}[],edges:[string,string][]) {
|
|
16
|
+
const adjacency=new Map<string,string[]>();for(const [from,to] of edges){if(!adjacency.has(from))adjacency.set(from,[]);adjacency.get(from)!.push(to);}
|
|
17
|
+
const done=new Set(),active=new Set();
|
|
18
|
+
const visit=(id:string)=>{if(active.has(id))fail('INVALID_PLANNING','层级或关系不能形成循环');if(done.has(id))return;active.add(id);for(const next of adjacency.get(id)||[])visit(next);active.delete(id);done.add(id);};
|
|
19
|
+
for(const node of nodes)visit(node.id);
|
|
20
|
+
}
|
|
21
|
+
export function validatePlanning(board:Board,novel:PlanningNovel):void {
|
|
22
|
+
if(!board||![1,2].includes(board.formatVersion)||!record(board.nodes)||!record(board.edges)||!Array.isArray(board.transactions)||(!Number.isSafeInteger(board.sequence)||board.sequence<0))fail('INVALID_PLANNING','规划数据格式无效');
|
|
23
|
+
board.groups ??= {};
|
|
24
|
+
const pages=board.pages??{},decorations=board.decorations??{};
|
|
25
|
+
if(!record(pages)||!record(decorations))fail('INVALID_PLANNING','页面或批注格式无效');
|
|
26
|
+
const validPage=(id:unknown)=>id===undefined||id===null||(typeof id==='string'&&own(pages,id)&&!pages[id].deleted);
|
|
27
|
+
for(const [id,p] of Object.entries(pages))if(!record(p)||id!==p.id||typeof p.name!=='string'||!p.name.trim()||typeof p.summary!=='string'||typeof p.deleted!=='boolean'||!Number.isSafeInteger(p.revision)||p.revision<1)fail('INVALID_PLANNING','规划页面无效');
|
|
28
|
+
for(const [id,d] of Object.entries(decorations)){
|
|
29
|
+
if(!record(d)||id!==d.id||!Number.isSafeInteger(d.revision)||d.revision<1||typeof d.deleted!=='boolean'||!['note','frame'].includes(d.kind)||typeof d.title!=='string'||typeof d.content!=='string'||(!d.deleted&&!validPage(d.pageId)))fail('INVALID_PLANNING','画布批注无效');
|
|
30
30
|
if(!record(d.position)||!Number.isFinite(d.position.x)||!Number.isFinite(d.position.y)||!Number.isFinite(d.width)||d.width<160||d.width>6000||!Number.isFinite(d.height)||d.height<80||d.height>6000||![14,18,24,32].includes(d.fontSize)||!['sans','serif'].includes(d.fontFamily)||!['neutral','sand','sage','sky','rose'].includes(d.color)||(d.moveContents!==undefined&&typeof d.moveContents!=='boolean'))fail('INVALID_PLANNING','批注样式或位置无效');
|
|
31
|
-
}
|
|
32
|
-
if(!record(board.groups))fail('INVALID_PLANNING','规划分组无效');
|
|
33
|
-
for(const [id,g] of Object.entries(board.groups)) if(!record(g)||id!==g.id||typeof g.name!=='string'||!g.name.trim()||typeof g.summary!=='string'||!Number.isSafeInteger(g.revision)||g.revision<1) fail('INVALID_PLANNING','规划分组无效');
|
|
34
|
-
if([...Object.values(board.nodes),...Object.values(board.edges)].some(v=>!record(v)))fail('INVALID_PLANNING','规划对象无效');
|
|
35
|
-
const nodes=Object.values(board.nodes).filter(n=>!n.deleted),edges=Object.values(board.edges).filter(e=>!e.deleted);
|
|
36
|
-
for(const [id,n] of Object.entries(board.nodes)){
|
|
37
|
-
if(!n.deleted&&!validPage(n.pageId))fail('INVALID_PLANNING','所属页面不存在或已删除');
|
|
31
|
+
}
|
|
32
|
+
if(!record(board.groups))fail('INVALID_PLANNING','规划分组无效');
|
|
33
|
+
for(const [id,g] of Object.entries(board.groups)) if(!record(g)||id!==g.id||typeof g.name!=='string'||!g.name.trim()||typeof g.summary!=='string'||!Number.isSafeInteger(g.revision)||g.revision<1) fail('INVALID_PLANNING','规划分组无效');
|
|
34
|
+
if([...Object.values(board.nodes),...Object.values(board.edges)].some(v=>!record(v)))fail('INVALID_PLANNING','规划对象无效');
|
|
35
|
+
const nodes=Object.values(board.nodes).filter(n=>!n.deleted),edges=Object.values(board.edges).filter(e=>!e.deleted);
|
|
36
|
+
for(const [id,n] of Object.entries(board.nodes)){
|
|
37
|
+
if(!n.deleted&&!validPage(n.pageId))fail('INVALID_PLANNING','所属页面不存在或已删除');
|
|
38
38
|
if(n.position!==undefined&&n.position!==null&&(!record(n.position)||!Number.isFinite(n.position.x)||!Number.isFinite(n.position.y)))fail('INVALID_PLANNING','画布位置无效');
|
|
39
|
-
if(!n.deleted&&n.groupId&&board.groups[n.groupId]?.deleted)fail('INVALID_PLANNING','所属分组已删除');
|
|
40
|
-
if(typeof n.deleted!=='boolean'||(n.parentId!==null&&typeof n.parentId!=='string')||id!==n.id||!Number.isSafeInteger(n.revision)||n.revision<1||typeof n.title!=='string'||!n.title.trim()||typeof n.content!=='string'||typeof n.summary!=='string'||(n.groupId!==null&&n.groupId!==undefined&&!Object.hasOwn(board.groups,n.groupId)))fail('INVALID_PLANNING','规划标题、内容或版本无效');
|
|
41
|
-
if(!['long','phase','near','unspecified'].includes(n.scope)||!['idea','selected','written','dropped'].includes(n.status))fail('INVALID_PLANNING','规划范围或状态无效');
|
|
42
|
-
if(!Array.isArray(n.threads)||n.threads.some(t=>typeof t!=='string')||!Array.isArray(n.chapterRefs)||!Array.isArray(n.memoryRefs))fail('INVALID_PLANNING','关联字段无效');
|
|
43
|
-
if(!n.deleted&&n.parentId&&(!own(board.nodes,n.parentId)||board.nodes[n.parentId].deleted))fail('INVALID_PLANNING','父规划不存在');
|
|
44
|
-
if(!n.deleted&&n.parentId&&(n.pageId??null)!==(board.nodes[n.parentId].pageId??null))fail('INVALID_PLANNING','父子规划须在同一页面');
|
|
45
|
-
for(const ref of n.chapterRefs)if(!record(ref)||!own(novel.chapters,ref.chapterId)||!novel.chapters[ref.chapterId]?.versions.some(v=>v.revision===ref.revision))fail('INVALID_PLANNING','关联的正文版本不存在');
|
|
46
|
-
for(const id of n.memoryRefs)if(!own(novel.nodes,id))fail('INVALID_PLANNING','关联设定不存在');
|
|
47
|
-
if(!n.deleted&&n.status==='written'&&!n.chapterRefs.length)fail('INVALID_PLANNING','已写入正文的规划至少需要一个正文版本关联');
|
|
48
|
-
}
|
|
49
|
-
const duplicates=new Set();
|
|
50
|
-
for(const [id,e] of Object.entries(board.edges)){
|
|
51
|
-
if(typeof e.deleted!=='boolean'||id!==e.id||!Number.isSafeInteger(e.revision)||e.revision<1||!['next','requires','alternative'].includes(e.type)||typeof e.label!=='string')fail('INVALID_PLANNING','关系无效');
|
|
52
|
-
if(e.deleted)continue;
|
|
53
|
-
if(e.from===e.to||!own(board.nodes,e.from)||!own(board.nodes,e.to)||board.nodes[e.from].deleted||board.nodes[e.to].deleted)fail('INVALID_PLANNING','关系端点无效');
|
|
54
|
-
if((board.nodes[e.from].pageId??null)!==(board.nodes[e.to].pageId??null))fail('INVALID_PLANNING','不同页面不能直接连线,请先移动规划或移除跨页连线');
|
|
55
|
-
const key=JSON.stringify([e.from,e.to,e.type]);if(duplicates.has(key))fail('INVALID_PLANNING','重复关系');duplicates.add(key);
|
|
56
|
-
}
|
|
57
|
-
for(const tx of board.transactions){if(typeof tx.id!=='string'||!Number.isSafeInteger(tx.sequence)||!Array.isArray(tx.changes))fail('INVALID_PLANNING','规划历史无效');for(const change of tx.changes)if(!['nodes','edges','groups','pages','decorations'].includes(change.collection)||typeof change.id!=='string'||change.after?.id!==change.id||!Number.isSafeInteger(change.after.revision)||change.after.revision<1||(change.before&&change.before.id!==change.id))fail('INVALID_PLANNING','规划历史快照无效');}
|
|
58
|
-
cycle(nodes,edges.filter(e=>e.type==='next'||e.type==='alternative').map(e=>[e.from,e.to]));
|
|
59
|
-
cycle(nodes,nodes.filter(n=>n.parentId).map(n=>[n.parentId!,n.id]));
|
|
60
|
-
for(const type of ['next','requires','alternative'])cycle(nodes,edges.filter(e=>e.type===type).map(e=>[e.from,e.to]));
|
|
61
|
-
}
|
|
39
|
+
if(!n.deleted&&n.groupId&&board.groups[n.groupId]?.deleted)fail('INVALID_PLANNING','所属分组已删除');
|
|
40
|
+
if(typeof n.deleted!=='boolean'||(n.parentId!==null&&typeof n.parentId!=='string')||id!==n.id||!Number.isSafeInteger(n.revision)||n.revision<1||typeof n.title!=='string'||!n.title.trim()||typeof n.content!=='string'||typeof n.summary!=='string'||(n.groupId!==null&&n.groupId!==undefined&&!Object.hasOwn(board.groups,n.groupId)))fail('INVALID_PLANNING','规划标题、内容或版本无效');
|
|
41
|
+
if(!['long','phase','near','unspecified'].includes(n.scope)||!['idea','selected','written','dropped'].includes(n.status))fail('INVALID_PLANNING','规划范围或状态无效');
|
|
42
|
+
if(!Array.isArray(n.threads)||n.threads.some(t=>typeof t!=='string')||!Array.isArray(n.chapterRefs)||!Array.isArray(n.memoryRefs))fail('INVALID_PLANNING','关联字段无效');
|
|
43
|
+
if(!n.deleted&&n.parentId&&(!own(board.nodes,n.parentId)||board.nodes[n.parentId].deleted))fail('INVALID_PLANNING','父规划不存在');
|
|
44
|
+
if(!n.deleted&&n.parentId&&(n.pageId??null)!==(board.nodes[n.parentId].pageId??null))fail('INVALID_PLANNING','父子规划须在同一页面');
|
|
45
|
+
for(const ref of n.chapterRefs)if(!record(ref)||!own(novel.chapters,ref.chapterId)||!novel.chapters[ref.chapterId]?.versions.some(v=>v.revision===ref.revision))fail('INVALID_PLANNING','关联的正文版本不存在');
|
|
46
|
+
for(const id of n.memoryRefs)if(!own(novel.nodes,id))fail('INVALID_PLANNING','关联设定不存在');
|
|
47
|
+
if(!n.deleted&&n.status==='written'&&!n.chapterRefs.length)fail('INVALID_PLANNING','已写入正文的规划至少需要一个正文版本关联');
|
|
48
|
+
}
|
|
49
|
+
const duplicates=new Set();
|
|
50
|
+
for(const [id,e] of Object.entries(board.edges)){
|
|
51
|
+
if(typeof e.deleted!=='boolean'||id!==e.id||!Number.isSafeInteger(e.revision)||e.revision<1||!['next','requires','alternative'].includes(e.type)||typeof e.label!=='string')fail('INVALID_PLANNING','关系无效');
|
|
52
|
+
if(e.deleted)continue;
|
|
53
|
+
if(e.from===e.to||!own(board.nodes,e.from)||!own(board.nodes,e.to)||board.nodes[e.from].deleted||board.nodes[e.to].deleted)fail('INVALID_PLANNING','关系端点无效');
|
|
54
|
+
if((board.nodes[e.from].pageId??null)!==(board.nodes[e.to].pageId??null))fail('INVALID_PLANNING','不同页面不能直接连线,请先移动规划或移除跨页连线');
|
|
55
|
+
const key=JSON.stringify([e.from,e.to,e.type]);if(duplicates.has(key))fail('INVALID_PLANNING','重复关系');duplicates.add(key);
|
|
56
|
+
}
|
|
57
|
+
for(const tx of board.transactions){if(typeof tx.id!=='string'||!Number.isSafeInteger(tx.sequence)||!Array.isArray(tx.changes))fail('INVALID_PLANNING','规划历史无效');for(const change of tx.changes)if(!['nodes','edges','groups','pages','decorations'].includes(change.collection)||typeof change.id!=='string'||change.after?.id!==change.id||!Number.isSafeInteger(change.after.revision)||change.after.revision<1||(change.before&&change.before.id!==change.id))fail('INVALID_PLANNING','规划历史快照无效');}
|
|
58
|
+
cycle(nodes,edges.filter(e=>e.type==='next'||e.type==='alternative').map(e=>[e.from,e.to]));
|
|
59
|
+
cycle(nodes,nodes.filter(n=>n.parentId).map(n=>[n.parentId!,n.id]));
|
|
60
|
+
for(const type of ['next','requires','alternative'])cycle(nodes,edges.filter(e=>e.type===type).map(e=>[e.from,e.to]));
|
|
61
|
+
}
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
const object = v => v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
-
const fail = message => { throw Object.assign(new Error(message), {code:'INVALID_PRESET'}); };
|
|
3
|
-
export function presetBlockReason(block) {
|
|
4
|
-
return block.marker && !block.content?.trim() ? '动态占位条目没有提示词正文,内容由催更姬按自己的方式读取' : '';
|
|
5
|
-
}
|
|
6
|
-
export function presetBlockNotice(block) {
|
|
7
|
-
if (presetBlockReason(block)) return presetBlockReason(block);
|
|
8
|
-
return /\{\{|\}\}/.test(block.content) ? '含酒馆宏或占位符:按原文保留,不执行替换;可在下方改成明确的写作要求。' : '';
|
|
9
|
-
}
|
|
10
|
-
function source(input) {
|
|
11
|
-
if (!object(input) || JSON.stringify(input).length > 2_000_000) fail('预设必须是小于 2 MB 的 JSON 对象');
|
|
12
|
-
if (Array.isArray(input.prompts)) return {data:input,format:'chat-completion'};
|
|
13
|
-
if (object(input.data) && Array.isArray(input.data.prompts)) return {data:input.data,format:'prompt-manager'};
|
|
14
|
-
if (typeof input.content === 'string' || typeof input.system_prompt === 'string') {
|
|
15
|
-
const fields=[['content','主要提示词'],['system_prompt','系统提示词'],['post_history','补充要求'],['post_history_instructions','补充指令']];
|
|
16
|
-
for(const [key] of fields)if(input[key]!==undefined&&typeof input[key]!=='string')fail(`${key} 必须是文本`);
|
|
17
|
-
const prompts=fields.filter(([key])=>typeof input[key]==='string').map(([key,name])=>({identifier:key,name,content:input[key],enabled:true}));
|
|
18
|
-
return {data:{prompts},format:'system-prompt'};
|
|
19
|
-
}
|
|
20
|
-
if ('story_string' in input || 'input_sequence' in input || 'output_sequence' in input) fail('这是上下文装填或 Instruct 分隔模板,没有独立提示词正文。请选择完整 Chat Completion 预设、提示词管理器导出或系统提示词文件。');
|
|
21
|
-
fail('未找到提示词内容。支持含 prompts 的完整预设、提示词管理器导出,以及含 content 的系统提示词 JSON;仅采样参数的文件不包含提示词。');
|
|
22
|
-
}
|
|
23
|
-
// Shared UI/backend normalization for nested and flat Prompt Manager orders.
|
|
24
|
-
export function presetImportOrders(input) {
|
|
25
|
-
const {data}=source(input),orders=data.prompt_order;
|
|
26
|
-
if(orders===undefined||orders===null)return [];
|
|
27
|
-
if(!Array.isArray(orders))fail('prompt_order 必须是数组');
|
|
28
|
-
if(!orders.length)return [];
|
|
29
|
-
if(orders.every(v=>object(v)&&typeof v.identifier==='string'))return [{character_id:'flat',order:orders}];
|
|
30
|
-
if(!orders.every(v=>object(v)&&v.character_id!==undefined&&Array.isArray(v.order)))fail('提示词排列结构无效');
|
|
31
|
-
if(new Set(orders.map(v=>String(v.character_id))).size!==orders.length)fail('提示词排列方案标识重复');
|
|
32
|
-
return orders;
|
|
33
|
-
}
|
|
34
|
-
export function importPreset(input, orderId) {
|
|
35
|
-
const {data,format}=source(input);
|
|
36
|
-
if (data.prompts.length > 500) fail('最多支持 500 个提示词条目');
|
|
37
|
-
const orders=presetImportOrders(input);
|
|
38
|
-
const order = orderId !== undefined ? orders.find(v=>String(v.character_id)===String(orderId)) : orders.find(v=>String(v.character_id)==='100001') || (orders.length===1?orders[0]:null);
|
|
39
|
-
if (orders.length && !order) fail('存在多个提示词排列,请先选择排列方案');
|
|
40
|
-
const map = new Map();
|
|
41
|
-
for (const p of data.prompts) {
|
|
42
|
-
if (!object(p) || typeof p.identifier !== 'string' || !p.identifier || map.has(p.identifier)) fail('条目标识缺失或重复');
|
|
43
|
-
if (p.content !== undefined && typeof p.content !== 'string') fail('提示词 content 必须是文本');
|
|
44
|
-
if (p.enabled !== undefined && typeof p.enabled !== 'boolean') fail('提示词 enabled 必须是布尔值');
|
|
45
|
-
map.set(p.identifier,p);
|
|
46
|
-
}
|
|
47
|
-
const seen = new Set(); const blocks = [], missing=[];
|
|
48
|
-
function add(p,enabled) {
|
|
49
|
-
if (seen.has(p.identifier)) return;
|
|
50
|
-
seen.add(p.identifier);
|
|
51
|
-
const block = { identifier:p.identifier, name:typeof p.name==='string'?p.name:p.identifier, content:p.content||'', role:p.role||'system', marker:p.marker===true,
|
|
52
|
-
injection_position:p.injection_position ?? 0, injection_trigger:p.injection_trigger ?? [], enabled:enabled===true };
|
|
53
|
-
const reason=presetBlockReason(block);
|
|
54
|
-
blocks.push({...block,enabled:enabled===true&&!reason,importWarning:presetBlockNotice(block)});
|
|
55
|
-
}
|
|
56
|
-
if (order) for (const item of order.order) {
|
|
57
|
-
if(!object(item)||typeof item.identifier!=='string'||(item.enabled!==undefined&&typeof item.enabled!=='boolean'))fail('提示词排列条目无效');
|
|
58
|
-
if (!map.has(item.identifier)) {
|
|
59
|
-
// Prompt Manager exports custom prompts without built-in runtime markers.
|
|
60
|
-
if(format==='prompt-manager'){missing.push(item.identifier);continue;}
|
|
61
|
-
fail(`排列引用了不存在的条目:${item.identifier}`);
|
|
62
|
-
}
|
|
63
|
-
add(map.get(item.identifier),item.enabled??map.get(item.identifier).enabled??true);
|
|
64
|
-
}
|
|
65
|
-
for (const p of data.prompts) add(p,order?false:p.enabled!==false);
|
|
66
|
-
const warnings=['导入后整体停用,核对后启用并保存。启用条目的正文按当前列表顺序加入写作要求。', '仅使用提示词内容;酒馆消息角色、触发、插入深度及装填规则不执行。模型和采样参数由 DSH 管理,原始文件随备份保留。'];
|
|
67
|
-
if(blocks.some(b=>/\{\{|\}\}/.test(b.content)))warnings.push('宏与占位符按原文保留,不执行。需要替换角色名或用户称谓时,请直接编辑提示词内容。');
|
|
68
|
-
if(missing.length)warnings.push(`排列中有 ${missing.length} 项未随文件导出,已跳过这些引用;未编造其提示词内容。`);
|
|
69
|
-
const result={name:typeof input.name==='string'&&input.name.trim()?input.name:typeof data.name==='string'&&data.name.trim()?data.name:'导入的写作预设',enabled:false,blocks,raw:input,importFormat:format,warnings};
|
|
70
|
-
validatePreset(result);
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
export function validatePreset(value) {
|
|
74
|
-
if (JSON.stringify(value)?.length > 2_000_000) fail('预设总大小超过 2 MB');
|
|
75
|
-
if (!object(value) || typeof value.name!=='string' || !value.name.trim() || typeof value.enabled!=='boolean' || !Array.isArray(value.blocks) || value.blocks.length>500) fail('预设格式无效');
|
|
76
|
-
if(value.importFormat!==undefined&&!['chat-completion','prompt-manager','system-prompt'].includes(value.importFormat))fail('预设导入格式标识无效');
|
|
77
|
-
const ids=new Set();let length=0;
|
|
78
|
-
for (const block of value.blocks) {
|
|
79
|
-
if (!object(block)||typeof block.identifier!=='string'||ids.has(block.identifier)||typeof block.content!=='string'||typeof block.name!=='string'||typeof block.enabled!=='boolean') fail('预设条目无效或标识重复');
|
|
80
|
-
ids.add(block.identifier);length+=block.content.length;
|
|
81
|
-
}
|
|
82
|
-
if(length>100000) fail('提示词总长度不能超过 100000 字符');
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
export function compilePreset(preset) {
|
|
86
|
-
if (!preset?.enabled) return '';
|
|
87
|
-
validatePreset(preset);
|
|
88
|
-
return preset.blocks.filter(block=>block.enabled&&block.content.trim()).map(block=>block.content).join('\n\n');
|
|
89
|
-
}
|
|
1
|
+
const object = v => v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const fail = message => { throw Object.assign(new Error(message), {code:'INVALID_PRESET'}); };
|
|
3
|
+
export function presetBlockReason(block) {
|
|
4
|
+
return block.marker && !block.content?.trim() ? '动态占位条目没有提示词正文,内容由催更姬按自己的方式读取' : '';
|
|
5
|
+
}
|
|
6
|
+
export function presetBlockNotice(block) {
|
|
7
|
+
if (presetBlockReason(block)) return presetBlockReason(block);
|
|
8
|
+
return /\{\{|\}\}/.test(block.content) ? '含酒馆宏或占位符:按原文保留,不执行替换;可在下方改成明确的写作要求。' : '';
|
|
9
|
+
}
|
|
10
|
+
function source(input) {
|
|
11
|
+
if (!object(input) || JSON.stringify(input).length > 2_000_000) fail('预设必须是小于 2 MB 的 JSON 对象');
|
|
12
|
+
if (Array.isArray(input.prompts)) return {data:input,format:'chat-completion'};
|
|
13
|
+
if (object(input.data) && Array.isArray(input.data.prompts)) return {data:input.data,format:'prompt-manager'};
|
|
14
|
+
if (typeof input.content === 'string' || typeof input.system_prompt === 'string') {
|
|
15
|
+
const fields=[['content','主要提示词'],['system_prompt','系统提示词'],['post_history','补充要求'],['post_history_instructions','补充指令']];
|
|
16
|
+
for(const [key] of fields)if(input[key]!==undefined&&typeof input[key]!=='string')fail(`${key} 必须是文本`);
|
|
17
|
+
const prompts=fields.filter(([key])=>typeof input[key]==='string').map(([key,name])=>({identifier:key,name,content:input[key],enabled:true}));
|
|
18
|
+
return {data:{prompts},format:'system-prompt'};
|
|
19
|
+
}
|
|
20
|
+
if ('story_string' in input || 'input_sequence' in input || 'output_sequence' in input) fail('这是上下文装填或 Instruct 分隔模板,没有独立提示词正文。请选择完整 Chat Completion 预设、提示词管理器导出或系统提示词文件。');
|
|
21
|
+
fail('未找到提示词内容。支持含 prompts 的完整预设、提示词管理器导出,以及含 content 的系统提示词 JSON;仅采样参数的文件不包含提示词。');
|
|
22
|
+
}
|
|
23
|
+
// Shared UI/backend normalization for nested and flat Prompt Manager orders.
|
|
24
|
+
export function presetImportOrders(input) {
|
|
25
|
+
const {data}=source(input),orders=data.prompt_order;
|
|
26
|
+
if(orders===undefined||orders===null)return [];
|
|
27
|
+
if(!Array.isArray(orders))fail('prompt_order 必须是数组');
|
|
28
|
+
if(!orders.length)return [];
|
|
29
|
+
if(orders.every(v=>object(v)&&typeof v.identifier==='string'))return [{character_id:'flat',order:orders}];
|
|
30
|
+
if(!orders.every(v=>object(v)&&v.character_id!==undefined&&Array.isArray(v.order)))fail('提示词排列结构无效');
|
|
31
|
+
if(new Set(orders.map(v=>String(v.character_id))).size!==orders.length)fail('提示词排列方案标识重复');
|
|
32
|
+
return orders;
|
|
33
|
+
}
|
|
34
|
+
export function importPreset(input, orderId) {
|
|
35
|
+
const {data,format}=source(input);
|
|
36
|
+
if (data.prompts.length > 500) fail('最多支持 500 个提示词条目');
|
|
37
|
+
const orders=presetImportOrders(input);
|
|
38
|
+
const order = orderId !== undefined ? orders.find(v=>String(v.character_id)===String(orderId)) : orders.find(v=>String(v.character_id)==='100001') || (orders.length===1?orders[0]:null);
|
|
39
|
+
if (orders.length && !order) fail('存在多个提示词排列,请先选择排列方案');
|
|
40
|
+
const map = new Map();
|
|
41
|
+
for (const p of data.prompts) {
|
|
42
|
+
if (!object(p) || typeof p.identifier !== 'string' || !p.identifier || map.has(p.identifier)) fail('条目标识缺失或重复');
|
|
43
|
+
if (p.content !== undefined && typeof p.content !== 'string') fail('提示词 content 必须是文本');
|
|
44
|
+
if (p.enabled !== undefined && typeof p.enabled !== 'boolean') fail('提示词 enabled 必须是布尔值');
|
|
45
|
+
map.set(p.identifier,p);
|
|
46
|
+
}
|
|
47
|
+
const seen = new Set(); const blocks = [], missing=[];
|
|
48
|
+
function add(p,enabled) {
|
|
49
|
+
if (seen.has(p.identifier)) return;
|
|
50
|
+
seen.add(p.identifier);
|
|
51
|
+
const block = { identifier:p.identifier, name:typeof p.name==='string'?p.name:p.identifier, content:p.content||'', role:p.role||'system', marker:p.marker===true,
|
|
52
|
+
injection_position:p.injection_position ?? 0, injection_trigger:p.injection_trigger ?? [], enabled:enabled===true };
|
|
53
|
+
const reason=presetBlockReason(block);
|
|
54
|
+
blocks.push({...block,enabled:enabled===true&&!reason,importWarning:presetBlockNotice(block)});
|
|
55
|
+
}
|
|
56
|
+
if (order) for (const item of order.order) {
|
|
57
|
+
if(!object(item)||typeof item.identifier!=='string'||(item.enabled!==undefined&&typeof item.enabled!=='boolean'))fail('提示词排列条目无效');
|
|
58
|
+
if (!map.has(item.identifier)) {
|
|
59
|
+
// Prompt Manager exports custom prompts without built-in runtime markers.
|
|
60
|
+
if(format==='prompt-manager'){missing.push(item.identifier);continue;}
|
|
61
|
+
fail(`排列引用了不存在的条目:${item.identifier}`);
|
|
62
|
+
}
|
|
63
|
+
add(map.get(item.identifier),item.enabled??map.get(item.identifier).enabled??true);
|
|
64
|
+
}
|
|
65
|
+
for (const p of data.prompts) add(p,order?false:p.enabled!==false);
|
|
66
|
+
const warnings=['导入后整体停用,核对后启用并保存。启用条目的正文按当前列表顺序加入写作要求。', '仅使用提示词内容;酒馆消息角色、触发、插入深度及装填规则不执行。模型和采样参数由 DSH 管理,原始文件随备份保留。'];
|
|
67
|
+
if(blocks.some(b=>/\{\{|\}\}/.test(b.content)))warnings.push('宏与占位符按原文保留,不执行。需要替换角色名或用户称谓时,请直接编辑提示词内容。');
|
|
68
|
+
if(missing.length)warnings.push(`排列中有 ${missing.length} 项未随文件导出,已跳过这些引用;未编造其提示词内容。`);
|
|
69
|
+
const result={name:typeof input.name==='string'&&input.name.trim()?input.name:typeof data.name==='string'&&data.name.trim()?data.name:'导入的写作预设',enabled:false,blocks,raw:input,importFormat:format,warnings};
|
|
70
|
+
validatePreset(result);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
export function validatePreset(value) {
|
|
74
|
+
if (JSON.stringify(value)?.length > 2_000_000) fail('预设总大小超过 2 MB');
|
|
75
|
+
if (!object(value) || typeof value.name!=='string' || !value.name.trim() || typeof value.enabled!=='boolean' || !Array.isArray(value.blocks) || value.blocks.length>500) fail('预设格式无效');
|
|
76
|
+
if(value.importFormat!==undefined&&!['chat-completion','prompt-manager','system-prompt'].includes(value.importFormat))fail('预设导入格式标识无效');
|
|
77
|
+
const ids=new Set();let length=0;
|
|
78
|
+
for (const block of value.blocks) {
|
|
79
|
+
if (!object(block)||typeof block.identifier!=='string'||ids.has(block.identifier)||typeof block.content!=='string'||typeof block.name!=='string'||typeof block.enabled!=='boolean') fail('预设条目无效或标识重复');
|
|
80
|
+
ids.add(block.identifier);length+=block.content.length;
|
|
81
|
+
}
|
|
82
|
+
if(length>100000) fail('提示词总长度不能超过 100000 字符');
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
export function compilePreset(preset) {
|
|
86
|
+
if (!preset?.enabled) return '';
|
|
87
|
+
validatePreset(preset);
|
|
88
|
+
return preset.blocks.filter(block=>block.enabled&&block.content.trim()).map(block=>block.content).join('\n\n');
|
|
89
|
+
}
|