@xia-sc/dsh-cc-studio 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -20
- package/lib/agent.js +80 -37
- package/lib/client.js +153 -23
- package/lib/index.js +258 -64
- package/package.json +6 -3
- package/presets/cc.patch.yml +328 -0
package/lib/client.js
CHANGED
|
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
"capsule.draft":"草稿",
|
|
19
19
|
"capsule.expanded":"已展开",
|
|
20
20
|
"capsule.collapseHint":"点击展开",
|
|
21
|
+
"capsule.slotWarn":"⚠ 草稿槽不一致",
|
|
21
22
|
"preset.ccLabel":"CC 模式",
|
|
22
23
|
"panel.title":"角色卡工坊",
|
|
23
24
|
"panel.subtitle":"CCv3 · 从点子到可导入卡",
|
|
@@ -112,6 +113,11 @@ window.__ModuleLoader__.load({
|
|
|
112
113
|
"workshop.export.modificationDate":"modification_date(导出自动刷新)",
|
|
113
114
|
"workshop.draft.fresh":"⚠️ 当前会话是全新空白草稿(无历史存档,可能是 host 重启后的新会话)。在工坊或对话中继续编辑前,请确认这是预期中的新建;如需继续旧卡请去右侧“已存角色”载入。",
|
|
114
115
|
"workshop.draft.recovered":"已从磁盘自动恢复草稿(host 重启后恢复),creation_date={date}。可继续编辑,下次写入后此提示消失。",
|
|
116
|
+
"workshop.slot.mismatch":"⚠ 草稿槽不一致:本次读到的是 `{got}` 槽,而当前会话是 `{want}`。为防止把别的槽画成你的草稿,本次内容未渲染;下一次轮询会按当前会话重新读取。",
|
|
117
|
+
"workshop.slot.recover":"发现另一份已落盘草稿:{name}({n} 条 lore,槽 {key})。如果这就是本会话此前写入的内容,可一键迁入当前会话槽 —— 内容没有丢,只是被写进了另一把 key。",
|
|
118
|
+
"workshop.slot.migrateOne":"迁入 {key}({n} 条 lore)",
|
|
119
|
+
"workshop.slot.migrated":"已把槽 {key} 的内容迁入当前会话({n} 条 lore)。",
|
|
120
|
+
"workshop.slot.migrateFailed":"迁移草稿槽失败",
|
|
115
121
|
"workshop.preview.title":"实时 CCv3 预览",
|
|
116
122
|
"workshop.preview.cbsTitle":"CBS 速查",
|
|
117
123
|
"workshop.preview.cbsHint":"大小写不敏感,逗号用 \\, 转义。",
|
|
@@ -223,6 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
223
229
|
"capsule.draft":"draft",
|
|
224
230
|
"capsule.expanded":"expanded",
|
|
225
231
|
"capsule.collapseHint":"Click to expand",
|
|
232
|
+
"capsule.slotWarn":"⚠ draft slot mismatch",
|
|
226
233
|
"preset.ccLabel":"CC 模式",
|
|
227
234
|
"panel.title":"Character Card Studio",
|
|
228
235
|
"panel.subtitle":"CCv3 · Idea to Importable Card",
|
|
@@ -317,6 +324,11 @@ window.__ModuleLoader__.load({
|
|
|
317
324
|
"workshop.export.modificationDate":"modification_date (auto on export)",
|
|
318
325
|
"workshop.draft.fresh":"⚠️ This session has a fresh blank draft (no saved history, possibly a new session after host restart). Please confirm this is an intended new card before editing; to continue an old card, load it from Saved Characters on the right.",
|
|
319
326
|
"workshop.draft.recovered":"Draft auto-restored from disk (after host restart), creation_date={date}. Keep editing; this notice clears after the next write.",
|
|
327
|
+
"workshop.slot.mismatch":"⚠ Draft slot mismatch: this read came from slot `{got}` while the current session is `{want}`. To avoid painting another slot's content as your draft, nothing was rendered; the next poll re-reads for the current session.",
|
|
328
|
+
"workshop.slot.recover":"Another persisted draft was found: {name} ({n} lore entries, slot {key}). If it belongs to this session, migrate it in one click — nothing was lost, it was just written under a different key.",
|
|
329
|
+
"workshop.slot.migrateOne":"Migrate {key} ({n} lore)",
|
|
330
|
+
"workshop.slot.migrated":"Migrated slot {key} into this session ({n} lore entries).",
|
|
331
|
+
"workshop.slot.migrateFailed":"Failed to migrate the draft slot",
|
|
320
332
|
"workshop.preview.title":"Live CCv3 Preview",
|
|
321
333
|
"workshop.preview.cbsTitle":"CBS Cheatsheet",
|
|
322
334
|
"workshop.preview.cbsHint":"Case-insensitive, escape comma with \\,.",
|
|
@@ -471,7 +483,7 @@ window.__ModuleLoader__.load({
|
|
|
471
483
|
var _saved = loadSettings();
|
|
472
484
|
// 兼容旧开关:poll=false 视为 0 秒
|
|
473
485
|
if(_saved.poll===false) _saved.pollInterval = 0;
|
|
474
|
-
var state={ draft: draft, draftStatus: null, cur:0, panelOpen:false, sideExpanded:false, isCcMode:false, currentSessionId:null, world:{ timeline:"", factions:"", geo:"", power:"", daily:"" }, busy:null, valid:{valid:true,errors:[],warnings:[]}, lastMsg:null, library:[], libraryQuery:"", libraryCollapsed:false, libraryBusy:null, currentLibraryId:null, editingWorld:null, editingWorldRevert:null, editingText:null, pollEnabled:_saved.poll, pollInterval:_saved.pollInterval, autoValidate:_saved.autoValidate, autoSaveOnImport:_saved.autoSaveOnImport };
|
|
486
|
+
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, currentLibraryId:null, editingWorld:null, editingWorldRevert:null, editingText:null, pollEnabled:_saved.poll, pollInterval:_saved.pollInterval, autoValidate:_saved.autoValidate, autoSaveOnImport:_saved.autoSaveOnImport };
|
|
475
487
|
var listeners=new Set();
|
|
476
488
|
function emit(){ listeners.forEach(function(fn){ try{fn();}catch(e){console.error(e);} }); }
|
|
477
489
|
function getSnapshot(){ return state; }
|
|
@@ -484,7 +496,29 @@ window.__ModuleLoader__.load({
|
|
|
484
496
|
// 写值恒等于 isCcMode,因此渲染条件里的 !s.triggered 是冗余项(删除前后行为完全一致)。
|
|
485
497
|
function setIsCcMode(v){ var on=!!v; if(on!==state.isCcMode) set({isCcMode: on}); }
|
|
486
498
|
function setDraftFromHost(draft, status){ try{ var j=JSON.stringify(draft); var curJ=JSON.stringify(state.draft); var stJ=JSON.stringify(status||null); var curStJ=JSON.stringify(state.draftStatus||null); if(stJ!==curStJ) set({draftStatus: status||null}); if(j!==curJ){ var w=draft&&draft.data&&draft.data.extensions&&draft.data.extensions.cc_world; if(w && typeof w==="object"){ var nw={ timeline:String(w.timeline||""), factions:String(w.factions||""), geo:String(w.geo||""), power:String(w.power||""), daily:String(w.daily||"") }; var curW=state.world; if(JSON.stringify(nw)!==JSON.stringify(curW)) set({draft: draft, world: nw}); else set({draft: draft}); } else set({draft: draft}); } }catch(e){ try{ var ww=draft&&draft.data&&draft.data.extensions&&draft.data.extensions.cc_world; if(ww){ var nn={ timeline:String(ww.timeline||""), factions:String(ww.factions||""), geo:String(ww.geo||""), power:String(ww.power||""), daily:String(ww.daily||"") }; set({draft: draft, world: nn}); } else set({draft: draft}); }catch(e2){ set({draft: draft}); } } }
|
|
487
|
-
function setSessionId(id){ if(id) set({currentSessionId:
|
|
499
|
+
function setSessionId(id){ if(!id) return; var v=String(id); if(v!==state.currentSessionId) set({currentSessionId: v}); }
|
|
500
|
+
// 草稿槽不一致告警(issue #5):主机回传的 key 与我们要的不是同一把时,绝不把它当当前草稿画出来。
|
|
501
|
+
function setSlotWarn(w){ set({slotWarn: w||null}); }
|
|
502
|
+
function setAlternateSlots(list){ set({alternateSlots: (list && list.length)? list : null}); }
|
|
503
|
+
// 把另一把槽的内容迁进当前会话槽(用户点「迁入当前会话槽」时调用)
|
|
504
|
+
async function migrateSlot(from){
|
|
505
|
+
var to = state.currentSessionId || null;
|
|
506
|
+
if(!from || !to) return;
|
|
507
|
+
set({busy:"migrate"});
|
|
508
|
+
try{
|
|
509
|
+
var res=await rpc("cc_migrateDraft", {from: from, to: to, sessionId: to});
|
|
510
|
+
if(res && res.ok && res.value && res.value.draft){
|
|
511
|
+
setDraftFromHost(res.value.draft, res.value.draftStatus||null);
|
|
512
|
+
// 迁过一次就不再重复提示「盘上还有一份 default 草稿」——源槽内容不删(不做破坏性操作),
|
|
513
|
+
// 但每开一个新会话都弹一次同样的横幅只会变成噪音。
|
|
514
|
+
if(from==="default"){ try{ localStorage.setItem("dsh-cc-studio-legacy-notice","1"); }catch(e){} }
|
|
515
|
+
set({busy:null, slotWarn:null, alternateSlots:null, currentLibraryId:null,
|
|
516
|
+
lastMsg: t("workshop.slot.migrated",{key:from, n:String((res.value.moved && res.value.moved.entries)||0)})});
|
|
517
|
+
} else {
|
|
518
|
+
set({busy:null, lastMsg: t("workshop.slot.migrateFailed") + (res && res.error ? (" — "+String(res.error.message)) : "")});
|
|
519
|
+
}
|
|
520
|
+
}catch(e){ set({busy:null, lastMsg: t("workshop.slot.migrateFailed")+" — "+String(e.message)}); }
|
|
521
|
+
}
|
|
488
522
|
function saveSettings(){
|
|
489
523
|
try{ localStorage.setItem("dsh-cc-studio-settings", JSON.stringify({ poll: !!state.pollEnabled, pollInterval: Number(state.pollInterval)||0, autoValidate: !!state.autoValidate, autoSaveOnImport: !!state.autoSaveOnImport })); }catch(e){}
|
|
490
524
|
}
|
|
@@ -871,7 +905,7 @@ window.__ModuleLoader__.load({
|
|
|
871
905
|
}catch(e){ set({lastMsg: String(e && e.message || e)}); }
|
|
872
906
|
set({libraryBusy:null});
|
|
873
907
|
}
|
|
874
|
-
return { getSnapshot:getSnapshot, subscribe:subscribe, setCur:setCur, setPanel:setPanel, setSideExpanded:setSideExpanded, setIsCcMode:setIsCcMode, setDraftFromHost:setDraftFromHost, setSessionId:setSessionId, 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, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, exportLibraryEntry:exportLibraryEntry, renameCurrentInLibrary:renameCurrentInLibrary };
|
|
908
|
+
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, loadFromLibrary:loadFromLibrary, deleteFromLibrary:deleteFromLibrary, exportLibraryEntry:exportLibraryEntry, renameCurrentInLibrary:renameCurrentInLibrary };
|
|
875
909
|
}
|
|
876
910
|
|
|
877
911
|
var S={
|
|
@@ -952,27 +986,69 @@ window.__ModuleLoader__.load({
|
|
|
952
986
|
var p = presetId ? String(presetId).toLowerCase() : "";
|
|
953
987
|
if(p) return p===CC_PRESET_ID; // 会话投影是权威本地来源
|
|
954
988
|
if(domVerdict) return true; // 预设芯片显示 CC
|
|
955
|
-
|
|
989
|
+
// 连会话上下文都没有(既没有会话 id、芯片也没显示 CC):**无权下否的结论**。
|
|
990
|
+
// issue #5 的「胶囊闪一下又消失」就出在这里:shell.overlay 是根域挂载点,拿不到会话 id,
|
|
991
|
+
// 它对同一个 store 每秒 evaluate() 一次,旧代码在这里返回 false,把会话域实例刚判定为
|
|
992
|
+
// true 的 isCcMode 清掉 → 胶囊被卸载,下一次会话域实例又装回来 = 一闪一闪。
|
|
993
|
+
// 判定不出「是」时只保持现状,让真正有会话上下文的实例(或 RPC)去改。
|
|
994
|
+
if(!hasSession) return prevIsCc ? true : false;
|
|
956
995
|
// 已有会话但投影尚未到达:不确定。此时关闭会造成「选 CC → 首次对话」瞬间的胶囊闪烁,
|
|
957
996
|
// 所以只有在当前并非 CC 时才返回 null,让 RPC 去确认。
|
|
958
997
|
return prevIsCc ? true : null;
|
|
959
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* 从 slot 的 props 里取「本组件所属的会话 id」。权威顺序:
|
|
1001
|
+
* ① session 域 slot 的标准属性 `sessionId`(SessionId);
|
|
1002
|
+
* ② `session`(会话域 ownerProps 里的 SessionSnapshot,id 在 .sessionId 上);
|
|
1003
|
+
* ③ 异形兜底:props.sessionId 是对象时读它的 .sessionId。
|
|
1004
|
+
* 注意:**不要**再读 useSessions() 的 `current` 字段 —— dsh 的 SessionListState 里从来没有它
|
|
1005
|
+
* (0.1.6-alpha.2 是 ids / byId / phase;0.1.7-alpha.1 又加了 projectionsBySession,仍然没有 current;
|
|
1006
|
+
* 见 dsh-api-session-controller 的 sessions/service.d.ts)。旧代码读它恒为 null,于是「会话投影 =
|
|
1007
|
+
* 权威」这条路径从来没生效过(issue #5)。**只准读 ids / byId / phase**:这个结构的字段集已经改过
|
|
1008
|
+
* 两轮(subagentsByParent / jobsBySession → projectionsBySession),多读一个字段就是下一个静默回归。
|
|
1009
|
+
*/
|
|
1010
|
+
function ccSessionIdOfProps(props){
|
|
1011
|
+
try{
|
|
1012
|
+
if(!props) return null;
|
|
1013
|
+
var direct=props.sessionId;
|
|
1014
|
+
if(typeof direct==="string" && direct) return direct;
|
|
1015
|
+
var snap=props.session;
|
|
1016
|
+
if(snap && typeof snap.sessionId==="string" && snap.sessionId) return snap.sessionId;
|
|
1017
|
+
if(direct && typeof direct==="object" && typeof direct.sessionId==="string" && direct.sessionId) return direct.sessionId;
|
|
1018
|
+
}catch(e){}
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
960
1021
|
/**
|
|
961
1022
|
* 探测 CC 模式并写入 store,返回当前会话 id。
|
|
962
1023
|
*/
|
|
963
1024
|
function useCcPreset(store, props, t){
|
|
964
1025
|
var useSessions=props.useSessions;
|
|
965
|
-
|
|
966
|
-
|
|
1026
|
+
// ownId:本组件自己绑定的会话 id(跨渲染稳定)。shell.overlay 是根域,没有会话绑定 → null,
|
|
1027
|
+
// 这时退到 store.currentSessionId(由带会话绑定的那个实例发布,两端共享同一份映射)。
|
|
1028
|
+
var ownId = ccSessionIdOfProps(props);
|
|
1029
|
+
var currentId = ownId || store.getSnapshot().currentSessionId || null;
|
|
1030
|
+
// 会话投影:必须用上面解析出来的会话 id 去 byId 里查(旧代码用不存在的 s.current 作下标)。
|
|
967
1031
|
var presetId = useSessions ? useSessions(function(s){
|
|
968
|
-
try{
|
|
1032
|
+
try{
|
|
1033
|
+
if(!currentId) return null;
|
|
1034
|
+
return ccPresetIdOf(s && s.byId && s.byId[currentId]);
|
|
1035
|
+
}catch(e){ return null; }
|
|
969
1036
|
}) : null;
|
|
970
1037
|
var ccLabel=(typeof t==="function" ? t("preset.ccLabel") : "") || "";
|
|
971
1038
|
var evaluate=React.useCallback(function(){
|
|
972
1039
|
var verdict=decideCcMode(presetId, ccModeFromDom(ccLabel), !!currentId, !!store.getSnapshot().isCcMode);
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1040
|
+
// 没有会话上下文的实例(根域的 shell.overlay)只能把结论改成「是」,不能改成「否」:
|
|
1041
|
+
// 它无从知道当前会话是不是 CC 模式,写 false 就是 issue #5 的胶囊闪退。
|
|
1042
|
+
var canConclude = !!ownId || !!currentId;
|
|
1043
|
+
if(verdict===null) return !!store.getSnapshot().isCcMode;
|
|
1044
|
+
if(verdict || canConclude) store.setIsCcMode(verdict);
|
|
1045
|
+
return verdict || !!store.getSnapshot().isCcMode;
|
|
1046
|
+
}, [store, presetId, currentId, ownId, ccLabel]);
|
|
1047
|
+
// 把「本组件所属会话」发布到 store:这是全局唯一一份「当前会话 → 草稿槽」的映射,
|
|
1048
|
+
// 草稿轮询、根域实例、以及所有 cc_* 调用都读它(issue #5 的两槽并存就是它一直是 null 造成的)。
|
|
1049
|
+
React.useEffect(function(){
|
|
1050
|
+
if(ownId) store.setSessionId(ownId);
|
|
1051
|
+
}, [store, ownId]);
|
|
976
1052
|
React.useEffect(function(){
|
|
977
1053
|
var isCc=evaluate();
|
|
978
1054
|
if(!currentId) return;
|
|
@@ -1006,9 +1082,11 @@ window.__ModuleLoader__.load({
|
|
|
1006
1082
|
if(!s.isCcMode && !s.panelOpen) return null;
|
|
1007
1083
|
var draft=s.draft; var loreCount=draft.data.character_book.entries.length;
|
|
1008
1084
|
var valid=s.valid && s.valid.valid;
|
|
1009
|
-
//
|
|
1085
|
+
// 计数文案走词表。注:dsh 的 t(key, params?) **是支持 `{name}` 插值的**(0.1.6 / 0.1.7 都支持),
|
|
1086
|
+
// 这里手写 replace 只因为这些键历史上就不带参数;新键请直接用 t(key, {n: ...}),
|
|
1087
|
+
// 千万别对同一个键既传 params 又手写 replace(会替换两次)。
|
|
1010
1088
|
var loreText=t("common.loreCount").replace("{n}", String(loreCount));
|
|
1011
|
-
var text=(t("capsule.open")+" · "+loreText+" · "+ (valid? t("capsule.exported"): t("capsule.draft")));
|
|
1089
|
+
var text=(t("capsule.open")+" · "+loreText+" · "+ (valid? t("capsule.exported"): t("capsule.draft")) + (s.slotWarn? (" · "+t("capsule.slotWarn")): ""));
|
|
1012
1090
|
return h("div",{style:S.dockWrap, "data-dsh-cc-studio":"dock-row"},
|
|
1013
1091
|
h("button",{type:"button", style:S.capsule, title: t("hint.capsule"), onClick:function(){ store.setPanel(true); }},
|
|
1014
1092
|
h("span",{style:{width:7,height:7,borderRadius:50, background: valid? "#2ee6a6":"#7c5cff", boxShadow:"0 0 8px "+(valid?"#2ee6a6":"#7c5cff"), display:"inline-block", flex:"none"}}),
|
|
@@ -1294,6 +1372,24 @@ window.__ModuleLoader__.load({
|
|
|
1294
1372
|
),
|
|
1295
1373
|
(function(){
|
|
1296
1374
|
var st = s.draftStatus;
|
|
1375
|
+
// 草稿槽不一致 / 发现别的槽有内容时的提示(issue #5):把「不一致」变成看得见的东西,
|
|
1376
|
+
// 并给一个真的能把内容捞回来的按钮 —— 用户看到空壳时的第一反应是「草稿丢了」。
|
|
1377
|
+
if(s.slotWarn){
|
|
1378
|
+
return h("div",{style:{margin:"10px 16px 0", padding:"8px 12px", borderRadius:10, fontSize:12, lineHeight:1.5, background:"rgba(232,120,120,.12)", border:"1px solid rgba(232,120,120,.5)", color:"var(--dsw-alias-label-primary)", flex:"none"}},
|
|
1379
|
+
t("workshop.slot.mismatch",{got:String(s.slotWarn.got||""), want:String(s.slotWarn.requested||"")}));
|
|
1380
|
+
}
|
|
1381
|
+
if(s.alternateSlots && s.alternateSlots.length){
|
|
1382
|
+
return h("div",{style:{margin:"10px 16px 0", padding:"8px 12px", borderRadius:10, fontSize:12, lineHeight:1.5, background:"rgba(255,170,60,.10)", border:"1px solid rgba(255,170,60,.45)", color:"var(--dsw-alias-label-primary)", flex:"none"}},
|
|
1383
|
+
h("div",null, t("workshop.slot.recover",{name:String(s.alternateSlots[0].name||"—"), n:String(s.alternateSlots[0].entries||0), key:String(s.alternateSlots[0].key||"")})),
|
|
1384
|
+
h("div",{style:{marginTop:8, display:"flex", gap:8, flexWrap:"wrap"}},
|
|
1385
|
+
s.alternateSlots.map(function(slot){
|
|
1386
|
+
return h("button",{key:String(slot.key), style:Object.assign({}, S.btn, {padding:"4px 10px", fontSize:11}),
|
|
1387
|
+
disabled: s.busy==="migrate",
|
|
1388
|
+
onClick:function(){ store.migrateSlot(String(slot.key)); }},
|
|
1389
|
+
t("workshop.slot.migrateOne",{key:String(slot.key), n:String(slot.entries||0)}));
|
|
1390
|
+
})
|
|
1391
|
+
));
|
|
1392
|
+
}
|
|
1297
1393
|
if(!st) return null;
|
|
1298
1394
|
if(st.isNew) return h("div",{style:{margin:"10px 16px 0", padding:"8px 12px", borderRadius:10, fontSize:12, lineHeight:1.5, background:"rgba(255,170,60,.10)", border:"1px solid rgba(255,170,60,.45)", color:"var(--dsw-alias-label-primary)", flex:"none"}}, t("workshop.draft.fresh"));
|
|
1299
1395
|
if(st.recovered){
|
|
@@ -1441,23 +1537,49 @@ window.__ModuleLoader__.load({
|
|
|
1441
1537
|
function apply(ctx){
|
|
1442
1538
|
ctx.effect(function(){ return ctx.locale.register(NS, { zh:zh, en:en }); }, "cc-studio:locale");
|
|
1443
1539
|
var store=createStore(ctx);
|
|
1444
|
-
// CC 模式检测 + 草稿同步:会话切换触发 + CC
|
|
1540
|
+
// CC 模式检测 + 草稿同步:会话切换触发 + CC 模式下按设置间隔轻量轮询。
|
|
1541
|
+
// 铁律:拉草稿必须带会话 id(见 pullDraft 注释,issue #5),绝不拿 default 槽顶替。
|
|
1445
1542
|
ctx.effect(function(){
|
|
1446
1543
|
var lastHostJson="";
|
|
1447
1544
|
var lastHostStatusJson="";
|
|
1545
|
+
var lastHostKey=null;
|
|
1546
|
+
// 拉一份草稿。**必须带 key**:没有会话 id 就什么都不拉。
|
|
1547
|
+
// 旧代码在 args={} 时照样发请求,主机在 HTTP 上下文里拿不到 currentInitiator(),
|
|
1548
|
+
// draftKeyFrom 回退成 "default" —— 于是「模型写 session-<id>、工坊画 default」两槽并存,
|
|
1549
|
+
// 用户看到空壳以为草稿丢了(issue #5)。宁可什么都不画,也不画错槽。
|
|
1448
1550
|
function pullDraft(sessionId){
|
|
1449
|
-
var
|
|
1450
|
-
if(
|
|
1451
|
-
|
|
1551
|
+
var key = (typeof sessionId==="string" && sessionId) ? sessionId : null;
|
|
1552
|
+
if(!key) return Promise.resolve();
|
|
1553
|
+
store.setSessionId(key);
|
|
1554
|
+
return ctx.connection.rpc.call("/dsh-cc-studio-rpc","cc_getDraft",{args:{sessionId:key}}).then(function(res){
|
|
1452
1555
|
if(!res || !res.ok || !res.value || !res.value.draft) return;
|
|
1556
|
+
var gotKey = (typeof res.value.key==="string" && res.value.key) ? res.value.key : null;
|
|
1557
|
+
// 期间用户切了会话:这次响应已经过期,丢掉(否则会把上一个会话的草稿画进新会话)
|
|
1558
|
+
var cur = store.getSnapshot().currentSessionId;
|
|
1559
|
+
if(cur && cur !== key) return;
|
|
1560
|
+
// 主机回的槽和我们点名的槽不是同一把:**不渲染**,只把不一致摆到台面上。
|
|
1561
|
+
// 正常情况下不该发生(我们总是显式传 sessionId);留着是为了让这类错位几秒内可见。
|
|
1562
|
+
if(gotKey && gotKey !== key){
|
|
1563
|
+
store.setSlotWarn({ requested:key, got:gotKey, source: res.value.keySource||null });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
store.setSlotWarn(null);
|
|
1567
|
+
var alts = res.value.alternateSlots || null;
|
|
1568
|
+
// 只对「老版本遗留的 default 槽」做一次性提示:迁过一次就别再每开一个新会话弹一遍
|
|
1569
|
+
if(alts && alts.length===1 && alts[0].key==="default"){
|
|
1570
|
+
try{ if(localStorage.getItem("dsh-cc-studio-legacy-notice")==="1") alts = null; }catch(e){}
|
|
1571
|
+
}
|
|
1572
|
+
store.setAlternateSlots(alts);
|
|
1453
1573
|
var draft=res.value.draft;
|
|
1454
1574
|
var draftStatus=res.value.draftStatus||null;
|
|
1455
1575
|
try{
|
|
1456
1576
|
var j=JSON.stringify(draft);
|
|
1457
1577
|
var stJ=JSON.stringify(draftStatus);
|
|
1458
|
-
|
|
1578
|
+
// 换 key 时缓存必须失效,否则「内容恰好相同」的两把槽之间会互相吞掉更新
|
|
1579
|
+
if(j!==lastHostJson || stJ!==lastHostStatusJson || gotKey!==lastHostKey){
|
|
1459
1580
|
lastHostJson=j;
|
|
1460
1581
|
lastHostStatusJson=stJ;
|
|
1582
|
+
lastHostKey=gotKey;
|
|
1461
1583
|
var curJ=JSON.stringify(store.getSnapshot().draft);
|
|
1462
1584
|
var curStJ=JSON.stringify(store.getSnapshot().draftStatus||null);
|
|
1463
1585
|
if(j!==curJ || stJ!==curStJ) store.setDraftFromHost(draft, draftStatus);
|
|
@@ -1466,25 +1588,33 @@ window.__ModuleLoader__.load({
|
|
|
1466
1588
|
}).catch(function(){});
|
|
1467
1589
|
}
|
|
1468
1590
|
var lastPullAt = 0;
|
|
1591
|
+
var lastPullKey = null;
|
|
1469
1592
|
function pullDraftThrottled(sid){
|
|
1593
|
+
var key = (typeof sid==="string" && sid) ? sid : null;
|
|
1594
|
+
if(!key) return; // 没有会话 id 就不拉(见 pullDraft 注释)
|
|
1595
|
+
// 只节流「同一把 key」的重复请求:旧代码一把全局时间戳会在启动时把
|
|
1596
|
+
// 「会话就绪后的第一次正式拉取」直接吞掉(apply 里的初始 pullDraft(null) 先占了坑),
|
|
1597
|
+
// 于是 store.currentSessionId 永远没人写 → 轮询永远读 default 槽。
|
|
1470
1598
|
var now = Date.now();
|
|
1471
|
-
if(now - lastPullAt < 500) return;
|
|
1599
|
+
if(key === lastPullKey && now - lastPullAt < 500) return;
|
|
1472
1600
|
lastPullAt = now;
|
|
1473
|
-
|
|
1601
|
+
lastPullKey = key;
|
|
1602
|
+
pullDraft(key);
|
|
1474
1603
|
}
|
|
1475
|
-
// 初始拉一次(default)
|
|
1476
|
-
pullDraftThrottled(null);
|
|
1477
1604
|
// 暴露给 Capsule/Workshop 按会话触发(带节流)
|
|
1478
1605
|
store._pullDraft = pullDraftThrottled;
|
|
1606
|
+
// 注意:这里**不再**做「初始拉一次 default」。启动时还没有会话,拉了只会把 default 槽
|
|
1607
|
+
// 的空壳画进工坊;store 里本来就有一份本地空白草稿,够新会话页用了。
|
|
1479
1608
|
// CC 模式下轻量轮询,保证 LLM 的 cc_* Tool 写入后胶囊实时同步(切换会话时会自动更新 currentSessionId)
|
|
1480
|
-
// 仅 CC 模式下以 4s
|
|
1609
|
+
// 仅 CC 模式下以 4s 心跳拉取,标准模式/未触发时不请求
|
|
1481
1610
|
var timer = setInterval(function(){
|
|
1482
1611
|
var s = store.getSnapshot();
|
|
1483
1612
|
var intervalSec = Number(s.pollInterval)||0;
|
|
1484
1613
|
if(!s.isCcMode || intervalSec===0) return;
|
|
1614
|
+
var sid = s.currentSessionId || null;
|
|
1615
|
+
if(!sid) return; // 会话 id 未知:不拿 default 顶替
|
|
1485
1616
|
var elapsed = Date.now() - lastPullAt;
|
|
1486
1617
|
if(elapsed < intervalSec*1000) return;
|
|
1487
|
-
var sid = s.currentSessionId || null;
|
|
1488
1618
|
pullDraftThrottled(sid);
|
|
1489
1619
|
}, 1000);
|
|
1490
1620
|
return function(){ clearInterval(timer); };
|