dsh-superinput 0.7.0 → 0.12.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 +265 -0
- package/README.md +18 -7
- package/client.js +2356 -251
- package/modules/compaction.js +413 -0
- package/modules/cost.js +571 -3
- package/modules/prompts.client.js +1068 -57
- package/modules/prompts.js +65 -0
- package/package.json +2 -3
- package/scripts/build-suite.mjs +14 -4
- package/scripts/smoke.mjs +54 -3
- package/server.js +1057 -3
package/client.js
CHANGED
|
@@ -24,7 +24,7 @@ function createCostClient(require) {
|
|
|
24
24
|
const ENDPOINT = '/api/dsh-cost'
|
|
25
25
|
const POLL_MS = 15000
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/* 客户端调试通道:关键锚定/装配事件上报服务端 debug log(真机取证)。 */
|
|
28
28
|
function clientDebug(message) {
|
|
29
29
|
try {
|
|
30
30
|
void fetch(`${ENDPOINT}/client-debug`, {
|
|
@@ -35,6 +35,108 @@ function createCostClient(require) {
|
|
|
35
35
|
} catch { /* ignore */ }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/* -- 双语(zh / en):localStorage 持久化,默认跟随系统 ------------- */
|
|
39
|
+
|
|
40
|
+
const LANG_KEY = 'dsh-cost-lang'
|
|
41
|
+
const STRINGS = {
|
|
42
|
+
zh: {
|
|
43
|
+
sectionLabel: '费用',
|
|
44
|
+
today: '今日', thisMonth: '本月', total: '累计',
|
|
45
|
+
callsUnit: '次调用', callTimes: ' 次',
|
|
46
|
+
noRecords: '暂无记录', noCallsToday: '今日暂无调用',
|
|
47
|
+
peakBadge: '峰时计价', offPeakBadge: '谷时计价',
|
|
48
|
+
peakNow: '峰时', offPeakNow: '谷时',
|
|
49
|
+
unmatchedModels: '个模型按默认档计费',
|
|
50
|
+
exportJson: '导出 JSON', exportCsv: '导出 CSV',
|
|
51
|
+
exportJsonTitle: '导出全部费用记录(JSON)', exportCsvTitle: '导出日汇总(CSV)',
|
|
52
|
+
syncPrices: '同步官方价格', syncPricesTitle: '从官方定价页同步现行价格(峰谷两档,历史账目不受影响)',
|
|
53
|
+
refreshRate: '刷新汇率', refreshRateTitle: '立即刷新 USD→CNY 汇率(平时每小时自动刷新)',
|
|
54
|
+
priceSynced: '价格已同步', rateRefreshed: '汇率已刷新', failed: '失败',
|
|
55
|
+
monthForecast: '月底预计', spent: '已花', dailyAvg: '日均', daysLeft: (n) => `剩 ${n} 天`, overBudget: '超出预算',
|
|
56
|
+
forecastNeedDays: (n) => `预测需要至少 3 天完整数据(当前 ${n} 天)`,
|
|
57
|
+
budget: '预算', budgetPeriodDay: '今日', budgetPeriodMonth: '本月', budgetPeriodAll: '累计',
|
|
58
|
+
turnCost: '费用', turnCostTitle: '本轮费用明细', turnCostPop: '本轮费用', turnEmpty: '该回合暂无用量记录',
|
|
59
|
+
costDetail: '费用明细', sessionCost: '本对话费用', officialBalance: '官方余额', recentCalls: '最近调用',
|
|
60
|
+
srcChat: '对话', srcEnhance: '提示词增强', srcFollowup: '跟进建议',
|
|
61
|
+
todaySidebar: '今日', loading: '加载中…', readFailed: '读取失败', gotIt: '知道了',
|
|
62
|
+
peakSoonPeak: (time) => `即将进入峰时计价(${time})`, peakSoonOff: (time) => `即将进入谷时计价(${time})`,
|
|
63
|
+
budgetOverTitle: 'dsh 费用预算已超限',
|
|
64
|
+
budgetOverBody: (label, used, amount, pct) => `${label}费用 ¥${used},已达预算 ¥${amount} 的 ${pct}%。`,
|
|
65
|
+
bucketInput: '输入', bucketOutput: '输出', bucketCacheRead: '缓存读取', bucketCacheWrite: '缓存写入', bucketReasoning: '推理',
|
|
66
|
+
heatTitle: '用量热力图', heatCost: '费用', heatCalls: '调用', heatTokens: 'Token',
|
|
67
|
+
heatLess: '少', heatMore: '多', heatEmpty: '暂无用量数据',
|
|
68
|
+
plansTitle: 'Coding Plan 配额', plansHint: '粘贴各家订阅的 API Key 查询剩余额度;Key 只保存在本机 settings.json,不会回显或上传。',
|
|
69
|
+
plansSave: '保存', plansSaved: '已保存', plansRefresh: '刷新', plansRefreshAll: '全部刷新',
|
|
70
|
+
plansConfigured: '已配置', plansError: '查询失败',
|
|
71
|
+
winFiveHour: '5 小时', winWeekly: '本周', winMonthly: '本月', winBalance: '余额',
|
|
72
|
+
winCredits: '额度', winPlan: '套餐', winCompensation: '补偿', winCurrent: '当前',
|
|
73
|
+
winResets: '重置', winUnlimited: '不限量',
|
|
74
|
+
planAnthropic: 'Anthropic (Claude Pro/Max)', planZai: 'Z.ai / 智谱 GLM Coding Plan',
|
|
75
|
+
planMinimax: 'MiniMax Token Plan', planKimi: 'Kimi Code / Moonshot',
|
|
76
|
+
planOpenrouter: 'OpenRouter', planSiliconflow: 'SiliconFlow 硅基流动',
|
|
77
|
+
planCommandcode: 'CommandCode', planClinepass: 'Cline Pass',
|
|
78
|
+
langHint: '设置分区标题等宿主注册项在刷新窗口后生效',
|
|
79
|
+
},
|
|
80
|
+
en: {
|
|
81
|
+
sectionLabel: 'Cost',
|
|
82
|
+
today: 'Today', thisMonth: 'This month', total: 'Total',
|
|
83
|
+
callsUnit: 'calls', callTimes: ' calls',
|
|
84
|
+
noRecords: 'No records yet', noCallsToday: 'No calls today',
|
|
85
|
+
peakBadge: 'Peak pricing', offPeakBadge: 'Off-peak pricing',
|
|
86
|
+
peakNow: 'peak', offPeakNow: 'off-peak',
|
|
87
|
+
unmatchedModels: ' models billed at default tier',
|
|
88
|
+
exportJson: 'Export JSON', exportCsv: 'Export CSV',
|
|
89
|
+
exportJsonTitle: 'Export all cost records (JSON)', exportCsvTitle: 'Export daily summary (CSV)',
|
|
90
|
+
syncPrices: 'Sync official prices', syncPricesTitle: 'Sync current prices from the official pricing page (peak/off-peak; history untouched)',
|
|
91
|
+
refreshRate: 'Refresh rate', refreshRateTitle: 'Refresh the USD→CNY rate now (auto hourly otherwise)',
|
|
92
|
+
priceSynced: 'Prices synced', rateRefreshed: 'Rate refreshed', failed: 'Failed',
|
|
93
|
+
monthForecast: 'Month-end forecast', spent: 'Spent ', dailyAvg: 'daily avg ', daysLeft: (n) => `${n} days left`, overBudget: 'over budget',
|
|
94
|
+
forecastNeedDays: (n) => `Forecast needs at least 3 full days of data (currently ${n})`,
|
|
95
|
+
budget: 'Budget', budgetPeriodDay: 'Daily', budgetPeriodMonth: 'Monthly', budgetPeriodAll: 'All time',
|
|
96
|
+
turnCost: 'Cost', turnCostTitle: 'Turn cost details', turnCostPop: 'This turn', turnEmpty: 'No usage recorded for this turn',
|
|
97
|
+
costDetail: 'Cost details', sessionCost: 'This conversation', officialBalance: 'Official balance', recentCalls: 'Recent calls',
|
|
98
|
+
srcChat: 'Chat', srcEnhance: 'Prompt enhancer', srcFollowup: 'Follow-ups',
|
|
99
|
+
todaySidebar: 'Today', loading: 'Loading…', readFailed: 'Failed to load', gotIt: 'Got it',
|
|
100
|
+
peakSoonPeak: (time) => `Peak pricing starts at ${time}`, peakSoonOff: (time) => `Off-peak pricing starts at ${time}`,
|
|
101
|
+
budgetOverTitle: 'dsh cost budget exceeded',
|
|
102
|
+
budgetOverBody: (label, used, amount, pct) => `${label} cost ¥${used} — ${pct}% of the ¥${amount} budget.`,
|
|
103
|
+
bucketInput: 'Input', bucketOutput: 'Output', bucketCacheRead: 'Cache read', bucketCacheWrite: 'Cache write', bucketReasoning: 'Reasoning',
|
|
104
|
+
heatTitle: 'Usage heatmap', heatCost: 'Cost', heatCalls: 'Calls', heatTokens: 'Tokens',
|
|
105
|
+
heatLess: 'Less', heatMore: 'More', heatEmpty: 'No usage data yet',
|
|
106
|
+
plansTitle: 'Coding Plan quotas', plansHint: 'Paste an API key per subscription to query remaining quota. Keys stay in the local settings.json — never echoed, never uploaded.',
|
|
107
|
+
plansSave: 'Save', plansSaved: 'Saved', plansRefresh: 'Refresh', plansRefreshAll: 'Refresh all',
|
|
108
|
+
plansConfigured: 'configured', plansError: 'Query failed',
|
|
109
|
+
winFiveHour: '5-hour', winWeekly: 'This week', winMonthly: 'Monthly', winBalance: 'Balance',
|
|
110
|
+
winCredits: 'Credits', winPlan: 'Plan', winCompensation: 'Bonus', winCurrent: 'Current',
|
|
111
|
+
winResets: 'Resets', winUnlimited: 'Unlimited',
|
|
112
|
+
planAnthropic: 'Anthropic (Claude Pro/Max)', planZai: 'Z.ai / Zhipu GLM Coding Plan',
|
|
113
|
+
planMinimax: 'MiniMax Token Plan', planKimi: 'Kimi Code / Moonshot',
|
|
114
|
+
planOpenrouter: 'OpenRouter', planSiliconflow: 'SiliconFlow',
|
|
115
|
+
planCommandcode: 'CommandCode', planClinepass: 'Cline Pass',
|
|
116
|
+
langHint: 'Host-registered labels (e.g. the settings section title) apply after a window reload',
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
let lang = 'zh'
|
|
120
|
+
try {
|
|
121
|
+
const saved = localStorage.getItem(LANG_KEY)
|
|
122
|
+
lang = saved === 'zh' || saved === 'en'
|
|
123
|
+
? saved
|
|
124
|
+
: (navigator.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
125
|
+
} catch { /* 无 localStorage:保持跟随系统默认 */ }
|
|
126
|
+
const t = (key) => STRINGS[lang][key] ?? STRINGS.zh[key] ?? key
|
|
127
|
+
/** 切语言:写 localStorage 并广播一次,让所有 useCostState 订阅组件重渲染。 */
|
|
128
|
+
function setLang(value) {
|
|
129
|
+
const next = value === 'en' || value === 'zh'
|
|
130
|
+
? value
|
|
131
|
+
: ((navigator.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en')
|
|
132
|
+
if (next === lang) return
|
|
133
|
+
lang = next
|
|
134
|
+
try { localStorage.setItem(LANG_KEY, lang) } catch { /* ignore */ }
|
|
135
|
+
for (const listener of listeners) {
|
|
136
|
+
try { listener(latestState) } catch { /* 单订阅者异常不断链 */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
38
140
|
/* 官方 token 配色(compaction-control / cost-meter 同基准),无自创色。 */
|
|
39
141
|
const CSS = `
|
|
40
142
|
.dsh-cost-inline {
|
|
@@ -223,6 +325,43 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
223
325
|
.dsh-cost-fill { height: 100%; border-radius: 3px; background: var(--dsw-alias-brand-primary); }
|
|
224
326
|
.dsh-cost-fill.warn { background: var(--dsw-alias-state-warn-primary); }
|
|
225
327
|
.dsh-cost-fill.over { background: var(--dsw-alias-state-error-primary); }
|
|
328
|
+
/* 用量热力图(GitHub 贡献图样式;色阶用官方品牌色 color-mix) */
|
|
329
|
+
.dsh-cost-heat { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 10px; background: var(--dsw-alias-bg-layer-1, transparent); }
|
|
330
|
+
.dsh-cost-heat-head { display: flex; align-items: center; gap: 8px; }
|
|
331
|
+
.dsh-cost-heat-title { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); flex: 1; }
|
|
332
|
+
.dsh-cost-heat-toggle { display: flex; gap: 2px; }
|
|
333
|
+
.dsh-cost-heat-toggle button { border: none; background: transparent; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 18px; padding: 1px 8px; border-radius: 4px; cursor: pointer; }
|
|
334
|
+
.dsh-cost-heat-toggle button[aria-pressed="true"] { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.2)); color: var(--dsw-alias-label-primary); }
|
|
335
|
+
.dsh-cost-heat-months { display: flex; gap: 3px; margin-left: 18px; height: 11px; font-size: 10px; line-height: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
336
|
+
.dsh-cost-heat-months span { width: 11px; flex: none; overflow: visible; white-space: nowrap; }
|
|
337
|
+
.dsh-cost-heat-weeks { display: flex; flex-direction: column; width: 14px; flex: none; font-size: 10px; line-height: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
338
|
+
.dsh-cost-heat-grid { display: flex; gap: 3px; overflow-x: auto; height: 95px; align-items: flex-start; }
|
|
339
|
+
.dsh-cost-heat-col { display: flex; flex-direction: column; gap: 3px; flex: none; }
|
|
340
|
+
.dsh-cost-heat-cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.22)); flex: none; }
|
|
341
|
+
.dsh-cost-heat-cell.blank { visibility: hidden; }
|
|
342
|
+
.dsh-cost-heat-legend { display: flex; align-items: center; gap: 3px; font-size: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
343
|
+
.dsh-cost-heat-legend .dsh-cost-heat-cell { width: 9px; height: 9px; }
|
|
344
|
+
/* Coding Plan 配额面板 */
|
|
345
|
+
.dsh-cost-plans { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 10px; background: var(--dsw-alias-bg-layer-1, transparent); }
|
|
346
|
+
.dsh-cost-plans-head { display: flex; align-items: center; gap: 8px; }
|
|
347
|
+
.dsh-cost-plans-title { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-primary); flex: 1; cursor: pointer; user-select: none; }
|
|
348
|
+
.dsh-cost-plans-caret { font-size: 10px; color: var(--dsw-alias-label-tertiary); flex: none; width: 12px; }
|
|
349
|
+
.dsh-cost-plans-hint { font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
|
|
350
|
+
.dsh-cost-plan { display: flex; flex-direction: column; gap: 5px; padding: 8px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2, transparent); }
|
|
351
|
+
.dsh-cost-plan-row { display: flex; align-items: center; gap: 8px; }
|
|
352
|
+
.dsh-cost-plan-name { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-primary); flex: 1 1 40%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
353
|
+
.dsh-cost-plan-input { flex: 2 1 180px; min-width: 0; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l1, transparent); border-radius: 6px; background: var(--dsw-alias-bg-layer-1, transparent); color: var(--dsw-alias-label-primary); font-size: 12px; outline: none; }
|
|
354
|
+
.dsh-cost-plan-input:focus { border-color: var(--dsw-alias-brand-primary); }
|
|
355
|
+
.dsh-cost-plan-btn { flex: none; border: none; border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.2)); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 20px; padding: 0 10px; cursor: pointer; }
|
|
356
|
+
.dsh-cost-plan-btn:hover { color: var(--dsw-alias-label-primary); }
|
|
357
|
+
.dsh-cost-plan-btn[disabled] { opacity: 0.45; cursor: default; }
|
|
358
|
+
.dsh-cost-plan-win { display: flex; align-items: center; gap: 8px; font-size: 11.5px; line-height: 18px; font-variant-numeric: tabular-nums; }
|
|
359
|
+
.dsh-cost-plan-win .dsh-cost-plan-winname { flex: none; width: 68px; color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
360
|
+
.dsh-cost-plan-win .dsh-cost-plan-bar { flex: 1; height: 4px; border-radius: 2px; background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.2)); overflow: hidden; }
|
|
361
|
+
.dsh-cost-plan-win .dsh-cost-plan-fill { height: 100%; border-radius: 2px; background: var(--dsw-alias-brand-primary); }
|
|
362
|
+
.dsh-cost-plan-win .dsh-cost-plan-fill.hot { background: var(--dsw-alias-state-warn-primary); }
|
|
363
|
+
.dsh-cost-plan-win .dsh-cost-plan-pct { flex: none; min-width: 88px; text-align: right; color: var(--dsw-alias-label-tertiary); }
|
|
364
|
+
.dsh-cost-plan-err { font-size: 11.5px; line-height: 18px; color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
|
|
226
365
|
`
|
|
227
366
|
|
|
228
367
|
function ensureStyle() {
|
|
@@ -250,11 +389,11 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
250
389
|
if (budgetNoticeKey === key) return
|
|
251
390
|
budgetNoticeKey = key
|
|
252
391
|
if (typeof Notification === 'undefined') return
|
|
253
|
-
const periodLabel = budget.period === 'day' ? '
|
|
392
|
+
const periodLabel = t(budget.period === 'day' ? 'budgetPeriodDay' : budget.period === 'month' ? 'budgetPeriodMonth' : 'budgetPeriodAll')
|
|
254
393
|
const fire = () => {
|
|
255
394
|
try {
|
|
256
|
-
const n = new Notification('
|
|
257
|
-
body:
|
|
395
|
+
const n = new Notification(t('budgetOverTitle'), {
|
|
396
|
+
body: t('budgetOverBody')(periodLabel, Number(budget.used).toFixed(2), Number(budget.amount).toFixed(2), (budget.usedPct * 100).toFixed(0)),
|
|
258
397
|
})
|
|
259
398
|
n.onclick = () => { window.focus?.(); n.close() }
|
|
260
399
|
} catch { /* 通知不可用:UI 超限变色兜底 */ }
|
|
@@ -488,8 +627,8 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
488
627
|
if (!state) return null
|
|
489
628
|
const today = state.today
|
|
490
629
|
const title = today
|
|
491
|
-
? `${today.calls}
|
|
492
|
-
: '
|
|
630
|
+
? `${today.calls} ${t('callsUnit')} · token ${(tokensOf(today) / 1000).toFixed(1)}k · ${state.peak?.isPeak ? t('peakNow') : t('offPeakNow')}`
|
|
631
|
+
: t('noCallsToday')
|
|
493
632
|
const overBudget = state.budget && state.budget.usedPct >= 0.8
|
|
494
633
|
return h('button', {
|
|
495
634
|
type: 'button',
|
|
@@ -497,13 +636,14 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
497
636
|
title,
|
|
498
637
|
onClick: () => { void pollOnce() },
|
|
499
638
|
},
|
|
500
|
-
h('span', null, '
|
|
639
|
+
h('span', null, t('todaySidebar')),
|
|
501
640
|
h('span', { className: 'dsh-cost-sidebar-value' }, moneyOf(state, today?.cost ?? 0)))
|
|
502
641
|
}
|
|
503
642
|
|
|
504
643
|
/* -- 回合边界感知(turnTail 槽位,隐形数据载体) -------------------- */
|
|
505
644
|
|
|
506
|
-
const
|
|
645
|
+
const TURN_BUCKET_KEYS = { input: 'bucketInput', output: 'bucketOutput', cacheRead: 'bucketCacheRead', cacheWrite: 'bucketCacheWrite', reasoning: 'bucketReasoning' }
|
|
646
|
+
const turnBucketName = (bucket) => t(TURN_BUCKET_KEYS[bucket] ?? bucket)
|
|
507
647
|
|
|
508
648
|
/**
|
|
509
649
|
* 用户要求去掉回合尾部的可见费用行(0.6.1);此组件退化为隐形载体:
|
|
@@ -541,7 +681,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
541
681
|
if (lastAlertedSwitch === nextSwitchAtMs) return
|
|
542
682
|
lastAlertedSwitch = nextSwitchAtMs
|
|
543
683
|
const time = new Date(nextSwitchAtMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
544
|
-
const text = state.peak.nextIsPeak ?
|
|
684
|
+
const text = state.peak.nextIsPeak ? t('peakSoonPeak')(time) : t('peakSoonOff')(time)
|
|
545
685
|
setMessage(text)
|
|
546
686
|
setVisible(true)
|
|
547
687
|
if (alerts.notify) {
|
|
@@ -551,7 +691,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
551
691
|
if (!visible) return null
|
|
552
692
|
return createPortal(h('div', { className: 'dsh-cost-alert' },
|
|
553
693
|
h('span', null, message),
|
|
554
|
-
h('button', { type: 'button', onClick: () => setVisible(false) }, '
|
|
694
|
+
h('button', { type: 'button', onClick: () => setVisible(false) }, t('gotIt'))), document.body)
|
|
555
695
|
}
|
|
556
696
|
|
|
557
697
|
/* -- 动作行费用按钮:锚定在官方「用量」按钮旁,弹卡同官方样式 ------- */
|
|
@@ -590,7 +730,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
590
730
|
if (!from || !to) return
|
|
591
731
|
const pop = document.createElement('div')
|
|
592
732
|
pop.className = 'dsh-cost-pop'
|
|
593
|
-
pop.textContent = '
|
|
733
|
+
pop.textContent = t('loading')
|
|
594
734
|
document.body.appendChild(pop)
|
|
595
735
|
usageAnchors.pop = pop
|
|
596
736
|
usageAnchors.anchor = anchor
|
|
@@ -608,7 +748,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
608
748
|
const head = document.createElement('div')
|
|
609
749
|
head.className = 'dsh-cost-pop-head'
|
|
610
750
|
const title = document.createElement('span')
|
|
611
|
-
title.textContent = '
|
|
751
|
+
title.textContent = t('turnCostPop')
|
|
612
752
|
const total = document.createElement('span')
|
|
613
753
|
total.className = 'dsh-cost-pop-num'
|
|
614
754
|
total.textContent = value.found ? moneyOf(latestState, value.cost) : '—'
|
|
@@ -619,7 +759,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
619
759
|
const line = document.createElement('div')
|
|
620
760
|
line.className = 'dsh-cost-pop-row'
|
|
621
761
|
const label = document.createElement('span')
|
|
622
|
-
label.textContent = `${String(row.model ?? '').replace(/^deepseek-/, '')} · ${
|
|
762
|
+
label.textContent = `${String(row.model ?? '').replace(/^deepseek-/, '')} · ${turnBucketName(row.bucket)}`
|
|
623
763
|
const numEl = document.createElement('span')
|
|
624
764
|
numEl.className = 'dsh-cost-pop-num'
|
|
625
765
|
numEl.textContent = `${Number(row.tokens).toLocaleString()} tok · ${moneyOf(latestState, row.cost)}`
|
|
@@ -630,7 +770,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
630
770
|
if (!value.found) {
|
|
631
771
|
const empty = document.createElement('div')
|
|
632
772
|
empty.className = 'dsh-cost-pop-row'
|
|
633
|
-
empty.textContent = '
|
|
773
|
+
empty.textContent = t('turnEmpty')
|
|
634
774
|
pop.appendChild(empty)
|
|
635
775
|
}
|
|
636
776
|
})
|
|
@@ -652,7 +792,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
652
792
|
const anchor = document.createElement('button')
|
|
653
793
|
anchor.className = 'dsh-cost-usage-anchor'
|
|
654
794
|
anchor.type = 'button'
|
|
655
|
-
anchor.textContent = '
|
|
795
|
+
anchor.textContent = `${t('turnCost')} —`
|
|
656
796
|
const from = details.getAttribute('data-turn-from') ?? ''
|
|
657
797
|
const to = details.getAttribute('data-turn-to') ?? ''
|
|
658
798
|
anchor.dataset.turnFrom = from
|
|
@@ -673,10 +813,10 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
673
813
|
.then((res) => (res.ok ? res.json() : null))
|
|
674
814
|
.then((value) => {
|
|
675
815
|
if (value?.found && anchor.isConnected) {
|
|
676
|
-
anchor.textContent =
|
|
677
|
-
anchor.title = '
|
|
816
|
+
anchor.textContent = `${t('turnCost')} ${moneyOf(latestState, value.cost)}`
|
|
817
|
+
anchor.title = t('turnCostTitle')
|
|
678
818
|
} else if (anchor.isConnected) {
|
|
679
|
-
anchor.textContent = '
|
|
819
|
+
anchor.textContent = `${t('turnCost')} —`
|
|
680
820
|
}
|
|
681
821
|
})
|
|
682
822
|
.catch(() => {})
|
|
@@ -707,10 +847,10 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
707
847
|
if (!budget) return null
|
|
708
848
|
const pct = Math.min(100, Math.max(0, budget.usedPct * 100))
|
|
709
849
|
const tier = budget.usedPct >= 1 ? 'over' : budget.usedPct >= 0.8 ? 'warn' : ''
|
|
710
|
-
const periodLabel = budget.period === 'day' ? '
|
|
850
|
+
const periodLabel = t(budget.period === 'day' ? 'budgetPeriodDay' : budget.period === 'month' ? 'budgetPeriodMonth' : 'budgetPeriodAll')
|
|
711
851
|
return h('div', { className: 'dsh-cost-budget' },
|
|
712
852
|
h('div', { className: 'dsh-cost-budget-head' },
|
|
713
|
-
h('span', null,
|
|
853
|
+
h('span', null, `${t('budget')} · ${periodLabel}`),
|
|
714
854
|
h('span', null, `¥${Number(budget.used).toFixed(2)} / ¥${Number(budget.amount).toFixed(2)} · ${(budget.usedPct * 100).toFixed(0)}%`)),
|
|
715
855
|
h('div', { className: 'dsh-cost-bar' }, h('div', { className: `dsh-cost-fill ${tier}`, style: { width: `${pct}%` } })))
|
|
716
856
|
}
|
|
@@ -751,7 +891,8 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
751
891
|
|
|
752
892
|
/* -- 状态行费用 pill:累计费用,点开看每笔调用(含提示词增强) ------ */
|
|
753
893
|
|
|
754
|
-
const
|
|
894
|
+
const SOURCE_KEYS = { chat: 'srcChat', enhance: 'srcEnhance', followup: 'srcFollowup' }
|
|
895
|
+
const sourceLabel = (key) => t(SOURCE_KEYS[key] ?? key)
|
|
755
896
|
let overviewPop = null
|
|
756
897
|
|
|
757
898
|
function closeOverviewPop(event) {
|
|
@@ -772,7 +913,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
772
913
|
const head = document.createElement('div')
|
|
773
914
|
head.className = 'dsh-cost-pop-head'
|
|
774
915
|
const title = document.createElement('span')
|
|
775
|
-
title.textContent = '
|
|
916
|
+
title.textContent = t('sessionCost')
|
|
776
917
|
const sum = document.createElement('span')
|
|
777
918
|
sum.textContent = moneyOf(state, data.total?.cost ?? 0)
|
|
778
919
|
head.append(title, sum)
|
|
@@ -783,7 +924,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
783
924
|
balRow.className = 'dsh-cost-src'
|
|
784
925
|
const balName = document.createElement('span')
|
|
785
926
|
balName.className = 'name'
|
|
786
|
-
balName.textContent = '
|
|
927
|
+
balName.textContent = t('officialBalance')
|
|
787
928
|
const balNum = document.createElement('span')
|
|
788
929
|
balNum.className = 'num'
|
|
789
930
|
balNum.textContent = '…'
|
|
@@ -809,10 +950,10 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
809
950
|
row.className = 'dsh-cost-src'
|
|
810
951
|
const name = document.createElement('span')
|
|
811
952
|
name.className = 'name'
|
|
812
|
-
name.textContent =
|
|
953
|
+
name.textContent = sourceLabel(key)
|
|
813
954
|
const cnt = document.createElement('span')
|
|
814
955
|
cnt.className = 'cnt'
|
|
815
|
-
cnt.textContent = entry.calls
|
|
956
|
+
cnt.textContent = `${entry.calls}${t('callTimes')}`
|
|
816
957
|
const num = document.createElement('span')
|
|
817
958
|
num.className = 'num'
|
|
818
959
|
num.textContent = moneyOf(state, entry.cost)
|
|
@@ -824,7 +965,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
824
965
|
sep.className = 'dsh-cost-pop-head'
|
|
825
966
|
sep.style.marginTop = '8px'
|
|
826
967
|
const sepTitle = document.createElement('span')
|
|
827
|
-
sepTitle.textContent = '
|
|
968
|
+
sepTitle.textContent = t('recentCalls')
|
|
828
969
|
sep.appendChild(sepTitle)
|
|
829
970
|
box.appendChild(sep)
|
|
830
971
|
|
|
@@ -833,10 +974,10 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
833
974
|
row.className = 'dsh-cost-src'
|
|
834
975
|
const time = document.createElement('span')
|
|
835
976
|
time.className = 'cnt'
|
|
836
|
-
time.textContent = new Date(call.at).toLocaleTimeString(
|
|
977
|
+
time.textContent = new Date(call.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
837
978
|
const name = document.createElement('span')
|
|
838
979
|
name.className = 'name'
|
|
839
|
-
name.textContent = (
|
|
980
|
+
name.textContent = sourceLabel(call.source) + ' · ' + (call.model || '—')
|
|
840
981
|
const num = document.createElement('span')
|
|
841
982
|
num.className = 'num'
|
|
842
983
|
num.textContent = moneyOf(state, call.cost)
|
|
@@ -917,7 +1058,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
917
1058
|
type: 'button',
|
|
918
1059
|
className: 'dsh-cost-pill',
|
|
919
1060
|
'aria-expanded': open ? 'true' : 'false',
|
|
920
|
-
title: '
|
|
1061
|
+
title: t('costDetail'),
|
|
921
1062
|
// 数值全部来自 dsh 的状态行规范:font-size 12px / line-height 20px /
|
|
922
1063
|
// 文字色 --dsw-alias-label-tertiary(暗色 rgb(173,178,184)) /
|
|
923
1064
|
// padding 1px 8px / 圆角 --dsw-radius-sm(8px)。不用 inherit,宿主那层
|
|
@@ -948,6 +1089,254 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
948
1089
|
}, amount))
|
|
949
1090
|
}
|
|
950
1091
|
|
|
1092
|
+
/* -- 用量热力图(GitHub 贡献图样式:周列 × 星期行,官方品牌色色阶) -- */
|
|
1093
|
+
|
|
1094
|
+
const HEAT_LEVEL_MIX = [0, 28, 52, 76, 100]
|
|
1095
|
+
const heatLevelColor = (level) => level > 0
|
|
1096
|
+
? `color-mix(in srgb, var(--dsw-alias-brand-primary) ${HEAT_LEVEL_MIX[level]}%, transparent)`
|
|
1097
|
+
: ''
|
|
1098
|
+
|
|
1099
|
+
function HeatmapCard() {
|
|
1100
|
+
useCostState() // 轮询触发重渲染,让热力图跟着账本更新
|
|
1101
|
+
const [data, setData] = React.useState(null)
|
|
1102
|
+
const [metric, setMetric] = React.useState(() => {
|
|
1103
|
+
try { return localStorage.getItem('dsh-cost-heat-metric') === 'calls' || localStorage.getItem('dsh-cost-heat-metric') === 'tokens' ? localStorage.getItem('dsh-cost-heat-metric') : 'cost' } catch { return 'cost' }
|
|
1104
|
+
})
|
|
1105
|
+
React.useEffect(() => {
|
|
1106
|
+
let alive = true
|
|
1107
|
+
let lastAt = 0
|
|
1108
|
+
const load = (force) => {
|
|
1109
|
+
const now = Date.now()
|
|
1110
|
+
if (!force && now - lastAt < 60_000) return
|
|
1111
|
+
lastAt = now
|
|
1112
|
+
fetch(`${ENDPOINT}/heatmap?days=182`, { cache: 'no-store' })
|
|
1113
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1114
|
+
.then((value) => { if (alive && value && Array.isArray(value.days)) setData(value) })
|
|
1115
|
+
.catch(() => {})
|
|
1116
|
+
}
|
|
1117
|
+
load(true)
|
|
1118
|
+
const timerId = setInterval(() => load(false), POLL_MS * 4)
|
|
1119
|
+
return () => { alive = false; clearInterval(timerId) }
|
|
1120
|
+
}, [])
|
|
1121
|
+
const pickMetric = (value) => {
|
|
1122
|
+
setMetric(value)
|
|
1123
|
+
try { localStorage.setItem('dsh-cost-heat-metric', value) } catch { /* ignore */ }
|
|
1124
|
+
}
|
|
1125
|
+
if (!data || !Array.isArray(data.days) || data.days.length === 0) {
|
|
1126
|
+
return h('div', { className: 'dsh-cost-heat' },
|
|
1127
|
+
h('div', { className: 'dsh-cost-heat-head' }, h('span', { className: 'dsh-cost-heat-title' }, t('heatTitle'))),
|
|
1128
|
+
h('div', { className: 'dsh-cost-empty' }, t('heatEmpty')))
|
|
1129
|
+
}
|
|
1130
|
+
const state = latestState
|
|
1131
|
+
const metricOf = (day) => (metric === 'calls' ? day.calls ?? 0 : metric === 'tokens' ? day.tokens ?? 0 : day.cost ?? 0)
|
|
1132
|
+
const metricText = (day) => metric === 'calls'
|
|
1133
|
+
? `${day.calls ?? 0} ${t('callsUnit')}`
|
|
1134
|
+
: metric === 'tokens' ? `${(day.tokens ?? 0).toLocaleString()} tok` : moneyOf(state, day.cost ?? 0)
|
|
1135
|
+
const byDate = new Map(data.days.map((day) => [day.date, day]))
|
|
1136
|
+
// 182 天窗口对齐周一(0=周日 → getDay()==0 时前推 6 天)。
|
|
1137
|
+
const end = new Date()
|
|
1138
|
+
end.setHours(12, 0, 0, 0)
|
|
1139
|
+
const start = new Date(end.getTime() - 181 * 86400000)
|
|
1140
|
+
start.setDate(start.getDate() - ((start.getDay() + 6) % 7))
|
|
1141
|
+
const columns = []
|
|
1142
|
+
let max = 0
|
|
1143
|
+
const cursor = new Date(start)
|
|
1144
|
+
while (cursor <= end) {
|
|
1145
|
+
const col = []
|
|
1146
|
+
for (let i = 0; i < 7 && cursor <= end; i += 1) {
|
|
1147
|
+
const key = `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}-${String(cursor.getDate()).padStart(2, '0')}`
|
|
1148
|
+
const day = byDate.get(key) ?? { date: key, cost: 0, calls: 0, tokens: 0 }
|
|
1149
|
+
max = Math.max(max, metricOf(day))
|
|
1150
|
+
col.push(day)
|
|
1151
|
+
cursor.setDate(cursor.getDate() + 1)
|
|
1152
|
+
}
|
|
1153
|
+
columns.push(col)
|
|
1154
|
+
// 补齐当前周剩余(未来日期 → 空白占位,保持 7 行对齐)。
|
|
1155
|
+
while (col.length < 7) col.push(null)
|
|
1156
|
+
}
|
|
1157
|
+
const levelOf = (value) => {
|
|
1158
|
+
if (!(value > 0) || !(max > 0)) return 0
|
|
1159
|
+
const ratio = value / max
|
|
1160
|
+
return ratio > 0.75 ? 4 : ratio > 0.5 ? 3 : ratio > 0.25 ? 2 : 1
|
|
1161
|
+
}
|
|
1162
|
+
const weekLabels = lang === 'zh' ? ['一', '三', '五'] : ['M', 'W', 'F']
|
|
1163
|
+
const months = columns.map((col, index) => {
|
|
1164
|
+
const first = col.find(Boolean)
|
|
1165
|
+
if (!first) return ''
|
|
1166
|
+
const month = Number(first.date.slice(5, 7))
|
|
1167
|
+
const prev = index > 0 ? columns[index - 1].find(Boolean) : null
|
|
1168
|
+
return prev && Number(prev.date.slice(5, 7)) === month ? '' : String(month)
|
|
1169
|
+
})
|
|
1170
|
+
return h('div', { className: 'dsh-cost-heat' },
|
|
1171
|
+
h('div', { className: 'dsh-cost-heat-head' },
|
|
1172
|
+
h('span', { className: 'dsh-cost-heat-title' }, t('heatTitle')),
|
|
1173
|
+
h('div', { className: 'dsh-cost-heat-toggle' },
|
|
1174
|
+
['cost', 'calls', 'tokens'].map((value) => h('button', {
|
|
1175
|
+
type: 'button',
|
|
1176
|
+
key: value,
|
|
1177
|
+
'aria-pressed': metric === value ? 'true' : 'false',
|
|
1178
|
+
onClick: () => pickMetric(value),
|
|
1179
|
+
}, value === 'cost' ? t('heatCost') : value === 'calls' ? t('heatCalls') : t('heatTokens'))))),
|
|
1180
|
+
h('div', { className: 'dsh-cost-heat-months' },
|
|
1181
|
+
months.map((label, index) => h('span', { key: index }, label))),
|
|
1182
|
+
h('div', { style: { display: 'flex', gap: '4px' } },
|
|
1183
|
+
h('div', { className: 'dsh-cost-heat-weeks' },
|
|
1184
|
+
[0, 2, 4].map((row, idx) => h('span', {
|
|
1185
|
+
key: row,
|
|
1186
|
+
style: { display: 'block', height: '11px', lineHeight: '11px', marginTop: idx === 0 ? '0' : '17px' },
|
|
1187
|
+
}, weekLabels[idx]))),
|
|
1188
|
+
h('div', { className: 'dsh-cost-heat-grid' },
|
|
1189
|
+
columns.map((col, colIndex) => h('div', { className: 'dsh-cost-heat-col', key: col[0]?.date ?? colIndex },
|
|
1190
|
+
col.map((day, rowIndex) => {
|
|
1191
|
+
if (!day) return h('div', { className: 'dsh-cost-heat-cell blank', key: `blank-${rowIndex}` })
|
|
1192
|
+
const level = levelOf(metricOf(day))
|
|
1193
|
+
return h('div', {
|
|
1194
|
+
className: 'dsh-cost-heat-cell',
|
|
1195
|
+
key: day.date,
|
|
1196
|
+
style: level > 0 ? { background: heatLevelColor(level) } : undefined,
|
|
1197
|
+
title: `${day.date} · ${metricText(day)}`,
|
|
1198
|
+
})
|
|
1199
|
+
}))))),
|
|
1200
|
+
h('div', { className: 'dsh-cost-heat-legend' },
|
|
1201
|
+
t('heatLess'),
|
|
1202
|
+
HEAT_LEVEL_MIX.map((_, level) => h('div', {
|
|
1203
|
+
className: 'dsh-cost-heat-cell',
|
|
1204
|
+
key: level,
|
|
1205
|
+
style: level > 0 ? { background: heatLevelColor(level) } : undefined,
|
|
1206
|
+
})),
|
|
1207
|
+
t('heatMore')))
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/* -- Coding Plan 配额面板(订阅额度适配器,Key 只存本机) ---------- */
|
|
1211
|
+
|
|
1212
|
+
const CODING_PLAN_LIST = ['anthropic', 'zai', 'kimi', 'minimax', 'openrouter', 'siliconflow', 'commandcode', 'clinepass']
|
|
1213
|
+
const CODING_PLAN_LABEL_KEYS = {
|
|
1214
|
+
anthropic: 'planAnthropic', zai: 'planZai', minimax: 'planMinimax', kimi: 'planKimi',
|
|
1215
|
+
openrouter: 'planOpenrouter', siliconflow: 'planSiliconflow', commandcode: 'planCommandcode', clinepass: 'planClinepass',
|
|
1216
|
+
}
|
|
1217
|
+
const PLAN_WINDOW_KEYS = {
|
|
1218
|
+
fiveHour: 'winFiveHour', '5h': 'winFiveHour', weekly: 'winWeekly', '7d': 'winWeekly',
|
|
1219
|
+
monthly: 'winMonthly', month: 'winMonthly', balance: 'winBalance', credits: 'winCredits',
|
|
1220
|
+
plan: 'winPlan', compensation: 'winCompensation', current: 'winCurrent',
|
|
1221
|
+
}
|
|
1222
|
+
const planWindowName = (name) => t(PLAN_WINDOW_KEYS[name] ?? name)
|
|
1223
|
+
|
|
1224
|
+
function CodingPlansCard() {
|
|
1225
|
+
const [open, setOpen] = React.useState(() => {
|
|
1226
|
+
try { return localStorage.getItem('dsh-cost-plans-open') === '1' } catch { return false }
|
|
1227
|
+
})
|
|
1228
|
+
const [providers, setProviders] = React.useState(null)
|
|
1229
|
+
const [drafts, setDrafts] = React.useState({})
|
|
1230
|
+
const [notes, setNotes] = React.useState({})
|
|
1231
|
+
const load = React.useCallback(() => {
|
|
1232
|
+
fetch(`${ENDPOINT}/coding-plans`, { cache: 'no-store' })
|
|
1233
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1234
|
+
.then((value) => { if (value && typeof value.providers === 'object') setProviders(value.providers) })
|
|
1235
|
+
.catch(() => {})
|
|
1236
|
+
}, [])
|
|
1237
|
+
React.useEffect(() => {
|
|
1238
|
+
if (!open) return undefined
|
|
1239
|
+
load()
|
|
1240
|
+
const timerId = setInterval(load, 60_000)
|
|
1241
|
+
return () => clearInterval(timerId)
|
|
1242
|
+
}, [open, load])
|
|
1243
|
+
const toggle = () => {
|
|
1244
|
+
const next = !open
|
|
1245
|
+
setOpen(next)
|
|
1246
|
+
try { localStorage.setItem('dsh-cost-plans-open', next ? '1' : '0') } catch { /* ignore */ }
|
|
1247
|
+
}
|
|
1248
|
+
const save = async (id) => {
|
|
1249
|
+
const key = String(drafts[id] ?? '').trim()
|
|
1250
|
+
if (!key) return
|
|
1251
|
+
try {
|
|
1252
|
+
await fetch(`${ENDPOINT}/coding-plans/config`, {
|
|
1253
|
+
method: 'POST',
|
|
1254
|
+
headers: { 'content-type': 'application/json' },
|
|
1255
|
+
body: JSON.stringify({ provider: id, key }),
|
|
1256
|
+
})
|
|
1257
|
+
setNotes((m) => ({ ...m, [id]: t('plansSaved') }))
|
|
1258
|
+
setDrafts((m) => ({ ...m, [id]: '' }))
|
|
1259
|
+
await fetch(`${ENDPOINT}/coding-plans/refresh`, {
|
|
1260
|
+
method: 'POST',
|
|
1261
|
+
headers: { 'content-type': 'application/json' },
|
|
1262
|
+
body: JSON.stringify({ provider: id }),
|
|
1263
|
+
})
|
|
1264
|
+
} catch { /* 下轮轮询兜底 */ }
|
|
1265
|
+
load()
|
|
1266
|
+
}
|
|
1267
|
+
const refreshAll = async () => {
|
|
1268
|
+
try {
|
|
1269
|
+
await fetch(`${ENDPOINT}/coding-plans/refresh`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
1270
|
+
} catch { /* ignore */ }
|
|
1271
|
+
load()
|
|
1272
|
+
}
|
|
1273
|
+
const resetAtText = (iso) => {
|
|
1274
|
+
const ms = Date.parse(iso)
|
|
1275
|
+
return Number.isFinite(ms)
|
|
1276
|
+
? ` · ${t('winResets')} ${new Date(ms).toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`
|
|
1277
|
+
: ''
|
|
1278
|
+
}
|
|
1279
|
+
return h('div', { className: 'dsh-cost-plans' },
|
|
1280
|
+
h('div', { className: 'dsh-cost-plans-head' },
|
|
1281
|
+
h('span', { className: 'dsh-cost-plans-caret' }, open ? '▾' : '▸'),
|
|
1282
|
+
h('span', { className: 'dsh-cost-plans-title', onClick: toggle }, t('plansTitle')),
|
|
1283
|
+
open && providers != null
|
|
1284
|
+
? h('button', { type: 'button', className: 'dsh-cost-plan-btn', onClick: () => { void refreshAll() } }, t('plansRefreshAll'))
|
|
1285
|
+
: null),
|
|
1286
|
+
open
|
|
1287
|
+
? h('div', { className: 'dsh-cost-plans-hint' }, t('plansHint'))
|
|
1288
|
+
: null,
|
|
1289
|
+
open
|
|
1290
|
+
? CODING_PLAN_LIST.map((id) => {
|
|
1291
|
+
const info = providers?.[id] ?? { hasKey: false }
|
|
1292
|
+
const windows = info.windows ?? null
|
|
1293
|
+
return h('div', { className: 'dsh-cost-plan', key: id },
|
|
1294
|
+
h('div', { className: 'dsh-cost-plan-row' },
|
|
1295
|
+
h('span', { className: 'dsh-cost-plan-name', title: t(CODING_PLAN_LABEL_KEYS[id]) }, t(CODING_PLAN_LABEL_KEYS[id])),
|
|
1296
|
+
h('input', {
|
|
1297
|
+
className: 'dsh-cost-plan-input',
|
|
1298
|
+
type: 'password',
|
|
1299
|
+
autoComplete: 'off',
|
|
1300
|
+
placeholder: info.hasKey ? `••••••(${t('plansConfigured')})` : 'API Key',
|
|
1301
|
+
value: drafts[id] ?? '',
|
|
1302
|
+
onChange: (event) => setDrafts((m) => ({ ...m, [id]: event.target.value })),
|
|
1303
|
+
onKeyDown: (event) => { if (event.key === 'Enter') void save(id) },
|
|
1304
|
+
}),
|
|
1305
|
+
h('button', {
|
|
1306
|
+
type: 'button',
|
|
1307
|
+
className: 'dsh-cost-plan-btn',
|
|
1308
|
+
disabled: !String(drafts[id] ?? '').trim(),
|
|
1309
|
+
onClick: () => { void save(id) },
|
|
1310
|
+
}, t('plansSave')),
|
|
1311
|
+
notes[id]
|
|
1312
|
+
? h('span', { className: 'dsh-cost-plan-btn', style: { background: 'transparent', cursor: 'default' } }, notes[id])
|
|
1313
|
+
: null),
|
|
1314
|
+
!info.hasKey
|
|
1315
|
+
? null
|
|
1316
|
+
: info.error
|
|
1317
|
+
? h('div', { className: 'dsh-cost-plan-err' }, `${t('plansError')}: ${info.error}`)
|
|
1318
|
+
: windows && Object.keys(windows).length > 0
|
|
1319
|
+
? Object.entries(windows).map(([name, win]) => {
|
|
1320
|
+
const bar = win.unlimited
|
|
1321
|
+
? null
|
|
1322
|
+
: h('div', { className: 'dsh-cost-plan-bar' },
|
|
1323
|
+
h('div', {
|
|
1324
|
+
className: 'dsh-cost-plan-fill' + (Number(win.percent) >= 80 ? ' hot' : ''),
|
|
1325
|
+
style: { width: `${Math.min(100, Math.max(0, Number(win.percent) || 0))}%` },
|
|
1326
|
+
}))
|
|
1327
|
+
const pct = win.unlimited
|
|
1328
|
+
? t('winUnlimited')
|
|
1329
|
+
: `${(Number(win.percent) || 0).toFixed(1)}%${resetAtText(win.resetsAt)}`
|
|
1330
|
+
return h('div', { className: 'dsh-cost-plan-win', key: name },
|
|
1331
|
+
h('span', { className: 'dsh-cost-plan-winname', title: name }, planWindowName(name)),
|
|
1332
|
+
bar ?? h('span', { className: 'dsh-cost-plan-winname', style: { flex: 1 } }, String(win.text ?? '')),
|
|
1333
|
+
h('span', { className: 'dsh-cost-plan-pct' }, pct))
|
|
1334
|
+
})
|
|
1335
|
+
: h('div', { className: 'dsh-cost-plan-err' }, '…'))
|
|
1336
|
+
})
|
|
1337
|
+
: null)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
951
1340
|
function CostSection() {
|
|
952
1341
|
const state = useCostState()
|
|
953
1342
|
const forecast = useForecast()
|
|
@@ -959,40 +1348,40 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
959
1348
|
const forecastCard = forecast && forecast.basis === 'recent'
|
|
960
1349
|
? h('div', { className: 'dsh-cost-forecast', 'data-over': String(!!overBudget) },
|
|
961
1350
|
h('div', { className: 'dsh-cost-forecast-head' },
|
|
962
|
-
h('span', { className: 'dsh-cost-card-title' }, '
|
|
1351
|
+
h('span', { className: 'dsh-cost-card-title' }, t('monthForecast')),
|
|
963
1352
|
h('span', { className: 'dsh-cost-forecast-value' }, moneyOf(state, forecast.projected))),
|
|
964
1353
|
h('div', { className: 'dsh-cost-forecast-sub' },
|
|
965
|
-
|
|
1354
|
+
`${t('spent')}${moneyOf(state, forecast.monthCost)} · ${t('dailyAvg')}${moneyOf(state, forecast.avgDaily)} · ${t('daysLeft')(forecast.daysLeft)}${overBudget ? ` · ${t('overBudget')}` : ''}`))
|
|
966
1355
|
: forecast && forecast.basis === 'insufficient'
|
|
967
1356
|
? h('div', { className: 'dsh-cost-forecast' },
|
|
968
1357
|
h('div', { className: 'dsh-cost-forecast-sub' },
|
|
969
|
-
|
|
1358
|
+
t('forecastNeedDays')(forecast.sampleDays ?? 0)))
|
|
970
1359
|
: null
|
|
971
1360
|
const cards = h('div', { className: 'dsh-cost-cards' },
|
|
972
|
-
h(CostCard, { title: '
|
|
973
|
-
h(CostCard, { title: '
|
|
974
|
-
h(CostCard, { title: '
|
|
1361
|
+
h(CostCard, { title: t('today'), row: state?.today, state }),
|
|
1362
|
+
h(CostCard, { title: t('thisMonth'), row: state?.month, state }),
|
|
1363
|
+
h(CostCard, { title: t('total'), row: state?.total, state }))
|
|
975
1364
|
const recent = Array.isArray(state?.recent) && state.recent.length > 0
|
|
976
1365
|
? h('div', { className: 'dsh-cost-table' },
|
|
977
1366
|
state.recent.slice().reverse().map((day) => h('div', { className: 'dsh-cost-row', key: day.date },
|
|
978
1367
|
h('span', { className: 'dsh-cost-dim' }, day.date.slice(5)),
|
|
979
|
-
h('span', { className: 'dsh-cost-grow' }, `${day.calls}
|
|
1368
|
+
h('span', { className: 'dsh-cost-grow' }, `${day.calls} ${t('callsUnit')}`),
|
|
980
1369
|
h('span', { className: 'dsh-cost-num' }, moneyOf(state, day.cost)))))
|
|
981
|
-
: h('div', { className: 'dsh-cost-table' }, h('div', { className: 'dsh-cost-empty' }, '
|
|
1370
|
+
: h('div', { className: 'dsh-cost-table' }, h('div', { className: 'dsh-cost-empty' }, t('noRecords')))
|
|
982
1371
|
const sessions = Array.isArray(state?.sessions) && state.sessions.length > 0
|
|
983
1372
|
? h('div', { className: 'dsh-cost-table' },
|
|
984
1373
|
state.sessions.slice(0, 10).map((session) => h('div', { className: 'dsh-cost-row', key: `${session.date}-${session.id}` },
|
|
985
1374
|
h('span', { className: 'dsh-cost-dim' }, new Date(session.at || session.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })),
|
|
986
1375
|
h('span', { className: 'dsh-cost-grow' }, String(session.id).replace(/^session-/, '').slice(0, 8)),
|
|
987
|
-
h('span', { className: 'dsh-cost-grow' }, `${session.calls}
|
|
1376
|
+
h('span', { className: 'dsh-cost-grow' }, `${session.calls}${t('callTimes')}`),
|
|
988
1377
|
h('span', { className: 'dsh-cost-num' }, moneyOf(state, session.cost)))))
|
|
989
1378
|
: null
|
|
990
1379
|
const footerBits = []
|
|
991
1380
|
if (state?.peak) {
|
|
992
|
-
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'peak' }, state.peak.isPeak ? '
|
|
1381
|
+
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'peak' }, state.peak.isPeak ? t('peakBadge') : t('offPeakBadge')))
|
|
993
1382
|
}
|
|
994
1383
|
if (Array.isArray(state?.unmatched) && state.unmatched.length > 0) {
|
|
995
|
-
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'unmatched', title: state.unmatched.join(', ') }, `${state.unmatched.length}
|
|
1384
|
+
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'unmatched', title: state.unmatched.join(', ') }, `${state.unmatched.length} ${t('unmatchedModels')}`))
|
|
996
1385
|
}
|
|
997
1386
|
const downloadExport = (format) => {
|
|
998
1387
|
const link = document.createElement('a')
|
|
@@ -1002,8 +1391,8 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
1002
1391
|
link.click()
|
|
1003
1392
|
link.remove()
|
|
1004
1393
|
}
|
|
1005
|
-
footerBits.push(h('button', { className: 'dsh-cost-badge action', key: 'export-json', title: '
|
|
1006
|
-
footerBits.push(h('button', { className: 'dsh-cost-badge action', key: 'export-csv', title: '
|
|
1394
|
+
footerBits.push(h('button', { className: 'dsh-cost-badge action', key: 'export-json', title: t('exportJsonTitle'), onClick: () => downloadExport('json') }, t('exportJson')))
|
|
1395
|
+
footerBits.push(h('button', { className: 'dsh-cost-badge action', key: 'export-csv', title: t('exportCsvTitle'), onClick: () => downloadExport('csv') }, t('exportCsv')))
|
|
1007
1396
|
// 官方价格同步 + 汇率刷新(点击后按钮短暂显示结果,再自动复原)。
|
|
1008
1397
|
const [syncNote, setSyncNote] = React.useState('')
|
|
1009
1398
|
React.useEffect(() => {
|
|
@@ -1016,22 +1405,35 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
1016
1405
|
const res = await fetch(`${ENDPOINT}/${endpoint}`, { method: 'POST' })
|
|
1017
1406
|
const data = await res.json().catch(() => null)
|
|
1018
1407
|
if (res.ok && data?.ok) setSyncNote(okLabel)
|
|
1019
|
-
else setSyncNote(
|
|
1408
|
+
else setSyncNote(`${t('failed')}: ${data?.error ?? res.status}`)
|
|
1020
1409
|
if (res.ok) void pollOnce()
|
|
1021
|
-
} catch (error) { setSyncNote(
|
|
1410
|
+
} catch (error) { setSyncNote(`${t('failed')}: ${String(error?.message ?? error).slice(0, 60)}`) }
|
|
1022
1411
|
}
|
|
1023
1412
|
footerBits.push(h('button', {
|
|
1024
1413
|
className: 'dsh-cost-badge action',
|
|
1025
1414
|
key: 'sync-prices',
|
|
1026
|
-
title: '
|
|
1027
|
-
onClick: () => { void postSync('fetch-prices', '
|
|
1028
|
-
}, '
|
|
1415
|
+
title: t('syncPricesTitle'),
|
|
1416
|
+
onClick: () => { void postSync('fetch-prices', t('priceSynced')) },
|
|
1417
|
+
}, t('syncPrices')))
|
|
1029
1418
|
footerBits.push(h('button', {
|
|
1030
1419
|
className: 'dsh-cost-badge action',
|
|
1031
1420
|
key: 'sync-rate',
|
|
1032
|
-
title: '
|
|
1033
|
-
onClick: () => { void postSync('fetch-rate', '
|
|
1034
|
-
}, '
|
|
1421
|
+
title: t('refreshRateTitle'),
|
|
1422
|
+
onClick: () => { void postSync('fetch-rate', t('rateRefreshed')) },
|
|
1423
|
+
}, t('refreshRate')))
|
|
1424
|
+
// 界面语言:中文 / English 两枚徽标按钮,立即生效(宿主注册项刷新后生效)。
|
|
1425
|
+
const langButtons = [
|
|
1426
|
+
['zh', '中文'],
|
|
1427
|
+
['en', 'English'],
|
|
1428
|
+
].map(([value, label]) => h('button', {
|
|
1429
|
+
className: 'dsh-cost-badge action',
|
|
1430
|
+
key: `lang-${value}`,
|
|
1431
|
+
'aria-pressed': lang === value ? 'true' : 'false',
|
|
1432
|
+
title: t('langHint'),
|
|
1433
|
+
style: lang === value ? { color: 'var(--dsw-alias-label-primary)', fontWeight: 500 } : undefined,
|
|
1434
|
+
onClick: () => setLang(value),
|
|
1435
|
+
}, label))
|
|
1436
|
+
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'lang-group', style: { gap: '6px' } }, langButtons))
|
|
1035
1437
|
if (syncNote) {
|
|
1036
1438
|
footerBits.push(h('span', { className: 'dsh-cost-badge', key: 'sync-note' }, syncNote))
|
|
1037
1439
|
}
|
|
@@ -1039,10 +1441,12 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
1039
1441
|
h(BudgetCard, { state }),
|
|
1040
1442
|
cards,
|
|
1041
1443
|
forecastCard,
|
|
1444
|
+
h(HeatmapCard, null),
|
|
1042
1445
|
recent,
|
|
1043
1446
|
sessions,
|
|
1044
1447
|
h(RecentCalls, { state }),
|
|
1045
|
-
h(
|
|
1448
|
+
h(CodingPlansCard, null),
|
|
1449
|
+
h('div', { style: { display: 'flex', gap: '6px', flexWrap: 'wrap' } }, footerBits))
|
|
1046
1450
|
}
|
|
1047
1451
|
|
|
1048
1452
|
/* -- 装配 ---------------------------------------------------------- */
|
|
@@ -1055,7 +1459,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
1055
1459
|
startSessionAnchor()
|
|
1056
1460
|
startUsageAnchors()
|
|
1057
1461
|
sessionWatchers.add(renderSessionAnchor)
|
|
1058
|
-
console.info('[dsh-cost] client 0.
|
|
1462
|
+
console.info('[dsh-cost] client 0.10.0 active')
|
|
1059
1463
|
// 只当 sessionId 感知器(渲染 null),费用本体锚定在宿主状态行末尾。
|
|
1060
1464
|
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
1061
1465
|
name: 'conversation.input.dock',
|
|
@@ -1089,7 +1493,7 @@ body[data-ds-dark-theme] .dsh-cost-pop {
|
|
|
1089
1493
|
name: 'settings.section',
|
|
1090
1494
|
id: 'dsh-cost',
|
|
1091
1495
|
order: 30,
|
|
1092
|
-
label: '
|
|
1496
|
+
label: t('sectionLabel'),
|
|
1093
1497
|
}, CostSection))
|
|
1094
1498
|
},
|
|
1095
1499
|
}
|
|
@@ -4329,171 +4733,1197 @@ svg[viewBox="0 0 640 40"] { visibility: hidden !important; }
|
|
|
4329
4733
|
}
|
|
4330
4734
|
}
|
|
4331
4735
|
|
|
4332
|
-
/*
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4736
|
+
/* 由 scripts/build-suite.mjs 从 dsh-compaction-control/client.js 机械生成;勿手改。 */
|
|
4737
|
+
/**
|
|
4738
|
+
* dsh-compaction-control 的 web 客户端部分。
|
|
4739
|
+
*
|
|
4740
|
+
* 在输入栏右侧区(conversation.input.right,list 槽位,渲染在模型选择器
|
|
4741
|
+
* 左边)放一颗和内置 ContextMeter 同款几何的上下文占用圆环:
|
|
4742
|
+
*
|
|
4743
|
+
* - 数据走官方会话投影:useProjection('contextPressure') /
|
|
4744
|
+
* 'contextBreakdown'(token-meter 注册,session 标准 props 自动下发),
|
|
4745
|
+
* 不碰内部 RPC。
|
|
4746
|
+
* - 点开面板 = 内置面板的三段构成(系统提示/工具/对话)+ 两个新能力:
|
|
4747
|
+
* 1. 「立即压缩」→ POST /api/dsh-compaction-control/compact,
|
|
4748
|
+
* 服务端 ctx.compaction.compactNow(agent, signal)(官方 /compact
|
|
4749
|
+
* 的编程入口,等价免打字)。
|
|
4750
|
+
* 2. 自动压缩调节(auto 开关 / 触发阈值 / 保留比例)→ POST
|
|
4751
|
+
* /state,服务端写 profile 用户补丁层,重启后生效。
|
|
4752
|
+
* - hideBuiltIn(默认开):运行时把官方占用环按钮 visibility:hidden。
|
|
4753
|
+
* 内置圆环是 InputBar 硬编码(不在槽位里),没有官方关闭开关;识别
|
|
4754
|
+
* 特征收严(0.2.2)= dialog 按钮 + svg 带圆 + 文本恰为「NN%」——
|
|
4755
|
+
* 0.2.1 的宽泛特征在真机命中了 dock 里的未知按钮,文本白名单把误伤
|
|
4756
|
+
* 半径收到最小。MutationObserver 在宿主重挂时补隐藏。
|
|
4757
|
+
* - 落位(0.2.5 定稿):圆环直接作为 conversation.input.right 槽位内容
|
|
4758
|
+
* 渲染——已装 app.asar 的 InputBar 源码实锤 standardControls 里
|
|
4759
|
+
* renderSlot("conversation.input.right") 与 renderSlot("conversation.
|
|
4760
|
+
* input.model")(模型选择器)是 JSX 兄弟节点且前者在前,槽位位置即
|
|
4761
|
+
* 「模型选择器紧左侧」,官方契约保证,零坐标手术。0.1.6 起四轮方案
|
|
4762
|
+
* 全废:fixed 不占布局流必压按钮;「发送左侧最靠右下拉」会把状态行
|
|
4763
|
+
* 的官方环/费用按钮误当选择器(不同行的按钮在窗口坐标里也分左右);
|
|
4764
|
+
* 剪贴板所在 composer.dock 实际渲染在会话状态行。另:本环永远渲染
|
|
4765
|
+
* ——0.2.1 的「hideBuiltIn=false 时退场」门已拆(持久化值恰为 false
|
|
4766
|
+
* 曾把环卸成空壳占位);该开关现在只管官方环的隐藏。占用比读取改
|
|
4767
|
+
* `||`(projectedTokens 可为合法 0,`??` 不会回落 pressureTokens,
|
|
4768
|
+
* 真机出现过 0% 环而官方 17%)。
|
|
4769
|
+
*
|
|
4770
|
+
* 诊断:boot/register/crash 上报服务端 /diag,落
|
|
4771
|
+
* ~/.dsh/compaction-control-debug.log;window.__dshCompactionControl 供检查。
|
|
4772
|
+
*/
|
|
4337
4773
|
|
|
4338
|
-
|
|
4339
|
-
|
|
4774
|
+
function createCompactionClient(require) {
|
|
4775
|
+
const React = require('react')
|
|
4776
|
+
const h = React.createElement
|
|
4777
|
+
const { useEffect, useRef, useState } = React
|
|
4340
4778
|
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4779
|
+
// react shim 没有 createPortal;必须从 react-dom 取(内置组件同款做法)。
|
|
4780
|
+
let createPortal = null
|
|
4781
|
+
try {
|
|
4782
|
+
const ReactDOM = require('react-dom')
|
|
4783
|
+
if (typeof ReactDOM?.createPortal === 'function') createPortal = ReactDOM.createPortal
|
|
4784
|
+
} catch { createPortal = null }
|
|
4785
|
+
|
|
4786
|
+
const ENDPOINT = '/api/dsh-compaction-control'
|
|
4787
|
+
const VERSION = '0.2.5'
|
|
4788
|
+
/** 官方环几何 14px viewBox / 2 单位描边;纯图标无百分比文字撑视觉重量,渲染尺寸放大到 18px(描边随比例 ≈2.6px)。 */
|
|
4789
|
+
const RADIUS = 5.5
|
|
4790
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
|
|
4791
|
+
const RING_SIZE = 18
|
|
4792
|
+
/** 官方面板宽度:min(264px, 100vw - 24px)。 */
|
|
4793
|
+
const PANEL_WIDTH = 264
|
|
4794
|
+
|
|
4795
|
+
/** 全部对齐官方 ContextMeter/菜单面板 tokens(ContextMeter.module.css 同源)。 */
|
|
4796
|
+
const CSS = `
|
|
4797
|
+
.dsh-cc-host { display: inline-flex; align-items: center; flex: none; margin: 0 2px; }
|
|
4798
|
+
.dsh-cc-root { display: inline-flex; }
|
|
4799
|
+
.dsh-cc-pending .dsh-cc-trigger { opacity: 0.65; }
|
|
4800
|
+
.dsh-cc-pending .dsh-cc-trigger:hover,
|
|
4801
|
+
.dsh-cc-pending .dsh-cc-trigger[aria-expanded='true'] { opacity: 1; }
|
|
4802
|
+
.dsh-cc-trigger {
|
|
4803
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
4804
|
+
flex: none; height: 28px; padding: 0 6px;
|
|
4805
|
+
border: none; border-radius: var(--dsw-radius-sm, 8px);
|
|
4806
|
+
background: transparent;
|
|
4807
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72));
|
|
4808
|
+
font-family: inherit; line-height: 20px;
|
|
4809
|
+
cursor: pointer;
|
|
4355
4810
|
}
|
|
4356
|
-
.dsh-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
z-index: 1100;
|
|
4360
|
-
box-sizing: border-box;
|
|
4361
|
-
border: 0;
|
|
4362
|
-
border-radius: var(--dsw-radius-lg, 12px);
|
|
4363
|
-
background: var(--dsw-specific-menu, rgba(67, 69, 74, 0.45));
|
|
4364
|
-
backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
4365
|
-
-webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
4366
|
-
width: min(380px, 100vw - 24px);
|
|
4367
|
-
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
|
|
4368
|
-
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px var(--dsw-elevation-stroke-color), 0 3px 8px 0 rgba(0, 0, 0, 0.04), 0 0 20px 0 rgba(0, 0, 0, 0.05));
|
|
4369
|
-
padding: 10px;
|
|
4370
|
-
display: flex;
|
|
4371
|
-
flex-direction: column;
|
|
4372
|
-
gap: 8px;
|
|
4811
|
+
.dsh-cc-trigger:hover, .dsh-cc-trigger[aria-expanded='true'] {
|
|
4812
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08));
|
|
4813
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72));
|
|
4373
4814
|
}
|
|
4374
|
-
.dsh-
|
|
4375
|
-
.dsh-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
border
|
|
4382
|
-
|
|
4815
|
+
.dsh-cc-ring-track { fill: none; stroke: var(--dsw-alias-border-l3, rgba(255, 255, 255, 0.18)); stroke-width: 2; }
|
|
4816
|
+
.dsh-cc-ring-fill { fill: none; stroke: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72)); stroke-width: 2; stroke-linecap: round; transition: stroke-dasharray 200ms ease; }
|
|
4817
|
+
|
|
4818
|
+
.dsh-cc-panel {
|
|
4819
|
+
position: fixed; z-index: 1100; box-sizing: border-box;
|
|
4820
|
+
width: min(264px, calc(100vw - 24px)); max-height: 70vh; overflow-y: auto;
|
|
4821
|
+
padding: 12px;
|
|
4822
|
+
border: 0; border-radius: 14px;
|
|
4823
|
+
/* 底色写死 0.94,不用 --dsw-specific-menu:Windows 默认值是 rgba(67,69,74,.45),
|
|
4824
|
+
那是给带 MenuSurface 底衬的官方菜单用的;插件 portal 到 body 没有底衬,
|
|
4825
|
+
45% 一旦 backdrop-filter 失效就发灰糊在背景上。0.94 取自 dsh 的 macOS 档。 */
|
|
4826
|
+
background: rgba(248, 249, 250, 0.94);
|
|
4827
|
+
backdrop-filter: blur(40px) saturate(150%);
|
|
4828
|
+
-webkit-backdrop-filter: blur(40px) saturate(150%);
|
|
4829
|
+
box-shadow:
|
|
4830
|
+
0 0 0 0.5px rgba(0, 0, 0, 0.07),
|
|
4831
|
+
0 3px 8px 0 rgba(0, 0, 0, 0.04),
|
|
4832
|
+
0 0 20px 0 rgba(0, 0, 0, 0.05);
|
|
4833
|
+
font-size: 12px; line-height: 20px;
|
|
4834
|
+
color: rgba(28, 30, 34, 0.86);
|
|
4835
|
+
cursor: default;
|
|
4836
|
+
animation: dsh-cc-in 140ms ease-out both;
|
|
4383
4837
|
}
|
|
4384
|
-
|
|
4385
|
-
.
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
4392
|
-
border-radius: var(--dsw-radius-sm, 6px);
|
|
4393
|
-
cursor: pointer;
|
|
4838
|
+
body[data-ds-dark-theme] .dsh-cc-panel {
|
|
4839
|
+
background: rgba(48, 49, 54, 0.94);
|
|
4840
|
+
box-shadow:
|
|
4841
|
+
0 0 0 0.5px rgba(255, 255, 255, 0.10),
|
|
4842
|
+
0 3px 8px 0 rgba(0, 0, 0, 0.04),
|
|
4843
|
+
0 0 20px 0 rgba(0, 0, 0, 0.05);
|
|
4844
|
+
color: rgba(232, 238, 250, 0.72);
|
|
4394
4845
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
.dsh-
|
|
4400
|
-
.dsh-
|
|
4401
|
-
.dsh-
|
|
4402
|
-
.dsh-
|
|
4403
|
-
|
|
4404
|
-
.dsh-
|
|
4405
|
-
.dsh-
|
|
4406
|
-
.dsh-
|
|
4407
|
-
.dsh-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4846
|
+
/* 只动 opacity:展开方向由内联 transform 控制,keyframes 不能碰 transform */
|
|
4847
|
+
@keyframes dsh-cc-in { from { opacity: 0; } to { opacity: 1; } }
|
|
4848
|
+
@media (prefers-reduced-motion: reduce) { .dsh-cc-panel { animation: none; } }
|
|
4849
|
+
|
|
4850
|
+
.dsh-cc-head { display: flex; align-items: center; gap: 6px; }
|
|
4851
|
+
.dsh-cc-head-label { color: var(--dsw-alias-label-tertiary, rgba(232, 238, 250, 0.55)); white-space: nowrap; }
|
|
4852
|
+
.dsh-cc-head-pct { font-weight: 500; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9)); }
|
|
4853
|
+
.dsh-cc-head-fig { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9)); }
|
|
4854
|
+
|
|
4855
|
+
.dsh-cc-bar { display: flex; gap: 1px; margin: 8px 0 8px; height: 4px; border-radius: 999px; background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); overflow: hidden; }
|
|
4856
|
+
.dsh-cc-bar-seg { flex: none; min-width: 2px; height: 100%; border-radius: 1px; background: var(--dsh-cc-tint, var(--dsw-alias-label-tertiary)); }
|
|
4857
|
+
.dsh-cc-tint-system { --dsh-cc-tint: var(--dsw-static-neutral-bluish-400, #9db2c8); }
|
|
4858
|
+
.dsh-cc-tint-tools { --dsh-cc-tint: rgb(167, 139, 250); }
|
|
4859
|
+
.dsh-cc-tint-messages { --dsh-cc-tint: var(--dsw-static-blue-450, #5b8def); }
|
|
4860
|
+
|
|
4861
|
+
.dsh-cc-rows { margin: 4px 0 0; }
|
|
4862
|
+
.dsh-cc-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 1px 0; }
|
|
4863
|
+
.dsh-cc-row dt { color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72)); }
|
|
4864
|
+
.dsh-cc-row dd { margin: 0; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9)); }
|
|
4865
|
+
.dsh-cc-dot { display: inline-block; margin-right: 6px; width: 8px; height: 8px; border-radius: 2px; background: var(--dsh-cc-tint); vertical-align: baseline; }
|
|
4866
|
+
|
|
4867
|
+
.dsh-cc-section { border-top: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08)); margin-top: 8px; padding-top: 6px; }
|
|
4868
|
+
|
|
4869
|
+
.dsh-cc-disclose {
|
|
4870
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
4871
|
+
width: 100%; height: 28px; padding: 0 4px 0 8px;
|
|
4872
|
+
border: none; border-radius: var(--dsw-radius-sm, 8px);
|
|
4873
|
+
background: transparent;
|
|
4874
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72));
|
|
4875
|
+
font-size: 13px; line-height: 20px; font-weight: 400;
|
|
4876
|
+
cursor: pointer;
|
|
4413
4877
|
}
|
|
4414
|
-
.dsh-
|
|
4415
|
-
.dsh-
|
|
4416
|
-
|
|
4417
|
-
.dsh-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
color: var(--dsw-alias-label-primary);
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
outline: none; font-family: inherit; resize: vertical;
|
|
4878
|
+
.dsh-cc-disclose:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); }
|
|
4879
|
+
.dsh-cc-caret { color: var(--dsw-alias-label-caption, rgba(232, 238, 250, 0.45)); font-size: 12px; }
|
|
4880
|
+
|
|
4881
|
+
.dsh-cc-compact-btn {
|
|
4882
|
+
width: 100%; height: 24px;
|
|
4883
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
4884
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
4885
|
+
background: transparent;
|
|
4886
|
+
color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9));
|
|
4887
|
+
font-size: 12px; line-height: 20px;
|
|
4888
|
+
cursor: pointer;
|
|
4889
|
+
transition: background 120ms ease;
|
|
4427
4890
|
}
|
|
4428
|
-
.dsh-
|
|
4429
|
-
.dsh-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
border
|
|
4891
|
+
.dsh-cc-compact-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); }
|
|
4892
|
+
.dsh-cc-compact-btn[disabled] { opacity: 0.5; cursor: default; }
|
|
4893
|
+
|
|
4894
|
+
.dsh-cc-fields { margin-top: 4px; }
|
|
4895
|
+
.dsh-cc-btnrow { display: flex; gap: 6px; margin-top: 4px; }
|
|
4896
|
+
.dsh-cc-save {
|
|
4897
|
+
flex: 1; height: 24px;
|
|
4898
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
4899
|
+
border-radius: var(--dsw-radius-sm, 8px);
|
|
4900
|
+
background: transparent;
|
|
4901
|
+
color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9));
|
|
4902
|
+
font-size: 12px; line-height: 20px;
|
|
4436
4903
|
cursor: pointer;
|
|
4904
|
+
transition: background 120ms ease;
|
|
4437
4905
|
}
|
|
4438
|
-
.dsh-
|
|
4439
|
-
.dsh-
|
|
4440
|
-
|
|
4441
|
-
.dsh-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4906
|
+
.dsh-cc-save:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); }
|
|
4907
|
+
.dsh-cc-save[disabled] { opacity: 0.5; cursor: default; }
|
|
4908
|
+
/* 恢复默认:官方菜单行样式(ghost 行,hover 背景无描边) */
|
|
4909
|
+
.dsh-cc-reset {
|
|
4910
|
+
flex: 1; height: 24px; padding: 0 4px;
|
|
4911
|
+
border: none; border-radius: var(--dsw-radius-sm, 8px);
|
|
4912
|
+
background: transparent;
|
|
4913
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72));
|
|
4914
|
+
font-size: 12px; line-height: 20px; font-weight: 400;
|
|
4915
|
+
cursor: pointer;
|
|
4446
4916
|
}
|
|
4447
|
-
.dsh-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
.dsh-
|
|
4451
|
-
|
|
4917
|
+
.dsh-cc-reset:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); }
|
|
4918
|
+
.dsh-cc-reset[disabled] { opacity: 0.5; cursor: default; }
|
|
4919
|
+
|
|
4920
|
+
.dsh-cc-field { display: flex; align-items: center; gap: 8px; padding: 1px 0; font-size: 12px; line-height: 20px; }
|
|
4921
|
+
.dsh-cc-field-label { flex: none; width: 56px; color: var(--dsw-alias-label-tertiary, rgba(232, 238, 250, 0.55)); }
|
|
4922
|
+
.dsh-cc-field-value { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, rgba(232, 238, 250, 0.9)); min-width: 30px; text-align: right; }
|
|
4923
|
+
.dsh-cc-slider { flex: 1; height: 2px; accent-color: var(--dsw-static-blue-450, #5b8def); cursor: pointer; }
|
|
4924
|
+
.dsh-cc-check { accent-color: var(--dsw-static-blue-450, #5b8def); width: 13px; height: 13px; cursor: pointer; }
|
|
4925
|
+
.dsh-cc-hint { font-size: 11px; line-height: 16px; color: rgb(234, 179, 8); margin: 2px 0 4px; display: none; }
|
|
4926
|
+
.dsh-cc-hint-show { display: block; }
|
|
4927
|
+
|
|
4928
|
+
.dsh-cc-flash { margin-top: 6px; font-size: 11px; line-height: 16px; border-radius: var(--dsw-radius-sm, 8px); padding: 4px 8px; display: none; }
|
|
4929
|
+
.dsh-cc-flash-ok { display: block; background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08)); color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72)); }
|
|
4930
|
+
.dsh-cc-flash-err { display: block; background: rgba(239, 68, 68, 0.12); color: #fca5a5; }
|
|
4452
4931
|
`
|
|
4453
4932
|
|
|
4454
|
-
|
|
4455
|
-
if (typeof document === 'undefined') return
|
|
4456
|
-
if (document.getElementById(STYLE_ID)) return
|
|
4457
|
-
const style = document.createElement('style')
|
|
4458
|
-
style.id = STYLE_ID
|
|
4459
|
-
style.textContent = CSS
|
|
4460
|
-
document.head.appendChild(style)
|
|
4461
|
-
}
|
|
4462
|
-
|
|
4463
|
-
async function api(path, body) {
|
|
4464
|
-
try {
|
|
4465
|
-
const res = await fetch(`${ENDPOINT}${path}`, body
|
|
4466
|
-
? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }
|
|
4467
|
-
: { cache: 'no-store' })
|
|
4468
|
-
return res.ok ? res.json() : null
|
|
4469
|
-
} catch { return null }
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
|
-
/** 异常留痕:走 prompts /diag(0.4.0 起套件统一通道,落 superinput-debug.log)。 */
|
|
4473
|
-
function report(message) {
|
|
4474
|
-
try {
|
|
4475
|
-
void fetch('/api/dsh-superinput/prompts/diag', {
|
|
4476
|
-
method: 'POST',
|
|
4477
|
-
headers: { 'content-type': 'application/json' },
|
|
4478
|
-
body: JSON.stringify({ message: `prompts: ${String(message).slice(0, 300)}` }),
|
|
4479
|
-
}).catch(() => {})
|
|
4480
|
-
} catch { /* ignore */ }
|
|
4481
|
-
}
|
|
4933
|
+
const STYLE_ID = 'dsh-compaction-control-style'
|
|
4482
4934
|
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
if (!found.includes(match[1])) found.push(match[1])
|
|
4935
|
+
function ensureStyle() {
|
|
4936
|
+
if (document.getElementById(STYLE_ID)) return
|
|
4937
|
+
const style = document.createElement('style')
|
|
4938
|
+
style.id = STYLE_ID
|
|
4939
|
+
style.textContent = CSS
|
|
4940
|
+
document.head.appendChild(style)
|
|
4490
4941
|
}
|
|
4491
|
-
return found
|
|
4492
|
-
}
|
|
4493
4942
|
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4943
|
+
const report = (phase, extra) => {
|
|
4944
|
+
try {
|
|
4945
|
+
fetch(`${ENDPOINT}/diag`, {
|
|
4946
|
+
method: 'POST',
|
|
4947
|
+
headers: { 'content-type': 'application/json' },
|
|
4948
|
+
body: JSON.stringify({ line: `${phase} ${JSON.stringify(extra ?? {})}`.slice(0, 1800) }),
|
|
4949
|
+
}).catch(() => {})
|
|
4950
|
+
} catch { /* ignore */ }
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
/** 与内置 ContextMeter 同款紧凑数字(K / M)。 */
|
|
4954
|
+
function formatTokens(value) {
|
|
4955
|
+
if (value < 1000) return String(value)
|
|
4956
|
+
const scale = (n) => (n >= 100 ? String(Math.round(n)) : String(Math.round(n * 10) / 10))
|
|
4957
|
+
if (value < 1000000) return `${scale(value / 1000)}K`
|
|
4958
|
+
return `${scale(value / 1000000)}M`
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
/** contextPressure → 占用(与 ui-conversation 的 contextOccupancy 同算法)。
|
|
4962
|
+
* 用 || 而非 ??:projectedTokens 可为合法 0(未投影出增量),此时必须
|
|
4963
|
+
* 回落 pressureTokens——`??` 会把 0 当有效值挡住回落,真机出现过官方
|
|
4964
|
+
* 17% 而本环 0% 的空圈。 */
|
|
4965
|
+
function occupancy(pressure) {
|
|
4966
|
+
const used = pressure?.projectedTokens || pressure?.pressureTokens
|
|
4967
|
+
if (used === undefined || pressure?.contextWindow === undefined) return null
|
|
4968
|
+
return {
|
|
4969
|
+
percent: Math.min(100, Math.round((used / pressure.contextWindow) * 100)),
|
|
4970
|
+
usedTokens: used,
|
|
4971
|
+
contextWindow: pressure.contextWindow,
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
/**
|
|
4976
|
+
* 官方占用环按钮的识别特征(0.2.2 收严):dialog 按钮 + svg 带圆 +
|
|
4977
|
+
* 文本恰为「NN%」。0.2.1 的「全文 dialog + 双 circle」宽泛特征在真机
|
|
4978
|
+
* 命中了 dock 里一个未知按钮(correct-trace mode:"meter" 坐标 965,796),
|
|
4979
|
+
* 宽泛特征必然误伤,文本白名单把爆炸半径收到最小;若官方环文本形态
|
|
4980
|
+
* 不同则不隐藏(无害降级,官方环照常显示)。
|
|
4981
|
+
*/
|
|
4982
|
+
function findBuiltInMeterButtons() {
|
|
4983
|
+
const found = []
|
|
4984
|
+
for (const btn of document.querySelectorAll('button[aria-haspopup="dialog"]')) {
|
|
4985
|
+
if (btn.classList.contains('dsh-cc-trigger')) continue
|
|
4986
|
+
if (!/^\d+%$/.test((btn.textContent ?? '').trim())) continue
|
|
4987
|
+
const svg = btn.querySelector('svg')
|
|
4988
|
+
if (svg && svg.querySelectorAll('circle').length >= 1) found.push(btn)
|
|
4989
|
+
}
|
|
4990
|
+
return found
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
let hideObserver = null
|
|
4994
|
+
let hideRefcount = 0
|
|
4995
|
+
// visibility:hidden 而非 display:none:官方环占位保留,状态行 flex 不
|
|
4996
|
+
// 会把费用/用量按钮挤过来盖住我们的环。
|
|
4997
|
+
let knownMeters = new Set()
|
|
4998
|
+
function hideBuiltInOnce(force) {
|
|
4999
|
+
if (!force && [...knownMeters].every((b) => b.isConnected && b.style.visibility === 'hidden')) return
|
|
5000
|
+
knownMeters = new Set(findBuiltInMeterButtons())
|
|
5001
|
+
for (const btn of knownMeters) btn.style.visibility = 'hidden'
|
|
5002
|
+
}
|
|
5003
|
+
function acquireDockHide() {
|
|
5004
|
+
hideRefcount += 1
|
|
5005
|
+
hideBuiltInOnce(true)
|
|
5006
|
+
if (!hideObserver) {
|
|
5007
|
+
hideObserver = new MutationObserver(() => hideBuiltInOnce(false))
|
|
5008
|
+
hideObserver.observe(document.body, { childList: true, subtree: true })
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
function releaseDockHide() {
|
|
5012
|
+
hideRefcount = Math.max(0, hideRefcount - 1)
|
|
5013
|
+
if (hideRefcount === 0) {
|
|
5014
|
+
if (hideObserver) { hideObserver.disconnect(); hideObserver = null }
|
|
5015
|
+
for (const btn of knownMeters) btn.style.visibility = ''
|
|
5016
|
+
knownMeters = new Set()
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
class QuietBoundary extends React.Component {
|
|
5021
|
+
constructor(props) { super(props); this.state = { error: null } }
|
|
5022
|
+
static getDerivedStateFromError(error) { return { error } }
|
|
5023
|
+
componentDidCatch(error) { report('crash', { message: String(error?.message ?? error) }) }
|
|
5024
|
+
render() { return this.state.error ? null : this.props.children }
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
// 官方命令通道:dock 槽位组件的标准 props 带 inputActions(followup
|
|
5028
|
+
// 同款)。「立即压缩」优先把 /compact 落进草稿再 submit——命令在
|
|
5029
|
+
// agent 宿主进程执行,不受本进程 webServer 无 agents 服务的影响。
|
|
5030
|
+
let commandBridge = null
|
|
5031
|
+
|
|
5032
|
+
function CommandBridge(props) {
|
|
5033
|
+
useEffect(() => {
|
|
5034
|
+
commandBridge = props.inputActions ?? null
|
|
5035
|
+
return () => { if (commandBridge === props.inputActions) commandBridge = null }
|
|
5036
|
+
}, [props.inputActions])
|
|
5037
|
+
return null
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
/** 走官方 /compact 命令(草稿→提交),返回是否已受理。 */
|
|
5041
|
+
const compactViaCommand = () => {
|
|
5042
|
+
if (!commandBridge) return false
|
|
5043
|
+
try {
|
|
5044
|
+
commandBridge.setDraft('/compact')
|
|
5045
|
+
window.setTimeout(() => {
|
|
5046
|
+
try { commandBridge.submit() } catch (err) { report('submit-error', { message: String(err?.message ?? err) }) }
|
|
5047
|
+
}, 250)
|
|
5048
|
+
return true
|
|
5049
|
+
} catch (err) {
|
|
5050
|
+
report('bridge-error', { message: String(err?.message ?? err) })
|
|
5051
|
+
return false
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
function RingSvg({ percent }) {
|
|
5056
|
+
return h('svg', { viewBox: '0 0 14 14', width: RING_SIZE, height: RING_SIZE, 'aria-hidden': true },
|
|
5057
|
+
h('circle', { className: 'dsh-cc-ring-track', cx: 7, cy: 7, r: RADIUS }),
|
|
5058
|
+
h('circle', {
|
|
5059
|
+
className: 'dsh-cc-ring-fill',
|
|
5060
|
+
cx: 7, cy: 7, r: RADIUS,
|
|
5061
|
+
strokeDasharray: `${(CIRCUMFERENCE * percent) / 100} ${CIRCUMFERENCE}`,
|
|
5062
|
+
transform: 'rotate(-90 7 7)',
|
|
5063
|
+
}))
|
|
5064
|
+
}
|
|
5065
|
+
|
|
5066
|
+
function Seat(props) {
|
|
5067
|
+
const useProjection = props.useProjection
|
|
5068
|
+
const sessionId = props.sessionId
|
|
5069
|
+
const pressure = useProjection('contextPressure')
|
|
5070
|
+
const breakdown = useProjection('contextBreakdown')
|
|
5071
|
+
|
|
5072
|
+
const [open, setOpen] = useState(false)
|
|
5073
|
+
const [pos, setPos] = useState(null)
|
|
5074
|
+
const [settings, setSettings] = useState(null)
|
|
5075
|
+
const [draft, setDraft] = useState(null)
|
|
5076
|
+
const [busy, setBusy] = useState(false)
|
|
5077
|
+
const [saving, setSaving] = useState(false)
|
|
5078
|
+
const [resetting, setResetting] = useState(false)
|
|
5079
|
+
const [expanded, setExpanded] = useState(false)
|
|
5080
|
+
const [flash, setFlash] = useState(null)
|
|
5081
|
+
|
|
5082
|
+
const rootRef = useRef(null)
|
|
5083
|
+
const panelRef = useRef(null)
|
|
5084
|
+
|
|
5085
|
+
// 圆环常驻:新会话/空会话还没有 contextPressure 时用 0% 占位,
|
|
5086
|
+
// 避免「模型选择器左侧的环莫名消失」;有数据后自动切回真实占用。
|
|
5087
|
+
const measured = occupancy(pressure)
|
|
5088
|
+
const context = measured ?? { percent: 0, usedTokens: 0, contextWindow: undefined }
|
|
5089
|
+
|
|
5090
|
+
// 挂载取证:0.2.5 起圆环是 conversation.input.right 槽位的直接内容,
|
|
5091
|
+
// 位置由官方 InputBar JSX 兄弟顺序保证(紧在模型选择器 renderSlot
|
|
5092
|
+
// 之前),不再有任何坐标手术;这条 trace 只为确认槽位真的渲染了。
|
|
5093
|
+
const mountReported = useRef(false)
|
|
5094
|
+
useEffect(() => {
|
|
5095
|
+
if (mountReported.current || context === null) return
|
|
5096
|
+
mountReported.current = true
|
|
5097
|
+
const rect = rootRef.current?.getBoundingClientRect()
|
|
5098
|
+
report('in-slot-mount', rect
|
|
5099
|
+
? { left: Math.round(rect.left), top: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height) }
|
|
5100
|
+
: {})
|
|
5101
|
+
})
|
|
5102
|
+
|
|
5103
|
+
// 挂载即拉一次服务端状态:hideBuiltIn 要在用户打开面板前就生效。
|
|
5104
|
+
useEffect(() => {
|
|
5105
|
+
let alive = true
|
|
5106
|
+
fetch(`${ENDPOINT}/state`)
|
|
5107
|
+
.then((res) => (res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))))
|
|
5108
|
+
.then((data) => {
|
|
5109
|
+
if (!alive || !data?.state) return
|
|
5110
|
+
setSettings(data)
|
|
5111
|
+
setDraft({
|
|
5112
|
+
auto: data.state.auto,
|
|
5113
|
+
thresholdRatio: data.state.thresholdRatio,
|
|
5114
|
+
retainRatio: data.state.retainRatio,
|
|
5115
|
+
hideBuiltIn: data.state.hideBuiltIn,
|
|
5116
|
+
checkpoint: data.state.checkpoint,
|
|
5117
|
+
})
|
|
5118
|
+
})
|
|
5119
|
+
.catch((err) => report('state-error', { message: String(err?.message ?? err) }))
|
|
5120
|
+
return () => { alive = false }
|
|
5121
|
+
}, [])
|
|
5122
|
+
|
|
5123
|
+
// 压缩前自动落档布防:占用率上穿「阈值-5%」的升沿 POST 一次 arm=true,
|
|
5124
|
+
// 回落(压缩完成/新会话)时 arm=false。服务端在下一次 pre-step 注入
|
|
5125
|
+
// 落档指令,agent 自己把项目状态写进工作区 checkpoint.md。
|
|
5126
|
+
const armRef = useRef(false)
|
|
5127
|
+
useEffect(() => {
|
|
5128
|
+
if (context === null || !sessionId) return
|
|
5129
|
+
const thresholdPct = Math.round((draft?.thresholdRatio ?? 0.8) * 100)
|
|
5130
|
+
const above = context.percent >= thresholdPct - 5
|
|
5131
|
+
if (above === armRef.current) return
|
|
5132
|
+
armRef.current = above
|
|
5133
|
+
void fetch(`${ENDPOINT}/checkpoint-arm`, {
|
|
5134
|
+
method: 'POST',
|
|
5135
|
+
headers: { 'content-type': 'application/json' },
|
|
5136
|
+
body: JSON.stringify({ sessionId: String(sessionId), arm: above }),
|
|
5137
|
+
}).then((res) => (res.ok ? res.json() : null))
|
|
5138
|
+
.then((data) => { if (data && data.armed) report('checkpoint-armed', { sessionId, percent: context.percent }) })
|
|
5139
|
+
.catch((err) => report('checkpoint-arm-error', { message: String(err?.message ?? err) }))
|
|
5140
|
+
}, [context?.percent, sessionId, draft?.thresholdRatio])
|
|
5141
|
+
|
|
5142
|
+
// 隐藏官方环的生命周期跟 draft.hideBuiltIn 走(面板里可切换)。
|
|
5143
|
+
// 未加载(undefined)也按默认隐藏——任何加载失败场景都不出现双环。
|
|
5144
|
+
useEffect(() => {
|
|
5145
|
+
if (draft?.hideBuiltIn === false) return undefined
|
|
5146
|
+
acquireDockHide()
|
|
5147
|
+
return () => releaseDockHide()
|
|
5148
|
+
}, [draft?.hideBuiltIn])
|
|
5149
|
+
|
|
5150
|
+
// 面板定位 + 外点关闭 + Esc。默认向上展开(translateY(-100%));
|
|
5151
|
+
// 上方空间不足 460px 时才向下。外点关闭 + Esc。
|
|
5152
|
+
useEffect(() => {
|
|
5153
|
+
if (!open) return undefined
|
|
5154
|
+
const rect = rootRef.current?.getBoundingClientRect()
|
|
5155
|
+
if (rect) {
|
|
5156
|
+
const left = Math.max(8, Math.min(rect.right - PANEL_WIDTH, window.innerWidth - PANEL_WIDTH - 8))
|
|
5157
|
+
const openUp = rect.top > 460
|
|
5158
|
+
setPos({
|
|
5159
|
+
left,
|
|
5160
|
+
top: openUp ? rect.top - 8 : rect.bottom + 8,
|
|
5161
|
+
transform: openUp ? 'translateY(-100%)' : 'none',
|
|
5162
|
+
})
|
|
5163
|
+
}
|
|
5164
|
+
const onPointerDown = (e) => {
|
|
5165
|
+
const t = e.target
|
|
5166
|
+
if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return
|
|
5167
|
+
setOpen(false)
|
|
5168
|
+
}
|
|
5169
|
+
const onKeyDown = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
5170
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
5171
|
+
document.addEventListener('keydown', onKeyDown)
|
|
5172
|
+
return () => {
|
|
5173
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
5174
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
5175
|
+
}
|
|
5176
|
+
}, [open])
|
|
5177
|
+
|
|
5178
|
+
// flash 3.5s 自动消散。
|
|
5179
|
+
useEffect(() => {
|
|
5180
|
+
if (!flash) return undefined
|
|
5181
|
+
const timer = setTimeout(() => setFlash(null), 3500)
|
|
5182
|
+
return () => clearTimeout(timer)
|
|
5183
|
+
}, [flash])
|
|
5184
|
+
|
|
5185
|
+
// 本环永远渲染:0.2.1 的「hideBuiltIn=false 时退场」门已拆——持久化
|
|
5186
|
+
// 值恰为 false 曾把环卸成空壳占位。该开关只管官方环的隐藏。
|
|
5187
|
+
if (createPortal === null) return null
|
|
5188
|
+
|
|
5189
|
+
const breakdownTotal = breakdown === undefined
|
|
5190
|
+
? 0
|
|
5191
|
+
: breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens
|
|
5192
|
+
const rows = [
|
|
5193
|
+
{ key: 'systemTokens', label: '系统提示', cls: 'dsh-cc-tint-system', value: breakdown?.systemTokens },
|
|
5194
|
+
{ key: 'toolsTokens', label: '工具定义', cls: 'dsh-cc-tint-tools', value: breakdown?.toolsTokens },
|
|
5195
|
+
{ key: 'messageTokens', label: '对话历史', cls: 'dsh-cc-tint-messages', value: breakdown?.messageTokens },
|
|
5196
|
+
]
|
|
5197
|
+
const segments = breakdownTotal > 0
|
|
5198
|
+
? rows
|
|
5199
|
+
.map((row) => ({ ...row, width: (context.percent * row.value) / breakdownTotal }))
|
|
5200
|
+
.filter((row) => row.width > 0)
|
|
5201
|
+
: [{ key: 'total', cls: '', width: context.percent }]
|
|
5202
|
+
|
|
5203
|
+
const onCompact = async () => {
|
|
5204
|
+
if (busy) return
|
|
5205
|
+
// 优先官方命令通道:结果以官方 chip 形式出现在对话流里。
|
|
5206
|
+
if (compactViaCommand()) {
|
|
5207
|
+
setFlash({ kind: 'ok', text: '已执行 /compact,结果见对话流' })
|
|
5208
|
+
return
|
|
5209
|
+
}
|
|
5210
|
+
setBusy(true)
|
|
5211
|
+
setFlash(null)
|
|
5212
|
+
try {
|
|
5213
|
+
const res = await fetch(`${ENDPOINT}/compact`, {
|
|
5214
|
+
method: 'POST',
|
|
5215
|
+
headers: { 'content-type': 'application/json' },
|
|
5216
|
+
body: JSON.stringify({ sessionId: String(sessionId) }),
|
|
5217
|
+
})
|
|
5218
|
+
const data = await res.json().catch(() => ({}))
|
|
5219
|
+
if (!res.ok || !data.ok) {
|
|
5220
|
+
setFlash({ kind: 'err', text: data?.error ?? `压缩失败(HTTP ${res.status})` })
|
|
5221
|
+
} else if (data.empty) {
|
|
5222
|
+
setFlash({ kind: 'ok', text: data.message ?? '暂无可压缩的历史' })
|
|
5223
|
+
} else {
|
|
5224
|
+
setFlash({ kind: 'ok', text: `已压缩 ${data.shadowed} 条历史(约 ${formatTokens(data.tokens ?? 0)} tokens)` })
|
|
5225
|
+
}
|
|
5226
|
+
} catch (err) {
|
|
5227
|
+
setFlash({ kind: 'err', text: `请求失败:${String(err?.message ?? err)}` })
|
|
5228
|
+
} finally {
|
|
5229
|
+
setBusy(false)
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
const onSave = async () => {
|
|
5234
|
+
if (saving || !draft) return
|
|
5235
|
+
if (draft.retainRatio >= draft.thresholdRatio) {
|
|
5236
|
+
setFlash({ kind: 'err', text: '保留比例必须小于触发阈值' })
|
|
5237
|
+
return
|
|
5238
|
+
}
|
|
5239
|
+
setSaving(true)
|
|
5240
|
+
setFlash(null)
|
|
5241
|
+
try {
|
|
5242
|
+
const res = await fetch(`${ENDPOINT}/state`, {
|
|
5243
|
+
method: 'POST',
|
|
5244
|
+
headers: { 'content-type': 'application/json' },
|
|
5245
|
+
body: JSON.stringify(draft),
|
|
5246
|
+
})
|
|
5247
|
+
const data = await res.json().catch(() => ({}))
|
|
5248
|
+
if (!res.ok || !data.ok) {
|
|
5249
|
+
setFlash({ kind: 'err', text: data?.error ?? `保存失败(HTTP ${res.status})` })
|
|
5250
|
+
} else {
|
|
5251
|
+
setSettings({ ...settings, state: data.state, profiles: data.profiles })
|
|
5252
|
+
setFlash({ kind: 'ok', text: '已保存,重启 dsh 后生效' })
|
|
5253
|
+
}
|
|
5254
|
+
} catch (err) {
|
|
5255
|
+
setFlash({ kind: 'err', text: `请求失败:${String(err?.message ?? err)}` })
|
|
5256
|
+
} finally {
|
|
5257
|
+
setSaving(false)
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
const onReset = async () => {
|
|
5262
|
+
if (resetting) return
|
|
5263
|
+
setResetting(true)
|
|
5264
|
+
setFlash(null)
|
|
5265
|
+
try {
|
|
5266
|
+
const res = await fetch(`${ENDPOINT}/reset`, { method: 'POST', headers: { 'content-type': 'application/json' } })
|
|
5267
|
+
const data = await res.json().catch(() => ({}))
|
|
5268
|
+
if (!res.ok || !data.ok) {
|
|
5269
|
+
setFlash({ kind: 'err', text: data?.error ?? `恢复失败(HTTP ${res.status})` })
|
|
5270
|
+
} else {
|
|
5271
|
+
setSettings({ ...settings, state: data.state, profiles: data.profiles })
|
|
5272
|
+
setDraft({
|
|
5273
|
+
auto: data.state.auto,
|
|
5274
|
+
thresholdRatio: data.state.thresholdRatio,
|
|
5275
|
+
retainRatio: data.state.retainRatio,
|
|
5276
|
+
hideBuiltIn: data.state.hideBuiltIn,
|
|
5277
|
+
})
|
|
5278
|
+
setExpanded(false)
|
|
5279
|
+
setFlash({ kind: 'ok', text: '已恢复默认,重启 dsh 后生效' })
|
|
5280
|
+
}
|
|
5281
|
+
} catch (err) {
|
|
5282
|
+
setFlash({ kind: 'err', text: `请求失败:${String(err?.message ?? err)}` })
|
|
5283
|
+
} finally {
|
|
5284
|
+
setResetting(false)
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
|
|
5288
|
+
const panel = h('div', {
|
|
5289
|
+
ref: panelRef,
|
|
5290
|
+
className: 'dsh-cc-panel',
|
|
5291
|
+
role: 'dialog',
|
|
5292
|
+
'aria-label': '上下文压缩控制',
|
|
5293
|
+
style: pos ? { left: pos.left, top: pos.top, transform: pos.transform } : { visibility: 'hidden', left: 0, top: 0 },
|
|
5294
|
+
},
|
|
5295
|
+
h('div', { className: 'dsh-cc-head' },
|
|
5296
|
+
h('span', { className: 'dsh-cc-head-label' }, '上下文已用'),
|
|
5297
|
+
h('span', { className: 'dsh-cc-head-pct' }, `${context.percent}%`),
|
|
5298
|
+
h('span', { className: 'dsh-cc-head-fig' },
|
|
5299
|
+
measured ? `~${formatTokens(context.usedTokens)} / ${formatTokens(context.contextWindow)}` : '当前会话暂无上下文数据')),
|
|
5300
|
+
h('div', { className: 'dsh-cc-bar' },
|
|
5301
|
+
segments.map((seg) => h('div', { key: seg.key, className: `dsh-cc-bar-seg ${seg.cls}`, style: { width: `${seg.width}%` } }))),
|
|
5302
|
+
breakdownTotal > 0
|
|
5303
|
+
? h('dl', { className: 'dsh-cc-rows' },
|
|
5304
|
+
rows.map((row) => h('div', { key: row.key, className: 'dsh-cc-row' },
|
|
5305
|
+
h('dt', null,
|
|
5306
|
+
h('span', { className: `dsh-cc-dot ${row.cls}`, 'aria-hidden': true }),
|
|
5307
|
+
row.label),
|
|
5308
|
+
h('dd', null, `~${formatTokens(row.value ?? 0)}`))))
|
|
5309
|
+
: null,
|
|
5310
|
+
|
|
5311
|
+
h('div', { className: 'dsh-cc-section' },
|
|
5312
|
+
h('button', { type: 'button', className: 'dsh-cc-compact-btn', disabled: busy, onClick: onCompact },
|
|
5313
|
+
busy ? '正在压缩…' : '立即压缩当前会话')),
|
|
5314
|
+
|
|
5315
|
+
h('div', { className: 'dsh-cc-section' },
|
|
5316
|
+
h('button', {
|
|
5317
|
+
type: 'button',
|
|
5318
|
+
className: 'dsh-cc-disclose',
|
|
5319
|
+
'aria-expanded': expanded,
|
|
5320
|
+
onClick: () => setExpanded(!expanded),
|
|
5321
|
+
},
|
|
5322
|
+
h('span', null, '自定义压缩'),
|
|
5323
|
+
h('span', { className: 'dsh-cc-caret' }, expanded ? '▾' : '▸')),
|
|
5324
|
+
expanded ? h('div', { className: 'dsh-cc-fields' },
|
|
5325
|
+
h('div', { className: 'dsh-cc-field' },
|
|
5326
|
+
h('span', { className: 'dsh-cc-field-label' }, '触发阈值'),
|
|
5327
|
+
h('input', {
|
|
5328
|
+
type: 'range', className: 'dsh-cc-slider', min: 50, max: 95, step: 1,
|
|
5329
|
+
value: Math.round((draft?.thresholdRatio ?? 0.8) * 100),
|
|
5330
|
+
onChange: (e) => setDraft({ ...draft, thresholdRatio: Number(e.target.value) / 100 }),
|
|
5331
|
+
}),
|
|
5332
|
+
h('span', { className: 'dsh-cc-field-value' }, `${Math.round((draft?.thresholdRatio ?? 0.8) * 100)}%`)),
|
|
5333
|
+
h('div', { className: 'dsh-cc-field' },
|
|
5334
|
+
h('span', { className: 'dsh-cc-field-label' }, '保留比例'),
|
|
5335
|
+
h('input', {
|
|
5336
|
+
type: 'range', className: 'dsh-cc-slider', min: 4, max: 40, step: 1,
|
|
5337
|
+
value: Math.round((draft?.retainRatio ?? 0.16) * 100),
|
|
5338
|
+
onChange: (e) => setDraft({ ...draft, retainRatio: Number(e.target.value) / 100 }),
|
|
5339
|
+
}),
|
|
5340
|
+
h('span', { className: 'dsh-cc-field-value' }, `${Math.round((draft?.retainRatio ?? 0.16) * 100)}%`)),
|
|
5341
|
+
h('div', { className: `dsh-cc-hint ${(draft?.retainRatio ?? 0) >= (draft?.thresholdRatio ?? 1) ? 'dsh-cc-hint-show' : ''}` },
|
|
5342
|
+
'保留比例需小于触发阈值'),
|
|
5343
|
+
h('div', { className: 'dsh-cc-field' },
|
|
5344
|
+
h('span', { className: 'dsh-cc-field-label' }, '官方圆环'),
|
|
5345
|
+
h('input', {
|
|
5346
|
+
type: 'checkbox', className: 'dsh-cc-check',
|
|
5347
|
+
checked: draft?.hideBuiltIn ?? true,
|
|
5348
|
+
onChange: (e) => setDraft({ ...draft, hideBuiltIn: e.target.checked }),
|
|
5349
|
+
}),
|
|
5350
|
+
h('span', { className: 'dsh-cc-field-value' }, draft?.hideBuiltIn === false ? '显示' : '已隐藏')),
|
|
5351
|
+
h('div', { className: 'dsh-cc-field' },
|
|
5352
|
+
h('span', { className: 'dsh-cc-field-label' }, '压缩前落档'),
|
|
5353
|
+
h('input', {
|
|
5354
|
+
type: 'checkbox', className: 'dsh-cc-check',
|
|
5355
|
+
checked: draft?.checkpoint ?? true,
|
|
5356
|
+
onChange: (e) => setDraft({ ...draft, checkpoint: e.target.checked }),
|
|
5357
|
+
}),
|
|
5358
|
+
h('span', { className: 'dsh-cc-field-value' }, draft?.checkpoint === false ? '关闭' : '写 checkpoint.md')),
|
|
5359
|
+
h('div', { className: 'dsh-cc-btnrow' },
|
|
5360
|
+
h('button', { type: 'button', className: 'dsh-cc-save', disabled: saving || !draft, onClick: onSave },
|
|
5361
|
+
saving ? '保存中…' : '保存设置'),
|
|
5362
|
+
h('button', { type: 'button', className: 'dsh-cc-reset', disabled: resetting, onClick: onReset },
|
|
5363
|
+
resetting ? '恢复中…' : '恢复默认')))
|
|
5364
|
+
: null),
|
|
5365
|
+
|
|
5366
|
+
flash ? h('div', { className: `dsh-cc-flash dsh-cc-flash-${flash.kind}` }, flash.text) : null)
|
|
5367
|
+
|
|
5368
|
+
return h('span', { ref: rootRef, className: measured ? 'dsh-cc-root' : 'dsh-cc-root dsh-cc-pending' },
|
|
5369
|
+
h('button', {
|
|
5370
|
+
type: 'button',
|
|
5371
|
+
className: 'dsh-cc-trigger',
|
|
5372
|
+
'aria-label': measured ? `上下文已用 ${context.percent}%,打开压缩控制` : '上下文数据加载中,打开压缩控制',
|
|
5373
|
+
'aria-haspopup': 'dialog',
|
|
5374
|
+
'aria-expanded': open,
|
|
5375
|
+
title: measured ? `上下文已用 ${context.percent}%` : '当前会话暂无上下文数据',
|
|
5376
|
+
onClick: () => setOpen(!open),
|
|
5377
|
+
},
|
|
5378
|
+
h(RingSvg, { percent: context.percent })),
|
|
5379
|
+
open ? createPortal(panel, document.body) : null)
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
return {
|
|
5383
|
+
inject: ['slots'],
|
|
5384
|
+
apply(ctx) {
|
|
5385
|
+
ensureStyle()
|
|
5386
|
+
console.info(`[dsh-compaction-control] client ${VERSION} loading`)
|
|
5387
|
+
report('boot', {
|
|
5388
|
+
slots: typeof ctx.slots,
|
|
5389
|
+
portal: createPortal ? 'react-dom' : 'missing',
|
|
5390
|
+
})
|
|
5391
|
+
window.__dshCompactionControl = { version: VERSION }
|
|
5392
|
+
|
|
5393
|
+
ctx.inject(['slots'], (scope) => {
|
|
5394
|
+
const gate = Promise.race([
|
|
5395
|
+
fetch(`${ENDPOINT}/enabled`)
|
|
5396
|
+
.then((res) => (res.ok ? res.json() : { enabled: true }))
|
|
5397
|
+
.catch(() => ({ enabled: true })),
|
|
5398
|
+
new Promise((resolve) => setTimeout(() => resolve({ enabled: true }), 2500)),
|
|
5399
|
+
])
|
|
5400
|
+
|
|
5401
|
+
gate.then(({ enabled }) => {
|
|
5402
|
+
if (enabled === false) {
|
|
5403
|
+
report('skip-disabled', {})
|
|
5404
|
+
return
|
|
5405
|
+
}
|
|
5406
|
+
// list 槽位:order 越小越靠前,0 让圆环落在模型选择器左边第一位。
|
|
5407
|
+
scope.slots.inject('conversation.input.right', () => scope.slots.register({
|
|
5408
|
+
name: 'conversation.input.right',
|
|
5409
|
+
id: 'dsh-compaction-control',
|
|
5410
|
+
order: 0,
|
|
5411
|
+
}, (props) => h(QuietBoundary, null, h(Seat, props))))
|
|
5412
|
+
// 隐形桥接:只为拿 inputActions(setDraft/submit),不渲染任何 UI。
|
|
5413
|
+
scope.slots.inject('conversation.input.dock', () => scope.slots.register({
|
|
5414
|
+
name: 'conversation.input.dock',
|
|
5415
|
+
id: 'dsh-compaction-control-bridge',
|
|
5416
|
+
order: 90,
|
|
5417
|
+
}, (props) => h(QuietBoundary, null, h(CommandBridge, props))))
|
|
5418
|
+
report('register', {})
|
|
5419
|
+
}).catch((err) => report('gate-error', { message: String(err?.message ?? err) }))
|
|
5420
|
+
})
|
|
5421
|
+
},
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
/* dsh-superinput · prompts 模块客户端:模板库入口 + 面板。 */
|
|
5426
|
+
function createPromptsClient(require) {
|
|
5427
|
+
const React = require('react')
|
|
5428
|
+
const { createPortal } = require('react-dom')
|
|
5429
|
+
const h = React.createElement
|
|
5430
|
+
|
|
5431
|
+
const STYLE_ID = 'dsh-superinput-prompts-style'
|
|
5432
|
+
const ENDPOINT = '/api/dsh-superinput/prompts'
|
|
5433
|
+
|
|
5434
|
+
/* 划选引用:官方 reference chip + Trae 规格。
|
|
5435
|
+
* CHIP_MARK 是不可见分隔符(U+2063),作为标签前缀用于 CSS 精确选中
|
|
5436
|
+
* 「我们插入的 chip」而不波及官方 @file chip;视觉与文本内容无影响。 */
|
|
5437
|
+
const SELECTION_SOURCE = 'superinput-selection'
|
|
5438
|
+
const SELECTION_RULE = 'dsh-superinput-selection'
|
|
5439
|
+
const CHIP_MARK = '\u2063'
|
|
5440
|
+
const CONTEXT_MAX = 8
|
|
5441
|
+
const CONTEXT_TEXT_MAX = 4000
|
|
5442
|
+
const CONTEXT_COMMENT_MAX = 600
|
|
5443
|
+
const STORAGE_KEY = 'dsh-superinput-selection-items'
|
|
5444
|
+
|
|
5445
|
+
const CSS = `
|
|
5446
|
+
.dsh-cp-btn {
|
|
5447
|
+
display: inline-flex;
|
|
5448
|
+
align-items: center;
|
|
5449
|
+
border: none !important;
|
|
5450
|
+
background: transparent !important;
|
|
5451
|
+
box-shadow: none !important;
|
|
5452
|
+
color: var(--dsw-alias-label-secondary, rgba(232, 238, 250, 0.72)) !important;
|
|
5453
|
+
font-size: 12px;
|
|
5454
|
+
line-height: 20px;
|
|
5455
|
+
padding: 1px 8px;
|
|
5456
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
5457
|
+
cursor: default;
|
|
5458
|
+
user-select: none;
|
|
5459
|
+
}
|
|
5460
|
+
.dsh-cp-btn:hover { background: var(--dsw-alias-interactive-bg-hover) !important; color: var(--dsw-alias-label-primary) !important; }
|
|
5461
|
+
.dsh-cp-panel {
|
|
5462
|
+
position: fixed;
|
|
5463
|
+
z-index: 1100;
|
|
5464
|
+
box-sizing: border-box;
|
|
5465
|
+
border: 0;
|
|
5466
|
+
border-radius: var(--dsw-radius-lg, 12px);
|
|
5467
|
+
background: var(--dsw-specific-menu, rgba(67, 69, 74, 0.45));
|
|
5468
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
5469
|
+
-webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
5470
|
+
width: min(380px, 100vw - 24px);
|
|
5471
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.08));
|
|
5472
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px var(--dsw-elevation-stroke-color), 0 3px 8px 0 rgba(0, 0, 0, 0.04), 0 0 20px 0 rgba(0, 0, 0, 0.05));
|
|
5473
|
+
padding: 10px;
|
|
5474
|
+
display: flex;
|
|
5475
|
+
flex-direction: column;
|
|
5476
|
+
gap: 8px;
|
|
5477
|
+
}
|
|
5478
|
+
.dsh-cp-head { display: flex; gap: 6px; align-items: center; }
|
|
5479
|
+
.dsh-cp-search {
|
|
5480
|
+
flex: 1 1 auto; min-width: 0; box-sizing: border-box;
|
|
5481
|
+
padding: 6px 10px; font-size: 12px; line-height: 18px;
|
|
5482
|
+
color: var(--dsw-alias-label-primary);
|
|
5483
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
5484
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
5485
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
5486
|
+
outline: none;
|
|
5487
|
+
}
|
|
5488
|
+
.dsh-cp-search:focus { border-color: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.2)); }
|
|
5489
|
+
.dsh-cp-new {
|
|
5490
|
+
flex: 0 0 auto; width: 26px; height: 26px;
|
|
5491
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
5492
|
+
font-size: 16px; line-height: 1;
|
|
5493
|
+
color: var(--dsw-alias-label-secondary);
|
|
5494
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
5495
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
5496
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
5497
|
+
cursor: pointer;
|
|
5498
|
+
}
|
|
5499
|
+
.dsh-cp-new:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
5500
|
+
.dsh-cp-list { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }
|
|
5501
|
+
.dsh-cp-group { font-size: 11px; line-height: 18px; color: var(--dsw-alias-label-tertiary); padding: 4px 6px 2px; }
|
|
5502
|
+
.dsh-cp-row { display: flex; align-items: flex-start; gap: 6px; padding: 6px; border-radius: var(--dsw-radius-sm, 6px); }
|
|
5503
|
+
.dsh-cp-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
5504
|
+
.dsh-cp-row-main { flex: 1 1 auto; min-width: 0; cursor: pointer; display: flex; flex-direction: column; gap: 2px; }
|
|
5505
|
+
.dsh-cp-title { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5506
|
+
.dsh-cp-uses { color: var(--dsw-alias-label-tertiary); font-size: 10px; margin-left: 4px; }
|
|
5507
|
+
.dsh-cp-preview { font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5508
|
+
.dsh-cp-vars { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
|
|
5509
|
+
.dsh-cp-var { font-size: 10px; line-height: 14px; padding: 0 4px; border-radius: 3px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.08)); }
|
|
5510
|
+
.dsh-cp-row-actions { flex: 0 0 auto; display: flex; gap: 2px; }
|
|
5511
|
+
.dsh-cp-mini {
|
|
5512
|
+
width: 20px; height: 20px; padding: 0;
|
|
5513
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
5514
|
+
font-size: 11px; line-height: 1;
|
|
5515
|
+
color: var(--dsw-alias-label-tertiary);
|
|
5516
|
+
background: transparent; border: none; border-radius: 4px; cursor: pointer;
|
|
5517
|
+
}
|
|
5518
|
+
.dsh-cp-mini:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.08)); }
|
|
5519
|
+
.dsh-cp-mini.danger:hover { color: #f87171; }
|
|
5520
|
+
.dsh-cp-mini.active { color: #3a83f7; }
|
|
5521
|
+
.dsh-cp-empty { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); padding: 12px 6px; text-align: center; }
|
|
5522
|
+
.dsh-cp-editor { display: flex; flex-direction: column; gap: 6px; }
|
|
5523
|
+
.dsh-cp-input, .dsh-cp-textarea {
|
|
5524
|
+
box-sizing: border-box; width: 100%;
|
|
5525
|
+
padding: 6px 10px; font-size: 12px; line-height: 18px;
|
|
5526
|
+
color: var(--dsw-alias-label-primary);
|
|
5527
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
5528
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
5529
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
5530
|
+
outline: none; font-family: inherit; resize: vertical;
|
|
5531
|
+
}
|
|
5532
|
+
.dsh-cp-textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
5533
|
+
.dsh-cp-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
|
5534
|
+
.dsh-cp-action {
|
|
5535
|
+
padding: 4px 12px; font-size: 12px; line-height: 18px;
|
|
5536
|
+
color: var(--dsw-alias-label-secondary);
|
|
5537
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
|
|
5538
|
+
border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
|
|
5539
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
5540
|
+
cursor: pointer;
|
|
5541
|
+
}
|
|
5542
|
+
.dsh-cp-action:hover { color: var(--dsw-alias-label-primary); }
|
|
5543
|
+
.dsh-cp-action.primary { color: #fff; background: #3a83f7; border-color: transparent; }
|
|
5544
|
+
.dsh-cp-action.primary:hover { background: #4f8ef9; }
|
|
5545
|
+
/* 四选项横条:完全官方样式——令牌与几何照抄官方 QuoteSelection 按钮
|
|
5546
|
+
(.U4oBDW_action:floating-fill 底 / 999px 胶囊 / panel 阴影 / xs-13 字), */
|
|
5547
|
+
.dsh-cp-selmenu { display: flex; flex-direction: row; align-items: center; gap: 2px; padding: 3px; border-radius: 999px;
|
|
5548
|
+
background: var(--dsw-alias-button-floating-fill, rgba(116, 126, 148, .24));
|
|
5549
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l3, rgba(255, 255, 255, .1));
|
|
5550
|
+
box-shadow: var(--dsw-elevation-panel, 0 0 0 .5px var(--dsw-elevation-stroke-color), 0 2px 6px rgba(0, 0, 0, .12));
|
|
5551
|
+
}
|
|
5552
|
+
.dsh-cp-selitem { border: none; background: transparent; text-align: center; padding: 5px 10px; font-size: 13px; line-height: 18px;
|
|
5553
|
+
color: var(--dsw-alias-label-primary); border-radius: 999px; cursor: pointer; white-space: nowrap;
|
|
5554
|
+
font-family: var(--dsw-font-family, inherit); letter-spacing: -.02em; }
|
|
5555
|
+
.dsh-cp-selitem:hover, .dsh-cp-selitem:focus-visible { background: var(--dsw-alias-button-floating-hover, rgba(116, 126, 148, .36)); }
|
|
5556
|
+
/* 官方浮动「引用文本」pill 由横条里的同名功能取代(同一 insertQuote API),隐藏避免双浮层 */
|
|
5557
|
+
[class*="U4oBDW_action"] { display: none !important; }
|
|
5558
|
+
.dsh-cp-toast { padding: 5px 12px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary, #fff);
|
|
5559
|
+
background: rgba(50, 50, 54, 0.95); border-radius: 8px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); }
|
|
5560
|
+
/* ---- 划选引用 chip:Trae comment-mention 规格(ai-modules-chat/index.css)----
|
|
5561
|
+
规格:高 22px / 圆角 --dsw-radius-sm(8) / business 浅色底 + business 主色文字 /
|
|
5562
|
+
12px 500 / max-width 240;左图标在 hover 时换成 ×(Trae remove-mention-icon
|
|
5563
|
+
同义)。颜色全部走 dsh 设计令牌,明暗主题各自生效,不再写死 Trae 的灰/蓝。 */
|
|
5564
|
+
[contenteditable] span[title^="${CHIP_MARK}"] {
|
|
5565
|
+
box-sizing: border-box;
|
|
5566
|
+
display: inline-flex;
|
|
5567
|
+
align-items: baseline;
|
|
5568
|
+
gap: 4px;
|
|
5569
|
+
max-width: min(80%, 300px);
|
|
5570
|
+
padding: 0;
|
|
5571
|
+
border: none;
|
|
5572
|
+
border-radius: 0;
|
|
5573
|
+
background: none;
|
|
5574
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
5575
|
+
font-family: var(--dsw-font-family, inherit);
|
|
5576
|
+
font-size: 14px;
|
|
5577
|
+
line-height: 20px;
|
|
5578
|
+
vertical-align: baseline;
|
|
5579
|
+
cursor: pointer;
|
|
5580
|
+
overflow: hidden;
|
|
5581
|
+
transition: background .14s ease;
|
|
5582
|
+
}
|
|
5583
|
+
[contenteditable] span[title^="${CHIP_MARK}"]:hover {
|
|
5584
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
5585
|
+
}
|
|
5586
|
+
/* 隐藏宿主默认前缀(@ / file 图标),换官方同款 ❝ 引用标记(hover 变 ×) */
|
|
5587
|
+
[contenteditable] span[title^="${CHIP_MARK}"] > span:first-child { display: none; }
|
|
5588
|
+
[contenteditable] span[title^="${CHIP_MARK}"]::before {
|
|
5589
|
+
content: '';
|
|
5590
|
+
flex: 0 0 14px;
|
|
5591
|
+
width: 14px;
|
|
5592
|
+
height: 14px;
|
|
5593
|
+
align-self: center;
|
|
5594
|
+
margin-right: 2px;
|
|
5595
|
+
background: currentColor;
|
|
5596
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
5597
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
5598
|
+
}
|
|
5599
|
+
[contenteditable] span[title^="${CHIP_MARK}"]:hover::before {
|
|
5600
|
+
content: '×';
|
|
5601
|
+
background: none;
|
|
5602
|
+
-webkit-mask: none;
|
|
5603
|
+
mask: none;
|
|
5604
|
+
color: var(--dsw-alias-label-primary, currentColor);
|
|
5605
|
+
font-size: 14px;
|
|
5606
|
+
line-height: 14px;
|
|
5607
|
+
text-align: center;
|
|
5608
|
+
}
|
|
5609
|
+
/* 消息正文里的同款 chip(user-text 规则节点;不在 contenteditable 里) */
|
|
5610
|
+
.dsh-si-msgchip { box-sizing: border-box; display: inline-flex; align-items: baseline; gap: 4px; max-width: min(80%, 300px);
|
|
5611
|
+
padding: 0; border: none; border-radius: 0; background: none;
|
|
5612
|
+
color: var(--dsw-alias-state-business-primary, #4176e6);
|
|
5613
|
+
font-family: var(--dsw-font-family, inherit); font-size: 14px; line-height: 20px;
|
|
5614
|
+
vertical-align: baseline; overflow: hidden; user-select: none; }
|
|
5615
|
+
.dsh-si-msgchip.interactive { cursor: pointer; }
|
|
5616
|
+
.dsh-si-msgchip-icon { flex: 0 0 14px; width: 14px; height: 14px; margin-right: 2px; opacity: .8; background: currentColor;
|
|
5617
|
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat;
|
|
5618
|
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M6.6 3.2C4.5 4.3 3 6.3 3 8.6c0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4L6.6 3.2Zm7 0c-2.1 1.1-3.6 3.1-3.6 5.4 0 2.3 1.3 3.7 3.2 3.7 1.6 0 2.7-1 2.7-2.5 0-1.4-.9-2.3-2.2-2.3-.4 0-.7 0-1 .2.3-1 .9-1.8 1.7-2.4l-.8-1.9Z' fill='black'/></svg>") center / 14px 14px no-repeat; }
|
|
5619
|
+
.dsh-si-msgchip-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5620
|
+
/* 详情弹窗:dsh 原生 Dialog 观感(layer-1 表面 / 16 圆角 / 突出投影 / 标准按钮) */
|
|
5621
|
+
.dsh-si-mask { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center;
|
|
5622
|
+
padding: 24px; background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, .24)); }
|
|
5623
|
+
.dsh-si-dialog { box-sizing: border-box; width: min(480px, 100%); max-height: min(600px, 82vh); display: flex; flex-direction: column; gap: 12px;
|
|
5624
|
+
padding: 16px; border-radius: var(--dsw-radius-lg, 16px); color: var(--dsw-alias-label-primary, #e8eefa); overflow-y: auto;
|
|
5625
|
+
font-family: var(--dsw-font-family, inherit);
|
|
5626
|
+
background: var(--dsw-alias-bg-layer-1, #232324);
|
|
5627
|
+
border: .5px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .08));
|
|
5628
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l4, rgba(255, 255, 255, .2));
|
|
5629
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 .5px var(--dsw-elevation-stroke-color), 0 8px 28px rgba(0, 0, 0, .28)); }
|
|
5630
|
+
.dsh-si-dialog-head { display: flex; align-items: center; gap: 8px; }
|
|
5631
|
+
.dsh-si-dialog-title { flex: 1 1 auto; min-width: 0; font-size: 16px; font-weight: 500; line-height: 24px;
|
|
5632
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5633
|
+
.dsh-si-dialog-x { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 0; border-radius: var(--dsw-radius-xs, 4px); cursor: pointer;
|
|
5634
|
+
background: transparent; color: var(--dsw-alias-label-tertiary, #8a9096); font-size: 15px; line-height: 1; }
|
|
5635
|
+
.dsh-si-dialog-x:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
5636
|
+
.dsh-si-dialog-quote { box-sizing: border-box; flex: 1 1 auto; min-height: 40px; max-height: 240px; overflow: auto; padding: 10px 12px; border-radius: var(--dsw-radius-sm, 8px);
|
|
5637
|
+
background: var(--dsw-alias-bg-module-platform, rgba(255, 255, 255, .06));
|
|
5638
|
+
border-left: 2px solid var(--dsw-alias-state-business-primary, #3a83f7);
|
|
5639
|
+
font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
5640
|
+
.dsh-si-dialog-label { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary, #8a9096); }
|
|
5641
|
+
.dsh-si-dialog-detail { font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
5642
|
+
.dsh-si-dialog-comment { box-sizing: border-box; width: 100%; min-height: 64px; max-height: 160px; resize: vertical; padding: 8px 10px;
|
|
5643
|
+
font-size: 14px; line-height: 22px; font-family: inherit; color: var(--dsw-alias-label-primary);
|
|
5644
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, .06)); border: .5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));
|
|
5645
|
+
border-radius: var(--dsw-radius-sm, 8px); outline: none; }
|
|
5646
|
+
.dsh-si-dialog-comment:focus {
|
|
5647
|
+
border-color: var(--dsw-alias-state-business-primary, #3a83f7);
|
|
5648
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #3a83f7) 16%, transparent);
|
|
5649
|
+
}
|
|
5650
|
+
.dsh-si-dialog-foot { display: flex; align-items: center; gap: 8px; }
|
|
5651
|
+
.dsh-si-dialog-spacer { flex: 1 1 auto; }
|
|
5652
|
+
.dsh-si-dialog-btn { height: 30px; padding: 0 14px; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer;
|
|
5653
|
+
color: var(--dsw-alias-label-primary); background: var(--dsw-alias-button-elevated-fill, transparent);
|
|
5654
|
+
border: .5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, .12)); border-radius: var(--dsw-radius-sm, 8px); }
|
|
5655
|
+
.dsh-si-dialog-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
5656
|
+
.dsh-si-dialog-btn.ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }
|
|
5657
|
+
.dsh-si-dialog-btn.ghost:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .08)); }
|
|
5658
|
+
.dsh-si-dialog-btn.primary { color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
5659
|
+
background: var(--dsw-alias-button-primary-fill, #3a83f7); border-color: transparent; }
|
|
5660
|
+
.dsh-si-dialog-btn.primary:hover { background: var(--dsw-alias-button-primary-hover, #4f8ef9); }
|
|
5661
|
+
.dsh-si-dialog-btn.danger { background: transparent; border-color: transparent; color: var(--dsw-alias-state-error-primary, #f87171); }
|
|
5662
|
+
.dsh-si-dialog-btn.danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(248, 113, 113, .12)); }
|
|
5663
|
+
/* 划选评论编辑器:照抄 Trae CommentEditorCard(solo-lite 7920 包源码)——
|
|
5664
|
+
previewCommentCardContainer(菜单底/1px 中性描边/radius 8/双层阴影)内嵌
|
|
5665
|
+
commentEditorCard(320px/padding 8/gap 4 列布局);无标题无预览无取消钮,
|
|
5666
|
+
单行 = 透明自增 textarea(13px/20px,max-height 140)+ ✓ 主按钮;Enter 提交、
|
|
5667
|
+
Shift+Enter 换行、Esc/点外取消。 */
|
|
5668
|
+
.dsh-si-comment-editor { position: fixed; z-index: 1300; box-sizing: border-box; width: 320px; padding: 8px;
|
|
5669
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
5670
|
+
border-radius: var(--dsw-radius-sm, 8px); color: var(--dsw-alias-label-primary);
|
|
5671
|
+
font-family: var(--dsw-font-family, inherit); background: var(--dsw-specific-menu, #202124);
|
|
5672
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(255, 255, 255, .1));
|
|
5673
|
+
/* specific-menu 是半透明底:必须配官方毛玻璃,否则透出下层文字(官方弹卡成对规则) */
|
|
5674
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
5675
|
+
-webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
|
|
5676
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, .19), 0 .5px rgba(255, 255, 255, .06), inset 0 .5px rgba(255, 255, 255, .06);
|
|
5677
|
+
overflow: hidden;
|
|
5678
|
+
transition: width .25s cubic-bezier(.4, 0, .2, 1), height .25s cubic-bezier(.4, 0, .2, 1);
|
|
5679
|
+
visibility: hidden; }
|
|
5680
|
+
.dsh-si-comment-row { display: flex; align-items: center; gap: 8px; }
|
|
5681
|
+
.dsh-si-comment-input { flex: 1; min-width: 0; box-sizing: border-box; background: none; border: none; outline: none; resize: none;
|
|
5682
|
+
color: var(--dsw-alias-label-primary); font-family: var(--dsw-font-family, inherit);
|
|
5683
|
+
font-size: 13px; font-weight: 400; line-height: 20px; max-height: 140px;
|
|
5684
|
+
padding: 0 8px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, rgba(255, 255, 255, .16)) transparent; }
|
|
5685
|
+
.dsh-si-comment-input::placeholder { color: var(--dsw-alias-label-tertiary); }
|
|
5686
|
+
.dsh-si-comment-submit { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
|
|
5687
|
+
border: none; border-radius: 6px; cursor: pointer;
|
|
5688
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, .12)); color: var(--dsw-alias-label-primary); }
|
|
5689
|
+
.dsh-si-comment-submit:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .2)); }
|
|
5690
|
+
.dsh-si-comment-submit:disabled { opacity: .5; cursor: default; }
|
|
5691
|
+
/* 点击标签跳源时对源元素的闪烁高亮。 */
|
|
5692
|
+
.dsh-cp-flash { outline: 2px solid var(--dsw-alias-state-business-primary, #3a83f7) !important; outline-offset: 1px; border-radius: 3px; transition: outline-color .3s ease; }
|
|
5693
|
+
`
|
|
5694
|
+
|
|
5695
|
+
function ensureStyle() {
|
|
5696
|
+
if (typeof document === 'undefined') return
|
|
5697
|
+
if (document.getElementById(STYLE_ID)) return
|
|
5698
|
+
const style = document.createElement('style')
|
|
5699
|
+
style.id = STYLE_ID
|
|
5700
|
+
style.textContent = CSS
|
|
5701
|
+
document.head.appendChild(style)
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
async function api(path, body) {
|
|
5705
|
+
try {
|
|
5706
|
+
const res = await fetch(`${ENDPOINT}${path}`, body
|
|
5707
|
+
? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }
|
|
5708
|
+
: { cache: 'no-store' })
|
|
5709
|
+
return res.ok ? res.json() : null
|
|
5710
|
+
} catch { return null }
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
/** 异常留痕:走 prompts /diag(0.4.0 起套件统一通道,落 superinput-debug.log)。 */
|
|
5714
|
+
function report(message) {
|
|
5715
|
+
try {
|
|
5716
|
+
void fetch('/api/dsh-superinput/prompts/diag', {
|
|
5717
|
+
method: 'POST',
|
|
5718
|
+
headers: { 'content-type': 'application/json' },
|
|
5719
|
+
body: JSON.stringify({ message: `prompts: ${String(message).slice(0, 300)}` }),
|
|
5720
|
+
}).catch(() => {})
|
|
5721
|
+
} catch { /* ignore */ }
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
/* ------------------------------------------------------------------ */
|
|
5725
|
+
/* 划选引用:输入框内联 chip(官方 reference source)+ /context 同步 */
|
|
5726
|
+
/* ------------------------------------------------------------------ */
|
|
5727
|
+
|
|
5728
|
+
const selectionItems = new Map() // id -> item(发送后归档,供消息正文的 chip 渲染)
|
|
5729
|
+
const selectionSyncTimers = new Map()
|
|
5730
|
+
const selectionActions = new Map() // sessionId -> InputActions
|
|
5731
|
+
const selectionDetailSubs = new Set()
|
|
5732
|
+
let selectionDetailRequest = null
|
|
5733
|
+
let selectionSessions = null // ClientSessions 服务(取每会话 binding.ctx 发输入事件)
|
|
5734
|
+
let selectionIdSeq = 0
|
|
5735
|
+
let selectionStorageTimer = null
|
|
5736
|
+
|
|
5737
|
+
const tokenOf = (id) => `[选段#${id}]`
|
|
5738
|
+
const previewOf = (text, max = 220) => String(text ?? '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
5739
|
+
|
|
5740
|
+
/** 选区矩形兜底:菜单给的 rect 失效时退回锚点元素/默认位置。 */
|
|
5741
|
+
const normalizeSelectionRect = (rect, anchorEl) => {
|
|
5742
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n)
|
|
5743
|
+
if (rect && finite(rect.left) && finite(rect.top) && finite(rect.right) && finite(rect.bottom)) return rect
|
|
5744
|
+
const el = anchorEl instanceof Element ? anchorEl : null
|
|
5745
|
+
if (el) {
|
|
5746
|
+
const r = el.getBoundingClientRect()
|
|
5747
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }
|
|
5748
|
+
}
|
|
5749
|
+
return { left: 16, top: 80, right: 16, bottom: 80, width: 0, height: 0 }
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
/** chip 标签:Trae 是 `N. 选段文本 · 评论`(评论内联在标签里)。
|
|
5753
|
+
* 有评论时把选段压到 ~14 字(Trae 的 text-content 也只占 50%),
|
|
5754
|
+
* 否则 240px 的 chip 会把评论整段挤没。 */
|
|
5755
|
+
const chipTextOf = (seq, text, comment) => {
|
|
5756
|
+
const quote = String(text ?? '').replace(/\s+/g, ' ').trim()
|
|
5757
|
+
const note = String(comment ?? '').replace(/\s+/g, ' ').trim()
|
|
5758
|
+
if (!note) return `${seq}. ${quote}`
|
|
5759
|
+
const head = quote.length > 14 ? `${quote.slice(0, 14)}…` : quote
|
|
5760
|
+
return `${seq}. ${head} · ${note.slice(0, 40)}`
|
|
5761
|
+
}
|
|
5762
|
+
const chipLabelOf = (item) => `${CHIP_MARK}${chipTextOf(item.seq, item.text, item.comment)}`
|
|
5763
|
+
|
|
5764
|
+
/**
|
|
5765
|
+
* 某条 chip 在 detect 坐标里的跨度。occurrences 的 offset 是 clipboard
|
|
5766
|
+
* 坐标(chip 展开成 clipboardText),detect 坐标里每条 chip 只占 1 个
|
|
5767
|
+
* U+FFFC——把前面所有 chip 的 (length-1) 扣掉即得。
|
|
5768
|
+
*/
|
|
5769
|
+
function chipSpanOf(occurrences, ref, draftRev) {
|
|
5770
|
+
const own = (occurrences ?? []).find((occ) => occ.source === SELECTION_SOURCE && occ.ref === ref)
|
|
5771
|
+
if (!own) return null
|
|
5772
|
+
let start = own.offset
|
|
5773
|
+
for (const occ of occurrences) if (occ.offset < own.offset) start -= occ.length - 1
|
|
5774
|
+
return { start, end: start + 1, draftRev }
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
/** 当前草稿里真实存在的条目,按草稿里的先后顺序(Trae 的编号顺序)。 */
|
|
5778
|
+
function liveSelectionItems(sessionId, occurrences) {
|
|
5779
|
+
if (!sessionId) return []
|
|
5780
|
+
const seen = new Set()
|
|
5781
|
+
const live = []
|
|
5782
|
+
for (const occ of occurrences ?? []) {
|
|
5783
|
+
if (occ.source !== SELECTION_SOURCE || seen.has(occ.ref)) continue
|
|
5784
|
+
seen.add(occ.ref)
|
|
5785
|
+
const item = selectionItems.get(occ.ref)
|
|
5786
|
+
if (item && item.sessionId === sessionId) live.push(item)
|
|
5787
|
+
}
|
|
5788
|
+
return live
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
async function postContext(sessionId, items) {
|
|
5792
|
+
if (!sessionId) return
|
|
5793
|
+
try {
|
|
5794
|
+
await fetch(`${ENDPOINT}/context`, {
|
|
5795
|
+
method: 'POST',
|
|
5796
|
+
headers: { 'content-type': 'application/json' },
|
|
5797
|
+
body: JSON.stringify({
|
|
5798
|
+
sessionId,
|
|
5799
|
+
items: items.map(({ text, comment }) => ({
|
|
5800
|
+
text: String(text ?? '').slice(0, CONTEXT_TEXT_MAX),
|
|
5801
|
+
comment: String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX),
|
|
5802
|
+
})),
|
|
5803
|
+
}),
|
|
5804
|
+
keepalive: true,
|
|
5805
|
+
})
|
|
5806
|
+
} catch { /* 网络失败时服务端保留上一份,不打断输入 */ }
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
/** 服务端 /context 同步:150ms 防抖;发送意图用 immediate flush。 */
|
|
5810
|
+
function syncSelectionContext(sessionId, items, immediate) {
|
|
5811
|
+
if (!sessionId) return
|
|
5812
|
+
const previous = selectionSyncTimers.get(sessionId)
|
|
5813
|
+
if (previous) clearTimeout(previous)
|
|
5814
|
+
const send = () => {
|
|
5815
|
+
selectionSyncTimers.delete(sessionId)
|
|
5816
|
+
void postContext(sessionId, items)
|
|
5817
|
+
}
|
|
5818
|
+
if (immediate) send()
|
|
5819
|
+
else selectionSyncTimers.set(sessionId, setTimeout(send, 150))
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
/** 落 sessionStorage:消息正文的 chip 在刷新后仍有文本/评论可渲染。 */
|
|
5823
|
+
function persistSelectionItems() {
|
|
5824
|
+
if (typeof sessionStorage === 'undefined' || selectionStorageTimer) return
|
|
5825
|
+
selectionStorageTimer = setTimeout(() => {
|
|
5826
|
+
selectionStorageTimer = null
|
|
5827
|
+
try {
|
|
5828
|
+
const snapshot = [...selectionItems.values()].slice(-120).map((item) => ({
|
|
5829
|
+
id: item.id,
|
|
5830
|
+
sessionId: item.sessionId,
|
|
5831
|
+
seq: item.seq,
|
|
5832
|
+
text: item.text,
|
|
5833
|
+
comment: item.comment,
|
|
5834
|
+
sent: item.sent === true,
|
|
5835
|
+
at: item.at,
|
|
5836
|
+
}))
|
|
5837
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot))
|
|
5838
|
+
} catch { /* 配额/隐私模式:忽略 */ }
|
|
5839
|
+
}, 400)
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5842
|
+
function loadSelectionItems() {
|
|
5843
|
+
if (typeof sessionStorage === 'undefined') return
|
|
5844
|
+
try {
|
|
5845
|
+
const saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '[]')
|
|
5846
|
+
if (!Array.isArray(saved)) return
|
|
5847
|
+
for (const raw of saved) {
|
|
5848
|
+
if (!raw?.id || !raw?.text) continue
|
|
5849
|
+
const id = String(raw.id)
|
|
5850
|
+
if (selectionItems.has(id)) continue
|
|
5851
|
+
const sessionId = String(raw.sessionId ?? '')
|
|
5852
|
+
const seq = Number(raw.seq) || 0
|
|
5853
|
+
selectionItems.set(id, {
|
|
5854
|
+
id, sessionId, seq,
|
|
5855
|
+
text: String(raw.text),
|
|
5856
|
+
comment: String(raw.comment ?? ''),
|
|
5857
|
+
anchor: null,
|
|
5858
|
+
sent: raw.sent === true,
|
|
5859
|
+
archived: true, // 历史条目:chip 已不在草稿里,只用于消息正文展示
|
|
5860
|
+
at: Number(raw.at) || Date.now(),
|
|
5861
|
+
})
|
|
5862
|
+
}
|
|
5863
|
+
} catch { /* 存档损坏:忽略 */ }
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5866
|
+
/** 走会话作用域的 slash/input-insert-reference 事件把 chip 插进官方编辑器。 */
|
|
5867
|
+
function applySelectionInsert(sessionId, item, span) {
|
|
5868
|
+
if (!span) return false
|
|
5869
|
+
const binding = selectionSessions?.binding?.(sessionId)
|
|
5870
|
+
const actx = binding?.ctx
|
|
5871
|
+
if (!actx) return false
|
|
5872
|
+
const request = {
|
|
5873
|
+
reference: {
|
|
5874
|
+
source: SELECTION_SOURCE,
|
|
5875
|
+
ref: item.id,
|
|
5876
|
+
label: chipLabelOf(item),
|
|
5877
|
+
clipboardText: tokenOf(item.id),
|
|
5878
|
+
},
|
|
5879
|
+
span,
|
|
5880
|
+
}
|
|
5881
|
+
try {
|
|
5882
|
+
if (typeof actx.bail === 'function') {
|
|
5883
|
+
try { return actx.bail(actx, 'slash/input-insert-reference', request) === true }
|
|
5884
|
+
catch { /* 有些版本只接受 (name, payload) */ }
|
|
5885
|
+
try { return actx.bail('slash/input-insert-reference', request) === true }
|
|
5886
|
+
catch { return false }
|
|
5887
|
+
}
|
|
5888
|
+
if (typeof actx.emit === 'function') {
|
|
5889
|
+
actx.emit(actx, 'slash/input-insert-reference', request)
|
|
5890
|
+
return true
|
|
5891
|
+
}
|
|
5892
|
+
} catch { return false }
|
|
5893
|
+
return false
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
/** 删除一条 chip:用 insertText('') 替换它的 detect 跨度(空文本即删除)。 */
|
|
5897
|
+
function applySelectionRemove(sessionId, item, span) {
|
|
5898
|
+
if (!span) return false
|
|
5899
|
+
const actions = selectionActions.get(sessionId)
|
|
5900
|
+
if (typeof actions?.insertText !== 'function') return false
|
|
5901
|
+
try { return actions.insertText('', span) === true } catch { return false }
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
function requestSelectionDetail(sessionId, id, focus) {
|
|
5905
|
+
selectionDetailRequest = { sessionId, id, focus: focus === true }
|
|
5906
|
+
for (const listener of selectionDetailSubs) {
|
|
5907
|
+
try { listener() } catch { /* ignore */ }
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
loadSelectionItems()
|
|
5912
|
+
|
|
5913
|
+
/** 找出正文里的 {{变量}},去重保序。 */
|
|
5914
|
+
function varsOf(text) {
|
|
5915
|
+
const found = []
|
|
5916
|
+
const re = /\{\{\s*([a-zA-Z_$][\w$]*)\s*\}\}/g
|
|
5917
|
+
let match
|
|
5918
|
+
while ((match = re.exec(String(text ?? '')))) {
|
|
5919
|
+
if (!found.includes(match[1])) found.push(match[1])
|
|
5920
|
+
}
|
|
5921
|
+
return found
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
/** 内置动态变量:同步可取的自动填值;取不到的(如剪贴板)留空手填/异步预填。 */
|
|
5925
|
+
function autoValue(name) {
|
|
5926
|
+
const now = new Date()
|
|
4497
5927
|
if (name === '日期') return now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
|
|
4498
5928
|
if (name === '时间') return now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
4499
5929
|
if (name === '星期') return now.toLocaleDateString('zh-CN', { weekday: 'long' })
|
|
@@ -4568,84 +5998,53 @@ function createPromptsClient(require) {
|
|
|
4568
5998
|
if (hideTimer) clearTimeout(hideTimer)
|
|
4569
5999
|
hideTimer = setTimeout(hide, 1800)
|
|
4570
6000
|
}
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
try {
|
|
4574
|
-
await navigator.clipboard?.writeText(text)
|
|
4575
|
-
toast('已复制')
|
|
4576
|
-
} catch {
|
|
4577
|
-
toast('复制失败')
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
|
|
4581
|
-
/** 把选中文字追加进输入框草稿(草稿桥是 enhancer 客户端挂的全局)。 */
|
|
4582
|
-
const addToDraft = (text) => {
|
|
4583
|
-
hide()
|
|
4584
|
-
try {
|
|
4585
|
-
const set = window.__dshSuperinputSetDraft
|
|
4586
|
-
if (!set) { toast('草稿桥未就绪,刷新后重试'); return }
|
|
4587
|
-
const current = String(window.__dshSuperinputDraft?.() ?? '')
|
|
4588
|
-
set(current ? `${current}\n${text}` : text)
|
|
4589
|
-
toast('已添加到输入框')
|
|
4590
|
-
} catch {
|
|
4591
|
-
toast('添加失败')
|
|
4592
|
-
}
|
|
4593
|
-
}
|
|
4594
|
-
|
|
4595
|
-
const save = async (text) => {
|
|
4596
|
-
const title = text.replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
4597
|
-
hide()
|
|
4598
|
-
try {
|
|
4599
|
-
const res = await fetch('/api/dsh-superinput/prompts/save', {
|
|
4600
|
-
method: 'POST',
|
|
4601
|
-
headers: { 'content-type': 'application/json' },
|
|
4602
|
-
body: JSON.stringify({ title, body: text }),
|
|
4603
|
-
})
|
|
4604
|
-
const data = res.ok ? await res.json() : null
|
|
4605
|
-
toast(data?.ok ? '已存为模板' : '保存失败')
|
|
4606
|
-
} catch {
|
|
4607
|
-
toast('保存失败')
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
6001
|
+
// 评论编辑器在 React 侧保存后要提示,复用这个文档级 toast。
|
|
6002
|
+
window.__dshSuperinputToast = toast
|
|
4610
6003
|
|
|
4611
|
-
|
|
6004
|
+
/* 四选项横条(用户拍板恢复 0.8.x 形态):复制 / 引用文本(官方
|
|
6005
|
+
* insertQuote 机制)/ 添加评论(Trae 评论卡 → chip)/ 存为模板。
|
|
6006
|
+
* 官方自己的浮动「引用文本」pill 用 CSS 隐藏——横条里的「引用文本」
|
|
6007
|
+
* 调同一个官方 API,避免双浮层。 */
|
|
4612
6008
|
let showTimer = 0
|
|
4613
6009
|
document.addEventListener('selectionchange', () => {
|
|
4614
6010
|
try {
|
|
4615
6011
|
if (showTimer) { clearTimeout(showTimer); showTimer = 0 }
|
|
4616
6012
|
const selection = window.getSelection?.()
|
|
4617
6013
|
const text = String(selection ?? '').trim()
|
|
4618
|
-
if (!text ||
|
|
6014
|
+
if (!text || !selection.rangeCount) { hide(); return }
|
|
4619
6015
|
showTimer = setTimeout(() => {
|
|
4620
6016
|
try {
|
|
4621
6017
|
const sel = window.getSelection?.()
|
|
4622
6018
|
const current = String(sel ?? '').trim()
|
|
4623
|
-
if (!current ||
|
|
6019
|
+
if (!current || !sel.rangeCount) { hide(); return }
|
|
4624
6020
|
const node = sel.anchorNode
|
|
4625
6021
|
const el = node instanceof Element ? node : node?.parentElement
|
|
4626
|
-
//
|
|
6022
|
+
// 只收对话正文:跳过输入区、我们自己的弹层,以及侧栏/设置等非对话区域
|
|
4627
6023
|
if (!el || el.closest('[data-dsh-superinput-portal]') || el.closest('[contenteditable="true"], textarea, input')) { hide(); return }
|
|
6024
|
+
if (!el.closest('[data-chat-flow], [data-slot="conversation.chat.flow"]')) { hide(); return }
|
|
4628
6025
|
const rect = sel.getRangeAt(0).getBoundingClientRect()
|
|
4629
6026
|
if (!rect || (rect.width === 0 && rect.height === 0)) { hide(); return }
|
|
4630
6027
|
const container = ensureHost()
|
|
4631
|
-
//
|
|
4632
|
-
container.style.left = `${Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth -
|
|
6028
|
+
// 横排四项 ≈320px:左缘对齐选区,右侧防溢出
|
|
6029
|
+
container.style.left = `${Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - 330))}px`
|
|
4633
6030
|
container.style.top = `${Math.max(8, rect.top - 44)}px`
|
|
4634
6031
|
container.style.transform = ''
|
|
4635
6032
|
container.style.display = 'block'
|
|
4636
6033
|
const menu = document.createElement('div')
|
|
4637
6034
|
menu.className = 'dsh-cp-selmenu'
|
|
4638
|
-
|
|
6035
|
+
// 阻止默认聚焦/清选区:否则 mousedown 触发 selectionchange,菜单会在 click 前被隐藏
|
|
6036
|
+
menu.addEventListener('mousedown', (e) => { e.stopPropagation(); e.preventDefault() })
|
|
4639
6037
|
const mkItem = (label, action) => {
|
|
4640
6038
|
const b = document.createElement('button')
|
|
4641
6039
|
b.className = 'dsh-cp-selitem'
|
|
4642
6040
|
b.textContent = label
|
|
4643
|
-
b.addEventListener('click', () => { void action(current) })
|
|
6041
|
+
b.addEventListener('click', () => { void action(current, el, rect) })
|
|
4644
6042
|
return b
|
|
4645
6043
|
}
|
|
4646
6044
|
menu.append(
|
|
4647
6045
|
mkItem('复制', copySel),
|
|
4648
|
-
mkItem('
|
|
6046
|
+
mkItem('引用文本', quoteToComposer),
|
|
6047
|
+
mkItem('添加评论', (text2) => openComment(text2, el, rect)),
|
|
4649
6048
|
mkItem('存为模板', save))
|
|
4650
6049
|
container.replaceChildren(menu)
|
|
4651
6050
|
} catch { /* ignore */ }
|
|
@@ -4659,6 +6058,128 @@ function createPromptsClient(require) {
|
|
|
4659
6058
|
document.addEventListener('selectionchange', () => {
|
|
4660
6059
|
if (!String(window.getSelection?.() ?? '').trim()) hide()
|
|
4661
6060
|
})
|
|
6061
|
+
|
|
6062
|
+
const copySel = async (text) => {
|
|
6063
|
+
hide()
|
|
6064
|
+
if (!navigator.clipboard?.writeText) { toast('当前环境不支持复制'); return }
|
|
6065
|
+
try {
|
|
6066
|
+
await navigator.clipboard.writeText(text)
|
|
6067
|
+
toast('已复制')
|
|
6068
|
+
} catch {
|
|
6069
|
+
toast('复制失败')
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
/** 官方「引用文本」同款:inputActions.insertQuote 插 Markdown blockquote。
|
|
6074
|
+
* plain 模式限制下返回 false 时,退回草稿桥手写 `> ` 前缀(发送后官方
|
|
6075
|
+
* quoteRule 一样渲染成引用样式)。 */
|
|
6076
|
+
const quoteToComposer = (text) => {
|
|
6077
|
+
hide()
|
|
6078
|
+
const insert = window.__dshSuperinputInsertQuote
|
|
6079
|
+
if (typeof insert === 'function' && insert(text) === true) { toast('已引用到输入框'); return }
|
|
6080
|
+
try {
|
|
6081
|
+
const set = window.__dshSuperinputSetDraft
|
|
6082
|
+
if (!set) { toast('输入框桥未就绪,刷新后重试'); return }
|
|
6083
|
+
const block = text.split('\n').map((line) => `> ${line}`).join('\n')
|
|
6084
|
+
const current = String(window.__dshSuperinputDraft?.() ?? '')
|
|
6085
|
+
set(current ? `${current}\n${block}` : block)
|
|
6086
|
+
toast('已引用到输入框')
|
|
6087
|
+
} catch {
|
|
6088
|
+
toast('引用失败')
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
/** Trae 的「添加评论」:在选区旁弹评论卡,保存后才插 chip(带评论)。
|
|
6093
|
+
* rect 是选区矩形,React 侧用它把卡片锚在选区附近。 */
|
|
6094
|
+
const openComment = (text, anchorEl, rect) => {
|
|
6095
|
+
hide()
|
|
6096
|
+
const open = window.__dshSuperinputCommentContext
|
|
6097
|
+
if (typeof open !== 'function') { toast('评论编辑器未就绪,刷新后重试'); return }
|
|
6098
|
+
let status = 'failed'
|
|
6099
|
+
try {
|
|
6100
|
+
status = open(text, anchorEl, {
|
|
6101
|
+
left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom,
|
|
6102
|
+
width: rect.width, height: rect.height,
|
|
6103
|
+
})
|
|
6104
|
+
} catch { status = 'failed' }
|
|
6105
|
+
if (status === 'full') toast(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
6106
|
+
else if (status === 'empty') toast('选段为空')
|
|
6107
|
+
else if (status === 'failed') toast('打开评论编辑器失败')
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
const save = async (text) => {
|
|
6111
|
+
const title = text.replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
6112
|
+
hide()
|
|
6113
|
+
try {
|
|
6114
|
+
const res = await fetch('/api/dsh-superinput/prompts/save', {
|
|
6115
|
+
method: 'POST',
|
|
6116
|
+
headers: { 'content-type': 'application/json' },
|
|
6117
|
+
body: JSON.stringify({ title, body: text }),
|
|
6118
|
+
})
|
|
6119
|
+
const data = res.ok ? await res.json() : null
|
|
6120
|
+
toast(data?.ok ? '已存为模板' : '保存失败')
|
|
6121
|
+
} catch {
|
|
6122
|
+
toast('保存失败')
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
/* Trae dragDropMention 等价:把对话里划选的文字拖进输入框,落成引用 chip。
|
|
6127
|
+
* dragstart 时对选区打自定义 dataTransfer 标记 + stash 文本;dragover 放行
|
|
6128
|
+
* composer;drop 拦截宿主的纯文本插入,走 __dshSuperinputDropContext 插 chip。 */
|
|
6129
|
+
let dragSelection = null // { text, anchorEl }
|
|
6130
|
+
document.addEventListener('dragstart', (event) => {
|
|
6131
|
+
try {
|
|
6132
|
+
const selection = window.getSelection?.()
|
|
6133
|
+
const text = String(selection ?? '').trim()
|
|
6134
|
+
const node = selection?.anchorNode
|
|
6135
|
+
const el = node instanceof Element ? node : node?.parentElement
|
|
6136
|
+
if (!text || !el?.closest?.('[data-chat-flow], [data-slot="conversation.chat.flow"]')
|
|
6137
|
+
|| el.closest?.('[contenteditable="true"], textarea, input')) { dragSelection = null; return }
|
|
6138
|
+
dragSelection = { text: text.slice(0, CONTEXT_TEXT_MAX), anchorEl: el }
|
|
6139
|
+
try { event.dataTransfer?.setData('application/x-dsh-selection', '1') } catch { /* 标记失败只影响识别 */ }
|
|
6140
|
+
} catch { dragSelection = null }
|
|
6141
|
+
}, true)
|
|
6142
|
+
document.addEventListener('dragover', (event) => {
|
|
6143
|
+
try {
|
|
6144
|
+
if (!dragSelection) return
|
|
6145
|
+
const target = event.target
|
|
6146
|
+
if (!(target instanceof Element) || !target.closest?.('[contenteditable="true"]')) return
|
|
6147
|
+
event.preventDefault()
|
|
6148
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
|
|
6149
|
+
} catch { /* ignore */ }
|
|
6150
|
+
}, true)
|
|
6151
|
+
document.addEventListener('drop', (event) => {
|
|
6152
|
+
try {
|
|
6153
|
+
if (!dragSelection) return
|
|
6154
|
+
const target = event.target
|
|
6155
|
+
if (!(target instanceof Element) || !target.closest?.('[contenteditable="true"]')) return
|
|
6156
|
+
event.preventDefault()
|
|
6157
|
+
event.stopPropagation()
|
|
6158
|
+
const { text, anchorEl } = dragSelection
|
|
6159
|
+
dragSelection = null
|
|
6160
|
+
// 尽力把光标挪到落点:宿主 captureInsertion 读到哪就是哪。
|
|
6161
|
+
try {
|
|
6162
|
+
const range = typeof document.caretRangeFromPoint === 'function'
|
|
6163
|
+
? document.caretRangeFromPoint(event.clientX, event.clientY)
|
|
6164
|
+
: null
|
|
6165
|
+
if (range) {
|
|
6166
|
+
window.getSelection()?.removeAllRanges()
|
|
6167
|
+
window.getSelection()?.addRange(range)
|
|
6168
|
+
}
|
|
6169
|
+
} catch { /* 定位失败就落原 caret */ }
|
|
6170
|
+
const insert = window.__dshSuperinputDropContext ?? window.__dshSuperinputAddContext
|
|
6171
|
+
if (typeof insert === 'function') {
|
|
6172
|
+
const status = insert(text, anchorEl)
|
|
6173
|
+
const toast = window.__dshSuperinputToast
|
|
6174
|
+
if (typeof toast === 'function') {
|
|
6175
|
+
if (status === 'added') toast('已添加到对话')
|
|
6176
|
+
else if (status === 'dup') toast('该选段已在列表中')
|
|
6177
|
+
else if (status === 'full') toast(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
} catch { /* ignore */ }
|
|
6181
|
+
}, true)
|
|
6182
|
+
document.addEventListener('dragend', () => { dragSelection = null }, true)
|
|
4662
6183
|
}
|
|
4663
6184
|
|
|
4664
6185
|
function PromptsPanel({ anchor, onClose, onSyncSlash }) {
|
|
@@ -4954,6 +6475,525 @@ function createPromptsClient(require) {
|
|
|
4954
6475
|
render() { return this.state.crashed ? null : this.props.children }
|
|
4955
6476
|
}
|
|
4956
6477
|
|
|
6478
|
+
/** 详情弹窗:Trae 点 chip 打开的**只读** Dialog(引用全文 + 评论 + 跳转原文)。
|
|
6479
|
+
* 评论不在这个弹窗里编辑——Trae 的评论在选区旁的 previewCommentCard 输入;
|
|
6480
|
+
* 这里只展示结果(与 ai-modules-chat 的 mention-info-modal 同构)。 */
|
|
6481
|
+
function SelectionDetail({ title, text, comment, onNavigate, onClose }) {
|
|
6482
|
+
const maskRef = React.useRef(null)
|
|
6483
|
+
|
|
6484
|
+
/* 桌面壳(Electron)里 body 会变成 0×0 的 fixed 包含块:`inset: 0` 的遮罩
|
|
6485
|
+
* 缩在左下角,弹窗被裁掉。打开后量一次遮罩实际矩形,若没有铺满视口,
|
|
6486
|
+
* 就按测量偏移把遮罩校正回视口原点并铺满——不依赖宿主沿用哪种 portal。 */
|
|
6487
|
+
React.useEffect(() => {
|
|
6488
|
+
const mask = maskRef.current
|
|
6489
|
+
if (!mask || typeof window === 'undefined') return undefined
|
|
6490
|
+
let corrected = false
|
|
6491
|
+
const fix = () => {
|
|
6492
|
+
const vw = window.innerWidth
|
|
6493
|
+
const vh = window.innerHeight
|
|
6494
|
+
if (!corrected) {
|
|
6495
|
+
const rect = mask.getBoundingClientRect()
|
|
6496
|
+
if (rect.width >= vw * 0.8 && rect.height >= vh * 0.8) return
|
|
6497
|
+
corrected = true
|
|
6498
|
+
mask.style.position = 'fixed'
|
|
6499
|
+
mask.style.inset = 'auto'
|
|
6500
|
+
mask.style.left = `${-rect.left}px`
|
|
6501
|
+
mask.style.top = `${-rect.top}px`
|
|
6502
|
+
mask.style.display = 'flex'
|
|
6503
|
+
mask.style.alignItems = 'center'
|
|
6504
|
+
mask.style.justifyContent = 'center'
|
|
6505
|
+
report(`selection dialog: corrected mask ${Math.round(rect.width)}x${Math.round(rect.height)}@${Math.round(rect.left)},${Math.round(rect.top)} to viewport ${vw}x${vh}`)
|
|
6506
|
+
}
|
|
6507
|
+
mask.style.width = `${vw}px`
|
|
6508
|
+
mask.style.height = `${vh}px`
|
|
6509
|
+
mask.style.boxSizing = 'border-box'
|
|
6510
|
+
}
|
|
6511
|
+
fix()
|
|
6512
|
+
window.addEventListener('resize', fix)
|
|
6513
|
+
return () => window.removeEventListener('resize', fix)
|
|
6514
|
+
}, [])
|
|
6515
|
+
|
|
6516
|
+
return h('div', {
|
|
6517
|
+
ref: maskRef,
|
|
6518
|
+
className: 'dsh-si-mask',
|
|
6519
|
+
onMouseDown: (event) => { event.stopPropagation(); onClose?.() },
|
|
6520
|
+
},
|
|
6521
|
+
h('div', {
|
|
6522
|
+
className: 'dsh-si-dialog',
|
|
6523
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
6524
|
+
onClick: (event) => event.stopPropagation(),
|
|
6525
|
+
},
|
|
6526
|
+
h('div', { className: 'dsh-si-dialog-head' },
|
|
6527
|
+
h('span', { className: 'dsh-si-dialog-title' }, title),
|
|
6528
|
+
h('button', { className: 'dsh-si-dialog-x', 'aria-label': '关闭', onClick: () => onClose?.() }, '×')),
|
|
6529
|
+
h('div', { className: 'dsh-si-dialog-quote' }, text),
|
|
6530
|
+
comment ? h(React.Fragment, null,
|
|
6531
|
+
h('div', { className: 'dsh-si-dialog-label' }, '评论'),
|
|
6532
|
+
h('div', { className: 'dsh-si-dialog-detail' }, comment)) : null,
|
|
6533
|
+
h('div', { className: 'dsh-si-dialog-foot' },
|
|
6534
|
+
onNavigate ? h('button', { className: 'dsh-si-dialog-btn', onClick: onNavigate }, '跳转原文') : null,
|
|
6535
|
+
h('span', { className: 'dsh-si-dialog-spacer' }),
|
|
6536
|
+
h('button', { className: 'dsh-si-dialog-btn ghost', onClick: () => onClose?.() }, '关闭')))
|
|
6537
|
+
)
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6540
|
+
/** 划选评论编辑器:照抄 Trae CommentEditorCard——无标题/无预览/无取消钮,
|
|
6541
|
+
* 单行 = 透明自增 textarea + ✓ 按钮;Enter 提交、Shift+Enter 换行、
|
|
6542
|
+
* Esc/点外取消;IME 组合期不提交。锚在选区旁,不遮罩。 */
|
|
6543
|
+
function SelectionCommentEditor({ draft, onSave, onCancel }) {
|
|
6544
|
+
const [value, setValue] = React.useState(draft.comment ?? '')
|
|
6545
|
+
const cardRef = React.useRef(null)
|
|
6546
|
+
const inputRef = React.useRef(null)
|
|
6547
|
+
const valueRef = React.useRef(value)
|
|
6548
|
+
React.useEffect(() => { valueRef.current = value }, [value])
|
|
6549
|
+
|
|
6550
|
+
// textarea 自增高(Trae 同款:height auto → scrollHeight,上限 140)
|
|
6551
|
+
React.useEffect(() => {
|
|
6552
|
+
const el = inputRef.current
|
|
6553
|
+
if (!el) return
|
|
6554
|
+
el.style.height = 'auto'
|
|
6555
|
+
el.style.height = `${Math.min(el.scrollHeight, 140)}px`
|
|
6556
|
+
}, [value])
|
|
6557
|
+
|
|
6558
|
+
React.useEffect(() => {
|
|
6559
|
+
const card = cardRef.current
|
|
6560
|
+
if (!card || typeof window === 'undefined') return
|
|
6561
|
+
const vw = window.innerWidth
|
|
6562
|
+
const vh = window.innerHeight
|
|
6563
|
+
const margin = 8
|
|
6564
|
+
const width = Math.min(320, vw - margin * 2)
|
|
6565
|
+
card.style.width = `${width}px`
|
|
6566
|
+
const height = card.getBoundingClientRect().height || 44
|
|
6567
|
+
const rect = draft.rect ?? {}
|
|
6568
|
+
let left = Math.min(Math.max(margin, (rect.left ?? 16) + 20), Math.max(margin, vw - width - margin))
|
|
6569
|
+
let top = (rect.bottom ?? 80) + 8
|
|
6570
|
+
if (top + height > vh - margin) top = Math.max(margin, (rect.top ?? 80) - height - 8)
|
|
6571
|
+
// 先放到期望位置;桌面壳若把 fixed 包含块变成 0×0,再按实际偏差校正。
|
|
6572
|
+
card.style.visibility = 'hidden'
|
|
6573
|
+
card.style.left = `${left}px`
|
|
6574
|
+
card.style.top = `${top}px`
|
|
6575
|
+
const actual = card.getBoundingClientRect()
|
|
6576
|
+
const dx = actual.left - left
|
|
6577
|
+
const dy = actual.top - top
|
|
6578
|
+
if (Math.abs(dx) > 1) card.style.left = `${left - dx}px`
|
|
6579
|
+
if (Math.abs(dy) > 1) card.style.top = `${top - dy}px`
|
|
6580
|
+
card.style.visibility = 'visible'
|
|
6581
|
+
const textarea = card.querySelector('textarea')
|
|
6582
|
+
if (textarea) {
|
|
6583
|
+
textarea.focus()
|
|
6584
|
+
textarea.setSelectionRange?.(textarea.value.length, textarea.value.length)
|
|
6585
|
+
}
|
|
6586
|
+
}, [draft])
|
|
6587
|
+
|
|
6588
|
+
React.useEffect(() => {
|
|
6589
|
+
const onDown = (event) => {
|
|
6590
|
+
if (cardRef.current?.contains(event.target)) return
|
|
6591
|
+
onCancel?.()
|
|
6592
|
+
}
|
|
6593
|
+
document.addEventListener('mousedown', onDown, true)
|
|
6594
|
+
return () => document.removeEventListener('mousedown', onDown, true)
|
|
6595
|
+
}, [onCancel])
|
|
6596
|
+
|
|
6597
|
+
const submit = () => {
|
|
6598
|
+
if (!value.trim()) return
|
|
6599
|
+
onSave?.(value)
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
return createPortal(h('div', {
|
|
6603
|
+
ref: cardRef,
|
|
6604
|
+
className: 'dsh-si-comment-editor',
|
|
6605
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
6606
|
+
onClick: (event) => event.stopPropagation(),
|
|
6607
|
+
},
|
|
6608
|
+
h('div', { className: 'dsh-si-comment-row' },
|
|
6609
|
+
h('textarea', {
|
|
6610
|
+
ref: inputRef,
|
|
6611
|
+
className: 'dsh-si-comment-input',
|
|
6612
|
+
rows: 1,
|
|
6613
|
+
value,
|
|
6614
|
+
placeholder: '输入你的评论...',
|
|
6615
|
+
'aria-label': '输入你的评论...',
|
|
6616
|
+
onInput: (event) => setValue(event.target.value),
|
|
6617
|
+
onKeyDown: (event) => {
|
|
6618
|
+
if (event.nativeEvent?.isComposing || event.nativeEvent?.keyCode === 229) return
|
|
6619
|
+
event.stopPropagation()
|
|
6620
|
+
if (event.key === 'Escape') { event.preventDefault(); onCancel?.() }
|
|
6621
|
+
else if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
6622
|
+
event.preventDefault()
|
|
6623
|
+
submit()
|
|
6624
|
+
}
|
|
6625
|
+
},
|
|
6626
|
+
}),
|
|
6627
|
+
h('button', {
|
|
6628
|
+
className: 'dsh-si-comment-submit',
|
|
6629
|
+
title: draft.comment ? '保存' : '评论到对话',
|
|
6630
|
+
'aria-label': draft.comment ? '保存' : '评论到对话',
|
|
6631
|
+
disabled: !value.trim(),
|
|
6632
|
+
onClick: submit,
|
|
6633
|
+
},
|
|
6634
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', 'aria-hidden': true },
|
|
6635
|
+
h('path', {
|
|
6636
|
+
d: 'M13.5 4.5L6.5 11.5L2.5 7.5',
|
|
6637
|
+
fill: 'none', stroke: 'currentColor', 'stroke-width': '1.8',
|
|
6638
|
+
'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
6639
|
+
}))))
|
|
6640
|
+
), document.body)
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
/** 划选引用桥:把「添加到对话」插成输入框内的官方 reference chip。
|
|
6644
|
+
* Trae 的引用是编辑器里的内联节点(conversation-selection-mention-symbol),
|
|
6645
|
+
* dsh 对外只开放 reference chip 这一条内联通道——这里注册自定义 source,
|
|
6646
|
+
* 用 slash/input-insert-reference 落 chip,并按 Trae 规格重着色。
|
|
6647
|
+
* 输入框上方不再有独立 pill 行。 */
|
|
6648
|
+
function SelectionContextBridge({ session, useInput, inputActions }) {
|
|
6649
|
+
const sessionId = session?.sessionId === undefined ? '' : String(session.sessionId)
|
|
6650
|
+
const draftRev = useInput((state) => state?.draftRev ?? 0)
|
|
6651
|
+
const occurrences = useInput((state) => state?.occurrences)
|
|
6652
|
+
const phase = useInput((state) => state?.phase)
|
|
6653
|
+
const snapshotRef = React.useRef({ draftRev, occurrences })
|
|
6654
|
+
const actionsRef = React.useRef(inputActions)
|
|
6655
|
+
const previousPhase = React.useRef(phase)
|
|
6656
|
+
const [, setVersion] = React.useState(0)
|
|
6657
|
+
const [dialog, setDialog] = React.useState(null)
|
|
6658
|
+
const [editor, setEditor] = React.useState(null)
|
|
6659
|
+
const bump = () => setVersion((value) => value + 1)
|
|
6660
|
+
|
|
6661
|
+
React.useEffect(() => {
|
|
6662
|
+
snapshotRef.current = { draftRev, occurrences }
|
|
6663
|
+
actionsRef.current = inputActions
|
|
6664
|
+
}, [draftRev, occurrences, inputActions])
|
|
6665
|
+
|
|
6666
|
+
React.useEffect(() => {
|
|
6667
|
+
if (!sessionId || !inputActions) return undefined
|
|
6668
|
+
selectionActions.set(sessionId, inputActions)
|
|
6669
|
+
return () => {
|
|
6670
|
+
if (selectionActions.get(sessionId) === inputActions) selectionActions.delete(sessionId)
|
|
6671
|
+
}
|
|
6672
|
+
}, [sessionId, inputActions])
|
|
6673
|
+
|
|
6674
|
+
/** 发送意图:把当前 chip 立即同步给服务端(pre-step 读取),并标记发送。 */
|
|
6675
|
+
const flushForSend = () => {
|
|
6676
|
+
if (!sessionId) return
|
|
6677
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
6678
|
+
if (live.length === 0) return
|
|
6679
|
+
syncSelectionContext(sessionId, live, true)
|
|
6680
|
+
for (const item of live) item.sent = true
|
|
6681
|
+
persistSelectionItems()
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6684
|
+
/** 插入/更新一条选段 chip;comment === undefined 表示「不改已有评论」。 */
|
|
6685
|
+
const insertSelection = (text, anchorEl, comment) => {
|
|
6686
|
+
const clean = String(text ?? '').trim().slice(0, CONTEXT_TEXT_MAX)
|
|
6687
|
+
if (!clean) return { status: 'empty' }
|
|
6688
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
6689
|
+
const existing = live.find((entry) => entry.text === clean)
|
|
6690
|
+
if (existing) {
|
|
6691
|
+
if (comment !== undefined) {
|
|
6692
|
+
const next = String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim()
|
|
6693
|
+
if (next !== (existing.comment ?? '')) {
|
|
6694
|
+
existing.comment = next
|
|
6695
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, existing.id, snapshotRef.current.draftRev)
|
|
6696
|
+
if (span) applySelectionInsert(sessionId, existing, span)
|
|
6697
|
+
bump()
|
|
6698
|
+
syncSelectionContext(sessionId, liveSelectionItems(sessionId, snapshotRef.current.occurrences), false)
|
|
6699
|
+
persistSelectionItems()
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
return { status: 'dup', id: existing.id, item: existing }
|
|
6703
|
+
}
|
|
6704
|
+
if (live.length >= CONTEXT_MAX) return { status: 'full' }
|
|
6705
|
+
const actions = actionsRef.current
|
|
6706
|
+
if (typeof actions?.captureInsertion !== 'function') return { status: 'failed' }
|
|
6707
|
+
let span
|
|
6708
|
+
try {
|
|
6709
|
+
const insertion = actions.captureInsertion()
|
|
6710
|
+
// 折叠到选区末尾:只插入、不替换用户已选中的草稿文字。
|
|
6711
|
+
span = { start: insertion.end, end: insertion.end, draftRev: insertion.draftRev }
|
|
6712
|
+
} catch { return { status: 'failed' } }
|
|
6713
|
+
// 显示序号按当前输入框里的条目数重排(Trae 每个草稿从 1 开始)。
|
|
6714
|
+
const seq = live.reduce((max, entry) => Math.max(max, entry.seq ?? 0), 0) + 1
|
|
6715
|
+
const id = `${Date.now().toString(36)}${(++selectionIdSeq).toString(36)}`
|
|
6716
|
+
const item = {
|
|
6717
|
+
id, sessionId, seq,
|
|
6718
|
+
text: clean,
|
|
6719
|
+
comment: comment === undefined ? '' : String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim(),
|
|
6720
|
+
anchor: anchorEl instanceof Element ? new WeakRef(anchorEl) : null,
|
|
6721
|
+
sent: false,
|
|
6722
|
+
archived: false,
|
|
6723
|
+
at: Date.now(),
|
|
6724
|
+
}
|
|
6725
|
+
if (!applySelectionInsert(sessionId, item, span)) return { status: 'failed' }
|
|
6726
|
+
selectionItems.set(id, item)
|
|
6727
|
+
bump()
|
|
6728
|
+
syncSelectionContext(sessionId, [...live, item], false)
|
|
6729
|
+
persistSelectionItems()
|
|
6730
|
+
return { status: 'added', id, item }
|
|
6731
|
+
}
|
|
6732
|
+
|
|
6733
|
+
/** 删除一条选段:chip + 条目 + 服务端同步(hover × / 编辑器移除共用)。 */
|
|
6734
|
+
const dropSelectionItem = (item) => {
|
|
6735
|
+
if (!item) return
|
|
6736
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, item.id, snapshotRef.current.draftRev)
|
|
6737
|
+
if (span) applySelectionRemove(item.sessionId, item, span)
|
|
6738
|
+
selectionItems.delete(item.id)
|
|
6739
|
+
bump()
|
|
6740
|
+
syncSelectionContext(item.sessionId, liveSelectionItems(item.sessionId, snapshotRef.current.occurrences), false)
|
|
6741
|
+
persistSelectionItems()
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
React.useEffect(() => {
|
|
6745
|
+
const was = previousPhase.current
|
|
6746
|
+
previousPhase.current = phase
|
|
6747
|
+
if (was !== 'submitting' && phase === 'submitting') flushForSend()
|
|
6748
|
+
}, [phase, sessionId])
|
|
6749
|
+
|
|
6750
|
+
// 发送按钮 / Enter 比 phase 更早、更可靠;window capture 先于宿主监听。
|
|
6751
|
+
React.useEffect(() => {
|
|
6752
|
+
if (!sessionId) return undefined
|
|
6753
|
+
const onClick = (event) => {
|
|
6754
|
+
const target = event.target
|
|
6755
|
+
if (target?.closest?.('button[aria-label*="发送消息"], button[aria-label*="Send"], button[aria-label*="send"]')) flushForSend()
|
|
6756
|
+
}
|
|
6757
|
+
const onKey = (event) => {
|
|
6758
|
+
if (event.key !== 'Enter' || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return
|
|
6759
|
+
if (!event.target?.closest?.('[contenteditable="true"]')) return
|
|
6760
|
+
flushForSend()
|
|
6761
|
+
}
|
|
6762
|
+
window.addEventListener('click', onClick, true)
|
|
6763
|
+
window.addEventListener('keydown', onKey, true)
|
|
6764
|
+
return () => {
|
|
6765
|
+
window.removeEventListener('click', onClick, true)
|
|
6766
|
+
window.removeEventListener('keydown', onKey, true)
|
|
6767
|
+
}
|
|
6768
|
+
}, [sessionId])
|
|
6769
|
+
|
|
6770
|
+
// 草稿里的 chip 被键盘删掉 / 发送后清空:以 occurrences 为真相修剪条目。
|
|
6771
|
+
React.useEffect(() => {
|
|
6772
|
+
if (!sessionId) return
|
|
6773
|
+
const refs = new Set()
|
|
6774
|
+
for (const occ of occurrences ?? []) if (occ.source === SELECTION_SOURCE) refs.add(occ.ref)
|
|
6775
|
+
let changed = false
|
|
6776
|
+
for (const [id, item] of selectionItems) {
|
|
6777
|
+
if (item.sessionId !== sessionId) continue
|
|
6778
|
+
if (item.archived) {
|
|
6779
|
+
// 草稿恢复出的历史 chip:重新激活,参与同步
|
|
6780
|
+
if (refs.has(id)) { item.archived = false; item.sent = false; changed = true }
|
|
6781
|
+
continue
|
|
6782
|
+
}
|
|
6783
|
+
if (refs.has(id)) continue
|
|
6784
|
+
if (item.sent) { item.archived = true; changed = true; continue }
|
|
6785
|
+
selectionItems.delete(id)
|
|
6786
|
+
changed = true
|
|
6787
|
+
}
|
|
6788
|
+
if (changed) bump()
|
|
6789
|
+
syncSelectionContext(sessionId, liveSelectionItems(sessionId, occurrences), false)
|
|
6790
|
+
persistSelectionItems()
|
|
6791
|
+
}, [occurrences, sessionId])
|
|
6792
|
+
|
|
6793
|
+
// 详情弹窗请求(chip 点击 → source.openReference;添加成功后也会主动打开)。
|
|
6794
|
+
React.useEffect(() => {
|
|
6795
|
+
if (!sessionId) return undefined
|
|
6796
|
+
const listener = () => {
|
|
6797
|
+
const request = selectionDetailRequest
|
|
6798
|
+
if (!request || request.sessionId !== sessionId) return
|
|
6799
|
+
selectionDetailRequest = null
|
|
6800
|
+
const item = selectionItems.get(request.id)
|
|
6801
|
+
if (!item) return
|
|
6802
|
+
setEditor(null)
|
|
6803
|
+
setDialog({ id: request.id })
|
|
6804
|
+
}
|
|
6805
|
+
selectionDetailSubs.add(listener)
|
|
6806
|
+
listener()
|
|
6807
|
+
return () => { selectionDetailSubs.delete(listener) }
|
|
6808
|
+
}, [sessionId])
|
|
6809
|
+
|
|
6810
|
+
// Trae 的两个入口(previewCommentCard):
|
|
6811
|
+
// - 添加到对话:先插 chip,再把选区旁的卡片原位切成评论编辑器(addThenComment);
|
|
6812
|
+
// - 评论到对话:先弹评论卡片,保存后才插 chip(带评论)。
|
|
6813
|
+
React.useEffect(() => {
|
|
6814
|
+
if (!sessionId) return undefined
|
|
6815
|
+
const add = (text, anchorEl, rect) => {
|
|
6816
|
+
const result = insertSelection(text, anchorEl, undefined)
|
|
6817
|
+
if (result.status === 'added' || result.status === 'dup') {
|
|
6818
|
+
const item = result.item ?? (result.id ? selectionItems.get(result.id) : null)
|
|
6819
|
+
if (item) {
|
|
6820
|
+
// Trae addThenComment:chips 已经进输入框,卡片原位切换为评论编辑。
|
|
6821
|
+
setDialog(null)
|
|
6822
|
+
setEditor({
|
|
6823
|
+
id: item.id,
|
|
6824
|
+
text: item.text,
|
|
6825
|
+
comment: item.comment ?? '',
|
|
6826
|
+
anchorEl: anchorEl instanceof Element ? anchorEl : null,
|
|
6827
|
+
rect: normalizeSelectionRect(rect, anchorEl),
|
|
6828
|
+
})
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
return result.status
|
|
6832
|
+
}
|
|
6833
|
+
const openComment = (text, anchorEl, rect) => {
|
|
6834
|
+
const clean = String(text ?? '').trim().slice(0, CONTEXT_TEXT_MAX)
|
|
6835
|
+
if (!clean) return 'empty'
|
|
6836
|
+
const live = liveSelectionItems(sessionId, snapshotRef.current.occurrences)
|
|
6837
|
+
const existing = live.find((entry) => entry.text === clean)
|
|
6838
|
+
if (!existing && live.length >= CONTEXT_MAX) return 'full'
|
|
6839
|
+
setDialog(null)
|
|
6840
|
+
setEditor({
|
|
6841
|
+
id: existing?.id ?? null,
|
|
6842
|
+
text: clean,
|
|
6843
|
+
comment: existing?.comment ?? '',
|
|
6844
|
+
anchorEl: anchorEl instanceof Element ? anchorEl : null,
|
|
6845
|
+
rect: normalizeSelectionRect(rect, anchorEl),
|
|
6846
|
+
})
|
|
6847
|
+
return 'opened'
|
|
6848
|
+
}
|
|
6849
|
+
window.__dshSuperinputAddContext = add
|
|
6850
|
+
window.__dshSuperinputCommentContext = openComment
|
|
6851
|
+
// Trae dragDropMention 同义:拖放进来的引用只插 chip,不弹评论卡
|
|
6852
|
+
//(拖拽是快速引用手势;要评论走划选菜单的「评论到对话」)。
|
|
6853
|
+
window.__dshSuperinputDropContext = (text, anchorEl) => insertSelection(text, anchorEl, undefined)
|
|
6854
|
+
return () => {
|
|
6855
|
+
if (window.__dshSuperinputAddContext === add) delete window.__dshSuperinputAddContext
|
|
6856
|
+
if (window.__dshSuperinputCommentContext === openComment) delete window.__dshSuperinputCommentContext
|
|
6857
|
+
if (window.__dshSuperinputDropContext === insertSelection) delete window.__dshSuperinputDropContext
|
|
6858
|
+
}
|
|
6859
|
+
}, [sessionId])
|
|
6860
|
+
|
|
6861
|
+
// chip 悬停时左侧图标变 ×(Trae remove-mention-icon);点左侧 20px 移除。
|
|
6862
|
+
React.useEffect(() => {
|
|
6863
|
+
if (!sessionId) return undefined
|
|
6864
|
+
const onClick = (event) => {
|
|
6865
|
+
const target = event.target
|
|
6866
|
+
const chip = target?.closest?.('span[title]')
|
|
6867
|
+
if (!chip || !chip.closest?.('[contenteditable="true"]')) return
|
|
6868
|
+
if (!String(chip.title || '').startsWith(CHIP_MARK)) return
|
|
6869
|
+
const rect = chip.getBoundingClientRect()
|
|
6870
|
+
if (event.clientX > rect.left + 20 || !chip.matches(':hover')) return
|
|
6871
|
+
const label = String(chip.title)
|
|
6872
|
+
const item = [...selectionItems.values()].find((entry) => entry.sessionId === sessionId && chipLabelOf(entry) === label)
|
|
6873
|
+
if (!item) return
|
|
6874
|
+
event.preventDefault()
|
|
6875
|
+
event.stopPropagation()
|
|
6876
|
+
dropSelectionItem(item)
|
|
6877
|
+
}
|
|
6878
|
+
document.addEventListener('click', onClick, true)
|
|
6879
|
+
return () => document.removeEventListener('click', onClick, true)
|
|
6880
|
+
}, [sessionId])
|
|
6881
|
+
|
|
6882
|
+
const navigateTo = (item) => {
|
|
6883
|
+
try {
|
|
6884
|
+
const el = item.anchor?.deref?.()
|
|
6885
|
+
if (el?.isConnected) {
|
|
6886
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
6887
|
+
el.classList.add('dsh-cp-flash')
|
|
6888
|
+
setTimeout(() => el.classList.remove('dsh-cp-flash'), 1600)
|
|
6889
|
+
}
|
|
6890
|
+
} catch { /* 锚点失效:静默降级 */ }
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
/** 评论卡片保存:已有条目更新评论,新条目插 chip(带评论)。 */
|
|
6894
|
+
const saveEditorComment = (comment) => {
|
|
6895
|
+
const draft = editor
|
|
6896
|
+
if (!draft) return
|
|
6897
|
+
const next = String(comment ?? '').slice(0, CONTEXT_COMMENT_MAX).trim()
|
|
6898
|
+
const existing = draft.id ? selectionItems.get(draft.id) : null
|
|
6899
|
+
if (existing) {
|
|
6900
|
+
if (next !== (existing.comment ?? '')) {
|
|
6901
|
+
existing.comment = next
|
|
6902
|
+
const span = chipSpanOf(snapshotRef.current.occurrences, existing.id, snapshotRef.current.draftRev)
|
|
6903
|
+
if (span) applySelectionInsert(existing.sessionId, existing, span)
|
|
6904
|
+
bump()
|
|
6905
|
+
syncSelectionContext(existing.sessionId, liveSelectionItems(existing.sessionId, snapshotRef.current.occurrences), false)
|
|
6906
|
+
persistSelectionItems()
|
|
6907
|
+
window.__dshSuperinputToast?.('已保存评论')
|
|
6908
|
+
}
|
|
6909
|
+
} else {
|
|
6910
|
+
const result = insertSelection(draft.text, draft.anchorEl, next)
|
|
6911
|
+
if (result.status === 'added' || result.status === 'dup') window.__dshSuperinputToast?.('已添加到对话')
|
|
6912
|
+
else if (result.status === 'full') window.__dshSuperinputToast?.(`最多添加 ${CONTEXT_MAX} 段对话内容`)
|
|
6913
|
+
else if (result.status === 'failed') window.__dshSuperinputToast?.('保存失败')
|
|
6914
|
+
}
|
|
6915
|
+
setEditor(null)
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
const dialogItem = dialog ? selectionItems.get(dialog.id) : null
|
|
6919
|
+
const editorNode = editor ? h(SelectionCommentEditor, {
|
|
6920
|
+
key: `comment:${editor.id ?? 'new'}:${editor.text}`,
|
|
6921
|
+
draft: editor,
|
|
6922
|
+
onSave: saveEditorComment,
|
|
6923
|
+
onCancel: () => setEditor(null),
|
|
6924
|
+
}) : null
|
|
6925
|
+
const detailNode = dialogItem ? createPortal(h(SelectionDetail, {
|
|
6926
|
+
title: `${dialogItem.seq}. ${previewOf(dialogItem.text, 48)}`,
|
|
6927
|
+
text: dialogItem.text,
|
|
6928
|
+
comment: dialogItem.comment,
|
|
6929
|
+
onNavigate: dialogItem.anchor ? () => navigateTo(dialogItem) : null,
|
|
6930
|
+
onClose: () => setDialog(null),
|
|
6931
|
+
}), document.body) : null
|
|
6932
|
+
if (!editorNode && !detailNode) return null
|
|
6933
|
+
return h(React.Fragment, null, editorNode, detailNode)
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
/** 消息正文的选段 chip:user-text 规则把 token 换成同款标签(Trae 消息里的
|
|
6937
|
+
* mention chip 同义)。刷新后条目由 sessionStorage 恢复。 */
|
|
6938
|
+
const selectionUserTextRule = {
|
|
6939
|
+
id: SELECTION_RULE,
|
|
6940
|
+
order: 5,
|
|
6941
|
+
match(text, from) {
|
|
6942
|
+
const re = /\[选段#([0-9a-z]+)\]/g
|
|
6943
|
+
re.lastIndex = from
|
|
6944
|
+
const found = re.exec(text)
|
|
6945
|
+
if (!found) return undefined
|
|
6946
|
+
const item = selectionItems.get(found[1])
|
|
6947
|
+
if (!item) return undefined
|
|
6948
|
+
return {
|
|
6949
|
+
id: SELECTION_RULE,
|
|
6950
|
+
start: found.index,
|
|
6951
|
+
end: found.index + found[0].length,
|
|
6952
|
+
data: { id: item.id, seq: item.seq, text: item.text, comment: item.comment },
|
|
6953
|
+
}
|
|
6954
|
+
},
|
|
6955
|
+
}
|
|
6956
|
+
|
|
6957
|
+
function SelectionMessageChip({ node, interactive }) {
|
|
6958
|
+
const data = node?.data ?? {}
|
|
6959
|
+
const [open, setOpen] = React.useState(false)
|
|
6960
|
+
const label = chipTextOf(data.seq, data.text, data.comment)
|
|
6961
|
+
return h(React.Fragment, null,
|
|
6962
|
+
h('span', {
|
|
6963
|
+
className: `dsh-si-msgchip${interactive ? ' interactive' : ''}`,
|
|
6964
|
+
title: data.text,
|
|
6965
|
+
onClick: interactive ? () => setOpen(true) : undefined,
|
|
6966
|
+
},
|
|
6967
|
+
h('span', { className: 'dsh-si-msgchip-icon', 'aria-hidden': true }),
|
|
6968
|
+
h('span', { className: 'dsh-si-msgchip-label' }, label)),
|
|
6969
|
+
open ? createPortal(h(SelectionDetail, {
|
|
6970
|
+
title: label,
|
|
6971
|
+
text: data.text,
|
|
6972
|
+
comment: data.comment,
|
|
6973
|
+
onClose: () => setOpen(false),
|
|
6974
|
+
}), document.body) : null)
|
|
6975
|
+
}
|
|
6976
|
+
|
|
6977
|
+
/** 草稿桥:划选「添加评论」、模板插入、/ 命令插入、横条「引用文本」共用。
|
|
6978
|
+
* 挂 conversation.input.dock——该槽位会注入 useInput / inputActions;
|
|
6979
|
+
* composer.dock 不注入这些 props。片段模块移除时桥被一并删掉,这里补回。 */
|
|
6980
|
+
function PromptsDraftBridge({ useInput, inputActions }) {
|
|
6981
|
+
const draft = useInput((state) => state?.draft)
|
|
6982
|
+
React.useEffect(() => {
|
|
6983
|
+
window.__dshSuperinputDraft = () => (typeof draft === 'string' ? draft : '')
|
|
6984
|
+
if (typeof inputActions?.setDraft === 'function') {
|
|
6985
|
+
window.__dshSuperinputSetDraft = (value) => {
|
|
6986
|
+
try { inputActions.setDraft(value) } catch { /* ignore */ }
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
// 官方「引用文本」同款 API(0.2.2-rc.1 起可用):plain 模式下插 blockquote
|
|
6990
|
+
window.__dshSuperinputInsertQuote = (text) => {
|
|
6991
|
+
try { return typeof inputActions?.insertQuote === 'function' && inputActions.insertQuote(text) === true } catch { return false }
|
|
6992
|
+
}
|
|
6993
|
+
}, [draft, inputActions])
|
|
6994
|
+
return null
|
|
6995
|
+
}
|
|
6996
|
+
|
|
4957
6997
|
function PromptsButton({ onSyncSlash }) {
|
|
4958
6998
|
const [open, setOpen] = React.useState(false)
|
|
4959
6999
|
const ref = React.useRef(null)
|
|
@@ -4967,7 +7007,7 @@ function createPromptsClient(require) {
|
|
|
4967
7007
|
}
|
|
4968
7008
|
|
|
4969
7009
|
return {
|
|
4970
|
-
inject: ['slots'],
|
|
7010
|
+
inject: ['slots', 'sessions', 'uiConversation'],
|
|
4971
7011
|
apply(ctx) {
|
|
4972
7012
|
ensureStyle()
|
|
4973
7013
|
installSelectionSaver()
|
|
@@ -5024,6 +7064,70 @@ function createPromptsClient(require) {
|
|
|
5024
7064
|
id: 'dsh-superinput-prompts-btn',
|
|
5025
7065
|
order: 4,
|
|
5026
7066
|
}, (props) => h(PromptsBoundary, null, h(PromptsButton, { ...props, onSyncSlash: syncSlash }))))
|
|
7067
|
+
|
|
7068
|
+
/* 划选引用的官方 input source:codec 决定 chip 的粘贴文本与送模文本;
|
|
7069
|
+
* candidates 恒空——只是借 @ 触发器的注册位拿到一个合法的自定义
|
|
7070
|
+
* source(官方 chip 插入/序列化按 source 名路由),不往 @ 菜单加条目。 */
|
|
7071
|
+
const selectionSource = {
|
|
7072
|
+
trigger: '@',
|
|
7073
|
+
name: SELECTION_SOURCE,
|
|
7074
|
+
showGroupTitle: false,
|
|
7075
|
+
async candidates() { return [] },
|
|
7076
|
+
onPick() { return undefined },
|
|
7077
|
+
openReference(session, reference) {
|
|
7078
|
+
const targetSession = session?.sessionId === undefined ? '' : String(session.sessionId)
|
|
7079
|
+
const ref = String(reference?.ref ?? '')
|
|
7080
|
+
if (!targetSession || !selectionItems.has(ref)) return false
|
|
7081
|
+
requestSelectionDetail(targetSession, ref, false)
|
|
7082
|
+
return true
|
|
7083
|
+
},
|
|
7084
|
+
codec: {
|
|
7085
|
+
clipboardText: (ref) => tokenOf(ref),
|
|
7086
|
+
serialize: async (ref) => tokenOf(ref),
|
|
7087
|
+
},
|
|
7088
|
+
}
|
|
7089
|
+
ctx.inject(['inputTriggers'], (scope) => {
|
|
7090
|
+
try {
|
|
7091
|
+
const service = scope.get('inputTriggers')
|
|
7092
|
+
ctx.effect(() => service.registerSource(selectionSource), 'dsh-superinput: selection source')
|
|
7093
|
+
} catch (error) {
|
|
7094
|
+
report(`selection source register failed: ${error?.message ?? error}`)
|
|
7095
|
+
}
|
|
7096
|
+
})
|
|
7097
|
+
ctx.inject(['sessions'], (scope) => {
|
|
7098
|
+
selectionSessions = scope.get('sessions')
|
|
7099
|
+
})
|
|
7100
|
+
|
|
7101
|
+
// 消息正文里的同款 chip:user-text 规则 + 节点渲染槽位。
|
|
7102
|
+
ctx.inject(['uiConversation'], (scope) => {
|
|
7103
|
+
try {
|
|
7104
|
+
const ui = scope.get('uiConversation')
|
|
7105
|
+
if (typeof ui?.userText?.register === 'function') {
|
|
7106
|
+
ctx.effect(() => ui.userText.register(selectionUserTextRule), 'dsh-superinput: selection user-text rule')
|
|
7107
|
+
}
|
|
7108
|
+
} catch (error) {
|
|
7109
|
+
report(`selection user-text rule failed: ${error?.message ?? error}`)
|
|
7110
|
+
}
|
|
7111
|
+
})
|
|
7112
|
+
ctx.slots.inject('conversation.user.text.node', () => ctx.slots.register({
|
|
7113
|
+
name: 'conversation.user.text.node',
|
|
7114
|
+
key: SELECTION_RULE,
|
|
7115
|
+
priority: 20,
|
|
7116
|
+
}, SelectionMessageChip))
|
|
7117
|
+
|
|
7118
|
+
// 草稿桥挂 input.dock(该槽位注入 useInput / inputActions)
|
|
7119
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
7120
|
+
name: 'conversation.input.dock',
|
|
7121
|
+
id: 'dsh-superinput-prompts-bridge',
|
|
7122
|
+
order: 96,
|
|
7123
|
+
}, PromptsDraftBridge))
|
|
7124
|
+
|
|
7125
|
+
// 划选引用桥:chip 已内联在编辑器里,这个槽位只提供入口与详情弹窗。
|
|
7126
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
7127
|
+
name: 'conversation.input.dock',
|
|
7128
|
+
id: 'dsh-superinput-selection-context',
|
|
7129
|
+
order: 97,
|
|
7130
|
+
}, (props) => h(PromptsBoundary, null, h(SelectionContextBridge, props))))
|
|
5027
7131
|
},
|
|
5028
7132
|
}
|
|
5029
7133
|
}
|
|
@@ -5258,6 +7362,7 @@ window.__ModuleLoader__.load({
|
|
|
5258
7362
|
['followup', createFollowupClient],
|
|
5259
7363
|
['voice', createVoiceClient],
|
|
5260
7364
|
['prompts', createPromptsClient],
|
|
7365
|
+
['compaction', createCompactionClient],
|
|
5261
7366
|
['hub', createHubClient],
|
|
5262
7367
|
]
|
|
5263
7368
|
// 模块异常上报:console 不落盘,失败必须走 prompts /diag 留痕。
|