@xia-sc/dsh-cc-studio 0.3.2 → 0.3.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/README.md +14 -12
- package/lib/agent.js +614 -614
- package/lib/client.js +1 -3
- package/package.json +1 -2
- package/presets/cc/agent.cordis.yml +20 -0
- package/presets/cc/preset.yml +2 -2
- package/prototypes/index.html +0 -11
- package/prototypes/prototype-a.html +0 -246
- package/prototypes/prototype-b.html +0 -175
package/lib/agent.js
CHANGED
|
@@ -1,614 +1,614 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @xia-sc/dsh-cc-studio/agent — CC 模式的 Agent Tools(仅在 CC preset 挂载,合并自 dsh-cc-agent)。
|
|
3
|
-
* 通过 globalThis.__CC_DRAFTS__ 与 host 共享草稿,让 LLM 的工具调用能实时反映到浏览器胶囊。
|
|
4
|
-
* 强制工作流:cc_get_card → cc_patch_character → cc_patch_world(≥3维,autoLorebook) → cc_add_lorebook_entries(≥5条) → cc_patch_greetings → cc_validate
|
|
5
|
-
*/
|
|
6
|
-
import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
7
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
const name = "dsh-cc-studio-agent";
|
|
11
|
-
const inject = ["tools", "agents"];
|
|
12
|
-
|
|
13
|
-
function nowSec(){ return Math.floor(Date.now()/1000); }
|
|
14
|
-
function draftsMap(){ if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map(); return globalThis.__CC_DRAFTS__; }
|
|
15
|
-
// —— 草稿持久化(fix #2):与 host 半共享 ~/.dsh/cc-drafts,变更即落盘,重启后恢复 ——
|
|
16
|
-
if(!globalThis.__CC_DRAFT_META__) globalThis.__CC_DRAFT_META__ = new Map();
|
|
17
|
-
function draftDir(){
|
|
18
|
-
try{ return join(homedir(), ".dsh", "cc-drafts"); }catch{ return join(process.cwd(), ".cc-drafts"); }
|
|
19
|
-
}
|
|
20
|
-
function safeDraftFile(key){
|
|
21
|
-
const raw = String(key || "default");
|
|
22
|
-
let h = 0;
|
|
23
|
-
for(let i=0;i<raw.length;i++){ h = (Math.imul(h, 31) + raw.charCodeAt(i)) | 0; }
|
|
24
|
-
const hex = (h >>> 0).toString(16).padStart(8, "0");
|
|
25
|
-
const safe = (raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 48)) || "default";
|
|
26
|
-
return safe + "-" + hex + ".json";
|
|
27
|
-
}
|
|
28
|
-
function draftPath(key){ return join(draftDir(), safeDraftFile(key)); }
|
|
29
|
-
function loadPersistedDraftSync(key){
|
|
30
|
-
try{
|
|
31
|
-
const raw = readFileSync(draftPath(key), "utf8");
|
|
32
|
-
const obj = JSON.parse(raw);
|
|
33
|
-
const d = obj && obj.draft ? obj.draft : null;
|
|
34
|
-
if(d && typeof d === "object" && d.spec === "chara_card_v3" && d.data && typeof d.data === "object") return d;
|
|
35
|
-
}catch{}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
function persistDraftSync(key, draft){
|
|
39
|
-
try{
|
|
40
|
-
mkdirSync(draftDir(), { recursive: true });
|
|
41
|
-
writeFileSync(draftPath(key), JSON.stringify({ key: String(key), updatedAt: nowSec(), draft }, null, 2), "utf8");
|
|
42
|
-
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft failed:", (e && e.message) || e); }catch{} }
|
|
43
|
-
}
|
|
44
|
-
function draftMeta(key){
|
|
45
|
-
const k = String(key || "default");
|
|
46
|
-
if(!globalThis.__CC_DRAFT_META__.has(k)) globalThis.__CC_DRAFT_META__.set(k, { isNew: false, recovered: false });
|
|
47
|
-
return globalThis.__CC_DRAFT_META__.get(k);
|
|
48
|
-
}
|
|
49
|
-
function draftStatusFor(key, draft){
|
|
50
|
-
const m = draftMeta(key);
|
|
51
|
-
return { isNew: !!m.isNew, recovered: !!m.recovered, creation_date: (draft && draft.data) ? draft.data.creation_date : null, key: String(key) };
|
|
52
|
-
}
|
|
53
|
-
function freshNotice(meta){
|
|
54
|
-
if(meta && meta.isNew) return "⚠️ 当前会话此前无草稿(可能为 host 重启后的新会话,或全新会话 id),已新建空白草稿。若此前有未 ★ 保存到侧边栏的内容可能已丢失,请先确认;如需继续旧卡请调 cc_list_library/cc_load_from_library。";
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
function recoveredNotice(key, draft){
|
|
58
|
-
const m = draftMeta(key);
|
|
59
|
-
if(m && m.recovered) return "已从磁盘持久化恢复草稿(host 重启后自动恢复),creation_date=" + ((draft && draft.data && draft.data.creation_date) || "未知") + "。";
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
62
|
-
function keyFor(ctx, exec){
|
|
63
|
-
try{
|
|
64
|
-
if(exec && exec.agent){
|
|
65
|
-
const a=exec.agent;
|
|
66
|
-
if(a.session && a.session.id) return String(a.session.id);
|
|
67
|
-
if(a.id) return String(a.id);
|
|
68
|
-
if(a.sessionId) return String(a.sessionId);
|
|
69
|
-
}
|
|
70
|
-
}catch{}
|
|
71
|
-
try{
|
|
72
|
-
const agents = ctx.get("agents");
|
|
73
|
-
if(agents && agents.currentInitiator){
|
|
74
|
-
const cur = agents.currentInitiator();
|
|
75
|
-
if(cur){
|
|
76
|
-
if(cur.session && cur.session.id) return String(cur.session.id);
|
|
77
|
-
if(cur.id) return String(cur.id);
|
|
78
|
-
if(cur.sessionId) return String(cur.sessionId);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}catch{}
|
|
82
|
-
return "default";
|
|
83
|
-
}
|
|
84
|
-
function getDraft(ctx, exec){
|
|
85
|
-
const m = draftsMap();
|
|
86
|
-
const k = keyFor(ctx, exec);
|
|
87
|
-
if(!m.has(k)){
|
|
88
|
-
// fix #2:内存 miss 时先尝试从磁盘恢复,而非静默建空
|
|
89
|
-
const restored = loadPersistedDraftSync(k);
|
|
90
|
-
if(restored){
|
|
91
|
-
m.set(k, restored);
|
|
92
|
-
const meta = draftMeta(k); meta.isNew = false; meta.recovered = true;
|
|
93
|
-
} else {
|
|
94
|
-
m.set(k, {
|
|
95
|
-
spec:"chara_card_v3", spec_version:"3.0",
|
|
96
|
-
data:{
|
|
97
|
-
name:"", nickname:"", tags:[], creator:"", character_version:"0.1",
|
|
98
|
-
description:"", personality:"", scenario:"", system_prompt:"", post_history_instructions:"",
|
|
99
|
-
first_mes:"", alternate_greetings:[], group_only_greetings:[], mes_example:"",
|
|
100
|
-
creator_notes:"", creator_notes_multilingual:{}, source:[], assets:[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}],
|
|
101
|
-
creation_date: nowSec(), modification_date: nowSec(),
|
|
102
|
-
character_book:{ name:"", description:"", scan_depth:4, token_budget:1200, recursive_scanning:false, extensions:{}, entries:[] }
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
const meta = draftMeta(k); meta.isNew = true; meta.recovered = false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return m.get(k);
|
|
109
|
-
}
|
|
110
|
-
function setDraft(ctx, draft, exec){
|
|
111
|
-
const m = draftsMap();
|
|
112
|
-
const k = keyFor(ctx, exec);
|
|
113
|
-
draft.data.modification_date = nowSec();
|
|
114
|
-
m.set(k, draft);
|
|
115
|
-
const meta = draftMeta(k); meta.isNew = false; meta.recovered = false;
|
|
116
|
-
persistDraftSync(k, draft);
|
|
117
|
-
return draft;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function libDir(){
|
|
121
|
-
try{ return join(homedir(), ".dsh", "cc-library"); }catch{ return join(process.cwd(), ".cc-library"); }
|
|
122
|
-
}
|
|
123
|
-
async function ensureLibDir(){
|
|
124
|
-
const dir=libDir();
|
|
125
|
-
await mkdir(dir,{recursive:true});
|
|
126
|
-
return dir;
|
|
127
|
-
}
|
|
128
|
-
function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
129
|
-
const data=draft&&draft.data?draft.data:{};
|
|
130
|
-
const now=nowSec();
|
|
131
|
-
const loreCount=Array.isArray(data.character_book?.entries)?data.character_book.entries.length:0;
|
|
132
|
-
const tags=Array.isArray(data.tags)?data.tags.slice(0,6).map(String):[];
|
|
133
|
-
return {
|
|
134
|
-
id:String(id),
|
|
135
|
-
name:(data.name && String(data.name).trim()) || (existingMeta && existingMeta.name) || "未命名角色",
|
|
136
|
-
loreCount, tags,
|
|
137
|
-
descriptionSnippet:String(data.description||"").slice(0,80),
|
|
138
|
-
createdAt: existingMeta && existingMeta.createdAt ? existingMeta.createdAt : now,
|
|
139
|
-
updatedAt: now,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
async function listLibraryEntries(){
|
|
143
|
-
const dir=await ensureLibDir();
|
|
144
|
-
let files=[];
|
|
145
|
-
try{ files=await readdir(dir); }catch{ return []; }
|
|
146
|
-
const entries=[];
|
|
147
|
-
for(const f of files){
|
|
148
|
-
if(!f.endsWith(".json")) continue;
|
|
149
|
-
const pth=join(dir,f);
|
|
150
|
-
try{
|
|
151
|
-
const raw=await readFile(pth,"utf8");
|
|
152
|
-
const obj=JSON.parse(raw);
|
|
153
|
-
if(obj && obj.meta && obj.id) entries.push(obj.meta);
|
|
154
|
-
else if(obj && obj.draft){
|
|
155
|
-
const id=f.replace(/\.json$/,"");
|
|
156
|
-
entries.push(makeLibraryEntryMeta(obj.draft,null,id));
|
|
157
|
-
}
|
|
158
|
-
}catch{}
|
|
159
|
-
}
|
|
160
|
-
entries.sort((a,b)=>(b.updatedAt||0)-(a.updatedAt||0));
|
|
161
|
-
return entries;
|
|
162
|
-
}
|
|
163
|
-
async function saveLibraryEntry(draft, opts){
|
|
164
|
-
const dir=await ensureLibDir();
|
|
165
|
-
let id=opts && opts.id ? String(opts.id) : "";
|
|
166
|
-
let existing=null;
|
|
167
|
-
if(id){
|
|
168
|
-
try{ const raw=await readFile(join(dir,id+".json"),"utf8"); existing=JSON.parse(raw);}catch{}
|
|
169
|
-
}
|
|
170
|
-
if(!id) id=String(Date.now())+"-"+Math.random().toString(36).slice(2,6);
|
|
171
|
-
const meta=makeLibraryEntryMeta(draft, existing?existing.meta:null, id);
|
|
172
|
-
if(opts && typeof opts.name==="string" && opts.name.trim()) meta.name=String(opts.name).trim().slice(0,60);
|
|
173
|
-
const payload={id,meta,draft};
|
|
174
|
-
await writeFile(join(dir,id+".json"), JSON.stringify(payload,null,2), "utf8");
|
|
175
|
-
return {id,meta,draft};
|
|
176
|
-
}
|
|
177
|
-
async function loadLibraryEntry(id){
|
|
178
|
-
const dir=await ensureLibDir();
|
|
179
|
-
const raw=await readFile(join(dir,String(id)+".json"),"utf8");
|
|
180
|
-
const obj=JSON.parse(raw);
|
|
181
|
-
if(!obj || !obj.draft) throw new Error("文件格式错误");
|
|
182
|
-
return obj;
|
|
183
|
-
}
|
|
184
|
-
async function deleteLibraryEntry(id){
|
|
185
|
-
const dir=await ensureLibDir();
|
|
186
|
-
await unlink(join(dir,String(id)+".json"));
|
|
187
|
-
}
|
|
188
|
-
async function renameLibraryEntry(id,newName){
|
|
189
|
-
const dir=await ensureLibDir();
|
|
190
|
-
const pth=join(dir,String(id)+".json");
|
|
191
|
-
const raw=await readFile(pth,"utf8");
|
|
192
|
-
const obj=JSON.parse(raw);
|
|
193
|
-
if(!obj || !obj.meta) throw new Error("文件格式错误");
|
|
194
|
-
const name=String(newName||"").trim().slice(0,60);
|
|
195
|
-
if(!name) throw new Error("name 不能为空");
|
|
196
|
-
obj.meta.name=name;
|
|
197
|
-
obj.meta.updatedAt=nowSec();
|
|
198
|
-
await writeFile(pth, JSON.stringify(obj,null,2), "utf8");
|
|
199
|
-
return obj.meta;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function out(schema){ return { schema, render: (args, result) => [{ type:"text", text: JSON.stringify(result, null, 2) }] }; }
|
|
203
|
-
|
|
204
|
-
// —— 强制工作流状态计算 ——
|
|
205
|
-
function workflowStatus(draft){
|
|
206
|
-
const d = draft && draft.data ? draft.data : {};
|
|
207
|
-
const w = d.extensions && d.extensions.cc_world ? d.extensions.cc_world : {};
|
|
208
|
-
const loreCount = Array.isArray(d.character_book?.entries) ? d.character_book.entries.length : 0;
|
|
209
|
-
const worldFilled = ["timeline","factions","geo","power","daily"].filter(k => typeof w[k]==="string" && String(w[k]).trim().length>=6).length;
|
|
210
|
-
const characterOk = !!(String(d.name||"").trim() && String(d.description||"").trim() && String(d.personality||"").trim() && String(d.scenario||"").trim());
|
|
211
|
-
const characterPartial = !!(String(d.name||"").trim() && String(d.description||"").trim());
|
|
212
|
-
const worldOk = worldFilled >= 3;
|
|
213
|
-
const loreOk = loreCount >= 5;
|
|
214
|
-
const greetingsOk = !!(String(d.first_mes||"").trim() && Array.isArray(d.group_only_greetings) && d.group_only_greetings.length>=1 && Array.isArray(d.alternate_greetings) && d.alternate_greetings.length>=2);
|
|
215
|
-
const greetingsPartial = !!(String(d.first_mes||"").trim() || (Array.isArray(d.group_only_greetings) && d.group_only_greetings.length>=1));
|
|
216
|
-
let next = "";
|
|
217
|
-
let step = 1;
|
|
218
|
-
if(!characterOk){
|
|
219
|
-
step = 2; next = "下一步必须 cc_patch_character:先与用户讨论角色定位(气质/关系张力等,1-2问)再补齐 name/description/personality/scenario,严禁直接推断填满";
|
|
220
|
-
} else if(!worldOk){
|
|
221
|
-
step = 3; next = "角色基础已齐,下一步必须 cc_patch_world:先与用户讨论世界观侧重(哪几维最想展开/有无私设)再补 5 维(≥3项)";
|
|
222
|
-
} else if(!loreOk){
|
|
223
|
-
step = 4; next = "五维已齐,下一步必须 cc_add_lorebook_entries:与用户讨论触发词/常驻偏好后补到≥5 条(各 1,至少 1 constant,@@decorator),当前 "+loreCount+" 条";
|
|
224
|
-
} else if(!greetingsOk){
|
|
225
|
-
step = 5; next = "世界书已齐("+loreCount+" 条),下一步必须 cc_patch_greetings:先问用户偏好场景/关系阶段再补问候语(first_mes + alternate≥2 + group≥1)";
|
|
226
|
-
} else {
|
|
227
|
-
step = 6; next = "全部已齐,下一步必须 cc_validate 校验,valid=true 才可收尾,提醒用户在工坊“导出 JSON”或“★ 保存到侧边栏”";
|
|
228
|
-
}
|
|
229
|
-
const hint = "工作流 "+step+"/6" + (characterOk?" ✓角色":" ✗角色") + (worldOk?" ✓五维("+worldFilled+"/5)":" ✗五维("+worldFilled+"/5)") + (loreOk?" ✓世界书("+loreCount+")":" ✗世界书("+loreCount+"/5)") + (greetingsOk?" ✓问候语":" ✗问候语") + " → "+next;
|
|
230
|
-
return { characterOk, characterPartial, worldOk, worldFilled, loreCount, loreOk, greetingsOk, greetingsPartial, step, next, hint };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function apply(ctx){
|
|
234
|
-
ctx.tools.register({
|
|
235
|
-
name: "cc_get_card",
|
|
236
|
-
description: "【工作流第1步·必调·先问再填】查看当前 CC 角色卡草稿与工作流进度。CC 模式下必须先调此工具,然后用自然语言向用户总结当前进度(角色/五维/世界书/问候语各缺什么),并用 1-2 个开放问题邀请用户表达偏好,再按步推进。严禁未与用户讨论就直接推断填满。",
|
|
237
|
-
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
238
|
-
output: out({ type:"object", additionalProperties:true, properties:{ draft:{type:"object"}, hint:{type:"string"}, loreCount:{type:"number"}, workflow:{type:"object"}, next:{type:"string"} } }),
|
|
239
|
-
async execute(args, exec){
|
|
240
|
-
const draft = getDraft(ctx, exec);
|
|
241
|
-
const k = keyFor(ctx, exec);
|
|
242
|
-
const st = draftStatusFor(k, draft);
|
|
243
|
-
const wf = workflowStatus(draft);
|
|
244
|
-
const fresh = freshNotice(draftMeta(k));
|
|
245
|
-
const rec = recoveredNotice(k, draft);
|
|
246
|
-
const notice = [rec, fresh].filter(Boolean).join(" ");
|
|
247
|
-
return { draft, draftStatus: st, workflow: wf, hint: wf.hint, next: wf.next, loreCount: wf.loreCount, notice };
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
ctx.tools.register({
|
|
252
|
-
name: "cc_patch_character",
|
|
253
|
-
description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world。",
|
|
254
|
-
parameters: {
|
|
255
|
-
type:"object",
|
|
256
|
-
properties:{
|
|
257
|
-
name:{type:"string", description:"角色显示名(data.name)"},
|
|
258
|
-
nickname:{type:"string", description:"nickname({{char}} 用)"},
|
|
259
|
-
description:{type:"string", description:"description 设定"},
|
|
260
|
-
personality:{type:"string", description:"personality"},
|
|
261
|
-
scenario:{type:"string", description:"scenario"},
|
|
262
|
-
system_prompt:{type:"string", description:"system_prompt"},
|
|
263
|
-
post_history_instructions:{type:"string", description:"post_history_instructions"},
|
|
264
|
-
tags:{type:"array", items:{type:"string"}, description:"标签数组,会整体覆盖"},
|
|
265
|
-
creator_notes:{type:"string", description:"creator_notes"}
|
|
266
|
-
},
|
|
267
|
-
additionalProperties:false
|
|
268
|
-
},
|
|
269
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
270
|
-
async execute(args, exec){
|
|
271
|
-
const k = keyFor(ctx, exec);
|
|
272
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
273
|
-
const wasFresh = draftMeta(k).isNew;
|
|
274
|
-
const recNote = recoveredNotice(k, cur);
|
|
275
|
-
if(args.name!==undefined) cur.data.name = String(args.name).slice(0,120);
|
|
276
|
-
if(args.nickname!==undefined) cur.data.nickname = String(args.nickname).slice(0,120);
|
|
277
|
-
if(args.description!==undefined) cur.data.description = String(args.description).slice(0,5000);
|
|
278
|
-
if(args.personality!==undefined) cur.data.personality = String(args.personality).slice(0,5000);
|
|
279
|
-
if(args.scenario!==undefined) cur.data.scenario = String(args.scenario).slice(0,5000);
|
|
280
|
-
if(args.system_prompt!==undefined) cur.data.system_prompt = String(args.system_prompt).slice(0,3000);
|
|
281
|
-
if(args.post_history_instructions!==undefined) cur.data.post_history_instructions = String(args.post_history_instructions).slice(0,3000);
|
|
282
|
-
if(Array.isArray(args.tags)) cur.data.tags = args.tags.map(String).slice(0,12);
|
|
283
|
-
if(args.creator_notes!==undefined) cur.data.creator_notes = String(args.creator_notes).slice(0,3000);
|
|
284
|
-
setDraft(ctx, cur, exec);
|
|
285
|
-
const wf = workflowStatus(cur);
|
|
286
|
-
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
287
|
-
const recPrefix = recNote ? recNote + " " : "";
|
|
288
|
-
return { ok:true, draft: cur, draftStatus: draftStatusFor(k, cur), workflow: wf, next: wf.next, notice: (recPrefix + warnFresh).trim(), message: recPrefix + "已写入胶囊(角色基础)," + wf.next + warnFresh };
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
ctx.tools.register({
|
|
293
|
-
name: "cc_patch_world",
|
|
294
|
-
description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。调后下一步是 cc_add_lorebook_entries。",
|
|
295
|
-
parameters: {
|
|
296
|
-
type:"object",
|
|
297
|
-
properties:{
|
|
298
|
-
timeline:{type:"string", description:"年表"},
|
|
299
|
-
factions:{type:"string", description:"势力"},
|
|
300
|
-
geo:{type:"string", description:"地理"},
|
|
301
|
-
power:{type:"string", description:"力量体系"},
|
|
302
|
-
daily:{type:"string", description:"日常"},
|
|
303
|
-
autoLorebook:{type:"boolean", description:"是否基于 5 维自动生成 3-6 条带 @@decorator 的 lorebook 条目,强烈建议 true"}
|
|
304
|
-
},
|
|
305
|
-
additionalProperties:false
|
|
306
|
-
},
|
|
307
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
308
|
-
async execute(args, exec){
|
|
309
|
-
const k = keyFor(ctx, exec);
|
|
310
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
311
|
-
const wasFresh = draftMeta(k).isNew;
|
|
312
|
-
const recNote = recoveredNotice(k, cur);
|
|
313
|
-
cur.data.extensions = cur.data.extensions || {};
|
|
314
|
-
const w = cur.data.extensions.cc_world || {};
|
|
315
|
-
if(args.timeline!==undefined) w.timeline = String(args.timeline).slice(0,1000);
|
|
316
|
-
if(args.factions!==undefined) w.factions = String(args.factions).slice(0,1000);
|
|
317
|
-
if(args.geo!==undefined) w.geo = String(args.geo).slice(0,1000);
|
|
318
|
-
if(args.power!==undefined) w.power = String(args.power).slice(0,1000);
|
|
319
|
-
if(args.daily!==undefined) w.daily = String(args.daily).slice(0,1000);
|
|
320
|
-
cur.data.extensions.cc_world = w;
|
|
321
|
-
const shouldAuto = args.autoLorebook !== false; // 默认 true,防偷懒
|
|
322
|
-
if(shouldAuto){
|
|
323
|
-
// 修复 #1:移除先前由 autoLorebook 生成的旧条目,避免重复与过时(保留手动条目)
|
|
324
|
-
// 兼容旧数据(无标记时按固定名匹配),新数据按 cc_auto 标记精确匹配
|
|
325
|
-
const autoNames = new Set(["年表","势力","地理","力量体系","日常"]);
|
|
326
|
-
cur.data.character_book.entries = cur.data.character_book.entries.filter(e => {
|
|
327
|
-
if(e.cc_auto === true) return false;
|
|
328
|
-
if(e.extensions && e.extensions.cc_auto) return false;
|
|
329
|
-
if(autoNames.has(e.name)) return false;
|
|
330
|
-
return true;
|
|
331
|
-
});
|
|
332
|
-
// 重新计算 insertion_order 保持连续(手动条目保留相对顺序)
|
|
333
|
-
cur.data.character_book.entries.forEach((e, i) => e.insertion_order = i);
|
|
334
|
-
const push=(name, keys, content, priority, constant=false)=>{
|
|
335
|
-
cur.data.character_book.entries.push({ keys, content, enabled:true, insertion_order: cur.data.character_book.entries.length, case_sensitive:false, use_regex:false, constant, name, priority, id: Date.now()+Math.floor(Math.random()*10000), cc_auto:true, extensions:{cc_auto:true} });
|
|
336
|
-
};
|
|
337
|
-
if(w.timeline) push("年表", ["年表","timeline"], "@@position after_desc\n"+w.timeline, 10);
|
|
338
|
-
if(w.factions) push("势力", ["势力","faction"], "@@depth 4\n"+w.factions, 9);
|
|
339
|
-
if(w.geo) push("地理", ["地理","geography"], "@@position scenario\n"+w.geo, 8);
|
|
340
|
-
if(w.power) push("力量体系", ["力量","power"], "@@activate\n"+w.power, 9, true);
|
|
341
|
-
if(w.daily) push("日常", ["日常","daily"], "@@depth 5\n"+w.daily, 7);
|
|
342
|
-
}
|
|
343
|
-
setDraft(ctx, cur, exec);
|
|
344
|
-
const wf = workflowStatus(cur);
|
|
345
|
-
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
346
|
-
const recPrefix = recNote ? recNote + " " : "";
|
|
347
|
-
return { ok:true, loreCount: cur.data.character_book.entries.length, draft: cur, draftStatus: draftStatusFor(k, cur), workflow:wf, next: wf.next, notice: (recPrefix + warnFresh).trim(), message: recPrefix + "已写入五维("+wf.worldFilled+"/5),"+wf.next + warnFresh };
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
ctx.tools.register({
|
|
352
|
-
name: "cc_add_lorebook_entries",
|
|
353
|
-
description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator)。调用前应与用户讨论:问用户想要哪些触发词/常驻条目/要藏什么彩蛋,拿到偏好后再生成。必须在五维之后、问候语之前调用,禁止一次性无讨论批量生成。",
|
|
354
|
-
parameters: {
|
|
355
|
-
type:"object",
|
|
356
|
-
properties:{
|
|
357
|
-
entries:{
|
|
358
|
-
type:"array", minItems:1, maxItems:5,
|
|
359
|
-
items:{
|
|
360
|
-
type:"object",
|
|
361
|
-
properties:{
|
|
362
|
-
name:{type:"string", description:"条目标题"},
|
|
363
|
-
keys:{type:"array", items:{type:"string"}, description:"触发关键词,为空且 constant=true 时为常驻条目"},
|
|
364
|
-
content:{type:"string", description:"正文,可在开头写 @@decorator 行"},
|
|
365
|
-
constant:{type:"boolean", description:"是否为常驻条目"},
|
|
366
|
-
use_regex:{type:"boolean", description:"keys 是否按正则匹配"},
|
|
367
|
-
priority:{type:"number", description:"优先级 0-10"}
|
|
368
|
-
},
|
|
369
|
-
required:["content"],
|
|
370
|
-
additionalProperties:false
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
required:["entries"],
|
|
375
|
-
additionalProperties:false
|
|
376
|
-
},
|
|
377
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
378
|
-
async execute(args, exec){
|
|
379
|
-
const k = keyFor(ctx, exec);
|
|
380
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
381
|
-
const wasFresh = draftMeta(k).isNew;
|
|
382
|
-
const recNote = recoveredNotice(k, cur);
|
|
383
|
-
args.entries.forEach(function(e){
|
|
384
|
-
cur.data.character_book.entries.push({
|
|
385
|
-
keys: Array.isArray(e.keys)? e.keys.map(String): [],
|
|
386
|
-
content: String(e.content).slice(0,5000),
|
|
387
|
-
enabled:true,
|
|
388
|
-
insertion_order: cur.data.character_book.entries.length,
|
|
389
|
-
case_sensitive:false,
|
|
390
|
-
use_regex: !!e.use_regex,
|
|
391
|
-
constant: !!e.constant,
|
|
392
|
-
name: String(e.name||"未命名").slice(0,60),
|
|
393
|
-
priority: typeof e.priority==="number"? e.priority: 5,
|
|
394
|
-
id: Date.now()+Math.floor(Math.random()*10000)
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
setDraft(ctx, cur, exec);
|
|
398
|
-
const wf = workflowStatus(cur);
|
|
399
|
-
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
400
|
-
const recPrefix = recNote ? recNote + " " : "";
|
|
401
|
-
return { ok:true, added: args.entries.length, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
ctx.tools.register({
|
|
406
|
-
name: "cc_delete_lorebook_entries",
|
|
407
|
-
description: "【Lorebook 管理】按 id 或 name 删除条目(用于清理过时条目,autoLorebook 已自动覆盖旧自动条目,手动条目需显式删除)。至少提供 ids 或 names 之一。",
|
|
408
|
-
parameters: {
|
|
409
|
-
type:"object",
|
|
410
|
-
properties:{
|
|
411
|
-
ids:{type:"array", items:{type:"number"}, description:"要删除的条目 id 列表"},
|
|
412
|
-
names:{type:"array", items:{type:"string"}, description:"要删除的条目 name 列表(精确匹配)"}
|
|
413
|
-
},
|
|
414
|
-
additionalProperties:false
|
|
415
|
-
},
|
|
416
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
417
|
-
async execute(args, exec){
|
|
418
|
-
const k = keyFor(ctx, exec);
|
|
419
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
420
|
-
const before = draftStatusFor(k, cur);
|
|
421
|
-
const beforeRec = recoveredNotice(k, cur);
|
|
422
|
-
const beforeFresh = before.isNew;
|
|
423
|
-
const beforeCount = cur.data.character_book.entries.length;
|
|
424
|
-
const idSet = new Set(Array.isArray(args.ids)? args.ids.map(Number): []);
|
|
425
|
-
const nameSet = new Set(Array.isArray(args.names)? args.names.map(String): []);
|
|
426
|
-
if(idSet.size===0 && nameSet.size===0) return { ok:false, error:"请提供 ids 或 names" };
|
|
427
|
-
cur.data.character_book.entries = cur.data.character_book.entries.filter(e => !idSet.has(e.id) && !nameSet.has(e.name));
|
|
428
|
-
// 重排 insertion_order
|
|
429
|
-
cur.data.character_book.entries.forEach((e,i)=> e.insertion_order=i);
|
|
430
|
-
setDraft(ctx, cur, exec);
|
|
431
|
-
const wf = workflowStatus(cur);
|
|
432
|
-
const warnFresh = beforeFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
433
|
-
const recPrefix = beforeRec ? beforeRec + " " : "";
|
|
434
|
-
return { ok:true, deleted: beforeCount - cur.data.character_book.entries.length, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
ctx.tools.register({
|
|
439
|
-
name: "cc_update_lorebook_entry",
|
|
440
|
-
description: "【Lorebook 管理】按 id 更新单条条目(content/keys/name 等),用于修正过时内容而非整批重建。",
|
|
441
|
-
parameters: {
|
|
442
|
-
type:"object",
|
|
443
|
-
properties:{
|
|
444
|
-
id:{type:"number", description:"要更新的条目 id(必填)"},
|
|
445
|
-
name:{type:"string", description:"新标题"},
|
|
446
|
-
keys:{type:"array", items:{type:"string"}, description:"新触发词"},
|
|
447
|
-
content:{type:"string", description:"新正文(含 @@decorator)"},
|
|
448
|
-
enabled:{type:"boolean"},
|
|
449
|
-
constant:{type:"boolean"},
|
|
450
|
-
use_regex:{type:"boolean"},
|
|
451
|
-
priority:{type:"number"}
|
|
452
|
-
},
|
|
453
|
-
required:["id"],
|
|
454
|
-
additionalProperties:false
|
|
455
|
-
},
|
|
456
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
457
|
-
async execute(args, exec){
|
|
458
|
-
const k = keyFor(ctx, exec);
|
|
459
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
460
|
-
const wasFresh = draftMeta(k).isNew;
|
|
461
|
-
const recNote = recoveredNotice(k, cur);
|
|
462
|
-
const e = cur.data.character_book.entries.find(x=> x.id===args.id);
|
|
463
|
-
if(!e) return { ok:false, error:"未找到 id="+args.id };
|
|
464
|
-
if(args.name!==undefined) e.name=String(args.name).slice(0,60);
|
|
465
|
-
if(Array.isArray(args.keys)) e.keys=args.keys.map(String);
|
|
466
|
-
if(args.content!==undefined) e.content=String(args.content).slice(0,5000);
|
|
467
|
-
if(args.enabled!==undefined) e.enabled=!!args.enabled;
|
|
468
|
-
if(args.constant!==undefined) e.constant=!!args.constant;
|
|
469
|
-
if(args.use_regex!==undefined) e.use_regex=!!args.use_regex;
|
|
470
|
-
if(typeof args.priority==="number") e.priority=args.priority;
|
|
471
|
-
setDraft(ctx, cur, exec);
|
|
472
|
-
const wf = workflowStatus(cur);
|
|
473
|
-
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
474
|
-
const recPrefix = recNote ? recNote + " " : "";
|
|
475
|
-
return { ok:true, entry:e, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
ctx.tools.register({
|
|
480
|
-
name: "cc_patch_greetings",
|
|
481
|
-
description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)后调用。",
|
|
482
|
-
parameters: {
|
|
483
|
-
type:"object",
|
|
484
|
-
properties:{
|
|
485
|
-
first_mes:{type:"string", description:"首条开场白"},
|
|
486
|
-
alternate_greetings:{type:"array", items:{type:"string"}, description:"备选开场白数组,至少 2 条"},
|
|
487
|
-
group_only_greetings:{type:"array", items:{type:"string"}, description:"群聊限定开场白(必填至少1条)"},
|
|
488
|
-
mes_example:{type:"string", description:"对话示例(可用 {{user}}/{{char}})"}
|
|
489
|
-
},
|
|
490
|
-
additionalProperties:false
|
|
491
|
-
},
|
|
492
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
493
|
-
async execute(args, exec){
|
|
494
|
-
const k = keyFor(ctx, exec);
|
|
495
|
-
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
496
|
-
const wasFresh = draftMeta(k).isNew;
|
|
497
|
-
const recNote = recoveredNotice(k, cur);
|
|
498
|
-
if(args.first_mes!==undefined) cur.data.first_mes = String(args.first_mes).slice(0,5000);
|
|
499
|
-
if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(String).slice(0,10);
|
|
500
|
-
if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(String).slice(0,10);
|
|
501
|
-
if(args.mes_example!==undefined) cur.data.mes_example = String(args.mes_example).slice(0,5000);
|
|
502
|
-
setDraft(ctx, cur, exec);
|
|
503
|
-
const wf = workflowStatus(cur);
|
|
504
|
-
// 若世界书/五维未齐,警告但仍写入,让 validate 卡住
|
|
505
|
-
let warn = "";
|
|
506
|
-
if(!wf.loreOk) warn = " 警告:世界书仅 "+wf.loreCount+"/5 条,validate 会失败,请先补世界书。";
|
|
507
|
-
if(!wf.worldOk) warn += " 警告:五维仅 "+wf.worldFilled+"/5 项,validate 会失败。";
|
|
508
|
-
if(wasFresh) warn += " " + freshNotice({ isNew: true });
|
|
509
|
-
const recPrefix = recNote ? recNote + " " : "";
|
|
510
|
-
return { ok:true, draft: cur, draftStatus: draftStatusFor(k, cur), workflow:wf, next: wf.next, warning: warn, notice: (recPrefix + warn).trim(), message: recPrefix + "已写入问候语"+warn+"。"+wf.next };
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// —— 已存角色库 CRUD(ID 化,模型可直接管理,与工坊侧栏共享 ~/.dsh/cc-library) ——
|
|
515
|
-
ctx.tools.register({
|
|
516
|
-
name: "cc_list_library",
|
|
517
|
-
description: "【库·查】列出已存角色库所有条目(id/name/loreCount/tags/updatedAt),用于让用户挑选载入或让模型决定更新哪一张。",
|
|
518
|
-
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
519
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
520
|
-
async execute(args, exec){
|
|
521
|
-
const entries=await listLibraryEntries();
|
|
522
|
-
return { ok:true, entries, count:entries.length };
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
ctx.tools.register({
|
|
526
|
-
name: "cc_save_to_library",
|
|
527
|
-
description: "【库·增/改】将当前会话草稿按唯一 ID 存入已存库(ID 化 CRUD)。不传 id 则新建 ID;传 id 则按 ID 原地覆盖(更新),无需先删旧卡。模型可在用户说“保存/更新”时直接调用。",
|
|
528
|
-
parameters: { type:"object", properties:{ id:{type:"string", description:"要覆盖的已存 ID(不传则新建)"}, name:{type:"string", description:"覆盖 meta.name(可选,不影响 draft.data.name)"} }, additionalProperties:false },
|
|
529
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
530
|
-
async execute(args, exec){
|
|
531
|
-
const draft=JSON.parse(JSON.stringify(getDraft(ctx,exec)));
|
|
532
|
-
// 至少需要 name
|
|
533
|
-
if(!draft.data.name || !String(draft.data.name).trim()) return { ok:false, error:"当前草稿缺少 data.name,请先 cc_patch_character 补齐" };
|
|
534
|
-
const res=await saveLibraryEntry(draft,{id:args.id, name:args.name});
|
|
535
|
-
return { ok:true, id:res.id, meta:res.meta, loreCount:res.meta.loreCount };
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
ctx.tools.register({
|
|
539
|
-
name: "cc_load_from_library",
|
|
540
|
-
description: "【库·载入】按 ID 将已存卡载入当前会话草稿(会覆盖当前 draft),载入后再次保存将按此 ID 更新。",
|
|
541
|
-
parameters: { type:"object", properties:{ id:{type:"string", description:"已存条目 ID"} }, required:["id"], additionalProperties:false },
|
|
542
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
543
|
-
async execute(args, exec){
|
|
544
|
-
const obj=await loadLibraryEntry(args.id);
|
|
545
|
-
const cur=JSON.parse(JSON.stringify(obj.draft));
|
|
546
|
-
setDraft(ctx,cur,exec);
|
|
547
|
-
const k = keyFor(ctx, exec);
|
|
548
|
-
return { ok:true, id:obj.id, meta:obj.meta, draft:cur, draftStatus: draftStatusFor(k, cur), loreCount:obj.meta.loreCount };
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
ctx.tools.register({
|
|
552
|
-
name: "cc_delete_from_library",
|
|
553
|
-
description: "【库·删】按 ID 删除已存条目,模型可在用户确认后调用。",
|
|
554
|
-
parameters: { type:"object", properties:{ id:{type:"string", description:"要删除的 ID"} }, required:["id"], additionalProperties:false },
|
|
555
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
556
|
-
async execute(args, exec){
|
|
557
|
-
await deleteLibraryEntry(args.id);
|
|
558
|
-
return { ok:true, id:args.id };
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
ctx.tools.register({
|
|
562
|
-
name: "cc_rename_in_library",
|
|
563
|
-
description: "【库·改名】按 ID 重命名已存条目(仅改 meta.name,不改 draft 内容)。",
|
|
564
|
-
parameters: { type:"object", properties:{ id:{type:"string"}, name:{type:"string", description:"新名称"} }, required:["id","name"], additionalProperties:false },
|
|
565
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
566
|
-
async execute(args, exec){
|
|
567
|
-
const meta=await renameLibraryEntry(args.id,args.name);
|
|
568
|
-
return { ok:true, meta };
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
ctx.tools.register({
|
|
572
|
-
name: "cc_get_library_entry",
|
|
573
|
-
description: "【库·查单条】按 ID 获取单条完整草稿与 meta,用于模型在重命名/更新前确认内容。",
|
|
574
|
-
parameters: { type:"object", properties:{ id:{type:"string"} }, required:["id"], additionalProperties:false },
|
|
575
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
576
|
-
async execute(args, exec){
|
|
577
|
-
const obj=await loadLibraryEntry(args.id);
|
|
578
|
-
return { ok:true, id:obj.id, meta:obj.meta, draft:obj.draft };
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
ctx.tools.register({
|
|
583
|
-
name: "cc_validate",
|
|
584
|
-
description: "【工作流第6步·收口必调】校验 CC 角色卡(SPEC_V3 + 角色四件套/五维≥3/世界书≥5/问候语齐)。valid=false 按 errors 与用户讨论继续补;valid=true 时总结成果并邀请用户在工坊微调后“导出 JSON”或“★ 保存到侧边栏”。全程强调共创:多问用户、多给草案供挑选。",
|
|
585
|
-
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
586
|
-
output: out({ type:"object", additionalProperties:true }),
|
|
587
|
-
async execute(args, exec){
|
|
588
|
-
const d = getDraft(ctx, exec);
|
|
589
|
-
const k = keyFor(ctx, exec);
|
|
590
|
-
const st = draftStatusFor(k, d);
|
|
591
|
-
const fresh = freshNotice(draftMeta(k));
|
|
592
|
-
const rec = recoveredNotice(k, d);
|
|
593
|
-
const notice = [rec, fresh].filter(Boolean).join(" ");
|
|
594
|
-
const wf = workflowStatus(d);
|
|
595
|
-
const errors=[], warnings=[];
|
|
596
|
-
if(!String(d.data.name||"").trim()) errors.push("缺 data.name(角色名)");
|
|
597
|
-
if(!String(d.data.description||"").trim()) errors.push("缺 data.description");
|
|
598
|
-
if(!String(d.data.personality||"").trim()) errors.push("缺 data.personality");
|
|
599
|
-
if(!String(d.data.scenario||"").trim()) warnings.push("scenario 为空,建议补充");
|
|
600
|
-
if(!Array.isArray(d.data.group_only_greetings) || d.data.group_only_greetings.length<1) errors.push("缺 group_only_greetings(至少 1 条必填)");
|
|
601
|
-
if(!Array.isArray(d.data.alternate_greetings) || d.data.alternate_greetings.length<2) warnings.push("alternate_greetings 建议至少 2 条");
|
|
602
|
-
if(!String(d.data.first_mes||"").trim()) warnings.push("first_mes 为空");
|
|
603
|
-
if(!wf.worldOk) errors.push("五维世界观未齐:需 timeline/factions/geo/power/daily 至少 3 项(当前 "+wf.worldFilled+"/5),请调 cc_patch_world");
|
|
604
|
-
if(!wf.loreOk) errors.push("世界书未齐:需至少 5 条(当前 "+wf.loreCount+" 条),请调 cc_add_lorebook_entries 补到 5 条");
|
|
605
|
-
if(wf.loreCount>0 && !d.data.character_book.entries.some(e=>e.constant)) warnings.push("世界书建议至少 1 条 constant 常驻条目");
|
|
606
|
-
if(d.data.character_book.entries.length>0 && d.data.character_book.entries.every(e=> !String(e.content||"").includes("@@"))) warnings.push("世界书条目建议带 @@decorator(@@position/@@depth/@@activate)");
|
|
607
|
-
const valid = errors.length===0;
|
|
608
|
-
const hint = valid ? "校验通过 ✓ 可导出/保存到侧边栏" : "校验未通过,按 errors 逐项补齐(严禁跳步)";
|
|
609
|
-
return { valid, errors, warnings, draftStatus: st, notice, workflow:wf, next: wf.next, hint, loreCount: wf.loreCount, name: d.data.name };
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
export { apply, inject, name };
|
|
1
|
+
/**
|
|
2
|
+
* @xia-sc/dsh-cc-studio/agent — CC 模式的 Agent Tools(仅在 CC preset 挂载,合并自 dsh-cc-agent)。
|
|
3
|
+
* 通过 globalThis.__CC_DRAFTS__ 与 host 共享草稿,让 LLM 的工具调用能实时反映到浏览器胶囊。
|
|
4
|
+
* 强制工作流:cc_get_card → cc_patch_character → cc_patch_world(≥3维,autoLorebook) → cc_add_lorebook_entries(≥5条) → cc_patch_greetings → cc_validate
|
|
5
|
+
*/
|
|
6
|
+
import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
const name = "dsh-cc-studio-agent";
|
|
11
|
+
const inject = ["tools", "agents"];
|
|
12
|
+
|
|
13
|
+
function nowSec(){ return Math.floor(Date.now()/1000); }
|
|
14
|
+
function draftsMap(){ if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map(); return globalThis.__CC_DRAFTS__; }
|
|
15
|
+
// —— 草稿持久化(fix #2):与 host 半共享 ~/.dsh/cc-drafts,变更即落盘,重启后恢复 ——
|
|
16
|
+
if(!globalThis.__CC_DRAFT_META__) globalThis.__CC_DRAFT_META__ = new Map();
|
|
17
|
+
function draftDir(){
|
|
18
|
+
try{ return join(homedir(), ".dsh", "cc-drafts"); }catch{ return join(process.cwd(), ".cc-drafts"); }
|
|
19
|
+
}
|
|
20
|
+
function safeDraftFile(key){
|
|
21
|
+
const raw = String(key || "default");
|
|
22
|
+
let h = 0;
|
|
23
|
+
for(let i=0;i<raw.length;i++){ h = (Math.imul(h, 31) + raw.charCodeAt(i)) | 0; }
|
|
24
|
+
const hex = (h >>> 0).toString(16).padStart(8, "0");
|
|
25
|
+
const safe = (raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 48)) || "default";
|
|
26
|
+
return safe + "-" + hex + ".json";
|
|
27
|
+
}
|
|
28
|
+
function draftPath(key){ return join(draftDir(), safeDraftFile(key)); }
|
|
29
|
+
function loadPersistedDraftSync(key){
|
|
30
|
+
try{
|
|
31
|
+
const raw = readFileSync(draftPath(key), "utf8");
|
|
32
|
+
const obj = JSON.parse(raw);
|
|
33
|
+
const d = obj && obj.draft ? obj.draft : null;
|
|
34
|
+
if(d && typeof d === "object" && d.spec === "chara_card_v3" && d.data && typeof d.data === "object") return d;
|
|
35
|
+
}catch{}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function persistDraftSync(key, draft){
|
|
39
|
+
try{
|
|
40
|
+
mkdirSync(draftDir(), { recursive: true });
|
|
41
|
+
writeFileSync(draftPath(key), JSON.stringify({ key: String(key), updatedAt: nowSec(), draft }, null, 2), "utf8");
|
|
42
|
+
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft failed:", (e && e.message) || e); }catch{} }
|
|
43
|
+
}
|
|
44
|
+
function draftMeta(key){
|
|
45
|
+
const k = String(key || "default");
|
|
46
|
+
if(!globalThis.__CC_DRAFT_META__.has(k)) globalThis.__CC_DRAFT_META__.set(k, { isNew: false, recovered: false });
|
|
47
|
+
return globalThis.__CC_DRAFT_META__.get(k);
|
|
48
|
+
}
|
|
49
|
+
function draftStatusFor(key, draft){
|
|
50
|
+
const m = draftMeta(key);
|
|
51
|
+
return { isNew: !!m.isNew, recovered: !!m.recovered, creation_date: (draft && draft.data) ? draft.data.creation_date : null, key: String(key) };
|
|
52
|
+
}
|
|
53
|
+
function freshNotice(meta){
|
|
54
|
+
if(meta && meta.isNew) return "⚠️ 当前会话此前无草稿(可能为 host 重启后的新会话,或全新会话 id),已新建空白草稿。若此前有未 ★ 保存到侧边栏的内容可能已丢失,请先确认;如需继续旧卡请调 cc_list_library/cc_load_from_library。";
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
function recoveredNotice(key, draft){
|
|
58
|
+
const m = draftMeta(key);
|
|
59
|
+
if(m && m.recovered) return "已从磁盘持久化恢复草稿(host 重启后自动恢复),creation_date=" + ((draft && draft.data && draft.data.creation_date) || "未知") + "。";
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
function keyFor(ctx, exec){
|
|
63
|
+
try{
|
|
64
|
+
if(exec && exec.agent){
|
|
65
|
+
const a=exec.agent;
|
|
66
|
+
if(a.session && a.session.id) return String(a.session.id);
|
|
67
|
+
if(a.id) return String(a.id);
|
|
68
|
+
if(a.sessionId) return String(a.sessionId);
|
|
69
|
+
}
|
|
70
|
+
}catch{}
|
|
71
|
+
try{
|
|
72
|
+
const agents = ctx.get("agents");
|
|
73
|
+
if(agents && agents.currentInitiator){
|
|
74
|
+
const cur = agents.currentInitiator();
|
|
75
|
+
if(cur){
|
|
76
|
+
if(cur.session && cur.session.id) return String(cur.session.id);
|
|
77
|
+
if(cur.id) return String(cur.id);
|
|
78
|
+
if(cur.sessionId) return String(cur.sessionId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}catch{}
|
|
82
|
+
return "default";
|
|
83
|
+
}
|
|
84
|
+
function getDraft(ctx, exec){
|
|
85
|
+
const m = draftsMap();
|
|
86
|
+
const k = keyFor(ctx, exec);
|
|
87
|
+
if(!m.has(k)){
|
|
88
|
+
// fix #2:内存 miss 时先尝试从磁盘恢复,而非静默建空
|
|
89
|
+
const restored = loadPersistedDraftSync(k);
|
|
90
|
+
if(restored){
|
|
91
|
+
m.set(k, restored);
|
|
92
|
+
const meta = draftMeta(k); meta.isNew = false; meta.recovered = true;
|
|
93
|
+
} else {
|
|
94
|
+
m.set(k, {
|
|
95
|
+
spec:"chara_card_v3", spec_version:"3.0",
|
|
96
|
+
data:{
|
|
97
|
+
name:"", nickname:"", tags:[], creator:"", character_version:"0.1",
|
|
98
|
+
description:"", personality:"", scenario:"", system_prompt:"", post_history_instructions:"",
|
|
99
|
+
first_mes:"", alternate_greetings:[], group_only_greetings:[], mes_example:"",
|
|
100
|
+
creator_notes:"", creator_notes_multilingual:{}, source:[], assets:[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}],
|
|
101
|
+
creation_date: nowSec(), modification_date: nowSec(),
|
|
102
|
+
character_book:{ name:"", description:"", scan_depth:4, token_budget:1200, recursive_scanning:false, extensions:{}, entries:[] }
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const meta = draftMeta(k); meta.isNew = true; meta.recovered = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return m.get(k);
|
|
109
|
+
}
|
|
110
|
+
function setDraft(ctx, draft, exec){
|
|
111
|
+
const m = draftsMap();
|
|
112
|
+
const k = keyFor(ctx, exec);
|
|
113
|
+
draft.data.modification_date = nowSec();
|
|
114
|
+
m.set(k, draft);
|
|
115
|
+
const meta = draftMeta(k); meta.isNew = false; meta.recovered = false;
|
|
116
|
+
persistDraftSync(k, draft);
|
|
117
|
+
return draft;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function libDir(){
|
|
121
|
+
try{ return join(homedir(), ".dsh", "cc-library"); }catch{ return join(process.cwd(), ".cc-library"); }
|
|
122
|
+
}
|
|
123
|
+
async function ensureLibDir(){
|
|
124
|
+
const dir=libDir();
|
|
125
|
+
await mkdir(dir,{recursive:true});
|
|
126
|
+
return dir;
|
|
127
|
+
}
|
|
128
|
+
function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
129
|
+
const data=draft&&draft.data?draft.data:{};
|
|
130
|
+
const now=nowSec();
|
|
131
|
+
const loreCount=Array.isArray(data.character_book?.entries)?data.character_book.entries.length:0;
|
|
132
|
+
const tags=Array.isArray(data.tags)?data.tags.slice(0,6).map(String):[];
|
|
133
|
+
return {
|
|
134
|
+
id:String(id),
|
|
135
|
+
name:(data.name && String(data.name).trim()) || (existingMeta && existingMeta.name) || "未命名角色",
|
|
136
|
+
loreCount, tags,
|
|
137
|
+
descriptionSnippet:String(data.description||"").slice(0,80),
|
|
138
|
+
createdAt: existingMeta && existingMeta.createdAt ? existingMeta.createdAt : now,
|
|
139
|
+
updatedAt: now,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function listLibraryEntries(){
|
|
143
|
+
const dir=await ensureLibDir();
|
|
144
|
+
let files=[];
|
|
145
|
+
try{ files=await readdir(dir); }catch{ return []; }
|
|
146
|
+
const entries=[];
|
|
147
|
+
for(const f of files){
|
|
148
|
+
if(!f.endsWith(".json")) continue;
|
|
149
|
+
const pth=join(dir,f);
|
|
150
|
+
try{
|
|
151
|
+
const raw=await readFile(pth,"utf8");
|
|
152
|
+
const obj=JSON.parse(raw);
|
|
153
|
+
if(obj && obj.meta && obj.id) entries.push(obj.meta);
|
|
154
|
+
else if(obj && obj.draft){
|
|
155
|
+
const id=f.replace(/\.json$/,"");
|
|
156
|
+
entries.push(makeLibraryEntryMeta(obj.draft,null,id));
|
|
157
|
+
}
|
|
158
|
+
}catch{}
|
|
159
|
+
}
|
|
160
|
+
entries.sort((a,b)=>(b.updatedAt||0)-(a.updatedAt||0));
|
|
161
|
+
return entries;
|
|
162
|
+
}
|
|
163
|
+
async function saveLibraryEntry(draft, opts){
|
|
164
|
+
const dir=await ensureLibDir();
|
|
165
|
+
let id=opts && opts.id ? String(opts.id) : "";
|
|
166
|
+
let existing=null;
|
|
167
|
+
if(id){
|
|
168
|
+
try{ const raw=await readFile(join(dir,id+".json"),"utf8"); existing=JSON.parse(raw);}catch{}
|
|
169
|
+
}
|
|
170
|
+
if(!id) id=String(Date.now())+"-"+Math.random().toString(36).slice(2,6);
|
|
171
|
+
const meta=makeLibraryEntryMeta(draft, existing?existing.meta:null, id);
|
|
172
|
+
if(opts && typeof opts.name==="string" && opts.name.trim()) meta.name=String(opts.name).trim().slice(0,60);
|
|
173
|
+
const payload={id,meta,draft};
|
|
174
|
+
await writeFile(join(dir,id+".json"), JSON.stringify(payload,null,2), "utf8");
|
|
175
|
+
return {id,meta,draft};
|
|
176
|
+
}
|
|
177
|
+
async function loadLibraryEntry(id){
|
|
178
|
+
const dir=await ensureLibDir();
|
|
179
|
+
const raw=await readFile(join(dir,String(id)+".json"),"utf8");
|
|
180
|
+
const obj=JSON.parse(raw);
|
|
181
|
+
if(!obj || !obj.draft) throw new Error("文件格式错误");
|
|
182
|
+
return obj;
|
|
183
|
+
}
|
|
184
|
+
async function deleteLibraryEntry(id){
|
|
185
|
+
const dir=await ensureLibDir();
|
|
186
|
+
await unlink(join(dir,String(id)+".json"));
|
|
187
|
+
}
|
|
188
|
+
async function renameLibraryEntry(id,newName){
|
|
189
|
+
const dir=await ensureLibDir();
|
|
190
|
+
const pth=join(dir,String(id)+".json");
|
|
191
|
+
const raw=await readFile(pth,"utf8");
|
|
192
|
+
const obj=JSON.parse(raw);
|
|
193
|
+
if(!obj || !obj.meta) throw new Error("文件格式错误");
|
|
194
|
+
const name=String(newName||"").trim().slice(0,60);
|
|
195
|
+
if(!name) throw new Error("name 不能为空");
|
|
196
|
+
obj.meta.name=name;
|
|
197
|
+
obj.meta.updatedAt=nowSec();
|
|
198
|
+
await writeFile(pth, JSON.stringify(obj,null,2), "utf8");
|
|
199
|
+
return obj.meta;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function out(schema){ return { schema, render: (args, result) => [{ type:"text", text: JSON.stringify(result, null, 2) }] }; }
|
|
203
|
+
|
|
204
|
+
// —— 强制工作流状态计算 ——
|
|
205
|
+
function workflowStatus(draft){
|
|
206
|
+
const d = draft && draft.data ? draft.data : {};
|
|
207
|
+
const w = d.extensions && d.extensions.cc_world ? d.extensions.cc_world : {};
|
|
208
|
+
const loreCount = Array.isArray(d.character_book?.entries) ? d.character_book.entries.length : 0;
|
|
209
|
+
const worldFilled = ["timeline","factions","geo","power","daily"].filter(k => typeof w[k]==="string" && String(w[k]).trim().length>=6).length;
|
|
210
|
+
const characterOk = !!(String(d.name||"").trim() && String(d.description||"").trim() && String(d.personality||"").trim() && String(d.scenario||"").trim());
|
|
211
|
+
const characterPartial = !!(String(d.name||"").trim() && String(d.description||"").trim());
|
|
212
|
+
const worldOk = worldFilled >= 3;
|
|
213
|
+
const loreOk = loreCount >= 5;
|
|
214
|
+
const greetingsOk = !!(String(d.first_mes||"").trim() && Array.isArray(d.group_only_greetings) && d.group_only_greetings.length>=1 && Array.isArray(d.alternate_greetings) && d.alternate_greetings.length>=2);
|
|
215
|
+
const greetingsPartial = !!(String(d.first_mes||"").trim() || (Array.isArray(d.group_only_greetings) && d.group_only_greetings.length>=1));
|
|
216
|
+
let next = "";
|
|
217
|
+
let step = 1;
|
|
218
|
+
if(!characterOk){
|
|
219
|
+
step = 2; next = "下一步必须 cc_patch_character:先与用户讨论角色定位(气质/关系张力等,1-2问)再补齐 name/description/personality/scenario,严禁直接推断填满";
|
|
220
|
+
} else if(!worldOk){
|
|
221
|
+
step = 3; next = "角色基础已齐,下一步必须 cc_patch_world:先与用户讨论世界观侧重(哪几维最想展开/有无私设)再补 5 维(≥3项)";
|
|
222
|
+
} else if(!loreOk){
|
|
223
|
+
step = 4; next = "五维已齐,下一步必须 cc_add_lorebook_entries:与用户讨论触发词/常驻偏好后补到≥5 条(各 1,至少 1 constant,@@decorator),当前 "+loreCount+" 条";
|
|
224
|
+
} else if(!greetingsOk){
|
|
225
|
+
step = 5; next = "世界书已齐("+loreCount+" 条),下一步必须 cc_patch_greetings:先问用户偏好场景/关系阶段再补问候语(first_mes + alternate≥2 + group≥1)";
|
|
226
|
+
} else {
|
|
227
|
+
step = 6; next = "全部已齐,下一步必须 cc_validate 校验,valid=true 才可收尾,提醒用户在工坊“导出 JSON”或“★ 保存到侧边栏”";
|
|
228
|
+
}
|
|
229
|
+
const hint = "工作流 "+step+"/6" + (characterOk?" ✓角色":" ✗角色") + (worldOk?" ✓五维("+worldFilled+"/5)":" ✗五维("+worldFilled+"/5)") + (loreOk?" ✓世界书("+loreCount+")":" ✗世界书("+loreCount+"/5)") + (greetingsOk?" ✓问候语":" ✗问候语") + " → "+next;
|
|
230
|
+
return { characterOk, characterPartial, worldOk, worldFilled, loreCount, loreOk, greetingsOk, greetingsPartial, step, next, hint };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function apply(ctx){
|
|
234
|
+
ctx.tools.register({
|
|
235
|
+
name: "cc_get_card",
|
|
236
|
+
description: "【工作流第1步·必调·先问再填】查看当前 CC 角色卡草稿与工作流进度。CC 模式下必须先调此工具,然后用自然语言向用户总结当前进度(角色/五维/世界书/问候语各缺什么),并用 1-2 个开放问题邀请用户表达偏好,再按步推进。严禁未与用户讨论就直接推断填满。",
|
|
237
|
+
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
238
|
+
output: out({ type:"object", additionalProperties:true, properties:{ draft:{type:"object"}, hint:{type:"string"}, loreCount:{type:"number"}, workflow:{type:"object"}, next:{type:"string"} } }),
|
|
239
|
+
async execute(args, exec){
|
|
240
|
+
const draft = getDraft(ctx, exec);
|
|
241
|
+
const k = keyFor(ctx, exec);
|
|
242
|
+
const st = draftStatusFor(k, draft);
|
|
243
|
+
const wf = workflowStatus(draft);
|
|
244
|
+
const fresh = freshNotice(draftMeta(k));
|
|
245
|
+
const rec = recoveredNotice(k, draft);
|
|
246
|
+
const notice = [rec, fresh].filter(Boolean).join(" ");
|
|
247
|
+
return { draft, draftStatus: st, workflow: wf, hint: wf.hint, next: wf.next, loreCount: wf.loreCount, notice };
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
ctx.tools.register({
|
|
252
|
+
name: "cc_patch_character",
|
|
253
|
+
description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world。",
|
|
254
|
+
parameters: {
|
|
255
|
+
type:"object",
|
|
256
|
+
properties:{
|
|
257
|
+
name:{type:"string", description:"角色显示名(data.name)"},
|
|
258
|
+
nickname:{type:"string", description:"nickname({{char}} 用)"},
|
|
259
|
+
description:{type:"string", description:"description 设定"},
|
|
260
|
+
personality:{type:"string", description:"personality"},
|
|
261
|
+
scenario:{type:"string", description:"scenario"},
|
|
262
|
+
system_prompt:{type:"string", description:"system_prompt"},
|
|
263
|
+
post_history_instructions:{type:"string", description:"post_history_instructions"},
|
|
264
|
+
tags:{type:"array", items:{type:"string"}, description:"标签数组,会整体覆盖"},
|
|
265
|
+
creator_notes:{type:"string", description:"creator_notes"}
|
|
266
|
+
},
|
|
267
|
+
additionalProperties:false
|
|
268
|
+
},
|
|
269
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
270
|
+
async execute(args, exec){
|
|
271
|
+
const k = keyFor(ctx, exec);
|
|
272
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
273
|
+
const wasFresh = draftMeta(k).isNew;
|
|
274
|
+
const recNote = recoveredNotice(k, cur);
|
|
275
|
+
if(args.name!==undefined) cur.data.name = String(args.name).slice(0,120);
|
|
276
|
+
if(args.nickname!==undefined) cur.data.nickname = String(args.nickname).slice(0,120);
|
|
277
|
+
if(args.description!==undefined) cur.data.description = String(args.description).slice(0,5000);
|
|
278
|
+
if(args.personality!==undefined) cur.data.personality = String(args.personality).slice(0,5000);
|
|
279
|
+
if(args.scenario!==undefined) cur.data.scenario = String(args.scenario).slice(0,5000);
|
|
280
|
+
if(args.system_prompt!==undefined) cur.data.system_prompt = String(args.system_prompt).slice(0,3000);
|
|
281
|
+
if(args.post_history_instructions!==undefined) cur.data.post_history_instructions = String(args.post_history_instructions).slice(0,3000);
|
|
282
|
+
if(Array.isArray(args.tags)) cur.data.tags = args.tags.map(String).slice(0,12);
|
|
283
|
+
if(args.creator_notes!==undefined) cur.data.creator_notes = String(args.creator_notes).slice(0,3000);
|
|
284
|
+
setDraft(ctx, cur, exec);
|
|
285
|
+
const wf = workflowStatus(cur);
|
|
286
|
+
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
287
|
+
const recPrefix = recNote ? recNote + " " : "";
|
|
288
|
+
return { ok:true, draft: cur, draftStatus: draftStatusFor(k, cur), workflow: wf, next: wf.next, notice: (recPrefix + warnFresh).trim(), message: recPrefix + "已写入胶囊(角色基础)," + wf.next + warnFresh };
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
ctx.tools.register({
|
|
293
|
+
name: "cc_patch_world",
|
|
294
|
+
description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。调后下一步是 cc_add_lorebook_entries。",
|
|
295
|
+
parameters: {
|
|
296
|
+
type:"object",
|
|
297
|
+
properties:{
|
|
298
|
+
timeline:{type:"string", description:"年表"},
|
|
299
|
+
factions:{type:"string", description:"势力"},
|
|
300
|
+
geo:{type:"string", description:"地理"},
|
|
301
|
+
power:{type:"string", description:"力量体系"},
|
|
302
|
+
daily:{type:"string", description:"日常"},
|
|
303
|
+
autoLorebook:{type:"boolean", description:"是否基于 5 维自动生成 3-6 条带 @@decorator 的 lorebook 条目,强烈建议 true"}
|
|
304
|
+
},
|
|
305
|
+
additionalProperties:false
|
|
306
|
+
},
|
|
307
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
308
|
+
async execute(args, exec){
|
|
309
|
+
const k = keyFor(ctx, exec);
|
|
310
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
311
|
+
const wasFresh = draftMeta(k).isNew;
|
|
312
|
+
const recNote = recoveredNotice(k, cur);
|
|
313
|
+
cur.data.extensions = cur.data.extensions || {};
|
|
314
|
+
const w = cur.data.extensions.cc_world || {};
|
|
315
|
+
if(args.timeline!==undefined) w.timeline = String(args.timeline).slice(0,1000);
|
|
316
|
+
if(args.factions!==undefined) w.factions = String(args.factions).slice(0,1000);
|
|
317
|
+
if(args.geo!==undefined) w.geo = String(args.geo).slice(0,1000);
|
|
318
|
+
if(args.power!==undefined) w.power = String(args.power).slice(0,1000);
|
|
319
|
+
if(args.daily!==undefined) w.daily = String(args.daily).slice(0,1000);
|
|
320
|
+
cur.data.extensions.cc_world = w;
|
|
321
|
+
const shouldAuto = args.autoLorebook !== false; // 默认 true,防偷懒
|
|
322
|
+
if(shouldAuto){
|
|
323
|
+
// 修复 #1:移除先前由 autoLorebook 生成的旧条目,避免重复与过时(保留手动条目)
|
|
324
|
+
// 兼容旧数据(无标记时按固定名匹配),新数据按 cc_auto 标记精确匹配
|
|
325
|
+
const autoNames = new Set(["年表","势力","地理","力量体系","日常"]);
|
|
326
|
+
cur.data.character_book.entries = cur.data.character_book.entries.filter(e => {
|
|
327
|
+
if(e.cc_auto === true) return false;
|
|
328
|
+
if(e.extensions && e.extensions.cc_auto) return false;
|
|
329
|
+
if(autoNames.has(e.name)) return false;
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
// 重新计算 insertion_order 保持连续(手动条目保留相对顺序)
|
|
333
|
+
cur.data.character_book.entries.forEach((e, i) => e.insertion_order = i);
|
|
334
|
+
const push=(name, keys, content, priority, constant=false)=>{
|
|
335
|
+
cur.data.character_book.entries.push({ keys, content, enabled:true, insertion_order: cur.data.character_book.entries.length, case_sensitive:false, use_regex:false, constant, name, priority, id: Date.now()+Math.floor(Math.random()*10000), cc_auto:true, extensions:{cc_auto:true} });
|
|
336
|
+
};
|
|
337
|
+
if(w.timeline) push("年表", ["年表","timeline"], "@@position after_desc\n"+w.timeline, 10);
|
|
338
|
+
if(w.factions) push("势力", ["势力","faction"], "@@depth 4\n"+w.factions, 9);
|
|
339
|
+
if(w.geo) push("地理", ["地理","geography"], "@@position scenario\n"+w.geo, 8);
|
|
340
|
+
if(w.power) push("力量体系", ["力量","power"], "@@activate\n"+w.power, 9, true);
|
|
341
|
+
if(w.daily) push("日常", ["日常","daily"], "@@depth 5\n"+w.daily, 7);
|
|
342
|
+
}
|
|
343
|
+
setDraft(ctx, cur, exec);
|
|
344
|
+
const wf = workflowStatus(cur);
|
|
345
|
+
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
346
|
+
const recPrefix = recNote ? recNote + " " : "";
|
|
347
|
+
return { ok:true, loreCount: cur.data.character_book.entries.length, draft: cur, draftStatus: draftStatusFor(k, cur), workflow:wf, next: wf.next, notice: (recPrefix + warnFresh).trim(), message: recPrefix + "已写入五维("+wf.worldFilled+"/5),"+wf.next + warnFresh };
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
ctx.tools.register({
|
|
352
|
+
name: "cc_add_lorebook_entries",
|
|
353
|
+
description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator)。调用前应与用户讨论:问用户想要哪些触发词/常驻条目/要藏什么彩蛋,拿到偏好后再生成。必须在五维之后、问候语之前调用,禁止一次性无讨论批量生成。",
|
|
354
|
+
parameters: {
|
|
355
|
+
type:"object",
|
|
356
|
+
properties:{
|
|
357
|
+
entries:{
|
|
358
|
+
type:"array", minItems:1, maxItems:5,
|
|
359
|
+
items:{
|
|
360
|
+
type:"object",
|
|
361
|
+
properties:{
|
|
362
|
+
name:{type:"string", description:"条目标题"},
|
|
363
|
+
keys:{type:"array", items:{type:"string"}, description:"触发关键词,为空且 constant=true 时为常驻条目"},
|
|
364
|
+
content:{type:"string", description:"正文,可在开头写 @@decorator 行"},
|
|
365
|
+
constant:{type:"boolean", description:"是否为常驻条目"},
|
|
366
|
+
use_regex:{type:"boolean", description:"keys 是否按正则匹配"},
|
|
367
|
+
priority:{type:"number", description:"优先级 0-10"}
|
|
368
|
+
},
|
|
369
|
+
required:["content"],
|
|
370
|
+
additionalProperties:false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
required:["entries"],
|
|
375
|
+
additionalProperties:false
|
|
376
|
+
},
|
|
377
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
378
|
+
async execute(args, exec){
|
|
379
|
+
const k = keyFor(ctx, exec);
|
|
380
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
381
|
+
const wasFresh = draftMeta(k).isNew;
|
|
382
|
+
const recNote = recoveredNotice(k, cur);
|
|
383
|
+
args.entries.forEach(function(e){
|
|
384
|
+
cur.data.character_book.entries.push({
|
|
385
|
+
keys: Array.isArray(e.keys)? e.keys.map(String): [],
|
|
386
|
+
content: String(e.content).slice(0,5000),
|
|
387
|
+
enabled:true,
|
|
388
|
+
insertion_order: cur.data.character_book.entries.length,
|
|
389
|
+
case_sensitive:false,
|
|
390
|
+
use_regex: !!e.use_regex,
|
|
391
|
+
constant: !!e.constant,
|
|
392
|
+
name: String(e.name||"未命名").slice(0,60),
|
|
393
|
+
priority: typeof e.priority==="number"? e.priority: 5,
|
|
394
|
+
id: Date.now()+Math.floor(Math.random()*10000)
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
setDraft(ctx, cur, exec);
|
|
398
|
+
const wf = workflowStatus(cur);
|
|
399
|
+
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
400
|
+
const recPrefix = recNote ? recNote + " " : "";
|
|
401
|
+
return { ok:true, added: args.entries.length, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
ctx.tools.register({
|
|
406
|
+
name: "cc_delete_lorebook_entries",
|
|
407
|
+
description: "【Lorebook 管理】按 id 或 name 删除条目(用于清理过时条目,autoLorebook 已自动覆盖旧自动条目,手动条目需显式删除)。至少提供 ids 或 names 之一。",
|
|
408
|
+
parameters: {
|
|
409
|
+
type:"object",
|
|
410
|
+
properties:{
|
|
411
|
+
ids:{type:"array", items:{type:"number"}, description:"要删除的条目 id 列表"},
|
|
412
|
+
names:{type:"array", items:{type:"string"}, description:"要删除的条目 name 列表(精确匹配)"}
|
|
413
|
+
},
|
|
414
|
+
additionalProperties:false
|
|
415
|
+
},
|
|
416
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
417
|
+
async execute(args, exec){
|
|
418
|
+
const k = keyFor(ctx, exec);
|
|
419
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
420
|
+
const before = draftStatusFor(k, cur);
|
|
421
|
+
const beforeRec = recoveredNotice(k, cur);
|
|
422
|
+
const beforeFresh = before.isNew;
|
|
423
|
+
const beforeCount = cur.data.character_book.entries.length;
|
|
424
|
+
const idSet = new Set(Array.isArray(args.ids)? args.ids.map(Number): []);
|
|
425
|
+
const nameSet = new Set(Array.isArray(args.names)? args.names.map(String): []);
|
|
426
|
+
if(idSet.size===0 && nameSet.size===0) return { ok:false, error:"请提供 ids 或 names" };
|
|
427
|
+
cur.data.character_book.entries = cur.data.character_book.entries.filter(e => !idSet.has(e.id) && !nameSet.has(e.name));
|
|
428
|
+
// 重排 insertion_order
|
|
429
|
+
cur.data.character_book.entries.forEach((e,i)=> e.insertion_order=i);
|
|
430
|
+
setDraft(ctx, cur, exec);
|
|
431
|
+
const wf = workflowStatus(cur);
|
|
432
|
+
const warnFresh = beforeFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
433
|
+
const recPrefix = beforeRec ? beforeRec + " " : "";
|
|
434
|
+
return { ok:true, deleted: beforeCount - cur.data.character_book.entries.length, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
ctx.tools.register({
|
|
439
|
+
name: "cc_update_lorebook_entry",
|
|
440
|
+
description: "【Lorebook 管理】按 id 更新单条条目(content/keys/name 等),用于修正过时内容而非整批重建。",
|
|
441
|
+
parameters: {
|
|
442
|
+
type:"object",
|
|
443
|
+
properties:{
|
|
444
|
+
id:{type:"number", description:"要更新的条目 id(必填)"},
|
|
445
|
+
name:{type:"string", description:"新标题"},
|
|
446
|
+
keys:{type:"array", items:{type:"string"}, description:"新触发词"},
|
|
447
|
+
content:{type:"string", description:"新正文(含 @@decorator)"},
|
|
448
|
+
enabled:{type:"boolean"},
|
|
449
|
+
constant:{type:"boolean"},
|
|
450
|
+
use_regex:{type:"boolean"},
|
|
451
|
+
priority:{type:"number"}
|
|
452
|
+
},
|
|
453
|
+
required:["id"],
|
|
454
|
+
additionalProperties:false
|
|
455
|
+
},
|
|
456
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
457
|
+
async execute(args, exec){
|
|
458
|
+
const k = keyFor(ctx, exec);
|
|
459
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
460
|
+
const wasFresh = draftMeta(k).isNew;
|
|
461
|
+
const recNote = recoveredNotice(k, cur);
|
|
462
|
+
const e = cur.data.character_book.entries.find(x=> x.id===args.id);
|
|
463
|
+
if(!e) return { ok:false, error:"未找到 id="+args.id };
|
|
464
|
+
if(args.name!==undefined) e.name=String(args.name).slice(0,60);
|
|
465
|
+
if(Array.isArray(args.keys)) e.keys=args.keys.map(String);
|
|
466
|
+
if(args.content!==undefined) e.content=String(args.content).slice(0,5000);
|
|
467
|
+
if(args.enabled!==undefined) e.enabled=!!args.enabled;
|
|
468
|
+
if(args.constant!==undefined) e.constant=!!args.constant;
|
|
469
|
+
if(args.use_regex!==undefined) e.use_regex=!!args.use_regex;
|
|
470
|
+
if(typeof args.priority==="number") e.priority=args.priority;
|
|
471
|
+
setDraft(ctx, cur, exec);
|
|
472
|
+
const wf = workflowStatus(cur);
|
|
473
|
+
const warnFresh = wasFresh ? " " + freshNotice({ isNew: true }) : "";
|
|
474
|
+
const recPrefix = recNote ? recNote + " " : "";
|
|
475
|
+
return { ok:true, entry:e, loreCount: cur.data.character_book.entries.length, draftStatus: draftStatusFor(k, cur), workflow:wf, next:wf.next, notice: (recPrefix + warnFresh).trim() };
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
ctx.tools.register({
|
|
480
|
+
name: "cc_patch_greetings",
|
|
481
|
+
description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)后调用。",
|
|
482
|
+
parameters: {
|
|
483
|
+
type:"object",
|
|
484
|
+
properties:{
|
|
485
|
+
first_mes:{type:"string", description:"首条开场白"},
|
|
486
|
+
alternate_greetings:{type:"array", items:{type:"string"}, description:"备选开场白数组,至少 2 条"},
|
|
487
|
+
group_only_greetings:{type:"array", items:{type:"string"}, description:"群聊限定开场白(必填至少1条)"},
|
|
488
|
+
mes_example:{type:"string", description:"对话示例(可用 {{user}}/{{char}})"}
|
|
489
|
+
},
|
|
490
|
+
additionalProperties:false
|
|
491
|
+
},
|
|
492
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
493
|
+
async execute(args, exec){
|
|
494
|
+
const k = keyFor(ctx, exec);
|
|
495
|
+
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
496
|
+
const wasFresh = draftMeta(k).isNew;
|
|
497
|
+
const recNote = recoveredNotice(k, cur);
|
|
498
|
+
if(args.first_mes!==undefined) cur.data.first_mes = String(args.first_mes).slice(0,5000);
|
|
499
|
+
if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(String).slice(0,10);
|
|
500
|
+
if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(String).slice(0,10);
|
|
501
|
+
if(args.mes_example!==undefined) cur.data.mes_example = String(args.mes_example).slice(0,5000);
|
|
502
|
+
setDraft(ctx, cur, exec);
|
|
503
|
+
const wf = workflowStatus(cur);
|
|
504
|
+
// 若世界书/五维未齐,警告但仍写入,让 validate 卡住
|
|
505
|
+
let warn = "";
|
|
506
|
+
if(!wf.loreOk) warn = " 警告:世界书仅 "+wf.loreCount+"/5 条,validate 会失败,请先补世界书。";
|
|
507
|
+
if(!wf.worldOk) warn += " 警告:五维仅 "+wf.worldFilled+"/5 项,validate 会失败。";
|
|
508
|
+
if(wasFresh) warn += " " + freshNotice({ isNew: true });
|
|
509
|
+
const recPrefix = recNote ? recNote + " " : "";
|
|
510
|
+
return { ok:true, draft: cur, draftStatus: draftStatusFor(k, cur), workflow:wf, next: wf.next, warning: warn, notice: (recPrefix + warn).trim(), message: recPrefix + "已写入问候语"+warn+"。"+wf.next };
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// —— 已存角色库 CRUD(ID 化,模型可直接管理,与工坊侧栏共享 ~/.dsh/cc-library) ——
|
|
515
|
+
ctx.tools.register({
|
|
516
|
+
name: "cc_list_library",
|
|
517
|
+
description: "【库·查】列出已存角色库所有条目(id/name/loreCount/tags/updatedAt),用于让用户挑选载入或让模型决定更新哪一张。",
|
|
518
|
+
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
519
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
520
|
+
async execute(args, exec){
|
|
521
|
+
const entries=await listLibraryEntries();
|
|
522
|
+
return { ok:true, entries, count:entries.length };
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
ctx.tools.register({
|
|
526
|
+
name: "cc_save_to_library",
|
|
527
|
+
description: "【库·增/改】将当前会话草稿按唯一 ID 存入已存库(ID 化 CRUD)。不传 id 则新建 ID;传 id 则按 ID 原地覆盖(更新),无需先删旧卡。模型可在用户说“保存/更新”时直接调用。",
|
|
528
|
+
parameters: { type:"object", properties:{ id:{type:"string", description:"要覆盖的已存 ID(不传则新建)"}, name:{type:"string", description:"覆盖 meta.name(可选,不影响 draft.data.name)"} }, additionalProperties:false },
|
|
529
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
530
|
+
async execute(args, exec){
|
|
531
|
+
const draft=JSON.parse(JSON.stringify(getDraft(ctx,exec)));
|
|
532
|
+
// 至少需要 name
|
|
533
|
+
if(!draft.data.name || !String(draft.data.name).trim()) return { ok:false, error:"当前草稿缺少 data.name,请先 cc_patch_character 补齐" };
|
|
534
|
+
const res=await saveLibraryEntry(draft,{id:args.id, name:args.name});
|
|
535
|
+
return { ok:true, id:res.id, meta:res.meta, loreCount:res.meta.loreCount };
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
ctx.tools.register({
|
|
539
|
+
name: "cc_load_from_library",
|
|
540
|
+
description: "【库·载入】按 ID 将已存卡载入当前会话草稿(会覆盖当前 draft),载入后再次保存将按此 ID 更新。",
|
|
541
|
+
parameters: { type:"object", properties:{ id:{type:"string", description:"已存条目 ID"} }, required:["id"], additionalProperties:false },
|
|
542
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
543
|
+
async execute(args, exec){
|
|
544
|
+
const obj=await loadLibraryEntry(args.id);
|
|
545
|
+
const cur=JSON.parse(JSON.stringify(obj.draft));
|
|
546
|
+
setDraft(ctx,cur,exec);
|
|
547
|
+
const k = keyFor(ctx, exec);
|
|
548
|
+
return { ok:true, id:obj.id, meta:obj.meta, draft:cur, draftStatus: draftStatusFor(k, cur), loreCount:obj.meta.loreCount };
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
ctx.tools.register({
|
|
552
|
+
name: "cc_delete_from_library",
|
|
553
|
+
description: "【库·删】按 ID 删除已存条目,模型可在用户确认后调用。",
|
|
554
|
+
parameters: { type:"object", properties:{ id:{type:"string", description:"要删除的 ID"} }, required:["id"], additionalProperties:false },
|
|
555
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
556
|
+
async execute(args, exec){
|
|
557
|
+
await deleteLibraryEntry(args.id);
|
|
558
|
+
return { ok:true, id:args.id };
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
ctx.tools.register({
|
|
562
|
+
name: "cc_rename_in_library",
|
|
563
|
+
description: "【库·改名】按 ID 重命名已存条目(仅改 meta.name,不改 draft 内容)。",
|
|
564
|
+
parameters: { type:"object", properties:{ id:{type:"string"}, name:{type:"string", description:"新名称"} }, required:["id","name"], additionalProperties:false },
|
|
565
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
566
|
+
async execute(args, exec){
|
|
567
|
+
const meta=await renameLibraryEntry(args.id,args.name);
|
|
568
|
+
return { ok:true, meta };
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
ctx.tools.register({
|
|
572
|
+
name: "cc_get_library_entry",
|
|
573
|
+
description: "【库·查单条】按 ID 获取单条完整草稿与 meta,用于模型在重命名/更新前确认内容。",
|
|
574
|
+
parameters: { type:"object", properties:{ id:{type:"string"} }, required:["id"], additionalProperties:false },
|
|
575
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
576
|
+
async execute(args, exec){
|
|
577
|
+
const obj=await loadLibraryEntry(args.id);
|
|
578
|
+
return { ok:true, id:obj.id, meta:obj.meta, draft:obj.draft };
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
ctx.tools.register({
|
|
583
|
+
name: "cc_validate",
|
|
584
|
+
description: "【工作流第6步·收口必调】校验 CC 角色卡(SPEC_V3 + 角色四件套/五维≥3/世界书≥5/问候语齐)。valid=false 按 errors 与用户讨论继续补;valid=true 时总结成果并邀请用户在工坊微调后“导出 JSON”或“★ 保存到侧边栏”。全程强调共创:多问用户、多给草案供挑选。",
|
|
585
|
+
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
586
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
587
|
+
async execute(args, exec){
|
|
588
|
+
const d = getDraft(ctx, exec);
|
|
589
|
+
const k = keyFor(ctx, exec);
|
|
590
|
+
const st = draftStatusFor(k, d);
|
|
591
|
+
const fresh = freshNotice(draftMeta(k));
|
|
592
|
+
const rec = recoveredNotice(k, d);
|
|
593
|
+
const notice = [rec, fresh].filter(Boolean).join(" ");
|
|
594
|
+
const wf = workflowStatus(d);
|
|
595
|
+
const errors=[], warnings=[];
|
|
596
|
+
if(!String(d.data.name||"").trim()) errors.push("缺 data.name(角色名)");
|
|
597
|
+
if(!String(d.data.description||"").trim()) errors.push("缺 data.description");
|
|
598
|
+
if(!String(d.data.personality||"").trim()) errors.push("缺 data.personality");
|
|
599
|
+
if(!String(d.data.scenario||"").trim()) warnings.push("scenario 为空,建议补充");
|
|
600
|
+
if(!Array.isArray(d.data.group_only_greetings) || d.data.group_only_greetings.length<1) errors.push("缺 group_only_greetings(至少 1 条必填)");
|
|
601
|
+
if(!Array.isArray(d.data.alternate_greetings) || d.data.alternate_greetings.length<2) warnings.push("alternate_greetings 建议至少 2 条");
|
|
602
|
+
if(!String(d.data.first_mes||"").trim()) warnings.push("first_mes 为空");
|
|
603
|
+
if(!wf.worldOk) errors.push("五维世界观未齐:需 timeline/factions/geo/power/daily 至少 3 项(当前 "+wf.worldFilled+"/5),请调 cc_patch_world");
|
|
604
|
+
if(!wf.loreOk) errors.push("世界书未齐:需至少 5 条(当前 "+wf.loreCount+" 条),请调 cc_add_lorebook_entries 补到 5 条");
|
|
605
|
+
if(wf.loreCount>0 && !d.data.character_book.entries.some(e=>e.constant)) warnings.push("世界书建议至少 1 条 constant 常驻条目");
|
|
606
|
+
if(d.data.character_book.entries.length>0 && d.data.character_book.entries.every(e=> !String(e.content||"").includes("@@"))) warnings.push("世界书条目建议带 @@decorator(@@position/@@depth/@@activate)");
|
|
607
|
+
const valid = errors.length===0;
|
|
608
|
+
const hint = valid ? "校验通过 ✓ 可导出/保存到侧边栏" : "校验未通过,按 errors 逐项补齐(严禁跳步)";
|
|
609
|
+
return { valid, errors, warnings, draftStatus: st, notice, workflow:wf, next: wf.next, hint, loreCount: wf.loreCount, name: d.data.name };
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export { apply, inject, name };
|