@xia-sc/dsh-cc-studio 0.7.1 → 0.8.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
@@ -132,6 +132,51 @@ window.__ModuleLoader__.load({
132
132
  "workshop.library.save":"★ 保存当前",
133
133
  "workshop.library.saveAs":"+ 另存为新",
134
134
  "workshop.library.rename":"✎ 重命名",
135
+ // —— 0.8.0 人物关系图谱 / 群聊分组 ——
136
+ "workshop.group.all":"全部",
137
+ "workshop.group.none":"未分组",
138
+ "workshop.group.new":"+ 组",
139
+ "workshop.group.summary":"{n} 卡 / {m} 关系",
140
+ "workshop.group.needRestart":"宿主半还是旧代码(这些按钮要重启 dsh 才生效):请退出并重新打开桌面应用 / 重启 dsh web,然后刷新页面",
141
+ "workshop.group.prompt":"新分组名称(同一世界观的角色放一组)",
142
+ "workshop.group.assign":"组",
143
+ "workshop.group.set":"已归组",
144
+ "workshop.rel.title":"人物关系",
145
+ "workshop.rel.open":"关系",
146
+ "workshop.rel.pickGroup":"先在左侧选一个具体分组(「全部 / 未分组」下没有可编辑的关系)。",
147
+ "workshop.rel.noGroup":"未选分组",
148
+ "workshop.rel.members":"成员卡",
149
+ "workshop.rel.noMember":"(还没有卡入组 —— 用卡片行上的「组」按钮把同一世界观的卡拉进来)",
150
+ "workshop.rel.edges":"关系",
151
+ "workshop.rel.noEdge":"(还没有关系:用下面的表单加一条,或让模型用 cc_patch_relations 批量建)",
152
+ "workshop.rel.add":"添加关系",
153
+ "workshop.rel.addNode":"+ NPC",
154
+ "workshop.rel.nodePrompt":"NPC 名字(没有独立角色卡的人物,例如「柳长老」)",
155
+ "workshop.rel.from":"从",
156
+ "workshop.rel.to":"到",
157
+ "workshop.rel.strength":"强度",
158
+ "workshop.rel.direction":"方向",
159
+ "workshop.rel.dirMutual":"对等",
160
+ "workshop.rel.dirOneWay":"单向",
161
+ "workshop.rel.note":"备注",
162
+ "workshop.rel.check":"体检",
163
+ "workshop.rel.render":"渲染到当前卡",
164
+ "workshop.rel.rendered":"已把 {n} 条关系写进当前卡(记得「保存当前」才会落库)",
165
+ "workshop.rel.checkResult":"体检:{n} 个问题",
166
+ "workshop.rel.checkOk":"体检通过:没有结构性问题",
167
+ "workshop.rel.saved":"关系已更新(rev {rev})",
168
+ "workshop.rel.needLoad":"先在左侧「载入」一张卡:渲染要知道写进哪张卡",
169
+ "workshop.rel.hint":"关系只存在插件侧,不写进 CCv3 字段;「渲染到当前卡」会把它们写进 description 的【关系】段,让分享出去的卡自包含。",
170
+ "rel.type.family":"亲属",
171
+ "rel.type.romance":"恋慕",
172
+ "rel.type.mentor":"师徒",
173
+ "rel.type.friend":"挚友",
174
+ "rel.type.colleague":"同门",
175
+ "rel.type.rival":"竞争",
176
+ "rel.type.enemy":"敌对",
177
+ "rel.type.debt":"恩义",
178
+ "rel.type.superior":"上下级",
179
+ "rel.type.other":"其他",
135
180
  "workshop.library.new":"+ 新建",
136
181
  "workshop.library.searchPlaceholder":"搜索 名称/标签…",
137
182
  "workshop.library.empty":"还没有已存角色\n去“世界观/角色细化”填好后点“保存当前”",
@@ -345,6 +390,51 @@ window.__ModuleLoader__.load({
345
390
  "workshop.library.save":"★ Save Current",
346
391
  "workshop.library.saveAs":"+ Save as New",
347
392
  "workshop.library.rename":"✎ Rename",
393
+ // —— 0.8.0 relationship graph / group chat ——
394
+ "workshop.group.all":"All",
395
+ "workshop.group.none":"Ungrouped",
396
+ "workshop.group.new":"+ Group",
397
+ "workshop.group.summary":"{n} cards / {m} relations",
398
+ "workshop.group.needRestart":"The host half is still running old code (these buttons need a dsh restart): quit and reopen the desktop app (or restart dsh web), then reload the page.",
399
+ "workshop.group.prompt":"New group name (characters of one world go together)",
400
+ "workshop.group.assign":"Group",
401
+ "workshop.group.set":"Grouped",
402
+ "workshop.rel.title":"Relationships",
403
+ "workshop.rel.open":"Relations",
404
+ "workshop.rel.pickGroup":"Pick a specific group on the left (no editable relations under All / Ungrouped).",
405
+ "workshop.rel.noGroup":"No group",
406
+ "workshop.rel.members":"Member cards",
407
+ "workshop.rel.noMember":"(no cards yet — use the “Group” button on a card row to pull in cards of the same world)",
408
+ "workshop.rel.edges":"Relations",
409
+ "workshop.rel.noEdge":"(none yet: add one with the form below, or let the model build them with cc_patch_relations)",
410
+ "workshop.rel.add":"Add relation",
411
+ "workshop.rel.addNode":"+ NPC",
412
+ "workshop.rel.nodePrompt":"NPC name (someone without their own card, e.g. an elder)",
413
+ "workshop.rel.from":"From",
414
+ "workshop.rel.to":"To",
415
+ "workshop.rel.strength":"Strength",
416
+ "workshop.rel.direction":"Direction",
417
+ "workshop.rel.dirMutual":"Mutual",
418
+ "workshop.rel.dirOneWay":"One-way",
419
+ "workshop.rel.note":"Note",
420
+ "workshop.rel.check":"Check",
421
+ "workshop.rel.render":"Render into current card",
422
+ "workshop.rel.rendered":"Wrote {n} relations into the current card (remember to “Save current”)",
423
+ "workshop.rel.checkResult":"Check: {n} issue(s)",
424
+ "workshop.rel.checkOk":"Check passed: no structural issues",
425
+ "workshop.rel.saved":"Relations updated (rev {rev})",
426
+ "workshop.rel.needLoad":"Load a card on the left first: rendering needs to know which card to write into",
427
+ "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.",
428
+ "rel.type.family":"Family",
429
+ "rel.type.romance":"Romance",
430
+ "rel.type.mentor":"Mentor",
431
+ "rel.type.friend":"Close friend",
432
+ "rel.type.colleague":"Colleague",
433
+ "rel.type.rival":"Rival",
434
+ "rel.type.enemy":"Enemy",
435
+ "rel.type.debt":"Obligation",
436
+ "rel.type.superior":"Superior",
437
+ "rel.type.other":"Other",
348
438
  "workshop.library.new":"+ New",
349
439
  "workshop.library.searchPlaceholder":"Search name/tags…",
350
440
  "workshop.library.empty":"No saved characters\nFill World/Character then ★ Save Current",
@@ -487,7 +577,7 @@ window.__ModuleLoader__.load({
487
577
  var _saved = loadSettings();
488
578
  // 兼容旧开关:poll=false 视为 0 秒
489
579
  if(_saved.poll===false) _saved.pollInterval = 0;
490
- 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 };
580
+ var state={ draft: draft, draftStatus: null, cur:0, panelOpen:false, sideExpanded:false, isCcMode:false, currentSessionId:null, slotWarn:null, alternateSlots:null, world:{ timeline:"", factions:"", geo:"", power:"", daily:"" }, busy:null, valid:{valid:true,errors:[],warnings:[]}, lastMsg:null, library:[], libraryQuery:"", libraryCollapsed:false, libraryBusy:null, groups:[], currentGroupId:"", relations:null, relBusy:null, currentLibraryId:null, editingWorld:null, editingWorldRevert:null, editingText:null, pollEnabled:_saved.poll, pollInterval:_saved.pollInterval, autoValidate:_saved.autoValidate, autoSaveOnImport:_saved.autoSaveOnImport };
491
581
  var listeners=new Set();
492
582
  function emit(){ listeners.forEach(function(fn){ try{fn();}catch(e){console.error(e);} }); }
493
583
  function getSnapshot(){ return state; }
@@ -791,6 +881,134 @@ window.__ModuleLoader__.load({
791
881
  });
792
882
  }
793
883
  function setLibraryQuery(v){ set({libraryQuery: String(v).slice(0,40)}); }
884
+ // ———————————————— 0.8.0 人物关系图谱 / 群聊分组 ————————————————
885
+ // 关系数据活在宿主侧(<DSH_HOME>/cc-groups/),前端只通过 RPC 读写;「卡属于哪个组」由库条目
886
+ // 的 meta.groupId 决定(分组文件里不存成员),所以这里每次入组/移组后都要 refreshLibrary。
887
+ function setCurrentGroupId(id){
888
+ var gid = String(id || "");
889
+ set({currentGroupId: gid});
890
+ // "" = 全部(不过滤)、"__none" = 只看未分组:这两个都不是真分组,没有图谱可载
891
+ if(gid && gid !== "__none") loadGroup(gid); else set({relations:null});
892
+ }
893
+ async function refreshGroups(){
894
+ try{
895
+ var res = await rpc("cc_listGroups", {});
896
+ if(res && res.ok && res.value){ set({groups: res.value.groups||[]}); return; }
897
+ // 宿主半还是旧代码时这里会拿到 unknown-endpoint —— 那是「只刷新了页面、没重启 dsh」的典型信号,
898
+ // 直接说人话,别让用户对着一堆没反应的按钮猜(0.8.1)。
899
+ var code = res && res.error && res.error.details && res.error.details.code;
900
+ if(code === "unknown-endpoint"){ set({lastMsg: t("workshop.group.needRestart")}); return; }
901
+ set({lastMsg: String((res && res.error && res.error.message) || "")});
902
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
903
+ }
904
+ /** 0.8.1:从侧栏进关系视图。未选具体分组时**给一句提示**,而不是把按钮静默禁用(用户会以为坏了)。 */
905
+ function openRelations(){
906
+ var gid = String(state.currentGroupId || "");
907
+ if(!gid || gid === "__none"){ set({lastMsg: t("workshop.rel.pickGroup")}); return; }
908
+ set({cur: 4});
909
+ loadGroup(gid);
910
+ }
911
+ async function loadGroup(id){
912
+ var gid = String(id || state.currentGroupId || "");
913
+ if(!gid){ set({relations:null}); return; }
914
+ set({relBusy:"load"});
915
+ try{
916
+ var res = await rpc("cc_getGroup", {id: gid});
917
+ if(res && res.ok && res.value) set({relations: res.value});
918
+ else set({relations:null, lastMsg:String((res && res.error && res.error.message) || "")});
919
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
920
+ set({relBusy:null});
921
+ }
922
+ async function createGroup(nameArg){
923
+ // 0.8.1:不再依赖 window.prompt(内嵌 webview 会拦掉原生对话框 → 用户看到的是「点了没反应」)。
924
+ // 名字由侧栏那个内联输入框传进来;没传就当取消。
925
+ var name = String(nameArg == null ? "" : nameArg).trim();
926
+ if(!name) return;
927
+ set({relBusy:"new"});
928
+ try{
929
+ var res = await rpc("cc_createGroup", {name: name});
930
+ if(res && res.ok && res.value){
931
+ await refreshGroups();
932
+ set({currentGroupId: res.value.group.id});
933
+ await loadGroup(res.value.group.id);
934
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
935
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
936
+ set({relBusy:null});
937
+ }
938
+ async function deleteGroup(id){
939
+ try{ if(typeof window!=="undefined" && window.confirm && !window.confirm(t("store.confirmDelete"))) return; }catch(e){}
940
+ set({relBusy:"del"});
941
+ try{
942
+ var res = await rpc("cc_deleteGroup", {id: id});
943
+ if(res && res.ok){ set({currentGroupId:"", relations:null}); await refreshGroups(); await refreshLibrary(); }
944
+ else set({lastMsg:String((res && res.error && res.error.message) || "")});
945
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
946
+ set({relBusy:null});
947
+ }
948
+ async function assignGroup(cardId, groupId){
949
+ if(!cardId) return;
950
+ set({libraryBusy:"group:"+cardId});
951
+ try{
952
+ var res = await rpc("cc_setCardGroup", {cardId: cardId, groupId: groupId || null});
953
+ if(res && res.ok){
954
+ await refreshLibrary();
955
+ await refreshGroups();
956
+ if(state.currentGroupId) await loadGroup(state.currentGroupId);
957
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
958
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
959
+ set({libraryBusy:null});
960
+ }
961
+ /** 改图谱:ops 与 cc_patch_relations 同构(addNode/updateNode/removeNode/addEdge/updateEdge/removeEdge)。 */
962
+ async function patchRelations(ops){
963
+ var gid = state.currentGroupId;
964
+ if(!gid){ set({lastMsg:t("workshop.rel.pickGroup")}); return; }
965
+ set({relBusy:"patch"});
966
+ try{
967
+ var args = { id: gid, ops: ops };
968
+ var rev = state.relations && state.relations.group ? state.relations.group.rev : null;
969
+ if(typeof rev === "number") args.expectedRev = rev;
970
+ var res = await rpc("cc_patchGroup", args);
971
+ if(res && res.ok && res.value){
972
+ set({lastMsg: t("workshop.rel.saved", {rev: String(res.value.group.rev)})});
973
+ await loadGroup(gid);
974
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
975
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
976
+ set({relBusy:null});
977
+ }
978
+ async function checkGroupNow(){
979
+ var gid = state.currentGroupId;
980
+ if(!gid){ set({lastMsg:t("workshop.rel.pickGroup")}); return; }
981
+ set({relBusy:"check"});
982
+ try{
983
+ var res = await rpc("cc_groupCheck", {id: gid});
984
+ if(res && res.ok && res.value){
985
+ var issues = res.value.issues || [];
986
+ set({lastMsg: issues.length
987
+ ? t("workshop.rel.checkResult", {n: String(issues.length)}) + " — " + issues.map(function(i){ return i.message; }).join(";")
988
+ : t("workshop.rel.checkOk")});
989
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
990
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
991
+ set({relBusy:null});
992
+ }
993
+ /** 把图谱里「我对别人的看法」渲染进当前卡的 description(固定标记区,重复渲染是替换)。 */
994
+ async function renderRelationsToCard(){
995
+ if(!state.currentLibraryId){ set({lastMsg:t("workshop.rel.needLoad")}); return; }
996
+ set({relBusy:"render"});
997
+ try{
998
+ var sid = state.currentSessionId || null;
999
+ var args = { cardId: state.currentLibraryId };
1000
+ if(state.currentGroupId) args.groupId = state.currentGroupId;
1001
+ if(sid) args.sessionId = sid;
1002
+ var res = await rpc("cc_renderRelations", args);
1003
+ if(res && res.ok && res.value){
1004
+ // 宿主改了草稿文件 → 重新拉一次,否则工坊里看到的还是旧 description
1005
+ var dres = await rpc("cc_getDraft", sid ? {sessionId: sid} : {});
1006
+ if(dres && dres.ok && dres.value && dres.value.draft) setDraftFromHost(dres.value.draft, dres.value.draftStatus || null);
1007
+ set({lastMsg: t("workshop.rel.rendered", {n: String(res.value.outgoing || 0)})});
1008
+ } else set({lastMsg:String((res && res.error && res.error.message) || "")});
1009
+ }catch(e){ set({lastMsg:String(e && e.message || e)}); }
1010
+ set({relBusy:null});
1011
+ }
794
1012
  function toggleLibraryCollapsed(){ set({libraryCollapsed: !state.libraryCollapsed}); }
795
1013
  async function refreshLibrary(){
796
1014
  set({libraryBusy:"refresh"});
@@ -919,7 +1137,7 @@ window.__ModuleLoader__.load({
919
1137
  // 0.4.1:这里原本还有一个 exportLibraryEntry —— 直接下载某张已存角色的 JSON。
920
1138
  // 三个导出入口归一之后,卡片上的「导出」改成切到导出页,导出动作只剩导出页那一套实现,
921
1139
  // 这条旁路就没有调用者了(留着它等于「同一次导出有两套行为」),故删除。
922
- 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 };
1140
+ 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 };
923
1141
  }
924
1142
 
925
1143
  var S={
@@ -1158,9 +1376,14 @@ window.__ModuleLoader__.load({
1158
1376
  // 从「已存角色」卡片跳进导出页时的来源卡(几秒后自动清掉):用来给那枚按钮一个即时反馈,
1159
1377
  // 并在导出页把「导出的仍是工坊里这张、你点的那张还没载入」说清楚。清空后回到常态提示。
1160
1378
  var jumpRef=React.useState(null); var jumpFrom=jumpRef[0], setJumpFrom=jumpRef[1];
1379
+ // 0.8.0 关系视图的「添加关系」表单(局部状态,不进 store:它只是草稿输入)
1380
+ var relFormRef=React.useState({from:"", to:"", type:"friend", strength:3, direction:"one-way", note:""}); var relForm=relFormRef[0], setRelForm=relFormRef[1];
1381
+ // 0.8.1:分组名 / NPC 名改用内联输入(不再用 window.prompt —— 内嵌 webview 会拦掉它)
1382
+ var groupDraftRef=React.useState(null); var groupDraft=groupDraftRef[0], setGroupDraft=groupDraftRef[1];
1383
+ var npcDraftRef=React.useState(""); var npcDraft=npcDraftRef[0], setNpcDraft=npcDraftRef[1];
1161
1384
  React.useEffect(function(){ return store.subscribe(function(){ setS(store.getSnapshot()); }); }, [store]);
1162
1385
  React.useEffect(function(){
1163
- if(s.panelOpen) store.refreshLibrary();
1386
+ if(s.panelOpen){ store.refreshLibrary(); store.refreshGroups(); }
1164
1387
  }, [s.panelOpen]);
1165
1388
  if(!s.panelOpen) return null;
1166
1389
  // 顶部胶囊与左侧导航都只有写作三步:导出不再是「第 4 步」(0.4.1 归一)——
@@ -1347,7 +1570,7 @@ window.__ModuleLoader__.load({
1347
1570
  s.lastMsg? h("div",{style:{marginTop:8, fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, s.lastMsg):null
1348
1571
  )
1349
1572
  );
1350
- } else {
1573
+ } else if(s.cur===3){
1351
1574
  var v=s.valid;
1352
1575
  main=h("div",null,
1353
1576
  h("div",{style:S.readCard},
@@ -1385,6 +1608,85 @@ window.__ModuleLoader__.load({
1385
1608
  s.lastMsg? h("div",{style:{marginTop:8, fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, s.lastMsg):null
1386
1609
  )
1387
1610
  );
1611
+ } else {
1612
+ // ── 关系视图(s.cur === 4)──
1613
+ // 与导出页同属「不在左侧导航里的视图」:入口是右侧分组选择器旁的「关系」按钮。
1614
+ // 这里编辑的是**插件侧图谱**(<DSH_HOME>/cc-groups/);只有「渲染到当前卡」才会把关系写进 description。
1615
+ var relNodes = (s.relations && s.relations.nodes) || [];
1616
+ var relEdges = (s.relations && s.relations.edges) || [];
1617
+ var relMembers = (s.relations && s.relations.members) || [];
1618
+ var relGroup = s.relations && s.relations.group;
1619
+ var gid = String(s.currentGroupId || "");
1620
+ var canEdit = !!gid && gid !== "__none";
1621
+ var relTypes = ["family","romance","mentor","friend","colleague","rival","enemy","debt","superior","other"];
1622
+ var nodeName = function(id){ var hit = relNodes.filter(function(x){ return x.id === id; })[0]; return hit ? hit.name : id; };
1623
+ var setF = function(patch){ setRelForm(Object.assign({}, relForm, patch)); };
1624
+ var smallSel = {fontSize:11, background:"transparent", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:6, color:"var(--dsw-alias-label-primary)", padding:"1px 4px"};
1625
+ main = h("div",null,
1626
+ h("div",{style:S.readCard},
1627
+ h("h3",{style:S.readTitle}, t("workshop.rel.title"),
1628
+ h("span",{style:{marginLeft:8, fontSize:12, fontWeight:400, color:"var(--dsw-alias-label-secondary)"}}, "· " + ((relGroup && relGroup.name) || t("workshop.rel.noGroup"))),
1629
+ relGroup ? h("span",{style:Object.assign({}, S.badge, {marginLeft:8})}, "rev " + relGroup.rev) : null),
1630
+ h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10, lineHeight:1.7}}, t("workshop.rel.hint")),
1631
+ !canEdit ? h("div",{style:{fontSize:12, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.pickGroup")) : h("div",null,
1632
+ h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}}, t("workshop.rel.members") + " · " + relMembers.length),
1633
+ h("div",{style:{display:"flex", flexWrap:"wrap", gap:6, marginBottom:14, alignItems:"center"}},
1634
+ relMembers.map(function(m){ return h("span",{key:m.id, style:S.badge}, m.name); }),
1635
+ relMembers.length===0 ? h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.noMember")) : null),
1636
+ h("div",{style:{display:"flex", flexWrap:"wrap", gap:6, marginBottom:14, alignItems:"center"}},
1637
+ relNodes.filter(function(n){ return n.kind === "npc"; }).map(function(n){ return h("span",{key:n.id, style:S.badge}, n.name); }),
1638
+ h("input",{placeholder:t("workshop.rel.nodePrompt"), value:npcDraft, style:{width:150, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"2px 6px", fontSize:11},
1639
+ onChange:function(ev){ setNpcDraft(ev.target.value); },
1640
+ onKeyDown:function(ev){ var v=String(npcDraft||"").trim(); if(ev.key==="Enter" && v){ store.patchRelations([{op:"addNode", kind:"npc", name:v}]); setNpcDraft(""); } }}),
1641
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"2px 8px", fontSize:11}), onClick:function(){
1642
+ var name = String(npcDraft||"").trim();
1643
+ if(name){ store.patchRelations([{op:"addNode", kind:"npc", name:name}]); setNpcDraft(""); }
1644
+ }}, t("workshop.rel.addNode"))),
1645
+ h("div",{style:{fontSize:12, fontWeight:600, marginBottom:4}}, t("workshop.rel.edges") + " · " + relEdges.length),
1646
+ relEdges.length===0 ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginBottom:10}}, t("workshop.rel.noEdge")) :
1647
+ h("div",null, relEdges.map(function(e){
1648
+ return h("div",{key:e.id, style:{borderBottom:"1px solid var(--dsw-alias-border-l1)", padding:"6px 0"}},
1649
+ h("div",{style:{display:"flex", alignItems:"center", gap:6, flexWrap:"wrap"}},
1650
+ h("span",{style:{fontSize:12}}, nodeName(e.from)),
1651
+ h("span",{style:{color:"#7c5cff"}}, e.direction === "mutual" ? "⇄" : "→"),
1652
+ h("span",{style:{fontSize:12}}, nodeName(e.to)),
1653
+ h("select",{style:smallSel, value:e.type, onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, type:ev.target.value}]); }},
1654
+ relTypes.map(function(ty){ return h("option",{key:ty, value:ty}, t("rel.type."+ty)); })),
1655
+ h("select",{style:smallSel, value:String(e.strength), onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, strength:Number(ev.target.value)}]); }},
1656
+ [1,2,3,4,5].map(function(n){ return h("option",{key:n, value:String(n)}, n + "/5"); })),
1657
+ h("select",{style:smallSel, value:e.direction, onChange:function(ev){ store.patchRelations([{op:"updateEdge", edgeId:e.id, direction:ev.target.value}]); }},
1658
+ h("option",{value:"one-way"}, t("workshop.rel.dirOneWay")),
1659
+ h("option",{value:"mutual"}, t("workshop.rel.dirMutual"))),
1660
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:6, padding:"1px 6px", fontSize:11}), onClick:function(){ store.patchRelations([{op:"removeEdge", edgeId:e.id}]); }}, "✕")),
1661
+ e.note ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2}}, e.note) : null);
1662
+ })),
1663
+ h("div",{style:{marginTop:14, paddingTop:10, borderTop:"1px dashed var(--dsw-alias-border-l1)"}},
1664
+ h("div",{style:{fontSize:12, fontWeight:600, marginBottom:6}}, t("workshop.rel.add")),
1665
+ h("div",{style:{display:"flex", gap:6, flexWrap:"wrap", alignItems:"center"}},
1666
+ h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.from")),
1667
+ h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)", maxWidth:130}), value:relForm.from, onChange:function(ev){ setF({from:ev.target.value}); }},
1668
+ h("option",{value:""}, "—"),
1669
+ relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); })),
1670
+ h("span",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)"}}, t("workshop.rel.to")),
1671
+ h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)", maxWidth:130}), value:relForm.to, onChange:function(ev){ setF({to:ev.target.value}); }},
1672
+ h("option",{value:""}, "—"),
1673
+ relNodes.map(function(n){ return h("option",{key:n.id, value:n.id}, n.name); })),
1674
+ h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:relForm.type, onChange:function(ev){ setF({type:ev.target.value}); }},
1675
+ relTypes.map(function(ty){ return h("option",{key:ty, value:ty}, t("rel.type."+ty)); })),
1676
+ h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:String(relForm.strength), onChange:function(ev){ setF({strength:Number(ev.target.value)}); }},
1677
+ [1,2,3,4,5].map(function(n){ return h("option",{key:n, value:String(n)}, t("workshop.rel.strength") + " " + n); })),
1678
+ h("select",{style:Object.assign({}, smallSel, {background:"var(--dsw-alias-bg-base)"}), value:relForm.direction, onChange:function(ev){ setF({direction:ev.target.value}); }},
1679
+ h("option",{value:"one-way"}, t("workshop.rel.dirOneWay")),
1680
+ h("option",{value:"mutual"}, t("workshop.rel.dirMutual"))),
1681
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"3px 10px", fontSize:11}), onClick:function(){ store.patchRelations([{op:"addEdge", from:relForm.from, to:relForm.to, type:relForm.type, strength:relForm.strength, direction:relForm.direction, note:relForm.note}]); }}, t("workshop.rel.add"))),
1682
+ h("input",{placeholder:t("workshop.rel.note"), style:{width:"100%", boxSizing:"border-box", marginTop:6, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 8px", fontSize:12}, value:relForm.note, onChange:function(ev){ setF({note:ev.target.value}); }})),
1683
+ h("div",{style:{display:"flex", gap:8, marginTop:14, flexWrap:"wrap"}},
1684
+ h("button",{type:"button", style:Object.assign({}, S.btn, {padding:"4px 12px", fontSize:12}), onClick:function(){ store.renderRelationsToCard(); }, disabled: s.relBusy==="render"}, s.relBusy==="render" ? "…" : t("workshop.rel.render")),
1685
+ 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")),
1686
+ 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")),
1687
+ 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")))),
1688
+ s.lastMsg ? h("div",{style:{marginTop:10, fontSize:12, color:"var(--dsw-alias-label-secondary)", lineHeight:1.7}}, s.lastMsg) : null)
1689
+ );
1388
1690
  }
1389
1691
  var stepsEls=steps.map(function(label, i){
1390
1692
  var active=s.cur===i;
@@ -1395,9 +1697,14 @@ window.__ModuleLoader__.load({
1395
1697
  onClick:function(){ store.setCur(i); }}, (i+1)+". "+label);
1396
1698
  });
1397
1699
  var filteredLibrary = (function(){
1700
+ var gid = String(s.currentGroupId||"");
1701
+ var base = s.library;
1702
+ // 0.8.0:分组筛选("" = 全部;"__none" = 只看未分组)
1703
+ if(gid === "__none") base = base.filter(function(e){ return !e.groupId; });
1704
+ else if(gid) base = base.filter(function(e){ return String(e.groupId||"")===gid; });
1398
1705
  var q = String(s.libraryQuery||"").trim().toLowerCase();
1399
- if(!q) return s.library;
1400
- return s.library.filter(function(e){
1706
+ if(!q) return base;
1707
+ return base.filter(function(e){
1401
1708
  var nameMatch = String(e.name||"").toLowerCase().indexOf(q)>=0;
1402
1709
  var tagMatch = Array.isArray(e.tags) && e.tags.some(function(t){ return String(t).toLowerCase().indexOf(q)>=0; });
1403
1710
  var loreMatch = String(e.loreCount||"").indexOf(q)>=0;
@@ -1443,6 +1750,19 @@ window.__ModuleLoader__.load({
1443
1750
  h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", alignSelf:"center", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", flex:1, textAlign:"right"}}, s.draft.data.name ? s.draft.data.name.slice(0,8) : t("workshop.library.unnamed"))
1444
1751
  ),
1445
1752
  h("input",{placeholder:t("workshop.library.searchPlaceholder"), style:{width:"100%", boxSizing:"border-box", maxWidth:"100%", background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"6px 8px", fontSize:12}, value:s.libraryQuery, onChange:function(e){ store.setLibraryQuery(e.target.value); }}),
1753
+ // 0.8.0 分组:同一世界观的角色拉进一组,关系网按组维护(「关系」按钮进关系视图,s.cur===4)
1754
+ h("div",{style:{display:"flex", gap:6, marginTop:6}},
1755
+ h("select",{style:{flex:1, minWidth:0, background:"var(--dsw-alias-bg-base)", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 6px", fontSize:12}, value:s.currentGroupId||"", onChange:function(e){ store.setCurrentGroupId(e.target.value); }},
1756
+ h("option",{value:""}, t("workshop.group.all")),
1757
+ h("option",{value:"__none"}, t("workshop.group.none")),
1758
+ (s.groups||[]).map(function(g){ return h("option",{key:g.id, value:g.id}, g.name + " · " + t("workshop.group.summary",{n:String(g.memberCount), m:String(g.edgeCount)})); })),
1759
+ (groupDraft === null
1760
+ ? h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:"var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11, flexShrink:0}), onClick:function(){ setGroupDraft(""); }}, t("workshop.group.new"))
1761
+ : h("input",{autoFocus:true, value:groupDraft, placeholder:t("workshop.group.prompt"), style:{width:104, flexShrink:0, background:"var(--dsw-alias-bg-base)", border:"1px solid #7c5cff", borderRadius:8, color:"var(--dsw-alias-label-primary)", padding:"4px 6px", fontSize:11},
1762
+ onChange:function(ev){ setGroupDraft(ev.target.value); },
1763
+ onKeyDown:function(ev){ if(ev.key==="Enter"){ store.createGroup(ev.target.value); setGroupDraft(null); } if(ev.key==="Escape"){ setGroupDraft(null); } },
1764
+ onBlur:function(ev){ var v=String(ev.target.value||"").trim(); if(v) store.createGroup(v); setGroupDraft(null); }})),
1765
+ h("button",{type:"button", style:Object.assign({}, S.btnGhost, {border:"1px solid #7c5cff", background:"transparent", color:"#7c5cff", borderRadius:8, padding:"4px 8px", fontSize:11, flexShrink:0}), onClick:function(){ store.openRelations(); }}, t("workshop.rel.open"))),
1446
1766
  s.lastMsg ? h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}, s.lastMsg) : null
1447
1767
  ),
1448
1768
  h("div",{style:S.libraryList},
@@ -1473,7 +1793,11 @@ window.__ModuleLoader__.load({
1473
1793
  // 想导这张已存角色先点「载入」—— 免得这里悄悄换掉用户正在编辑的草稿。
1474
1794
  // 点下去按钮自己先变(「已切到 ✓」),不然切页发生在中栏、用户视线却在右侧,会以为没反应。
1475
1795
  h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid var(--dsw-alias-border-l1)", background:"transparent", color:(jumpFrom && jumpFrom.id===entry.id)? "#a6e8c8" : "var(--dsw-alias-label-secondary)", borderRadius:8, padding:"4px 8px", fontSize:11}), title:t("workshop.library.exportTip"), onClick:function(){ jumpToExport(entry); }}, (jumpFrom && jumpFrom.id===entry.id)? t("workshop.library.exportJumped"): t("workshop.library.export")),
1476
- h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.deleteFromLibrary(entry.id); }, disabled: !!isBusy}, t("common.delete"))
1796
+ h("button",{style:Object.assign({}, S.btnGhost, {border:"1px solid #3a2a2a", background:"transparent", color:"#e8a6a6", borderRadius:8, padding:"4px 8px", fontSize:11}), onClick:function(){ store.deleteFromLibrary(entry.id); }, disabled: !!isBusy}, t("common.delete")),
1797
+ // 0.8.0:直接把卡归到某个分组(空 = 未分组)。分组是关系图谱的载体。
1798
+ h("select",{style:{background:"transparent", border:"1px solid var(--dsw-alias-border-l1)", borderRadius:8, color:(entry.groupId?"#7c5cff":"var(--dsw-alias-label-secondary)"), fontSize:11, padding:"2px 4px", maxWidth:92, flexShrink:0}, value:String(entry.groupId||""), disabled: !!isBusy, title:t("workshop.group.assign"), onChange:function(ev){ store.assignGroup(entry.id, ev.target.value||null); }},
1799
+ h("option",{value:""}, t("workshop.group.none")),
1800
+ (s.groups||[]).map(function(g){ return h("option",{key:g.id, value:g.id}, g.name); }))
1477
1801
  )
1478
1802
  );
1479
1803
  })