dsh-compact-relay 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -3
- package/README.en.md +18 -1
- package/README.md +20 -3
- package/ROADMAP.md +9 -2
- package/lib/actions/better-compact.js +38 -1
- package/lib/actions/compact-run.js +29 -3
- package/lib/actions/compress-hint.js +21 -1
- package/lib/actions/diagnostics.js +130 -0
- package/lib/actions/handoff.js +90 -11
- package/lib/actions/pin.js +6 -23
- package/lib/actions/state.js +47 -5
- package/lib/adapters/http.js +70 -7
- package/lib/adapters/prompt.js +71 -10
- package/lib/adapters/tool.js +9 -4
- package/lib/client.js +485 -13
- package/lib/core/compress-cue.js +48 -16
- package/lib/core/handoff-doc.js +4 -0
- package/lib/core/handoff-edit.js +86 -0
- package/lib/core/handoff-facts.js +17 -2
- package/lib/core/handoff-spec.js +18 -0
- package/lib/core/ledger.js +176 -0
- package/lib/core/native-line.js +65 -0
- package/lib/core/pin-anchors.js +93 -0
- package/lib/core/pin-suggest.js +129 -0
- package/lib/core/policy.js +16 -0
- package/lib/dsh/diag-counters.js +204 -0
- package/lib/dsh/diagnostics.js +527 -0
- package/lib/dsh/handoff-writer.js +7 -5
- package/lib/dsh/ledger.js +108 -0
- package/lib/dsh/native-compaction.js +128 -0
- package/lib/dsh/pressure.js +62 -10
- package/lib/dsh/relay.js +23 -6
- package/lib/dsh/session-view.js +32 -4
- package/lib/dsh/sessions.js +1 -1
- package/lib/index.js +6 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -818,6 +818,14 @@ window.__ModuleLoader__.load({
|
|
|
818
818
|
const { sessionId, waitAndOpen } = props;
|
|
819
819
|
const hasSession = typeof sessionId === 'string' && sessionId.length > 0;
|
|
820
820
|
const [state, setState] = useState({ advice: 'none', pressure: null, window: null, summaries: 0, colors: null, compactRange: undefined, busy: false, pins: { count: 0, seqs: [], marks: [] }, compressHint: null, native: null });
|
|
821
|
+
// 0.4 P2-3:原生自动压缩的「有效线」——优先真实触发线(thresholdTokens/window),
|
|
822
|
+
// 老宿主/算不出时回落 thresholdRatio(窗口比例)。
|
|
823
|
+
const effectiveNativeRatio = (native) => {
|
|
824
|
+
if (native === null || native === undefined || native.autoEnabled === false) return null;
|
|
825
|
+
if (typeof native.ratio === 'number' && Number.isFinite(native.ratio) && native.ratio > 0 && native.ratio < 1) return native.ratio;
|
|
826
|
+
if (typeof native.thresholdRatio === 'number' && Number.isFinite(native.thresholdRatio) && native.thresholdRatio > 0) return native.thresholdRatio;
|
|
827
|
+
return null;
|
|
828
|
+
};
|
|
821
829
|
const [open, setOpen] = useState(false);
|
|
822
830
|
const [busy, setBusy] = useState(false);
|
|
823
831
|
const [hover, setHover] = useState(false);
|
|
@@ -877,6 +885,8 @@ window.__ModuleLoader__.load({
|
|
|
877
885
|
}, []);
|
|
878
886
|
// A1b:交接预览(九段草稿 + 就绪清单),只读不落盘。
|
|
879
887
|
const [handoffPreview, setHandoffPreview] = useState(null);
|
|
888
|
+
/** 0.4 P2-1:交接预览里「约束 / 下一步」两段的编辑值(键=段标题;空值回落原文)。 */
|
|
889
|
+
const [handoffEditValues, setHandoffEditValues] = useState({});
|
|
880
890
|
const handoffPreviewRef = useRef(null);
|
|
881
891
|
const handoffPreviewAbortRef = useRef(null);
|
|
882
892
|
const closeHandoffPreview = useCallback(() => {
|
|
@@ -936,13 +946,20 @@ window.__ModuleLoader__.load({
|
|
|
936
946
|
: '/api/compact-relay-state';
|
|
937
947
|
|
|
938
948
|
const applierRef = useRef(() => {});
|
|
949
|
+
// 0.3.7:最后一帧(SSE 帧或轮询响应)的到达时间。远程/代理链路可能让 SSE
|
|
950
|
+
// 「onopen 已连上、之后却一帧都不来」地静默挂住,表现为压缩后界面不刷新
|
|
951
|
+
// (Win 远程客户端实测:手动刷新才变)。帧静默超时后由兜底轮询补拉。
|
|
952
|
+
const lastFrameAtRef = useRef(0);
|
|
939
953
|
const fetchState = useCallback(async () => {
|
|
940
954
|
if (!hasSession) return;
|
|
941
955
|
try {
|
|
942
956
|
const res = await fetch(url, { cache: 'no-store' });
|
|
943
957
|
if (!res.ok) return;
|
|
944
958
|
const data = await res.json();
|
|
945
|
-
if (data && typeof data === 'object')
|
|
959
|
+
if (data && typeof data === 'object') {
|
|
960
|
+
lastFrameAtRef.current = Date.now();
|
|
961
|
+
applierRef.current(data);
|
|
962
|
+
}
|
|
946
963
|
} catch {
|
|
947
964
|
/* host 不在就保持上一次状态 */
|
|
948
965
|
}
|
|
@@ -999,6 +1016,28 @@ window.__ModuleLoader__.load({
|
|
|
999
1016
|
typeof data.native.thresholdRatio === 'number' && Number.isFinite(data.native.thresholdRatio)
|
|
1000
1017
|
? Math.max(0, Math.min(1, data.native.thresholdRatio))
|
|
1001
1018
|
: 0.8,
|
|
1019
|
+
// 0.4 P2-3:真实触发线(thresholdTokens/window);宿主算不出时为 null。
|
|
1020
|
+
ratio:
|
|
1021
|
+
typeof data.native.ratio === 'number' && Number.isFinite(data.native.ratio)
|
|
1022
|
+
? Math.max(0, Math.min(1, data.native.ratio))
|
|
1023
|
+
: null,
|
|
1024
|
+
thresholdTokens:
|
|
1025
|
+
typeof data.native.thresholdTokens === 'number' && Number.isFinite(data.native.thresholdTokens)
|
|
1026
|
+
? data.native.thresholdTokens
|
|
1027
|
+
: null,
|
|
1028
|
+
contextWindow:
|
|
1029
|
+
typeof data.native.contextWindow === 'number' && Number.isFinite(data.native.contextWindow)
|
|
1030
|
+
? data.native.contextWindow
|
|
1031
|
+
: null,
|
|
1032
|
+
headroomTokens:
|
|
1033
|
+
typeof data.native.headroomTokens === 'number' && Number.isFinite(data.native.headroomTokens)
|
|
1034
|
+
? data.native.headroomTokens
|
|
1035
|
+
: null,
|
|
1036
|
+
overridden: data.native.overridden === true,
|
|
1037
|
+
route:
|
|
1038
|
+
data.native.route && typeof data.native.route === 'object' && typeof data.native.route.model === 'string'
|
|
1039
|
+
? { provider: String(data.native.route.provider ?? ''), model: data.native.route.model }
|
|
1040
|
+
: null,
|
|
1002
1041
|
}
|
|
1003
1042
|
: null,
|
|
1004
1043
|
colors: data.colors && typeof data.colors === 'object' ? data.colors : null,
|
|
@@ -1040,8 +1079,12 @@ window.__ModuleLoader__.load({
|
|
|
1040
1079
|
if (dead) return;
|
|
1041
1080
|
streamLiveRef.current = true;
|
|
1042
1081
|
setStreamLive(true);
|
|
1082
|
+
lastFrameAtRef.current = Date.now();
|
|
1083
|
+
// 0.3.7:刚连上先拉一次,补齐「断开期间错过的帧」(重连场景)。
|
|
1084
|
+
void fetchState();
|
|
1043
1085
|
};
|
|
1044
1086
|
source.onmessage = (event) => {
|
|
1087
|
+
lastFrameAtRef.current = Date.now();
|
|
1045
1088
|
try {
|
|
1046
1089
|
applyState(JSON.parse(event.data));
|
|
1047
1090
|
} catch {
|
|
@@ -1073,14 +1116,17 @@ window.__ModuleLoader__.load({
|
|
|
1073
1116
|
// 兜底轮询:
|
|
1074
1117
|
// - 还没就绪(会话刚打开 / 没有 agent)→ 1.5s 快轮询(修「重启后停在上下文未知」);
|
|
1075
1118
|
// - 已就绪但 SSE 没连上 → 5s 轮询兜底;
|
|
1076
|
-
// - 已就绪且 SSE 活着 →
|
|
1119
|
+
// - 已就绪且 SSE 活着 → 5s 计时器只检查「帧是否静默超时」;
|
|
1120
|
+
// - 0.3.7:SSE 活着但 30s 没来帧(远程/代理链路静默挂住)→ 补拉一次,
|
|
1121
|
+
// 修「压缩后远程客户端不刷新、要手动刷新才变」。
|
|
1077
1122
|
useEffect(() => {
|
|
1078
1123
|
if (!hasSession) return;
|
|
1079
1124
|
let timer;
|
|
1080
1125
|
let dead = false;
|
|
1081
1126
|
const tick = () => {
|
|
1082
1127
|
if (dead) return;
|
|
1083
|
-
|
|
1128
|
+
const sseSilent = streamLiveRef.current && Date.now() - lastFrameAtRef.current > 30000;
|
|
1129
|
+
if (!readyRef.current || !streamLiveRef.current || sseSilent) void fetchState();
|
|
1084
1130
|
timer = setTimeout(tick, readyRef.current ? 5000 : 1500);
|
|
1085
1131
|
};
|
|
1086
1132
|
tick();
|
|
@@ -1090,6 +1136,24 @@ window.__ModuleLoader__.load({
|
|
|
1090
1136
|
};
|
|
1091
1137
|
}, [fetchState, hasSession]);
|
|
1092
1138
|
|
|
1139
|
+
// 0.3.7:窗口重新可见 / 重新获得焦点 / 网络恢复时拉一次——覆盖「页面被挂起后
|
|
1140
|
+
// 计时器被节流、SSE 断流」的场景(远程客户端切走再切回来也会走到这里)。
|
|
1141
|
+
useEffect(() => {
|
|
1142
|
+
if (!hasSession) return;
|
|
1143
|
+
const refresh = () => {
|
|
1144
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
|
1145
|
+
void fetchState();
|
|
1146
|
+
};
|
|
1147
|
+
document.addEventListener('visibilitychange', refresh);
|
|
1148
|
+
window.addEventListener('focus', refresh);
|
|
1149
|
+
window.addEventListener('online', refresh);
|
|
1150
|
+
return () => {
|
|
1151
|
+
document.removeEventListener('visibilitychange', refresh);
|
|
1152
|
+
window.removeEventListener('focus', refresh);
|
|
1153
|
+
window.removeEventListener('online', refresh);
|
|
1154
|
+
};
|
|
1155
|
+
}, [fetchState, hasSession]);
|
|
1156
|
+
|
|
1093
1157
|
|
|
1094
1158
|
// 点外面 / Esc 关闭菜单、压缩确认卡与交接单面板。
|
|
1095
1159
|
useEffect(() => {
|
|
@@ -1188,7 +1252,7 @@ window.__ModuleLoader__.load({
|
|
|
1188
1252
|
// DSH 原生自动压缩线(报警最高阈值):开着时在环上画一个「径向刻度」(不是沿环的弧),
|
|
1189
1253
|
// 用紫色 + 主题底色描边——刻意不用五级配色里的颜色,保证在任何分段颜色上都看得清。
|
|
1190
1254
|
// 越过原生线后:整环红色闪烁(作者 2026-10-20 反馈)。
|
|
1191
|
-
const nativeAuto = state.native
|
|
1255
|
+
const nativeAuto = effectiveNativeRatio(state.native);
|
|
1192
1256
|
const nativeValid = nativeAuto !== null && nativeAuto > 0.02 && nativeAuto < 1;
|
|
1193
1257
|
const nativeCrossed = nativeValid && ratio > nativeAuto;
|
|
1194
1258
|
// 断点缺口:0.9 SVG 单位(≈6.4°),折算成比例;段太短时缺口会吃掉整段,就跳过该段。
|
|
@@ -1310,11 +1374,11 @@ window.__ModuleLoader__.load({
|
|
|
1310
1374
|
// 配合指示灯呼吸光晕,解决「指示灯太安静、新用户不知道这里有个菜单」。
|
|
1311
1375
|
// 设置页「再看一次初次引导」按钮通过 window 事件重播(两个组件同页,用事件解耦)。
|
|
1312
1376
|
const onboardingText = useCallback(() => {
|
|
1313
|
-
const nativeRatio = state.native
|
|
1377
|
+
const nativeRatio = effectiveNativeRatio(state.native);
|
|
1314
1378
|
const nativePart =
|
|
1315
1379
|
nativeRatio === null
|
|
1316
1380
|
? 'DSH 原生自动压缩已关闭'
|
|
1317
|
-
: `DSH 原生自动压缩线在 ${(nativeRatio * 100).toFixed(
|
|
1381
|
+
: `DSH 原生自动压缩线在 ${(nativeRatio * 100).toFixed(1)}%,到那之前先压缩或交接更稳`;
|
|
1318
1382
|
return `这是上下文指示灯:点它压缩旧会话、锁定重要消息、交接新会话。${nativePart}。环上的小缺口是我判断「该压缩」的位置(模型判断或到线自动出现)。重要的结论/路径先锁定,压缩时才不会被压掉。`;
|
|
1319
1383
|
}, [state.native]);
|
|
1320
1384
|
|
|
@@ -1437,6 +1501,7 @@ window.__ModuleLoader__.load({
|
|
|
1437
1501
|
setPreview(null);
|
|
1438
1502
|
closeDocs();
|
|
1439
1503
|
closeDiff();
|
|
1504
|
+
setHandoffEditValues({});
|
|
1440
1505
|
setHandoffPreview({ loading: true });
|
|
1441
1506
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1442
1507
|
handoffPreviewAbortRef.current = controller;
|
|
@@ -1626,7 +1691,7 @@ window.__ModuleLoader__.load({
|
|
|
1626
1691
|
}, [effectiveBusy, sessionId]);
|
|
1627
1692
|
|
|
1628
1693
|
const run = useCallback(
|
|
1629
|
-
async (action) => {
|
|
1694
|
+
async (action, extra) => {
|
|
1630
1695
|
// A3:确认必须带预览时拿到的 planId,宿主重算不一致会以 changed 拒绝。
|
|
1631
1696
|
const expected =
|
|
1632
1697
|
action === 'compact' && preview !== null && preview.loading !== true && typeof preview.data?.planId === 'string'
|
|
@@ -1651,6 +1716,8 @@ window.__ModuleLoader__.load({
|
|
|
1651
1716
|
body: JSON.stringify({
|
|
1652
1717
|
action,
|
|
1653
1718
|
...(expected === undefined ? {} : { expected }),
|
|
1719
|
+
// 0.4 P2-1:`extra.edits` = 只含用户真正改过的「约束/下一步」两段。
|
|
1720
|
+
...(extra !== undefined && extra !== null && typeof extra === 'object' ? extra : {}),
|
|
1654
1721
|
...(typeof sessionId === 'string' && sessionId.length > 0 ? { sessionId } : {}),
|
|
1655
1722
|
}),
|
|
1656
1723
|
...(controller === null ? {} : { signal: controller.signal }),
|
|
@@ -1690,10 +1757,17 @@ window.__ModuleLoader__.load({
|
|
|
1690
1757
|
state.compressHint !== null && typeof state.compressHint.ratio === 'number'
|
|
1691
1758
|
? `压缩判断点 ${(state.compressHint.ratio * 100).toFixed(0)}%`
|
|
1692
1759
|
: '';
|
|
1760
|
+
const nativeEffective = effectiveNativeRatio(state.native);
|
|
1761
|
+
const nativeDetail =
|
|
1762
|
+
state.native !== null && state.native.thresholdTokens !== null && state.native.contextWindow !== null
|
|
1763
|
+
? `(${fmt(state.native.thresholdTokens)} / ${fmt(state.native.contextWindow)}${
|
|
1764
|
+
state.native.route !== null ? `,${state.native.route.provider}/${state.native.route.model}` : ''
|
|
1765
|
+
}${state.native.overridden ? ',按路由覆盖' : ''})`
|
|
1766
|
+
: '';
|
|
1693
1767
|
const nativeLineText =
|
|
1694
|
-
|
|
1695
|
-
? `原生自动压缩线 ${(
|
|
1696
|
-
ratio >
|
|
1768
|
+
nativeEffective !== null
|
|
1769
|
+
? `原生自动压缩线 ${(nativeEffective * 100).toFixed(1)}%${nativeDetail}${
|
|
1770
|
+
ratio > nativeEffective ? '(已越过:整环红色闪烁)' : ''
|
|
1697
1771
|
}`
|
|
1698
1772
|
: 'DSH 原生自动压缩已关闭';
|
|
1699
1773
|
const pressureKnown = pressureValue !== null;
|
|
@@ -2030,6 +2104,12 @@ window.__ModuleLoader__.load({
|
|
|
2030
2104
|
const previewRange = preview?.data?.preview ?? null;
|
|
2031
2105
|
// 0.3.1:范围太小时预览阶段就拦下(服务端已按 800 tokens 判定),不让用户白确认一次。
|
|
2032
2106
|
const previewTooSmall = previewRange !== null && previewRange.tooSmall === true;
|
|
2107
|
+
// 0.4 P1-1/P1-2:建议锁定候选 + pin 命中数(宿主已算好,客户端只渲染;不自动锁)。
|
|
2108
|
+
const pinSuggest =
|
|
2109
|
+
previewRange !== null && previewRange.pinSuggest !== null && typeof previewRange.pinSuggest === 'object' && Array.isArray(previewRange.pinSuggest.items)
|
|
2110
|
+
? previewRange.pinSuggest
|
|
2111
|
+
: null;
|
|
2112
|
+
const pinHits = previewRange !== null && previewRange.pinHits !== null && typeof previewRange.pinHits === 'object' ? previewRange.pinHits : null;
|
|
2033
2113
|
const confirmPanel =
|
|
2034
2114
|
preview === null
|
|
2035
2115
|
? null
|
|
@@ -2047,6 +2127,8 @@ window.__ModuleLoader__.load({
|
|
|
2047
2127
|
maxWidth: 'calc(100vw - 16px)',
|
|
2048
2128
|
padding: '10px 12px',
|
|
2049
2129
|
borderRadius: 12,
|
|
2130
|
+
maxHeight: 'min(70vh, 440px)',
|
|
2131
|
+
overflowY: 'auto',
|
|
2050
2132
|
...panelChrome(theme),
|
|
2051
2133
|
color: theme.panelFg,
|
|
2052
2134
|
zIndex: 42,
|
|
@@ -2077,6 +2159,45 @@ window.__ModuleLoader__.load({
|
|
|
2077
2159
|
h('br'),
|
|
2078
2160
|
'只估算范围和容量;摘要内容确认后才生成,不在这里展示。压缩后旧会话将替换为摘要,无法还原为原文。',
|
|
2079
2161
|
),
|
|
2162
|
+
pinSuggest !== null && pinSuggest.items.length > 0
|
|
2163
|
+
? h(
|
|
2164
|
+
'div',
|
|
2165
|
+
{
|
|
2166
|
+
'data-cr-pin-suggest': '1',
|
|
2167
|
+
style: {
|
|
2168
|
+
display: 'flex',
|
|
2169
|
+
flexDirection: 'column',
|
|
2170
|
+
gap: 2,
|
|
2171
|
+
padding: '6px 8px',
|
|
2172
|
+
borderRadius: 8,
|
|
2173
|
+
border: `1px solid ${theme.panelBorder}`,
|
|
2174
|
+
background: theme.hover,
|
|
2175
|
+
},
|
|
2176
|
+
},
|
|
2177
|
+
h('div', { style: { fontSize: 11, fontWeight: 700, color: theme.panelFg } }, `建议锁定 ${pinSuggest.items.length} 条(先 /pin 再压)`),
|
|
2178
|
+
...pinSuggest.items.map((item) =>
|
|
2179
|
+
h(
|
|
2180
|
+
'div',
|
|
2181
|
+
{ key: `pin-suggest-${item.seq}`, style: { fontSize: 11, lineHeight: '16px', color: theme.subText } },
|
|
2182
|
+
`${item.role} · ${item.reason}:${item.snippet}`,
|
|
2183
|
+
),
|
|
2184
|
+
),
|
|
2185
|
+
h(
|
|
2186
|
+
'div',
|
|
2187
|
+
{ style: { fontSize: 10, lineHeight: '14px', color: theme.subText } },
|
|
2188
|
+
'只建议、不自动锁定;锁定后这些消息会原样保留(/pin list 可按序号挑)。',
|
|
2189
|
+
),
|
|
2190
|
+
)
|
|
2191
|
+
: null,
|
|
2192
|
+
pinHits !== null && pinHits.total > 0
|
|
2193
|
+
? h(
|
|
2194
|
+
'div',
|
|
2195
|
+
{ 'data-cr-pin-hits': '1', style: { fontSize: 11, lineHeight: '16px', color: theme.subText } },
|
|
2196
|
+
pinHits.inRange > 0
|
|
2197
|
+
? `pin 命中 ${pinHits.inRange} 条:本次范围里被锁定的消息已跳过(会话共 ${pinHits.total} 条锁定)。`
|
|
2198
|
+
: `pin 命中 0 条:本次范围内没有锁定消息(会话共 ${pinHits.total} 条锁定,均在范围外)。`,
|
|
2199
|
+
)
|
|
2200
|
+
: null,
|
|
2080
2201
|
h(
|
|
2081
2202
|
'div',
|
|
2082
2203
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
@@ -2214,6 +2335,46 @@ window.__ModuleLoader__.load({
|
|
|
2214
2335
|
: handoffDraft,
|
|
2215
2336
|
),
|
|
2216
2337
|
),
|
|
2338
|
+
// 0.4 P2-1:只允许编辑「约束」「下一步」两段;其余段由模型/程序生成,UI 不给入口。
|
|
2339
|
+
handoffData !== null && typeof handoffData === 'object' && handoffData.editable !== null && typeof handoffData.editable === 'object'
|
|
2340
|
+
? h(
|
|
2341
|
+
'div',
|
|
2342
|
+
{ 'data-cr-handoff-editable': '1', style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2343
|
+
h('div', { style: { fontSize: 11, fontWeight: 600 } }, '可编辑两段(改完点「确认交接」才生效)'),
|
|
2344
|
+
...(Array.isArray(handoffData.editable.titles) ? handoffData.editable.titles : []).map((title) => {
|
|
2345
|
+
const original = typeof handoffData.editable.sections?.[title] === 'string' ? handoffData.editable.sections[title] : '';
|
|
2346
|
+
return h(
|
|
2347
|
+
'label',
|
|
2348
|
+
{ key: `handoff-edit-${title}`, style: { display: 'flex', flexDirection: 'column', gap: 3, fontSize: 11 } },
|
|
2349
|
+
h('span', { style: { color: theme.subText } }, `${title}(留空 = 使用原文)`),
|
|
2350
|
+
h('textarea', {
|
|
2351
|
+
'data-cr-handoff-edit': title,
|
|
2352
|
+
value: typeof handoffEditValues[title] === 'string' ? handoffEditValues[title] : original,
|
|
2353
|
+
rows: 3,
|
|
2354
|
+
maxLength: Number.isInteger(handoffData.editable.maxChars) ? handoffData.editable.maxChars : 4000,
|
|
2355
|
+
onChange: (event) => {
|
|
2356
|
+
const value = event.target.value;
|
|
2357
|
+
setHandoffEditValues((previous) => ({ ...previous, [title]: value }));
|
|
2358
|
+
},
|
|
2359
|
+
style: {
|
|
2360
|
+
width: '100%',
|
|
2361
|
+
boxSizing: 'border-box',
|
|
2362
|
+
resize: 'vertical',
|
|
2363
|
+
fontFamily: 'inherit',
|
|
2364
|
+
fontSize: 11,
|
|
2365
|
+
lineHeight: '16px',
|
|
2366
|
+
color: 'inherit',
|
|
2367
|
+
background: 'transparent',
|
|
2368
|
+
border: `1px solid ${theme.panelBorder}`,
|
|
2369
|
+
borderRadius: 6,
|
|
2370
|
+
padding: '4px 6px',
|
|
2371
|
+
},
|
|
2372
|
+
}),
|
|
2373
|
+
);
|
|
2374
|
+
}),
|
|
2375
|
+
h('div', { style: { fontSize: 10, lineHeight: '14px', color: theme.subText } }, handoffData.editable.note ?? '程序生成的段不可改。'),
|
|
2376
|
+
)
|
|
2377
|
+
: null,
|
|
2217
2378
|
h('div', { style: { fontSize: 11, color: theme.subText } }, '确认后会先压缩旧会话,再按压缩后的历史生成最终交接单;预览不落盘。'),
|
|
2218
2379
|
h(
|
|
2219
2380
|
'div',
|
|
@@ -2234,8 +2395,18 @@ window.__ModuleLoader__.load({
|
|
|
2234
2395
|
type: 'button',
|
|
2235
2396
|
'data-cr-handoff-run': '1',
|
|
2236
2397
|
onClick: () => {
|
|
2398
|
+
// P2-1:只把「用户真的改过、且非空」的两段发给宿主;空值/未改 = 用重新生成的原文。
|
|
2399
|
+
const editable = handoffData !== null && typeof handoffData === 'object' ? handoffData.editable : null;
|
|
2400
|
+
const edits = {};
|
|
2401
|
+
if (editable !== null && typeof editable === 'object' && Array.isArray(editable.titles)) {
|
|
2402
|
+
for (const title of editable.titles) {
|
|
2403
|
+
const original = typeof editable.sections?.[title] === 'string' ? editable.sections[title].trim() : '';
|
|
2404
|
+
const current = typeof handoffEditValues[title] === 'string' ? handoffEditValues[title].trim() : '';
|
|
2405
|
+
if (current.length > 0 && current !== original) edits[title] = current;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2237
2408
|
closeHandoffPreview();
|
|
2238
|
-
void run('handoff');
|
|
2409
|
+
void run('handoff', Object.keys(edits).length > 0 ? { edits } : undefined);
|
|
2239
2410
|
},
|
|
2240
2411
|
style: {
|
|
2241
2412
|
border: 'none',
|
|
@@ -2837,6 +3008,8 @@ window.__ModuleLoader__.load({
|
|
|
2837
3008
|
const [native, setNative] = useState(null);
|
|
2838
3009
|
/** UI-002:设置文件路径(「关于与危险操作」区块展示)。 */
|
|
2839
3010
|
const [settingsPath, setSettingsPath] = useState('');
|
|
3011
|
+
/** 0.4 P1-3:轻账本累计数字(独立账本文件算出来的;null = 没有数据/读不到,显示引导文案)。 */
|
|
3012
|
+
const [ledger, setLedger] = useState(null);
|
|
2840
3013
|
const [confirmNative, setConfirmNative] = useState(null); // null | 'disable' | 'enable'
|
|
2841
3014
|
const [nativeBusy, setNativeBusy] = useState(false);
|
|
2842
3015
|
/** UI-003:结果提示就地显示在开关旁(以前只写页面底部,用户看不到)。 */
|
|
@@ -2844,6 +3017,32 @@ window.__ModuleLoader__.load({
|
|
|
2844
3017
|
/** 作者 2026-10-20:设置页「再看一次初次引导」的就地提示。 */
|
|
2845
3018
|
const [onboardingNotice, setOnboardingNotice] = useState(null);
|
|
2846
3019
|
const nativeConfirmRef = useRef(null);
|
|
3020
|
+
/**
|
|
3021
|
+
* 0.4 P0-2:诊断信息(只在本机生成;可预览/复制,插件不会把它发到任何地方)。
|
|
3022
|
+
* diagData 是端点返回的 {schema,version,generatedAt,markdown,json,issueUrl}。
|
|
3023
|
+
*/
|
|
3024
|
+
const [diagData, setDiagData] = useState(null);
|
|
3025
|
+
const [diagLoading, setDiagLoading] = useState(false);
|
|
3026
|
+
const [diagOpen, setDiagOpen] = useState(false);
|
|
3027
|
+
/** 弹层顶部说明卡:只在本会话第一次生成时出现一次。 */
|
|
3028
|
+
const [diagIntroSeen, setDiagIntroSeen] = useState(false);
|
|
3029
|
+
const [diagStatus, setDiagStatus] = useState(null);
|
|
3030
|
+
/** 复制/打开结果的小 toast 计时器(A4:成功与失败都要有 toast 反馈)。 */
|
|
3031
|
+
const diagToastTimer = useRef(null);
|
|
3032
|
+
|
|
3033
|
+
/** 就地提示 + 4.2 秒后自动消失的 toast。 */
|
|
3034
|
+
const flashDiagStatus = useCallback((next) => {
|
|
3035
|
+
setDiagStatus(next);
|
|
3036
|
+
if (diagToastTimer.current !== null) clearTimeout(diagToastTimer.current);
|
|
3037
|
+
diagToastTimer.current = setTimeout(() => setDiagStatus(null), 4200);
|
|
3038
|
+
}, []);
|
|
3039
|
+
|
|
3040
|
+
useEffect(
|
|
3041
|
+
() => () => {
|
|
3042
|
+
if (diagToastTimer.current !== null) clearTimeout(diagToastTimer.current);
|
|
3043
|
+
},
|
|
3044
|
+
[],
|
|
3045
|
+
);
|
|
2847
3046
|
|
|
2848
3047
|
/** 把服务端返回的 view 落到表单:同时更新「已保存」快照和设置文件错误。 */
|
|
2849
3048
|
const applyLoaded = useCallback((data) => {
|
|
@@ -2853,6 +3052,7 @@ window.__ModuleLoader__.load({
|
|
|
2853
3052
|
setNative(data.native && typeof data.native === 'object' ? data.native : null);
|
|
2854
3053
|
if (typeof data.version === 'string') setVersion(data.version);
|
|
2855
3054
|
if (typeof data.path === 'string') setSettingsPath(data.path);
|
|
3055
|
+
setLedger(data.ledger !== null && typeof data.ledger === 'object' ? data.ledger : null);
|
|
2856
3056
|
}, []);
|
|
2857
3057
|
|
|
2858
3058
|
const load = useCallback(async () => {
|
|
@@ -2957,6 +3157,77 @@ window.__ModuleLoader__.load({
|
|
|
2957
3157
|
}
|
|
2958
3158
|
}, [confirmNative]);
|
|
2959
3159
|
|
|
3160
|
+
/**
|
|
3161
|
+
* 0.4 P0-2:生成诊断信息。
|
|
3162
|
+
*
|
|
3163
|
+
* 唯一的网络调用是同源只读 GET(`/api/compact-relay-diagnostics`,仅此一次);
|
|
3164
|
+
* 产物由宿主生成(白名单 + 已脱敏),这里只负责展示/复制。
|
|
3165
|
+
*/
|
|
3166
|
+
const generateDiagnostics = useCallback(
|
|
3167
|
+
async ({ openPreview = true } = {}) => {
|
|
3168
|
+
if (diagLoading) return null;
|
|
3169
|
+
setDiagLoading(true);
|
|
3170
|
+
setDiagStatus(null);
|
|
3171
|
+
try {
|
|
3172
|
+
const res = await fetch('/api/compact-relay-diagnostics', { cache: 'no-store' });
|
|
3173
|
+
const data = await res.json().catch(() => ({}));
|
|
3174
|
+
if (!res.ok || data.ok === false) throw new Error(data && data.message ? String(data.message) : '生成失败');
|
|
3175
|
+
setDiagData(data);
|
|
3176
|
+
if (openPreview) setDiagOpen(true);
|
|
3177
|
+
return data;
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
setDiagStatus({ error: true, text: `生成失败:${error && error.message ? error.message : '未知错误'}` });
|
|
3180
|
+
return null;
|
|
3181
|
+
} finally {
|
|
3182
|
+
setDiagLoading(false);
|
|
3183
|
+
}
|
|
3184
|
+
},
|
|
3185
|
+
[diagLoading],
|
|
3186
|
+
);
|
|
3187
|
+
|
|
3188
|
+
/** 「打开问题报告页」:没生成就先生成,再打开宿主拼好的短字段 URL(不碰诊断正文)。 */
|
|
3189
|
+
const openIssueReport = useCallback(async () => {
|
|
3190
|
+
let data = diagData;
|
|
3191
|
+
if (data === null || typeof data.issueUrl !== 'string') data = await generateDiagnostics({ openPreview: false });
|
|
3192
|
+
if (data === null || typeof data.issueUrl !== 'string') return;
|
|
3193
|
+
try {
|
|
3194
|
+
window.open(data.issueUrl, '_blank', 'noopener');
|
|
3195
|
+
} catch {
|
|
3196
|
+
flashDiagStatus({ error: true, text: '打开问题报告页失败:新窗口被浏览器拦截了' });
|
|
3197
|
+
}
|
|
3198
|
+
}, [diagData, generateDiagnostics, flashDiagStatus]);
|
|
3199
|
+
|
|
3200
|
+
/** 复制 Markdown / JSON:统一走 navigator.clipboard?.writeText,失败就地提示。 */
|
|
3201
|
+
const copyDiagnostics = useCallback(
|
|
3202
|
+
(format) => {
|
|
3203
|
+
if (diagData === null) {
|
|
3204
|
+
flashDiagStatus({ error: true, text: '还没有可复制的诊断信息,请先生成' });
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const text =
|
|
3208
|
+
format === 'json' ? JSON.stringify(diagData.json, null, 2) : typeof diagData.markdown === 'string' ? diagData.markdown : '';
|
|
3209
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
3210
|
+
flashDiagStatus({ error: true, text: '还没有可复制的诊断信息,请先生成' });
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const done = () => flashDiagStatus({ text: '已复制诊断信息(脱敏)' });
|
|
3214
|
+
const failed = () => flashDiagStatus({ error: true, text: '复制失败:浏览器不允许访问剪贴板,可手动选中预览文本' });
|
|
3215
|
+
try {
|
|
3216
|
+
const written = navigator.clipboard?.writeText(text);
|
|
3217
|
+
if (written !== undefined && typeof written.then === 'function') written.then(done, failed);
|
|
3218
|
+
else done();
|
|
3219
|
+
} catch {
|
|
3220
|
+
failed();
|
|
3221
|
+
}
|
|
3222
|
+
},
|
|
3223
|
+
[diagData, flashDiagStatus],
|
|
3224
|
+
);
|
|
3225
|
+
|
|
3226
|
+
const closeDiagnosticsPreview = useCallback(() => {
|
|
3227
|
+
setDiagOpen(false);
|
|
3228
|
+
setDiagIntroSeen(true);
|
|
3229
|
+
}, []);
|
|
3230
|
+
|
|
2960
3231
|
if (form === null) {
|
|
2961
3232
|
return h(
|
|
2962
3233
|
'div',
|
|
@@ -2969,11 +3240,21 @@ window.__ModuleLoader__.load({
|
|
|
2969
3240
|
const compactPercent = percent(form.thresholds.compactRatio);
|
|
2970
3241
|
const handoffPercent = percent(form.thresholds.handoffRatio);
|
|
2971
3242
|
const count = (value) => (Number.isFinite(value) ? value : 0);
|
|
3243
|
+
// 0.4 P1-3:轻账本一行累计数字(只读;数据来自宿主账本,与账本文件同源)。
|
|
3244
|
+
const ledgerLine = (() => {
|
|
3245
|
+
if (ledger === null) return '累计:暂无记录(完成一次压缩/交接后开始统计)';
|
|
3246
|
+
const freed = count(ledger.freedTokens);
|
|
3247
|
+
const freedText = freed >= 1_000_000 ? `${(freed / 1_000_000).toFixed(1)}M` : freed >= 1000 ? `${(freed / 1000).toFixed(1)}K` : String(freed);
|
|
3248
|
+
const avg = ledger.avgDurationMs !== null && Number.isFinite(ledger.avgDurationMs) ? `${(ledger.avgDurationMs / 1000).toFixed(1)}s` : '—';
|
|
3249
|
+
return `累计:已压缩 ${count(ledger.compactCount)} 次 · 交接 ${count(ledger.handoffCount)} 次 · 腾出约 ${freedText} tokens · 平均耗时 ${avg}`;
|
|
3250
|
+
})();
|
|
2972
3251
|
|
|
2973
3252
|
const autoOnNative = native !== null && native.autoEnabled !== false;
|
|
2974
3253
|
const nativePercent = native !== null && typeof native.thresholdRatio === 'number' ? Math.round(native.thresholdRatio * 100) : 80;
|
|
2975
3254
|
const previewLight = paletteForScheme(form.colors, 'light');
|
|
2976
3255
|
const previewDark = paletteForScheme(form.colors, 'dark');
|
|
3256
|
+
// 0.4 P0-2:诊断预览弹层复用指示灯那套浮层材质(15-shared.panelChrome)。
|
|
3257
|
+
const theme = detectTheme();
|
|
2977
3258
|
const banner = settingsError === null ? null : h(
|
|
2978
3259
|
'div',
|
|
2979
3260
|
{ style: { marginBottom: 14, padding: '8px 10px', borderRadius: 6, border: '1px solid var(--dsw-alias-state-warning-primary, #d97706)', background: 'var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.08))' } },
|
|
@@ -3448,12 +3729,60 @@ window.__ModuleLoader__.load({
|
|
|
3448
3729
|
// 2026-10-20:0.3.1 已发布到 npm,帮助区直接给包页面入口。
|
|
3449
3730
|
githubLink('npm 包页面', 'https://www.npmjs.com/package/dsh-compact-relay', ''),
|
|
3450
3731
|
),
|
|
3732
|
+
// 0.4 P0-2:诊断信息 —— 本机生成、白名单、已脱敏;只提供「生成/预览/复制」。
|
|
3733
|
+
h(
|
|
3734
|
+
'div',
|
|
3735
|
+
{ style: { marginBottom: 18, paddingTop: 12, borderTop: '1px dashed var(--dsw-alias-border-l2, rgba(127,127,127,0.25))' }, 'data-cr-diagnostics': '1' },
|
|
3736
|
+
h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 4 } }, '诊断信息'),
|
|
3737
|
+
h('p', { style: { ...settingsHintStyle, margin: '0 0 2px' } }, '只在本机生成,不会自动外发;生成后可以先预览,再决定把哪部分粘贴到哪里。'),
|
|
3738
|
+
h(
|
|
3739
|
+
'p',
|
|
3740
|
+
{ style: settingsHintStyle },
|
|
3741
|
+
'内容只有版本、平台、配置数值、状态摘要、计数、耗时与错误码;不含聊天内容、摘要、交接单正文、文件路径、用户名、会话 id、密钥或 cookie。',
|
|
3742
|
+
),
|
|
3743
|
+
h(
|
|
3744
|
+
'div',
|
|
3745
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginTop: 8 } },
|
|
3746
|
+
h(
|
|
3747
|
+
'button',
|
|
3748
|
+
{
|
|
3749
|
+
type: 'button',
|
|
3750
|
+
'data-cr-diagnostics-generate': '1',
|
|
3751
|
+
style: settingsButtonStyle,
|
|
3752
|
+
disabled: diagLoading,
|
|
3753
|
+
onClick: () => void generateDiagnostics(),
|
|
3754
|
+
},
|
|
3755
|
+
diagLoading ? '生成中…' : '生成并预览',
|
|
3756
|
+
),
|
|
3757
|
+
h(
|
|
3758
|
+
'button',
|
|
3759
|
+
{ type: 'button', 'data-cr-diagnostics-issue': '1', style: settingsButtonStyle, onClick: () => void openIssueReport() },
|
|
3760
|
+
'打开问题报告页',
|
|
3761
|
+
),
|
|
3762
|
+
diagStatus === null
|
|
3763
|
+
? null
|
|
3764
|
+
: h(
|
|
3765
|
+
'span',
|
|
3766
|
+
{
|
|
3767
|
+
'data-cr-diagnostics-status': diagStatus.error ? 'error' : 'ok',
|
|
3768
|
+
style: {
|
|
3769
|
+
fontSize: 11,
|
|
3770
|
+
lineHeight: '16px',
|
|
3771
|
+
color: diagStatus.error ? 'var(--dsw-alias-state-error-primary, #D92D20)' : 'var(--dsw-alias-state-success-primary, #15803D)',
|
|
3772
|
+
},
|
|
3773
|
+
},
|
|
3774
|
+
diagStatus.text,
|
|
3775
|
+
),
|
|
3776
|
+
),
|
|
3777
|
+
h('p', { style: { ...settingsHintStyle, marginTop: 6 } }, 'issue 页是公开的;粘贴前请自己再看一眼。'),
|
|
3778
|
+
),
|
|
3451
3779
|
),
|
|
3452
3780
|
sectionHeading('关于与危险操作'),
|
|
3453
3781
|
h(
|
|
3454
3782
|
'div',
|
|
3455
3783
|
{ style: { display: 'flex', flexDirection: 'column', paddingLeft: 16, borderLeft: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))' }, 'data-cr-group': '关于与危险操作' },
|
|
3456
3784
|
h('p', { style: settingsHintStyle }, `版本:${version || '—'}`),
|
|
3785
|
+
h('p', { style: settingsHintStyle, 'data-cr-ledger': '1' }, ledgerLine),
|
|
3457
3786
|
h('p', { style: settingsHintStyle }, `设置文件:${settingsPath || '—'}`),
|
|
3458
3787
|
h('p', { style: { ...settingsHintStyle, marginTop: 8 } }, '输入框左侧的上下文指示灯:看容量、提醒压缩/交接、锁住重要消息;到线了用 /bc 或 /handoff,重要内容用 /pin 锁住。'),
|
|
3459
3788
|
h('p', { style: settingsHintStyle }, '遇到 bug 或想提改进:走上面「帮助」里的 GitHub Issues;也欢迎直接发邮件。'),
|
|
@@ -3470,8 +3799,151 @@ window.__ModuleLoader__.load({
|
|
|
3470
3799
|
)
|
|
3471
3800
|
: h('button', { type: 'button', style: settingsButtonStyle, disabled: saving, onClick: () => setConfirmReset(true) }, '恢复默认'),
|
|
3472
3801
|
),
|
|
3473
|
-
)
|
|
3474
|
-
|
|
3802
|
+
),
|
|
3803
|
+
// 0.4 P0-2:诊断信息预览弹层(只读等宽文本 + 复制 Markdown/JSON)。
|
|
3804
|
+
// 只有用户点了「生成并预览」/「打开问题报告页」才会出现;关掉后本次会话不再显示首次说明卡。
|
|
3805
|
+
diagOpen && diagData !== null
|
|
3806
|
+
? h(
|
|
3807
|
+
'div',
|
|
3808
|
+
{
|
|
3809
|
+
'data-cr-diagnostics-modal': '1',
|
|
3810
|
+
style: {
|
|
3811
|
+
position: 'fixed',
|
|
3812
|
+
inset: 0,
|
|
3813
|
+
zIndex: 60,
|
|
3814
|
+
display: 'flex',
|
|
3815
|
+
alignItems: 'center',
|
|
3816
|
+
justifyContent: 'center',
|
|
3817
|
+
padding: 24,
|
|
3818
|
+
background: 'rgba(0,0,0,0.36)',
|
|
3819
|
+
},
|
|
3820
|
+
},
|
|
3821
|
+
h(
|
|
3822
|
+
'div',
|
|
3823
|
+
{
|
|
3824
|
+
role: 'dialog',
|
|
3825
|
+
'aria-modal': 'true',
|
|
3826
|
+
'aria-label': '诊断信息预览(脱敏)',
|
|
3827
|
+
style: {
|
|
3828
|
+
...panelChrome(theme),
|
|
3829
|
+
display: 'flex',
|
|
3830
|
+
flexDirection: 'column',
|
|
3831
|
+
width: '100%',
|
|
3832
|
+
maxWidth: 760,
|
|
3833
|
+
maxHeight: '80vh',
|
|
3834
|
+
borderRadius: 12,
|
|
3835
|
+
padding: '14px 16px',
|
|
3836
|
+
},
|
|
3837
|
+
},
|
|
3838
|
+
h(
|
|
3839
|
+
'div',
|
|
3840
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 } },
|
|
3841
|
+
h('div', { style: { fontSize: 13, fontWeight: 700, color: 'var(--dsw-alias-label-primary, inherit)' } }, '诊断信息预览(脱敏)'),
|
|
3842
|
+
h('button', { type: 'button', style: settingsButtonStyle, onClick: closeDiagnosticsPreview }, '关闭'),
|
|
3843
|
+
),
|
|
3844
|
+
diagIntroSeen
|
|
3845
|
+
? null
|
|
3846
|
+
: h(
|
|
3847
|
+
'div',
|
|
3848
|
+
{
|
|
3849
|
+
style: {
|
|
3850
|
+
marginTop: 10,
|
|
3851
|
+
padding: '8px 10px',
|
|
3852
|
+
borderRadius: 8,
|
|
3853
|
+
border: '1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.25))',
|
|
3854
|
+
background: 'var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))',
|
|
3855
|
+
},
|
|
3856
|
+
},
|
|
3857
|
+
h('div', { style: { fontSize: 12, fontWeight: 600, marginBottom: 4 } }, '第一次生成:先看三点'),
|
|
3858
|
+
h(
|
|
3859
|
+
'p',
|
|
3860
|
+
{ style: settingsHintStyle },
|
|
3861
|
+
'① 只在本机生成,插件不会自己发到任何地方;② 只有数值、枚举与错误码,不含聊天内容与文件路径;③ issue 页是公开的,粘贴前请自己再看一眼。',
|
|
3862
|
+
),
|
|
3863
|
+
),
|
|
3864
|
+
h(
|
|
3865
|
+
'pre',
|
|
3866
|
+
{
|
|
3867
|
+
'data-cr-diagnostics-preview': '1',
|
|
3868
|
+
'aria-label': '诊断信息 Markdown 预览(只读)',
|
|
3869
|
+
style: {
|
|
3870
|
+
flex: 1,
|
|
3871
|
+
minHeight: 120,
|
|
3872
|
+
overflow: 'auto',
|
|
3873
|
+
margin: '10px 0',
|
|
3874
|
+
padding: 10,
|
|
3875
|
+
borderRadius: 8,
|
|
3876
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))',
|
|
3877
|
+
background: 'var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))',
|
|
3878
|
+
color: 'inherit',
|
|
3879
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
3880
|
+
fontSize: 11,
|
|
3881
|
+
lineHeight: '16px',
|
|
3882
|
+
whiteSpace: 'pre-wrap',
|
|
3883
|
+
wordBreak: 'break-word',
|
|
3884
|
+
userSelect: 'text',
|
|
3885
|
+
},
|
|
3886
|
+
},
|
|
3887
|
+
typeof diagData.markdown === 'string' && diagData.markdown.length > 0 ? diagData.markdown : '(没有内容)',
|
|
3888
|
+
),
|
|
3889
|
+
h(
|
|
3890
|
+
'div',
|
|
3891
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
3892
|
+
h(
|
|
3893
|
+
'button',
|
|
3894
|
+
{ type: 'button', 'data-cr-diagnostics-copy': 'markdown', style: settingsButtonStyle, onClick: () => copyDiagnostics('markdown') },
|
|
3895
|
+
'复制 Markdown',
|
|
3896
|
+
),
|
|
3897
|
+
h(
|
|
3898
|
+
'button',
|
|
3899
|
+
{ type: 'button', 'data-cr-diagnostics-copy': 'json', style: settingsButtonStyle, onClick: () => copyDiagnostics('json') },
|
|
3900
|
+
'复制 JSON',
|
|
3901
|
+
),
|
|
3902
|
+
h('span', { style: settingsHintStyle }, 'issue 是公开的;粘贴前请自己再看一眼。'),
|
|
3903
|
+
diagStatus === null
|
|
3904
|
+
? null
|
|
3905
|
+
: h(
|
|
3906
|
+
'span',
|
|
3907
|
+
{
|
|
3908
|
+
'data-cr-diagnostics-copy-status': diagStatus.error ? 'error' : 'ok',
|
|
3909
|
+
style: {
|
|
3910
|
+
fontSize: 11,
|
|
3911
|
+
lineHeight: '16px',
|
|
3912
|
+
color: diagStatus.error
|
|
3913
|
+
? 'var(--dsw-alias-state-error-primary, #D92D20)'
|
|
3914
|
+
: 'var(--dsw-alias-state-success-primary, #15803D)',
|
|
3915
|
+
},
|
|
3916
|
+
},
|
|
3917
|
+
diagStatus.text,
|
|
3918
|
+
),
|
|
3919
|
+
),
|
|
3920
|
+
),
|
|
3921
|
+
)
|
|
3922
|
+
: null,
|
|
3923
|
+
// A4:复制/打开结果的短 toast(成功与失败都提示;自动消失,不挡操作)。
|
|
3924
|
+
diagStatus === null
|
|
3925
|
+
? null
|
|
3926
|
+
: h(
|
|
3927
|
+
'div',
|
|
3928
|
+
{
|
|
3929
|
+
'data-cr-diagnostics-toast': diagStatus.error ? 'error' : 'ok',
|
|
3930
|
+
role: 'status',
|
|
3931
|
+
style: {
|
|
3932
|
+
...panelChrome(theme),
|
|
3933
|
+
position: 'fixed',
|
|
3934
|
+
right: 24,
|
|
3935
|
+
bottom: 24,
|
|
3936
|
+
zIndex: 70,
|
|
3937
|
+
maxWidth: 360,
|
|
3938
|
+
padding: '8px 12px',
|
|
3939
|
+
borderRadius: 8,
|
|
3940
|
+
fontSize: 12,
|
|
3941
|
+
lineHeight: '17px',
|
|
3942
|
+
color: diagStatus.error ? 'var(--dsw-alias-state-error-primary, #D92D20)' : 'var(--dsw-alias-label-primary, inherit)',
|
|
3943
|
+
},
|
|
3944
|
+
},
|
|
3945
|
+
diagStatus.text,
|
|
3946
|
+
),
|
|
3475
3947
|
);
|
|
3476
3948
|
}
|
|
3477
3949
|
|