@xia-sc/dsh-cc-studio 0.8.0 → 0.8.2

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/lib/client.js CHANGED
@@ -11,6 +11,20 @@ window.__ModuleLoader__.load({
11
11
  var React=require("react");
12
12
  var name="dsh-cc-studio";
13
13
  var inject=["slots","connection","locale"];
14
+ // 原生 <select> 展开后的 option 列表不继承页面配色(深色模式下呈白底浅字、读不出来)。
15
+ // option 无法用 inline style 逐个维护,这里补一条跟随主题变量的全局规则。
16
+ (function(){
17
+ try{
18
+ var st=document.createElement("style");
19
+ st.setAttribute("data-dsh-cc-studio","select-theme");
20
+ st.textContent=[
21
+ "select option{background-color:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}",
22
+ "select option:checked{background-color:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-weight:600}",
23
+ "select option:disabled{color:var(--dsw-alias-label-secondary)}"
24
+ ].join("");
25
+ (document.head||document.documentElement).appendChild(st);
26
+ }catch(e){}
27
+ })();
14
28
  var NS="dshCcStudio";
15
29
  var zh={
16
30
  "capsule.open":"🎭 角色卡工坊",
@@ -23,13 +37,65 @@ window.__ModuleLoader__.load({
23
37
  "panel.title":"角色卡工坊",
24
38
  "panel.subtitle":"CCv3 · 从点子到可导入卡",
25
39
  "panel.badge":"dsh-cc-studio · 融合",
26
- "step.world":"5维世界观",
27
- "step.char":"角色细化",
40
+ "step.world":"世界观",
41
+ "step.char":"角色",
28
42
  "step.lore":"世界书",
29
43
  "step.export":"校验导出",
44
+ "step.overview":"总览",
45
+ "step.greetings":"开场与示例",
46
+ "step.rel":"关系与分组",
47
+ "step.book":"共享世界书",
30
48
  "step.world.desc":"年表/势力/地理/力量/日常",
31
- "step.char.desc":"六件套 + 开场",
32
- "step.lore.desc":"Lorebook + Decorator",
49
+ "step.char.desc":"六件套 · 标签 · 作者",
50
+ "step.lore.desc":"条目 · 预算 · 触发词",
51
+ "step.overview.desc":"进度 · 统计 · 快捷动作",
52
+ "step.greetings.desc":"首条 · 备选 · 群聊 · 示例",
53
+ "step.rel.desc":"图谱 · 成员 · 渲染",
54
+ "step.book.desc":"一组一份 · 去重",
55
+ // —— 0.9.0 共享世界书(组级)——
56
+ "workshop.rel.edit":"编辑",
57
+ "workshop.rel.editTitle":"编辑关系",
58
+ "workshop.rel.addTitle":"添加关系",
59
+ "workshop.rel.typeLabel":"类型",
60
+ "workshop.rel.dirLabel":"方向",
61
+ "workshop.rel.myCard":"当前卡",
62
+ "book.title":"共享世界书",
63
+ "book.hint":"一组一份:世界设定(年表/势力/地理/力量/日常)放这里,成员卡里只留角色私有条目。导出后是 CCv3 官方的 lorebook_v3 JSON —— 在聊天客户端里绑成全局世界书,多张卡共用一本,卡里就不必再重复存。",
64
+ "book.noGroup":"共享世界书是按分组存放的:先在右侧「已存角色」里建一个分组,并把成员卡拉进去。",
65
+ "book.empty":"这个分组还没有共享世界书。",
66
+ "book.name":"书名",
67
+ "book.desc":"备注",
68
+ "book.depth":"扫描深度",
69
+ "book.budget":"token 预算",
70
+ "book.tokens":"约 {n} token",
71
+ "book.entries":"条目 · {n}",
72
+ "book.groups":"共 {n} 组",
73
+ "book.constant":"常驻",
74
+ "book.dupTitle":"成员卡之间的重复条目",
75
+ "book.dupNone":"成员卡之间没有内容重复的条目。",
76
+ "book.dupItem":"{n} 张卡都有 · 可省 {chars} 字符",
77
+ "book.dupMismatch":"名字/触发词不一致",
78
+ "book.promote":"提升到共享书",
79
+ "book.promoteHint":"把这 N 条搬进共享书,并从每张卡里删掉 —— 导出前先做这一步,卡里就不再有重复。",
80
+ "book.promotePreview":"将提升 {n} 条、从各卡共删 {m} 条重复",
81
+ "book.promoteDone":"已提升 {n} 条(删掉 {m} 条重复)",
82
+ "book.exportBook":"导出共享世界书 JSON",
83
+ "book.exported":"已导出 {name}",
84
+ "book.applyCard":"合并进当前卡",
85
+ "book.applyDone":"已往当前卡补了 {n} 条",
86
+ "book.applyNone":"当前卡已包含这本共享书的全部条目。",
87
+ "book.entryName":"条目名",
88
+ "book.entryKeys":"触发词(逗号分隔)",
89
+ "book.entryContent":"内容(可带 @@position / @@depth / @@activate 等装饰器)",
90
+ "book.refresh":"刷新",
91
+ "book.saved":"已保存(rev {rev})",
92
+ // —— 0.9.0 导出页的共享书块 ——
93
+ "export.book.title":"共享世界书",
94
+ "export.book.hint":"当前卡所属分组的共享设定可以单独导出(CCv3 lorebook_v3 格式):在客户端里把它绑成全局世界书,多张成员卡共用一本。",
95
+ "export.book.none":"当前卡不在任何分组里,或这个分组还没有共享世界书。",
96
+ "export.book.button":"⬇ 导出共享世界书",
97
+ "export.book.merge":"导出 JSON 时把共享条目并入这张卡(给不支持全局世界书的客户端)",
98
+ "export.book.merged":"(已并入 {n} 条共享条目)",
33
99
  "action.expand":"✦ AI 扩写",
34
100
  "action.validate":"校验",
35
101
  "action.exportJson":"⬇ JSON",
@@ -137,6 +203,9 @@ window.__ModuleLoader__.load({
137
203
  "workshop.group.none":"未分组",
138
204
  "workshop.group.new":"+ 组",
139
205
  "workshop.group.summary":"{n} 卡 / {m} 关系",
206
+ "workshop.group.newGroup":"+ 新建分组",
207
+ "workshop.group.empty":"(空)",
208
+ "workshop.group.needRestart":"宿主半还是旧代码(这些按钮要重启 dsh 才生效):请退出并重新打开桌面应用 / 重启 dsh web,然后刷新页面",
140
209
  "workshop.group.prompt":"新分组名称(同一世界观的角色放一组)",
141
210
  "workshop.group.assign":"组",
142
211
  "workshop.group.set":"已归组",
@@ -280,13 +349,63 @@ window.__ModuleLoader__.load({
280
349
  "panel.title":"Character Card Studio",
281
350
  "panel.subtitle":"CCv3 · Idea to Importable Card",
282
351
  "panel.badge":"dsh-cc-studio · fusion",
283
- "step.world":"World 5D",
352
+ "step.world":"World",
284
353
  "step.char":"Character",
285
354
  "step.lore":"Lorebook",
286
355
  "step.export":"Validate & Export",
356
+ "step.overview":"Overview",
357
+ "step.greetings":"Openings & examples",
358
+ "step.rel":"Relations & groups",
359
+ "step.book":"Shared lorebook",
287
360
  "step.world.desc":"Timeline/Factions/Geo/Power/Daily",
288
- "step.char.desc":"6-piece + Openings",
289
- "step.lore.desc":"Lorebook + Decorator",
361
+ "step.char.desc":"Core fields · tags · creator",
362
+ "step.lore.desc":"Entries · budget · keys",
363
+ "step.overview.desc":"Progress · stats · quick actions",
364
+ "step.greetings.desc":"First · alternates · group · examples",
365
+ "step.rel.desc":"Graph · members · render",
366
+ "step.book.desc":"One per group · dedupe",
367
+ "workshop.rel.edit":"Edit",
368
+ "workshop.rel.editTitle":"Edit relation",
369
+ "workshop.rel.addTitle":"Add relation",
370
+ "workshop.rel.typeLabel":"Type",
371
+ "workshop.rel.dirLabel":"Direction",
372
+ "workshop.rel.myCard":"current card",
373
+ "book.title":"Shared lorebook",
374
+ "book.hint":"One book per group: put the world setting (timeline / factions / geography / power / daily) here and keep only character-private entries in each member card. The export is CCv3's standalone lorebook_v3 JSON — bind it as a global lorebook in your client and every member card shares one copy instead of repeating it.",
375
+ "book.noGroup":"Shared lorebooks live on a group: create one under “Saved characters” on the right and put the member cards in it.",
376
+ "book.empty":"This group has no shared lorebook yet.",
377
+ "book.name":"Book name",
378
+ "book.desc":"Note",
379
+ "book.depth":"Scan depth",
380
+ "book.budget":"Token budget",
381
+ "book.tokens":"~{n} tokens",
382
+ "book.entries":"Entries · {n}",
383
+ "book.groups":"{n} groups",
384
+ "book.constant":"constant",
385
+ "book.dupTitle":"Entries duplicated across member cards",
386
+ "book.dupNone":"No entry is duplicated across member cards.",
387
+ "book.dupItem":"in {n} cards · saves {chars} chars",
388
+ "book.dupMismatch":"name/keys differ",
389
+ "book.promote":"Promote into the shared book",
390
+ "book.promoteHint":"Move these N entries into the shared book and delete them from every card — do this before exporting and the cards hold no duplicates.",
391
+ "book.promotePreview":"Will promote {n} and delete {m} duplicates across the cards",
392
+ "book.promoteDone":"Promoted {n} (removed {m} duplicates)",
393
+ "book.exportBook":"Export shared lorebook JSON",
394
+ "book.exported":"Exported {name}",
395
+ "book.applyCard":"Merge into the current card",
396
+ "book.applyDone":"Added {n} entries to the current card",
397
+ "book.applyNone":"The current card already contains every entry of this book.",
398
+ "book.entryName":"Entry name",
399
+ "book.entryKeys":"Keys (comma separated)",
400
+ "book.entryContent":"Content (may carry @@position / @@depth / @@activate decorators)",
401
+ "book.refresh":"Refresh",
402
+ "book.saved":"Saved (rev {rev})",
403
+ "export.book.title":"Shared lorebook",
404
+ "export.book.hint":"The shared setting of this card's group can be exported on its own (CCv3 lorebook_v3 format): bind it as a global lorebook in your client and every member card shares one copy.",
405
+ "export.book.none":"This card is in no group, or its group has no shared lorebook yet.",
406
+ "export.book.button":"⬇ Export shared lorebook",
407
+ "export.book.merge":"Merge the shared entries into this card when exporting JSON (for clients without a global lorebook)",
408
+ "export.book.merged":"({n} shared entries merged in)",
290
409
  "action.expand":"AI Expand",
291
410
  "action.validate":"Validate",
292
411
  "action.exportJson":"⬇ JSON",
@@ -394,6 +513,9 @@ window.__ModuleLoader__.load({
394
513
  "workshop.group.none":"Ungrouped",
395
514
  "workshop.group.new":"+ Group",
396
515
  "workshop.group.summary":"{n} cards / {m} relations",
516
+ "workshop.group.newGroup":"+ New group",
517
+ "workshop.group.empty":"(empty)",
518
+ "workshop.group.needRestart":"The host half is still running old code (these buttons need a dsh restart): quit and reopen the desktop app (or restart dsh web), then reload the page.",
397
519
  "workshop.group.prompt":"New group name (characters of one world go together)",
398
520
  "workshop.group.assign":"Group",
399
521
  "workshop.group.set":"Grouped",
@@ -575,7 +697,7 @@ window.__ModuleLoader__.load({
575
697
  var _saved = loadSettings();
576
698
  // 兼容旧开关:poll=false 视为 0 秒
577
699
  if(_saved.poll===false) _saved.pollInterval = 0;
578
- var state={ draft: draft, draftStatus: null, cur:0, panelOpen:false, sideExpanded:false, isCcMode:false, currentSessionId:null, slotWarn:null, alternateSlots:null, world:{ timeline:"", factions:"", geo:"", power:"", daily:"" }, busy:null, valid:{valid:true,errors:[],warnings:[]}, lastMsg:null, library:[], libraryQuery:"", libraryCollapsed:false, libraryBusy:null, groups:[], currentGroupId:"", relations:null, relBusy:null, currentLibraryId:null, editingWorld:null, editingWorldRevert:null, editingText:null, pollEnabled:_saved.poll, pollInterval:_saved.pollInterval, autoValidate:_saved.autoValidate, autoSaveOnImport:_saved.autoSaveOnImport };
700
+ var state={ draft: draft, draftStatus: null, cur:0, panelOpen:false, sideExpanded:false, isCcMode:false, currentSessionId:null, slotWarn:null, alternateSlots:null, world:{ timeline:"", factions:"", geo:"", power:"", daily:"" }, busy:null, valid:{valid:true,errors:[],warnings:[]}, lastMsg:null, library:[], libraryQuery:"", libraryCollapsed:false, libraryBusy:null, groups:[], currentGroupId:"", relations:null, relBusy:null, currentLibraryId:null, book:null, bookBusy:null, bookMerge:false, editingWorld:null, editingWorldRevert:null, editingText:null, pollEnabled:_saved.poll, pollInterval:_saved.pollInterval, autoValidate:_saved.autoValidate, autoSaveOnImport:_saved.autoSaveOnImport };
579
701
  var listeners=new Set();
580
702
  function emit(){ listeners.forEach(function(fn){ try{fn();}catch(e){console.error(e);} }); }
581
703
  function getSnapshot(){ return state; }
@@ -774,10 +896,31 @@ window.__ModuleLoader__.load({
774
896
  }catch(e){ set({lastMsg:String(e&&e.message||e)}); }
775
897
  }
776
898
  function importJSON(){ return importAny(); }
899
+ /** 条目内容比对(折叠空白后逐字)——导出时把共享条目并进卡用的,与两半同一套口径。 */
900
+ function sameEntryContent(a, b){
901
+ var norm=function(e){ return String((e&&e.content)!=null? e.content : "").replace(/\s+/g," ").trim(); };
902
+ var x=norm(a); return !!x && x===norm(b);
903
+ }
904
+ /**
905
+ * 导出 JSON。0.9.0:若在导出页勾了「并入共享条目」,则**只在导出的副本上**把该组的共享世界书
906
+ * 里卡里还没有的条目补进去(给不支持全局世界书的客户端兜底)——草稿本身一个字节都不动。
907
+ */
777
908
  function exportJSON(){
778
- var blob=new Blob([JSON.stringify(state.draft,null,2)],{type:"application/json"});
909
+ var out=state.draft, merged=0;
910
+ var book=state.book && state.book.book;
911
+ if(state.bookMerge && book && Array.isArray(book.data && book.data.entries) && book.data.entries.length){
912
+ var ents=(state.draft.data.character_book && state.draft.data.character_book.entries) || [];
913
+ var add=[];
914
+ book.data.entries.forEach(function(e){ if(!ents.some(function(x){ return sameEntryContent(x, e); })) add.push(e); });
915
+ if(add.length){
916
+ out=JSON.parse(JSON.stringify(state.draft));
917
+ out.data.character_book=Object.assign({}, out.data.character_book, { entries: ents.concat(add) });
918
+ merged=add.length;
919
+ }
920
+ }
921
+ var blob=new Blob([JSON.stringify(out,null,2)],{type:"application/json"});
779
922
  var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=(state.draft.data.name||"card")+".json"; a.click(); setTimeout(function(){ URL.revokeObjectURL(a.href); },800);
780
- set({lastMsg:t("store.exportedJson")});
923
+ set({lastMsg: merged? t("store.exportedJson") + " " + t("export.book.merged",{n:String(merged)}) : t("store.exportedJson")});
781
924
  }
782
925
  function exportPNG(){
783
926
  var sid=state.currentSessionId||null;
@@ -886,14 +1029,33 @@ window.__ModuleLoader__.load({
886
1029
  var gid = String(id || "");
887
1030
  set({currentGroupId: gid});
888
1031
  // "" = 全部(不过滤)、"__none" = 只看未分组:这两个都不是真分组,没有图谱可载
889
- if(gid && gid !== "__none") loadGroup(gid); else set({relations:null});
1032
+ if(gid && gid !== "__none"){ loadGroup(gid); loadBook(gid); } else { set({relations:null, book:null}); }
890
1033
  }
891
1034
  async function refreshGroups(){
892
1035
  try{
893
1036
  var res = await rpc("cc_listGroups", {});
894
- if(res && res.ok && res.value) set({groups: res.value.groups||[]});
1037
+ if(res && res.ok && res.value){ set({groups: res.value.groups||[]}); return; }
1038
+ // 宿主半还是旧代码时这里会拿到 unknown-endpoint —— 那是「只刷新了页面、没重启 dsh」的典型信号,
1039
+ // 直接说人话,别让用户对着一堆没反应的按钮猜(0.8.1)。
1040
+ var code = res && res.error && res.error.details && res.error.details.code;
1041
+ if(code === "unknown-endpoint"){ set({lastMsg: t("workshop.group.needRestart")}); return; }
1042
+ set({lastMsg: String((res && res.error && res.error.message) || "")});
895
1043
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
896
1044
  }
1045
+ /** 从某个分组面板直接进关系视图(折叠面板标题上的「关系」按钮用它)。 */
1046
+ function openRelationsFor(gid){
1047
+ var id = String(gid || "");
1048
+ if(!id || id === "__none"){ set({lastMsg: t("workshop.rel.pickGroup")}); return; }
1049
+ set({currentGroupId: id, cur: 4});
1050
+ loadGroup(id);
1051
+ }
1052
+ /** 0.8.1:从侧栏进关系视图。未选具体分组时**给一句提示**,而不是把按钮静默禁用(用户会以为坏了)。 */
1053
+ function openRelations(){
1054
+ var gid = String(state.currentGroupId || "");
1055
+ if(!gid || gid === "__none"){ set({lastMsg: t("workshop.rel.pickGroup")}); return; }
1056
+ set({cur: 4});
1057
+ loadGroup(gid);
1058
+ }
897
1059
  async function loadGroup(id){
898
1060
  var gid = String(id || state.currentGroupId || "");
899
1061
  if(!gid){ set({relations:null}); return; }
@@ -905,9 +1067,82 @@ window.__ModuleLoader__.load({
905
1067
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
906
1068
  set({relBusy:null});
907
1069
  }
908
- async function createGroup(){
909
- var name = "";
910
- try{ if(typeof window!=="undefined" && window.prompt) name = String(window.prompt(t("workshop.group.prompt"), "")||"").trim(); }catch(e){}
1070
+ // ——— 0.9.0 组级共享世界书:一组一份,成员卡里只留私有条目 ———
1071
+ async function loadBook(groupId){
1072
+ var gid = String(groupId || state.currentGroupId || "");
1073
+ if(!gid){ set({book:null}); return; }
1074
+ set({bookBusy:"load"});
1075
+ try{
1076
+ var res = await rpc("cc_getBook", {groupId: gid});
1077
+ if(res && res.ok && res.value) set({book: res.value});
1078
+ else set({book:null, lastMsg:String((res && res.error && res.error.message) || "")});
1079
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1080
+ set({bookBusy:null});
1081
+ }
1082
+ async function patchBook(args){
1083
+ var gid = String(state.currentGroupId||"");
1084
+ if(!gid) return;
1085
+ set({bookBusy:"patch"});
1086
+ try{
1087
+ var res = await rpc("cc_patchBook", Object.assign({groupId: gid}, args||{}));
1088
+ if(res && res.ok && res.value){ await loadBook(gid); set({lastMsg:t("book.saved",{rev:String(res.value.rev)})}); }
1089
+ else set({lastMsg:String((res && res.error && res.error.message) || "")});
1090
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1091
+ set({bookBusy:null});
1092
+ }
1093
+ /** 提升去重:`apply` 为假时只扫描预览(宿主半默认也是 dryRun,两边都不写盘)。 */
1094
+ async function promoteBook(keys, apply){
1095
+ var gid = String(state.currentGroupId||"");
1096
+ if(!gid) return;
1097
+ set({bookBusy: apply? "promote" : "scan"});
1098
+ try{
1099
+ var res = await rpc("cc_promoteBook", {groupId: gid, apply: !!apply, keys: (keys && keys.length)? keys : undefined});
1100
+ if(res && res.ok && res.value){
1101
+ var v = res.value;
1102
+ set({lastMsg: v.dryRun
1103
+ ? t("book.promotePreview",{n:String(v.wouldPromote||0), m:String(v.removedTotal||0)})
1104
+ : t("book.promoteDone",{n:String(v.promoted||0), m:String(v.removedTotal||0)})});
1105
+ await loadBook(gid);
1106
+ if(!v.dryRun){ await refreshLibrary(); await loadGroup(gid); }
1107
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
1108
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1109
+ set({bookBusy:null});
1110
+ }
1111
+ async function applyBook(cardId){
1112
+ var gid = String(state.currentGroupId||"");
1113
+ if(!gid) return;
1114
+ set({bookBusy:"apply"});
1115
+ try{
1116
+ var res = await rpc("cc_applyBook", {groupId: gid, cardId: cardId||undefined});
1117
+ if(res && res.ok && res.value){
1118
+ var v = res.value;
1119
+ set({lastMsg: v.added ? t("book.applyDone",{n:String(v.added)}) : t("book.applyNone")});
1120
+ await refreshLibrary();
1121
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
1122
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1123
+ set({bookBusy:null});
1124
+ }
1125
+ /**
1126
+ * 导出共享世界书:文件本身就是 CCv3 官方的独立 lorebook 格式({spec:"lorebook_v3", data}),
1127
+ * 拷出去就能在聊天客户端里当全局/世界书绑 —— 绑定后所有成员卡共用这一本,卡里不必重复存。
1128
+ */
1129
+ function exportBookFile(){
1130
+ var b = state.book && state.book.book;
1131
+ if(!b) return;
1132
+ try{
1133
+ var blob = new Blob([JSON.stringify({spec:"lorebook_v3", data:b.data}, null, 2)], {type:"application/json"});
1134
+ var a=document.createElement("a"); a.href=URL.createObjectURL(blob);
1135
+ a.download = String(b.data.name||"shared-lorebook").replace(/[\\/:*?"<>|]/g,"_") + ".lorebook.json";
1136
+ a.click();
1137
+ setTimeout(function(){ URL.revokeObjectURL(a.href); }, 800);
1138
+ set({lastMsg: t("book.exported",{name:String(b.data.name||"")})});
1139
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1140
+ }
1141
+ function setBookMerge(v){ set({bookMerge: !!v}); }
1142
+ async function createGroup(nameArg){
1143
+ // 0.8.1:不再依赖 window.prompt(内嵌 webview 会拦掉原生对话框 → 用户看到的是「点了没反应」)。
1144
+ // 名字由侧栏那个内联输入框传进来;没传就当取消。
1145
+ var name = String(nameArg == null ? "" : nameArg).trim();
911
1146
  if(!name) return;
912
1147
  set({relBusy:"new"});
913
1148
  try{
@@ -1080,6 +1315,10 @@ window.__ModuleLoader__.load({
1080
1315
  if(res && res.ok && res.value && res.value.draft){
1081
1316
  setDraftFromHost(res.value.draft);
1082
1317
  set({currentLibraryId: id});
1318
+ // 0.9.0:载入一张卡就**跟着切到它所属的分组**。实机反馈:右边载入了陆昭,中间「人物关系」
1319
+ // 却还停在江晚那个「测试」组,必须再点一次那个组的「关系」按钮才会切过去 —— 非常费解。
1320
+ // 关系视图与共享世界书都是**按组**看的,所以「换卡」必须带着组一起换(没有组就清空)。
1321
+ setCurrentGroupId((res.value.meta && res.value.meta.groupId) ? String(res.value.meta.groupId) : "");
1083
1322
  set({lastMsg:t("store.loadedCard",{name:(res.value.meta.name||id), n:(res.value.draft.data.character_book.entries.length||0), id:id.slice(0,8)})});
1084
1323
  } else set({lastMsg: res && res.error? String(res.error.message):t("store.errLoadFailed")});
1085
1324
  }catch(e){ set({lastMsg: String(e && e.message || e)}); }
@@ -1122,7 +1361,7 @@ window.__ModuleLoader__.load({
1122
1361
  // 0.4.1:这里原本还有一个 exportLibraryEntry —— 直接下载某张已存角色的 JSON。
1123
1362
  // 三个导出入口归一之后,卡片上的「导出」改成切到导出页,导出动作只剩导出页那一套实现,
1124
1363
  // 这条旁路就没有调用者了(留着它等于「同一次导出有两套行为」),故删除。
1125
- return { getSnapshot:getSnapshot, subscribe:subscribe, setCur:setCur, setPanel:setPanel, setSideExpanded:setSideExpanded, setIsCcMode:setIsCcMode, setDraftFromHost:setDraftFromHost, setSessionId:setSessionId, setSlotWarn:setSlotWarn, setAlternateSlots:setAlternateSlots, migrateSlot:migrateSlot, checkIsCcMode:checkIsCcMode, setPollEnabled:setPollEnabled, setPollInterval:setPollInterval, setAutoValidate:setAutoValidate, setAutoSaveOnImport:setAutoSaveOnImport, updateDraft:updateDraft, validate:validate, exportJSON:exportJSON, exportPNG:exportPNG, embedIntoPng:embedIntoPng, exportCharx:exportCharx, importJSON:importJSON, importAny:importAny, setWorldPatch:setWorldPatch, setEditingWorld:setEditingWorld, cancelEditingWorld:cancelEditingWorld, setEditingText:setEditingText, clearEditingText:clearEditingText, cancelEditingText:cancelEditingText, updateEditingText:updateEditingText, addEntry:addEntry, setLibraryQuery:setLibraryQuery, toggleLibraryCollapsed:toggleLibraryCollapsed, refreshLibrary:refreshLibrary, saveCurrentToLibrary:saveCurrentToLibrary, saveAsNewToLibrary:saveAsNewToLibrary, clearCurrentLibraryId:clearCurrentLibraryId, clearDraft:clearDraft, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, renameCurrentInLibrary:renameCurrentInLibrary, setCurrentGroupId:setCurrentGroupId, refreshGroups:refreshGroups, createGroup:createGroup, loadGroup:loadGroup, assignGroup:assignGroup, deleteGroup:deleteGroup, patchRelations:patchRelations, checkGroupNow:checkGroupNow, renderRelationsToCard:renderRelationsToCard };
1364
+ return { getSnapshot:getSnapshot, subscribe:subscribe, setCur:setCur, setPanel:setPanel, setSideExpanded:setSideExpanded, setIsCcMode:setIsCcMode, setDraftFromHost:setDraftFromHost, setSessionId:setSessionId, setSlotWarn:setSlotWarn, setAlternateSlots:setAlternateSlots, migrateSlot:migrateSlot, checkIsCcMode:checkIsCcMode, setPollEnabled:setPollEnabled, setPollInterval:setPollInterval, setAutoValidate:setAutoValidate, setAutoSaveOnImport:setAutoSaveOnImport, updateDraft:updateDraft, validate:validate, exportJSON:exportJSON, exportPNG:exportPNG, embedIntoPng:embedIntoPng, exportCharx:exportCharx, importJSON:importJSON, importAny:importAny, setWorldPatch:setWorldPatch, setEditingWorld:setEditingWorld, cancelEditingWorld:cancelEditingWorld, setEditingText:setEditingText, clearEditingText:clearEditingText, cancelEditingText:cancelEditingText, updateEditingText:updateEditingText, addEntry:addEntry, setLibraryQuery:setLibraryQuery, toggleLibraryCollapsed:toggleLibraryCollapsed, refreshLibrary:refreshLibrary, saveCurrentToLibrary:saveCurrentToLibrary, saveAsNewToLibrary:saveAsNewToLibrary, clearCurrentLibraryId:clearCurrentLibraryId, clearDraft:clearDraft, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, renameCurrentInLibrary:renameCurrentInLibrary, setCurrentGroupId:setCurrentGroupId, refreshGroups:refreshGroups, createGroup:createGroup, loadGroup:loadGroup, assignGroup:assignGroup, deleteGroup:deleteGroup, patchRelations:patchRelations, checkGroupNow:checkGroupNow, renderRelationsToCard:renderRelationsToCard, openRelations:openRelations, openRelationsFor:openRelationsFor, loadBook:loadBook, patchBook:patchBook, promoteBook:promoteBook, applyBook:applyBook, exportBookFile:exportBookFile, setBookMerge:setBookMerge };
1126
1365
  }
1127
1366
 
1128
1367
  var S={
@@ -1362,31 +1601,50 @@ window.__ModuleLoader__.load({
1362
1601
  // 并在导出页把「导出的仍是工坊里这张、你点的那张还没载入」说清楚。清空后回到常态提示。
1363
1602
  var jumpRef=React.useState(null); var jumpFrom=jumpRef[0], setJumpFrom=jumpRef[1];
1364
1603
  // 0.8.0 关系视图的「添加关系」表单(局部状态,不进 store:它只是草稿输入)
1365
- var relFormRef=React.useState({from:"", to:"", type:"friend", strength:3, direction:"one-way", note:""}); var relForm=relFormRef[0], setRelForm=relFormRef[1];
1604
+ var relFormRef=React.useState({from:"", to:"", type:"friend", strength:3, direction:"one-way", note:""}); var relForm=relFormRef[0], setRelForm=relFormRef[1];
1605
+ // 0.9.0:关系的「添加 / 编辑」改成二级弹窗(列表回到只读的「关系表」形态)
1606
+ var relEditRef=React.useState(null); var relEdit=relEditRef[0], setRelEdit=relEditRef[1];
1607
+ var relAddRef=React.useState(false); var relAddOpen=relAddRef[0], setRelAddOpen=relAddRef[1];
1608
+ // 0.8.1:分组名 / NPC 名改用内联输入(不再用 window.prompt —— 内嵌 webview 会拦掉它)
1609
+ var groupDraftRef=React.useState(null); var groupDraft=groupDraftRef[0], setGroupDraft=groupDraftRef[1];
1610
+ var npcDraftRef=React.useState(""); var npcDraft=npcDraftRef[0], setNpcDraft=npcDraftRef[1];
1611
+ // 折叠面板:哪些分组被收起(键 = 分组 id,"" 表示未分组那一节)
1612
+ var collapsedRef=React.useState({}); var collapsedGroups=collapsedRef[0], setCollapsedGroups=collapsedRef[1];
1613
+ // 0.9.0 共享世界书的「+ 新条目」表单(局部状态,不进 store:只是草稿输入)
1614
+ var bookFormRef=React.useState({name:"", keys:"", content:""}); var bookForm=bookFormRef[0], setBookForm=bookFormRef[1];
1615
+ function toggleGroupCollapsed(gid){ var key=String(gid||""); var next=Object.assign({}, collapsedGroups); next[key]=!next[key]; setCollapsedGroups(next); }
1366
1616
  React.useEffect(function(){ return store.subscribe(function(){ setS(store.getSnapshot()); }); }, [store]);
1367
1617
  React.useEffect(function(){
1368
- if(s.panelOpen) store.refreshLibrary();
1618
+ if(s.panelOpen){
1619
+ store.refreshLibrary(); store.refreshGroups();
1620
+ // 0.9.0:面板一开就把当前组的共享世界书拉回来(左侧徽章与共享书视图都靠它)
1621
+ if(String(s.currentGroupId||"") && s.currentGroupId !== "__none") store.loadBook();
1622
+ }
1369
1623
  }, [s.panelOpen]);
1370
1624
  if(!s.panelOpen) return null;
1371
- // 顶部胶囊与左侧导航都只有写作三步:导出不再是「第 4 步」(0.4.1 归一)——
1372
- // 「校验导出」并进左下角那枚按钮,已存角色卡片上的「导出」也指向同一页,三处入口收成两处。
1373
- var steps=[t("step.world"), t("step.char"), t("step.lore")];
1625
+ // 顶部胶囊(会话域那几个快捷入口):0.9.0 起跟左侧新结构走,取最常看的三处 ——
1626
+ // 总览(整卡状态)/ 角色 / 世界书。其余入口都在左侧导航里,胶囊不追求全。
1627
+ var steps=[{cur:5, key:"step.overview"}, {cur:1, key:"step.char"}, {cur:2, key:"step.lore"}];
1374
1628
  // 步骤项的三个视觉状态严格互斥:选中(active) / 键盘焦点(focus-visible) / 悬停(hover)。
1375
1629
  // 鼠标点击走 onMouseDown preventDefault,不把焦点抢到按钮上 —— 否则旧项的焦点环会留在原地,
1376
1630
  // 与新的选中项同时发亮,看起来像「同时选中了两个」(这是本次修的问题)。
1377
- function navItem(i, title, desc){
1378
- var active=s.cur===i;
1379
- var hovered=hoverKey==="nav:"+i;
1380
- var focused=focusKey==="nav:"+i;
1381
- return h("button",{key:i, type:"button", "aria-current": active? "step": undefined,
1631
+ function navItem(cur, title, desc, prog){
1632
+ var active=s.cur===cur;
1633
+ var hovered=hoverKey==="nav:"+cur;
1634
+ var focused=focusKey==="nav:"+cur;
1635
+ var dot = prog ? stateDot(prog.state) : null;
1636
+ return h("button",{key:"step:"+cur, type:"button", "aria-current": active? "step": undefined,
1382
1637
  style: Object.assign({}, S.navBtn, hovered&&!active? S.navBtnHover:{}, focused? S.navBtnFocus:{}, active? S.navBtnActive:{}),
1383
1638
  onMouseDown:function(e){ if(e && e.preventDefault) e.preventDefault(); },
1384
- onMouseEnter:function(){ setHoverKey("nav:"+i); },
1639
+ onMouseEnter:function(){ setHoverKey("nav:"+cur); },
1385
1640
  onMouseLeave:function(){ setHoverKey(null); },
1386
- onFocus:function(e){ setFocusKey(isFocusVisible(e)? "nav:"+i : null); },
1641
+ onFocus:function(e){ setFocusKey(isFocusVisible(e)? "nav:"+cur : null); },
1387
1642
  onBlur:function(){ setFocusKey(null); },
1388
- onClick:function(){ store.setCur(i); }
1389
- }, title, h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2}}, desc));
1643
+ onClick:function(){ store.setCur(cur); }
1644
+ }, title,
1645
+ dot? h("span",{style:{fontSize:11, color:dot.color, marginLeft:6}}, dot.text) : null,
1646
+ (prog && prog.note)? h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginLeft:6}}, prog.note) : null,
1647
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2}}, desc));
1390
1648
  }
1391
1649
  // 工具按钮(⛶ / 删除…)鼠标按下就拦掉默认行为:别把焦点抢走,否则又留下一圈焦点描边
1392
1650
  function stopFocusSteal(e){ if(e && e.preventDefault) e.preventDefault(); }
@@ -1468,6 +1726,31 @@ window.__ModuleLoader__.load({
1468
1726
  })
1469
1727
  );
1470
1728
  }
1729
+ // 0.9.0 总览用的进度行:与工具半 workflow.state 同口径(三态),但纯客户端算 —— 省一次 RPC,
1730
+ // 也保证「左侧导航的徽章」和「总览页」永远一致(同一个函数算出来的)。
1731
+ function draftProgress(s2){
1732
+ var d = s2.draft.data;
1733
+ var entries = Array.isArray(d.character_book && d.character_book.entries) ? d.character_book.entries : [];
1734
+ var manual = entries.filter(function(e){ return !(e && (e.cc_auto || (e.extensions && e.extensions.cc_auto))); }).length;
1735
+ var filled = function(v){ return String(v==null? "": v).trim().length > 0; };
1736
+ var charN = [d.name, d.description, d.personality, d.scenario].filter(filled).length;
1737
+ var worldN = ["timeline","factions","geo","power","daily"].filter(function(k){ return filled(s2.world[k]); }).length;
1738
+ var alt = (d.alternate_greetings||[]).length, grp = (d.group_only_greetings||[]).length;
1739
+ var firstOk = filled(d.first_mes);
1740
+ var three = function(n, need){ return n>=need ? "ok" : (n>0 ? "partial" : "missing"); };
1741
+ return [
1742
+ { cur:1, title:t("step.char"), state: three(charN,4), note: charN+"/4" },
1743
+ { cur:0, title:t("step.world"), state: three(worldN,3), note: worldN+"/5" },
1744
+ { cur:2, title:t("step.lore"), state: (entries.length>=5 && manual>=1) ? "ok" : (entries.length>0 ? "partial" : "missing"),
1745
+ note: t("common.entries",{n:String(entries.length)}) },
1746
+ { cur:6, title:t("step.greetings"), state: (firstOk && alt>=2 && grp>=1) ? "ok" : ((firstOk||alt||grp) ? "partial" : "missing"),
1747
+ note: (firstOk?1:0)+"+"+alt+"+"+grp },
1748
+ ];
1749
+ }
1750
+ function stateDot(state){
1751
+ return { color: state==="ok" ? "#a6e8c8" : (state==="partial" ? "#ffc46b" : "var(--dsw-alias-label-secondary)"),
1752
+ text: state==="ok" ? "✓" : (state==="partial" ? "◐" : "○") };
1753
+ }
1471
1754
  var main=null;
1472
1755
  if(s.cur===0){
1473
1756
  // 五维世界观:默认直接读全文(无边框 textarea 自动撑高),⛶ 仍可放大框写长文
@@ -1518,7 +1801,135 @@ window.__ModuleLoader__.load({
1518
1801
  field("system_prompt","system_prompt",{rowKey:"sys"}),
1519
1802
  field("post_history_instructions","post_history_instructions",{rowKey:"phi"})
1520
1803
  )
1521
- ),
1804
+ )
1805
+ );
1806
+ } else if(s.cur===5){
1807
+ // ── 总览(0.9.0 新增):一眼看完整张卡 + 快捷动作。写作已交给 LLM,工坊的入口应该是「状态」。
1808
+ var prog=draftProgress(s);
1809
+ main=h("div",null,
1810
+ h("div",{style:S.readCard},
1811
+ h("h3",{style:S.readTitle}, t("step.overview"),
1812
+ h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"var(--dsw-alias-label-secondary)"}},
1813
+ "· "+(String(s.draft.data.name||"").trim() || t("workshop.library.unnamed")))),
1814
+ h("div",{style:{display:"flex", flexWrap:"wrap", gap:6, marginBottom:10}},
1815
+ (s.draft.data.tags||[]).map(function(tg){ return h("span",{key:String(tg), style:S.badge}, String(tg)); }),
1816
+ String(s.draft.data.creator||"").trim()? h("span",{style:S.badge}, "✎ "+String(s.draft.data.creator)) : null,
1817
+ (s.draftStatus && s.draftStatus.cardId)? h("span",{style:S.badge}, "ID:"+String(s.draftStatus.cardId).slice(0,8)) : null),
1818
+ prog.map(function(row){
1819
+ var dot=stateDot(row.state);
1820
+ return h("button",{key:"prog-"+row.cur, type:"button",
1821
+ style:{display:"flex", alignItems:"center", gap:10, width:"100%", textAlign:"left", background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:10, padding:"8px 10px", marginBottom:6, cursor:"pointer", color:"var(--dsw-alias-label-primary)", font:"inherit"},
1822
+ onMouseDown:stopFocusSteal, onClick:function(){ store.setCur(row.cur); }},
1823
+ h("span",{style:{fontSize:14, width:16, textAlign:"center", color:dot.color}}, dot.text),
1824
+ h("span",{style:{flex:1, fontSize:13}}, row.title),
1825
+ h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, row.note));
1826
+ }),
1827
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", margin:"10px 0", lineHeight:1.7}}, t("step.overview.desc")),
1828
+ h("div",{style:{display:"flex", gap:8, flexWrap:"wrap"}},
1829
+ h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.validate(); }}, s.busy==="validate"? t("action.validating") : t("action.validate")),
1830
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.setCur(3); }}, t("step.export")),
1831
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.saveCurrentToLibrary(); }}, t("workshop.library.save"))),
1832
+ s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg):null)
1833
+ );
1834
+ } else if(s.cur===7){
1835
+ // ── 共享世界书(0.9.0):一组一份,成员卡里只留角色私有条目 ──
1836
+ var bk = s.book;
1837
+ var gidB = String(s.currentGroupId||"");
1838
+ var bookReady = !!(bk && bk.exists && bk.book);
1839
+ var dup = (bk && bk.scan && bk.scan.candidates) ? bk.scan.candidates : [];
1840
+ main=h("div",null,
1841
+ h("div",{style:S.readCard},
1842
+ h("h3",{style:S.readTitle}, t("book.title"),
1843
+ bookReady? h("span",{style:Object.assign({}, S.badge, {marginLeft:8})}, "rev "+bk.rev) : null,
1844
+ (bk && bk.group)? h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"var(--dsw-alias-label-secondary)"}}, "· "+bk.group.name) : null),
1845
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("book.hint")),
1846
+ !gidB
1847
+ ? h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, t("book.noGroup"))
1848
+ : h("div",null,
1849
+ // 书级参数:defaultValue + onBlur 提交 —— 不随每次击键发 RPC
1850
+ h("div",{style:{display:"grid", gridTemplateColumns:"1fr 1fr", gap:"0 18px"}},
1851
+ h("div",{style:S.fieldRow},
1852
+ h("div",{style:S.fieldLabel}, h("label",{style:S.fieldName}, t("book.name"))),
1853
+ h("input",{style:S.readInput, defaultValue: bookReady? bk.book.data.name : "", placeholder:t("workshop.field.empty"),
1854
+ onBlur:function(ev){ if(bookReady && ev.target.value !== bk.book.data.name) store.patchBook({name: ev.target.value}); }})),
1855
+ h("div",{style:S.fieldRow},
1856
+ h("div",{style:S.fieldLabel}, h("label",{style:S.fieldName}, t("book.budget"))),
1857
+ h("input",{style:S.readInput, defaultValue: bookReady? String(bk.book.data.token_budget) : "", placeholder:"2000",
1858
+ onBlur:function(ev){ var n=Number(ev.target.value); if(bookReady && n>0 && n!==bk.book.data.token_budget) store.patchBook({token_budget:n}); }})),
1859
+ h("div",{style:S.fieldRow},
1860
+ h("div",{style:S.fieldLabel}, h("label",{style:S.fieldName}, t("book.depth"))),
1861
+ h("input",{style:S.readInput, defaultValue: bookReady? String(bk.book.data.scan_depth) : "", placeholder:"4",
1862
+ onBlur:function(ev){ var n=Number(ev.target.value); if(bookReady && n>0 && n!==bk.book.data.scan_depth) store.patchBook({scan_depth:n}); }})),
1863
+ h("div",{style:S.fieldRow},
1864
+ h("div",{style:S.fieldLabel}, h("label",{style:S.fieldName}, t("book.desc"))),
1865
+ h("input",{style:S.readInput, defaultValue: bookReady? bk.book.data.description : "", placeholder:t("workshop.field.empty"),
1866
+ onBlur:function(ev){ if(bookReady && ev.target.value !== bk.book.data.description) store.patchBook({description: ev.target.value}); }}))),
1867
+ h("div",{style:{marginTop:12, marginBottom:6, fontSize:12, fontWeight:600}},
1868
+ t("book.entries",{n:String(bookReady? bk.book.data.entries.length : 0)}),
1869
+ bookReady? h("span",{style:Object.assign({}, S.badge, {marginLeft:8})}, t("book.tokens",{n:String(bk.tokens||0)})) : null),
1870
+ // ⚠️ 宿主端点给的是**原始文档**(book.data.entries)—— 预览与长度在客户端算、删除按 index 定位。
1871
+ // 别再读成顶层字段:关系视图就是这么踩过一次(数据在 group 里、界面却显示 0 条)。
1872
+ bookReady? h("div",null, bk.book.data.entries.map(function(e, ei){
1873
+ var txt = String(e.content||"");
1874
+ return h("div",{key:ei, style:{border:"1px solid var(--dsw-alias-border-l1)", borderRadius:10, padding:"8px 10px", marginBottom:8, background:"var(--dsw-alias-bg-base)"}},
1875
+ h("div",{style:{display:"flex", alignItems:"center", gap:6, flexWrap:"wrap"}},
1876
+ h("span",{style:{fontWeight:600, fontSize:12}}, e.name||"—"),
1877
+ h("span",{style:Object.assign({}, S.badge, {fontSize:10})}, e.constant? t("book.constant") : ((e.keys||[]).join(", ")||"—")),
1878
+ h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)"}}, String(txt.length)+" chars"),
1879
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {marginLeft:"auto", padding:"2px 8px", fontSize:11, border:"1px solid #3a2a2a", color:"#e8a6a6", background:"transparent"}), onMouseDown:stopFocusSteal,
1880
+ onClick:function(){ store.patchBook({ops:[{op:"removeEntry", index:ei}]}); }}, t("common.delete"))),
1881
+ h("div",{style:{fontSize:12, lineHeight:1.7, marginTop:4, whiteSpace:"pre-wrap", wordBreak:"break-word", color:"var(--dsw-alias-label-secondary)"}}, txt.slice(0,80).replace(/\s+/g," ") + (txt.length>80? " …":"")));
1882
+ })) : null,
1883
+ // 新条目:内联表单(不用 window.prompt —— 内嵌 webview 会拦掉原生对话框,表现为「点了没反应」)
1884
+ h("div",{style:{marginTop:6, paddingTop:10, borderTop:"1px dashed var(--dsw-alias-border-l1)"}},
1885
+ h("div",{style:{display:"flex", gap:6, flexWrap:"wrap", alignItems:"center"}},
1886
+ h("input",{placeholder:t("book.entryName"), value:bookForm.name, style:{width:120, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 8px", fontSize:12}, onChange:function(ev){ setBookForm(Object.assign({}, bookForm, {name: ev.target.value})); }}),
1887
+ h("input",{placeholder:t("book.entryKeys"), value:bookForm.keys, style:{width:180, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 8px", fontSize:12}, onChange:function(ev){ setBookForm(Object.assign({}, bookForm, {keys: ev.target.value})); }}),
1888
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 12px", fontSize:12}), onMouseDown:stopFocusSteal,
1889
+ disabled: s.bookBusy==="patch",
1890
+ onClick:function(){
1891
+ var content=String(bookForm.content||"").trim();
1892
+ if(!content) return;
1893
+ store.patchBook({ops:[{op:"addEntry", entry:{ name:String(bookForm.name||"").trim(),
1894
+ keys:String(bookForm.keys||"").split(",").map(function(x){ return x.trim(); }).filter(Boolean), content: content }}]});
1895
+ setBookForm({name:"", keys:"", content:""});
1896
+ }}, s.bookBusy==="patch"? "…" : t("action.addEntry"))),
1897
+ h("textarea",{placeholder:t("book.entryContent"), value:bookForm.content, style:Object.assign({}, S.textarea, {minHeight:70, marginTop:6}),
1898
+ onChange:function(ev){ setBookForm(Object.assign({}, bookForm, {content: ev.target.value})); }})),
1899
+ // 成员卡之间的重复条目 → 一键提升(去重的正解)
1900
+ h("div",{style:{marginTop:14, paddingTop:10, borderTop:"1px dashed var(--dsw-alias-border-l1)"}},
1901
+ h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}}, t("book.dupTitle")),
1902
+ dup.length===0
1903
+ ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("book.dupNone"))
1904
+ : h("div",null,
1905
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:6, lineHeight:1.7}}, t("book.promoteHint")),
1906
+ dup.map(function(c, i){
1907
+ return h("div",{key:c.signature||i, style:{border:"1px solid var(--dsw-alias-border-l1)", borderRadius:10, padding:"8px 10px", marginBottom:6, background:"var(--dsw-alias-bg-base)"}},
1908
+ h("div",{style:{display:"flex", alignItems:"center", gap:6, flexWrap:"wrap"}},
1909
+ h("span",{style:{fontWeight:600, fontSize:12}}, c.name||"—"),
1910
+ h("span",{style:Object.assign({}, S.badge, {fontSize:10})}, t("book.dupItem",{n:String(c.cards), chars:String(c.savedChars||0)})),
1911
+ (c.nameMismatch||c.keysMismatch)? h("span",{style:Object.assign({}, S.badge, {fontSize:10, color:"#ffc46b", borderColor:"#ffc46b"})}, t("book.dupMismatch")) : null),
1912
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:4, whiteSpace:"pre-wrap", wordBreak:"break-word"}}, c.preview));
1913
+ }),
1914
+ h("div",{style:{display:"flex", gap:8, marginTop:8, flexWrap:"wrap"}},
1915
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 12px", fontSize:12}), onMouseDown:stopFocusSteal,
1916
+ disabled: !!(s.bookBusy==="scan"||s.bookBusy==="promote"),
1917
+ onClick:function(){ store.promoteBook(null, true); }}, (s.bookBusy==="promote")? "…" : t("book.promote")))))),
1918
+ s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg):null),
1919
+ (gidB && bookReady) ? h("div",{style:S.readCard},
1920
+ h("h3",{style:S.readTitle}, t("export.book.title")),
1921
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("export.book.hint")),
1922
+ h("div",{style:{display:"flex", gap:8, flexWrap:"wrap"}},
1923
+ h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
1924
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, disabled: s.bookBusy==="apply", onClick:function(){ store.applyBook(); }}, s.bookBusy==="apply"? "…" : t("book.applyCard")),
1925
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.loadBook(); }}, t("book.refresh"))),
1926
+ s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, s.lastMsg):null) : null
1927
+ );
1928
+ } else if(s.cur===6){
1929
+ // 0.9.0:开场与示例从「角色」页拆出来 —— 左侧导航改按**卡片结构**排(角色 / 世界观 / 世界书 /
1930
+ // 开场与示例 / 关系与分组 / 共享世界书),不再按「写作第几步」排:写作已交给 LLM,
1931
+ // 工坊现在的职责是「看 + 微调 + 导出」,按结构分页比按步骤分页好找。
1932
+ main=h("div",null,
1522
1933
  h("div",{style:S.readCard},
1523
1934
  h("h3",{style:S.readTitle},t("workshop.char.openingTitle")),
1524
1935
  field("first_mes","first_mes",{rowKey:"first"}),
@@ -1575,6 +1986,27 @@ window.__ModuleLoader__.load({
1575
1986
  h("button",{style: Object.assign({}, S.btn, S.btnGhost), onClick:function(){ store.validate(); }}, s.busy==="validate"? t("action.validating"): t("action.validate")),
1576
1987
  h("span",{style:{color:"var(--dsw-alias-label-secondary)", fontSize:11, alignSelf:"center"}}, t("workshop.export.hint2"))
1577
1988
  ),
1989
+ // 0.9.0:组级共享世界书 —— 导出独立文件(零重复的做法),或按需把共享条目并进这张卡
1990
+ (function(){
1991
+ var bk=s.book, gid=String(s.currentGroupId||"");
1992
+ var ready=!!(bk && bk.exists && bk.book);
1993
+ if(!gid || !ready){
1994
+ return h("div",{style:{fontSize:11, margin:"-4px 0 10px", lineHeight:1.7, color:"var(--dsw-alias-label-secondary)"}},
1995
+ t("export.book.title") + " · " + t("export.book.none"));
1996
+ }
1997
+ return h("div",{style:{border:"1px solid var(--dsw-alias-border-l1)", borderRadius:10, padding:"10px 12px", margin:"-4px 0 12px", background:"var(--dsw-alias-bg-base)"}},
1998
+ h("div",{style:{display:"flex", alignItems:"center", gap:8, flexWrap:"wrap"}},
1999
+ h("strong",{style:{fontSize:12}}, t("export.book.title")),
2000
+ h("span",{style:S.badge}, bk.book.name||"—"),
2001
+ h("span",{style:S.badge}, t("book.entries",{n:String(bk.book.entries.length)})),
2002
+ h("span",{style:S.badge}, t("book.tokens",{n:String(bk.tokens||0)}))),
2003
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", margin:"6px 0 8px", lineHeight:1.7}}, t("export.book.hint")),
2004
+ h("div",{style:{display:"flex", gap:8, flexWrap:"wrap", alignItems:"center"}},
2005
+ h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
2006
+ h("label",{style:{display:"flex", alignItems:"center", gap:6, fontSize:11, color:"var(--dsw-alias-label-secondary)", cursor:"pointer"}},
2007
+ h("input",{type:"checkbox", checked:!!s.bookMerge, onChange:function(ev){ store.setBookMerge(ev.target.checked); }}),
2008
+ t("export.book.merge"))));
2009
+ })(),
1578
2010
  // 导出页只认「当前工坊里的卡」:右侧卡片上的「导出」也是切到这一页,所以这里把口径写清楚
1579
2011
  // 从卡片跳进来、且那张卡不是工坊里这张时,把口径说透(导出的是谁、要导它该怎么做);
1580
2012
  // 其余情况只留常态说明。
@@ -1594,94 +2026,139 @@ window.__ModuleLoader__.load({
1594
2026
  // ── 关系视图(s.cur === 4)──
1595
2027
  // 与导出页同属「不在左侧导航里的视图」:入口是右侧分组选择器旁的「关系」按钮。
1596
2028
  // 这里编辑的是**插件侧图谱**(<DSH_HOME>/cc-groups/);只有「渲染到当前卡」才会把关系写进 description。
1597
- var relNodes = (s.relations && s.relations.nodes) || [];
1598
- var relEdges = (s.relations && s.relations.edges) || [];
2029
+ // ⚠️ 节点与边在 `relations.group` **里面** —— 宿主半 cc_getGroup 的返回是 { group, members, rev }。
2030
+ // 曾经按工具半 cc_get_relations 的形状读顶层的 s.relations.nodes / .edges,于是**永远读到空数组**:
2031
+ // 界面显示「关系 · 0」、NPC 名字区空白,而 rev 与成员数正常(一个在 group 里、一个在顶层)——
2032
+ // 看起来像「卡里的关系没读到」,实际是客户端读错了返回形状。
2033
+ var relGroup = (s.relations && s.relations.group) || null;
2034
+ var relNodes = (relGroup && relGroup.nodes) || [];
2035
+ var relEdges = (relGroup && relGroup.edges) || [];
1599
2036
  var relMembers = (s.relations && s.relations.members) || [];
1600
- var relGroup = s.relations && s.relations.group;
1601
2037
  var gid = String(s.currentGroupId || "");
1602
2038
  var canEdit = !!gid && gid !== "__none";
1603
2039
  var relTypes = ["family","romance","mentor","friend","colleague","rival","enemy","debt","superior","other"];
1604
2040
  var nodeName = function(id){ var hit = relNodes.filter(function(x){ return x.id === id; })[0]; return hit ? hit.name : id; };
1605
2041
  var setF = function(patch){ setRelForm(Object.assign({}, relForm, patch)); };
1606
2042
  var smallSel = {fontSize:11, background:"transparent", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:6, color:"var(--dsw-alias-label-primary)", padding:"1px 4px"};
2043
+ // 「当前卡」在组里的节点(用来在成员区标出「我」、并给「添加关系」挑默认的 from)
2044
+ var myNode = relNodes.filter(function(n){ return n.kind === "card" && n.cardId === String(s.currentLibraryId||""); })[0];
2045
+ var myNodeId = myNode ? myNode.id : "";
2046
+ var relLabelOf = function(e){ return (e && e.label && String(e.label).trim()) ? String(e.label).trim() : t("rel.type."+((e && e.type) || "other")); };
2047
+ /**
2048
+ * 关系编辑的二级弹窗(添加 / 修改共用)。
2049
+ * 为什么挪出来:原来每行关系都摆着「类型 / 强度 / 方向」三个下拉 + ✕,12 条关系就把整页占满,
2050
+ * 读起来也不像「关系表」。现在列表是一行只读文本 + 一枚「编辑」,改哪条点哪条。
2051
+ * 弹窗用 position:fixed 明确覆盖全屏(不依赖 S.overlay 的定位方式,因为它可能挂在主区里)。
2052
+ */
2053
+ function relDialog(titleKey, val, setVal, onSave, onDelete, withNodes){
2054
+ var setV = function(patch){ setVal(Object.assign({}, val, patch)); };
2055
+ var selStyle = {fontSize:12, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 8px"};
2056
+ var labStyle = {fontSize:11, color:"var(--dsw-alias-label-secondary)"};
2057
+ var close = function(){ setVal(null); };
2058
+ return h("div",{style:{position:"fixed", inset:0, background:"rgba(0,0,0,.55)", zIndex:45, display:"flex", alignItems:"center", justifyContent:"center"},
2059
+ onClick:function(e){ if(e.target===e.currentTarget) close(); }},
2060
+ h("div",{style:{width:"min(470px, 92vw)", background:"var(--dsw-alias-bg-layer-1)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:16, padding:"16px", boxShadow:"var(--dsw-shadow-lv3)"}},
2061
+ h("div",{style:{display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12}},
2062
+ h("strong",{style:{fontSize:14, color:"var(--dsw-alias-label-primary)"}}, t(titleKey)),
2063
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {padding:"4px 10px"}), onMouseDown:stopFocusSteal, onClick:close}, t("action.closeX"))),
2064
+ withNodes ? h("div",{style:{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginBottom:10}},
2065
+ h("label",{style:labStyle}, t("workshop.rel.from"),
2066
+ h("select",{style:Object.assign({}, selStyle, {width:"100%", marginTop:4}), value:val.from, onChange:function(ev){ setV({from:ev.target.value}); }},
2067
+ relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); }))),
2068
+ h("label",{style:labStyle}, t("workshop.rel.to"),
2069
+ h("select",{style:Object.assign({}, selStyle, {width:"100%", marginTop:4}), value:val.to, onChange:function(ev){ setV({to:ev.target.value}); }},
2070
+ relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); })))) : null,
2071
+ h("div",{style:{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:10}},
2072
+ h("label",{style:labStyle}, t("workshop.rel.typeLabel"),
2073
+ h("select",{style:Object.assign({}, selStyle, {width:"100%", marginTop:4}), value:val.type, onChange:function(ev){ setV({type:ev.target.value}); }},
2074
+ relTypes.map(function(ty){ return h("option",{key:ty, value:ty}, t("rel.type."+ty)); }))),
2075
+ h("label",{style:labStyle}, t("workshop.rel.strength"),
2076
+ h("select",{style:Object.assign({}, selStyle, {width:"100%", marginTop:4}), value:String(val.strength), onChange:function(ev){ setV({strength:Number(ev.target.value)}); }},
2077
+ [1,2,3,4,5].map(function(n){ return h("option",{key:n, value:String(n)}, n+"/5"); }))),
2078
+ h("label",{style:labStyle}, t("workshop.rel.dirLabel"),
2079
+ h("select",{style:Object.assign({}, selStyle, {width:"100%", marginTop:4}), value:val.direction, onChange:function(ev){ setV({direction:ev.target.value}); }},
2080
+ h("option",{value:"one-way"}, t("workshop.rel.dirOneWay")),
2081
+ h("option",{value:"mutual"}, t("workshop.rel.dirMutual"))))),
2082
+ h("input",{placeholder:t("workshop.rel.note"), value:val.note||"", style:Object.assign({}, selStyle, {width:"100%", boxSizing:"border-box", marginTop:10}), onChange:function(ev){ setV({note:ev.target.value}); }}),
2083
+ h("div",{style:{display:"flex", gap:8, marginTop:14, justifyContent:"space-between"}},
2084
+ onDelete ? h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid rgba(224,85,85,.55)", color:"#e05555", background:"transparent", padding:"4px 12px"}), onMouseDown:stopFocusSteal, onClick:onDelete}, t("common.delete")) : h("span",null),
2085
+ h("div",{style:{display:"flex", gap:8}},
2086
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-primary)", padding:"4px 12px"}), onMouseDown:stopFocusSteal, onClick:close}, t("action.cancel")),
2087
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 14px"}), onMouseDown:stopFocusSteal, onClick:function(){ onSave(val); }}, t("action.done"))))));
2088
+ }
1607
2089
  main = h("div",null,
1608
2090
  h("div",{style:S.readCard},
1609
2091
  h("h3",{style:S.readTitle}, t("workshop.rel.title"),
1610
2092
  h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"var(--dsw-alias-label-secondary)"}}, "· " + ((relGroup && relGroup.name) || t("workshop.rel.noGroup"))),
1611
- relGroup ? h("span",{style:Object.assign({}, S.badge, {marginLeft:8})}, "rev " + relGroup.rev) : null),
2093
+ relGroup ? h("span",{style:Object.assign({}, S.badge, {marginLeft:8})}, "rev " + relGroup.rev) : null,
2094
+ // 0.9.0:把「这是哪张卡的视角」写在标题上 —— 关系是按组看的,很容易看错对象
2095
+ myNode ? h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"#7c5cff"}}, t("workshop.rel.myCard") + ":" + nodeName(myNodeId)) : null),
1612
2096
  h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("workshop.rel.hint")),
1613
2097
  !canEdit ? h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.pickGroup")) : h("div",null,
1614
2098
  h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}}, t("workshop.rel.members") + " · " + relMembers.length),
1615
2099
  h("div",{style:{display:"flex", flexWrap:"wrap", gap:6, marginBottom:14, alignItems:"center"}},
1616
- relMembers.map(function(m){ return h("span",{key:m.id, style:S.badge}, m.name); }),
2100
+ relMembers.map(function(m){
2101
+ var isMe = String(m.id) === String(s.currentLibraryId||"");
2102
+ return h("span",{key:m.id, style:Object.assign({}, S.badge, isMe? {borderColor:"#7c5cff", color:"#7c5cff"}:{})}, m.name + (isMe? " · " + t("workshop.rel.myCard") : ""));
2103
+ }),
1617
2104
  relMembers.length===0 ? h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.noMember")) : null),
1618
2105
  h("div",{style:{display:"flex", flexWrap:"wrap", gap:6, marginBottom:14, alignItems:"center"}},
1619
2106
  relNodes.filter(function(n){ return n.kind === "npc"; }).map(function(n){ return h("span",{key:n.id, style:S.badge}, n.name); }),
2107
+ h("input",{placeholder:t("workshop.rel.nodePrompt"), value:npcDraft, style:{width:150, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"2px 6px", fontSize:11},
2108
+ onChange:function(ev){ setNpcDraft(ev.target.value); },
2109
+ onKeyDown:function(ev){ var v=String(npcDraft||"").trim(); if(ev.key==="Enter" && v){ store.patchRelations([{op:"addNode", kind:"npc", name:v}]); setNpcDraft(""); } }}),
1620
2110
  h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"2px 8px", fontSize:11}), onClick:function(){
1621
- var name = "";
1622
- try{ if(typeof window!=="undefined" && window.prompt) name = String(window.prompt(t("workshop.rel.nodePrompt"), "")||"").trim(); }catch(e){}
1623
- if(name) store.patchRelations([{op:"addNode", kind:"npc", name:name}]);
2111
+ var name = String(npcDraft||"").trim();
2112
+ if(name){ store.patchRelations([{op:"addNode", kind:"npc", name:name}]); setNpcDraft(""); }
1624
2113
  }}, t("workshop.rel.addNode"))),
1625
- h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}}, t("workshop.rel.edges") + " · " + relEdges.length),
2114
+ h("div",{style:{display:"flex", alignItems:"center", gap:8, marginBottom:6}},
2115
+ h("div",{style:{fontSize:12, fontWeight:600, flex:1}}, t("workshop.rel.edges") + " · " + relEdges.length),
2116
+ // 「添加关系」挪进二级弹窗:原来那张内联表单要占两行,把列表挤得很长
2117
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"3px 10px", fontSize:11}), onMouseDown:stopFocusSteal,
2118
+ onClick:function(){
2119
+ var fromId = myNodeId || ((relNodes[0] && relNodes[0].id) || "");
2120
+ var other = relNodes.filter(function(n){ return n.id !== fromId; })[0];
2121
+ setRelForm({ from: fromId, to: other ? other.id : "", type: "friend", strength: 3, direction: "one-way", note: "" });
2122
+ setRelAddOpen(true);
2123
+ }}, t("workshop.rel.add"))),
1626
2124
  relEdges.length===0 ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10}}, t("workshop.rel.noEdge")) :
2125
+ // 一行只读 + 一枚「编辑」:类型/强度/方向/备注都在弹窗里改,列表因此回到「关系表」的样子
1627
2126
  h("div",null, relEdges.map(function(e){
1628
- return h("div",{key:e.id, style:{borderBottom:"1px solid var(--dsw-alias-border-l1)", padding:"6px 0"}},
1629
- h("div",{style:{display:"flex", alignItems:"center", gap:6, flexWrap:"wrap"}},
1630
- h("span",{style:{fontSize:12}}, nodeName(e.from)),
1631
- h("span",{style:{color:"#7c5cff"}}, e.direction === "mutual" ? "⇄" : "→"),
1632
- h("span",{style:{fontSize:12}}, nodeName(e.to)),
1633
- h("select",{style:smallSel, value:e.type, onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, type:ev.target.value}]); }},
1634
- relTypes.map(function(ty){ return h("option",{key:ty, value:ty}, t("rel.type."+ty)); })),
1635
- h("select",{style:smallSel, value:String(e.strength), onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, strength:Number(ev.target.value)}]); }},
1636
- [1,2,3,4,5].map(function(n){ return h("option",{key:n, value:String(n)}, n + "/5"); })),
1637
- h("select",{style:smallSel, value:e.direction, onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, direction:ev.target.value}]); }},
1638
- h("option",{value:"one-way"}, t("workshop.rel.dirOneWay")),
1639
- h("option",{value:"mutual"}, t("workshop.rel.dirMutual"))),
1640
- h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:6, padding:"1px 6px", fontSize:11}), onClick:function(){ store.patchRelations([{op:"removeEdge", edgeId:e.id}]); }}, "✕")),
1641
- e.note ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2}}, e.note) : null);
2127
+ return h("div",{key:e.id, style:{display:"flex", alignItems:"center", gap:8, padding:"5px 0", borderBottom:"1px solid var(--dsw-alias-border-l1)"}},
2128
+ h("span",{style:{fontSize:12, flexShrink:0}}, nodeName(e.from)),
2129
+ h("span",{style:{color:"#7c5cff", flexShrink:0}}, e.direction === "mutual" ? "⇄" : "→"),
2130
+ h("span",{style:{fontSize:12, flexShrink:0}}, nodeName(e.to)),
2131
+ h("span",{style:Object.assign({}, S.badge, {fontSize:10, flexShrink:0})}, relLabelOf(e)),
2132
+ h("span",{style:Object.assign({}, S.badge, {fontSize:10, flexShrink:0})}, e.strength+"/5"),
2133
+ h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", flex:1, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}, e.note||""),
2134
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {flexShrink:0, padding:"2px 8px", fontSize:11, border:"1px solid var(--dsw-alias-border-l1)", color:"var(--dsw-alias-label-primary)", background:"transparent"}), onMouseDown:stopFocusSteal,
2135
+ onClick:function(){ setRelEdit({ edgeId:e.id, from:e.from, to:e.to, type:e.type, strength:e.strength, direction:e.direction, note:e.note||"" }); }}, t("workshop.rel.edit")));
1642
2136
  })),
1643
- h("div",{style:{marginTop:14, paddingTop:10, borderTop:"1px dashed var(--dsw-alias-border-l1)"}},
1644
- h("div",{style:{fontSize:12, fontWeight:600, marginBottom:6}}, t("workshop.rel.add")),
1645
- h("div",{style:{display:"flex", gap:6, flexWrap:"wrap", alignItems:"center"}},
1646
- h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.from")),
1647
- h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)", maxWidth:130}), value:relForm.from, onChange:function(ev){ setF({from:ev.target.value}); }},
1648
- h("option",{value:""}, "—"),
1649
- relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); })),
1650
- h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.to")),
1651
- h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)", maxWidth:130}), value:relForm.to, onChange:function(ev){ setF({to:ev.target.value}); }},
1652
- h("option",{value:""}, "—"),
1653
- relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); })),
1654
- h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:relForm.type, onChange:function(ev){ setF({type:ev.target.value}); }},
1655
- relTypes.map(function(ty){ return h("option",{key:ty, value:ty}, t("rel.type."+ty)); })),
1656
- h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:String(relForm.strength), onChange:function(ev){ setF({strength:Number(ev.target.value)}); }},
1657
- [1,2,3,4,5].map(function(n){ return h("option",{key:n, value:String(n)}, t("workshop.rel.strength") + " " + n); })),
1658
- h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:relForm.direction, onChange:function(ev){ setF({direction:ev.target.value}); }},
1659
- h("option",{value:"one-way"}, t("workshop.rel.dirOneWay")),
1660
- h("option",{value:"mutual"}, t("workshop.rel.dirMutual"))),
1661
- h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"3px 10px", fontSize:11}), onClick:function(){ store.patchRelations([{op:"addEdge", from:relForm.from, to:relForm.to, type:relForm.type, strength:relForm.strength, direction:relForm.direction, note:relForm.note}]); }}, t("workshop.rel.add"))),
1662
- h("input",{placeholder:t("workshop.rel.note"), style:{width:"100%", boxSizing:"border-box", marginTop:6, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 8px", fontSize:12}, value:relForm.note, onChange:function(ev){ setF({note:ev.target.value}); }})),
1663
2137
  h("div",{style:{display:"flex", gap:8, marginTop:14, flexWrap:"wrap"}},
1664
2138
  h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 12px", fontSize:12}), onClick:function(){ store.renderRelationsToCard(); }, disabled: s.relBusy==="render"}, s.relBusy==="render" ? "…" : t("workshop.rel.render")),
1665
2139
  h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-primary)", borderRadius:8, padding:"4px 12px", fontSize:12}), onClick:function(){ store.checkGroupNow(); }, disabled: s.relBusy==="check"}, s.relBusy==="check" ? "…" : t("workshop.rel.check")),
1666
2140
  h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 12px", fontSize:12}), onClick:function(){ store.loadGroup(gid); }}, t("action.refresh")),
1667
2141
  h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 12px", fontSize:12}), onClick:function(){ store.deleteGroup(gid); }}, t("common.delete")))),
1668
- s.lastMsg ? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg) : null)
2142
+ s.lastMsg ? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg) : null,
2143
+ // —— 二级弹窗:改一条关系 / 加一条关系(列表只读,动作都在这里)——
2144
+ relEdit ? relDialog("workshop.rel.editTitle", relEdit, setRelEdit,
2145
+ function(v){ store.patchRelations([{op:"updateEdge", edgeId:v.edgeId, type:v.type, strength:v.strength, direction:v.direction, note:v.note}]); setRelEdit(null); },
2146
+ function(){ store.patchRelations([{op:"removeEdge", edgeId:relEdit.edgeId}]); setRelEdit(null); }, false) : null,
2147
+ relAddOpen ? relDialog("workshop.rel.addTitle", relForm,
2148
+ function(v){ if(v===null){ setRelAddOpen(false); return; } setRelForm(v); },
2149
+ function(v){ if(!v.from || !v.to || v.from===v.to) return; store.patchRelations([{op:"addEdge", from:v.from, to:v.to, type:v.type, strength:v.strength, direction:v.direction, note:v.note}]); setRelAddOpen(false); }, null, true) : null)
1669
2150
  );
1670
2151
  }
1671
- var stepsEls=steps.map(function(label, i){
1672
- var active=s.cur===i;
2152
+ var stepsEls=steps.map(function(it, i){
2153
+ var active=s.cur===it.cur;
1673
2154
  var hovered=hoverKey==="step:"+i;
1674
2155
  return h("span",{key:i, style: Object.assign({}, S.step, hovered&&!active? S.stepHover:{}, active? S.stepActive:{}),
1675
2156
  onMouseEnter:function(){ setHoverKey("step:"+i); },
1676
2157
  onMouseLeave:function(){ setHoverKey(null); },
1677
- onClick:function(){ store.setCur(i); }}, (i+1)+". "+label);
2158
+ onClick:function(){ store.setCur(it.cur); }}, (i+1)+". "+t(it.key));
1678
2159
  });
1679
2160
  var filteredLibrary = (function(){
1680
- var gid = String(s.currentGroupId||"");
1681
2161
  var base = s.library;
1682
- // 0.8.0:分组筛选("" = 全部;"__none" = 只看未分组)
1683
- if(gid === "__none") base = base.filter(function(e){ return !e.groupId; });
1684
- else if(gid) base = base.filter(function(e){ return String(e.groupId||"")===gid; });
1685
2162
  var q = String(s.libraryQuery||"").trim().toLowerCase();
1686
2163
  if(!q) return base;
1687
2164
  return base.filter(function(e){
@@ -1692,7 +2169,83 @@ window.__ModuleLoader__.load({
1692
2169
  });
1693
2170
  })();
1694
2171
  var bodyStyle = Object.assign({}, S.body, {gridTemplateColumns: s.libraryCollapsed ? "200px 1fr 40px" : "200px 1fr 268px"});
1695
- function renderLibrary(){
2172
+ // 单张已存卡的渲染(抽成函数:分组折叠面板与搜索结果共用)
2173
+ function renderCardEntry(entry){
2174
+ var isBusy = s.libraryBusy && String(s.libraryBusy).indexOf(entry.id)>=0;
2175
+ var isCurrent = s.currentLibraryId===entry.id;
2176
+ // 列表展示前 4 个标签,「还有几个没显示」用 meta.tagsTotal 算(meta.tags 本身截断到 6 个 —— P1-11)
2177
+ var tagTotal = (typeof entry.tagsTotal==="number") ? entry.tagsTotal : (entry.tags||[]).length;
2178
+ var tagShown = Math.min((entry.tags||[]).length, 4);
2179
+ var itemStyle = isCurrent ? Object.assign({}, S.libraryItem, {borderColor:"#7c5cff", background:"color-mix(in srgb, #7c5cff 8%, var(--dsw-alias-bg-layer-2))"}) : S.libraryItem;
2180
+ return h("div",{key:entry.id, style:itemStyle},
2181
+ h("div",{style:{display:"flex", alignItems:"center", justifyContent:"space-between", gap:6}},
2182
+ h("div",{style:{fontWeight:600, fontSize:12, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", flex:1}}, entry.name),
2183
+ h("span",{style:Object.assign({}, S.badge, {flexShrink:0})}, t("common.loreCount").replace("{n}", String(entry.loreCount)))
2184
+ ),
2185
+ h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:2, fontFamily:"ui-monospace,Menlo,monospace"}}, "ID:"+entry.id.slice(0,8)),
2186
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}, entry.descriptionSnippet || (entry.tags||[]).join(" · ") || "—"),
2187
+ h("div",{style:{display:"flex", flexWrap:"wrap", gap:4, marginTop:6}},
2188
+ (entry.tags||[]).slice(0,4).map(function(t2){ return h("span",{key:t2, style:Object.assign({}, S.badge, {fontSize:10, padding:"1px 5px"})}, t2); }),
2189
+ tagTotal > tagShown ? h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)"}}, "+"+(tagTotal-tagShown)) : null
2190
+ ),
2191
+ h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:4}}, new Date(entry.updatedAt*1000).toLocaleString()),
2192
+ h("div",{style:{display:"flex", gap:6, marginTop:8}},
2193
+ h("button",{style:Object.assign({}, S.btn, {padding:"4px 8px", fontSize:11, flex:1}), onClick:function(){ store.loadFromLibrary(entry.id); }, disabled: !!isBusy}, isBusy?"…":t("workshop.library.load")),
2194
+ // 「导出」保留原位、只切到导出页(0.4.1 归一):导出页导出的始终是当前工坊里的卡
2195
+ h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:(jumpFrom && jumpFrom.id===entry.id)? "#a6e8c8" : "var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11}), title:t("workshop.library.exportTip"), onClick:function(){ jumpToExport(entry); }}, (jumpFrom && jumpFrom.id===entry.id)? t("workshop.library.exportJumped"): t("workshop.library.export")),
2196
+ h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.deleteFromLibrary(entry.id); }, disabled: !!isBusy}, t("common.delete")),
2197
+ h("select",{style:{background:"transparent", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:(entry.groupId?"#7c5cff":"var(--dsw-alias-label-secondary)"), fontSize:11, padding:"2px 4px", maxWidth:92, flexShrink:0}, value:String(entry.groupId||""), disabled: !!isBusy, title:t("workshop.group.assign"), onChange:function(ev){ store.assignGroup(entry.id, ev.target.value||null); }},
2198
+ h("option",{value:""}, t("workshop.group.none")),
2199
+ (s.groups||[]).map(function(g){ return h("option",{key:g.id, value:g.id}, g.name); }))
2200
+ )
2201
+ );
2202
+ }
2203
+ /**
2204
+ * 已存角色列表(折叠面板版):**一个分组一节**,节标题就是分组名、点一下折叠/展开;
2205
+ * 没归组的卡拉进「未分组」那一节(永远显示,即使为空)。搜索时平铺全部命中项 —— 那时折叠只会碍事。
2206
+ */
2207
+ function renderGroupedList(){
2208
+ var q = String(s.libraryQuery||"").trim().toLowerCase();
2209
+ var all = s.library || [];
2210
+ if(q){
2211
+ var hits = all.filter(function(e){
2212
+ var nameMatch = String(e.name||"").toLowerCase().indexOf(q)>=0;
2213
+ var tagMatch = Array.isArray(e.tags) && e.tags.some(function(x){ return String(x).toLowerCase().indexOf(q)>=0; });
2214
+ var loreMatch = String(e.loreCount||"").indexOf(q)>=0;
2215
+ return nameMatch || tagMatch || loreMatch;
2216
+ });
2217
+ if(!hits.length) return h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)", textAlign:"center", padding:"24px 8px"}}, t("workshop.library.noMatch"));
2218
+ return h("div",{style:{display:"flex", flexDirection:"column", gap:6}}, hits.map(renderCardEntry));
2219
+ }
2220
+ if(!all.length) return h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)", textAlign:"center", padding:"24px 8px"}}, t("workshop.library.empty"));
2221
+ var byGroup = {};
2222
+ all.forEach(function(e){ var g = String(e.groupId||""); if(!byGroup[g]) byGroup[g] = []; byGroup[g].push(e); });
2223
+ var sections = (s.groups||[]).map(function(g){ return { id:g.id, name:g.name, items: byGroup[g.id]||[], hasBook: !!g.hasBook, bookEntries: g.bookEntries||0 }; });
2224
+ sections.push({ id:"", name: t("workshop.group.none"), items: byGroup[""]||[], hasBook:false, bookEntries:0 });
2225
+ var out = sections.map(function(sec){
2226
+ var collapsed = !!collapsedGroups[sec.id];
2227
+ return h("div",{key:"sec:"+sec.id, style:{marginBottom:6}},
2228
+ h("div",{style:{display:"flex", alignItems:"center", gap:6, padding:"5px 6px", cursor:"pointer", borderRadius:8, background:"var(--dsw-alias-bg-layer-1)", border:"1px solid var(--dsw-alias-border-l1)"}, onClick:function(){ toggleGroupCollapsed(sec.id); }},
2229
+ h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", width:10, flexShrink:0}}, collapsed ? "▸" : "▾"),
2230
+ h("span",{style:{fontSize:12, fontWeight:600, flex:1, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}, sec.name),
2231
+ h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", flexShrink:0}}, String(sec.items.length)),
2232
+ // 0.9.0:这组有共享世界书时挂一枚入口(点它切到共享书视图并选中这个组)
2233
+ (sec.id && sec.hasBook) ? h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #7c5cff", background:"transparent", color:"#7c5cff", borderRadius:6, padding:"1px 6px", fontSize:10, flexShrink:0}), title:t("step.book"), onClick:function(ev){ ev.stopPropagation(); store.setCurrentGroupId(sec.id); store.setCur(7); }}, "📖 "+String(sec.bookEntries||0)) : null,
2234
+ sec.id ? h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #7c5cff", background:"transparent", color:"#7c5cff", borderRadius:6, padding:"1px 6px", fontSize:10, flexShrink:0}), onClick:function(ev){ ev.stopPropagation(); store.openRelationsFor(sec.id); }}, t("workshop.rel.open")) : null),
2235
+ collapsed ? null : h("div",{style:{paddingLeft:2, display:"flex", flexDirection:"column", gap:6, marginTop:6}},
2236
+ sec.items.length ? sec.items.map(renderCardEntry)
2237
+ : h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", padding:"2px 8px"}}, t("workshop.group.empty")))
2238
+ );
2239
+ });
2240
+ out.push(h("div",{key:"new-group", style:{marginTop:8}},
2241
+ groupDraft === null
2242
+ ? h("button",{type:"button", style:Object.assign({}, S.btnGhost, {width:"100%", border:"1px dashed var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"6px 8px", fontSize:11}), onClick:function(){ setGroupDraft(""); }}, t("workshop.group.newGroup"))
2243
+ : h("input",{autoFocus:true, value:groupDraft, placeholder:t("workshop.group.prompt"), style:{width:"100%", boxSizing:"border-box", background:"var(--dsw-alias-bg-base)", border:"1px solid #7c5cff", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"6px 8px", fontSize:11},
2244
+ onChange:function(ev){ setGroupDraft(ev.target.value); },
2245
+ onKeyDown:function(ev){ if(ev.key==="Enter"){ store.createGroup(ev.target.value); setGroupDraft(null); } if(ev.key==="Escape"){ setGroupDraft(null); } },
2246
+ onBlur:function(ev){ var v=String(ev.target.value||"").trim(); if(v) store.createGroup(v); setGroupDraft(null); }})));
2247
+ return h("div",null, out);
2248
+ } function renderLibrary(){
1696
2249
  if(s.libraryCollapsed){
1697
2250
  return h("div",{style:S.libraryCollapsedBar},
1698
2251
  h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-primary)", borderRadius:8, padding:"6px 8px", fontSize:12, cursor:"pointer", width:28, height:28, display:"flex", alignItems:"center", justifyContent:"center"}), onClick:function(){ store.toggleLibraryCollapsed(); }, title:t("workshop.library.expand")}, "◀"),
@@ -1730,53 +2283,10 @@ window.__ModuleLoader__.load({
1730
2283
  h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", alignSelf:"center", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", flex:1, textAlign:"right"}}, s.draft.data.name ? s.draft.data.name.slice(0,8) : t("workshop.library.unnamed"))
1731
2284
  ),
1732
2285
  h("input",{placeholder:t("workshop.library.searchPlaceholder"), style:{width:"100%", boxSizing:"border-box", maxWidth:"100%", background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"6px 8px", fontSize:12}, value:s.libraryQuery, onChange:function(e){ store.setLibraryQuery(e.target.value); }}),
1733
- // 0.8.0 分组:同一世界观的角色拉进一组,关系网按组维护(「关系」按钮进关系视图,s.cur===4)
1734
- h("div",{style:{display:"flex", gap:6, marginTop:6}},
1735
- h("select",{style:{flex:1, minWidth:0, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 6px", fontSize:12}, value:s.currentGroupId||"", onChange:function(e){ store.setCurrentGroupId(e.target.value); }},
1736
- h("option",{value:""}, t("workshop.group.all")),
1737
- h("option",{value:"__none"}, t("workshop.group.none")),
1738
- (s.groups||[]).map(function(g){ return h("option",{key:g.id, value:g.id}, g.name + " · " + t("workshop.group.summary",{n:String(g.memberCount), m:String(g.edgeCount)})); })),
1739
- h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11, flexShrink:0}), onClick:function(){ store.createGroup(); }}, t("workshop.group.new")),
1740
- h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #7c5cff", background:"transparent", color:"#7c5cff", borderRadius:8, padding:"4px 8px", fontSize:11, flexShrink:0}), disabled: !s.currentGroupId || s.currentGroupId==="__none", onClick:function(){ store.setCur(4); }}, t("workshop.rel.open"))),
2286
+
1741
2287
  s.lastMsg ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}, s.lastMsg) : null
1742
2288
  ),
1743
- h("div",{style:S.libraryList},
1744
- filteredLibrary.length===0 ? h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)", textAlign:"center", padding:"24px 8px"}}, s.library.length===0 ? t("workshop.library.empty") : t("workshop.library.noMatch")) :
1745
- filteredLibrary.map(function(entry){
1746
- var isBusy = s.libraryBusy && String(s.libraryBusy).indexOf(entry.id)>=0;
1747
- var isCurrent = s.currentLibraryId===entry.id;
1748
- // 列表展示前 4 个标签,但「还有几个没显示」必须用 meta.tagsTotal 算:meta.tags 本身截断到 6 个,
1749
- // 拿它的长度当总数会偏小(卡里 7 个、列表只显示 6 个,用户会以为标签没存上 —— LLM 侧回测报的 P1-11)
1750
- var tagTotal = (typeof entry.tagsTotal==="number") ? entry.tagsTotal : (entry.tags||[]).length;
1751
- var tagShown = Math.min((entry.tags||[]).length, 4);
1752
- var itemStyle = isCurrent ? Object.assign({}, S.libraryItem, {borderColor:"#7c5cff", background:"color-mix(in srgb, #7c5cff 8%, var(--dsw-alias-bg-layer-2))"}) : S.libraryItem;
1753
- return h("div",{key:entry.id, style:itemStyle},
1754
- h("div",{style:{display:"flex", alignItems:"center", justifyContent:"space-between", gap:6}},
1755
- h("div",{style:{fontWeight:600, fontSize:12, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", flex:1}}, entry.name),
1756
- h("span",{style:Object.assign({}, S.badge, {flexShrink:0})}, t("common.loreCount").replace("{n}", String(entry.loreCount)))
1757
- ),
1758
- h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:2, fontFamily:"ui-monospace,Menlo,monospace"}}, "ID:"+entry.id.slice(0,8)),
1759
- h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}, entry.descriptionSnippet || entry.tags.join(" · ") || "—"),
1760
- h("div",{style:{display:"flex", flexWrap:"wrap", gap:4, marginTop:6}},
1761
- (entry.tags||[]).slice(0,4).map(function(t){ return h("span",{key:t, style:Object.assign({}, S.badge, {fontSize:10, padding:"1px 5px"})}, t); }),
1762
- tagTotal > tagShown ? h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)"}}, "+"+(tagTotal-tagShown)) : null
1763
- ),
1764
- h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:4}}, new Date(entry.updatedAt*1000).toLocaleString()),
1765
- h("div",{style:{display:"flex", gap:6, marginTop:8}},
1766
- h("button",{style:Object.assign({}, S.btn, {padding:"4px 8px", fontSize:11, flex:1}), onClick:function(){ store.loadFromLibrary(entry.id); }, disabled: !!isBusy}, isBusy?"…":t("workshop.library.load")),
1767
- // 「导出」保留在原位,但改成切到导出页(0.4.1 归一):导出页导出的始终是当前工坊里的卡,
1768
- // 想导这张已存角色先点「载入」—— 免得这里悄悄换掉用户正在编辑的草稿。
1769
- // 点下去按钮自己先变(「已切到 ✓」),不然切页发生在中栏、用户视线却在右侧,会以为没反应。
1770
- h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:(jumpFrom && jumpFrom.id===entry.id)? "#a6e8c8" : "var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11}), title:t("workshop.library.exportTip"), onClick:function(){ jumpToExport(entry); }}, (jumpFrom && jumpFrom.id===entry.id)? t("workshop.library.exportJumped"): t("workshop.library.export")),
1771
- h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.deleteFromLibrary(entry.id); }, disabled: !!isBusy}, t("common.delete")),
1772
- // 0.8.0:直接把卡归到某个分组(空 = 未分组)。分组是关系图谱的载体。
1773
- h("select",{style:{background:"transparent", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:(entry.groupId?"#7c5cff":"var(--dsw-alias-label-secondary)"), fontSize:11, padding:"2px 4px", maxWidth:92, flexShrink:0}, value:String(entry.groupId||""), disabled: !!isBusy, title:t("workshop.group.assign"), onChange:function(ev){ store.assignGroup(entry.id, ev.target.value||null); }},
1774
- h("option",{value:""}, t("workshop.group.none")),
1775
- (s.groups||[]).map(function(g){ return h("option",{key:g.id, value:g.id}, g.name); }))
1776
- )
1777
- );
1778
- })
1779
- )
2289
+ h("div",{style:S.libraryList}, renderGroupedList())
1780
2290
  );
1781
2291
  }
1782
2292
  return h("div",{style:S.overlay,
@@ -1838,9 +2348,33 @@ window.__ModuleLoader__.load({
1838
2348
  })(),
1839
2349
  h("div",{style:bodyStyle},
1840
2350
  h("div",{style:S.nav},
1841
- navItem(0, t("step.world"), t("step.world.desc")),
1842
- navItem(1, t("step.char"), t("step.char.desc")),
1843
- navItem(2, t("step.lore"), t("step.lore.desc")),
2351
+ // 0.9.0:左侧从「写作三步」改成**卡片结构 + 进度徽章**。写作早交给 LLM 了,
2352
+ // 工坊现在的职责是「看 + 微调 + 导出」,所以按卡片组成分页,每项右侧挂三态进度点与计数。
2353
+ (function(){
2354
+ var prog = draftProgress(s);
2355
+ var byCur = {};
2356
+ prog.forEach(function(p){ byCur[p.cur] = p; });
2357
+ var curGid = String(s.currentGroupId||"");
2358
+ var curGroup = null;
2359
+ for(var i=0;i<s.groups.length;i++){ if(String(s.groups[i].id)===curGid) curGroup = s.groups[i]; }
2360
+ var relProg = (s.relations && s.relations.group)
2361
+ // ⚠️ 边在 group **里面**(宿主 cc_getGroup 的形状是 {group,members,rev})——
2362
+ // 这里曾经读顶层 s.relations.edges,于是左侧永远显示「0 条」(视图里却有 12 条)。
2363
+ ? { state: (((s.relations.group.edges||[]).length)? "ok":"partial"), note: t("common.entries",{n:String((s.relations.group.edges||[]).length)}) }
2364
+ : { state: "missing", note: s.groups.length? t("book.groups",{n:String(s.groups.length)}) : "—" };
2365
+ var bookProg = (curGroup && curGroup.hasBook)
2366
+ ? { state: (curGroup.bookEntries? "ok":"partial"), note: t("common.entries",{n:String(curGroup.bookEntries||0)}) }
2367
+ : { state: "missing", note: "—" };
2368
+ return [
2369
+ navItem(5, t("step.overview"), t("step.overview.desc"), null),
2370
+ navItem(1, t("step.char"), t("step.char.desc"), byCur[1]),
2371
+ navItem(0, t("step.world"), t("step.world.desc"), byCur[0]),
2372
+ navItem(2, t("step.lore"), t("step.lore.desc"), byCur[2]),
2373
+ navItem(6, t("step.greetings"), t("step.greetings.desc"), byCur[6]),
2374
+ navItem(4, t("step.rel"), t("step.rel.desc"), relProg),
2375
+ navItem(7, t("step.book"), t("step.book.desc"), bookProg)
2376
+ ];
2377
+ })(),
1844
2378
  h("div",{style:{marginTop:"auto", paddingTop:12, borderTop:"1px solid var(--dsw-alias-border-l1)", display:"flex", flexDirection:"column", gap:8}},
1845
2379
  // 「校验导出」入口:这一步原本是导航里的第 4 项,与这枚按钮重复,0.4.1 合并成一枚。
1846
2380
  // 点它切到导出页(校验按钮在那一页里)。