@xia-sc/dsh-cc-studio 0.8.3 → 0.9.1

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":"书名",
@@ -79,8 +79,10 @@ window.__ModuleLoader__.load({
79
79
  "book.promoteHint":"把这 N 条搬进共享书,并从每张卡里删掉 —— 导出前先做这一步,卡里就不再有重复。",
80
80
  "book.promotePreview":"将提升 {n} 条、从各卡共删 {m} 条重复",
81
81
  "book.promoteDone":"已提升 {n} 条(删掉 {m} 条重复)",
82
+ "book.promoteSyncedDraft":"已同步当前草稿,删掉 {n} 条重复(否则保存时会把它们写回去)",
82
83
  "book.exportBook":"导出共享世界书 JSON",
83
84
  "book.exported":"已导出 {name}",
85
+ "book.exportedSt":"已导出 SillyTavern 世界书「{name}」—— 在 SillyTavern 里「世界信息 → 导入」选它即可",
84
86
  "book.applyCard":"合并进当前卡",
85
87
  "book.applyDone":"已往当前卡补了 {n} 条",
86
88
  "book.applyNone":"当前卡已包含这本共享书的全部条目。",
@@ -91,9 +93,11 @@ window.__ModuleLoader__.load({
91
93
  "book.saved":"已保存(rev {rev})",
92
94
  // —— 0.9.0 导出页的共享书块 ——
93
95
  "export.book.title":"共享世界书",
94
- "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}」",
95
98
  "export.book.none":"当前卡不在任何分组里,或这个分组还没有共享世界书。",
96
- "export.book.button":"⬇ 导出共享世界书",
99
+ "export.book.button":"⬇ CCv3 独立世界书",
100
+ "export.book.buttonSt":"⬇ SillyTavern 世界书",
97
101
  "export.book.merge":"导出 JSON 时把共享条目并入这张卡(给不支持全局世界书的客户端)",
98
102
  "export.book.merged":"(已并入 {n} 条共享条目)",
99
103
  "action.expand":"✦ AI 扩写",
@@ -271,11 +275,10 @@ window.__ModuleLoader__.load({
271
275
  "store.validateFailed":"校验未通过",
272
276
  "store.importedOk":"已导入 {k}:{name} · {n} lore(可在侧边栏 ★ 保存)",
273
277
  "store.importedSaved":"已导入并自动保存:{name}",
274
- "store.errNotCcJson":"不是合法的 CCv3 JSON(缺少 spec)",
275
- "store.errSpecMismatch":"spec 需为 chara_card_v3,当前为 {v}",
276
- "store.errNoData":"缺少 data",
278
+ "store.importedUpgraded":"{v} 卡已升级为 CCv3",
277
279
  "store.errImportFailed":"导入失败:{msg}",
278
280
  "store.errReadFileFailed":"读取文件失败",
281
+ "store.parsingJson":"正在解析 JSON…",
279
282
  "store.parsingPng":"正在解析 PNG…",
280
283
  "store.errPngImportFailed":"PNG 导入失败:{msg}",
281
284
  "store.errReadPngFailed":"读取 PNG 失败",
@@ -311,6 +314,8 @@ window.__ModuleLoader__.load({
311
314
  "store.loadedCard":"已载入:{name} · {n} lore(ID:{id},再次保存将更新此卡)",
312
315
  "store.errLoadFailed":"载入失败",
313
316
  "store.confirmDelete":"确定删除该角色?此操作不可撤销。",
317
+ "relations.confirmDeleteGroup":"确定删除这个分组?分组里的节点与关系、以及该组的共享世界书(若有)会一起删掉,且不可恢复。成员卡不会被删除,只会离开这个组。",
318
+ "book.dupBook":"另有 {n} 条在成员卡里与共享书逐字重复(回填进卡的副本)—— 提升时会一并清掉卡里那几份,不会再往书里加。",
314
319
  "store.deleted":"已删除",
315
320
  "store.errDeleteFailed":"删除失败",
316
321
  "store.needLoadFirst":"请先载入一张卡后再重命名",
@@ -371,7 +376,7 @@ window.__ModuleLoader__.load({
371
376
  "workshop.rel.dirLabel":"Direction",
372
377
  "workshop.rel.myCard":"current card",
373
378
  "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.",
379
+ "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).",
375
380
  "book.noGroup":"Shared lorebooks live on a group: create one under “Saved characters” on the right and put the member cards in it.",
376
381
  "book.empty":"This group has no shared lorebook yet.",
377
382
  "book.name":"Book name",
@@ -390,8 +395,10 @@ window.__ModuleLoader__.load({
390
395
  "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
396
  "book.promotePreview":"Will promote {n} and delete {m} duplicates across the cards",
392
397
  "book.promoteDone":"Promoted {n} (removed {m} duplicates)",
398
+ "book.promoteSyncedDraft":"Current draft synced — removed {n} duplicates (saving would have written them back)",
393
399
  "book.exportBook":"Export shared lorebook JSON",
394
400
  "book.exported":"Exported {name}",
401
+ "book.exportedSt":"Exported the SillyTavern lorebook “{name}” — pick it in SillyTavern via World Info → Import",
395
402
  "book.applyCard":"Merge into the current card",
396
403
  "book.applyDone":"Added {n} entries to the current card",
397
404
  "book.applyNone":"The current card already contains every entry of this book.",
@@ -401,9 +408,11 @@ window.__ModuleLoader__.load({
401
408
  "book.refresh":"Refresh",
402
409
  "book.saved":"Saved (rev {rev})",
403
410
  "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.",
411
+ "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.",
412
+ "export.stWorldBound":"SillyTavern world binding written: “{name}”",
405
413
  "export.book.none":"This card is in no group, or its group has no shared lorebook yet.",
406
- "export.book.button":"⬇ Export shared lorebook",
414
+ "export.book.button":"⬇ CCv3 standalone lorebook",
415
+ "export.book.buttonSt":"⬇ SillyTavern lorebook",
407
416
  "export.book.merge":"Merge the shared entries into this card when exporting JSON (for clients without a global lorebook)",
408
417
  "export.book.merged":"({n} shared entries merged in)",
409
418
  "action.expand":"AI Expand",
@@ -581,11 +590,10 @@ window.__ModuleLoader__.load({
581
590
  "store.validateFailed":"Validation failed",
582
591
  "store.importedOk":"Imported {k}: {name} · {n} lore (★ Save in the sidebar)",
583
592
  "store.importedSaved":"Imported and auto-saved: {name}",
584
- "store.errNotCcJson":"Not a valid CCv3 JSON (missing spec)",
585
- "store.errSpecMismatch":"spec must be chara_card_v3, got {v}",
586
- "store.errNoData":"missing data",
593
+ "store.importedUpgraded":"{v} card upgraded to CCv3",
587
594
  "store.errImportFailed":"Import failed: {msg}",
588
595
  "store.errReadFileFailed":"Failed to read the file",
596
+ "store.parsingJson":"Parsing JSON…",
589
597
  "store.parsingPng":"Parsing PNG…",
590
598
  "store.errPngImportFailed":"PNG import failed: {msg}",
591
599
  "store.errReadPngFailed":"Failed to read the PNG",
@@ -621,6 +629,8 @@ window.__ModuleLoader__.load({
621
629
  "store.loadedCard":"Loaded: {name} · {n} lore (ID:{id}, saving again will update this card)",
622
630
  "store.errLoadFailed":"Load failed",
623
631
  "store.confirmDelete":"Delete this character? This cannot be undone.",
632
+ "relations.confirmDeleteGroup":"Delete this group? Its nodes, relations and the group's shared lorebook (if any) go with it, and this cannot be undone. Member cards are not deleted — they just leave the group.",
633
+ "book.dupBook":"{n} more entries in the member cards are byte-identical to the shared book (copies merged into the cards) — promoting also cleans those up, without adding anything to the book.",
624
634
  "store.deleted":"Deleted",
625
635
  "store.errDeleteFailed":"Delete failed",
626
636
  "store.needLoadFirst":"Load a card before renaming",
@@ -803,7 +813,11 @@ window.__ModuleLoader__.load({
803
813
  for(var i=0;i<bin.length;i++) bytes[i]=bin.charCodeAt(i);
804
814
  return bytes;
805
815
  }
806
- function handleImportedDraft(obj, label){
816
+ /**
817
+ * 导入结果的统一收尾。`upgradedFrom` 非空 = 这份卡原来是 V2/V1(0.9.1 B2 自动升级成 CCv3),
818
+ * 必须在提示里说出来 —— 静默把一张 V2 卡变成 V3 形状不算「什么都没发生」。
819
+ */
820
+ function handleImportedDraft(obj, label, upgradedFrom){
807
821
  obj.spec_version=obj.spec_version||"3.0";
808
822
  obj.data.character_book=obj.data.character_book||{name:"",description:"",scan_depth:4,token_budget:1200,recursive_scanning:false,extensions:{},entries:[]};
809
823
  if(!Array.isArray(obj.data.character_book.entries)) obj.data.character_book.entries=[];
@@ -812,7 +826,9 @@ window.__ModuleLoader__.load({
812
826
  if(!Array.isArray(obj.data.tags)) obj.data.tags=[];
813
827
  if(!Array.isArray(obj.data.assets)) obj.data.assets=[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}];
814
828
  setDraftFromHost(obj);
815
- 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)})});
829
+ var msg=t("store.importedOk",{k:label.trim(), name:(obj.data.name||t("workshop.library.unnamed")), n:(obj.data.character_book.entries.length||0)});
830
+ if(upgradedFrom) msg += " · " + t("store.importedUpgraded",{v:(String(upgradedFrom)==="chara_card_v1"? "V1":"V2")});
831
+ set({currentLibraryId:null, lastMsg: msg});
816
832
  setTimeout(pushDraftToHost,80);
817
833
  if(state.autoValidate) rpc("validate",{card:obj}).then(function(r){ if(r&&r.ok) set({valid:r.value}); }).catch(function(){});
818
834
  if(state.autoSaveOnImport){
@@ -840,16 +856,22 @@ window.__ModuleLoader__.load({
840
856
  else isJson=true;
841
857
  }
842
858
  if(isJson){
859
+ // 0.9.1(B2):JSON 也交给宿主升级 —— 客户端以前自己 parse 且只认 chara_card_v3,
860
+ // 于是 ST 里导出的 V2 卡一张都进不来。现在三条导入路径(JSON/PNG/CHARX)共用宿主那份
861
+ // upgradeCardToV3,客户端只负责把文本递过去、把「已升级」说出来。
843
862
  var reader=new FileReader();
844
863
  reader.onload=function(){
845
864
  try{
846
865
  var text=String(reader.result||"");
847
- var obj=JSON.parse(text);
848
- if(obj && obj.draft && obj.draft.spec) obj=obj.draft;
849
- if(!obj || typeof obj!=="object" || !obj.spec) throw new Error(t("store.errNotCcJson"));
850
- if(obj.spec!=="chara_card_v3") throw new Error(t("store.errSpecMismatch",{v:String(obj.spec)}));
851
- if(!obj.data || typeof obj.data!=="object") throw new Error(t("store.errNoData"));
852
- handleImportedDraft(obj, " JSON");
866
+ var sid=state.currentSessionId||null;
867
+ var args=sid?{json:text, sessionId:sid}:{json:text};
868
+ set({lastMsg:t("store.parsingJson"), busy:"importJson"});
869
+ rpc("cc_importFromJson", args).then(function(res){
870
+ set({busy:null});
871
+ if(res && res.ok && res.value && res.value.draft){
872
+ handleImportedDraft(res.value.draft, " JSON", res.value.upgradedFrom);
873
+ } else set({lastMsg:t("store.errImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
874
+ }).catch(function(e){ set({busy:null, lastMsg:t("store.errImportFailed",{msg:String(e&&e.message||e)})}); });
853
875
  }catch(e){ set({lastMsg:t("store.errImportFailed",{msg:String(e&&e.message||e)})}); }
854
876
  };
855
877
  reader.onerror=function(){ set({lastMsg:t("store.errReadFileFailed")}); };
@@ -865,7 +887,7 @@ window.__ModuleLoader__.load({
865
887
  rpc("cc_importFromPng", args).then(function(res){
866
888
  set({busy:null});
867
889
  if(res && res.ok && res.value && res.value.draft){
868
- handleImportedDraft(res.value.draft, " PNG");
890
+ handleImportedDraft(res.value.draft, " PNG", res.value.upgradedFrom);
869
891
  } else set({lastMsg:t("store.errPngImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
870
892
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errPngImportFailed",{msg:String(e&&e.message||e)})}); });
871
893
  }catch(e){ set({lastMsg:t("store.errPngImportFailed",{msg:String(e&&e.message||e)})}); }
@@ -883,7 +905,7 @@ window.__ModuleLoader__.load({
883
905
  rpc("cc_importFromCharx", args2).then(function(res){
884
906
  set({busy:null});
885
907
  if(res && res.ok && res.value && res.value.draft){
886
- handleImportedDraft(res.value.draft, " CHARX");
908
+ handleImportedDraft(res.value.draft, " CHARX", res.value.upgradedFrom);
887
909
  } else set({lastMsg:t("store.errCharxImportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
888
910
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errCharxImportFailed",{msg:String(e&&e.message||e)})}); });
889
911
  }catch(e){ set({lastMsg:t("store.errCharxImportFailed",{msg:String(e&&e.message||e)})}); }
@@ -901,9 +923,20 @@ window.__ModuleLoader__.load({
901
923
  var norm=function(e){ return String((e&&e.content)!=null? e.content : "").replace(/\s+/g," ").trim(); };
902
924
  var x=norm(a); return !!x && x===norm(b);
903
925
  }
926
+ /**
927
+ * ST 兼容层(0.9.1):当前分组的共享书名 —— SillyTavern 的「角色↔世界书」绑定就是
928
+ * `data.extensions.world = <世界书文件名>`(`characters.js:391,617`),而我们导出的那本世界书
929
+ * 就叫 `<书名>.json`,所以把书名写进去即可。没有共享书时返回 ""(**不写**绑定,免得留一个悬空的名字)。
930
+ */
931
+ function stWorldName(){
932
+ var bk=state.book;
933
+ if(!bk || !bk.exists) return "";
934
+ return String((bk.book && bk.book.data && bk.book.data.name) || "").trim();
935
+ }
904
936
  /**
905
937
  * 导出 JSON。0.9.0:若在导出页勾了「并入共享条目」,则**只在导出的副本上**把该组的共享世界书
906
938
  * 里卡里还没有的条目补进去(给不支持全局世界书的客户端兜底)——草稿本身一个字节都不动。
939
+ * 0.9.1:同样只在副本上写 ST 的世界书绑定(`data.extensions.world`)。
907
940
  */
908
941
  function exportJSON(){
909
942
  var out=state.draft, merged=0;
@@ -918,13 +951,22 @@ window.__ModuleLoader__.load({
918
951
  merged=add.length;
919
952
  }
920
953
  }
954
+ var bound=stWorldName();
955
+ if(bound){
956
+ if(out===state.draft) out=JSON.parse(JSON.stringify(state.draft));
957
+ out.data.extensions=Object.assign({}, out.data.extensions);
958
+ out.data.extensions.world=bound;
959
+ }
921
960
  var blob=new Blob([JSON.stringify(out,null,2)],{type:"application/json"});
922
961
  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);
923
- set({lastMsg: merged? t("store.exportedJson") + " " + t("export.book.merged",{n:String(merged)}) : t("store.exportedJson")});
962
+ set({lastMsg: (merged? t("store.exportedJson") + " " + t("export.book.merged",{n:String(merged)}) : t("store.exportedJson"))
963
+ + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
924
964
  }
925
965
  function exportPNG(){
926
966
  var sid=state.currentSessionId||null;
967
+ var bound=stWorldName();
927
968
  var args=sid?{sessionId:sid}:{};
969
+ if(bound) args.stWorld=bound;
928
970
  set({busy:"exportPng", lastMsg:t("store.generatingPng")});
929
971
  rpc("cc_exportPng", args).then(function(res){
930
972
  set({busy:null});
@@ -933,7 +975,7 @@ window.__ModuleLoader__.load({
933
975
  var bytes=b64ToBytes(res.value.b64);
934
976
  var blob=new Blob([bytes],{type:"image/png"});
935
977
  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);
936
- set({lastMsg:t("store.exportedPng",{name:(res.value.filename||"card.png")})});
978
+ set({lastMsg:t("store.exportedPng",{name:(res.value.filename||"card.png")}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
937
979
  }catch(e){ set({lastMsg:t("store.errPngExportFailed",{msg:String(e&&e.message||e)})}); }
938
980
  } else set({lastMsg:t("store.errPngExportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
939
981
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errPngExportFailed",{msg:String(e&&e.message||e)})}); });
@@ -950,7 +992,9 @@ window.__ModuleLoader__.load({
950
992
  try{
951
993
  var b64=arrayBufferToB64(reader.result);
952
994
  var sid=state.currentSessionId||null;
995
+ var bound=stWorldName();
953
996
  var args=sid?{sessionId:sid, imageB64:b64}:{imageB64:b64};
997
+ if(bound) args.stWorld=bound;
954
998
  set({busy:"exportPng", lastMsg:t("store.writingPng",{name:f.name})});
955
999
  rpc("cc_exportPng", args).then(function(res){
956
1000
  set({busy:null});
@@ -959,7 +1003,7 @@ window.__ModuleLoader__.load({
959
1003
  var bytes=b64ToBytes(res.value.b64);
960
1004
  var blob=new Blob([bytes],{type:"image/png"});
961
1005
  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);
962
- set({lastMsg:t("store.wrotePng",{name:(res.value.filename||f.name)})});
1006
+ set({lastMsg:t("store.wrotePng",{name:(res.value.filename||f.name)}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
963
1007
  }catch(e){ set({lastMsg:t("store.errWritePngFailed",{msg:String(e&&e.message||e)})}); }
964
1008
  } else set({lastMsg:t("store.errWritePngFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
965
1009
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errWritePngFailed",{msg:String(e&&e.message||e)})}); });
@@ -973,7 +1017,9 @@ window.__ModuleLoader__.load({
973
1017
  }
974
1018
  function exportCharx(){
975
1019
  var sid=state.currentSessionId||null;
1020
+ var bound=stWorldName();
976
1021
  var args=sid?{sessionId:sid}:{};
1022
+ if(bound) args.stWorld=bound;
977
1023
  set({busy:"exportCharx", lastMsg:t("store.generatingCharx")});
978
1024
  rpc("cc_exportCharx", args).then(function(res){
979
1025
  set({busy:null});
@@ -982,7 +1028,7 @@ window.__ModuleLoader__.load({
982
1028
  var bytes2=b64ToBytes(res.value.b64);
983
1029
  var blob2=new Blob([bytes2],{type:"application/zip"});
984
1030
  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);
985
- set({lastMsg:t("store.exportedCharx",{name:(res.value.filename||"card.charx")})});
1031
+ set({lastMsg:t("store.exportedCharx",{name:(res.value.filename||"card.charx")}) + (bound? " · " + t("export.stWorldBound",{name:bound}) : "")});
986
1032
  }catch(e){ set({lastMsg:t("store.errCharxExportFailed",{msg:String(e&&e.message||e)})}); }
987
1033
  } else set({lastMsg:t("store.errCharxExportFailed",{msg:(res&&res.error?String(res.error.message):t("common.unknownError"))})});
988
1034
  }).catch(function(e){ set({busy:null, lastMsg:t("store.errCharxExportFailed",{msg:String(e&&e.message||e)})}); });
@@ -1090,6 +1136,40 @@ window.__ModuleLoader__.load({
1090
1136
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1091
1137
  set({bookBusy:null});
1092
1138
  }
1139
+ /**
1140
+ * 提升之后同步**界面里的草稿**(与工具半 syncDraftAfterPromote 同一套判据)。
1141
+ *
1142
+ * 宿主半 `cc_promoteBook` 删的是**库里的卡**,界面里这份草稿是另一个副本 —— 不同步的话,
1143
+ * 用户接着点「保存当前」就会把刚删掉的重复条目原样写回去,去重白做且毫无提示。
1144
+ * 三条自律:① 只对「这次真的被删过条目、且正是当前编辑的那张卡」动手;
1145
+ * ② 只认**内容逐字相符**(用户在界面里改过内容的条目一律不动);
1146
+ * ③ 共享书没读到(state.book 为空)时什么都不做 —— 宁可不同步,也不猜着删。
1147
+ */
1148
+ function syncDraftAfterPromote(v){
1149
+ try{
1150
+ var cid = String(state.currentLibraryId||"");
1151
+ if(!cid) return 0;
1152
+ var touched = ((v && Array.isArray(v.perCard)) ? v.perCard : []).filter(function(c){
1153
+ return c && !c.error && c.removed > 0 && String(c.id) === cid;
1154
+ });
1155
+ if(!touched.length) return 0;
1156
+ var bk = state.book;
1157
+ var bookEntries = (bk && bk.book && bk.book.data && Array.isArray(bk.book.data.entries)) ? bk.book.data.entries : [];
1158
+ var contents = {};
1159
+ bookEntries.forEach(function(e){ var c = String((e && e.content)==null? "" : e.content); if(c.trim()) contents[c] = true; });
1160
+ var d = state.draft;
1161
+ var entries = (d && d.data && d.data.character_book && Array.isArray(d.data.character_book.entries)) ? d.data.character_book.entries : [];
1162
+ var removed = 0;
1163
+ entries.forEach(function(e){ if(contents[String((e && e.content)==null? "": e.content)]) removed++; });
1164
+ if(!removed) return 0;
1165
+ updateDraft(function(data){
1166
+ var cb = data.character_book;
1167
+ if(!cb || !Array.isArray(cb.entries)) return;
1168
+ cb.entries = cb.entries.filter(function(e){ return !contents[String((e && e.content)==null? "": e.content)]; });
1169
+ });
1170
+ return removed;
1171
+ }catch(e){ return 0; }
1172
+ }
1093
1173
  /** 提升去重:`apply` 为假时只扫描预览(宿主半默认也是 dryRun,两边都不写盘)。 */
1094
1174
  async function promoteBook(keys, apply){
1095
1175
  var gid = String(state.currentGroupId||"");
@@ -1099,11 +1179,16 @@ window.__ModuleLoader__.load({
1099
1179
  var res = await rpc("cc_promoteBook", {groupId: gid, apply: !!apply, keys: (keys && keys.length)? keys : undefined});
1100
1180
  if(res && res.ok && res.value){
1101
1181
  var v = res.value;
1102
- set({lastMsg: v.dryRun
1182
+ var msg = v.dryRun
1103
1183
  ? 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)})});
1184
+ : t("book.promoteDone",{n:String(v.promoted||0), m:String(v.removedTotal||0)});
1185
+ set({lastMsg: msg});
1105
1186
  await loadBook(gid);
1106
- if(!v.dryRun){ await refreshLibrary(); await loadGroup(gid); }
1187
+ if(!v.dryRun){
1188
+ var synced = syncDraftAfterPromote(v); // 必须在 loadBook 之后:那时 state.book 才是提升后的新书
1189
+ if(synced) set({lastMsg: msg + " · " + t("book.promoteSyncedDraft",{n:String(synced)})});
1190
+ await refreshLibrary(); await loadGroup(gid);
1191
+ }
1107
1192
  } else set({lastMsg:String((res && res.error && res.error.message) || "")});
1108
1193
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1109
1194
  set({bookBusy:null});
@@ -1138,6 +1223,152 @@ window.__ModuleLoader__.load({
1138
1223
  set({lastMsg: t("book.exported",{name:String(b.data.name||"")})});
1139
1224
  }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1140
1225
  }
1226
+ /**
1227
+ * 共享世界书 → **SillyTavern 原生世界书**(`{ entries: { "<uid>": {...} } }`)。
1228
+ *
1229
+ * 为什么要另出一份文件(0.9.1,用户实机报告):ST 服务端 `/api/worldinfo/import` 拿到文件后
1230
+ * 第一件事就是 `if (!('entries' in worldContent)) throw`(SillyTavern `src/endpoints/worldinfo.js`),
1231
+ * 而 CCv3 独立 lorebook 的顶层是 `{spec, data}` —— 于是用户把 `.lorebook.json` 拖进 ST,看到的是
1232
+ * `Error importing world info: … Bad Request`。ST 客户端那条 `importWorldInfo()` 也只转
1233
+ * Novel / Agnai / Risu 三种格式,1.19.0 的整份 `world-info.js` 里**搜不到 `lorebook_v3`** ——
1234
+ * 所以这不是「换个后缀」能解决的,是规范本身 ST 不认,必须按它的形态另导一份。
1235
+ *
1236
+ * 条目映射照抄 ST 自己的 `convertCharacterBook()`(ST 唯一权威的 CC 书转换器,1.19.0):
1237
+ * uid←id、key←keys、keysecondary←secondary_keys、comment←comment/name、content←content、
1238
+ * order←insertion_order、disable←!enabled、addMemo←有没有标题,其余可调项一律从
1239
+ * `extensions.*` 取(`cc_auto` / `cc_world_ref` 也随 `extensions` 原样带过去 —— ST 的条目本来
1240
+ * 就有这个自由字段)。
1241
+ *
1242
+ * 唯一 ST 自己做不到、必须我们补的是 **`@@position` / `@@depth` 装饰器**:ST 的
1243
+ * `parseDecorators()` 只认 `@@activate` / `@@dont_activate`(`KNOWN_DECORATORS`),其余 `@@` 行
1244
+ * **连同正文一起被丢掉**(它从第一个非 `@@` 行开始截取正文)—— 不翻译的话,自动生成的 5 条会
1245
+ * 全部落在默认插入位置、装饰器静默消失。所以这两类翻译成 ST 字段并删掉该行;`@@activate`
1246
+ * 留给 ST 自己认(它本来就有这个语义,硬翻成 constant 只是多一层猜测)。
1247
+ */
1248
+ 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:[] };
1249
+ /** ST 的 `world_info_position`:before=0 / after=1 / ANTop=2 / ANBottom=3 / atDepth=4 / EMTop=5 / EMBottom=6。 */
1250
+ 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 };
1251
+ /** ST 的 `??` 语义:`extensions.<key>` 优先,缺省才用默认值(与 convertCharacterBook 逐行对应)。 */
1252
+ function stExt(ext, key, dflt){ var v = ext[key]; return (v === undefined || v === null)? dflt : v; }
1253
+ /**
1254
+ * 解析正文**开头连续**的 `@@` 行(与 ST 的 `parseDecorators` 同构:只认开头那一段)。
1255
+ * 只翻译 ST 认不出的 `@@position` / `@@depth`;`@@activate` / `@@dont_activate` 与未知写法
1256
+ * 原样留下 —— ST 或者自己有语义、或者自己会丢,我们不猜。
1257
+ */
1258
+ function unfurlBookDecorators(content){
1259
+ var text = String(content == null? "" : content);
1260
+ var lines = text.split("\n");
1261
+ var drop = [], pos = null, depth = null;
1262
+ for(var i=0;i<lines.length;i++){
1263
+ var line = lines[i];
1264
+ if(line.slice(0,2) !== "@@") break; // 装饰器区到头
1265
+ if(line.slice(0,3) === "@@@") break; // ST 的转义写法:后面全当原文,不碰
1266
+ var m = /^@@([a-z_]+)(?:\s+(.*))?$/.exec(line.trim());
1267
+ if(!m) break; // 语法可疑:整段交给 ST 自己处理
1268
+ var name = m[1], arg = String(m[2] == null? "" : m[2]).trim();
1269
+ if(name === "position"){
1270
+ // ST 没有「scenario」之类插入点;ST 自己的 convertCharacterBook 对除 before_char 之外的
1271
+ // 一律当 after —— 认不出的值就跟它的回退走,不自己发明位置。
1272
+ var p = ST_POSITION[arg.toLowerCase().replace(/[\s-]+/g,"_")];
1273
+ pos = (typeof p === "number")? p : 1;
1274
+ drop.push(i);
1275
+ } else if(name === "depth"){
1276
+ var n = parseInt(arg, 10);
1277
+ if(isFinite(n) && n >= 0){ depth = n; pos = 4; drop.push(i); } // atDepth
1278
+ }
1279
+ }
1280
+ if(!drop.length) return { position:null, depth:null, content:text };
1281
+ var kept = lines.filter(function(_, i){ return drop.indexOf(i) < 0; });
1282
+ while(kept.length && !String(kept[0]).trim()) kept.shift(); // 删掉装饰器后留下的空行
1283
+ return { position:pos, depth:depth, content: kept.join("\n") };
1284
+ }
1285
+ /** 单条共享书条目 → ST 条目(字段映射与优先级见上面那段注释)。 */
1286
+ function bookEntryToSt(entry, index){
1287
+ var e = entry || {};
1288
+ var ext = (e.extensions && typeof e.extensions === "object")? e.extensions : {};
1289
+ var dec = unfurlBookDecorators(e.content);
1290
+ var title = String(e.comment || e.name || "");
1291
+ var pos = dec.position;
1292
+ if(pos === null) pos = (typeof ext.position === "number")? ext.position : null; // ST 自己的位置偏好
1293
+ if(pos === null) pos = (String(e.position || "") === "before_char")? 0 : 1; // ST convertCharacterBook 的判据
1294
+ var id = (e.id === undefined || e.id === null || e.id === "")? index : e.id;
1295
+ var out = Object.assign({}, ST_ENTRY_TEMPLATE, {
1296
+ uid: id,
1297
+ key: Array.isArray(e.keys)? e.keys.map(String) : [],
1298
+ keysecondary: Array.isArray(e.secondary_keys)? e.secondary_keys.map(String) : [],
1299
+ comment: title,
1300
+ content: dec.content,
1301
+ constant: e.constant === true,
1302
+ selective: e.selective === true,
1303
+ selectiveLogic: stExt(ext, "selectiveLogic", 0),
1304
+ addMemo: !!title,
1305
+ order: (typeof e.insertion_order === "number")? e.insertion_order : 100,
1306
+ position: pos,
1307
+ disable: e.enabled === false,
1308
+ ignoreBudget: ext.ignore_budget === true,
1309
+ excludeRecursion: stExt(ext, "exclude_recursion", false),
1310
+ preventRecursion: stExt(ext, "prevent_recursion", false),
1311
+ matchPersonaDescription: stExt(ext, "match_persona_description", false),
1312
+ matchCharacterDescription: stExt(ext, "match_character_description", false),
1313
+ matchCharacterPersonality: stExt(ext, "match_character_personality", false),
1314
+ matchCharacterDepthPrompt: stExt(ext, "match_character_depth_prompt", false),
1315
+ matchScenario: stExt(ext, "match_scenario", false),
1316
+ matchCreatorNotes: stExt(ext, "match_creator_notes", false),
1317
+ delayUntilRecursion: stExt(ext, "delay_until_recursion", 0),
1318
+ probability: stExt(ext, "probability", 100),
1319
+ useProbability: stExt(ext, "useProbability", true),
1320
+ depth: stExt(ext, "depth", 4),
1321
+ outletName: stExt(ext, "outlet_name", ""),
1322
+ group: stExt(ext, "group", ""),
1323
+ groupOverride: stExt(ext, "group_override", false),
1324
+ groupWeight: stExt(ext, "group_weight", 100),
1325
+ scanDepth: stExt(ext, "scan_depth", null),
1326
+ caseSensitive: stExt(ext, "case_sensitive", null),
1327
+ matchWholeWords: stExt(ext, "match_whole_words", null),
1328
+ useGroupScoring: stExt(ext, "use_group_scoring", null),
1329
+ automationId: stExt(ext, "automation_id", ""),
1330
+ role: stExt(ext, "role", 0),
1331
+ vectorized: stExt(ext, "vectorized", false),
1332
+ sticky: stExt(ext, "sticky", null),
1333
+ cooldown: stExt(ext, "cooldown", null),
1334
+ delay: stExt(ext, "delay", null),
1335
+ displayIndex: stExt(ext, "display_index", index),
1336
+ extensions: ext,
1337
+ triggers: Array.isArray(ext.triggers)? ext.triggers.slice() : [],
1338
+ });
1339
+ if(dec.depth !== null && typeof dec.depth === "number") out.depth = dec.depth;
1340
+ return out;
1341
+ }
1342
+ /**
1343
+ * 整本书 → ST 世界书。顶层只留 `entries`:ST 的世界文件就是这个形状,多一个 `spec` 就 400;
1344
+ * `data.extensions.cc`(rev / groupId / lastPromote)是插件自己的状态,不往外带。
1345
+ */
1346
+ function bookToStWorldInfo(data){
1347
+ var d = data || {};
1348
+ var out = { entries: {} };
1349
+ (Array.isArray(d.entries)? d.entries : []).forEach(function(e, i){
1350
+ var st = bookEntryToSt(e, i);
1351
+ out.entries[String(st.uid)] = st;
1352
+ });
1353
+ return out;
1354
+ }
1355
+ /**
1356
+ * 导出 SillyTavern 世界书。文件名 = ST 里的世界书名(ST 用**上传文件名**建世界),所以这里
1357
+ * 不能再挂 CCv3 那份的 `.lorebook` 后缀,否则导入后书名会变成「锁天司·共享设定.lorebook」。
1358
+ */
1359
+ function exportBookStFile(){
1360
+ var b = state.book && state.book.book;
1361
+ if(!b) return;
1362
+ try{
1363
+ var name = String((b.data && b.data.name) || "").trim() || "shared-lorebook";
1364
+ var blob = new Blob([JSON.stringify(bookToStWorldInfo(b.data), null, 2)], {type:"application/json"});
1365
+ var a=document.createElement("a"); a.href=URL.createObjectURL(blob);
1366
+ a.download = name.replace(/[\\/:*?"<>|]/g,"_") + ".json";
1367
+ a.click();
1368
+ setTimeout(function(){ URL.revokeObjectURL(a.href); }, 800);
1369
+ set({lastMsg: t("book.exportedSt",{name:name})});
1370
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1371
+ }
1141
1372
  function setBookMerge(v){ set({bookMerge: !!v}); }
1142
1373
  async function createGroup(nameArg){
1143
1374
  // 0.8.1:不再依赖 window.prompt(内嵌 webview 会拦掉原生对话框 → 用户看到的是「点了没反应」)。
@@ -1156,7 +1387,9 @@ window.__ModuleLoader__.load({
1156
1387
  set({relBusy:null});
1157
1388
  }
1158
1389
  async function deleteGroup(id){
1159
- try{ if(typeof window!=="undefined" && window.confirm && !window.confirm(t("store.confirmDelete"))) return; }catch(e){}
1390
+ // 删组的确认文案要**专用**(原来复用了「确定删除该角色?」)—— 而且必须提共享世界书:
1391
+ // 那是用户可能整理了很久的东西,删掉不可恢复(第二轮报告 4.1 的同一条信息完整性要求)。
1392
+ try{ if(typeof window!=="undefined" && window.confirm && !window.confirm(t("relations.confirmDeleteGroup"))) return; }catch(e){}
1160
1393
  set({relBusy:"del"});
1161
1394
  try{
1162
1395
  var res = await rpc("cc_deleteGroup", {id: id});
@@ -1361,7 +1594,7 @@ window.__ModuleLoader__.load({
1361
1594
  // 0.4.1:这里原本还有一个 exportLibraryEntry —— 直接下载某张已存角色的 JSON。
1362
1595
  // 三个导出入口归一之后,卡片上的「导出」改成切到导出页,导出动作只剩导出页那一套实现,
1363
1596
  // 这条旁路就没有调用者了(留着它等于「同一次导出有两套行为」),故删除。
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 };
1597
+ 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 };
1365
1598
  }
1366
1599
 
1367
1600
  var S={
@@ -1837,6 +2070,9 @@ window.__ModuleLoader__.load({
1837
2070
  var gidB = String(s.currentGroupId||"");
1838
2071
  var bookReady = !!(bk && bk.exists && bk.book);
1839
2072
  var dup = (bk && bk.scan && bk.scan.candidates) ? bk.scan.candidates : [];
2073
+ // 第二类重复:卡里的条目与共享书**逐字相同**(`cc_apply_shared_book` 回填进卡的副本)。
2074
+ // 界面从前也看不见它 —— 卡片间没重复、书里也没重复,于是那几份副本静静躺着准备双重插入。
2075
+ var dupeBook = (bk && bk.scan && bk.scan.bookDupes) ? bk.scan.bookDupes : [];
1840
2076
  main=h("div",null,
1841
2077
  h("div",{style:S.readCard},
1842
2078
  h("h3",{style:S.readTitle}, t("book.title"),
@@ -1915,12 +2151,16 @@ window.__ModuleLoader__.load({
1915
2151
  h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 12px", fontSize:12}), onMouseDown:stopFocusSteal,
1916
2152
  disabled: !!(s.bookBusy==="scan"||s.bookBusy==="promote"),
1917
2153
  onClick:function(){ store.promoteBook(null, true); }}, (s.bookBusy==="promote")? "…" : t("book.promote")))))),
2154
+ dupeBook.length
2155
+ ? h("div",{style:{fontSize:11, color:"#ffc46b", marginTop:8, lineHeight:1.7}}, t("book.dupBook", {n:String(dupeBook.length)}))
2156
+ : null,
1918
2157
  s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg):null),
1919
2158
  (gidB && bookReady) ? h("div",{style:S.readCard},
1920
2159
  h("h3",{style:S.readTitle}, t("export.book.title")),
1921
2160
  h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("export.book.hint")),
1922
2161
  h("div",{style:{display:"flex", gap:8, flexWrap:"wrap"}},
1923
2162
  h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
2163
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookStFile(); }}, t("export.book.buttonSt")),
1924
2164
  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
2165
  h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.loadBook(); }}, t("book.refresh"))),
1926
2166
  s.lastMsg? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, s.lastMsg):null) : null
@@ -2006,6 +2246,7 @@ window.__ModuleLoader__.load({
2006
2246
  h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", margin:"6px 0 8px", lineHeight:1.7}}, t("export.book.hint")),
2007
2247
  h("div",{style:{display:"flex", gap:8, flexWrap:"wrap", alignItems:"center"}},
2008
2248
  h("button",{type:"button", style:S.btn, onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookFile(); }}, t("export.book.button")),
2249
+ h("button",{type:"button", style:Object.assign({}, S.btn, S.btnGhost), onMouseDown:stopFocusSteal, onClick:function(){ store.exportBookStFile(); }}, t("export.book.buttonSt")),
2009
2250
  h("label",{style:{display:"flex", alignItems:"center", gap:6, fontSize:11, color:"var(--dsw-alias-label-secondary)", cursor:"pointer"}},
2010
2251
  h("input",{type:"checkbox", checked:!!s.bookMerge, onChange:function(ev){ store.setBookMerge(ev.target.checked); }}),
2011
2252
  t("export.book.merge"))));