dsh-compact-relay 0.5.2 → 0.5.4
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 +55 -0
- package/README.en.md +3 -0
- package/README.md +28 -3
- package/lib/actions/better-compact.js +66 -6
- package/lib/actions/compact-run.js +54 -5
- package/lib/actions/favorite.js +435 -0
- package/lib/actions/force-archive.js +60 -12
- package/lib/actions/handoff.js +3 -1
- package/lib/actions/recover.js +17 -4
- package/lib/actions/state.js +6 -0
- package/lib/adapters/commands.js +36 -0
- package/lib/adapters/http.js +90 -12
- package/lib/client.js +654 -101
- package/lib/config.js +8 -0
- package/lib/core/config-schema.js +23 -0
- package/lib/core/errors.js +9 -3
- package/lib/core/favorite-score.js +220 -0
- package/lib/core/favorites.js +167 -0
- package/lib/core/relay-record.js +3 -0
- package/lib/dsh/favorite-scorer.js +91 -0
- package/lib/dsh/favorite-watch.js +244 -0
- package/lib/dsh/favorites-file.js +349 -0
- package/lib/dsh/relay.js +3 -0
- package/lib/dsh/select-range.js +2 -2
- package/lib/index.js +13 -2
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1129,6 +1129,11 @@ window.__ModuleLoader__.load({
|
|
|
1129
1129
|
const previewRef = useRef(null);
|
|
1130
1130
|
const previewAbortRef = useRef(null);
|
|
1131
1131
|
const runAbortRef = useRef(null);
|
|
1132
|
+
// 0.6 预览卡收藏区:只在「这张卡里点成功的收藏」记局部状态(seq → 行数据),
|
|
1133
|
+
// 不引入全局星标快照、不改服务端预览数据;失败提示也留在卡内。
|
|
1134
|
+
const [favoriteMarks, setFavoriteMarks] = useState({});
|
|
1135
|
+
const [favoritePendingSeq, setFavoritePendingSeq] = useState(null);
|
|
1136
|
+
const [favoriteError, setFavoriteError] = useState(null);
|
|
1132
1137
|
// M6:交接单列表(菜单入口,只读)。
|
|
1133
1138
|
const [docs, setDocs] = useState(null);
|
|
1134
1139
|
const docsRef = useRef(null);
|
|
@@ -1752,7 +1757,7 @@ window.__ModuleLoader__.load({
|
|
|
1752
1757
|
nativeRatio === null
|
|
1753
1758
|
? 'DSH 原生自动压缩已关闭'
|
|
1754
1759
|
: `DSH 原生自动压缩线在 ${(nativeRatio * 100).toFixed(1)}%,到那之前先压缩或交接更稳`;
|
|
1755
|
-
return
|
|
1760
|
+
return `这是上下文指示灯:点它压缩旧会话、收藏重要回复、交接新会话。${nativePart}。环上的小缺口是我判断「该压缩」的位置(模型判断或到线自动出现)。重要的结论/路径先点星收藏,压缩时会把原文写进摘要目录和快照(DSH 原生自动压缩不认收藏,被它压走的消息建议交接前留档)。交接后会默认自动归档旧会话(侧栏收起,可取消勾选「交接后归档」;旧会话在侧栏置顶时会先问你,因为归档会取消置顶)。`;
|
|
1756
1761
|
}, [state.native]);
|
|
1757
1762
|
|
|
1758
1763
|
/** 播放一次引导:呼吸光晕 + 提示。手动重播用更长时间(用户要关掉设置页才看得到)。 */
|
|
@@ -1803,19 +1808,6 @@ window.__ModuleLoader__.load({
|
|
|
1803
1808
|
return () => window.removeEventListener('compact-relay:replay-onboarding', onReplay);
|
|
1804
1809
|
}, [playOnboarding]);
|
|
1805
1810
|
|
|
1806
|
-
// 0.5.1 批 2:原生压缩即将覆盖锁定消息 —— 事前主动提示一次(同一风险签名只弹一次)。
|
|
1807
|
-
const pinRiskToastRef = useRef('');
|
|
1808
|
-
useEffect(() => {
|
|
1809
|
-
if (pinRisk === null) return;
|
|
1810
|
-
const signature = `${state.sessionId ?? ''}:${pinRisk.seqs.join(',')}`;
|
|
1811
|
-
if (signature === pinRiskToastRef.current) return;
|
|
1812
|
-
pinRiskToastRef.current = signature;
|
|
1813
|
-
showToast(
|
|
1814
|
-
`⚠ 原生压缩预计覆盖你锁定的 ${pinRisk.count} 条消息(通常在本轮回复收尾时触发)。建议先交接(/handoff)保留原文,或解锁让压缩带走。`,
|
|
1815
|
-
12000,
|
|
1816
|
-
);
|
|
1817
|
-
}, [pinRisk, state.sessionId, showToast]);
|
|
1818
|
-
|
|
1819
1811
|
// 0.4.2(P1):换模型后首次发现「交接线已晚」弹一次提示(同路由不重复打扰)。
|
|
1820
1812
|
// 只在「观察到路由变化」(localStorage 记着上一次的 provider/model)时弹;
|
|
1821
1813
|
// 首次见到某路由不弹(不是换模型导致的),设置页有常驻提示兜底。
|
|
@@ -1878,6 +1870,9 @@ window.__ModuleLoader__.load({
|
|
|
1878
1870
|
setOpen(false);
|
|
1879
1871
|
setHandoffPreview(null);
|
|
1880
1872
|
setPreview({ loading: true });
|
|
1873
|
+
setFavoriteMarks({});
|
|
1874
|
+
setFavoritePendingSeq(null);
|
|
1875
|
+
setFavoriteError(null);
|
|
1881
1876
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
1882
1877
|
previewAbortRef.current = controller;
|
|
1883
1878
|
try {
|
|
@@ -2173,6 +2168,52 @@ window.__ModuleLoader__.load({
|
|
|
2173
2168
|
[sessionId, waitAndOpen, preview, startPreview],
|
|
2174
2169
|
);
|
|
2175
2170
|
|
|
2171
|
+
// 0.6 预览卡「收藏」按钮:只有用户点击才发请求(绝不自动收藏)。
|
|
2172
|
+
// 走既有 action:'favorite-mark'(按 sessionId+seq,含用户发言、不需要活 agent);
|
|
2173
|
+
// 显式 active:true = 只加不切换——即使预览数据过期/重复点,也不会误删已有收藏。
|
|
2174
|
+
// 成功只在本卡做局部标记(该行变「已收藏 ✓」、计数本地更新),不重拉预览、不碰压缩计划。
|
|
2175
|
+
const markFavoriteFromPreview = useCallback(
|
|
2176
|
+
async (item) => {
|
|
2177
|
+
const seq = item !== null && typeof item === 'object' && Number.isInteger(item.seq) ? item.seq : null;
|
|
2178
|
+
if (seq === null || favoritePendingSeq !== null) return;
|
|
2179
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
2180
|
+
setFavoriteError('拿不到会话 id,先等会话就绪再收藏');
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
setFavoritePendingSeq(seq);
|
|
2184
|
+
setFavoriteError(null);
|
|
2185
|
+
try {
|
|
2186
|
+
const res = await fetch('/api/compact-relay-action', {
|
|
2187
|
+
method: 'POST',
|
|
2188
|
+
headers: { 'content-type': 'application/json' },
|
|
2189
|
+
body: JSON.stringify({ action: 'favorite-mark', sessionId, seq, active: true }),
|
|
2190
|
+
});
|
|
2191
|
+
const data = await res.json().catch(() => ({}));
|
|
2192
|
+
if (data !== null && typeof data === 'object' && data.ok !== false && data.active === true) {
|
|
2193
|
+
setFavoriteMarks((prev) => ({
|
|
2194
|
+
...prev,
|
|
2195
|
+
[seq]: {
|
|
2196
|
+
seq,
|
|
2197
|
+
role: typeof item.role === 'string' ? item.role : '',
|
|
2198
|
+
title: typeof item.title === 'string' && item.title.length > 0 ? item.title : typeof item.snippet === 'string' ? item.snippet : '',
|
|
2199
|
+
},
|
|
2200
|
+
}));
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
setFavoriteError(
|
|
2204
|
+
data !== null && typeof data === 'object' && typeof data.message === 'string' && data.message.length > 0
|
|
2205
|
+
? data.message
|
|
2206
|
+
: '收藏失败,请重试',
|
|
2207
|
+
);
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
setFavoriteError('收藏失败:网络或宿主不可用');
|
|
2210
|
+
} finally {
|
|
2211
|
+
setFavoritePendingSeq(null);
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
[sessionId, favoritePendingSeq],
|
|
2215
|
+
);
|
|
2216
|
+
|
|
2176
2217
|
const adviceText = adviseHandoff ? '建议交接' : adviseCompact ? '建议压缩' : '正常';
|
|
2177
2218
|
// 0.3.1:模型判断的断点位置(「这里适合压缩」);DSH 原生自动压缩线(报警最高阈值)。
|
|
2178
2219
|
const compressHintText =
|
|
@@ -2202,9 +2243,6 @@ window.__ModuleLoader__.load({
|
|
|
2202
2243
|
(compressHintText !== '' ? `|${compressHintText}` : '') +
|
|
2203
2244
|
(nativeLineText !== '' ? `|${nativeLineText}` : '') +
|
|
2204
2245
|
(handoffWarnText !== '' ? `|${handoffWarnText}` : '') +
|
|
2205
|
-
(pins.count > 0 ? `|已锁定 ${pins.count} 条` : '') +
|
|
2206
|
-
(pinStaleCount > 0 ? `|${staleLabel}` : '') +
|
|
2207
|
-
(pinRisk !== null ? `|⚠ ${pinRiskLabel}(建议先交接或解锁)` : '') +
|
|
2208
2246
|
(effectiveBusy ? '|处理中…' : '');
|
|
2209
2247
|
|
|
2210
2248
|
// 浮层定位:fixed(不受 composer 裁剪)+ 视口左右收边 + 上方空间不够时翻到下方。
|
|
@@ -2261,20 +2299,8 @@ window.__ModuleLoader__.load({
|
|
|
2261
2299
|
h(
|
|
2262
2300
|
'div',
|
|
2263
2301
|
{ style: { color: theme.subText } },
|
|
2264
|
-
`已压缩 ${state.summaries} 次 · ${adviceText}${compressHintText !== '' ? ` · ${compressHintText}` : ''}${nativeLineText !== '' ? ` · ${nativeLineText}` : ''}${
|
|
2302
|
+
`已压缩 ${state.summaries} 次 · ${adviceText}${compressHintText !== '' ? ` · ${compressHintText}` : ''}${nativeLineText !== '' ? ` · ${nativeLineText}` : ''}${effectiveBusy ? ' · 处理中…' : ''}`,
|
|
2265
2303
|
),
|
|
2266
|
-
// 0.5.1 批 2:失效 pin 明确标出来(不假装还在、不消失)。
|
|
2267
|
-
pinStaleCount > 0
|
|
2268
|
-
? h('div', { 'data-cr-pin-stale': '1', style: { marginTop: 2, color: theme.subText } }, staleLabel)
|
|
2269
|
-
: null,
|
|
2270
|
-
// 0.5.1 批 2:下一次原生自动压缩会覆盖锁定消息 → 事前提示(先交接/保留或解锁)。
|
|
2271
|
-
pinRisk !== null
|
|
2272
|
-
? h(
|
|
2273
|
-
'div',
|
|
2274
|
-
{ 'data-cr-pin-risk': '1', style: { marginTop: 2, color: palette.handoff } },
|
|
2275
|
-
`⚠ ${pinRiskLabel}:下一次原生压缩(通常在本轮回复收尾时)会压掉它们。建议先交接(/handoff)保留,或解锁让压缩带走。`,
|
|
2276
|
-
)
|
|
2277
|
-
: null,
|
|
2278
2304
|
// 0.4.2:交接线晚于原生压缩时,悬停卡里给一行显式警示(只告警,不改设置)。
|
|
2279
2305
|
handoffWarnText !== ''
|
|
2280
2306
|
? h('div', { 'data-cr-handoff-warn-text': '1', style: { marginTop: 2, color: palette.handoff } }, handoffWarnText)
|
|
@@ -2435,15 +2461,8 @@ window.__ModuleLoader__.load({
|
|
|
2435
2461
|
};
|
|
2436
2462
|
|
|
2437
2463
|
// 0.5.1 批 2:交接菜单项的提示(原生压缩预判风险优先于「交接线已晚」,两条都有就并排)。
|
|
2438
|
-
const handoffItemNote =
|
|
2439
|
-
|
|
2440
|
-
? `⚠ ${pinRiskLabel}:建议先交接保留`
|
|
2441
|
-
: handoffLate
|
|
2442
|
-
? handoffWarnText
|
|
2443
|
-
: '';
|
|
2444
|
-
const handoffItemTitle =
|
|
2445
|
-
[pinRisk !== null ? `原生压缩会在下一次触发时(通常本轮回复收尾)压掉你锁定的 ${pinRisk.count} 条消息。先交接可保留原文;也可以解锁让压缩带走。` : '',
|
|
2446
|
-
handoffLate ? handoffGuardInfo.message : ''].filter((text) => text !== '').join('\n') || undefined;
|
|
2464
|
+
const handoffItemNote = handoffLate ? handoffWarnText : '';
|
|
2465
|
+
const handoffItemTitle = handoffLate ? handoffGuardInfo.message : undefined;
|
|
2447
2466
|
// undefined=宿主没上报(老版本/未知);null=明确没有可压范围;object=可压多少。
|
|
2448
2467
|
const rangeInfo = state.compactRange;
|
|
2449
2468
|
// 0.3.5:有可压范围、但会话还没有活 agent(passivized live session)——preview 需要
|
|
@@ -2488,10 +2507,7 @@ window.__ModuleLoader__.load({
|
|
|
2488
2507
|
...(rangeInfo === null
|
|
2489
2508
|
? {
|
|
2490
2509
|
disabled: true,
|
|
2491
|
-
note:
|
|
2492
|
-
pins.count > 0
|
|
2493
|
-
? `暂无可压内容:有 ${pins.count} 条消息被锁定保护(先点「解锁消息」)`
|
|
2494
|
-
: '暂无可压缩内容(会话太短,或近期会话需保留原文)',
|
|
2510
|
+
note: '暂无可压缩内容(会话太短,或近期会话需保留原文)',
|
|
2495
2511
|
}
|
|
2496
2512
|
: passiveRangeNote !== null
|
|
2497
2513
|
? { disabled: true, title: passiveRangeNote, note: passiveRangeNote }
|
|
@@ -2503,35 +2519,6 @@ window.__ModuleLoader__.load({
|
|
|
2503
2519
|
},
|
|
2504
2520
|
},
|
|
2505
2521
|
),
|
|
2506
|
-
item(
|
|
2507
|
-
pins.count > 0
|
|
2508
|
-
? `解锁消息(${pins.count} 条)`
|
|
2509
|
-
: pinStaleCount > 0
|
|
2510
|
-
? `解除失效锁定(${pinStaleCount} 条)`
|
|
2511
|
-
: '锁定重要消息',
|
|
2512
|
-
pins.count > 0
|
|
2513
|
-
? '解除锁定,允许后续压缩合并这些消息'
|
|
2514
|
-
: pinStaleCount > 0
|
|
2515
|
-
? `${staleLabel}:原文已不在当前上下文,点此清空锁定记录`
|
|
2516
|
-
: '锁定的消息压缩时不会碰(重要结论/路径先锁住,也可 /pin list 挑一条)',
|
|
2517
|
-
'pin',
|
|
2518
|
-
null,
|
|
2519
|
-
hasSession
|
|
2520
|
-
? {
|
|
2521
|
-
oneLine: true,
|
|
2522
|
-
right:
|
|
2523
|
-
pins.count > 0
|
|
2524
|
-
? `已锁 ${pins.count} 条${pinStaleCount > 0 ? ` · 失效 ${pinStaleCount}` : ''}`
|
|
2525
|
-
: pinStaleCount > 0
|
|
2526
|
-
? `失效 ${pinStaleCount} 条`
|
|
2527
|
-
: '未锁定',
|
|
2528
|
-
...(pinRisk !== null ? { note: `⚠ ${pinRiskLabel}(建议先交接)`, title: pinRiskLabel } : {}),
|
|
2529
|
-
onClick: () => {
|
|
2530
|
-
void togglePins();
|
|
2531
|
-
},
|
|
2532
|
-
}
|
|
2533
|
-
: { oneLine: true, disabled: true, right: '未就绪', note: '会话还没就绪' },
|
|
2534
|
-
),
|
|
2535
2522
|
item(
|
|
2536
2523
|
'交接新会话',
|
|
2537
2524
|
'生成交接单并开新会话接着聊,旧会话保留(/handoff)',
|
|
@@ -2575,12 +2562,42 @@ window.__ModuleLoader__.load({
|
|
|
2575
2562
|
const previewRange = preview?.data?.preview ?? null;
|
|
2576
2563
|
// 0.3.1:范围太小时预览阶段就拦下(服务端已按 800 tokens 判定),不让用户白确认一次。
|
|
2577
2564
|
const previewTooSmall = previewRange !== null && previewRange.tooSmall === true;
|
|
2578
|
-
// 0.
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2565
|
+
// 0.6 预览卡收藏区:服务端 additive 字段 favoritePreview(老宿主没有 → 整块不渲染)。
|
|
2566
|
+
// 只读渲染;「建议补」= 本段未收藏的候选(服务端已用 pinSuggest 同一套识别过滤过)。
|
|
2567
|
+
const favoritePreview =
|
|
2568
|
+
previewRange !== null && previewRange.favoritePreview !== null && typeof previewRange.favoritePreview === 'object'
|
|
2569
|
+
? previewRange.favoritePreview
|
|
2570
|
+
: null;
|
|
2571
|
+
const favoriteRowOf = (item) =>
|
|
2572
|
+
item !== null && typeof item === 'object' && Number.isInteger(item.seq)
|
|
2573
|
+
? {
|
|
2574
|
+
seq: item.seq,
|
|
2575
|
+
role: typeof item.role === 'string' ? item.role : '',
|
|
2576
|
+
title: typeof item.title === 'string' ? item.title : '',
|
|
2577
|
+
reason: typeof item.reason === 'string' ? item.reason : '',
|
|
2578
|
+
snippet: typeof item.snippet === 'string' ? item.snippet : '',
|
|
2579
|
+
}
|
|
2582
2580
|
: null;
|
|
2583
|
-
const
|
|
2581
|
+
const favoriteBaseItems =
|
|
2582
|
+
favoritePreview !== null && Array.isArray(favoritePreview.favorited)
|
|
2583
|
+
? favoritePreview.favorited.map(favoriteRowOf).filter((item) => item !== null)
|
|
2584
|
+
: [];
|
|
2585
|
+
const favoriteSuggestList =
|
|
2586
|
+
favoritePreview !== null && Array.isArray(favoritePreview.suggest)
|
|
2587
|
+
? favoritePreview.suggest.map(favoriteRowOf).filter((item) => item !== null)
|
|
2588
|
+
: [];
|
|
2589
|
+
const favoriteRangeTotal =
|
|
2590
|
+
favoritePreview !== null && Number.isInteger(favoritePreview.total) ? favoritePreview.total : (previewRange?.nodes ?? 0);
|
|
2591
|
+
// 卡内点成功的收藏:补进「本段已收藏」列表,并从「建议补」计数里扣掉(本地即时,不重拉预览)。
|
|
2592
|
+
const favoriteExtraItems = favoriteSuggestList
|
|
2593
|
+
.filter((item) => favoriteMarks[item.seq] !== undefined)
|
|
2594
|
+
.map((item) => ({ seq: item.seq, role: item.role, title: item.snippet }));
|
|
2595
|
+
const favoriteDisplayItems = [...favoriteBaseItems, ...favoriteExtraItems];
|
|
2596
|
+
const favoriteFavoritedCount =
|
|
2597
|
+
(favoritePreview !== null && Number.isInteger(favoritePreview.favoritedCount)
|
|
2598
|
+
? favoritePreview.favoritedCount
|
|
2599
|
+
: favoriteBaseItems.length) + favoriteExtraItems.length;
|
|
2600
|
+
const favoriteSuggestRemaining = favoriteSuggestList.length - favoriteExtraItems.length;
|
|
2584
2601
|
const confirmPanel =
|
|
2585
2602
|
preview === null
|
|
2586
2603
|
? null
|
|
@@ -2630,43 +2647,139 @@ window.__ModuleLoader__.load({
|
|
|
2630
2647
|
h('br'),
|
|
2631
2648
|
'只估算范围和容量;摘要内容确认后才生成,不在这里展示。压缩后旧会话将替换为摘要,无法还原为原文。',
|
|
2632
2649
|
),
|
|
2633
|
-
|
|
2650
|
+
favoritePreview !== null
|
|
2634
2651
|
? h(
|
|
2635
2652
|
'div',
|
|
2636
2653
|
{
|
|
2637
|
-
'data-cr-
|
|
2654
|
+
'data-cr-favorite-preview': '1',
|
|
2638
2655
|
style: {
|
|
2639
2656
|
display: 'flex',
|
|
2640
2657
|
flexDirection: 'column',
|
|
2641
|
-
gap:
|
|
2658
|
+
gap: 4,
|
|
2642
2659
|
padding: '6px 8px',
|
|
2643
2660
|
borderRadius: 8,
|
|
2644
2661
|
border: `1px solid ${theme.panelBorder}`,
|
|
2645
2662
|
background: theme.hover,
|
|
2646
2663
|
},
|
|
2647
2664
|
},
|
|
2648
|
-
h('div', { style: { fontSize: 11, fontWeight: 700, color: theme.panelFg } }, `建议锁定 ${pinSuggest.items.length} 条(先 /pin 再压)`),
|
|
2649
|
-
...pinSuggest.items.map((item) =>
|
|
2650
|
-
h(
|
|
2651
|
-
'div',
|
|
2652
|
-
{ key: `pin-suggest-${item.seq}`, style: { fontSize: 11, lineHeight: '16px', color: theme.subText } },
|
|
2653
|
-
`${item.role} · ${item.reason}:${item.snippet}`,
|
|
2654
|
-
),
|
|
2655
|
-
),
|
|
2656
2665
|
h(
|
|
2657
2666
|
'div',
|
|
2658
|
-
{
|
|
2659
|
-
|
|
2667
|
+
{
|
|
2668
|
+
'data-cr-favorite-summary': '1',
|
|
2669
|
+
style: { fontSize: 11, fontWeight: 700, color: theme.panelFg },
|
|
2670
|
+
},
|
|
2671
|
+
`本段 ${favoriteRangeTotal} 条 · 已收藏 ${favoriteFavoritedCount} 条 · 建议补 ${favoriteSuggestRemaining} 条`,
|
|
2660
2672
|
),
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2673
|
+
favoriteDisplayItems.length > 0
|
|
2674
|
+
? h(
|
|
2675
|
+
'div',
|
|
2676
|
+
{ 'data-cr-favorite-list': '1', style: { display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
2677
|
+
h('div', { style: { fontSize: 10, fontWeight: 700, color: theme.subText } }, '本段已收藏'),
|
|
2678
|
+
...favoriteDisplayItems.map((item) =>
|
|
2679
|
+
h(
|
|
2680
|
+
'div',
|
|
2681
|
+
{
|
|
2682
|
+
key: `favorite-item-${item.seq}`,
|
|
2683
|
+
'data-cr-favorite-item': String(item.seq),
|
|
2684
|
+
style: {
|
|
2685
|
+
fontSize: 11,
|
|
2686
|
+
lineHeight: '16px',
|
|
2687
|
+
color: theme.subText,
|
|
2688
|
+
whiteSpace: 'normal',
|
|
2689
|
+
wordBreak: 'break-word',
|
|
2690
|
+
},
|
|
2691
|
+
},
|
|
2692
|
+
`[${item.role}] ${item.title}`,
|
|
2693
|
+
),
|
|
2694
|
+
),
|
|
2695
|
+
favoriteFavoritedCount > favoriteDisplayItems.length
|
|
2696
|
+
? h(
|
|
2697
|
+
'div',
|
|
2698
|
+
{ style: { fontSize: 10, lineHeight: '14px', color: theme.subText } },
|
|
2699
|
+
`…另有 ${favoriteFavoritedCount - favoriteDisplayItems.length} 条,用 /favorite list 查看`,
|
|
2700
|
+
)
|
|
2701
|
+
: null,
|
|
2702
|
+
)
|
|
2703
|
+
: null,
|
|
2704
|
+
favoriteSuggestList.length > 0
|
|
2705
|
+
? h(
|
|
2706
|
+
'div',
|
|
2707
|
+
{ 'data-cr-favorite-suggest': '1', style: { display: 'flex', flexDirection: 'column', gap: 3 } },
|
|
2708
|
+
h('div', { style: { fontSize: 10, fontWeight: 700, color: theme.subText } }, '建议先收藏(本段未收藏)'),
|
|
2709
|
+
...favoriteSuggestList.map((item) => {
|
|
2710
|
+
const done = favoriteMarks[item.seq] !== undefined;
|
|
2711
|
+
const pending = favoritePendingSeq === item.seq;
|
|
2712
|
+
return h(
|
|
2713
|
+
'div',
|
|
2714
|
+
{
|
|
2715
|
+
key: `favorite-suggest-${item.seq}`,
|
|
2716
|
+
'data-cr-favorite-suggest-row': String(item.seq),
|
|
2717
|
+
style: { display: 'flex', alignItems: 'flex-start', gap: 6 },
|
|
2718
|
+
},
|
|
2719
|
+
h(
|
|
2720
|
+
'span',
|
|
2721
|
+
{
|
|
2722
|
+
style: {
|
|
2723
|
+
flex: 1,
|
|
2724
|
+
minWidth: 0,
|
|
2725
|
+
fontSize: 11,
|
|
2726
|
+
lineHeight: '16px',
|
|
2727
|
+
color: theme.subText,
|
|
2728
|
+
whiteSpace: 'normal',
|
|
2729
|
+
wordBreak: 'break-word',
|
|
2730
|
+
},
|
|
2731
|
+
},
|
|
2732
|
+
`[${item.role}] ${item.reason}:${item.snippet}`,
|
|
2733
|
+
),
|
|
2734
|
+
done
|
|
2735
|
+
? h(
|
|
2736
|
+
'span',
|
|
2737
|
+
{
|
|
2738
|
+
'data-cr-favorite-done': String(item.seq),
|
|
2739
|
+
style: { flex: 'none', fontSize: 10, fontWeight: 600, lineHeight: '16px', color: '#2fb344' },
|
|
2740
|
+
},
|
|
2741
|
+
'已收藏 ✓',
|
|
2742
|
+
)
|
|
2743
|
+
: h(
|
|
2744
|
+
'button',
|
|
2745
|
+
{
|
|
2746
|
+
type: 'button',
|
|
2747
|
+
'data-cr-favorite-mark': String(item.seq),
|
|
2748
|
+
disabled: favoritePendingSeq !== null,
|
|
2749
|
+
onClick: () => {
|
|
2750
|
+
void markFavoriteFromPreview(item);
|
|
2751
|
+
},
|
|
2752
|
+
style: {
|
|
2753
|
+
flex: 'none',
|
|
2754
|
+
border: `1px solid ${theme.panelBorder}`,
|
|
2755
|
+
borderRadius: 6,
|
|
2756
|
+
padding: '1px 8px',
|
|
2757
|
+
fontSize: 10,
|
|
2758
|
+
lineHeight: '16px',
|
|
2759
|
+
cursor: favoritePendingSeq === null ? 'pointer' : 'default',
|
|
2760
|
+
background: 'transparent',
|
|
2761
|
+
color: theme.panelFg,
|
|
2762
|
+
opacity: favoritePendingSeq === null ? 1 : 0.5,
|
|
2763
|
+
},
|
|
2764
|
+
},
|
|
2765
|
+
pending ? '收藏中…' : '收藏',
|
|
2766
|
+
),
|
|
2767
|
+
);
|
|
2768
|
+
}),
|
|
2769
|
+
h(
|
|
2770
|
+
'div',
|
|
2771
|
+
{ 'data-cr-favorite-note': '1', style: { fontSize: 10, lineHeight: '14px', color: theme.subText } },
|
|
2772
|
+
'只提示、不自动收藏;点「收藏」只写一条收藏标记,不改压缩范围。',
|
|
2773
|
+
),
|
|
2774
|
+
)
|
|
2775
|
+
: null,
|
|
2776
|
+
favoriteError !== null
|
|
2777
|
+
? h(
|
|
2778
|
+
'div',
|
|
2779
|
+
{ 'data-cr-favorite-error': '1', style: { fontSize: 10, lineHeight: '14px', color: COLORS.error } },
|
|
2780
|
+
favoriteError,
|
|
2781
|
+
)
|
|
2782
|
+
: null,
|
|
2670
2783
|
)
|
|
2671
2784
|
: null,
|
|
2672
2785
|
h(
|
|
@@ -5223,6 +5336,288 @@ window.__ModuleLoader__.load({
|
|
|
5223
5336
|
menu,
|
|
5224
5337
|
);
|
|
5225
5338
|
}
|
|
5339
|
+
/**
|
|
5340
|
+
* 收藏星标(0.6)—— 挂在助手消息底部的动作行(conversation.chat.assistant-actions)。
|
|
5341
|
+
*
|
|
5342
|
+
* 数据:GET /api/compact-relay-state?sessionId=… 的 favorites.messageIds(判断是否已收藏);
|
|
5343
|
+
* 点击:POST /api/compact-relay-action { action:'favorite-toggle', messageId, sessionId }。
|
|
5344
|
+
*
|
|
5345
|
+
* 2026-10-29 真机 BUG:以前 state 请求不带 sessionId,宿主回「最后一个活 agent」的会话
|
|
5346
|
+
* (多工作区同时开着时那是别的会话)→ 点星 POST 到错会话 → 「找不到这条消息」→ 点了没反应。
|
|
5347
|
+
* 现在会话 id 只认 slot 的 session 作用域注入(register 的 inject),state 也按它请求。
|
|
5348
|
+
* 另外:点失败要出声(宿主可能回「已被压缩」/「没有活动会话」),不再静默。
|
|
5349
|
+
*/
|
|
5350
|
+
const FAVORITE_STAR_PATH = 'M12 2.8l2.82 5.72 6.31.92-4.57 4.45 1.08 6.29L12 17.2l-5.64 2.98 1.08-6.29-4.57-4.45 6.31-.92z';
|
|
5351
|
+
|
|
5352
|
+
// 收藏状态共享快照:几十个星标组件共用一个请求(避免每条助手消息各发一次 GET)。
|
|
5353
|
+
// 30s 内复用;并发挂载共用同一个 pending Promise。快照按 sessionId 区分(切会话不会串)。
|
|
5354
|
+
const FAVORITE_SNAPSHOT_TTL_MS = 30000;
|
|
5355
|
+
let favoriteSnapshot = null; // { sessionId, ids, suggested, freshUntil }
|
|
5356
|
+
let favoriteSnapshotPending = null;
|
|
5357
|
+
let favoriteSnapshotPendingSession = null;
|
|
5358
|
+
/** 点亮色(配置 colors.base.favorite;设计稿到位后改配置值即可)。 */
|
|
5359
|
+
let favoriteTint = '#F5C542';
|
|
5360
|
+
|
|
5361
|
+
/**
|
|
5362
|
+
* 推荐态样式只注入一次:金色呼吸光晕(0.5.3 UI)。
|
|
5363
|
+
* 静态 1.5px 细环在长消息流里太容易漏看;改成 2.4s 呼吸 + 更亮的峰值。
|
|
5364
|
+
* 系统开了「减少动态效果」就退回静态加亮版本。
|
|
5365
|
+
*/
|
|
5366
|
+
function ensureFavoriteStyle() {
|
|
5367
|
+
try {
|
|
5368
|
+
if (typeof document === 'undefined' || document.head === null) return;
|
|
5369
|
+
if (document.getElementById('dcr-favorite-style') !== null) return;
|
|
5370
|
+
const style = document.createElement('style');
|
|
5371
|
+
style.id = 'dcr-favorite-style';
|
|
5372
|
+
style.textContent =
|
|
5373
|
+
'@keyframes dcr-favorite-pulse {' +
|
|
5374
|
+
'0%,100% { box-shadow: 0 0 0 2px rgba(245,197,66,0.78), 0 0 6px rgba(245,197,66,0.45); }' +
|
|
5375
|
+
'50% { box-shadow: 0 0 0 2.5px rgba(245,197,66,0.98), 0 0 12px rgba(245,197,66,0.85); }' +
|
|
5376
|
+
'}' +
|
|
5377
|
+
// 与宿主助手消息动作行(._action)完全对齐:基础用 label-tertiary,
|
|
5378
|
+
// hover 切 label-secondary + 交互背景(与邻座逐项相同)。
|
|
5379
|
+
'.dcr-favorite-star { background: transparent; color: var(--dsw-alias-label-tertiary, currentColor); }' +
|
|
5380
|
+
'.dcr-favorite-star:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16)); color: var(--dsw-alias-label-secondary, currentColor); }' +
|
|
5381
|
+
'@media (prefers-reduced-motion: reduce) { .dcr-favorite-star { animation: none !important; } }';
|
|
5382
|
+
document.head.appendChild(style);
|
|
5383
|
+
} catch (error) {
|
|
5384
|
+
/* 样式注入失败不影响收藏功能 */
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
ensureFavoriteStyle();
|
|
5388
|
+
|
|
5389
|
+
/** 轻提示:收藏失败/被压缩时告诉用户一声(不依赖宿主 toast API)。 */
|
|
5390
|
+
function favoriteNotify(text) {
|
|
5391
|
+
try {
|
|
5392
|
+
if (typeof document === 'undefined' || document.body === null) return;
|
|
5393
|
+
const el = document.createElement('div');
|
|
5394
|
+
el.textContent = text;
|
|
5395
|
+
el.style.cssText =
|
|
5396
|
+
'position:fixed;left:50%;bottom:32px;transform:translateX(-50%);z-index:2147483000;max-width:80vw;' +
|
|
5397
|
+
'padding:8px 14px;border-radius:8px;background:rgba(30,30,30,0.92);color:#fff;font-size:13px;' +
|
|
5398
|
+
'line-height:1.4;box-shadow:0 6px 24px rgba(0,0,0,0.25);pointer-events:none;';
|
|
5399
|
+
document.body.appendChild(el);
|
|
5400
|
+
setTimeout(() => {
|
|
5401
|
+
try {
|
|
5402
|
+
el.remove();
|
|
5403
|
+
} catch (error) {
|
|
5404
|
+
/* ignore */
|
|
5405
|
+
}
|
|
5406
|
+
}, 4000);
|
|
5407
|
+
} catch (error) {
|
|
5408
|
+
/* 提示失败不影响收藏 */
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5412
|
+
/**
|
|
5413
|
+
* 收藏列表(按会话缓存)。
|
|
5414
|
+
* @param {string|null} sessionId 当前会话(来自 slot 注入;拿不到就退回旧行为)
|
|
5415
|
+
* @returns {Promise<string[]|null>}
|
|
5416
|
+
*/
|
|
5417
|
+
function favoriteIdsOf(sessionId) {
|
|
5418
|
+
const sid = typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null;
|
|
5419
|
+
if (favoriteSnapshot !== null && favoriteSnapshot.sessionId === sid && Date.now() < favoriteSnapshot.freshUntil) {
|
|
5420
|
+
return Promise.resolve(favoriteSnapshot.ids);
|
|
5421
|
+
}
|
|
5422
|
+
if (favoriteSnapshotPending !== null && favoriteSnapshotPendingSession === sid) return favoriteSnapshotPending;
|
|
5423
|
+
favoriteSnapshotPendingSession = sid;
|
|
5424
|
+
favoriteSnapshotPending = (async () => {
|
|
5425
|
+
try {
|
|
5426
|
+
const url =
|
|
5427
|
+
sid === null ? '/api/compact-relay-state' : `/api/compact-relay-state?sessionId=${encodeURIComponent(sid)}`;
|
|
5428
|
+
const res = await fetch(url);
|
|
5429
|
+
const data = await res.json();
|
|
5430
|
+
const fav = data !== null && typeof data === 'object' ? data.favorites : null;
|
|
5431
|
+
const ids = fav !== null && typeof fav === 'object' && Array.isArray(fav.messageIds) ? fav.messageIds : [];
|
|
5432
|
+
// 0.5.3 P1-6:推荐收藏建议(只点亮星标,不写收藏)。按 messageId 建索引,组件 O(1) 查。
|
|
5433
|
+
const suggested = {};
|
|
5434
|
+
const suggestList =
|
|
5435
|
+
data !== null && typeof data === 'object' && Array.isArray(data.favoriteSuggest) ? data.favoriteSuggest : [];
|
|
5436
|
+
for (const item of suggestList) {
|
|
5437
|
+
if (item === null || typeof item !== 'object') continue;
|
|
5438
|
+
if (typeof item.messageId !== 'string' || item.messageId.length === 0) continue;
|
|
5439
|
+
suggested[item.messageId] = {
|
|
5440
|
+
score: typeof item.score === 'number' ? item.score : null,
|
|
5441
|
+
reason: typeof item.reason === 'string' ? item.reason : '',
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5444
|
+
// 只有「没带 sessionId」的兜底请求才信响应里的会话;带了就以请求的为准。
|
|
5445
|
+
const effectiveSid =
|
|
5446
|
+
sid !== null ? sid : data !== null && typeof data === 'object' && typeof data.sessionId === 'string' ? data.sessionId : null;
|
|
5447
|
+
const color = data !== null && typeof data === 'object' && data.colors && data.colors.base ? data.colors.base.favorite : null;
|
|
5448
|
+
if (typeof color === 'string' && color.length > 0) favoriteTint = color;
|
|
5449
|
+
favoriteSnapshot = {
|
|
5450
|
+
sessionId: effectiveSid,
|
|
5451
|
+
ids: ids.slice(),
|
|
5452
|
+
suggested,
|
|
5453
|
+
freshUntil: Date.now() + FAVORITE_SNAPSHOT_TTL_MS,
|
|
5454
|
+
};
|
|
5455
|
+
return favoriteSnapshot.ids;
|
|
5456
|
+
} catch (error) {
|
|
5457
|
+
return null;
|
|
5458
|
+
} finally {
|
|
5459
|
+
favoriteSnapshotPending = null;
|
|
5460
|
+
favoriteSnapshotPendingSession = null;
|
|
5461
|
+
}
|
|
5462
|
+
})();
|
|
5463
|
+
return favoriteSnapshotPending;
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
// 点击后同步共享快照,避免同屏其它星标(或稍后挂载的)显示旧状态。
|
|
5467
|
+
function rememberFavorite(messageId, active) {
|
|
5468
|
+
if (favoriteSnapshot === null) return;
|
|
5469
|
+
const ids = favoriteSnapshot.ids;
|
|
5470
|
+
const index = ids.indexOf(messageId);
|
|
5471
|
+
if (active && index === -1) ids.push(messageId);
|
|
5472
|
+
else if (!active && index !== -1) ids.splice(index, 1);
|
|
5473
|
+
// 已收藏的条目不再算「推荐」(取消收藏后本回合内也不重新点亮,等下一条回复)。
|
|
5474
|
+
if (active && favoriteSnapshot.suggested !== null && typeof favoriteSnapshot.suggested === 'object') {
|
|
5475
|
+
delete favoriteSnapshot.suggested[messageId];
|
|
5476
|
+
}
|
|
5477
|
+
favoriteSnapshot.freshUntil = Date.now() + FAVORITE_SNAPSHOT_TTL_MS;
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
/** 查某条消息是否有推荐(当前快照会话内);返回 {score, reason} 或 null。 */
|
|
5481
|
+
function favoriteSuggestionOf(sessionId, messageId) {
|
|
5482
|
+
if (favoriteSnapshot === null || messageId === null) return null;
|
|
5483
|
+
if (favoriteSnapshot.sessionId !== sessionId) return null;
|
|
5484
|
+
const suggested = favoriteSnapshot.suggested;
|
|
5485
|
+
if (suggested === null || typeof suggested !== 'object') return null;
|
|
5486
|
+
const hit = suggested[messageId];
|
|
5487
|
+
return hit !== undefined && hit !== null ? hit : null;
|
|
5488
|
+
}
|
|
5489
|
+
|
|
5490
|
+
function FavoriteStar(props) {
|
|
5491
|
+
const messageId =
|
|
5492
|
+
props !== null && props !== undefined && typeof props.messageId === 'string' ? props.messageId : null;
|
|
5493
|
+
const sessionId =
|
|
5494
|
+
props !== null && props !== undefined && typeof props.sessionId === 'string' && props.sessionId.length > 0
|
|
5495
|
+
? props.sessionId
|
|
5496
|
+
: null;
|
|
5497
|
+
const [active, setActive] = useState(false);
|
|
5498
|
+
const [busy, setBusy] = useState(false);
|
|
5499
|
+
const [tint, setTint] = useState('#F5C542');
|
|
5500
|
+
const [suggested, setSuggested] = useState(null);
|
|
5501
|
+
|
|
5502
|
+
useEffect(() => {
|
|
5503
|
+
if (messageId === null) return undefined;
|
|
5504
|
+
let cancelled = false;
|
|
5505
|
+
(async () => {
|
|
5506
|
+
const ids = await favoriteIdsOf(sessionId);
|
|
5507
|
+
if (cancelled || ids === null) return;
|
|
5508
|
+
setActive(ids.includes(messageId));
|
|
5509
|
+
setSuggested(favoriteSuggestionOf(sessionId, messageId));
|
|
5510
|
+
setTint(favoriteTint);
|
|
5511
|
+
})();
|
|
5512
|
+
return () => {
|
|
5513
|
+
cancelled = true;
|
|
5514
|
+
};
|
|
5515
|
+
}, [messageId, sessionId]);
|
|
5516
|
+
|
|
5517
|
+
const toggle = useCallback(async () => {
|
|
5518
|
+
if (messageId === null || busy) return undefined;
|
|
5519
|
+
setBusy(true);
|
|
5520
|
+
try {
|
|
5521
|
+
const res = await fetch('/api/compact-relay-action', {
|
|
5522
|
+
method: 'POST',
|
|
5523
|
+
headers: { 'content-type': 'application/json' },
|
|
5524
|
+
body: JSON.stringify({
|
|
5525
|
+
action: 'favorite-toggle',
|
|
5526
|
+
messageId,
|
|
5527
|
+
...(sessionId === null ? {} : { sessionId }),
|
|
5528
|
+
}),
|
|
5529
|
+
});
|
|
5530
|
+
const data = await res.json();
|
|
5531
|
+
if (data !== null && typeof data === 'object' && typeof data.active === 'boolean') {
|
|
5532
|
+
setActive(data.active);
|
|
5533
|
+
if (data.active) setSuggested(null);
|
|
5534
|
+
rememberFavorite(messageId, data.active);
|
|
5535
|
+
return undefined;
|
|
5536
|
+
}
|
|
5537
|
+
// 失败要出声:宿主回「找不到这条消息(会话可能被压缩过)」等具体原因。
|
|
5538
|
+
const message =
|
|
5539
|
+
data !== null && typeof data === 'object' && typeof data.message === 'string' && data.message.length > 0
|
|
5540
|
+
? data.message
|
|
5541
|
+
: '收藏失败:请重试';
|
|
5542
|
+
favoriteNotify(message);
|
|
5543
|
+
return undefined;
|
|
5544
|
+
} catch (error) {
|
|
5545
|
+
favoriteNotify('收藏失败:网络或宿主不可用');
|
|
5546
|
+
return undefined;
|
|
5547
|
+
} finally {
|
|
5548
|
+
setBusy(false);
|
|
5549
|
+
}
|
|
5550
|
+
}, [messageId, sessionId, busy]);
|
|
5551
|
+
|
|
5552
|
+
if (messageId === null) return null;
|
|
5553
|
+
const isSuggested = active !== true && suggested !== null;
|
|
5554
|
+
const label = active
|
|
5555
|
+
? '取消收藏'
|
|
5556
|
+
: isSuggested
|
|
5557
|
+
? `推荐收藏${suggested.score !== null ? `(${suggested.score} 分)` : ''}`
|
|
5558
|
+
: '收藏这条';
|
|
5559
|
+
return h(
|
|
5560
|
+
'button',
|
|
5561
|
+
{
|
|
5562
|
+
type: 'button',
|
|
5563
|
+
className: 'dcr-favorite-star',
|
|
5564
|
+
title: label,
|
|
5565
|
+
'aria-label': label,
|
|
5566
|
+
'aria-pressed': active ? 'true' : 'false',
|
|
5567
|
+
onClick: toggle,
|
|
5568
|
+
disabled: busy,
|
|
5569
|
+
style: {
|
|
5570
|
+
display: 'inline-flex',
|
|
5571
|
+
alignItems: 'center',
|
|
5572
|
+
justifyContent: 'center',
|
|
5573
|
+
width: isSuggested ? 'auto' : '28px',
|
|
5574
|
+
height: '28px',
|
|
5575
|
+
gap: isSuggested ? '4px' : '0',
|
|
5576
|
+
padding: isSuggested ? '6px 8px 6px 6px' : '6px',
|
|
5577
|
+
margin: '0',
|
|
5578
|
+
border: 'none',
|
|
5579
|
+
cursor: busy ? 'default' : 'pointer',
|
|
5580
|
+
...(active || isSuggested ? { color: tint } : {}),
|
|
5581
|
+
opacity: busy ? 0.5 : 1,
|
|
5582
|
+
borderRadius: '8px',
|
|
5583
|
+
// 推荐态:金色光晕(只点亮、不自动收藏;点击仍走同一 toggle)。
|
|
5584
|
+
boxShadow: isSuggested ? '0 0 0 2px rgba(245,197,66,0.9), 0 0 8px rgba(245,197,66,0.6)' : 'none',
|
|
5585
|
+
animation: isSuggested ? 'dcr-favorite-pulse 2.4s ease-in-out infinite' : 'none',
|
|
5586
|
+
lineHeight: 1,
|
|
5587
|
+
verticalAlign: 'middle',
|
|
5588
|
+
},
|
|
5589
|
+
},
|
|
5590
|
+
h(
|
|
5591
|
+
'svg',
|
|
5592
|
+
{
|
|
5593
|
+
width: 16,
|
|
5594
|
+
height: 16,
|
|
5595
|
+
viewBox: '0 0 24 24',
|
|
5596
|
+
fill: active ? 'currentColor' : 'none',
|
|
5597
|
+
stroke: 'currentColor',
|
|
5598
|
+
strokeWidth: active ? 0 : 1.8,
|
|
5599
|
+
strokeLinejoin: 'round',
|
|
5600
|
+
},
|
|
5601
|
+
h('path', { d: FAVORITE_STAR_PATH }),
|
|
5602
|
+
),
|
|
5603
|
+
// 推荐态:加一个金色「推荐」小标签(用户确认要提高可见度,允许轻微打断阅读)。
|
|
5604
|
+
// 标签在按钮内部 —— 点击任意位置都走同一个 toggle,不新增交互面。
|
|
5605
|
+
isSuggested
|
|
5606
|
+
? h(
|
|
5607
|
+
'span',
|
|
5608
|
+
{
|
|
5609
|
+
style: {
|
|
5610
|
+
fontSize: '11px',
|
|
5611
|
+
lineHeight: '1.5',
|
|
5612
|
+
letterSpacing: '0.02em',
|
|
5613
|
+
whiteSpace: 'nowrap',
|
|
5614
|
+
},
|
|
5615
|
+
},
|
|
5616
|
+
'推荐',
|
|
5617
|
+
)
|
|
5618
|
+
: null,
|
|
5619
|
+
);
|
|
5620
|
+
}
|
|
5226
5621
|
|
|
5227
5622
|
/** 交接结果的一行式展示(放在聊天流里的 /handoff 命令卡)。 */
|
|
5228
5623
|
function handoffRow(text, tone, action) {
|
|
@@ -5508,6 +5903,10 @@ window.__ModuleLoader__.load({
|
|
|
5508
5903
|
const [nativeBusy, setNativeBusy] = useState(false);
|
|
5509
5904
|
/** UI-003:结果提示就地显示在开关旁(以前只写页面底部,用户看不到)。 */
|
|
5510
5905
|
const [nativeStatus, setNativeStatus] = useState(null);
|
|
5906
|
+
// 0.5.3 P1-7:推荐收藏「试算」的输入与结果(只读,不写收藏)。
|
|
5907
|
+
const [trialText, setTrialText] = useState('');
|
|
5908
|
+
const [trialResult, setTrialResult] = useState(null);
|
|
5909
|
+
const [trialBusy, setTrialBusy] = useState(false);
|
|
5511
5910
|
/** 作者 2026-10-20:设置页「再看一次初次引导」的就地提示。 */
|
|
5512
5911
|
const [onboardingNotice, setOnboardingNotice] = useState(null);
|
|
5513
5912
|
const nativeConfirmRef = useRef(null);
|
|
@@ -5675,6 +6074,11 @@ window.__ModuleLoader__.load({
|
|
|
5675
6074
|
|
|
5676
6075
|
/** 表单与已保存快照不一致 → 有未保存修改。 */
|
|
5677
6076
|
const dirty = form !== null && savedSnapshot !== null && JSON.stringify(form) !== savedSnapshot;
|
|
6077
|
+
// 0.5.3 P1-6:推荐收藏表单(旧设置 payload 可能没有 favorite 块 → 用默认值兜底,不崩)。
|
|
6078
|
+
const favoriteForm =
|
|
6079
|
+
form !== null && typeof form === 'object' && form.favorite !== null && form.favorite !== undefined && typeof form.favorite === 'object'
|
|
6080
|
+
? form.favorite
|
|
6081
|
+
: { enabled: true, level: 'standard', mustWords: [], muteWords: [] };
|
|
5678
6082
|
|
|
5679
6083
|
/**
|
|
5680
6084
|
* 保存设置。0.4.2:body 带上 `nativeRatio`(当前模型实际触发线),
|
|
@@ -5778,6 +6182,64 @@ window.__ModuleLoader__.load({
|
|
|
5778
6182
|
[nativeBusy, load],
|
|
5779
6183
|
);
|
|
5780
6184
|
|
|
6185
|
+
/** 0.5.3 P1-7:词表输入(逗号/空格分隔)→ 规范化数组。 */
|
|
6186
|
+
const parseFavoriteWords = (text) =>
|
|
6187
|
+
String(text)
|
|
6188
|
+
.split(/[,,、\s]+/)
|
|
6189
|
+
.map((word) => word.trim())
|
|
6190
|
+
.filter((word) => word.length > 0)
|
|
6191
|
+
.slice(0, 20);
|
|
6192
|
+
|
|
6193
|
+
/** 0.5.3 P1-7:试算——按当前表单里的档位/词表跑一次打分,只读结果。 */
|
|
6194
|
+
const runTrial = useCallback(async () => {
|
|
6195
|
+
if (trialBusy) return;
|
|
6196
|
+
const text = trialText.trim();
|
|
6197
|
+
if (text.length === 0) {
|
|
6198
|
+
setTrialResult({ error: true, text: '先粘一段文本再试算' });
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
setTrialBusy(true);
|
|
6202
|
+
setTrialResult(null);
|
|
6203
|
+
try {
|
|
6204
|
+
// 试算需要模型路由:先取当前活会话 id(设置页不是会话作用域)。
|
|
6205
|
+
const stateRes = await fetch('/api/compact-relay-state', { cache: 'no-store' });
|
|
6206
|
+
const state = await stateRes.json().catch(() => ({}));
|
|
6207
|
+
const sessionId = state !== null && typeof state === 'object' && typeof state.sessionId === 'string' ? state.sessionId : '';
|
|
6208
|
+
if (sessionId.length === 0) throw new Error('没有活动会话,先发一条消息唤醒 agent 再试算');
|
|
6209
|
+
const res = await fetch('/api/compact-relay-action', {
|
|
6210
|
+
method: 'POST',
|
|
6211
|
+
headers: { 'content-type': 'application/json' },
|
|
6212
|
+
body: JSON.stringify({
|
|
6213
|
+
action: 'favorite-score-trial',
|
|
6214
|
+
sessionId,
|
|
6215
|
+
text,
|
|
6216
|
+
level: favoriteForm.level,
|
|
6217
|
+
mustWords: favoriteForm.mustWords,
|
|
6218
|
+
muteWords: favoriteForm.muteWords,
|
|
6219
|
+
}),
|
|
6220
|
+
});
|
|
6221
|
+
const data = await res.json().catch(() => ({}));
|
|
6222
|
+
if (!res.ok || data.ok === false) throw new Error(data && data.message ? String(data.message) : '试算失败');
|
|
6223
|
+
const scoreText = typeof data.score === 'number' ? `${data.score} 分` : '没拿到分数';
|
|
6224
|
+
const verdict = data.suggest === true ? '会点亮' : '不点亮';
|
|
6225
|
+
const why =
|
|
6226
|
+
data.decisionReason === 'mute-word'
|
|
6227
|
+
? `(命中屏蔽词「${data.matched}」)`
|
|
6228
|
+
: data.decisionReason === 'must-word'
|
|
6229
|
+
? `(命中必亮词「${data.matched}」)`
|
|
6230
|
+
: data.decisionReason === 'too-short'
|
|
6231
|
+
? '(太短)'
|
|
6232
|
+
: data.decisionReason === 'unknown-score'
|
|
6233
|
+
? '(模型没给分)'
|
|
6234
|
+
: '';
|
|
6235
|
+
setTrialResult({ text: `${scoreText} → ${verdict}${why}` });
|
|
6236
|
+
} catch (error) {
|
|
6237
|
+
setTrialResult({ error: true, text: error && error.message ? error.message : '试算失败' });
|
|
6238
|
+
} finally {
|
|
6239
|
+
setTrialBusy(false);
|
|
6240
|
+
}
|
|
6241
|
+
}, [trialBusy, trialText, favoriteForm]);
|
|
6242
|
+
|
|
5781
6243
|
// UI-003:确认块出现时滚进视野(以前它渲染在设置页底部以外,用户以为「点了没反应」)。
|
|
5782
6244
|
useEffect(() => {
|
|
5783
6245
|
if (confirmNative !== null && nativeConfirmRef.current && typeof nativeConfirmRef.current.scrollIntoView === 'function') {
|
|
@@ -5949,7 +6411,7 @@ window.__ModuleLoader__.load({
|
|
|
5949
6411
|
h(
|
|
5950
6412
|
'p',
|
|
5951
6413
|
{ style: { margin: '0 0 4px', fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary, rgba(127,127,127,0.95))' } },
|
|
5952
|
-
'功能:看上下文容量、到线提醒并帮你压缩旧会话 /
|
|
6414
|
+
'功能:看上下文容量、到线提醒并帮你压缩旧会话 / 交接新会话、收藏重要回复;边界:只动本插件的压缩与交接历史,不改模型回复,也不接管 DSH 原生自动压缩(只在本页开关、改了要重启)。',
|
|
5953
6415
|
),
|
|
5954
6416
|
h('p', { style: { ...settingsHintStyle, marginBottom: 10 } }, '这里改的设置立即生效,不需要重启。阈值按窗口比例算:换不同上下文窗口的模型会自动缩放。'),
|
|
5955
6417
|
banner,
|
|
@@ -6129,6 +6591,81 @@ window.__ModuleLoader__.load({
|
|
|
6129
6591
|
),
|
|
6130
6592
|
),
|
|
6131
6593
|
),
|
|
6594
|
+
sectionHeading('推荐收藏'),
|
|
6595
|
+
h(
|
|
6596
|
+
'div',
|
|
6597
|
+
{ style: { display: 'flex', flexDirection: 'column', paddingLeft: 16, borderLeft: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))' }, 'data-cr-group': '推荐收藏' },
|
|
6598
|
+
settingsRow(
|
|
6599
|
+
'推荐收藏',
|
|
6600
|
+
'助手回复落地后,用一个小模型打分(0-100);够分的星标会金色微亮,提示「这条值得收藏」。',
|
|
6601
|
+
'只点亮、**绝不自动收藏**;确认仍要你点星或 /favorite mark。关掉后不再打分(也就不花这份钱)。',
|
|
6602
|
+
h('label', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 } },
|
|
6603
|
+
h('input', {
|
|
6604
|
+
type: 'checkbox', 'aria-label': '开启推荐收藏', checked: favoriteForm.enabled !== false,
|
|
6605
|
+
onChange: (event) => patch(['favorite', 'enabled'], event.target.checked),
|
|
6606
|
+
}),
|
|
6607
|
+
'开启推荐收藏',
|
|
6608
|
+
),
|
|
6609
|
+
),
|
|
6610
|
+
settingsRow(
|
|
6611
|
+
'推荐档位',
|
|
6612
|
+
'分高才亮:宽松 60 / 标准 75 / 严格 85。',
|
|
6613
|
+
'标准档每 4 条回复亮 1 次(准确率约 76%);严格档更少但更准(约 89%)。',
|
|
6614
|
+
h('select', {
|
|
6615
|
+
'aria-label': '推荐档位',
|
|
6616
|
+
value: favoriteForm.level ?? 'standard',
|
|
6617
|
+
style: { ...settingsInputStyle, width: 120 },
|
|
6618
|
+
onChange: (event) => patch(['favorite', 'level'], event.target.value),
|
|
6619
|
+
},
|
|
6620
|
+
h('option', { value: 'loose' }, '宽松(60)'),
|
|
6621
|
+
h('option', { value: 'standard' }, '标准(75)'),
|
|
6622
|
+
h('option', { value: 'strict' }, '严格(85)'),
|
|
6623
|
+
),
|
|
6624
|
+
),
|
|
6625
|
+
settingsRow(
|
|
6626
|
+
'必亮词',
|
|
6627
|
+
'出现这些词就直接点亮,跳过打分(模型失败也亮)。',
|
|
6628
|
+
'多个词用逗号或空格分隔,最多 20 个;适合「定案」「结论」这类固定说法。',
|
|
6629
|
+
h('input', {
|
|
6630
|
+
type: 'text', 'aria-label': '必亮词', placeholder: '例如:定案,结论,就按这个',
|
|
6631
|
+
value: (favoriteForm.mustWords ?? []).join(','),
|
|
6632
|
+
style: { ...settingsInputStyle, width: '100%' },
|
|
6633
|
+
onChange: (event) => patch(['favorite', 'mustWords'], parseFavoriteWords(event.target.value)),
|
|
6634
|
+
}),
|
|
6635
|
+
),
|
|
6636
|
+
settingsRow(
|
|
6637
|
+
'屏蔽词',
|
|
6638
|
+
'出现这些词就绝不点亮(优先于必亮词和分数)。',
|
|
6639
|
+
'屏蔽词优先;比如「测试」「随便试试」这类不该进收藏的说法。',
|
|
6640
|
+
h('input', {
|
|
6641
|
+
type: 'text', 'aria-label': '屏蔽词', placeholder: '例如:测试,随便',
|
|
6642
|
+
value: (favoriteForm.muteWords ?? []).join(','),
|
|
6643
|
+
style: { ...settingsInputStyle, width: '100%' },
|
|
6644
|
+
onChange: (event) => patch(['favorite', 'muteWords'], parseFavoriteWords(event.target.value)),
|
|
6645
|
+
}),
|
|
6646
|
+
),
|
|
6647
|
+
settingsRow(
|
|
6648
|
+
'试算',
|
|
6649
|
+
'粘一段助手回复原文,按上面的档位/词表跑一次真实打分(只读,不写收藏)。',
|
|
6650
|
+
'试算会真的调一次小模型(约 1-2 秒、少量 token);结果只用来调档位。',
|
|
6651
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, width: '100%' } },
|
|
6652
|
+
h('textarea', {
|
|
6653
|
+
'aria-label': '试算文本',
|
|
6654
|
+
rows: 3,
|
|
6655
|
+
placeholder: '粘贴一段助手回复原文…',
|
|
6656
|
+
value: trialText,
|
|
6657
|
+
style: { ...settingsInputStyle, width: '100%', boxSizing: 'border-box', resize: 'vertical', minHeight: 60 },
|
|
6658
|
+
onChange: (event) => setTrialText(event.target.value),
|
|
6659
|
+
}),
|
|
6660
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
6661
|
+
h('button', { type: 'button', style: settingsButtonStyle, disabled: trialBusy, onClick: runTrial }, trialBusy ? '试算中…' : '试算'),
|
|
6662
|
+
trialResult === null
|
|
6663
|
+
? null
|
|
6664
|
+
: h('span', { style: { ...settingsHintStyle, color: trialResult.error ? 'var(--dsw-alias-state-warning-primary, #b45309)' : undefined } }, trialResult.text),
|
|
6665
|
+
),
|
|
6666
|
+
),
|
|
6667
|
+
),
|
|
6668
|
+
),
|
|
6132
6669
|
sectionHeading('压缩行为'),
|
|
6133
6670
|
h(
|
|
6134
6671
|
'div',
|
|
@@ -6572,7 +7109,7 @@ window.__ModuleLoader__.load({
|
|
|
6572
7109
|
h('p', { style: settingsHintStyle }, `版本:${version || '—'}`),
|
|
6573
7110
|
h('p', { style: settingsHintStyle, 'data-cr-ledger': '1' }, ledgerLine),
|
|
6574
7111
|
h('p', { style: settingsHintStyle }, `设置文件:${settingsPath || '—'}`),
|
|
6575
|
-
h('p', { style: { ...settingsHintStyle, marginTop: 8 } }, '
|
|
7112
|
+
h('p', { style: { ...settingsHintStyle, marginTop: 8 } }, '输入框左侧的上下文指示灯:看容量、提醒压缩/交接、收藏重要回复;到线了用 /bc 或 /handoff,重要内容用 /fin 锁住。'),
|
|
6576
7113
|
h('p', { style: settingsHintStyle }, '遇到 bug 或想提改进:走上面「帮助」里的 GitHub Issues;也欢迎直接发邮件。'),
|
|
6577
7114
|
h('p', { style: settingsHintStyle }, '联系方式:cfanzx@136.com(合作、安全问题,以及不便公开讨论的事项)。'),
|
|
6578
7115
|
h('p', { style: settingsHintStyle }, 'npm 包页面:https://www.npmjs.com/package/dsh-compact-relay'),
|
|
@@ -6896,6 +7433,22 @@ window.__ModuleLoader__.load({
|
|
|
6896
7433
|
}
|
|
6897
7434
|
};
|
|
6898
7435
|
|
|
7436
|
+
// 0.6 收藏星标:挂助手消息底部动作行(宿主只对已定稿助手消息渲染)。
|
|
7437
|
+
// 会话 id 必须由 slot 的 session 作用域注入,不能靠 state 接口猜「当前会话」:
|
|
7438
|
+
// 多工作区同时活着时宿主会回「最后一个活 agent」的会话(2026-10-29 真机 BUG:点星静默失败)。
|
|
7439
|
+
ctx.slots.inject('conversation.chat.assistant-actions', () =>
|
|
7440
|
+
ctx.slots.register(
|
|
7441
|
+
{
|
|
7442
|
+
name: 'conversation.chat.assistant-actions',
|
|
7443
|
+
id: 'compact-relay-favorite',
|
|
7444
|
+
inject: (sessionId) => ({
|
|
7445
|
+
sessionId: typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : undefined,
|
|
7446
|
+
}),
|
|
7447
|
+
},
|
|
7448
|
+
FavoriteStar,
|
|
7449
|
+
),
|
|
7450
|
+
);
|
|
7451
|
+
|
|
6899
7452
|
ctx.slots.inject('conversation.input.left', () =>
|
|
6900
7453
|
ctx.slots.register(
|
|
6901
7454
|
{
|