dsh-compact-relay 0.3.6 → 0.4.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/CHANGELOG.md +92 -2
- package/README.en.md +22 -5
- package/README.md +23 -6
- package/ROADMAP.md +10 -3
- package/TUTORIAL.md +2 -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 +609 -32
- 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
|
@@ -742,6 +742,59 @@ window.__ModuleLoader__.load({
|
|
|
742
742
|
);
|
|
743
743
|
};
|
|
744
744
|
|
|
745
|
+
/**
|
|
746
|
+
* 0.4.1:原生自动压缩线「配置阈值 vs 实际触发线」。
|
|
747
|
+
* DSH 按 token 预算触发:min(窗口×配置阈值, 窗口−输出预留−余量),
|
|
748
|
+
* 所以实际触发点常比配置比例更早(deepseek-flash 配置 80%、实际 67.85%)。
|
|
749
|
+
* autoEnabled=false 时 effective/percentText 为空(未开启),但 actualText 仍给出
|
|
750
|
+
* 「开启后实际会触发多少」,供关闭态文案使用。
|
|
751
|
+
* @param {object|null} native readState 下发的 native 字段
|
|
752
|
+
* @returns {{configured:number|null,configuredText:string|null,actual:number|null,actualText:string,effective:number|null,percentText:string,differs:boolean,autoOff:boolean,why:string}}
|
|
753
|
+
*/
|
|
754
|
+
const nativeLineInfo = (native) => {
|
|
755
|
+
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
|
|
756
|
+
const ratioOf = (value) => {
|
|
757
|
+
const n = num(value);
|
|
758
|
+
return n !== null && n > 0 && n < 1 ? n : null;
|
|
759
|
+
};
|
|
760
|
+
const src = native === null || native === undefined ? null : native;
|
|
761
|
+
const configured = ratioOf(src === null ? null : src.thresholdRatio);
|
|
762
|
+
const actual = ratioOf(src === null ? null : src.ratio);
|
|
763
|
+
const autoOff = src !== null && src.autoEnabled === false;
|
|
764
|
+
const effective = autoOff ? null : actual !== null ? actual : configured;
|
|
765
|
+
const pctText = (value) => {
|
|
766
|
+
const p = value * 100;
|
|
767
|
+
return Math.abs(p - Math.round(p)) < 0.05 ? String(Math.round(p)) : p.toFixed(1);
|
|
768
|
+
};
|
|
769
|
+
const configuredText = configured === null ? null : pctText(configured);
|
|
770
|
+
const actualText = actual === null ? '' : pctText(actual);
|
|
771
|
+
const percentText = effective === null ? '' : pctText(effective);
|
|
772
|
+
const differs = configured !== null && effective !== null && Math.abs(effective - configured) >= 0.005;
|
|
773
|
+
let why = '';
|
|
774
|
+
if (differs) {
|
|
775
|
+
const overridden = src.overridden === true;
|
|
776
|
+
const windowTokens = num(src.contextWindow);
|
|
777
|
+
const reserved = num(src.reservedCompletionTokens);
|
|
778
|
+
const headroom = num(src.headroomTokens);
|
|
779
|
+
// 只有「预算分支确实更小」时,才能把差异归因为输出预留/余量。
|
|
780
|
+
const budgetTokens = windowTokens !== null ? windowTokens - (reserved ?? 0) - (headroom ?? 0) : null;
|
|
781
|
+
const budgetBinding = budgetTokens !== null && configured !== null && budgetTokens < windowTokens * configured;
|
|
782
|
+
const parts = [];
|
|
783
|
+
if (reserved !== null && reserved > 0) parts.push(`模型输出要预留 ${fmt(reserved)}`);
|
|
784
|
+
if (headroom !== null && headroom > 0) parts.push(`DSH 自己还要留 ${fmt(headroom)} 余量`);
|
|
785
|
+
if (overridden) {
|
|
786
|
+
why = `这个模型单独配了阈值(按路由覆盖),实际触发是 ${actualText}%(全局配置 ${configuredText}%)。`;
|
|
787
|
+
} else if (budgetBinding && parts.length > 0) {
|
|
788
|
+
why = `配置阈值 ${configuredText}% 只是理论上限;${parts.join('、')},所以实际打到 ${percentText}% 就会自动压。`;
|
|
789
|
+
} else if (actual !== null && configured !== null && actual > configured) {
|
|
790
|
+
why = `这个路由的输出预留更小,实际触发晚于配置阈值 ${configuredText}%(实际 ${actualText}%)。`;
|
|
791
|
+
} else {
|
|
792
|
+
why = `实际触发 ${actualText}% 与配置阈值 ${configuredText}% 不同,按宿主 token 预算算的。`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return { configured, configuredText, actual, actualText, effective, percentText, differs, autoOff, why };
|
|
796
|
+
};
|
|
797
|
+
|
|
745
798
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
746
799
|
|
|
747
800
|
/**
|
|
@@ -818,6 +871,14 @@ window.__ModuleLoader__.load({
|
|
|
818
871
|
const { sessionId, waitAndOpen } = props;
|
|
819
872
|
const hasSession = typeof sessionId === 'string' && sessionId.length > 0;
|
|
820
873
|
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 });
|
|
874
|
+
// 0.4 P2-3:原生自动压缩的「有效线」——优先真实触发线(thresholdTokens/window),
|
|
875
|
+
// 老宿主/算不出时回落 thresholdRatio(窗口比例)。
|
|
876
|
+
const effectiveNativeRatio = (native) => {
|
|
877
|
+
if (native === null || native === undefined || native.autoEnabled === false) return null;
|
|
878
|
+
if (typeof native.ratio === 'number' && Number.isFinite(native.ratio) && native.ratio > 0 && native.ratio < 1) return native.ratio;
|
|
879
|
+
if (typeof native.thresholdRatio === 'number' && Number.isFinite(native.thresholdRatio) && native.thresholdRatio > 0) return native.thresholdRatio;
|
|
880
|
+
return null;
|
|
881
|
+
};
|
|
821
882
|
const [open, setOpen] = useState(false);
|
|
822
883
|
const [busy, setBusy] = useState(false);
|
|
823
884
|
const [hover, setHover] = useState(false);
|
|
@@ -877,6 +938,8 @@ window.__ModuleLoader__.load({
|
|
|
877
938
|
}, []);
|
|
878
939
|
// A1b:交接预览(九段草稿 + 就绪清单),只读不落盘。
|
|
879
940
|
const [handoffPreview, setHandoffPreview] = useState(null);
|
|
941
|
+
/** 0.4 P2-1:交接预览里「约束 / 下一步」两段的编辑值(键=段标题;空值回落原文)。 */
|
|
942
|
+
const [handoffEditValues, setHandoffEditValues] = useState({});
|
|
880
943
|
const handoffPreviewRef = useRef(null);
|
|
881
944
|
const handoffPreviewAbortRef = useRef(null);
|
|
882
945
|
const closeHandoffPreview = useCallback(() => {
|
|
@@ -936,13 +999,20 @@ window.__ModuleLoader__.load({
|
|
|
936
999
|
: '/api/compact-relay-state';
|
|
937
1000
|
|
|
938
1001
|
const applierRef = useRef(() => {});
|
|
1002
|
+
// 0.3.7:最后一帧(SSE 帧或轮询响应)的到达时间。远程/代理链路可能让 SSE
|
|
1003
|
+
// 「onopen 已连上、之后却一帧都不来」地静默挂住,表现为压缩后界面不刷新
|
|
1004
|
+
// (Win 远程客户端实测:手动刷新才变)。帧静默超时后由兜底轮询补拉。
|
|
1005
|
+
const lastFrameAtRef = useRef(0);
|
|
939
1006
|
const fetchState = useCallback(async () => {
|
|
940
1007
|
if (!hasSession) return;
|
|
941
1008
|
try {
|
|
942
1009
|
const res = await fetch(url, { cache: 'no-store' });
|
|
943
1010
|
if (!res.ok) return;
|
|
944
1011
|
const data = await res.json();
|
|
945
|
-
if (data && typeof data === 'object')
|
|
1012
|
+
if (data && typeof data === 'object') {
|
|
1013
|
+
lastFrameAtRef.current = Date.now();
|
|
1014
|
+
applierRef.current(data);
|
|
1015
|
+
}
|
|
946
1016
|
} catch {
|
|
947
1017
|
/* host 不在就保持上一次状态 */
|
|
948
1018
|
}
|
|
@@ -999,6 +1069,33 @@ window.__ModuleLoader__.load({
|
|
|
999
1069
|
typeof data.native.thresholdRatio === 'number' && Number.isFinite(data.native.thresholdRatio)
|
|
1000
1070
|
? Math.max(0, Math.min(1, data.native.thresholdRatio))
|
|
1001
1071
|
: 0.8,
|
|
1072
|
+
// 0.4 P2-3:真实触发线(thresholdTokens/window);宿主算不出时为 null。
|
|
1073
|
+
ratio:
|
|
1074
|
+
typeof data.native.ratio === 'number' && Number.isFinite(data.native.ratio)
|
|
1075
|
+
? Math.max(0, Math.min(1, data.native.ratio))
|
|
1076
|
+
: null,
|
|
1077
|
+
thresholdTokens:
|
|
1078
|
+
typeof data.native.thresholdTokens === 'number' && Number.isFinite(data.native.thresholdTokens)
|
|
1079
|
+
? data.native.thresholdTokens
|
|
1080
|
+
: null,
|
|
1081
|
+
contextWindow:
|
|
1082
|
+
typeof data.native.contextWindow === 'number' && Number.isFinite(data.native.contextWindow)
|
|
1083
|
+
? data.native.contextWindow
|
|
1084
|
+
: null,
|
|
1085
|
+
headroomTokens:
|
|
1086
|
+
typeof data.native.headroomTokens === 'number' && Number.isFinite(data.native.headroomTokens)
|
|
1087
|
+
? data.native.headroomTokens
|
|
1088
|
+
: null,
|
|
1089
|
+
// 0.4.1:悬停卡解释「为什么实际触发比配置阈值早」要用到输出预留。
|
|
1090
|
+
reservedCompletionTokens:
|
|
1091
|
+
typeof data.native.reservedCompletionTokens === 'number' && Number.isFinite(data.native.reservedCompletionTokens)
|
|
1092
|
+
? data.native.reservedCompletionTokens
|
|
1093
|
+
: null,
|
|
1094
|
+
overridden: data.native.overridden === true,
|
|
1095
|
+
route:
|
|
1096
|
+
data.native.route && typeof data.native.route === 'object' && typeof data.native.route.model === 'string'
|
|
1097
|
+
? { provider: String(data.native.route.provider ?? ''), model: data.native.route.model }
|
|
1098
|
+
: null,
|
|
1002
1099
|
}
|
|
1003
1100
|
: null,
|
|
1004
1101
|
colors: data.colors && typeof data.colors === 'object' ? data.colors : null,
|
|
@@ -1040,8 +1137,12 @@ window.__ModuleLoader__.load({
|
|
|
1040
1137
|
if (dead) return;
|
|
1041
1138
|
streamLiveRef.current = true;
|
|
1042
1139
|
setStreamLive(true);
|
|
1140
|
+
lastFrameAtRef.current = Date.now();
|
|
1141
|
+
// 0.3.7:刚连上先拉一次,补齐「断开期间错过的帧」(重连场景)。
|
|
1142
|
+
void fetchState();
|
|
1043
1143
|
};
|
|
1044
1144
|
source.onmessage = (event) => {
|
|
1145
|
+
lastFrameAtRef.current = Date.now();
|
|
1045
1146
|
try {
|
|
1046
1147
|
applyState(JSON.parse(event.data));
|
|
1047
1148
|
} catch {
|
|
@@ -1073,14 +1174,17 @@ window.__ModuleLoader__.load({
|
|
|
1073
1174
|
// 兜底轮询:
|
|
1074
1175
|
// - 还没就绪(会话刚打开 / 没有 agent)→ 1.5s 快轮询(修「重启后停在上下文未知」);
|
|
1075
1176
|
// - 已就绪但 SSE 没连上 → 5s 轮询兜底;
|
|
1076
|
-
// - 已就绪且 SSE 活着 →
|
|
1177
|
+
// - 已就绪且 SSE 活着 → 5s 计时器只检查「帧是否静默超时」;
|
|
1178
|
+
// - 0.3.7:SSE 活着但 30s 没来帧(远程/代理链路静默挂住)→ 补拉一次,
|
|
1179
|
+
// 修「压缩后远程客户端不刷新、要手动刷新才变」。
|
|
1077
1180
|
useEffect(() => {
|
|
1078
1181
|
if (!hasSession) return;
|
|
1079
1182
|
let timer;
|
|
1080
1183
|
let dead = false;
|
|
1081
1184
|
const tick = () => {
|
|
1082
1185
|
if (dead) return;
|
|
1083
|
-
|
|
1186
|
+
const sseSilent = streamLiveRef.current && Date.now() - lastFrameAtRef.current > 30000;
|
|
1187
|
+
if (!readyRef.current || !streamLiveRef.current || sseSilent) void fetchState();
|
|
1084
1188
|
timer = setTimeout(tick, readyRef.current ? 5000 : 1500);
|
|
1085
1189
|
};
|
|
1086
1190
|
tick();
|
|
@@ -1090,6 +1194,24 @@ window.__ModuleLoader__.load({
|
|
|
1090
1194
|
};
|
|
1091
1195
|
}, [fetchState, hasSession]);
|
|
1092
1196
|
|
|
1197
|
+
// 0.3.7:窗口重新可见 / 重新获得焦点 / 网络恢复时拉一次——覆盖「页面被挂起后
|
|
1198
|
+
// 计时器被节流、SSE 断流」的场景(远程客户端切走再切回来也会走到这里)。
|
|
1199
|
+
useEffect(() => {
|
|
1200
|
+
if (!hasSession) return;
|
|
1201
|
+
const refresh = () => {
|
|
1202
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
|
1203
|
+
void fetchState();
|
|
1204
|
+
};
|
|
1205
|
+
document.addEventListener('visibilitychange', refresh);
|
|
1206
|
+
window.addEventListener('focus', refresh);
|
|
1207
|
+
window.addEventListener('online', refresh);
|
|
1208
|
+
return () => {
|
|
1209
|
+
document.removeEventListener('visibilitychange', refresh);
|
|
1210
|
+
window.removeEventListener('focus', refresh);
|
|
1211
|
+
window.removeEventListener('online', refresh);
|
|
1212
|
+
};
|
|
1213
|
+
}, [fetchState, hasSession]);
|
|
1214
|
+
|
|
1093
1215
|
|
|
1094
1216
|
// 点外面 / Esc 关闭菜单、压缩确认卡与交接单面板。
|
|
1095
1217
|
useEffect(() => {
|
|
@@ -1188,7 +1310,7 @@ window.__ModuleLoader__.load({
|
|
|
1188
1310
|
// DSH 原生自动压缩线(报警最高阈值):开着时在环上画一个「径向刻度」(不是沿环的弧),
|
|
1189
1311
|
// 用紫色 + 主题底色描边——刻意不用五级配色里的颜色,保证在任何分段颜色上都看得清。
|
|
1190
1312
|
// 越过原生线后:整环红色闪烁(作者 2026-10-20 反馈)。
|
|
1191
|
-
const nativeAuto = state.native
|
|
1313
|
+
const nativeAuto = effectiveNativeRatio(state.native);
|
|
1192
1314
|
const nativeValid = nativeAuto !== null && nativeAuto > 0.02 && nativeAuto < 1;
|
|
1193
1315
|
const nativeCrossed = nativeValid && ratio > nativeAuto;
|
|
1194
1316
|
// 断点缺口:0.9 SVG 单位(≈6.4°),折算成比例;段太短时缺口会吃掉整段,就跳过该段。
|
|
@@ -1310,11 +1432,11 @@ window.__ModuleLoader__.load({
|
|
|
1310
1432
|
// 配合指示灯呼吸光晕,解决「指示灯太安静、新用户不知道这里有个菜单」。
|
|
1311
1433
|
// 设置页「再看一次初次引导」按钮通过 window 事件重播(两个组件同页,用事件解耦)。
|
|
1312
1434
|
const onboardingText = useCallback(() => {
|
|
1313
|
-
const nativeRatio = state.native
|
|
1435
|
+
const nativeRatio = effectiveNativeRatio(state.native);
|
|
1314
1436
|
const nativePart =
|
|
1315
1437
|
nativeRatio === null
|
|
1316
1438
|
? 'DSH 原生自动压缩已关闭'
|
|
1317
|
-
: `DSH 原生自动压缩线在 ${(nativeRatio * 100).toFixed(
|
|
1439
|
+
: `DSH 原生自动压缩线在 ${(nativeRatio * 100).toFixed(1)}%,到那之前先压缩或交接更稳`;
|
|
1318
1440
|
return `这是上下文指示灯:点它压缩旧会话、锁定重要消息、交接新会话。${nativePart}。环上的小缺口是我判断「该压缩」的位置(模型判断或到线自动出现)。重要的结论/路径先锁定,压缩时才不会被压掉。`;
|
|
1319
1441
|
}, [state.native]);
|
|
1320
1442
|
|
|
@@ -1437,6 +1559,7 @@ window.__ModuleLoader__.load({
|
|
|
1437
1559
|
setPreview(null);
|
|
1438
1560
|
closeDocs();
|
|
1439
1561
|
closeDiff();
|
|
1562
|
+
setHandoffEditValues({});
|
|
1440
1563
|
setHandoffPreview({ loading: true });
|
|
1441
1564
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1442
1565
|
handoffPreviewAbortRef.current = controller;
|
|
@@ -1626,7 +1749,7 @@ window.__ModuleLoader__.load({
|
|
|
1626
1749
|
}, [effectiveBusy, sessionId]);
|
|
1627
1750
|
|
|
1628
1751
|
const run = useCallback(
|
|
1629
|
-
async (action) => {
|
|
1752
|
+
async (action, extra) => {
|
|
1630
1753
|
// A3:确认必须带预览时拿到的 planId,宿主重算不一致会以 changed 拒绝。
|
|
1631
1754
|
const expected =
|
|
1632
1755
|
action === 'compact' && preview !== null && preview.loading !== true && typeof preview.data?.planId === 'string'
|
|
@@ -1651,6 +1774,8 @@ window.__ModuleLoader__.load({
|
|
|
1651
1774
|
body: JSON.stringify({
|
|
1652
1775
|
action,
|
|
1653
1776
|
...(expected === undefined ? {} : { expected }),
|
|
1777
|
+
// 0.4 P2-1:`extra.edits` = 只含用户真正改过的「约束/下一步」两段。
|
|
1778
|
+
...(extra !== undefined && extra !== null && typeof extra === 'object' ? extra : {}),
|
|
1654
1779
|
...(typeof sessionId === 'string' && sessionId.length > 0 ? { sessionId } : {}),
|
|
1655
1780
|
}),
|
|
1656
1781
|
...(controller === null ? {} : { signal: controller.signal }),
|
|
@@ -1690,11 +1815,19 @@ window.__ModuleLoader__.load({
|
|
|
1690
1815
|
state.compressHint !== null && typeof state.compressHint.ratio === 'number'
|
|
1691
1816
|
? `压缩判断点 ${(state.compressHint.ratio * 100).toFixed(0)}%`
|
|
1692
1817
|
: '';
|
|
1818
|
+
const nativeEffective = effectiveNativeRatio(state.native);
|
|
1819
|
+
const nativeDetail =
|
|
1820
|
+
state.native !== null && state.native.thresholdTokens !== null && state.native.contextWindow !== null
|
|
1821
|
+
? `(${fmt(state.native.thresholdTokens)} / ${fmt(state.native.contextWindow)}${
|
|
1822
|
+
state.native.route !== null ? `,${state.native.route.provider}/${state.native.route.model}` : ''
|
|
1823
|
+
}${state.native.overridden ? ',按路由覆盖' : ''})`
|
|
1824
|
+
: '';
|
|
1825
|
+
const nativeInfo = nativeLineInfo(state.native);
|
|
1693
1826
|
const nativeLineText =
|
|
1694
|
-
|
|
1695
|
-
? `原生自动压缩线 ${(
|
|
1696
|
-
|
|
1697
|
-
}`
|
|
1827
|
+
nativeEffective !== null
|
|
1828
|
+
? `原生自动压缩线 ${(nativeEffective * 100).toFixed(1)}%${nativeDetail}${
|
|
1829
|
+
nativeInfo.differs ? `;${nativeInfo.why}` : ''
|
|
1830
|
+
}${ratio > nativeEffective ? '(已越过:整环红色闪烁)' : ''}`
|
|
1698
1831
|
: 'DSH 原生自动压缩已关闭';
|
|
1699
1832
|
const pressureKnown = pressureValue !== null;
|
|
1700
1833
|
const title =
|
|
@@ -2030,6 +2163,12 @@ window.__ModuleLoader__.load({
|
|
|
2030
2163
|
const previewRange = preview?.data?.preview ?? null;
|
|
2031
2164
|
// 0.3.1:范围太小时预览阶段就拦下(服务端已按 800 tokens 判定),不让用户白确认一次。
|
|
2032
2165
|
const previewTooSmall = previewRange !== null && previewRange.tooSmall === true;
|
|
2166
|
+
// 0.4 P1-1/P1-2:建议锁定候选 + pin 命中数(宿主已算好,客户端只渲染;不自动锁)。
|
|
2167
|
+
const pinSuggest =
|
|
2168
|
+
previewRange !== null && previewRange.pinSuggest !== null && typeof previewRange.pinSuggest === 'object' && Array.isArray(previewRange.pinSuggest.items)
|
|
2169
|
+
? previewRange.pinSuggest
|
|
2170
|
+
: null;
|
|
2171
|
+
const pinHits = previewRange !== null && previewRange.pinHits !== null && typeof previewRange.pinHits === 'object' ? previewRange.pinHits : null;
|
|
2033
2172
|
const confirmPanel =
|
|
2034
2173
|
preview === null
|
|
2035
2174
|
? null
|
|
@@ -2047,6 +2186,8 @@ window.__ModuleLoader__.load({
|
|
|
2047
2186
|
maxWidth: 'calc(100vw - 16px)',
|
|
2048
2187
|
padding: '10px 12px',
|
|
2049
2188
|
borderRadius: 12,
|
|
2189
|
+
maxHeight: 'min(70vh, 440px)',
|
|
2190
|
+
overflowY: 'auto',
|
|
2050
2191
|
...panelChrome(theme),
|
|
2051
2192
|
color: theme.panelFg,
|
|
2052
2193
|
zIndex: 42,
|
|
@@ -2077,6 +2218,45 @@ window.__ModuleLoader__.load({
|
|
|
2077
2218
|
h('br'),
|
|
2078
2219
|
'只估算范围和容量;摘要内容确认后才生成,不在这里展示。压缩后旧会话将替换为摘要,无法还原为原文。',
|
|
2079
2220
|
),
|
|
2221
|
+
pinSuggest !== null && pinSuggest.items.length > 0
|
|
2222
|
+
? h(
|
|
2223
|
+
'div',
|
|
2224
|
+
{
|
|
2225
|
+
'data-cr-pin-suggest': '1',
|
|
2226
|
+
style: {
|
|
2227
|
+
display: 'flex',
|
|
2228
|
+
flexDirection: 'column',
|
|
2229
|
+
gap: 2,
|
|
2230
|
+
padding: '6px 8px',
|
|
2231
|
+
borderRadius: 8,
|
|
2232
|
+
border: `1px solid ${theme.panelBorder}`,
|
|
2233
|
+
background: theme.hover,
|
|
2234
|
+
},
|
|
2235
|
+
},
|
|
2236
|
+
h('div', { style: { fontSize: 11, fontWeight: 700, color: theme.panelFg } }, `建议锁定 ${pinSuggest.items.length} 条(先 /pin 再压)`),
|
|
2237
|
+
...pinSuggest.items.map((item) =>
|
|
2238
|
+
h(
|
|
2239
|
+
'div',
|
|
2240
|
+
{ key: `pin-suggest-${item.seq}`, style: { fontSize: 11, lineHeight: '16px', color: theme.subText } },
|
|
2241
|
+
`${item.role} · ${item.reason}:${item.snippet}`,
|
|
2242
|
+
),
|
|
2243
|
+
),
|
|
2244
|
+
h(
|
|
2245
|
+
'div',
|
|
2246
|
+
{ style: { fontSize: 10, lineHeight: '14px', color: theme.subText } },
|
|
2247
|
+
'只建议、不自动锁定;锁定后这些消息会原样保留(/pin list 可按序号挑)。',
|
|
2248
|
+
),
|
|
2249
|
+
)
|
|
2250
|
+
: null,
|
|
2251
|
+
pinHits !== null && pinHits.total > 0
|
|
2252
|
+
? h(
|
|
2253
|
+
'div',
|
|
2254
|
+
{ 'data-cr-pin-hits': '1', style: { fontSize: 11, lineHeight: '16px', color: theme.subText } },
|
|
2255
|
+
pinHits.inRange > 0
|
|
2256
|
+
? `pin 命中 ${pinHits.inRange} 条:本次范围里被锁定的消息已跳过(会话共 ${pinHits.total} 条锁定)。`
|
|
2257
|
+
: `pin 命中 0 条:本次范围内没有锁定消息(会话共 ${pinHits.total} 条锁定,均在范围外)。`,
|
|
2258
|
+
)
|
|
2259
|
+
: null,
|
|
2080
2260
|
h(
|
|
2081
2261
|
'div',
|
|
2082
2262
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: 6 } },
|
|
@@ -2214,6 +2394,46 @@ window.__ModuleLoader__.load({
|
|
|
2214
2394
|
: handoffDraft,
|
|
2215
2395
|
),
|
|
2216
2396
|
),
|
|
2397
|
+
// 0.4 P2-1:只允许编辑「约束」「下一步」两段;其余段由模型/程序生成,UI 不给入口。
|
|
2398
|
+
handoffData !== null && typeof handoffData === 'object' && handoffData.editable !== null && typeof handoffData.editable === 'object'
|
|
2399
|
+
? h(
|
|
2400
|
+
'div',
|
|
2401
|
+
{ 'data-cr-handoff-editable': '1', style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2402
|
+
h('div', { style: { fontSize: 11, fontWeight: 600 } }, '可编辑两段(改完点「确认交接」才生效)'),
|
|
2403
|
+
...(Array.isArray(handoffData.editable.titles) ? handoffData.editable.titles : []).map((title) => {
|
|
2404
|
+
const original = typeof handoffData.editable.sections?.[title] === 'string' ? handoffData.editable.sections[title] : '';
|
|
2405
|
+
return h(
|
|
2406
|
+
'label',
|
|
2407
|
+
{ key: `handoff-edit-${title}`, style: { display: 'flex', flexDirection: 'column', gap: 3, fontSize: 11 } },
|
|
2408
|
+
h('span', { style: { color: theme.subText } }, `${title}(留空 = 使用原文)`),
|
|
2409
|
+
h('textarea', {
|
|
2410
|
+
'data-cr-handoff-edit': title,
|
|
2411
|
+
value: typeof handoffEditValues[title] === 'string' ? handoffEditValues[title] : original,
|
|
2412
|
+
rows: 3,
|
|
2413
|
+
maxLength: Number.isInteger(handoffData.editable.maxChars) ? handoffData.editable.maxChars : 4000,
|
|
2414
|
+
onChange: (event) => {
|
|
2415
|
+
const value = event.target.value;
|
|
2416
|
+
setHandoffEditValues((previous) => ({ ...previous, [title]: value }));
|
|
2417
|
+
},
|
|
2418
|
+
style: {
|
|
2419
|
+
width: '100%',
|
|
2420
|
+
boxSizing: 'border-box',
|
|
2421
|
+
resize: 'vertical',
|
|
2422
|
+
fontFamily: 'inherit',
|
|
2423
|
+
fontSize: 11,
|
|
2424
|
+
lineHeight: '16px',
|
|
2425
|
+
color: 'inherit',
|
|
2426
|
+
background: 'transparent',
|
|
2427
|
+
border: `1px solid ${theme.panelBorder}`,
|
|
2428
|
+
borderRadius: 6,
|
|
2429
|
+
padding: '4px 6px',
|
|
2430
|
+
},
|
|
2431
|
+
}),
|
|
2432
|
+
);
|
|
2433
|
+
}),
|
|
2434
|
+
h('div', { style: { fontSize: 10, lineHeight: '14px', color: theme.subText } }, handoffData.editable.note ?? '程序生成的段不可改。'),
|
|
2435
|
+
)
|
|
2436
|
+
: null,
|
|
2217
2437
|
h('div', { style: { fontSize: 11, color: theme.subText } }, '确认后会先压缩旧会话,再按压缩后的历史生成最终交接单;预览不落盘。'),
|
|
2218
2438
|
h(
|
|
2219
2439
|
'div',
|
|
@@ -2234,8 +2454,18 @@ window.__ModuleLoader__.load({
|
|
|
2234
2454
|
type: 'button',
|
|
2235
2455
|
'data-cr-handoff-run': '1',
|
|
2236
2456
|
onClick: () => {
|
|
2457
|
+
// P2-1:只把「用户真的改过、且非空」的两段发给宿主;空值/未改 = 用重新生成的原文。
|
|
2458
|
+
const editable = handoffData !== null && typeof handoffData === 'object' ? handoffData.editable : null;
|
|
2459
|
+
const edits = {};
|
|
2460
|
+
if (editable !== null && typeof editable === 'object' && Array.isArray(editable.titles)) {
|
|
2461
|
+
for (const title of editable.titles) {
|
|
2462
|
+
const original = typeof editable.sections?.[title] === 'string' ? editable.sections[title].trim() : '';
|
|
2463
|
+
const current = typeof handoffEditValues[title] === 'string' ? handoffEditValues[title].trim() : '';
|
|
2464
|
+
if (current.length > 0 && current !== original) edits[title] = current;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2237
2467
|
closeHandoffPreview();
|
|
2238
|
-
void run('handoff');
|
|
2468
|
+
void run('handoff', Object.keys(edits).length > 0 ? { edits } : undefined);
|
|
2239
2469
|
},
|
|
2240
2470
|
style: {
|
|
2241
2471
|
border: 'none',
|
|
@@ -2837,6 +3067,8 @@ window.__ModuleLoader__.load({
|
|
|
2837
3067
|
const [native, setNative] = useState(null);
|
|
2838
3068
|
/** UI-002:设置文件路径(「关于与危险操作」区块展示)。 */
|
|
2839
3069
|
const [settingsPath, setSettingsPath] = useState('');
|
|
3070
|
+
/** 0.4 P1-3:轻账本累计数字(独立账本文件算出来的;null = 没有数据/读不到,显示引导文案)。 */
|
|
3071
|
+
const [ledger, setLedger] = useState(null);
|
|
2840
3072
|
const [confirmNative, setConfirmNative] = useState(null); // null | 'disable' | 'enable'
|
|
2841
3073
|
const [nativeBusy, setNativeBusy] = useState(false);
|
|
2842
3074
|
/** UI-003:结果提示就地显示在开关旁(以前只写页面底部,用户看不到)。 */
|
|
@@ -2844,22 +3076,78 @@ window.__ModuleLoader__.load({
|
|
|
2844
3076
|
/** 作者 2026-10-20:设置页「再看一次初次引导」的就地提示。 */
|
|
2845
3077
|
const [onboardingNotice, setOnboardingNotice] = useState(null);
|
|
2846
3078
|
const nativeConfirmRef = useRef(null);
|
|
3079
|
+
/**
|
|
3080
|
+
* 0.4 P0-2:诊断信息(只在本机生成;可预览/复制,插件不会把它发到任何地方)。
|
|
3081
|
+
* diagData 是端点返回的 {schema,version,generatedAt,markdown,json,issueUrl}。
|
|
3082
|
+
*/
|
|
3083
|
+
const [diagData, setDiagData] = useState(null);
|
|
3084
|
+
const [diagLoading, setDiagLoading] = useState(false);
|
|
3085
|
+
const [diagOpen, setDiagOpen] = useState(false);
|
|
3086
|
+
/** 弹层顶部说明卡:只在本会话第一次生成时出现一次。 */
|
|
3087
|
+
const [diagIntroSeen, setDiagIntroSeen] = useState(false);
|
|
3088
|
+
const [diagStatus, setDiagStatus] = useState(null);
|
|
3089
|
+
/** 复制/打开结果的小 toast 计时器(A4:成功与失败都要有 toast 反馈)。 */
|
|
3090
|
+
const diagToastTimer = useRef(null);
|
|
3091
|
+
|
|
3092
|
+
/** 就地提示 + 4.2 秒后自动消失的 toast。 */
|
|
3093
|
+
const flashDiagStatus = useCallback((next) => {
|
|
3094
|
+
setDiagStatus(next);
|
|
3095
|
+
if (diagToastTimer.current !== null) clearTimeout(diagToastTimer.current);
|
|
3096
|
+
diagToastTimer.current = setTimeout(() => setDiagStatus(null), 4200);
|
|
3097
|
+
}, []);
|
|
3098
|
+
|
|
3099
|
+
useEffect(
|
|
3100
|
+
() => () => {
|
|
3101
|
+
if (diagToastTimer.current !== null) clearTimeout(diagToastTimer.current);
|
|
3102
|
+
},
|
|
3103
|
+
[],
|
|
3104
|
+
);
|
|
3105
|
+
|
|
3106
|
+
/** 0.4.1:设置接口只给「配置阈值」,实际触发线要从 state 接口补(同一套 readState 数据)。 */
|
|
3107
|
+
const nativeLineExtra = useRef(null);
|
|
3108
|
+
const mergeNative = useCallback((view) => {
|
|
3109
|
+
if (view === null || view === undefined) return null;
|
|
3110
|
+
const extra = nativeLineExtra.current;
|
|
3111
|
+
return extra === null ? view : { ...view, ...extra };
|
|
3112
|
+
}, []);
|
|
2847
3113
|
|
|
2848
3114
|
/** 把服务端返回的 view 落到表单:同时更新「已保存」快照和设置文件错误。 */
|
|
2849
3115
|
const applyLoaded = useCallback((data) => {
|
|
2850
3116
|
setForm(data.config);
|
|
2851
3117
|
setSavedSnapshot(JSON.stringify(data.config));
|
|
2852
3118
|
setSettingsError(data.settingsError ?? null);
|
|
2853
|
-
setNative(data.native && typeof data.native === 'object' ? data.native : null);
|
|
3119
|
+
setNative(mergeNative(data.native && typeof data.native === 'object' ? data.native : null));
|
|
2854
3120
|
if (typeof data.version === 'string') setVersion(data.version);
|
|
2855
3121
|
if (typeof data.path === 'string') setSettingsPath(data.path);
|
|
2856
|
-
|
|
3122
|
+
setLedger(data.ledger !== null && typeof data.ledger === 'object' ? data.ledger : null);
|
|
3123
|
+
}, [mergeNative]);
|
|
2857
3124
|
|
|
2858
3125
|
const load = useCallback(async () => {
|
|
2859
3126
|
try {
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
|
|
3127
|
+
const [settingsRes, stateRes] = await Promise.all([
|
|
3128
|
+
fetch('/api/compact-relay-settings', { cache: 'no-store' }),
|
|
3129
|
+
fetch('/api/compact-relay-state', { cache: 'no-store' }).catch(() => null),
|
|
3130
|
+
]);
|
|
3131
|
+
const data = await settingsRes.json().catch(() => ({}));
|
|
3132
|
+
if (!settingsRes.ok || data.ok === false) throw new Error(data && data.message ? String(data.message) : '读取设置失败');
|
|
3133
|
+
// 0.4.1:补上实际触发线相关字段(配置接口没有这些,只有 state 接口有)。
|
|
3134
|
+
let extra = null;
|
|
3135
|
+
if (stateRes !== null && stateRes.ok) {
|
|
3136
|
+
const stateData = await stateRes.json().catch(() => ({}));
|
|
3137
|
+
const sn = stateData !== null && stateData.native !== null && typeof stateData.native === 'object' ? stateData.native : null;
|
|
3138
|
+
if (sn !== null) {
|
|
3139
|
+
extra = {
|
|
3140
|
+
ratio: typeof sn.ratio === 'number' ? sn.ratio : null,
|
|
3141
|
+
thresholdTokens: typeof sn.thresholdTokens === 'number' ? sn.thresholdTokens : null,
|
|
3142
|
+
contextWindow: typeof sn.contextWindow === 'number' ? sn.contextWindow : null,
|
|
3143
|
+
reservedCompletionTokens: typeof sn.reservedCompletionTokens === 'number' ? sn.reservedCompletionTokens : null,
|
|
3144
|
+
headroomTokens: typeof sn.headroomTokens === 'number' ? sn.headroomTokens : null,
|
|
3145
|
+
overridden: sn.overridden === true,
|
|
3146
|
+
route: sn.route !== null && typeof sn.route === 'object' ? sn.route : null,
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
nativeLineExtra.current = extra;
|
|
2863
3151
|
applyLoaded(data);
|
|
2864
3152
|
setStatus(null);
|
|
2865
3153
|
} catch (error) {
|
|
@@ -2957,6 +3245,77 @@ window.__ModuleLoader__.load({
|
|
|
2957
3245
|
}
|
|
2958
3246
|
}, [confirmNative]);
|
|
2959
3247
|
|
|
3248
|
+
/**
|
|
3249
|
+
* 0.4 P0-2:生成诊断信息。
|
|
3250
|
+
*
|
|
3251
|
+
* 唯一的网络调用是同源只读 GET(`/api/compact-relay-diagnostics`,仅此一次);
|
|
3252
|
+
* 产物由宿主生成(白名单 + 已脱敏),这里只负责展示/复制。
|
|
3253
|
+
*/
|
|
3254
|
+
const generateDiagnostics = useCallback(
|
|
3255
|
+
async ({ openPreview = true } = {}) => {
|
|
3256
|
+
if (diagLoading) return null;
|
|
3257
|
+
setDiagLoading(true);
|
|
3258
|
+
setDiagStatus(null);
|
|
3259
|
+
try {
|
|
3260
|
+
const res = await fetch('/api/compact-relay-diagnostics', { cache: 'no-store' });
|
|
3261
|
+
const data = await res.json().catch(() => ({}));
|
|
3262
|
+
if (!res.ok || data.ok === false) throw new Error(data && data.message ? String(data.message) : '生成失败');
|
|
3263
|
+
setDiagData(data);
|
|
3264
|
+
if (openPreview) setDiagOpen(true);
|
|
3265
|
+
return data;
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
setDiagStatus({ error: true, text: `生成失败:${error && error.message ? error.message : '未知错误'}` });
|
|
3268
|
+
return null;
|
|
3269
|
+
} finally {
|
|
3270
|
+
setDiagLoading(false);
|
|
3271
|
+
}
|
|
3272
|
+
},
|
|
3273
|
+
[diagLoading],
|
|
3274
|
+
);
|
|
3275
|
+
|
|
3276
|
+
/** 「打开问题报告页」:没生成就先生成,再打开宿主拼好的短字段 URL(不碰诊断正文)。 */
|
|
3277
|
+
const openIssueReport = useCallback(async () => {
|
|
3278
|
+
let data = diagData;
|
|
3279
|
+
if (data === null || typeof data.issueUrl !== 'string') data = await generateDiagnostics({ openPreview: false });
|
|
3280
|
+
if (data === null || typeof data.issueUrl !== 'string') return;
|
|
3281
|
+
try {
|
|
3282
|
+
window.open(data.issueUrl, '_blank', 'noopener');
|
|
3283
|
+
} catch {
|
|
3284
|
+
flashDiagStatus({ error: true, text: '打开问题报告页失败:新窗口被浏览器拦截了' });
|
|
3285
|
+
}
|
|
3286
|
+
}, [diagData, generateDiagnostics, flashDiagStatus]);
|
|
3287
|
+
|
|
3288
|
+
/** 复制 Markdown / JSON:统一走 navigator.clipboard?.writeText,失败就地提示。 */
|
|
3289
|
+
const copyDiagnostics = useCallback(
|
|
3290
|
+
(format) => {
|
|
3291
|
+
if (diagData === null) {
|
|
3292
|
+
flashDiagStatus({ error: true, text: '还没有可复制的诊断信息,请先生成' });
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
const text =
|
|
3296
|
+
format === 'json' ? JSON.stringify(diagData.json, null, 2) : typeof diagData.markdown === 'string' ? diagData.markdown : '';
|
|
3297
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
3298
|
+
flashDiagStatus({ error: true, text: '还没有可复制的诊断信息,请先生成' });
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const done = () => flashDiagStatus({ text: '已复制诊断信息(脱敏)' });
|
|
3302
|
+
const failed = () => flashDiagStatus({ error: true, text: '复制失败:浏览器不允许访问剪贴板,可手动选中预览文本' });
|
|
3303
|
+
try {
|
|
3304
|
+
const written = navigator.clipboard?.writeText(text);
|
|
3305
|
+
if (written !== undefined && typeof written.then === 'function') written.then(done, failed);
|
|
3306
|
+
else done();
|
|
3307
|
+
} catch {
|
|
3308
|
+
failed();
|
|
3309
|
+
}
|
|
3310
|
+
},
|
|
3311
|
+
[diagData, flashDiagStatus],
|
|
3312
|
+
);
|
|
3313
|
+
|
|
3314
|
+
const closeDiagnosticsPreview = useCallback(() => {
|
|
3315
|
+
setDiagOpen(false);
|
|
3316
|
+
setDiagIntroSeen(true);
|
|
3317
|
+
}, []);
|
|
3318
|
+
|
|
2960
3319
|
if (form === null) {
|
|
2961
3320
|
return h(
|
|
2962
3321
|
'div',
|
|
@@ -2969,11 +3328,30 @@ window.__ModuleLoader__.load({
|
|
|
2969
3328
|
const compactPercent = percent(form.thresholds.compactRatio);
|
|
2970
3329
|
const handoffPercent = percent(form.thresholds.handoffRatio);
|
|
2971
3330
|
const count = (value) => (Number.isFinite(value) ? value : 0);
|
|
3331
|
+
// 0.4 P1-3:轻账本一行累计数字(只读;数据来自宿主账本,与账本文件同源)。
|
|
3332
|
+
const ledgerLine = (() => {
|
|
3333
|
+
if (ledger === null) return '累计:暂无记录(完成一次压缩/交接后开始统计)';
|
|
3334
|
+
const freed = count(ledger.freedTokens);
|
|
3335
|
+
const freedText = freed >= 1_000_000 ? `${(freed / 1_000_000).toFixed(1)}M` : freed >= 1000 ? `${(freed / 1000).toFixed(1)}K` : String(freed);
|
|
3336
|
+
const avg = ledger.avgDurationMs !== null && Number.isFinite(ledger.avgDurationMs) ? `${(ledger.avgDurationMs / 1000).toFixed(1)}s` : '—';
|
|
3337
|
+
return `累计:已压缩 ${count(ledger.compactCount)} 次 · 交接 ${count(ledger.handoffCount)} 次 · 腾出约 ${freedText} tokens · 平均耗时 ${avg}`;
|
|
3338
|
+
})();
|
|
2972
3339
|
|
|
2973
3340
|
const autoOnNative = native !== null && native.autoEnabled !== false;
|
|
2974
|
-
|
|
3341
|
+
// 0.4.1:配置阈值(thresholdRatio)只是理论上限;实际触发线由宿主按 token 预算算。
|
|
3342
|
+
const nativeInfo = nativeLineInfo(native);
|
|
3343
|
+
const nativePercent = nativeInfo.configured !== null ? Math.round(nativeInfo.configured * 100) : 80;
|
|
3344
|
+
const nativeCapRatio = nativeInfo.effective !== null ? nativeInfo.effective : nativeInfo.configured !== null ? nativeInfo.configured : 0.8;
|
|
3345
|
+
// 交接线可设上限:按实际触发线向下取整,输入框 max 与 onChange 用同一个数。
|
|
3346
|
+
const nativeCapPercent = Math.max(5, Math.floor(nativeCapRatio * 100 - 1));
|
|
3347
|
+
// 原生压缩已关闭时 effective/percentText 为空:回落到「开启后实际会触发多少」,再回落配置阈值。
|
|
3348
|
+
const nativeActualText = nativeInfo.percentText !== '' ? nativeInfo.percentText : nativeInfo.actualText !== '' ? nativeInfo.actualText : nativeInfo.configuredText !== null ? nativeInfo.configuredText : String(nativePercent);
|
|
3349
|
+
const handoffOverNative = nativeInfo.effective !== null && form !== null && typeof form.thresholds === 'object' && typeof form.thresholds.handoffRatio === 'number' && form.thresholds.handoffRatio > nativeInfo.effective + 1e-9;
|
|
3350
|
+
const handoffNativeWarning = handoffOverNative ? `当前交接线 ${Math.round(form.thresholds.handoffRatio * 100)}% 高于原生实际触发线 ${nativeInfo.percentText}%:DSH 会先自己压历史,这条提醒可能来不及。建议调到 ${nativeInfo.percentText}% 以下(改完立即生效)。` : '';
|
|
2975
3351
|
const previewLight = paletteForScheme(form.colors, 'light');
|
|
2976
3352
|
const previewDark = paletteForScheme(form.colors, 'dark');
|
|
3353
|
+
// 0.4 P0-2:诊断预览弹层复用指示灯那套浮层材质(15-shared.panelChrome)。
|
|
3354
|
+
const theme = detectTheme();
|
|
2977
3355
|
const banner = settingsError === null ? null : h(
|
|
2978
3356
|
'div',
|
|
2979
3357
|
{ 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))' } },
|
|
@@ -3041,19 +3419,19 @@ window.__ModuleLoader__.load({
|
|
|
3041
3419
|
settingsRow(
|
|
3042
3420
|
'交接提醒线',
|
|
3043
3421
|
'到这条线内圆亮红点,菜单建议「交接新会话」。',
|
|
3044
|
-
|
|
3422
|
+
`调高 → 更晚换会话、单会话更久;调低 → 更早换会话、单会话更短。必须大于压缩线。${handoffNativeWarning}`,
|
|
3045
3423
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
3046
3424
|
h('input', {
|
|
3047
3425
|
type: 'number',
|
|
3048
3426
|
min: 5,
|
|
3049
|
-
max: native !== null && native.autoEnabled !== false ?
|
|
3427
|
+
max: native !== null && native.autoEnabled !== false ? nativeCapPercent : 99,
|
|
3050
3428
|
step: 1,
|
|
3051
3429
|
value: handoffPercent,
|
|
3052
3430
|
'aria-label': '交接提醒线(百分比)',
|
|
3053
3431
|
style: { ...settingsInputStyle, width: 90 },
|
|
3054
3432
|
onChange: (event) => {
|
|
3055
3433
|
const raw = Math.max(0.05, Number(event.target.value) / 100);
|
|
3056
|
-
const cap = native !== null && native.autoEnabled !== false ?
|
|
3434
|
+
const cap = native !== null && native.autoEnabled !== false ? nativeCapPercent / 100 : 0.99;
|
|
3057
3435
|
patch(['thresholds', 'handoffRatio'], Math.min(cap, raw));
|
|
3058
3436
|
},
|
|
3059
3437
|
}),
|
|
@@ -3106,10 +3484,10 @@ window.__ModuleLoader__.load({
|
|
|
3106
3484
|
native === null
|
|
3107
3485
|
? '正在读取 DSH 配置…'
|
|
3108
3486
|
: autoOnNative
|
|
3109
|
-
? `当前开启:容量到 ${
|
|
3487
|
+
? `当前开启:容量到 ${nativeActualText}% 时,DSH 会自己把历史压成摘要(不读本插件的锁定)。`
|
|
3110
3488
|
: '当前已关闭:DSH 不再自动压历史,只有你主动压缩/交接才会动历史。',
|
|
3111
3489
|
autoOnNative
|
|
3112
|
-
? `开启时:容量到 ${
|
|
3490
|
+
? `开启时:容量到 ${nativeActualText}% DSH 会自己压历史(不读锁定);关闭后:只在你主动压缩/交接时才动历史。${nativeInfo.differs ? `(${nativeInfo.why})` : ''}`
|
|
3113
3491
|
: '开启后:DSH 会在容量到原生阈值时自动压历史,可能压掉锁定的消息;本插件的提醒照常。',
|
|
3114
3492
|
h(
|
|
3115
3493
|
'div',
|
|
@@ -3156,7 +3534,13 @@ window.__ModuleLoader__.load({
|
|
|
3156
3534
|
}),
|
|
3157
3535
|
),
|
|
3158
3536
|
h('span', { style: { fontSize: 12, fontWeight: 600, minWidth: 44 } }, native === null ? '读取中…' : nativeBusy ? '处理中…' : autoOnNative ? '已开启' : '已关闭'),
|
|
3159
|
-
h('span', { style: settingsHintStyle }, native !== null
|
|
3537
|
+
h('span', { style: settingsHintStyle }, native !== null
|
|
3538
|
+
? (nativeInfo.autoOff
|
|
3539
|
+
? (nativeInfo.actualText !== ''
|
|
3540
|
+
? `原生阈值 配置 ${nativeInfo.configuredText ?? nativePercent}%(已关闭;开启后实际触发 ${nativeInfo.actualText}%)`
|
|
3541
|
+
: `原生阈值 配置 ${nativeInfo.configuredText ?? nativePercent}%(已关闭)`)
|
|
3542
|
+
: (nativeInfo.differs ? `原生阈值 配置 ${nativeInfo.configuredText}%(实际触发 ${nativeInfo.percentText}%)` : `原生阈值 ${nativeActualText}%`))
|
|
3543
|
+
: ''),
|
|
3160
3544
|
nativeStatus === null
|
|
3161
3545
|
? null
|
|
3162
3546
|
: h(
|
|
@@ -3193,9 +3577,9 @@ window.__ModuleLoader__.load({
|
|
|
3193
3577
|
{ style: { ...settingsHintStyle, marginBottom: 8 } },
|
|
3194
3578
|
confirmNative === 'disable'
|
|
3195
3579
|
? `操作内容:在 DSH profile 的 cordis.patch.yml 里把 compaction-basic 的 auto 改为 false(写前备份原文件,可回滚)。` +
|
|
3196
|
-
`改变:DSH 不再在容量到 ${
|
|
3580
|
+
`改变:DSH 不再在容量到 ${nativeActualText}% 时自动压缩历史;锁定的消息不会被压掉;本插件的提醒和手动压缩照常。需要重启 DSH 生效。`
|
|
3197
3581
|
: `操作内容:在 DSH profile 的 cordis.patch.yml 里把 compaction-basic 的 auto 改回 true(写前备份,可回滚)。` +
|
|
3198
|
-
`改变:DSH 重新在容量到 ${
|
|
3582
|
+
`改变:DSH 重新在容量到 ${nativeActualText}% 时自动压历史,可能压掉锁定的消息。需要重启 DSH 生效。`,
|
|
3199
3583
|
),
|
|
3200
3584
|
h(
|
|
3201
3585
|
'div',
|
|
@@ -3383,13 +3767,15 @@ window.__ModuleLoader__.load({
|
|
|
3383
3767
|
{ ratio: compactRatio + 0.08, hint: null, title: '进入压缩区', text: '琥珀:建议压缩旧会话' },
|
|
3384
3768
|
{ ratio: handoffRatio + 0.15, hint: null, title: '越过交接线', text: '红:建议交接新会话' },
|
|
3385
3769
|
];
|
|
3386
|
-
if (native !== null && native.autoEnabled !== false) {
|
|
3770
|
+
if (native !== null && native.autoEnabled !== false && nativeInfo.effective !== null) {
|
|
3387
3771
|
legend.push({
|
|
3388
|
-
ratio:
|
|
3772
|
+
ratio: Math.min(0.95, nativeInfo.effective + 0.06),
|
|
3389
3773
|
hint: null,
|
|
3390
|
-
nativeAt:
|
|
3774
|
+
nativeAt: nativeInfo.effective,
|
|
3391
3775
|
title: '原生自动压缩线',
|
|
3392
|
-
text:
|
|
3776
|
+
text: nativeInfo.differs
|
|
3777
|
+
? `实际 ${nativeInfo.percentText}%:DSH 会自己压历史(配置阈值 ${nativeInfo.configuredText}%,越过整环红色闪烁)`
|
|
3778
|
+
: `${nativeInfo.percentText}%:DSH 会自己压历史(越过整环红色闪烁)`,
|
|
3393
3779
|
});
|
|
3394
3780
|
}
|
|
3395
3781
|
return h(
|
|
@@ -3448,12 +3834,60 @@ window.__ModuleLoader__.load({
|
|
|
3448
3834
|
// 2026-10-20:0.3.1 已发布到 npm,帮助区直接给包页面入口。
|
|
3449
3835
|
githubLink('npm 包页面', 'https://www.npmjs.com/package/dsh-compact-relay', ''),
|
|
3450
3836
|
),
|
|
3837
|
+
// 0.4 P0-2:诊断信息 —— 本机生成、白名单、已脱敏;只提供「生成/预览/复制」。
|
|
3838
|
+
h(
|
|
3839
|
+
'div',
|
|
3840
|
+
{ style: { marginBottom: 18, paddingTop: 12, borderTop: '1px dashed var(--dsw-alias-border-l2, rgba(127,127,127,0.25))' }, 'data-cr-diagnostics': '1' },
|
|
3841
|
+
h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 4 } }, '诊断信息'),
|
|
3842
|
+
h('p', { style: { ...settingsHintStyle, margin: '0 0 2px' } }, '只在本机生成,不会自动外发;生成后可以先预览,再决定把哪部分粘贴到哪里。'),
|
|
3843
|
+
h(
|
|
3844
|
+
'p',
|
|
3845
|
+
{ style: settingsHintStyle },
|
|
3846
|
+
'内容只有版本、平台、配置数值、状态摘要、计数、耗时与错误码;不含聊天内容、摘要、交接单正文、文件路径、用户名、会话 id、密钥或 cookie。',
|
|
3847
|
+
),
|
|
3848
|
+
h(
|
|
3849
|
+
'div',
|
|
3850
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginTop: 8 } },
|
|
3851
|
+
h(
|
|
3852
|
+
'button',
|
|
3853
|
+
{
|
|
3854
|
+
type: 'button',
|
|
3855
|
+
'data-cr-diagnostics-generate': '1',
|
|
3856
|
+
style: settingsButtonStyle,
|
|
3857
|
+
disabled: diagLoading,
|
|
3858
|
+
onClick: () => void generateDiagnostics(),
|
|
3859
|
+
},
|
|
3860
|
+
diagLoading ? '生成中…' : '生成并预览',
|
|
3861
|
+
),
|
|
3862
|
+
h(
|
|
3863
|
+
'button',
|
|
3864
|
+
{ type: 'button', 'data-cr-diagnostics-issue': '1', style: settingsButtonStyle, onClick: () => void openIssueReport() },
|
|
3865
|
+
'打开问题报告页',
|
|
3866
|
+
),
|
|
3867
|
+
diagStatus === null
|
|
3868
|
+
? null
|
|
3869
|
+
: h(
|
|
3870
|
+
'span',
|
|
3871
|
+
{
|
|
3872
|
+
'data-cr-diagnostics-status': diagStatus.error ? 'error' : 'ok',
|
|
3873
|
+
style: {
|
|
3874
|
+
fontSize: 11,
|
|
3875
|
+
lineHeight: '16px',
|
|
3876
|
+
color: diagStatus.error ? 'var(--dsw-alias-state-error-primary, #D92D20)' : 'var(--dsw-alias-state-success-primary, #15803D)',
|
|
3877
|
+
},
|
|
3878
|
+
},
|
|
3879
|
+
diagStatus.text,
|
|
3880
|
+
),
|
|
3881
|
+
),
|
|
3882
|
+
h('p', { style: { ...settingsHintStyle, marginTop: 6 } }, 'issue 页是公开的;粘贴前请自己再看一眼。'),
|
|
3883
|
+
),
|
|
3451
3884
|
),
|
|
3452
3885
|
sectionHeading('关于与危险操作'),
|
|
3453
3886
|
h(
|
|
3454
3887
|
'div',
|
|
3455
3888
|
{ 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
3889
|
h('p', { style: settingsHintStyle }, `版本:${version || '—'}`),
|
|
3890
|
+
h('p', { style: settingsHintStyle, 'data-cr-ledger': '1' }, ledgerLine),
|
|
3457
3891
|
h('p', { style: settingsHintStyle }, `设置文件:${settingsPath || '—'}`),
|
|
3458
3892
|
h('p', { style: { ...settingsHintStyle, marginTop: 8 } }, '输入框左侧的上下文指示灯:看容量、提醒压缩/交接、锁住重要消息;到线了用 /bc 或 /handoff,重要内容用 /pin 锁住。'),
|
|
3459
3893
|
h('p', { style: settingsHintStyle }, '遇到 bug 或想提改进:走上面「帮助」里的 GitHub Issues;也欢迎直接发邮件。'),
|
|
@@ -3470,8 +3904,151 @@ window.__ModuleLoader__.load({
|
|
|
3470
3904
|
)
|
|
3471
3905
|
: h('button', { type: 'button', style: settingsButtonStyle, disabled: saving, onClick: () => setConfirmReset(true) }, '恢复默认'),
|
|
3472
3906
|
),
|
|
3473
|
-
)
|
|
3474
|
-
|
|
3907
|
+
),
|
|
3908
|
+
// 0.4 P0-2:诊断信息预览弹层(只读等宽文本 + 复制 Markdown/JSON)。
|
|
3909
|
+
// 只有用户点了「生成并预览」/「打开问题报告页」才会出现;关掉后本次会话不再显示首次说明卡。
|
|
3910
|
+
diagOpen && diagData !== null
|
|
3911
|
+
? h(
|
|
3912
|
+
'div',
|
|
3913
|
+
{
|
|
3914
|
+
'data-cr-diagnostics-modal': '1',
|
|
3915
|
+
style: {
|
|
3916
|
+
position: 'fixed',
|
|
3917
|
+
inset: 0,
|
|
3918
|
+
zIndex: 60,
|
|
3919
|
+
display: 'flex',
|
|
3920
|
+
alignItems: 'center',
|
|
3921
|
+
justifyContent: 'center',
|
|
3922
|
+
padding: 24,
|
|
3923
|
+
background: 'rgba(0,0,0,0.36)',
|
|
3924
|
+
},
|
|
3925
|
+
},
|
|
3926
|
+
h(
|
|
3927
|
+
'div',
|
|
3928
|
+
{
|
|
3929
|
+
role: 'dialog',
|
|
3930
|
+
'aria-modal': 'true',
|
|
3931
|
+
'aria-label': '诊断信息预览(脱敏)',
|
|
3932
|
+
style: {
|
|
3933
|
+
...panelChrome(theme),
|
|
3934
|
+
display: 'flex',
|
|
3935
|
+
flexDirection: 'column',
|
|
3936
|
+
width: '100%',
|
|
3937
|
+
maxWidth: 760,
|
|
3938
|
+
maxHeight: '80vh',
|
|
3939
|
+
borderRadius: 12,
|
|
3940
|
+
padding: '14px 16px',
|
|
3941
|
+
},
|
|
3942
|
+
},
|
|
3943
|
+
h(
|
|
3944
|
+
'div',
|
|
3945
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 } },
|
|
3946
|
+
h('div', { style: { fontSize: 13, fontWeight: 700, color: 'var(--dsw-alias-label-primary, inherit)' } }, '诊断信息预览(脱敏)'),
|
|
3947
|
+
h('button', { type: 'button', style: settingsButtonStyle, onClick: closeDiagnosticsPreview }, '关闭'),
|
|
3948
|
+
),
|
|
3949
|
+
diagIntroSeen
|
|
3950
|
+
? null
|
|
3951
|
+
: h(
|
|
3952
|
+
'div',
|
|
3953
|
+
{
|
|
3954
|
+
style: {
|
|
3955
|
+
marginTop: 10,
|
|
3956
|
+
padding: '8px 10px',
|
|
3957
|
+
borderRadius: 8,
|
|
3958
|
+
border: '1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,0.25))',
|
|
3959
|
+
background: 'var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))',
|
|
3960
|
+
},
|
|
3961
|
+
},
|
|
3962
|
+
h('div', { style: { fontSize: 12, fontWeight: 600, marginBottom: 4 } }, '第一次生成:先看三点'),
|
|
3963
|
+
h(
|
|
3964
|
+
'p',
|
|
3965
|
+
{ style: settingsHintStyle },
|
|
3966
|
+
'① 只在本机生成,插件不会自己发到任何地方;② 只有数值、枚举与错误码,不含聊天内容与文件路径;③ issue 页是公开的,粘贴前请自己再看一眼。',
|
|
3967
|
+
),
|
|
3968
|
+
),
|
|
3969
|
+
h(
|
|
3970
|
+
'pre',
|
|
3971
|
+
{
|
|
3972
|
+
'data-cr-diagnostics-preview': '1',
|
|
3973
|
+
'aria-label': '诊断信息 Markdown 预览(只读)',
|
|
3974
|
+
style: {
|
|
3975
|
+
flex: 1,
|
|
3976
|
+
minHeight: 120,
|
|
3977
|
+
overflow: 'auto',
|
|
3978
|
+
margin: '10px 0',
|
|
3979
|
+
padding: 10,
|
|
3980
|
+
borderRadius: 8,
|
|
3981
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))',
|
|
3982
|
+
background: 'var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))',
|
|
3983
|
+
color: 'inherit',
|
|
3984
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
3985
|
+
fontSize: 11,
|
|
3986
|
+
lineHeight: '16px',
|
|
3987
|
+
whiteSpace: 'pre-wrap',
|
|
3988
|
+
wordBreak: 'break-word',
|
|
3989
|
+
userSelect: 'text',
|
|
3990
|
+
},
|
|
3991
|
+
},
|
|
3992
|
+
typeof diagData.markdown === 'string' && diagData.markdown.length > 0 ? diagData.markdown : '(没有内容)',
|
|
3993
|
+
),
|
|
3994
|
+
h(
|
|
3995
|
+
'div',
|
|
3996
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
3997
|
+
h(
|
|
3998
|
+
'button',
|
|
3999
|
+
{ type: 'button', 'data-cr-diagnostics-copy': 'markdown', style: settingsButtonStyle, onClick: () => copyDiagnostics('markdown') },
|
|
4000
|
+
'复制 Markdown',
|
|
4001
|
+
),
|
|
4002
|
+
h(
|
|
4003
|
+
'button',
|
|
4004
|
+
{ type: 'button', 'data-cr-diagnostics-copy': 'json', style: settingsButtonStyle, onClick: () => copyDiagnostics('json') },
|
|
4005
|
+
'复制 JSON',
|
|
4006
|
+
),
|
|
4007
|
+
h('span', { style: settingsHintStyle }, 'issue 是公开的;粘贴前请自己再看一眼。'),
|
|
4008
|
+
diagStatus === null
|
|
4009
|
+
? null
|
|
4010
|
+
: h(
|
|
4011
|
+
'span',
|
|
4012
|
+
{
|
|
4013
|
+
'data-cr-diagnostics-copy-status': diagStatus.error ? 'error' : 'ok',
|
|
4014
|
+
style: {
|
|
4015
|
+
fontSize: 11,
|
|
4016
|
+
lineHeight: '16px',
|
|
4017
|
+
color: diagStatus.error
|
|
4018
|
+
? 'var(--dsw-alias-state-error-primary, #D92D20)'
|
|
4019
|
+
: 'var(--dsw-alias-state-success-primary, #15803D)',
|
|
4020
|
+
},
|
|
4021
|
+
},
|
|
4022
|
+
diagStatus.text,
|
|
4023
|
+
),
|
|
4024
|
+
),
|
|
4025
|
+
),
|
|
4026
|
+
)
|
|
4027
|
+
: null,
|
|
4028
|
+
// A4:复制/打开结果的短 toast(成功与失败都提示;自动消失,不挡操作)。
|
|
4029
|
+
diagStatus === null
|
|
4030
|
+
? null
|
|
4031
|
+
: h(
|
|
4032
|
+
'div',
|
|
4033
|
+
{
|
|
4034
|
+
'data-cr-diagnostics-toast': diagStatus.error ? 'error' : 'ok',
|
|
4035
|
+
role: 'status',
|
|
4036
|
+
style: {
|
|
4037
|
+
...panelChrome(theme),
|
|
4038
|
+
position: 'fixed',
|
|
4039
|
+
right: 24,
|
|
4040
|
+
bottom: 24,
|
|
4041
|
+
zIndex: 70,
|
|
4042
|
+
maxWidth: 360,
|
|
4043
|
+
padding: '8px 12px',
|
|
4044
|
+
borderRadius: 8,
|
|
4045
|
+
fontSize: 12,
|
|
4046
|
+
lineHeight: '17px',
|
|
4047
|
+
color: diagStatus.error ? 'var(--dsw-alias-state-error-primary, #D92D20)' : 'var(--dsw-alias-label-primary, inherit)',
|
|
4048
|
+
},
|
|
4049
|
+
},
|
|
4050
|
+
diagStatus.text,
|
|
4051
|
+
),
|
|
3475
4052
|
);
|
|
3476
4053
|
}
|
|
3477
4054
|
|