@xia-sc/dsh-cc-studio 0.9.0 → 0.10.0

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
@@ -60,7 +60,7 @@ window.__ModuleLoader__.load({
60
60
  "workshop.rel.dirLabel":"方向",
61
61
  "workshop.rel.myCard":"当前卡",
62
62
  "book.title":"共享世界书",
63
- "book.hint":"一组一份:世界设定(年表/势力/地理/力量/日常)放这里,成员卡里只留角色私有条目。导出后是 CCv3 官方的 lorebook_v3 JSON —— 在聊天客户端里绑成全局世界书,多张卡共用一本,卡里就不必再重复存。",
63
+ "book.hint":"一组一份:世界设定(年表/势力/地理/力量/日常)放这里,成员卡里只留角色私有条目。导出有两种:CCv3 独立世界书(lorebook_v3,给支持该规范的客户端绑成全局世界书)与 SillyTavern 世界书(ST 的导入接口只认 {entries:{…}} 形态,直接把 CCv3 那份拖进去会报 Bad Request)。",
64
64
  "book.noGroup":"共享世界书是按分组存放的:先在右侧「已存角色」里建一个分组,并把成员卡拉进去。",
65
65
  "book.empty":"这个分组还没有共享世界书。",
66
66
  "book.name":"书名",
@@ -82,6 +82,7 @@ window.__ModuleLoader__.load({
82
82
  "book.promoteSyncedDraft":"已同步当前草稿,删掉 {n} 条重复(否则保存时会把它们写回去)",
83
83
  "book.exportBook":"导出共享世界书 JSON",
84
84
  "book.exported":"已导出 {name}",
85
+ "book.exportedSt":"已导出 SillyTavern 世界书「{name}」—— 在 SillyTavern 里「世界信息 → 导入」选它即可",
85
86
  "book.applyCard":"合并进当前卡",
86
87
  "book.applyDone":"已往当前卡补了 {n} 条",
87
88
  "book.applyNone":"当前卡已包含这本共享书的全部条目。",
@@ -92,9 +93,11 @@ window.__ModuleLoader__.load({
92
93
  "book.saved":"已保存(rev {rev})",
93
94
  // —— 0.9.0 导出页的共享书块 ——
94
95
  "export.book.title":"共享世界书",
95
- "export.book.hint":"当前卡所属分组的共享设定可以单独导出(CCv3 lorebook_v3 格式):在客户端里把它绑成全局世界书,多张成员卡共用一本。",
96
+ "export.book.hint":"当前卡所属分组的共享设定可以单独导出:CCv3 独立世界书给支持 lorebook_v3 的客户端当全局世界书;SillyTavern 世界书是 ST 原生形态({entries:{…}})—— ST 的导入接口只认这一种,拖 CCv3 那份进去会报 Bad Request。ST 里的顺序是**先导世界书、再导卡**:卡会自动带上绑定(data.extensions.world);卡内私有条目还要在 ST 里点一次 Import Card Lore 才生效。",
97
+ "export.stWorldBound":"已写入 SillyTavern 世界书绑定「{name}」",
96
98
  "export.book.none":"当前卡不在任何分组里,或这个分组还没有共享世界书。",
97
- "export.book.button":"⬇ 导出共享世界书",
99
+ "export.book.button":"⬇ CCv3 独立世界书",
100
+ "export.book.buttonSt":"⬇ SillyTavern 世界书",
98
101
  "export.book.merge":"导出 JSON 时把共享条目并入这张卡(给不支持全局世界书的客户端)",
99
102
  "export.book.merged":"(已并入 {n} 条共享条目)",
100
103
  "action.expand":"✦ AI 扩写",
@@ -233,6 +236,8 @@ window.__ModuleLoader__.load({
233
236
  "workshop.rel.rendered":"已把 {n} 条关系写进当前卡(记得「保存当前」才会落库)",
234
237
  "workshop.rel.checkResult":"体检:{n} 个问题",
235
238
  "workshop.rel.checkOk":"体检通过:没有结构性问题",
239
+ "workshop.rel.checkStats":"体检:{e} 个错误 · {w} 个警告",
240
+ "workshop.rel.checkPass":"体检通过:没有发现问题",
236
241
  "workshop.rel.saved":"关系已更新(rev {rev})",
237
242
  "workshop.rel.needLoad":"先在左侧「载入」一张卡:渲染要知道写进哪张卡",
238
243
  "workshop.rel.hint":"关系只存在插件侧,不写进 CCv3 字段;「渲染到当前卡」会把它们写进 description 的【关系】段,让分享出去的卡自包含。",
@@ -272,11 +277,10 @@ window.__ModuleLoader__.load({
272
277
  "store.validateFailed":"校验未通过",
273
278
  "store.importedOk":"已导入 {k}:{name} · {n} lore(可在侧边栏 ★ 保存)",
274
279
  "store.importedSaved":"已导入并自动保存:{name}",
275
- "store.errNotCcJson":"不是合法的 CCv3 JSON(缺少 spec)",
276
- "store.errSpecMismatch":"spec 需为 chara_card_v3,当前为 {v}",
277
- "store.errNoData":"缺少 data",
280
+ "store.importedUpgraded":"{v} 卡已升级为 CCv3",
278
281
  "store.errImportFailed":"导入失败:{msg}",
279
282
  "store.errReadFileFailed":"读取文件失败",
283
+ "store.parsingJson":"正在解析 JSON…",
280
284
  "store.parsingPng":"正在解析 PNG…",
281
285
  "store.errPngImportFailed":"PNG 导入失败:{msg}",
282
286
  "store.errReadPngFailed":"读取 PNG 失败",
@@ -374,7 +378,7 @@ window.__ModuleLoader__.load({
374
378
  "workshop.rel.dirLabel":"Direction",
375
379
  "workshop.rel.myCard":"current card",
376
380
  "book.title":"Shared lorebook",
377
- "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.",
381
+ "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. Two export formats: the CCv3 standalone lorebook (lorebook_v3, for clients that support the spec) and the SillyTavern lorebook (ST's importer only accepts the {entries:{…}} shape — feeding it the CCv3 file answers Bad Request).",
378
382
  "book.noGroup":"Shared lorebooks live on a group: create one under “Saved characters” on the right and put the member cards in it.",
379
383
  "book.empty":"This group has no shared lorebook yet.",
380
384
  "book.name":"Book name",
@@ -396,6 +400,7 @@ window.__ModuleLoader__.load({
396
400
  "book.promoteSyncedDraft":"Current draft synced — removed {n} duplicates (saving would have written them back)",
397
401
  "book.exportBook":"Export shared lorebook JSON",
398
402
  "book.exported":"Exported {name}",
403
+ "book.exportedSt":"Exported the SillyTavern lorebook “{name}” — pick it in SillyTavern via World Info → Import",
399
404
  "book.applyCard":"Merge into the current card",
400
405
  "book.applyDone":"Added {n} entries to the current card",
401
406
  "book.applyNone":"The current card already contains every entry of this book.",
@@ -405,9 +410,11 @@ window.__ModuleLoader__.load({
405
410
  "book.refresh":"Refresh",
406
411
  "book.saved":"Saved (rev {rev})",
407
412
  "export.book.title":"Shared lorebook",
408
- "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.",
413
+ "export.book.hint":"The shared setting of this card's group can be exported on its own: the CCv3 standalone lorebook for clients that support lorebook_v3, and the SillyTavern lorebook in ST's native shape ({entries:{…}}) — ST's importer accepts nothing else, so feeding it the CCv3 file answers Bad Request. In ST, import the lorebook first and the card second: the card carries the binding automatically (data.extensions.world); the entries inside the card still need one “Import Card Lore” click.",
414
+ "export.stWorldBound":"SillyTavern world binding written: “{name}”",
409
415
  "export.book.none":"This card is in no group, or its group has no shared lorebook yet.",
410
- "export.book.button":"⬇ Export shared lorebook",
416
+ "export.book.button":"⬇ CCv3 standalone lorebook",
417
+ "export.book.buttonSt":"⬇ SillyTavern lorebook",
411
418
  "export.book.merge":"Merge the shared entries into this card when exporting JSON (for clients without a global lorebook)",
412
419
  "export.book.merged":"({n} shared entries merged in)",
413
420
  "action.expand":"AI Expand",
@@ -546,6 +553,8 @@ window.__ModuleLoader__.load({
546
553
  "workshop.rel.rendered":"Wrote {n} relations into the current card (remember to “Save current”)",
547
554
  "workshop.rel.checkResult":"Check: {n} issue(s)",
548
555
  "workshop.rel.checkOk":"Check passed: no structural issues",
556
+ "workshop.rel.checkStats":"Check: {e} error(s) · {w} warning(s)",
557
+ "workshop.rel.checkPass":"Check passed: no issues",
549
558
  "workshop.rel.saved":"Relations updated (rev {rev})",
550
559
  "workshop.rel.needLoad":"Load a card on the left first: rendering needs to know which card to write into",
551
560
  "workshop.rel.hint":"Relations live on the plugin side only and are never written into CCv3 fields; “Render into current card” writes them into the 【关系】 block of description so a shared card stays self-contained.",
@@ -585,11 +594,10 @@ window.__ModuleLoader__.load({
585
594
  "store.validateFailed":"Validation failed",
586
595
  "store.importedOk":"Imported {k}: {name} · {n} lore (★ Save in the sidebar)",
587
596
  "store.importedSaved":"Imported and auto-saved: {name}",
588
- "store.errNotCcJson":"Not a valid CCv3 JSON (missing spec)",
589
- "store.errSpecMismatch":"spec must be chara_card_v3, got {v}",
590
- "store.errNoData":"missing data",
597
+ "store.importedUpgraded":"{v} card upgraded to CCv3",
591
598
  "store.errImportFailed":"Import failed: {msg}",
592
599
  "store.errReadFileFailed":"Failed to read the file",
600
+ "store.parsingJson":"Parsing JSON…",
593
601
  "store.parsingPng":"Parsing PNG…",
594
602
  "store.errPngImportFailed":"PNG import failed: {msg}",
595
603
  "store.errReadPngFailed":"Failed to read the PNG",
@@ -703,7 +711,7 @@ window.__ModuleLoader__.load({
703
711
  var _saved = loadSettings();
704
712
  // 兼容旧开关:poll=false 视为 0 秒
705
713
  if(_saved.poll===false) _saved.pollInterval = 0;
706
- 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 };
714
+ 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, relIssues:null, relByCheck: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 };
707
715
  var listeners=new Set();
708
716
  function emit(){ listeners.forEach(function(fn){ try{fn();}catch(e){console.error(e);} }); }
709
717
  function getSnapshot(){ return state; }
@@ -809,7 +817,11 @@ window.__ModuleLoader__.load({
809
817
  for(var i=0;i<bin.length;i++) bytes[i]=bin.charCodeAt(i);
810
818
  return bytes;
811
819
  }
812
- function handleImportedDraft(obj, label){
820
+ /**
821
+ * 导入结果的统一收尾。`upgradedFrom` 非空 = 这份卡原来是 V2/V1(0.9.1 B2 自动升级成 CCv3),
822
+ * 必须在提示里说出来 —— 静默把一张 V2 卡变成 V3 形状不算「什么都没发生」。
823
+ */
824
+ function handleImportedDraft(obj, label, upgradedFrom){
813
825
  obj.spec_version=obj.spec_version||"3.0";
814
826
  obj.data.character_book=obj.data.character_book||{name:"",description:"",scan_depth:4,token_budget:1200,recursive_scanning:false,extensions:{},entries:[]};
815
827
  if(!Array.isArray(obj.data.character_book.entries)) obj.data.character_book.entries=[];
@@ -818,7 +830,9 @@ window.__ModuleLoader__.load({
818
830
  if(!Array.isArray(obj.data.tags)) obj.data.tags=[];
819
831
  if(!Array.isArray(obj.data.assets)) obj.data.assets=[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}];
820
832
  setDraftFromHost(obj);
821
- set({currentLibraryId:null, lastMsg:t("store.importedOk",{k:label.trim(), name:(obj.data.name||t("workshop.library.unnamed")), n:(obj.data.character_book.entries.length||0)})});
833
+ var msg=t("store.importedOk",{k:label.trim(), name:(obj.data.name||t("workshop.library.unnamed")), n:(obj.data.character_book.entries.length||0)});
834
+ if(upgradedFrom) msg += " · " + t("store.importedUpgraded",{v:(String(upgradedFrom)==="chara_card_v1"? "V1":"V2")});
835
+ set({currentLibraryId:null, lastMsg: msg});
822
836
  setTimeout(pushDraftToHost,80);
823
837
  if(state.autoValidate) rpc("validate",{card:obj}).then(function(r){ if(r&&r.ok) set({valid:r.value}); }).catch(function(){});
824
838
  if(state.autoSaveOnImport){
@@ -846,16 +860,22 @@ window.__ModuleLoader__.load({
846
860
  else isJson=true;
847
861
  }
848
862
  if(isJson){
863
+ // 0.9.1(B2):JSON 也交给宿主升级 —— 客户端以前自己 parse 且只认 chara_card_v3,
864
+ // 于是 ST 里导出的 V2 卡一张都进不来。现在三条导入路径(JSON/PNG/CHARX)共用宿主那份
865
+ // upgradeCardToV3,客户端只负责把文本递过去、把「已升级」说出来。
849
866
  var reader=new FileReader();
850
867
  reader.onload=function(){
851
868
  try{
852
869
  var text=String(reader.result||"");
853
- var obj=JSON.parse(text);
854
- if(obj && obj.draft && obj.draft.spec) obj=obj.draft;
855
- if(!obj || typeof obj!=="object" || !obj.spec) throw new Error(t("store.errNotCcJson"));
856
- if(obj.spec!=="chara_card_v3") throw new Error(t("store.errSpecMismatch",{v:String(obj.spec)}));
857
- if(!obj.data || typeof obj.data!=="object") throw new Error(t("store.errNoData"));
858
- handleImportedDraft(obj, " JSON");
870
+ var sid=state.currentSessionId||null;
871
+ var args=sid?{json:text, sessionId:sid}:{json:text};
872
+ set({lastMsg:t("store.parsingJson"), busy:"importJson"});
873
+ rpc("cc_importFromJson", args).then(function(res){
874
+ set({busy:null});
875
+ if(res && res.ok && res.value && res.value.draft){
876
+ handleImportedDraft(res.value.draft, " JSON", res.value.upgradedFrom);
877
+ } else set({lastMsg:t("store.errImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
878
+ }).catch(function(e){ set({busy:null, lastMsg:t("store.errImportFailed",{msg:String(e&&e.message||e)})}); });
859
879
  }catch(e){ set({lastMsg:t("store.errImportFailed",{msg:String(e&&e.message||e)})}); }
860
880
  };
861
881
  reader.onerror=function(){ set({lastMsg:t("store.errReadFileFailed")}); };
@@ -871,7 +891,7 @@ window.__ModuleLoader__.load({
871
891
  rpc("cc_importFromPng", args).then(function(res){
872
892
  set({busy:null});
873
893
  if(res && res.ok && res.value && res.value.draft){
874
- handleImportedDraft(res.value.draft, " PNG");
894
+ handleImportedDraft(res.value.draft, " PNG", res.value.upgradedFrom);
875
895
  } else set({lastMsg:t("store.errPngImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
876
896
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errPngImportFailed",{msg:String(e&&e.message||e)})}); });
877
897
  }catch(e){ set({lastMsg:t("store.errPngImportFailed",{msg:String(e&&e.message||e)})}); }
@@ -889,7 +909,7 @@ window.__ModuleLoader__.load({
889
909
  rpc("cc_importFromCharx", args2).then(function(res){
890
910
  set({busy:null});
891
911
  if(res && res.ok && res.value && res.value.draft){
892
- handleImportedDraft(res.value.draft, " CHARX");
912
+ handleImportedDraft(res.value.draft, " CHARX", res.value.upgradedFrom);
893
913
  } else set({lastMsg:t("store.errCharxImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
894
914
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errCharxImportFailed",{msg:String(e&&e.message||e)})}); });
895
915
  }catch(e){ set({lastMsg:t("store.errCharxImportFailed",{msg:String(e&&e.message||e)})}); }
@@ -907,9 +927,20 @@ window.__ModuleLoader__.load({
907
927
  var norm=function(e){ return String((e&&e.content)!=null? e.content : "").replace(/\s+/g," ").trim(); };
908
928
  var x=norm(a); return !!x && x===norm(b);
909
929
  }
930
+ /**
931
+ * ST 兼容层(0.9.1):当前分组的共享书名 —— SillyTavern 的「角色↔世界书」绑定就是
932
+ * `data.extensions.world = <世界书文件名>`(`characters.js:391,617`),而我们导出的那本世界书
933
+ * 就叫 `<书名>.json`,所以把书名写进去即可。没有共享书时返回 ""(**不写**绑定,免得留一个悬空的名字)。
934
+ */
935
+ function stWorldName(){
936
+ var bk=state.book;
937
+ if(!bk || !bk.exists) return "";
938
+ return String((bk.book && bk.book.data && bk.book.data.name) || "").trim();
939
+ }
910
940
  /**
911
941
  * 导出 JSON。0.9.0:若在导出页勾了「并入共享条目」,则**只在导出的副本上**把该组的共享世界书
912
942
  * 里卡里还没有的条目补进去(给不支持全局世界书的客户端兜底)——草稿本身一个字节都不动。
943
+ * 0.9.1:同样只在副本上写 ST 的世界书绑定(`data.extensions.world`)。
913
944
  */
914
945
  function exportJSON(){
915
946
  var out=state.draft, merged=0;
@@ -924,13 +955,22 @@ window.__ModuleLoader__.load({
924
955
  merged=add.length;
925
956
  }
926
957
  }
958
+ var bound=stWorldName();
959
+ if(bound){
960
+ if(out===state.draft) out=JSON.parse(JSON.stringify(state.draft));
961
+ out.data.extensions=Object.assign({}, out.data.extensions);
962
+ out.data.extensions.world=bound;
963
+ }
927
964
  var blob=new Blob([JSON.stringify(out,null,2)],{type:"application/json"});
928
965
  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);
929
- set({lastMsg: merged? t("store.exportedJson") + " " + t("export.book.merged",{n:String(merged)}) : t("store.exportedJson")});
966
+ set({lastMsg: (merged? t("store.exportedJson") + " " + t("export.book.merged",{n:String(merged)}) : t("store.exportedJson"))
967
+ + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
930
968
  }
931
969
  function exportPNG(){
932
970
  var sid=state.currentSessionId||null;
971
+ var bound=stWorldName();
933
972
  var args=sid?{sessionId:sid}:{};
973
+ if(bound) args.stWorld=bound;
934
974
  set({busy:"exportPng", lastMsg:t("store.generatingPng")});
935
975
  rpc("cc_exportPng", args).then(function(res){
936
976
  set({busy:null});
@@ -939,7 +979,7 @@ window.__ModuleLoader__.load({
939
979
  var bytes=b64ToBytes(res.value.b64);
940
980
  var blob=new Blob([bytes],{type:"image/png"});
941
981
  var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=res.value.filename||((state.draft.data.name||"card")+".png"); a.click(); setTimeout(function(){ URL.revokeObjectURL(a.href); },800);
942
- set({lastMsg:t("store.exportedPng",{name:(res.value.filename||"card.png")})});
982
+ set({lastMsg:t("store.exportedPng",{name:(res.value.filename||"card.png")}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
943
983
  }catch(e){ set({lastMsg:t("store.errPngExportFailed",{msg:String(e&&e.message||e)})}); }
944
984
  } else set({lastMsg:t("store.errPngExportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
945
985
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errPngExportFailed",{msg:String(e&&e.message||e)})}); });
@@ -956,7 +996,9 @@ window.__ModuleLoader__.load({
956
996
  try{
957
997
  var b64=arrayBufferToB64(reader.result);
958
998
  var sid=state.currentSessionId||null;
999
+ var bound=stWorldName();
959
1000
  var args=sid?{sessionId:sid, imageB64:b64}:{imageB64:b64};
1001
+ if(bound) args.stWorld=bound;
960
1002
  set({busy:"exportPng", lastMsg:t("store.writingPng",{name:f.name})});
961
1003
  rpc("cc_exportPng", args).then(function(res){
962
1004
  set({busy:null});
@@ -965,7 +1007,7 @@ window.__ModuleLoader__.load({
965
1007
  var bytes=b64ToBytes(res.value.b64);
966
1008
  var blob=new Blob([bytes],{type:"image/png"});
967
1009
  var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=res.value.filename||((state.draft.data.name||"card")+".png"); a.click(); setTimeout(function(){ URL.revokeObjectURL(a.href); },800);
968
- set({lastMsg:t("store.wrotePng",{name:(res.value.filename||f.name)})});
1010
+ set({lastMsg:t("store.wrotePng",{name:(res.value.filename||f.name)}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
969
1011
  }catch(e){ set({lastMsg:t("store.errWritePngFailed",{msg:String(e&&e.message||e)})}); }
970
1012
  } else set({lastMsg:t("store.errWritePngFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
971
1013
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errWritePngFailed",{msg:String(e&&e.message||e)})}); });
@@ -979,7 +1021,9 @@ window.__ModuleLoader__.load({
979
1021
  }
980
1022
  function exportCharx(){
981
1023
  var sid=state.currentSessionId||null;
1024
+ var bound=stWorldName();
982
1025
  var args=sid?{sessionId:sid}:{};
1026
+ if(bound) args.stWorld=bound;
983
1027
  set({busy:"exportCharx", lastMsg:t("store.generatingCharx")});
984
1028
  rpc("cc_exportCharx", args).then(function(res){
985
1029
  set({busy:null});
@@ -988,7 +1032,7 @@ window.__ModuleLoader__.load({
988
1032
  var bytes2=b64ToBytes(res.value.b64);
989
1033
  var blob2=new Blob([bytes2],{type:"application/zip"});
990
1034
  var a2=document.createElement("a"); a2.href=URL.createObjectURL(blob2); a2.download=res.value.filename||((state.draft.data.name||"card")+".charx"); a2.click(); setTimeout(function(){ URL.revokeObjectURL(a2.href); },800);
991
- set({lastMsg:t("store.exportedCharx",{name:(res.value.filename||"card.charx")})});
1035
+ set({lastMsg:t("store.exportedCharx",{name:(res.value.filename||"card.charx")}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
992
1036
  }catch(e){ set({lastMsg:t("store.errCharxExportFailed",{msg:String(e&&e.message||e)})}); }
993
1037
  } else set({lastMsg:t("store.errCharxExportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
994
1038
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errCharxExportFailed",{msg:String(e&&e.message||e)})}); });
@@ -1064,12 +1108,13 @@ window.__ModuleLoader__.load({
1064
1108
  }
1065
1109
  async function loadGroup(id){
1066
1110
  var gid = String(id || state.currentGroupId || "");
1067
- if(!gid){ set({relations:null}); return; }
1111
+ if(!gid){ set({relations:null, relIssues:null}); return; }
1068
1112
  set({relBusy:"load"});
1069
1113
  try{
1070
1114
  var res = await rpc("cc_getGroup", {id: gid});
1071
- if(res && res.ok && res.value) set({relations: res.value});
1072
- else set({relations:null, lastMsg:String((res && res.error && res.error.message) || "")});
1115
+ // 换了组就把上一次的体检清单清掉:留着会让用户以为新组也有那些问题(0.10.0)
1116
+ if(res && res.ok && res.value) set({relations: res.value, relIssues:null, relByCheck:null});
1117
+ else set({relations:null, relIssues:null, relByCheck:null, lastMsg:String((res && res.error && res.error.message) || "")});
1073
1118
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1074
1119
  set({relBusy:null});
1075
1120
  }
@@ -1183,6 +1228,152 @@ window.__ModuleLoader__.load({
1183
1228
  set({lastMsg: t("book.exported",{name:String(b.data.name||"")})});
1184
1229
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1185
1230
  }
1231
+ /**
1232
+ * 共享世界书 → **SillyTavern 原生世界书**(`{ entries: { "<uid>": {...} } }`)。
1233
+ *
1234
+ * 为什么要另出一份文件(0.9.1,用户实机报告):ST 服务端 `/api/worldinfo/import` 拿到文件后
1235
+ * 第一件事就是 `if (!('entries' in worldContent)) throw`(SillyTavern `src/endpoints/worldinfo.js`),
1236
+ * 而 CCv3 独立 lorebook 的顶层是 `{spec, data}` —— 于是用户把 `.lorebook.json` 拖进 ST,看到的是
1237
+ * `Error importing world info: … Bad Request`。ST 客户端那条 `importWorldInfo()` 也只转
1238
+ * Novel / Agnai / Risu 三种格式,1.19.0 的整份 `world-info.js` 里**搜不到 `lorebook_v3`** ——
1239
+ * 所以这不是「换个后缀」能解决的,是规范本身 ST 不认,必须按它的形态另导一份。
1240
+ *
1241
+ * 条目映射照抄 ST 自己的 `convertCharacterBook()`(ST 唯一权威的 CC 书转换器,1.19.0):
1242
+ * uid←id、key←keys、keysecondary←secondary_keys、comment←comment/name、content←content、
1243
+ * order←insertion_order、disable←!enabled、addMemo←有没有标题,其余可调项一律从
1244
+ * `extensions.*` 取(`cc_auto` / `cc_world_ref` 也随 `extensions` 原样带过去 —— ST 的条目本来
1245
+ * 就有这个自由字段)。
1246
+ *
1247
+ * 唯一 ST 自己做不到、必须我们补的是 **`@@position` / `@@depth` 装饰器**:ST 的
1248
+ * `parseDecorators()` 只认 `@@activate` / `@@dont_activate`(`KNOWN_DECORATORS`),其余 `@@` 行
1249
+ * **连同正文一起被丢掉**(它从第一个非 `@@` 行开始截取正文)—— 不翻译的话,自动生成的 5 条会
1250
+ * 全部落在默认插入位置、装饰器静默消失。所以这两类翻译成 ST 字段并删掉该行;`@@activate`
1251
+ * 留给 ST 自己认(它本来就有这个语义,硬翻成 constant 只是多一层猜测)。
1252
+ */
1253
+ var ST_ENTRY_TEMPLATE = { key:[], keysecondary:[], comment:"", content:"", constant:false, vectorized:false, selective:true, selectiveLogic:0, addMemo:false, order:100, position:0, disable:false, ignoreBudget:false, excludeRecursion:false, preventRecursion:false, matchPersonaDescription:false, matchCharacterDescription:false, matchCharacterPersonality:false, matchCharacterDepthPrompt:false, matchScenario:false, matchCreatorNotes:false, delayUntilRecursion:0, probability:100, useProbability:true, depth:4, outletName:"", group:"", groupOverride:false, groupWeight:100, scanDepth:null, caseSensitive:null, matchWholeWords:null, useGroupScoring:null, automationId:"", role:0, sticky:null, cooldown:null, delay:null, triggers:[] };
1254
+ /** ST 的 `world_info_position`:before=0 / after=1 / ANTop=2 / ANBottom=3 / atDepth=4 / EMTop=5 / EMBottom=6。 */
1255
+ var ST_POSITION = { before:0, before_char:0, before_desc:0, after:1, after_char:1, after_desc:1, an_top:2, antop:2, an_bottom:3, anbottom:3, at_depth:4, atdepth:4, em_top:5, emtop:5, em_bottom:6, embottom:6 };
1256
+ /** ST 的 `??` 语义:`extensions.<key>` 优先,缺省才用默认值(与 convertCharacterBook 逐行对应)。 */
1257
+ function stExt(ext, key, dflt){ var v = ext[key]; return (v === undefined || v === null)? dflt : v; }
1258
+ /**
1259
+ * 解析正文**开头连续**的 `@@` 行(与 ST 的 `parseDecorators` 同构:只认开头那一段)。
1260
+ * 只翻译 ST 认不出的 `@@position` / `@@depth`;`@@activate` / `@@dont_activate` 与未知写法
1261
+ * 原样留下 —— ST 或者自己有语义、或者自己会丢,我们不猜。
1262
+ */
1263
+ function unfurlBookDecorators(content){
1264
+ var text = String(content == null? "" : content);
1265
+ var lines = text.split("\n");
1266
+ var drop = [], pos = null, depth = null;
1267
+ for(var i=0;i<lines.length;i++){
1268
+ var line = lines[i];
1269
+ if(line.slice(0,2) !== "@@") break; // 装饰器区到头
1270
+ if(line.slice(0,3) === "@@@") break; // ST 的转义写法:后面全当原文,不碰
1271
+ var m = /^@@([a-z_]+)(?:\s+(.*))?$/.exec(line.trim());
1272
+ if(!m) break; // 语法可疑:整段交给 ST 自己处理
1273
+ var name = m[1], arg = String(m[2] == null? "" : m[2]).trim();
1274
+ if(name === "position"){
1275
+ // ST 没有「scenario」之类插入点;ST 自己的 convertCharacterBook 对除 before_char 之外的
1276
+ // 一律当 after —— 认不出的值就跟它的回退走,不自己发明位置。
1277
+ var p = ST_POSITION[arg.toLowerCase().replace(/[\s-]+/g,"_")];
1278
+ pos = (typeof p === "number")? p : 1;
1279
+ drop.push(i);
1280
+ } else if(name === "depth"){
1281
+ var n = parseInt(arg, 10);
1282
+ if(isFinite(n) && n >= 0){ depth = n; pos = 4; drop.push(i); } // atDepth
1283
+ }
1284
+ }
1285
+ if(!drop.length) return { position:null, depth:null, content:text };
1286
+ var kept = lines.filter(function(_, i){ return drop.indexOf(i) < 0; });
1287
+ while(kept.length && !String(kept[0]).trim()) kept.shift(); // 删掉装饰器后留下的空行
1288
+ return { position:pos, depth:depth, content: kept.join("\n") };
1289
+ }
1290
+ /** 单条共享书条目 → ST 条目(字段映射与优先级见上面那段注释)。 */
1291
+ function bookEntryToSt(entry, index){
1292
+ var e = entry || {};
1293
+ var ext = (e.extensions && typeof e.extensions === "object")? e.extensions : {};
1294
+ var dec = unfurlBookDecorators(e.content);
1295
+ var title = String(e.comment || e.name || "");
1296
+ var pos = dec.position;
1297
+ if(pos === null) pos = (typeof ext.position === "number")? ext.position : null; // ST 自己的位置偏好
1298
+ if(pos === null) pos = (String(e.position || "") === "before_char")? 0 : 1; // ST convertCharacterBook 的判据
1299
+ var id = (e.id === undefined || e.id === null || e.id === "")? index : e.id;
1300
+ var out = Object.assign({}, ST_ENTRY_TEMPLATE, {
1301
+ uid: id,
1302
+ key: Array.isArray(e.keys)? e.keys.map(String) : [],
1303
+ keysecondary: Array.isArray(e.secondary_keys)? e.secondary_keys.map(String) : [],
1304
+ comment: title,
1305
+ content: dec.content,
1306
+ constant: e.constant === true,
1307
+ selective: e.selective === true,
1308
+ selectiveLogic: stExt(ext, "selectiveLogic", 0),
1309
+ addMemo: !!title,
1310
+ order: (typeof e.insertion_order === "number")? e.insertion_order : 100,
1311
+ position: pos,
1312
+ disable: e.enabled === false,
1313
+ ignoreBudget: ext.ignore_budget === true,
1314
+ excludeRecursion: stExt(ext, "exclude_recursion", false),
1315
+ preventRecursion: stExt(ext, "prevent_recursion", false),
1316
+ matchPersonaDescription: stExt(ext, "match_persona_description", false),
1317
+ matchCharacterDescription: stExt(ext, "match_character_description", false),
1318
+ matchCharacterPersonality: stExt(ext, "match_character_personality", false),
1319
+ matchCharacterDepthPrompt: stExt(ext, "match_character_depth_prompt", false),
1320
+ matchScenario: stExt(ext, "match_scenario", false),
1321
+ matchCreatorNotes: stExt(ext, "match_creator_notes", false),
1322
+ delayUntilRecursion: stExt(ext, "delay_until_recursion", 0),
1323
+ probability: stExt(ext, "probability", 100),
1324
+ useProbability: stExt(ext, "useProbability", true),
1325
+ depth: stExt(ext, "depth", 4),
1326
+ outletName: stExt(ext, "outlet_name", ""),
1327
+ group: stExt(ext, "group", ""),
1328
+ groupOverride: stExt(ext, "group_override", false),
1329
+ groupWeight: stExt(ext, "group_weight", 100),
1330
+ scanDepth: stExt(ext, "scan_depth", null),
1331
+ caseSensitive: stExt(ext, "case_sensitive", null),
1332
+ matchWholeWords: stExt(ext, "match_whole_words", null),
1333
+ useGroupScoring: stExt(ext, "use_group_scoring", null),
1334
+ automationId: stExt(ext, "automation_id", ""),
1335
+ role: stExt(ext, "role", 0),
1336
+ vectorized: stExt(ext, "vectorized", false),
1337
+ sticky: stExt(ext, "sticky", null),
1338
+ cooldown: stExt(ext, "cooldown", null),
1339
+ delay: stExt(ext, "delay", null),
1340
+ displayIndex: stExt(ext, "display_index", index),
1341
+ extensions: ext,
1342
+ triggers: Array.isArray(ext.triggers)? ext.triggers.slice() : [],
1343
+ });
1344
+ if(dec.depth !== null && typeof dec.depth === "number") out.depth = dec.depth;
1345
+ return out;
1346
+ }
1347
+ /**
1348
+ * 整本书 → ST 世界书。顶层只留 `entries`:ST 的世界文件就是这个形状,多一个 `spec` 就 400;
1349
+ * `data.extensions.cc`(rev / groupId / lastPromote)是插件自己的状态,不往外带。
1350
+ */
1351
+ function bookToStWorldInfo(data){
1352
+ var d = data || {};
1353
+ var out = { entries: {} };
1354
+ (Array.isArray(d.entries)? d.entries : []).forEach(function(e, i){
1355
+ var st = bookEntryToSt(e, i);
1356
+ out.entries[String(st.uid)] = st;
1357
+ });
1358
+ return out;
1359
+ }
1360
+ /**
1361
+ * 导出 SillyTavern 世界书。文件名 = ST 里的世界书名(ST 用**上传文件名**建世界),所以这里
1362
+ * 不能再挂 CCv3 那份的 `.lorebook` 后缀,否则导入后书名会变成「锁天司·共享设定.lorebook」。
1363
+ */
1364
+ function exportBookStFile(){
1365
+ var b = state.book && state.book.book;
1366
+ if(!b) return;
1367
+ try{
1368
+ var name = String((b.data && b.data.name) || "").trim() || "shared-lorebook";
1369
+ var blob = new Blob([JSON.stringify(bookToStWorldInfo(b.data), null, 2)], {type:"application/json"});
1370
+ var a=document.createElement("a"); a.href=URL.createObjectURL(blob);
1371
+ a.download = name.replace(/[\\/:*?"<>|]/g,"_") + ".json";
1372
+ a.click();
1373
+ setTimeout(function(){ URL.revokeObjectURL(a.href); }, 800);
1374
+ set({lastMsg: t("book.exportedSt",{name:name})});
1375
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1376
+ }
1186
1377
  function setBookMerge(v){ set({bookMerge: !!v}); }
1187
1378
  async function createGroup(nameArg){
1188
1379
  // 0.8.1:不再依赖 window.prompt(内嵌 webview 会拦掉原生对话框 → 用户看到的是「点了没反应」)。
@@ -1250,9 +1441,10 @@ window.__ModuleLoader__.load({
1250
1441
  var res = await rpc("cc_groupCheck", {id: gid});
1251
1442
  if(res && res.ok && res.value){
1252
1443
  var issues = res.value.issues || [];
1253
- set({lastMsg: issues.length
1254
- ? t("workshop.rel.checkResult", {n: String(issues.length)}) + " — " + issues.map(function(i){ return i.message; }).join(";")
1255
- : t("workshop.rel.checkOk")});
1444
+ // 0.10.0:清单存进 state 由下面分组渲染(不再把十来个问题挤成一行 lastMsg —— 加了跨卡一致性检查以后
1445
+ // 一行文本会长到没法读);lastMsg 只留一句计数,兼容旧观感。
1446
+ set({relIssues: issues, relByCheck: res.value.byCheck || null,
1447
+ lastMsg: issues.length ? t("workshop.rel.checkResult", {n: String(issues.length)}) : t("workshop.rel.checkOk")});
1256
1448
  } else set({lastMsg:String((res && res.error && res.error.message) || "")});
1257
1449
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1258
1450
  set({relBusy:null});
@@ -1408,7 +1600,7 @@ window.__ModuleLoader__.load({
1408
1600
  // 0.4.1:这里原本还有一个 exportLibraryEntry —— 直接下载某张已存角色的 JSON。
1409
1601
  // 三个导出入口归一之后,卡片上的「导出」改成切到导出页,导出动作只剩导出页那一套实现,
1410
1602
  // 这条旁路就没有调用者了(留着它等于「同一次导出有两套行为」),故删除。
1411
- 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 };
1603
+ 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, exportBookStFile:exportBookStFile, setBookMerge:setBookMerge };
1412
1604
  }
1413
1605
 
1414
1606
  var S={
@@ -1974,6 +2166,7 @@ window.__ModuleLoader__.load({
1974
2166
  h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("export.book.hint")),
1975
2167
  h("div",{style:{display:"flex", gap:8, flexWrap:"wrap"}},
1976
2168
  h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
2169
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookStFile(); }}, t("export.book.buttonSt")),
1977
2170
  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")),
1978
2171
  h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.loadBook(); }}, t("book.refresh"))),
1979
2172
  s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, s.lastMsg):null) : null
@@ -2059,6 +2252,7 @@ window.__ModuleLoader__.load({
2059
2252
  h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", margin:"6px 0 8px", lineHeight:1.7}}, t("export.book.hint")),
2060
2253
  h("div",{style:{display:"flex", gap:8, flexWrap:"wrap", alignItems:"center"}},
2061
2254
  h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
2255
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookStFile(); }}, t("export.book.buttonSt")),
2062
2256
  h("label",{style:{display:"flex", alignItems:"center", gap:6, fontSize:11, color:"var(--dsw-alias-label-secondary)", cursor:"pointer"}},
2063
2257
  h("input",{type:"checkbox", checked:!!s.bookMerge, onChange:function(ev){ store.setBookMerge(ev.target.checked); }}),
2064
2258
  t("export.book.merge"))));
@@ -2195,6 +2389,25 @@ window.__ModuleLoader__.load({
2195
2389
  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")),
2196
2390
  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")),
2197
2391
  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")))),
2392
+ // —— 体检清单(0.10.0):按 error / warn 分组显示,每条前面挂检查类别 ——
2393
+ // 界面上原本只有一行拼接文本;加上跨卡一致性检查以后问题可能十来条,一行会糊成一片。
2394
+ // 这里只负责**显示**宿主返回的 issues / byCheck,不参与任何统计口径(数字都在卡组层算好了)。
2395
+ (function(){
2396
+ var issues = s.relIssues;
2397
+ if(!issues || !issues.length) return null;
2398
+ var errs = issues.filter(function(i){ return i.level === "error"; });
2399
+ var warns = issues.filter(function(i){ return i.level !== "error"; });
2400
+ function row(i, key){
2401
+ return h("div",{key:key, style:{display:"flex", gap:6, alignItems:"flex-start", padding:"2px 0", lineHeight:1.6}},
2402
+ h("span",{style:Object.assign({}, S.badge, {flexShrink:0, fontSize:10, opacity:0.85})}, String(i.check || "")),
2403
+ h("span",{style:{fontSize:11.5, color: i.level === "error" ? "#e8a6a6" : "var(--dsw-alias-label-secondary)"}}, String(i.message || "")));
2404
+ }
2405
+ return h("div",{style:{marginTop:12, borderTop:"1px solid var(--dsw-alias-border-l1)", paddingTop:10}},
2406
+ h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}},
2407
+ t("workshop.rel.checkStats", {e: String(errs.length), w: String(warns.length)})),
2408
+ errs.map(function(i, idx){ return row(i, "ce" + idx); }),
2409
+ warns.map(function(i, idx){ return row(i, "cw" + idx); }));
2410
+ })(),
2198
2411
  s.lastMsg ? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg) : null,
2199
2412
  // —— 二级弹窗:改一条关系 / 加一条关系(列表只读,动作都在这里)——
2200
2413
  relEdit ? relDialog("workshop.rel.editTitle", relEdit, setRelEdit,