dsh-waifu-agents 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/client.js +257 -115
- package/http-route.js +178 -9
- package/index.js +150 -17
- package/memory-ops.js +159 -55
- package/package.json +2 -1
- package/panel-ops.js +2958 -2356
- package/persona-memory.js +232 -40
- package/personas/README.md +1 -1
- package/scripts/run-suites.mjs +2 -1
- package/serialize.js +133 -0
- package/team-memory.js +227 -17
- package/team-runtime.js +47 -26
- package/team-simulate.js +61 -16
- package/team.js +14 -3
package/README.md
CHANGED
|
@@ -84,11 +84,13 @@ group: 组名
|
|
|
84
84
|
几条习惯:
|
|
85
85
|
|
|
86
86
|
- 编辑是**离开输入框自动保存**,没有保存按钮。
|
|
87
|
-
-
|
|
87
|
+
- 新增与编辑都开在**窗口**里(团队编辑器同样):关掉窗口就是保存完,名册本身不会被挤走。
|
|
88
|
+
- 只有行首的 **▸** 会打开编辑窗口,点名字不会。
|
|
88
89
|
- 行尾的**眼睛**看组装后的成品(模型真正收到的那份)。
|
|
89
90
|
- 改名字时**文件名自动跟着变**;改名不会丢正文。
|
|
90
91
|
- 隐藏是改名成 `.md.off`,正文还在,随时取消隐藏。
|
|
91
92
|
- 会动数据的事都会先弹一次确认。
|
|
93
|
+
- 新建任务时那一列人设的顺序是:**默认人设 → 有分组的人设(按组名)→ 没分组的人设**。组内仍按「拖拽排序」的位置。
|
|
92
94
|
|
|
93
95
|
## 聊天模式与工作模式
|
|
94
96
|
|
|
@@ -180,6 +182,8 @@ group: 组名
|
|
|
180
182
|
|
|
181
183
|
**某一行提示「激活失败」「ID 占用冲突」?** 多半是热重载时新旧实例并存,**完全退出 DSH 再启动**,提示消失就没事。
|
|
182
184
|
|
|
185
|
+
**改完插件后整个面板打不开(或一片空白)?** 热重载时新旧两个实例会抢同一条接口地址,新版有可能一直没抢到。插件现在会自己重试半秒多,还失败就在日志里留一句「面板路由没注册上」。真的看到那句,**完全退出 DSH 再启动**就能恢复——人设、记忆、团队都没受影响,丢的只是那一版的面板。
|
|
186
|
+
|
|
183
187
|
**保存被拒,说「磁盘上变过」?** 这行在别处被改过了,重新打开载入最新内容再改一次(被拒时磁盘上什么都没写)。
|
|
184
188
|
|
|
185
189
|
**拖不动行?** 「拖拽排序」每次打开面板都是关的,在工具条里打开;开着时行内按钮会一起锁住。
|
package/client.js
CHANGED
|
@@ -95,6 +95,9 @@ window.__ModuleLoader__.load({
|
|
|
95
95
|
tabMemory: '记忆',
|
|
96
96
|
search: '搜索名字、简述或正文…',
|
|
97
97
|
add: '新增',
|
|
98
|
+
/* 两个编辑器的窗口标题:'新增人设' / '编辑人设'。 */
|
|
99
|
+
personaNew: '新增人设',
|
|
100
|
+
personaEdit: '编辑人设',
|
|
98
101
|
import: '导入 md',
|
|
99
102
|
exportAll: '导出全部',
|
|
100
103
|
exportOne: '导出这一条',
|
|
@@ -196,7 +199,8 @@ window.__ModuleLoader__.load({
|
|
|
196
199
|
name: '名字',
|
|
197
200
|
description: '简述',
|
|
198
201
|
bodyPlaceholder: '只写她独有的部分;公共条款在骨架里',
|
|
199
|
-
|
|
202
|
+
save: '保存',
|
|
203
|
+
saveHint: '改动要点「保存」才会写入',
|
|
200
204
|
nameDrivesFile: '文件名会跟着「名字」走',
|
|
201
205
|
saving: '保存中…',
|
|
202
206
|
saved: '已保存',
|
|
@@ -433,6 +437,8 @@ window.__ModuleLoader__.load({
|
|
|
433
437
|
tabMemory: 'Memory',
|
|
434
438
|
search: 'Search name, summary or body…',
|
|
435
439
|
add: 'New',
|
|
440
|
+
personaNew: 'New persona',
|
|
441
|
+
personaEdit: 'Edit persona',
|
|
436
442
|
import: 'Import md',
|
|
437
443
|
exportAll: 'Export all',
|
|
438
444
|
exportOne: 'Export this file',
|
|
@@ -528,7 +534,8 @@ window.__ModuleLoader__.load({
|
|
|
528
534
|
name: 'Name',
|
|
529
535
|
description: 'Summary',
|
|
530
536
|
bodyPlaceholder: 'Only her own part; the shared clauses live in the skeleton',
|
|
531
|
-
|
|
537
|
+
save: 'save',
|
|
538
|
+
saveHint: 'Changes are written when you press Save',
|
|
532
539
|
nameDrivesFile: 'The file name follows this name',
|
|
533
540
|
saving: 'Saving…',
|
|
534
541
|
saved: 'Saved',
|
|
@@ -1036,6 +1043,26 @@ window.__ModuleLoader__.load({
|
|
|
1036
1043
|
borderRadius: 'var(--dsw-radius-panel, 12px)',
|
|
1037
1044
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1038
1045
|
},
|
|
1046
|
+
/*
|
|
1047
|
+
* The wide dialog behind `Sheet`. Same surface as `dialog`, but its height
|
|
1048
|
+
* is capped and its body scrolls: a persona body can be arbitrarily long,
|
|
1049
|
+
* and a window taller than the viewport puts the close button off screen.
|
|
1050
|
+
*/
|
|
1051
|
+
sheet: {
|
|
1052
|
+
display: 'flex',
|
|
1053
|
+
flexDirection: 'column',
|
|
1054
|
+
gap: '12px',
|
|
1055
|
+
maxWidth: 'calc(100vw - 48px)',
|
|
1056
|
+
maxHeight: '86vh',
|
|
1057
|
+
padding: '16px',
|
|
1058
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1059
|
+
borderRadius: 'var(--dsw-radius-panel, 12px)',
|
|
1060
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1061
|
+
boxSizing: 'border-box',
|
|
1062
|
+
boxShadow: '0 18px 48px rgba(0, 0, 0, 0.28)',
|
|
1063
|
+
},
|
|
1064
|
+
sheetHead: { display: 'flex', alignItems: 'flex-start', gap: '8px' },
|
|
1065
|
+
sheetBody: { display: 'flex', flexDirection: 'column', gap: '10px', overflow: 'auto', minHeight: 0 },
|
|
1039
1066
|
dragging: { opacity: 0.45 },
|
|
1040
1067
|
reorderRow: { cursor: 'grab', userSelect: 'none' },
|
|
1041
1068
|
dropTarget: { borderColor: 'var(--dsw-alias-state-business-primary)', borderStyle: 'dashed' },
|
|
@@ -1569,6 +1596,75 @@ window.__ModuleLoader__.load({
|
|
|
1569
1596
|
);
|
|
1570
1597
|
}
|
|
1571
1598
|
|
|
1599
|
+
/**
|
|
1600
|
+
* 一块从中间升起的面板:新增/编辑人设、新建/编辑团队、查看组装结果都用它。
|
|
1601
|
+
*
|
|
1602
|
+
* 为什么不用平台的 `Modal`:它的宽度不在本运行时的契约里(面板拿到的
|
|
1603
|
+
* 只有 `Button`/`Tag`/`MarkdownText` 的已核实形状),而这三样恰恰需要
|
|
1604
|
+
* 比一句确认宽得多的版面——整段正文、一列成员勾选、一整份组装后的
|
|
1605
|
+
* 提示词。`Confirm` / `Alert` 那种一句话对话框仍然走平台,两者不冲突:
|
|
1606
|
+
* 这里只借用同一层遮罩色和同一套主题 token。
|
|
1607
|
+
*
|
|
1608
|
+
* 收尾路径分两档。未锁定时三条都要有:右上角的关闭、点遮罩、Esc——少一
|
|
1609
|
+
* 条,用户就会在某一次编辑后发现"关不掉",而那正是弹窗最容易招人烦的地
|
|
1610
|
+
* 方。锁定(`locked`)时只剩显式按钮:编辑器正攥着一份未保存的草稿,一
|
|
1611
|
+
* 次走神的点击落在遮罩上、或者一个惯性 Esc,就把整段正文无声丢掉——这
|
|
1612
|
+
* 种"没有确认、没有恢复"的丢失比关不掉恶劣得多,所以两个编辑器都锁。
|
|
1613
|
+
*/
|
|
1614
|
+
function Sheet({ open, title, hint, width, t, onClose, footer, children, locked }) {
|
|
1615
|
+
React.useEffect(() => {
|
|
1616
|
+
if (open !== true) return undefined;
|
|
1617
|
+
if (locked === true) return undefined;
|
|
1618
|
+
const onKey = (event) => {
|
|
1619
|
+
if (event.key === 'Escape') onClose();
|
|
1620
|
+
};
|
|
1621
|
+
document.addEventListener('keydown', onKey);
|
|
1622
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
1623
|
+
}, [open, onClose, locked]);
|
|
1624
|
+
if (open !== true) return null;
|
|
1625
|
+
return h(
|
|
1626
|
+
'div',
|
|
1627
|
+
{
|
|
1628
|
+
style: { ...L.overlay, zIndex: 60 },
|
|
1629
|
+
// 锁定时遮罩不再接 onClose:点外面什么也不发生,关窗只能走按钮。
|
|
1630
|
+
onClick: locked === true ? undefined : onClose,
|
|
1631
|
+
},
|
|
1632
|
+
h(
|
|
1633
|
+
'div',
|
|
1634
|
+
{
|
|
1635
|
+
style: { ...L.sheet, width: width ?? '760px' },
|
|
1636
|
+
role: 'dialog',
|
|
1637
|
+
'aria-modal': 'true',
|
|
1638
|
+
'aria-label': title,
|
|
1639
|
+
// A click that landed on the window itself — typing, selecting
|
|
1640
|
+
// text, dragging the scrollbar — must not close it.
|
|
1641
|
+
onClick: (event) => event.stopPropagation(),
|
|
1642
|
+
},
|
|
1643
|
+
h(
|
|
1644
|
+
'div',
|
|
1645
|
+
{ style: L.sheetHead },
|
|
1646
|
+
h(
|
|
1647
|
+
'div',
|
|
1648
|
+
{ style: L.rowMain },
|
|
1649
|
+
h('span', { style: L.rowTitle }, title),
|
|
1650
|
+
hint === undefined || hint === null || hint === '' ? null : h('span', { style: L.hint }, hint),
|
|
1651
|
+
),
|
|
1652
|
+
h(Btn, {
|
|
1653
|
+
variant: 'toolbar',
|
|
1654
|
+
size: 'sm',
|
|
1655
|
+
icon: h(Icon, { name: 'close', size: 16 }),
|
|
1656
|
+
title: t('close'),
|
|
1657
|
+
'aria-label': t('close'),
|
|
1658
|
+
style: { padding: '0 6px' },
|
|
1659
|
+
onClick: onClose,
|
|
1660
|
+
}),
|
|
1661
|
+
),
|
|
1662
|
+
h('div', { style: L.sheetBody }, children),
|
|
1663
|
+
footer ?? null,
|
|
1664
|
+
),
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1572
1668
|
/**
|
|
1573
1669
|
* A v4 UUID, minted by the PANEL rather than the Host.
|
|
1574
1670
|
*
|
|
@@ -1831,8 +1927,8 @@ window.__ModuleLoader__.load({
|
|
|
1831
1927
|
* a draft with unsaved keystrokes in it, and a preview that reads from the
|
|
1832
1928
|
* draft can never be what the model sees. This one is fetched from the Host
|
|
1833
1929
|
* (`get` again), so it shows the file as it is on disk with the skeleton
|
|
1834
|
-
* poured in —
|
|
1835
|
-
*
|
|
1930
|
+
* poured in — "on disk" now means "the last version you pressed Save on";
|
|
1931
|
+
* edits still in the editor are deliberately not in it.
|
|
1836
1932
|
*/
|
|
1837
1933
|
function PreviewWindow({ open, title, text, loading, error, t, onClose }) {
|
|
1838
1934
|
if (open !== true) return null;
|
|
@@ -1847,41 +1943,21 @@ window.__ModuleLoader__.load({
|
|
|
1847
1943
|
h(Markdown, { text: text ?? '' }),
|
|
1848
1944
|
);
|
|
1849
1945
|
/*
|
|
1850
|
-
* The
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1946
|
+
* The same wide window the two editors use, for two reasons.
|
|
1947
|
+
*
|
|
1948
|
+
* Width: this shows the assembled prompt — the thing the model actually
|
|
1949
|
+
* receives — and it was being read through a 560px column, which is
|
|
1950
|
+
* narrower than the editor that produced it. Reading the result should
|
|
1951
|
+
* not be harder than writing it.
|
|
1952
|
+
*
|
|
1953
|
+
* Consistency: the editor that this opens from is a Sheet, so a
|
|
1954
|
+
* different dialog shape one click later would read as two different
|
|
1955
|
+
* products sharing a page.
|
|
1853
1956
|
*/
|
|
1854
|
-
if (primitives !== null && typeof primitives.Modal === 'function') {
|
|
1855
|
-
return h(primitives.Modal, { open: true, onClose, title, closeLabel: t('close'), children: content });
|
|
1856
|
-
}
|
|
1857
1957
|
return h(
|
|
1858
|
-
|
|
1859
|
-
{
|
|
1860
|
-
|
|
1861
|
-
'div',
|
|
1862
|
-
{
|
|
1863
|
-
style: { ...L.dialog, maxWidth: '560px', width: '100%' },
|
|
1864
|
-
// The backdrop closes it; a click that landed on the window itself
|
|
1865
|
-
// — reading, selecting text — must not.
|
|
1866
|
-
onClick: (event) => event.stopPropagation(),
|
|
1867
|
-
},
|
|
1868
|
-
h(
|
|
1869
|
-
'div',
|
|
1870
|
-
{ style: L.bar },
|
|
1871
|
-
h('span', { style: L.rowTitle }, title),
|
|
1872
|
-
h('div', { style: L.push }),
|
|
1873
|
-
h(Btn, {
|
|
1874
|
-
variant: 'toolbar',
|
|
1875
|
-
size: 'sm',
|
|
1876
|
-
icon: h(Icon, { name: 'close', size: 16 }),
|
|
1877
|
-
title: t('close'),
|
|
1878
|
-
'aria-label': t('close'),
|
|
1879
|
-
style: { padding: '0 6px' },
|
|
1880
|
-
onClick: onClose,
|
|
1881
|
-
}),
|
|
1882
|
-
),
|
|
1883
|
-
content,
|
|
1884
|
-
),
|
|
1958
|
+
Sheet,
|
|
1959
|
+
{ open: true, title, width: '980px', t, onClose },
|
|
1960
|
+
content,
|
|
1885
1961
|
);
|
|
1886
1962
|
}
|
|
1887
1963
|
|
|
@@ -1932,7 +2008,20 @@ window.__ModuleLoader__.load({
|
|
|
1932
2008
|
);
|
|
1933
2009
|
}
|
|
1934
2010
|
|
|
1935
|
-
|
|
2011
|
+
/*
|
|
2012
|
+
* 人设编辑器:弹窗里的一张表,改动**不**自动保存。
|
|
2013
|
+
*
|
|
2014
|
+
* 曾经每个输入框失焦就写一次盘("改动会在离开输入框时自动保存")。
|
|
2015
|
+
* 那意味着"保存"这个动作不可见:点谁、路过谁都会触发一次写入,而
|
|
2016
|
+
* 写一半想去看看别人、再回来的路上一句话已经被存成了半句。现在写入
|
|
2017
|
+
* 只有一个入口——下面的「保存」按钮;关掉窗口(× 或「取消」)丢掉
|
|
2018
|
+
* 没保存的改动,文件保持上一次保存的样子。
|
|
2019
|
+
*
|
|
2020
|
+
* `dirty` 由调用方算(`draftKeyOf(draft) !== draft.savedKey`):它让
|
|
2021
|
+
* 「保存」在没有改动时是灰的,替代旧模型里"没改动就不发请求"的
|
|
2022
|
+
* 静默去重——去重还在(`save` 里),只是用户现在也看得见。
|
|
2023
|
+
*/
|
|
2024
|
+
function Editor({ draft, t, busy, onPatch, onSave, onCancel, dirty, tall }) {
|
|
1936
2025
|
const loading = draft.loading === true;
|
|
1937
2026
|
const field = (label, ...controls) => h('div', { style: L.field }, h('span', { style: L.label }, label), ...controls);
|
|
1938
2027
|
const text = draft.body ?? '';
|
|
@@ -1951,7 +2040,6 @@ window.__ModuleLoader__.load({
|
|
|
1951
2040
|
value: draft.name ?? '',
|
|
1952
2041
|
disabled: busy,
|
|
1953
2042
|
onChange: (event) => onPatch({ name: event.target.value }),
|
|
1954
|
-
onBlur: onCommit,
|
|
1955
2043
|
}),
|
|
1956
2044
|
),
|
|
1957
2045
|
field(
|
|
@@ -1961,7 +2049,6 @@ window.__ModuleLoader__.load({
|
|
|
1961
2049
|
value: draft.description ?? '',
|
|
1962
2050
|
disabled: busy,
|
|
1963
2051
|
onChange: (event) => onPatch({ description: event.target.value }),
|
|
1964
|
-
onBlur: onCommit,
|
|
1965
2052
|
}),
|
|
1966
2053
|
),
|
|
1967
2054
|
field(
|
|
@@ -1971,7 +2058,6 @@ window.__ModuleLoader__.load({
|
|
|
1971
2058
|
value: draft.group ?? '',
|
|
1972
2059
|
disabled: busy,
|
|
1973
2060
|
onChange: (event) => onPatch({ group: event.target.value }),
|
|
1974
|
-
onBlur: onCommit,
|
|
1975
2061
|
}),
|
|
1976
2062
|
),
|
|
1977
2063
|
field(
|
|
@@ -1980,10 +2066,7 @@ window.__ModuleLoader__.load({
|
|
|
1980
2066
|
checked: (draft.template ?? '') === SKELETON_FILE,
|
|
1981
2067
|
disabled: busy,
|
|
1982
2068
|
title: t('useSkeleton'),
|
|
1983
|
-
onChange: (next) => {
|
|
1984
|
-
onPatch({ template: next ? SKELETON_FILE : '' });
|
|
1985
|
-
onCommit();
|
|
1986
|
-
},
|
|
2069
|
+
onChange: (next) => onPatch({ template: next ? SKELETON_FILE : '' }),
|
|
1987
2070
|
}),
|
|
1988
2071
|
),
|
|
1989
2072
|
/*
|
|
@@ -1993,12 +2076,11 @@ window.__ModuleLoader__.load({
|
|
|
1993
2076
|
* exactly one mode and no state to remember.
|
|
1994
2077
|
*/
|
|
1995
2078
|
h('textarea', {
|
|
1996
|
-
style: L.textarea,
|
|
2079
|
+
style: tall === true ? { ...L.textarea, minHeight: '44vh' } : L.textarea,
|
|
1997
2080
|
value: text,
|
|
1998
2081
|
disabled: busy,
|
|
1999
2082
|
placeholder: t('bodyPlaceholder'),
|
|
2000
2083
|
onChange: (event) => onPatch({ body: event.target.value }),
|
|
2001
|
-
onBlur: onCommit,
|
|
2002
2084
|
}),
|
|
2003
2085
|
(draft.problems ?? []).length > 0
|
|
2004
2086
|
? h(
|
|
@@ -2008,7 +2090,17 @@ window.__ModuleLoader__.load({
|
|
|
2008
2090
|
...(draft.problems ?? []).map((line, index) => h('div', { key: index, style: L.error }, line)),
|
|
2009
2091
|
)
|
|
2010
2092
|
: null,
|
|
2011
|
-
h(
|
|
2093
|
+
h(
|
|
2094
|
+
'div',
|
|
2095
|
+
{ style: { ...L.bar, justifyContent: 'space-between', marginTop: '4px' } },
|
|
2096
|
+
h('span', { style: L.hint }, `${t('saveHint')} · ${t('nameDrivesFile')}`),
|
|
2097
|
+
h(
|
|
2098
|
+
'div',
|
|
2099
|
+
{ style: L.bar },
|
|
2100
|
+
h(Btn, { variant: 'toolbar', disabled: busy, onClick: onCancel }, t('cancel')),
|
|
2101
|
+
h(Btn, { variant: 'primary', disabled: busy || dirty === false, onClick: onSave }, t('save')),
|
|
2102
|
+
),
|
|
2103
|
+
),
|
|
2012
2104
|
),
|
|
2013
2105
|
);
|
|
2014
2106
|
}
|
|
@@ -2225,9 +2317,9 @@ window.__ModuleLoader__.load({
|
|
|
2225
2317
|
'aria-label': t('preview'),
|
|
2226
2318
|
/*
|
|
2227
2319
|
* Not disabled while `busy`. Reading does not fight the writer, and
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2320
|
+
* an explicit-save editor means the click never starts a write —
|
|
2321
|
+
* the preview shows the last saved version, unsaved keystrokes
|
|
2322
|
+
* stay in the editor where they belong.
|
|
2231
2323
|
*/
|
|
2232
2324
|
disabled: locked,
|
|
2233
2325
|
style: { padding: '0 6px' },
|
|
@@ -2627,27 +2719,50 @@ window.__ModuleLoader__.load({
|
|
|
2627
2719
|
}
|
|
2628
2720
|
const problem =
|
|
2629
2721
|
team.problem === null || team.problem === undefined ? null : h('span', { style: L.error }, team.problem);
|
|
2630
|
-
|
|
2722
|
+
/*
|
|
2723
|
+
* The team editor is a window over the list, the same shape as the
|
|
2724
|
+
* persona editor. It used to replace the list outright: opening one team
|
|
2725
|
+
* hid every other team and the count above them, so the answer to
|
|
2726
|
+
* "is that the one I already have?" was behind the dialog being edited.
|
|
2727
|
+
*/
|
|
2728
|
+
const editor =
|
|
2729
|
+
draft === null
|
|
2730
|
+
? null
|
|
2731
|
+
: h(
|
|
2732
|
+
Sheet,
|
|
2733
|
+
{
|
|
2734
|
+
open: true,
|
|
2735
|
+
title:
|
|
2736
|
+
draft.name === undefined || draft.name === ''
|
|
2737
|
+
? t('teamNew')
|
|
2738
|
+
: `${t('teamEdit')} · ${draft.name}`,
|
|
2739
|
+
width: '860px',
|
|
2740
|
+
t,
|
|
2741
|
+
onClose: onCancelDraft,
|
|
2742
|
+
// 与人设编辑器同一档:草稿只认显式的取消/保存。
|
|
2743
|
+
locked: true,
|
|
2744
|
+
},
|
|
2745
|
+
h(TeamDraftCard, {
|
|
2746
|
+
draft,
|
|
2747
|
+
t,
|
|
2748
|
+
busy,
|
|
2749
|
+
rows: team.rows,
|
|
2750
|
+
groups: team.groups,
|
|
2751
|
+
onPatch,
|
|
2752
|
+
onSave: onSaveDraft,
|
|
2753
|
+
onCancel: onCancelDraft,
|
|
2754
|
+
problem: fieldProblem,
|
|
2755
|
+
}),
|
|
2756
|
+
);
|
|
2757
|
+
if (team.teams.length === 0) {
|
|
2631
2758
|
return h(
|
|
2632
2759
|
'div',
|
|
2633
2760
|
{ style: L.list },
|
|
2634
2761
|
problem,
|
|
2635
|
-
h(
|
|
2636
|
-
|
|
2637
|
-
t,
|
|
2638
|
-
busy,
|
|
2639
|
-
rows: team.rows,
|
|
2640
|
-
groups: team.groups,
|
|
2641
|
-
onPatch,
|
|
2642
|
-
onSave: onSaveDraft,
|
|
2643
|
-
onCancel: onCancelDraft,
|
|
2644
|
-
problem: fieldProblem,
|
|
2645
|
-
}),
|
|
2762
|
+
h('div', { style: L.card }, h('span', { style: L.hint }, t('teamEmpty'))),
|
|
2763
|
+
editor,
|
|
2646
2764
|
);
|
|
2647
2765
|
}
|
|
2648
|
-
if (team.teams.length === 0) {
|
|
2649
|
-
return h('div', { style: L.list }, problem, h('div', { style: L.card }, h('span', { style: L.hint }, t('teamEmpty'))));
|
|
2650
|
-
}
|
|
2651
2766
|
return h(
|
|
2652
2767
|
'div',
|
|
2653
2768
|
{ style: L.list },
|
|
@@ -2742,6 +2857,7 @@ window.__ModuleLoader__.load({
|
|
|
2742
2857
|
h(TeamPreview, { preview: entry.preview, t }),
|
|
2743
2858
|
);
|
|
2744
2859
|
}),
|
|
2860
|
+
editor,
|
|
2745
2861
|
);
|
|
2746
2862
|
}
|
|
2747
2863
|
|
|
@@ -3547,7 +3663,9 @@ window.__ModuleLoader__.load({
|
|
|
3547
3663
|
onImportClick,
|
|
3548
3664
|
onDownloadTemplate,
|
|
3549
3665
|
onPatchDraft,
|
|
3550
|
-
|
|
3666
|
+
onSave,
|
|
3667
|
+
dirty,
|
|
3668
|
+
onCloseDraft,
|
|
3551
3669
|
page,
|
|
3552
3670
|
pageSize,
|
|
3553
3671
|
onPage,
|
|
@@ -3669,45 +3787,55 @@ window.__ModuleLoader__.load({
|
|
|
3669
3787
|
groupFolded(group.key)
|
|
3670
3788
|
? null
|
|
3671
3789
|
: group.rows.map((item) =>
|
|
3672
|
-
h(
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
onDragEnd,
|
|
3697
|
-
}),
|
|
3698
|
-
openId === item.id && draft !== null && draft.id === item.id
|
|
3699
|
-
? h(Editor, { draft, t, busy, onPatch: onPatchDraft, onCommit })
|
|
3700
|
-
: null,
|
|
3701
|
-
),
|
|
3790
|
+
h(WifeRow, {
|
|
3791
|
+
key: item.id,
|
|
3792
|
+
item,
|
|
3793
|
+
t,
|
|
3794
|
+
open: openId === item.id,
|
|
3795
|
+
busy,
|
|
3796
|
+
isDefault: data !== null && data.defaultId === item.id,
|
|
3797
|
+
broken: brokenById === undefined ? null : (brokenById[item.id] ?? null),
|
|
3798
|
+
selected: batches.includes(item.id),
|
|
3799
|
+
batchMode,
|
|
3800
|
+
onToggle: () => onOpenRow(item),
|
|
3801
|
+
onSelect: () => onSelect(item.id),
|
|
3802
|
+
onPreview: () => onPreviewOne(item),
|
|
3803
|
+
onExport: () => onExportOne(item.id),
|
|
3804
|
+
onHide: () => onHideOne(item),
|
|
3805
|
+
onDefault: (next) => onSetDefault(item, next),
|
|
3806
|
+
dragging,
|
|
3807
|
+
dropTarget,
|
|
3808
|
+
reorder: reorderMode === true,
|
|
3809
|
+
onDragStart,
|
|
3810
|
+
onDragOver,
|
|
3811
|
+
onDrop,
|
|
3812
|
+
onDragEnd,
|
|
3813
|
+
}),
|
|
3702
3814
|
),
|
|
3703
3815
|
),
|
|
3704
3816
|
),
|
|
3705
|
-
|
|
3817
|
+
/*
|
|
3818
|
+
* The editor is a window now, not a row that grew.
|
|
3819
|
+
*
|
|
3820
|
+
* A row sits inside the scrolling list, so opening one pushed everything
|
|
3821
|
+
* below it down, and the body was read through a window four rows tall.
|
|
3822
|
+
* One window, sized for the work, opened over the list: the roster stays
|
|
3823
|
+
* where it was, and closing it cannot leave a half-open row behind.
|
|
3824
|
+
*/
|
|
3825
|
+
draft !== null && openId !== null
|
|
3706
3826
|
? h(
|
|
3707
|
-
|
|
3708
|
-
{
|
|
3709
|
-
|
|
3710
|
-
|
|
3827
|
+
Sheet,
|
|
3828
|
+
{
|
|
3829
|
+
open: true,
|
|
3830
|
+
// A new persona has no name yet; the edit draft names itself.
|
|
3831
|
+
title: draft.mode === 'new' ? t('personaNew') : `${t('personaEdit')} · ${draft.name || draft.id}`,
|
|
3832
|
+
width: '820px',
|
|
3833
|
+
t,
|
|
3834
|
+
onClose: onCloseDraft,
|
|
3835
|
+
// 点遮罩、Esc 都不收:草稿只认显式的取消/保存。
|
|
3836
|
+
locked: true,
|
|
3837
|
+
},
|
|
3838
|
+
h(Editor, { draft, t, busy, onPatch: onPatchDraft, onSave, onCancel: onCloseDraft, dirty, tall: true }),
|
|
3711
3839
|
)
|
|
3712
3840
|
: null,
|
|
3713
3841
|
pageCount > 1
|
|
@@ -3838,12 +3966,12 @@ window.__ModuleLoader__.load({
|
|
|
3838
3966
|
const idAlias = React.useRef(new Map());
|
|
3839
3967
|
const fileInput = React.useRef(null);
|
|
3840
3968
|
/**
|
|
3841
|
-
* The
|
|
3969
|
+
* The explicit save currently in flight, if any.
|
|
3842
3970
|
*
|
|
3843
|
-
*
|
|
3844
|
-
*
|
|
3845
|
-
*
|
|
3846
|
-
*
|
|
3971
|
+
* Saves start only from the editor's Save button now, so there is no
|
|
3972
|
+
* blur race to lose — but a second click on Save while one write is
|
|
3973
|
+
* still running must not start a duplicate, and `openPreview` keeps
|
|
3974
|
+
* waiting on this slot so a save and a read can never interleave.
|
|
3847
3975
|
*/
|
|
3848
3976
|
const savingRef = React.useRef(null);
|
|
3849
3977
|
|
|
@@ -4416,6 +4544,13 @@ window.__ModuleLoader__.load({
|
|
|
4416
4544
|
savedKey: written,
|
|
4417
4545
|
});
|
|
4418
4546
|
}
|
|
4547
|
+
/*
|
|
4548
|
+
* 保存成功即收窗。写盘已经完成,草稿没有值得留下的东西——这与
|
|
4549
|
+
* 团队编辑器的「成功即 `setTeamDraft(null)`」是同一条约定;留窗
|
|
4550
|
+
* 是失焦自动保存时代的习惯,那时窗口必须开着等下一次失焦。
|
|
4551
|
+
* 失败路径不动:弹窗留在原地,错误贴在窗内。
|
|
4552
|
+
*/
|
|
4553
|
+
putDraft(null);
|
|
4419
4554
|
await load();
|
|
4420
4555
|
await loadRoster();
|
|
4421
4556
|
} catch (failure) {
|
|
@@ -4430,7 +4565,13 @@ window.__ModuleLoader__.load({
|
|
|
4430
4565
|
}
|
|
4431
4566
|
}
|
|
4432
4567
|
|
|
4433
|
-
/**
|
|
4568
|
+
/**
|
|
4569
|
+
* 编辑器「保存」按钮的唯一入口。
|
|
4570
|
+
*
|
|
4571
|
+
* 曾经失焦也会走到这里(blur → commit),如今唯一的调用方是按钮:
|
|
4572
|
+
* 写盘必须是人按下去的那个动作。`save` 自己做去重——草稿与上一次
|
|
4573
|
+
* 保存一致时不发请求——所以连点两下、或者没改就点,都是空操作。
|
|
4574
|
+
*/
|
|
4434
4575
|
const commit = React.useCallback(() => {
|
|
4435
4576
|
const current = draftRef.current;
|
|
4436
4577
|
if (current === null || current.loading === true) return;
|
|
@@ -4443,8 +4584,8 @@ window.__ModuleLoader__.load({
|
|
|
4443
4584
|
});
|
|
4444
4585
|
}, []);
|
|
4445
4586
|
|
|
4587
|
+
/* 关窗即放弃:没有点「保存」的改动一个字都不落盘。 */
|
|
4446
4588
|
function closeRow() {
|
|
4447
|
-
commit();
|
|
4448
4589
|
showRow(null);
|
|
4449
4590
|
putDraft(null);
|
|
4450
4591
|
setDialog(null);
|
|
@@ -4463,8 +4604,8 @@ window.__ModuleLoader__.load({
|
|
|
4463
4604
|
const id = item.id;
|
|
4464
4605
|
setPeek({ id, name: item.name ?? id, loading: true, text: '', error: '' });
|
|
4465
4606
|
try {
|
|
4466
|
-
//
|
|
4467
|
-
//
|
|
4607
|
+
// A save pressed moments ago may still be in flight; reading first
|
|
4608
|
+
// would answer with the previous version of the file.
|
|
4468
4609
|
if (savingRef.current !== null) await savingRef.current;
|
|
4469
4610
|
const payload = await post({ op: 'get', id, assembled: true });
|
|
4470
4611
|
setPeek((prev) =>
|
|
@@ -4486,7 +4627,6 @@ window.__ModuleLoader__.load({
|
|
|
4486
4627
|
closeRow();
|
|
4487
4628
|
return;
|
|
4488
4629
|
}
|
|
4489
|
-
commit();
|
|
4490
4630
|
setDialog(null);
|
|
4491
4631
|
setNote(null);
|
|
4492
4632
|
showRow(item.id);
|
|
@@ -4534,7 +4674,6 @@ window.__ModuleLoader__.load({
|
|
|
4534
4674
|
closeRow();
|
|
4535
4675
|
return;
|
|
4536
4676
|
}
|
|
4537
|
-
commit();
|
|
4538
4677
|
setDialog(null);
|
|
4539
4678
|
setNote(null);
|
|
4540
4679
|
showRow(NEW);
|
|
@@ -4561,7 +4700,6 @@ window.__ModuleLoader__.load({
|
|
|
4561
4700
|
closeRow();
|
|
4562
4701
|
return;
|
|
4563
4702
|
}
|
|
4564
|
-
commit();
|
|
4565
4703
|
showRow(SKELETON);
|
|
4566
4704
|
setNote(null);
|
|
4567
4705
|
putSkeletonDraft({ text: '', slots: 0, nameSlots: 0, problems: [], rev: null, loading: true });
|
|
@@ -5472,7 +5610,11 @@ window.__ModuleLoader__.load({
|
|
|
5472
5610
|
onImportClick: () => fileInput.current?.click(),
|
|
5473
5611
|
onDownloadTemplate: downloadTemplate,
|
|
5474
5612
|
onPatchDraft: patchDraft,
|
|
5475
|
-
|
|
5613
|
+
// 显式保存:只有按钮会写盘。关窗(onCloseDraft)丢掉未保存的改动。
|
|
5614
|
+
onSave: commit,
|
|
5615
|
+
dirty: draft !== null && draft.savedKey !== undefined && draftKeyOf(draft) !== draft.savedKey,
|
|
5616
|
+
// Closing the editor window: drop the draft without writing.
|
|
5617
|
+
onCloseDraft: closeRow,
|
|
5476
5618
|
onOpenSkeleton: openSkeleton,
|
|
5477
5619
|
skeletonDraft,
|
|
5478
5620
|
onPatchSkeleton: patchSkeletonDraft,
|