dsh-bottom-info-bar 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +975 -114
- package/lib/constants.js +69 -0
- package/lib/index.js +1259 -181
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -25,6 +25,12 @@ const HIDE_SPEED_FIELDS = true;
|
|
|
25
25
|
// RPC 超时兜底:host 侧 15s 超时之上再留余量;端点挂起时 20s 内必失败,杜绝永久"加载中…"
|
|
26
26
|
const RPC_TIMEOUT_MS = 20000;
|
|
27
27
|
|
|
28
|
+
// 首启强制刷新窗口:页面刚打开/用户手动刷新后的这几秒内,快照请求带 force=true,
|
|
29
|
+
// host 会绕过缓存与失败退避当场重查服务商——用户刷新页面立即看到最新余额/额度,
|
|
30
|
+
// 不必干等后台 60s 自动周期。覆盖窗口需容纳会话模型从空到确定的短暂翻转期。
|
|
31
|
+
const BOOT_AT = Date.now();
|
|
32
|
+
const FORCE_REFRESH_WINDOW_MS = 6000;
|
|
33
|
+
|
|
28
34
|
// rpc(method, args, externalSignal):
|
|
29
35
|
// - 超时:20s 未响应 → abort 并以"请求超时"失败(fetch 挂起不阻塞界面)
|
|
30
36
|
// - 可中止:传入外部 AbortSignal(组件卸载时 abort)→ 立即取消并拒绝"请求已取消"
|
|
@@ -88,6 +94,256 @@ function mergeLoadResults(prev, results) {
|
|
|
88
94
|
return next;
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
// ---------- v1.9.0 PR2:字段显隐/颜色配置(宿主落盘,设置页变更后经 CustomEvent 即时同步) ----------
|
|
98
|
+
// 字段注册表/预设色板由构建从 constants.js 注入(单一来源,宿主白名单同源)
|
|
99
|
+
const FIELD_REGISTRY = [
|
|
100
|
+
// 插件字段 · 服务锚点(D6:与其他字段同等可隐藏)
|
|
101
|
+
{ id: 'anchorGroup', label: '服务商与模型', group: 'plugin', modes: ['balance'], anchor: true, colorKind: 'provider', note: '当前对话使用的服务商与模型名,信息栏的身份锚点' },
|
|
102
|
+
{ id: 'subServiceGroup', label: '订阅服务与模型', group: 'plugin', modes: ['subscription'], anchor: true, colorKind: 'provider', note: '订阅服务名(ChatGPT/Codex/OpenCode Go/智谱/小米 MiMo)与模型或套餐档位' },
|
|
103
|
+
{ id: 'billingServiceGroup', label: '账单服务与模型', group: 'plugin', modes: ['billing'], anchor: true, colorKind: 'provider', note: '云账单服务名(Together/Fireworks/AWS Bedrock/Cloudflare)与模型名' },
|
|
104
|
+
// 插件字段 · 通用(多模式共用)
|
|
105
|
+
{ id: 'sessionCost', label: '本会话花费', group: 'plugin', modes: ['balance', 'subscription'], colorKind: 'inherit', note: '当前会话(含子代理)的真实花费;新会话尚无记账时显示 ¥0.000,悬停可看今天/近一月/全部' },
|
|
106
|
+
// 插件字段 · 余额制
|
|
107
|
+
{ id: 'balance', label: '余额', group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: '服务商账户的真实余额;低余额时数字变红并带「低」字' },
|
|
108
|
+
{ id: 'period', label: '定价时段', group: 'plugin', modes: ['balance'], colorKind: 'period', note: '仅峰谷价模型显示(当前为 DeepSeek 系列):高峰价红色 / 空闲价绿色' },
|
|
109
|
+
{ id: 'countdown', label: '时段切换倒计时', group: 'plugin', modes: ['balance'], colorKind: 'inherit', note: '仅峰谷价模型显示:距空闲/距高峰的小时计倒计时' },
|
|
110
|
+
// 插件字段 · 订阅制
|
|
111
|
+
{ id: 'expiry', label: '订阅到期日', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '订阅登录凭据中带有到期信息时显示(如 Codex 套餐)' },
|
|
112
|
+
{ id: 'subWindow5h', label: '5 小时额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '5 小时滚动额度剩余百分比;简洁模式下仅显示时间最短的可用窗口' },
|
|
113
|
+
{ id: 'subWindowWeek', label: '周额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '每周额度剩余百分比;完整模式显示,简洁模式让位给更短窗口' },
|
|
114
|
+
{ id: 'subWindowMonth', label: '月额度窗口', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '每月额度剩余百分比;完整模式显示,简洁模式让位给更短窗口' },
|
|
115
|
+
{ id: 'resetCountdown', label: '额度重置倒计时', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '当前显示的额度窗口距重置的倒计时' },
|
|
116
|
+
{ id: 'subBalance', label: '充值余额', group: 'plugin', modes: ['subscription'], colorKind: 'inherit', note: '订阅源为充值余额形态时显示(如智谱按量账户),与额度窗口互斥' },
|
|
117
|
+
// 插件字段 · 账单制
|
|
118
|
+
{ id: 'billingSpend', label: '本月花费/用量', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '云账单当前计费周期的真实花费或用量' },
|
|
119
|
+
{ id: 'budget', label: '预算使用', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '仅提供预算查询的服务商显示(当前为 AWS Bedrock)' },
|
|
120
|
+
{ id: 'freeQuota', label: '免费额度与重置', group: 'plugin', modes: ['billing'], colorKind: 'inherit', note: '接口显式给出每日免费额度与重置时刻时才显示,绝不编造' },
|
|
121
|
+
// 原生字段(DeepSeek 原生底部栏原有;完整模式独占可见)
|
|
122
|
+
{ id: 'turnsSteps', label: '轮次与步数', group: 'native', modes: ['native'], colorKind: 'inherit', note: '「N 轮 · M 步」对话进度统计' },
|
|
123
|
+
{ id: 'llmTime', label: 'LLM 耗时', group: 'native', modes: ['native'], colorKind: 'inherit', note: '模型推理累计耗时' },
|
|
124
|
+
{ id: 'toolTime', label: '工具调用耗时', group: 'native', modes: ['native'], colorKind: 'inherit', note: '工具调用累计耗时' },
|
|
125
|
+
{ id: 'cacheHit', label: '缓存命中', group: 'native', modes: ['native'], colorKind: 'inherit', note: '提示词缓存命中率百分比' },
|
|
126
|
+
{ id: 'tokensIO', label: '输入/输出 tokens', group: 'native', modes: ['native'], colorKind: 'inherit', note: '「输入 X tok · 输出 Y tok」本次会话累计用量' },
|
|
127
|
+
// 插件字段 · 状态与提醒(建议保留)
|
|
128
|
+
{ id: 'unmapped', label: '未适配提示', group: 'plugin', modes: ['balance'], colorKind: 'muted', note: '当前服务商暂无余额查询适配时出现的弱提示' },
|
|
129
|
+
{ id: 'noKeyHint', label: '未配置凭据提示', group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: '缺少 API Key 时的设置引导文案' },
|
|
130
|
+
{ id: 'balanceError', label: '余额获取/刷新失败提示', group: 'plugin', modes: ['balance'], suggestKeep: true, colorKind: 'alert', note: '余额查询失败或正在显示上次数据时的提示' },
|
|
131
|
+
{ id: 'usageError', label: '花费获取失败提示', group: 'plugin', modes: ['balance', 'subscription'], suggestKeep: true, colorKind: 'alert', note: '花费数据暂时不可用时的提示' },
|
|
132
|
+
{ id: 'refreshFailure', label: '刷新失败提示', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: '任一数据源降级时在行尾合并显示的一个提醒(多个来源自动去重)' },
|
|
133
|
+
{ id: 'persistWarning', label: '账单未保存提醒', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: '账本落盘异常时出现;隐藏后金额可能悄悄不准,建议保留' },
|
|
134
|
+
{ id: 'updateNotice', label: '新版本提醒', group: 'plugin', modes: ['balance', 'subscription', 'billing'], suggestKeep: true, colorKind: 'alert', note: 'npm 上有新版本时提醒更新' },
|
|
135
|
+
];
|
|
136
|
+
const PRESET_COLORS = ['red', 'green', 'blue', 'purple', 'orange', 'neutral'];
|
|
137
|
+
const PRESET_COLOR_SET = new Set(PRESET_COLORS);
|
|
138
|
+
|
|
139
|
+
let fieldConfig = { fields: {}, colors: {} };
|
|
140
|
+
let fieldConfigVersion = 0;
|
|
141
|
+
let fieldConfigServerVersion = -1; // 宿主 configVersion(-1=尚未取得);过期响应据此丢弃(D3)
|
|
142
|
+
const fieldConfigListeners = new Set();
|
|
143
|
+
let fieldConfigInFlight = false;
|
|
144
|
+
let fieldConfigRefetchPending = false; // 在途期间收到刷新请求(如设置页 CustomEvent)→ 当前响应落地后立即补拉(D3)
|
|
145
|
+
|
|
146
|
+
function applyFieldConfigSnapshot(next) {
|
|
147
|
+
fieldConfig = {
|
|
148
|
+
fields: next && next.fields && typeof next.fields === 'object' ? next.fields : {},
|
|
149
|
+
colors: next && next.colors && typeof next.colors === 'object' ? next.colors : {},
|
|
150
|
+
};
|
|
151
|
+
fieldConfigVersion += 1;
|
|
152
|
+
fieldConfigListeners.forEach(function (listener) { listener(); });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 版本守卫(纯函数,供单测):只有严格更新的快照才应用。
|
|
156
|
+
// - incoming 缺失(旧宿主无 configVersion)→ 接受,兼容不阻断;
|
|
157
|
+
// - 尚未应用过任何快照(applied < 0)→ 接受;
|
|
158
|
+
// - 相同版本不重复应用(宿主每次变更必 +1,同版本即同内容,省去 30s 轮询的无谓重渲染);
|
|
159
|
+
// - 过期响应(incoming < applied)→ 拒绝,绝不回退用户刚保存的配置。
|
|
160
|
+
// 注:宿主重启必然伴随 dsh web 重启与页面重载,客户端版本号随之重置,不存在版本回退场景。
|
|
161
|
+
function fieldConfigSnapshotIsNewer(incomingVersion, appliedVersion) {
|
|
162
|
+
if (typeof incomingVersion !== 'number') return true;
|
|
163
|
+
if (!(appliedVersion >= 0)) return true;
|
|
164
|
+
return incomingVersion > appliedVersion;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 宿主把配置常驻内存缓存,拉取即回;在途去重避免 30s 轮询叠加请求。
|
|
168
|
+
// D3:在途期间的新刷新请求(设置页 CustomEvent)记为 pending,当前响应落地后立即补拉,
|
|
169
|
+
// 配合版本守卫——过期响应直接丢弃——保证刚保存的配置最迟一次往返内生效,绝不被旧响应覆盖。
|
|
170
|
+
function refreshFieldConfig() {
|
|
171
|
+
if (fieldConfigInFlight) { fieldConfigRefetchPending = true; return; }
|
|
172
|
+
fieldConfigInFlight = true;
|
|
173
|
+
rpc('getFieldConfig').then(function (cfg) {
|
|
174
|
+
fieldConfigInFlight = false;
|
|
175
|
+
if (cfg && typeof cfg === 'object') {
|
|
176
|
+
const incoming = typeof cfg.configVersion === 'number' ? cfg.configVersion : null;
|
|
177
|
+
if (fieldConfigSnapshotIsNewer(incoming, fieldConfigServerVersion)) {
|
|
178
|
+
if (incoming !== null) fieldConfigServerVersion = incoming;
|
|
179
|
+
applyFieldConfigSnapshot(cfg);
|
|
180
|
+
}
|
|
181
|
+
// 过期/相同版本响应:直接丢弃,不触发重渲染
|
|
182
|
+
}
|
|
183
|
+
if (fieldConfigRefetchPending) { fieldConfigRefetchPending = false; refreshFieldConfig(); }
|
|
184
|
+
}).catch(function () {
|
|
185
|
+
fieldConfigInFlight = false;
|
|
186
|
+
if (fieldConfigRefetchPending) { fieldConfigRefetchPending = false; refreshFieldConfig(); }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 未知/缺省 id 一律视为显示:与历史行为一致(默认值全部=显示),前向兼容新字段
|
|
191
|
+
function fieldVisible(id) {
|
|
192
|
+
return fieldConfig.fields[id] !== false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function fieldColor(id) {
|
|
196
|
+
const value = fieldConfig.colors[id];
|
|
197
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 深色主题下把自定义 hex 向白色混合 45%,避免深底上不可读;预设色名走三套主题变量,无需处理
|
|
201
|
+
function readableDarkVariant(hex) {
|
|
202
|
+
const value = parseInt(hex.slice(1), 16);
|
|
203
|
+
function mix(channel) { return Math.round(channel + (255 - channel) * 0.45); }
|
|
204
|
+
const r = mix((value >> 16) & 255);
|
|
205
|
+
const g = mix((value >> 8) & 255);
|
|
206
|
+
const b = mix(value & 255);
|
|
207
|
+
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// WCAG 相对亮度(对比度计算用,纯函数供单测)
|
|
211
|
+
function hexLuminance(value) {
|
|
212
|
+
function linear(channel) {
|
|
213
|
+
const c = channel / 255;
|
|
214
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
215
|
+
}
|
|
216
|
+
return 0.2126 * linear((value >> 16) & 255) + 0.7152 * linear((value >> 8) & 255) + 0.0722 * linear(value & 255);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 浅色主题钳制(D2):自定义 hex 对白底对比度 < 4.5:1 时逐级向黑混合(每步 5%,至多 95%),
|
|
220
|
+
// 直到可读;本就可读的颜色原样返回(大写规范化)。深浅两套变量由此始终各自可读。
|
|
221
|
+
function readableLightVariant(hex) {
|
|
222
|
+
const value = parseInt(hex.slice(1), 16);
|
|
223
|
+
if (1.05 / (hexLuminance(value) + 0.05) >= 4.5) return hex.toUpperCase();
|
|
224
|
+
for (let step = 1; step <= 19; step++) {
|
|
225
|
+
const keep = 1 - step * 0.05;
|
|
226
|
+
const r = Math.round(((value >> 16) & 255) * keep);
|
|
227
|
+
const g = Math.round(((value >> 8) & 255) * keep);
|
|
228
|
+
const b = Math.round((value & 255) * keep);
|
|
229
|
+
const candidate = (r << 16) | (g << 8) | b;
|
|
230
|
+
if (step === 19 || 1.05 / (hexLuminance(candidate) + 0.05) >= 4.5) {
|
|
231
|
+
return '#' + candidate.toString(16).padStart(6, '0').toUpperCase();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return hex.toUpperCase();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 字段已自定义颜色时返回要注入的 CSS 变量;未自定义返回 undefined → 变量不存在 → 完全沿用现有颜色(零回归)
|
|
238
|
+
// 浅色默认层取浅色钳制变体(--bi-field-<id>),深色覆盖层取向白加亮变体(--bi-field-<id>-dark)
|
|
239
|
+
function fieldStyle(id) {
|
|
240
|
+
const color = fieldColor(id);
|
|
241
|
+
if (!color) return undefined;
|
|
242
|
+
const style = {};
|
|
243
|
+
if (PRESET_COLOR_SET.has(color)) {
|
|
244
|
+
style['--bi-field-' + id] = 'var(--bi-palette-' + color + ')';
|
|
245
|
+
return style;
|
|
246
|
+
}
|
|
247
|
+
style['--bi-field-' + id] = readableLightVariant(color);
|
|
248
|
+
style['--bi-field-' + id + '-dark'] = readableDarkVariant(color);
|
|
249
|
+
return style;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 字段级颜色 CSS 生成:回退值=各字段原语义色,未自定义时渲染结果与旧版一致。
|
|
253
|
+
// 深色主题规则用更高优先级选择器优先取 hex 加亮变体(--bi-field-<id>-dark;预设色名无该变量时自然回落)。
|
|
254
|
+
function buildFieldColorCss() {
|
|
255
|
+
const rules = [];
|
|
256
|
+
for (let i = 0; i < FIELD_REGISTRY.length; i++) {
|
|
257
|
+
const field = FIELD_REGISTRY[i];
|
|
258
|
+
const id = field.id;
|
|
259
|
+
const kind = field.colorKind;
|
|
260
|
+
const attr = '[data-field="' + id + '"]';
|
|
261
|
+
let pairs;
|
|
262
|
+
if (kind === 'alert') {
|
|
263
|
+
pairs = [
|
|
264
|
+
['.bi-root ' + attr + '.bi-err', 'var(--bi-state-alert)'],
|
|
265
|
+
['.bi-root ' + attr + '.bi-stale', 'var(--bi-state-alert)'],
|
|
266
|
+
['.bi-root ' + attr + '.bi-update', 'var(--bi-state-alert)'],
|
|
267
|
+
];
|
|
268
|
+
} else if (kind === 'period') {
|
|
269
|
+
pairs = [
|
|
270
|
+
['.bi-root ' + attr + '.bi-peak', 'var(--bi-state-alert)'],
|
|
271
|
+
['.bi-root ' + attr + '.bi-offpeak', 'var(--bi-state-price-low)'],
|
|
272
|
+
];
|
|
273
|
+
} else if (kind === 'provider') {
|
|
274
|
+
pairs = [
|
|
275
|
+
['.bi-root ' + attr, 'inherit'],
|
|
276
|
+
['.bi-root ' + attr + ' .bi-model-provider', 'var(--bi-label-primary)'],
|
|
277
|
+
];
|
|
278
|
+
} else if (kind === 'muted') {
|
|
279
|
+
pairs = [['.bi-root ' + attr, 'var(--bi-label-supporting)']];
|
|
280
|
+
} else {
|
|
281
|
+
pairs = [['.bi-root ' + attr, 'inherit']];
|
|
282
|
+
}
|
|
283
|
+
for (let j = 0; j < pairs.length; j++) {
|
|
284
|
+
const selector = pairs[j][0];
|
|
285
|
+
const fallback = pairs[j][1];
|
|
286
|
+
const light = 'var(--bi-field-' + id + ', ' + fallback + ')';
|
|
287
|
+
const dark = 'var(--bi-field-' + id + '-dark, ' + light + ')';
|
|
288
|
+
rules.push(selector + ' { color: ' + light + '; }');
|
|
289
|
+
rules.push('body[data-ds-dark-theme] ' + selector + ' { color: ' + dark + '; }');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return rules.join('\n');
|
|
293
|
+
}
|
|
294
|
+
const FIELD_COLOR_CSS = buildFieldColorCss();
|
|
295
|
+
// 订阅窗口 key → 字段 id(简洁模式只显示优先窗口的既有逻辑保持不变,只叠加显隐过滤)
|
|
296
|
+
const WINDOW_FIELD_IDS = { five_hour: 'subWindow5h', seven_day: 'subWindowWeek', monthly: 'subWindowMonth' };
|
|
297
|
+
function windowFieldVisible(key) {
|
|
298
|
+
const id = WINDOW_FIELD_IDS[key];
|
|
299
|
+
return id ? fieldVisible(id) : true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 降级节点现在包着 data-field 容器(fieldSpan 着色),文案要穿透一层包装再读
|
|
303
|
+
function trailingErrorText(node) {
|
|
304
|
+
let current = node;
|
|
305
|
+
for (let depth = 0; depth < 3 && current && current.props; depth++) {
|
|
306
|
+
const child = current.props.children;
|
|
307
|
+
if (typeof child === 'string') return child;
|
|
308
|
+
current = child;
|
|
309
|
+
}
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 行组装(模块级纯函数,createElement 注入以便单测用桩验证分隔符收合与全隐藏零输出):
|
|
314
|
+
// 居中组之间插一个分隔符;尾部错误组多来源「刷新失败」去重后逐个接在右侧,
|
|
315
|
+
// 分隔符只在已有内容之后出现——全空输入返回空数组(D6:零节点/零分隔符)。
|
|
316
|
+
function assembleInfoBarRow(groups, trailingErrorGroups, createElement) {
|
|
317
|
+
const nodes = [];
|
|
318
|
+
for (let i = 0; i < groups.length; i++) {
|
|
319
|
+
if (i > 0) nodes.push(createElement('span', { key: 'sep' + i, className: 'bi-sep' }, '|'));
|
|
320
|
+
nodes.push(createElement('span', { key: 'g' + i }, groups[i]));
|
|
321
|
+
}
|
|
322
|
+
const seenRefreshFailure = { value: false };
|
|
323
|
+
const visibleErrors = trailingErrorGroups.filter(function (node) {
|
|
324
|
+
const text = trailingErrorText(node);
|
|
325
|
+
if (text !== '刷新失败') return true;
|
|
326
|
+
if (seenRefreshFailure.value) return false;
|
|
327
|
+
seenRefreshFailure.value = true;
|
|
328
|
+
return true;
|
|
329
|
+
});
|
|
330
|
+
for (let i = 0; i < visibleErrors.length; i++) {
|
|
331
|
+
if (groups.length > 0 || i > 0) nodes.push(createElement('span', { key: 'errsep' + i, className: 'bi-sep' }, '|'));
|
|
332
|
+
nodes.push(createElement('span', { key: 'err' + i }, visibleErrors[i]));
|
|
333
|
+
}
|
|
334
|
+
return nodes;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// D6 用户拍板:全部字段隐藏 = 底栏彻底移除。此判定为纯函数供单测:
|
|
338
|
+
// 注册表内没有任何可见字段,或渲染结果(原生行/主行/错误组)全空 → 信息栏整体不渲染,
|
|
339
|
+
// 不留空行、占位高度或悬空分隔符;density 点击因无 DOM 而天然无副作用。
|
|
340
|
+
function infoBarShouldRemoveAll(registry, isVisible) {
|
|
341
|
+
for (let i = 0; i < registry.length; i++) {
|
|
342
|
+
if (isVisible(registry[i].id)) return false;
|
|
343
|
+
}
|
|
344
|
+
return registry.length > 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
91
347
|
function installStyles() {
|
|
92
348
|
const id = 'dsh-bottom-info-bar';
|
|
93
349
|
const existing = document.querySelector('style[data-plugin-css="' + id + '"]');
|
|
@@ -146,6 +402,12 @@ function installStyles() {
|
|
|
146
402
|
.bi-model-capability-pending { visibility: hidden; }
|
|
147
403
|
/* 读屏说明不参与视觉排版。 */
|
|
148
404
|
.bi-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
405
|
+
/* v1.9.0 PR2 预设色板:浅色默认 → 深色覆盖 → 增强对比三套成对(与现有语义色同一套规则);设置页共用 .bib-set-root 取同一色板 */
|
|
406
|
+
.bi-root, .bib-set-root { --bi-palette-red: #d92d20; --bi-palette-green: #087f5b; --bi-palette-blue: #0044cc; --bi-palette-purple: #6941c6; --bi-palette-orange: #b54708; --bi-palette-neutral: var(--dsw-alias-label-primary, var(--bi-label-primary, #333)); }
|
|
407
|
+
body[data-ds-dark-theme] .bi-root, body[data-ds-dark-theme] .bib-set-root { --bi-palette-red: #ff6961; --bi-palette-green: #86efac; --bi-palette-blue: #66a3ff; --bi-palette-purple: #b19cf7; --bi-palette-orange: #fdb022; }
|
|
408
|
+
@media (prefers-contrast: more) { body:not([data-ds-dark-theme]) .bi-root, body:not([data-ds-dark-theme]) .bib-set-root { --bi-palette-red: #ad1717; --bi-palette-green: #05603a; --bi-palette-blue: #003399; --bi-palette-purple: #4a1fb8; --bi-palette-orange: #7a2e0e; } body[data-ds-dark-theme] .bi-root, body[data-ds-dark-theme] .bib-set-root { --bi-palette-red: #ff7770; --bi-palette-blue: #80b3ff; --bi-palette-purple: #c9b8ff; --bi-palette-orange: #ffcc80; } }
|
|
409
|
+
/* 字段级颜色消费规则由注册表生成,拼接在样式表顶层(任何环境生效)——严禁并入上方 @media 块(D1 回归警戒) */
|
|
410
|
+
` + FIELD_COLOR_CSS + `
|
|
149
411
|
`;
|
|
150
412
|
document.head.appendChild(style);
|
|
151
413
|
return function () { style.remove(); };
|
|
@@ -235,6 +497,16 @@ module.exports = {
|
|
|
235
497
|
}
|
|
236
498
|
} catch (err) { /* 默认完整 */ }
|
|
237
499
|
|
|
500
|
+
// v1.9.0 PR2:字段配置与密度同型——启动拉取一次;设置页保存成功后派发 CustomEvent 即时同步;
|
|
501
|
+
// load() 周期顺带校准(宿主常驻内存缓存,拉取即回)
|
|
502
|
+
refreshFieldConfig();
|
|
503
|
+
ctx.effect(function () {
|
|
504
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined;
|
|
505
|
+
const onConfigChanged = function () { refreshFieldConfig(); };
|
|
506
|
+
document.addEventListener('dsh-bib-config-changed', onConfigChanged);
|
|
507
|
+
return function () { document.removeEventListener('dsh-bib-config-changed', onConfigChanged); };
|
|
508
|
+
}, 'dsh-bottom-info-bar: field config sync');
|
|
509
|
+
|
|
238
510
|
// ---------- 组件 ----------
|
|
239
511
|
function BottomInfoBar(props) {
|
|
240
512
|
// 原生/会话投影(hooks 无条件调用)
|
|
@@ -268,6 +540,17 @@ module.exports = {
|
|
|
268
540
|
};
|
|
269
541
|
}, []);
|
|
270
542
|
|
|
543
|
+
// v1.9.0 PR2:字段配置订阅——设置页保存(CustomEvent)或周期校准更新配置时重渲染。
|
|
544
|
+
// 配置本体存模块级单例,组件只记版本号;订阅建立时立即回读避免首帧用过期配置。
|
|
545
|
+
const [fieldConfigTick, setFieldConfigTick] = React.useState(fieldConfigVersion);
|
|
546
|
+
React.useEffect(function () {
|
|
547
|
+
const listener = function () { setFieldConfigTick(fieldConfigVersion); };
|
|
548
|
+
fieldConfigListeners.add(listener);
|
|
549
|
+
setFieldConfigTick(fieldConfigVersion);
|
|
550
|
+
return function () { fieldConfigListeners.delete(listener); };
|
|
551
|
+
}, []);
|
|
552
|
+
void fieldConfigTick;
|
|
553
|
+
|
|
271
554
|
// 当前会话 ID 多路获取:slotProps 标准 kit → session 快照 → 运行时 sessions 服务
|
|
272
555
|
// (DSH 各版本注入方式不同,任一路可用即拿到真实会话 ID,避免回退到上一会话的账)
|
|
273
556
|
const propsRef = React.useRef(props);
|
|
@@ -340,14 +623,19 @@ module.exports = {
|
|
|
340
623
|
const cachedSessionModel = sessionId ? sessionModelCache.get(sessionId) : null;
|
|
341
624
|
const activeSessionModel = sessionModel && sessionModel.sessionId === sessionId ? sessionModel : (cachedSessionModel || null);
|
|
342
625
|
const load = React.useCallback(function (selection) {
|
|
626
|
+
// v1.9.0 PR2:周期顺带校准字段配置(宿主内存缓存,即回;设置页变更另有 CustomEvent 即时通道)
|
|
627
|
+
refreshFieldConfig();
|
|
343
628
|
const requestVersion = ++loadVersionRef.current;
|
|
344
629
|
const activeSelection = selection || activeSessionModel;
|
|
345
630
|
const selectionArgs = activeSelection ? { selection: { provider: activeSelection.provider, model: activeSelection.model } } : {};
|
|
631
|
+
// 首启窗口内(打开/刷新网页头几秒)→ 快照类请求强制重查;之后的 30s 周期轮询走常规缓存节奏
|
|
632
|
+
const force = Date.now() - BOOT_AT < FORCE_REFRESH_WINDOW_MS;
|
|
633
|
+
if (force) { selectionArgs.force = true; }
|
|
346
634
|
const signal = abortRef.current ? abortRef.current.signal : null;
|
|
347
635
|
// 逐接口容错:allSettled 等全部 settle(最坏 20s 超时兜底),任一失败只降级该端点,
|
|
348
636
|
// 不拖垮其他成功数据;合并逻辑在 mergeLoadResults(失败端点保留旧值 + 记录错误)
|
|
349
637
|
Promise.allSettled([
|
|
350
|
-
rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider } : null, signal),
|
|
638
|
+
rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider, force: force } : (force ? { force: true } : null), signal),
|
|
351
639
|
rpc('getPricing', selectionArgs, signal),
|
|
352
640
|
rpc('getUsageSummary', Object.assign({ sessionId: sessionId }, selectionArgs), signal),
|
|
353
641
|
rpc('getBillingMode', selectionArgs, signal),
|
|
@@ -598,91 +886,125 @@ module.exports = {
|
|
|
598
886
|
);
|
|
599
887
|
}
|
|
600
888
|
|
|
889
|
+
// 字段包装:带 data-field 与(可选)颜色变量的容器 span,供 CSS 按字段着色(未自定义时变量不存在,零回归)
|
|
890
|
+
function fieldSpan(id, key, children) {
|
|
891
|
+
return React.createElement('span', { key: key, 'data-field': id, style: fieldStyle(id) }, children);
|
|
892
|
+
}
|
|
893
|
+
|
|
601
894
|
// ---- 余额制模式(v1.0.0 现状,完全不动):服务商+模型 → 余额 → 时段 → 倒计时 → 本会话花费 ----
|
|
895
|
+
// v1.9.0 PR2:每个渲染片段按设置过滤(fieldVisible);隐藏不占位,组间分隔符由组装层自动收合
|
|
602
896
|
function pushBalanceGroups(groups, trailingErrorGroups) {
|
|
603
897
|
const bal = state.balance;
|
|
604
898
|
const errors = state.errors || {};
|
|
605
899
|
const alertActive = !!(bal && bal.alert && bal.alert.active);
|
|
606
|
-
|
|
900
|
+
if (fieldVisible('anchorGroup')) {
|
|
901
|
+
const anchor = providerGroup();
|
|
902
|
+
groups.push(React.cloneElement(anchor, { 'data-field': 'anchorGroup', style: fieldStyle('anchorGroup') }));
|
|
903
|
+
}
|
|
607
904
|
|
|
608
905
|
// v1.6 T7:未适配账户渲染"未适配"弱提示
|
|
609
906
|
if (bal && bal.unmapped) {
|
|
610
|
-
|
|
907
|
+
if (fieldVisible('unmapped')) {
|
|
908
|
+
trailingErrorGroups.push(fieldSpan('unmapped', 'unmapped',
|
|
909
|
+
React.createElement('span', { className: 'bi-muted', title: '该服务商暂未适配余额查询,后续版本将支持。' }, '未适配')));
|
|
910
|
+
}
|
|
611
911
|
}
|
|
612
912
|
// 余额(纯金额;hover 仅展示余额,不显示充值/赠金)
|
|
613
913
|
// v1.6 T7:未配置提示改为按账户显示凭据名(去掉写死的 DeepSeek 文案)
|
|
614
914
|
else if (bal && bal.error && bal.error.kind === 'no-key') {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
'
|
|
915
|
+
if (fieldVisible('noKeyHint')) {
|
|
916
|
+
const credName = bal.error.message ? String(bal.error.message).replace('未配置 ', '') : 'API_KEY';
|
|
917
|
+
trailingErrorGroups.push(fieldSpan('noKeyHint', 'nokey',
|
|
918
|
+
React.createElement('span', { className: 'bi-err', title: '未配置 ' + credName + '。请在"设置 → 模型"中填写。' },
|
|
919
|
+
'未配置 ' + credName + ' → 设置→模型 填写')));
|
|
920
|
+
}
|
|
618
921
|
} else if (bal && bal.data) {
|
|
619
922
|
const symbol = bal.currency === 'USD' ? '$' : '¥';
|
|
620
923
|
const balTitle = bal.estimate
|
|
621
924
|
? '估算余额:' + symbol + fmt(bal.data.total)
|
|
622
925
|
: '余额:' + symbol + fmt(bal.data.total);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
926
|
+
if (fieldVisible('balance')) {
|
|
927
|
+
groups.push(fieldSpan('balance', 'bal', React.createElement('span', { title: balTitle },
|
|
928
|
+
metric('余额', symbol + fmt(bal.data.total), alertActive ? 'bi-alert-num' : ''),
|
|
929
|
+
alertActive ? React.createElement('span', { className: 'bi-low-status' }, '低') : null,
|
|
930
|
+
bal.estimate ? React.createElement('span', { className: 'bi-muted' }, '(估算)') : null,
|
|
931
|
+
)));
|
|
932
|
+
}
|
|
628
933
|
// host 快照失败(bal.error)或本次 RPC 失败(errors.balance)→ 均保留旧数据 + 降级标记
|
|
629
|
-
if (bal.error || errors.balance) {
|
|
630
|
-
trailingErrorGroups.push(
|
|
934
|
+
if (fieldVisible('balanceError') && (bal.error || errors.balance)) {
|
|
935
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'balerr',
|
|
936
|
+
React.createElement('span', { className: 'bi-stale', title: '余额暂不可用;正在显示上次数据并自动重试。' }, '刷新失败')));
|
|
631
937
|
}
|
|
632
938
|
} else if (bal && bal.error) {
|
|
633
|
-
|
|
939
|
+
if (fieldVisible('balanceError')) {
|
|
940
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
941
|
+
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
942
|
+
}
|
|
634
943
|
} else if (errors.balance) {
|
|
635
944
|
// 本次 RPC 失败且无旧数据:只降级余额块,其余端点数据照常渲染
|
|
636
|
-
|
|
945
|
+
if (fieldVisible('balanceError')) {
|
|
946
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
947
|
+
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
948
|
+
}
|
|
637
949
|
}
|
|
638
950
|
|
|
639
951
|
// 时段:仅峰谷价服务商显示"高峰价/空闲价"(flat/unknown 服务商不显示;hover 展示具体价格)
|
|
640
952
|
const pr = visiblePricing;
|
|
641
|
-
if (pr && pr.mode === 'peak-valley') {
|
|
953
|
+
if (pr && pr.mode === 'peak-valley' && fieldVisible('period')) {
|
|
642
954
|
const peakNow = pr.period === 'peak';
|
|
643
955
|
const p = pr.prices || {};
|
|
644
956
|
const periodTitle = '北京时间 ' + (peakNow ? '高峰价' : '空闲价') + ':输入 ¥' + (p.inputCacheMiss != null ? p.inputCacheMiss : '?')
|
|
645
957
|
+ '/M · 缓存 ¥' + (p.inputCacheHit != null ? p.inputCacheHit : '?')
|
|
646
958
|
+ '/M · 输出 ¥' + (p.output != null ? p.output : '?') + '/M';
|
|
647
|
-
groups.push(React.createElement('span', {
|
|
648
|
-
peakNow ? '高峰价' : '空闲价'));
|
|
959
|
+
groups.push(fieldSpan('period', 'period', React.createElement('span', { className: peakNow ? 'bi-peak' : 'bi-offpeak', title: periodTitle },
|
|
960
|
+
peakNow ? '高峰价' : '空闲价')));
|
|
649
961
|
}
|
|
650
962
|
|
|
651
963
|
// 倒计时:仅峰谷价服务商显示"距高峰/距空闲"(hover 展示下次切换时刻;数字加粗)
|
|
652
|
-
if (pr && pr.mode === 'peak-valley' && pr.nextSwitch) {
|
|
964
|
+
if (pr && pr.mode === 'peak-valley' && pr.nextSwitch && fieldVisible('countdown')) {
|
|
653
965
|
const peakNow = pr.period === 'peak';
|
|
654
966
|
const countdownTitle = '北京时间 ' + pr.nextSwitch.atLabel + ' 切换为' + (peakNow ? '空闲价' : '高峰价') + '。';
|
|
655
|
-
groups.push(React.createElement('span', {
|
|
656
|
-
metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now))));
|
|
967
|
+
groups.push(fieldSpan('countdown', 'countdown', React.createElement('span', { title: countdownTitle },
|
|
968
|
+
metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now)))));
|
|
657
969
|
}
|
|
658
970
|
|
|
659
|
-
//
|
|
971
|
+
// 本会话花费(公共小部件 pushSessionCost:只显示钱;hover 显示 今天/近一月/全部)
|
|
660
972
|
// 始终显示:新会话/对话刚开始尚无记账时显示 ¥0.000,hover 仍可查看持久化的 今天/近一月/全部
|
|
973
|
+
pushSessionCost(groups, trailingErrorGroups, !!(bal && bal.currency === 'USD'));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// 本会话花费块(余额制 与 订阅·充值余额 形态共用的小部件):
|
|
977
|
+
// 只显示钱;hover 浮窗显示 含子代理说明 + 今天 / 近一月 / 全部;金额数字加粗。
|
|
978
|
+
// usdSymbol:账户币种为美元时,hover 汇总行用 $ 前缀(本会话单值仍按其真实计价币种)
|
|
979
|
+
function pushSessionCost(groups, trailingErrorGroups, usdSymbol) {
|
|
661
980
|
const usg = state.usage;
|
|
981
|
+
const errs = state.errors || {};
|
|
662
982
|
if (usg) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
trailingErrorGroups.push(
|
|
983
|
+
// 隐藏不占位(错误提示属于独立字段 usageError,两支互斥不受影响)
|
|
984
|
+
if (fieldVisible('sessionCost')) {
|
|
985
|
+
const cs = usg.currentSession;
|
|
986
|
+
const costCNY = cs && cs.costs && cs.costs.CNY != null ? cs.costs.CNY : null;
|
|
987
|
+
const costUSD = cs && cs.costs && cs.costs.USD != null ? cs.costs.USD : null;
|
|
988
|
+
const symbol = usdSymbol ? '$' : '¥';
|
|
989
|
+
const costTxt = costCNY != null ? '¥' + costCNY.toFixed(3)
|
|
990
|
+
: (costUSD != null ? '$' + costUSD.toFixed(3) : symbol + (0).toFixed(3));
|
|
991
|
+
const today = usg.todaySpend != null ? '今天 ' + symbol + fmt(usg.todaySpend, 3) : '';
|
|
992
|
+
const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
|
|
993
|
+
const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
|
|
994
|
+
const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
|
|
995
|
+
groups.push(fieldSpan('sessionCost', 'convo', React.createElement('span', {
|
|
996
|
+
title: '本会话 ' + costTxt + '(含子代理)' + (detail ? '\n' + detail : '') },
|
|
997
|
+
metric('本会话', costTxt))));
|
|
998
|
+
}
|
|
999
|
+
} else if (errs.usage && fieldVisible('usageError')) {
|
|
1000
|
+
trailingErrorGroups.push(fieldSpan('usageError', 'usageerr',
|
|
1001
|
+
React.createElement('span', { className: 'bi-err', title: '花费暂不可用;不会影响对话。' }, '花费获取失败')));
|
|
681
1002
|
}
|
|
682
1003
|
}
|
|
683
1004
|
|
|
684
|
-
// ----
|
|
685
|
-
//
|
|
1005
|
+
// ---- 订阅制模式(互斥替换余额制版):
|
|
1006
|
+
// 套餐额度型:订阅服务+模型 → 三窗口额度 → 距重置倒计时(余额/时段/花费/token 不显示)
|
|
1007
|
+
// 充值余额型(如智谱按量账户,windows 空且有 sub.balance):服务商+模型 → 余额 → 本会话花费 ----
|
|
686
1008
|
function subscriptionFailureHint(error, source) {
|
|
687
1009
|
const kind = error && error.kind;
|
|
688
1010
|
const serviceName = subscriptionServiceName(source);
|
|
@@ -699,31 +1021,58 @@ module.exports = {
|
|
|
699
1021
|
}
|
|
700
1022
|
|
|
701
1023
|
function pushSubscriptionGroups(groups, trailingErrorGroups) {
|
|
702
|
-
|
|
1024
|
+
if (fieldVisible('subServiceGroup')) {
|
|
1025
|
+
const subAnchor = subscriptionProviderGroup();
|
|
1026
|
+
groups.push(React.cloneElement(subAnchor, { 'data-field': 'subServiceGroup', style: fieldStyle('subServiceGroup') }));
|
|
1027
|
+
}
|
|
703
1028
|
const sub = state.sub;
|
|
704
1029
|
const errors = state.errors || {};
|
|
705
1030
|
// v1.7 FR-8:JWT 订阅卡——真实套餐到期日(纯本地解码;无登录态/解析失败不显示此处)
|
|
706
|
-
if (sub && sub.planType && sub.expiryAt) {
|
|
707
|
-
groups.push(React.createElement('span', {
|
|
708
|
-
metric('到期', formatDate(sub.expiryAt))));
|
|
1031
|
+
if (sub && sub.planType && sub.expiryAt && fieldVisible('expiry')) {
|
|
1032
|
+
groups.push(fieldSpan('expiry', 'subexp', React.createElement('span', { title: '订阅到期:' + formatDate(sub.expiryAt) + '(本地时区)' },
|
|
1033
|
+
metric('到期', formatDate(sub.expiryAt)))));
|
|
709
1034
|
}
|
|
710
1035
|
if (!sub) {
|
|
711
|
-
if (errors.sub) {
|
|
1036
|
+
if (errors.sub && fieldVisible('refreshFailure')) {
|
|
712
1037
|
// 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
|
|
713
|
-
trailingErrorGroups.push(
|
|
1038
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'suberr',
|
|
1039
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint({ kind: 'exception', message: String(errors.sub) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
|
|
714
1040
|
}
|
|
715
1041
|
return;
|
|
716
1042
|
}
|
|
717
1043
|
const rawWindows = Array.isArray(sub.windows) ? sub.windows : [];
|
|
1044
|
+
// v1.9.0 PR2:逐窗口显隐过滤(5h/周/月各自独立开关),后续优先窗口/重置倒计时都基于过滤后的集合
|
|
718
1045
|
const windows = rawWindows.filter(function (w) {
|
|
719
|
-
return w && typeof w.usedPercent === 'number';
|
|
1046
|
+
return w && typeof w.usedPercent === 'number' && windowFieldVisible(w.key);
|
|
720
1047
|
});
|
|
721
1048
|
const hasData = windows.length > 0;
|
|
722
1049
|
// 错误分支:无旧数据时给出明确引导 / 错误文案;有旧数据时走下方渲染并附"刷新失败"标记。
|
|
723
1050
|
// no-key(无令牌/缺 access_token)与 auth(令牌失效 401)→ 统一"未绑定/重新绑定"引导——
|
|
724
1051
|
// 令牌由独立插件 dsh-chatgpt-subscription 维护,本插件只读令牌显示额度,不自行绑定/续期
|
|
725
1052
|
if (sub.error && !hasData) {
|
|
726
|
-
|
|
1053
|
+
if (fieldVisible('refreshFailure')) {
|
|
1054
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1055
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
// v1.8:充值余额模式——无额度窗口但有 balance 字段(如智谱普通 API 余额用户)
|
|
1060
|
+
// 显示"余额 ¥XX.XX",与 Coding Plan 额度窗口互斥
|
|
1061
|
+
if (!hasData && typeof sub.balance === 'number' && isFinite(sub.balance)) {
|
|
1062
|
+
if (fieldVisible('subBalance')) {
|
|
1063
|
+
const balTxt = '¥' + fmt(sub.balance, 2);
|
|
1064
|
+
const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + '(' + (sub.plan || '充值余额') + ')',
|
|
1065
|
+
'可用余额:' + balTxt];
|
|
1066
|
+
groups.push(fieldSpan('subBalance', 'subbal', React.createElement('span', { title: titleLines.join('\n') },
|
|
1067
|
+
metric('余额', balTxt))));
|
|
1068
|
+
}
|
|
1069
|
+
// 充值余额用户按量付费,花销与余额同等重要 → 追加公共花费块(含子代理聚合)
|
|
1070
|
+
pushSessionCost(groups, trailingErrorGroups, false);
|
|
1071
|
+
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 保留旧数据 + 降级标记
|
|
1072
|
+
if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
|
|
1073
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1074
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1075
|
+
}
|
|
727
1076
|
return;
|
|
728
1077
|
}
|
|
729
1078
|
// 窗口缺失(如 Codex 无 5 小时窗口)→ 跳过窗口组,不占位、不报错
|
|
@@ -739,10 +1088,10 @@ module.exports = {
|
|
|
739
1088
|
return pa - pb;
|
|
740
1089
|
})[0]
|
|
741
1090
|
: null;
|
|
742
|
-
|
|
1091
|
+
|
|
743
1092
|
// 完整模式显示全部窗口;简洁模式只显示选中的那个窗口
|
|
744
1093
|
const visible = full ? windows : (displayWindow ? [displayWindow] : []);
|
|
745
|
-
|
|
1094
|
+
|
|
746
1095
|
// 预警触发条件:已用 ≥80%(= 剩余 ≤20%)→ 鲜红色文字;正常额度使用中性文字。
|
|
747
1096
|
const LOW_QUOTA_PERCENT = 20;
|
|
748
1097
|
const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + (sub.plan ? '(' + sub.plan + ')' : '')]
|
|
@@ -756,20 +1105,26 @@ module.exports = {
|
|
|
756
1105
|
if (i > 0) winNodes.push(' · ');
|
|
757
1106
|
const remaining = remainingPercent(w);
|
|
758
1107
|
const numberClass = remaining <= LOW_QUOTA_PERCENT ? 'bi-quota-low' : '';
|
|
759
|
-
|
|
1108
|
+
// 每个窗口独立 data-field(subWindow5h/Week/Month),色变量按字段注入;「低」字标签保留
|
|
1109
|
+
winNodes.push(fieldSpan(WINDOW_FIELD_IDS[w.key] || 'subWindow5h', 'w' + i,
|
|
1110
|
+
metric(compactWindowLabel(w.key), remaining + '%', numberClass)));
|
|
760
1111
|
if (remaining <= LOW_QUOTA_PERCENT) winNodes.push(React.createElement('span', { key: 'low' + i, className: 'bi-low-status' }, '低'));
|
|
761
1112
|
}
|
|
762
|
-
|
|
1113
|
+
// 全部窗口被隐藏(或紧凑模式无候选)→ 整组不推送,分隔符由组装层正确收合
|
|
1114
|
+
if (winNodes.length > 0) {
|
|
1115
|
+
groups.push(React.createElement('span', { key: 'subwin', title: titleLines.join('\n') }, ...winNodes));
|
|
1116
|
+
}
|
|
763
1117
|
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 均保留旧数据 + 降级标记
|
|
764
|
-
if (sub.error || errors.sub) {
|
|
765
|
-
trailingErrorGroups.push(
|
|
1118
|
+
if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
|
|
1119
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1120
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
766
1121
|
}
|
|
767
1122
|
// 距重置倒计时(与显示的窗口一致,确保额度与倒计时匹配)
|
|
768
|
-
if (displayWindow && displayWindow.resetsAt) {
|
|
1123
|
+
if (displayWindow && displayWindow.resetsAt && fieldVisible('resetCountdown')) {
|
|
769
1124
|
const cdTitle = displayWindow.label + '窗口 剩余 ' + remainingPercent(displayWindow)
|
|
770
1125
|
+ '%(已用 ' + displayWindow.usedPercent + '%) · 重置 ' + formatDateTime(displayWindow.resetsAt);
|
|
771
|
-
groups.push(React.createElement('span', {
|
|
772
|
-
metric('距重置', fmtResetCountdown(displayWindow.resetsAt - now))));
|
|
1126
|
+
groups.push(fieldSpan('resetCountdown', 'subcd', React.createElement('span', { title: cdTitle },
|
|
1127
|
+
metric('距重置', fmtResetCountdown(displayWindow.resetsAt - now)))));
|
|
773
1128
|
}
|
|
774
1129
|
}
|
|
775
1130
|
}
|
|
@@ -789,19 +1144,26 @@ module.exports = {
|
|
|
789
1144
|
}
|
|
790
1145
|
|
|
791
1146
|
function pushBillingGroups(groups, trailingErrorGroups) {
|
|
792
|
-
|
|
1147
|
+
if (fieldVisible('billingServiceGroup')) {
|
|
1148
|
+
const billAnchor = billingProviderGroup();
|
|
1149
|
+
groups.push(React.cloneElement(billAnchor, { 'data-field': 'billingServiceGroup', style: fieldStyle('billingServiceGroup') }));
|
|
1150
|
+
}
|
|
793
1151
|
const bill = state.billing;
|
|
794
1152
|
const errors = state.errors || {};
|
|
795
1153
|
if (!bill) {
|
|
796
|
-
if (errors.billing) {
|
|
797
|
-
trailingErrorGroups.push(
|
|
1154
|
+
if (errors.billing && fieldVisible('refreshFailure')) {
|
|
1155
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billerr',
|
|
1156
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint({ kind: 'exception', message: String(errors.billing) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
|
|
798
1157
|
}
|
|
799
1158
|
return;
|
|
800
1159
|
}
|
|
801
1160
|
const d = bill.data;
|
|
802
1161
|
const hasSpend = !!(d && (d.currentPeriodSpend != null || d.usage != null));
|
|
803
1162
|
if (bill.error && !hasSpend) {
|
|
804
|
-
|
|
1163
|
+
if (fieldVisible('refreshFailure')) {
|
|
1164
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
|
|
1165
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1166
|
+
}
|
|
805
1167
|
return;
|
|
806
1168
|
}
|
|
807
1169
|
if (hasSpend) {
|
|
@@ -811,17 +1173,28 @@ module.exports = {
|
|
|
811
1173
|
.concat(d.currentPeriodSpend != null ? ['本月花费:' + symbol + fmt(d.currentPeriodSpend, 2)] : [])
|
|
812
1174
|
.concat(d.budgetPercent != null ? ['预算使用:' + fmt(d.budgetPercent, 0) + '%'] : [])
|
|
813
1175
|
.concat(d.freeRemaining != null ? ['每日免费额度剩余:' + fmt(d.freeRemaining, 0)] : []);
|
|
1176
|
+
// v1.9.0 PR2:本月/预算/免费额度三片段各自显隐,分隔符只在可见片段之间
|
|
814
1177
|
const nodes = [];
|
|
815
|
-
if (d.currentPeriodSpend != null
|
|
816
|
-
|
|
817
|
-
if (d.
|
|
1178
|
+
if (d.currentPeriodSpend != null && fieldVisible('billingSpend')) {
|
|
1179
|
+
nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月', symbol + fmt(d.currentPeriodSpend, 2))));
|
|
1180
|
+
} else if (d.usage != null && fieldVisible('billingSpend')) {
|
|
1181
|
+
nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月用量', fmt(d.usage, 2) + (d.usageUnit ? ' ' + d.usageUnit : ''))));
|
|
1182
|
+
}
|
|
1183
|
+
if (d.budgetPercent != null && fieldVisible('budget')) {
|
|
1184
|
+
if (nodes.length > 0) nodes.push(' · ');
|
|
1185
|
+
nodes.push(fieldSpan('budget', 'billbudget', metric('预算', fmt(d.budgetPercent, 0) + '%')));
|
|
1186
|
+
}
|
|
818
1187
|
// 免费额度仅当接口显式给出(freeRemaining/resetsAt 同时存在)才显示,绝不编造
|
|
819
|
-
if (d.freeRemaining != null && d.resetsAt) {
|
|
820
|
-
nodes.
|
|
1188
|
+
if (d.freeRemaining != null && d.resetsAt && fieldVisible('freeQuota')) {
|
|
1189
|
+
if (nodes.length > 0) nodes.push(' · ');
|
|
1190
|
+
nodes.push(fieldSpan('freeQuota', 'billfree', metric('免费', fmt(d.freeRemaining, 0) + ' · 距重置 ' + fmtResetCountdown(d.resetsAt - now))));
|
|
821
1191
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1192
|
+
if (nodes.length > 0) {
|
|
1193
|
+
groups.push(React.createElement('span', { key: 'bill', title: titleLines.join('\n') }, ...nodes));
|
|
1194
|
+
}
|
|
1195
|
+
if (fieldVisible('refreshFailure') && (bill.error || errors.billing)) {
|
|
1196
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
|
|
1197
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error || { kind: 'exception', message: String(errors.billing || '') }, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
825
1198
|
}
|
|
826
1199
|
}
|
|
827
1200
|
}
|
|
@@ -872,53 +1245,37 @@ module.exports = {
|
|
|
872
1245
|
if (errors.billingMode) failedLabels.push('模式');
|
|
873
1246
|
if (errors.sub) failedLabels.push('订阅额度');
|
|
874
1247
|
if (errors.billing) failedLabels.push('账单');
|
|
875
|
-
if (failedLabels.length > 0) {
|
|
876
|
-
trailingErrorGroups.push(
|
|
877
|
-
|
|
1248
|
+
if (failedLabels.length > 0 && fieldVisible('refreshFailure')) {
|
|
1249
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'degraded',
|
|
1250
|
+
React.createElement('span', { className: 'bi-stale', key: 'degraded',
|
|
1251
|
+
title: failedLabels.join('、') + '暂不可用;正在保留上次数据并自动重试。' }, '刷新失败')));
|
|
878
1252
|
}
|
|
879
1253
|
const persistence = state.usage && state.usage.persistence;
|
|
880
|
-
if (persistence && persistence.state && persistence.state !== 'ok') {
|
|
1254
|
+
if (persistence && persistence.state && persistence.state !== 'ok' && fieldVisible('persistWarning')) {
|
|
881
1255
|
const snapshotOnly = persistence.state === 'snapshot-stale';
|
|
882
|
-
trailingErrorGroups.push(React.createElement('span', {
|
|
883
|
-
className: snapshotOnly ? 'bi-stale' : 'bi-err',
|
|
1256
|
+
trailingErrorGroups.push(fieldSpan('persistWarning', 'ledger-save', React.createElement('span', {
|
|
1257
|
+
className: snapshotOnly ? 'bi-stale' : 'bi-err',
|
|
884
1258
|
title: snapshotOnly
|
|
885
1259
|
? '账单流水已保存,但可直接查看的账单文件暂未更新:' + (persistence.message || '未知原因')
|
|
886
1260
|
: '本次账单未保存,不会计入金额:' + (persistence.message || '未知原因'),
|
|
887
|
-
}, snapshotOnly ? '账单待整理' : '账单未保存'));
|
|
1261
|
+
}, snapshotOnly ? '账单待整理' : '账单未保存')));
|
|
888
1262
|
}
|
|
889
1263
|
|
|
890
|
-
if (updateInfo && updateInfo.available === true) {
|
|
891
|
-
groups.push(React.createElement('span', {
|
|
892
|
-
className: 'bi-update',
|
|
893
|
-
}, '新版本提醒'));
|
|
1264
|
+
if (updateInfo && updateInfo.available === true && fieldVisible('updateNotice')) {
|
|
1265
|
+
groups.push(fieldSpan('updateNotice', 'update', React.createElement('span', {
|
|
1266
|
+
className: 'bi-update', title: '请告知你的 Agent 将本插件更新到“' + updateInfo.latest + '”版本。',
|
|
1267
|
+
}, '新版本提醒')));
|
|
894
1268
|
}
|
|
895
1269
|
|
|
896
|
-
// ----
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
for (let i = 0; i < groups.length; i++) {
|
|
900
|
-
if (i > 0) nodes.push(React.createElement('span', { key: 'sep' + i, className: 'bi-sep' }, '|'));
|
|
901
|
-
nodes.push(React.createElement('span', { key: 'g' + i }, groups[i]));
|
|
902
|
-
}
|
|
903
|
-
const seenRefreshFailure = { value: false };
|
|
904
|
-
const visibleErrors = trailingErrorGroups.filter(function (node) {
|
|
905
|
-
const text = node && node.props ? node.props.children : '';
|
|
906
|
-
if (text !== '刷新失败') return true;
|
|
907
|
-
if (seenRefreshFailure.value) return false;
|
|
908
|
-
seenRefreshFailure.value = true;
|
|
909
|
-
return true;
|
|
910
|
-
});
|
|
911
|
-
for (let i = 0; i < visibleErrors.length; i++) {
|
|
912
|
-
if (groups.length > 0 || i > 0) nodes.push(React.createElement('span', { key: 'errsep' + i, className: 'bi-sep' }, '|'));
|
|
913
|
-
nodes.push(React.createElement('span', { key: 'err' + i }, visibleErrors[i]));
|
|
914
|
-
}
|
|
915
|
-
const row2 = React.createElement('div', { id: 'dsh-bottom-info-bar-primary', className: 'bi-row2' }, ...nodes);
|
|
1270
|
+
// ---- 组装(分隔符收合与「刷新失败」去重见模块级 assembleInfoBarRow) ----
|
|
1271
|
+
const nodes = assembleInfoBarRow(groups, trailingErrorGroups, React.createElement);
|
|
1272
|
+
const row2 = React.createElement('div', { id: 'dsh-bottom-info-bar-primary', className: 'bi-row2' }, ...nodes);
|
|
916
1273
|
|
|
917
1274
|
let row1 = null;
|
|
918
1275
|
if (statsProj) {
|
|
919
|
-
// 每组:{ nodes: React 节点数组(数字用 num 加粗), text: 纯文本(title
|
|
1276
|
+
// 每组:{ nodes: React 节点数组(数字用 num 加粗), text: 纯文本(title 用), fieldId: 字段 id(着色用) }
|
|
920
1277
|
const ng = [];
|
|
921
|
-
function group(parts, hidden) {
|
|
1278
|
+
function group(parts, hidden, fieldId) {
|
|
922
1279
|
const nodesArr = [];
|
|
923
1280
|
const texts = [];
|
|
924
1281
|
function textOf(part) {
|
|
@@ -934,27 +1291,35 @@ module.exports = {
|
|
|
934
1291
|
else if (Array.isArray(p)) { for (let j = 0; j < p.length; j++) nodesArr.push(p[j]); texts.push(textOf(p)); }
|
|
935
1292
|
else { nodesArr.push(p); texts.push(textOf(p)); }
|
|
936
1293
|
}
|
|
937
|
-
ng.push({ nodes: nodesArr, text: texts.join(''), hidden: !!hidden });
|
|
1294
|
+
ng.push({ nodes: nodesArr, text: texts.join(''), hidden: !!hidden, fieldId: fieldId || null });
|
|
938
1295
|
}
|
|
939
1296
|
|
|
940
|
-
//
|
|
941
|
-
|
|
1297
|
+
// v1.9.0 PR2:原生统计行字段按设置过滤(隐藏组完全不进 ng,不占版式也不进 title)
|
|
1298
|
+
if (fieldVisible('turnsSteps')) {
|
|
1299
|
+
// 中文界面遵循数字与汉字混排留白:数值与量词视觉上分开,便于快速扫读。
|
|
1300
|
+
group([num(statsProj.turns + ' 轮'), ' · ', num(statsProj.steps + ' 步')], false, 'turnsSteps');
|
|
1301
|
+
}
|
|
942
1302
|
|
|
943
1303
|
const durations = [];
|
|
944
|
-
if (statsProj.llmMs > 0) durations.push(metric('LLM', formatDuration(statsProj.llmMs)));
|
|
945
|
-
if (statsProj.toolMs > 0
|
|
1304
|
+
if (statsProj.llmMs > 0 && fieldVisible('llmTime')) durations.push(fieldSpan('llmTime', 'durl', metric('LLM', formatDuration(statsProj.llmMs))));
|
|
1305
|
+
if (statsProj.toolMs > 0 && fieldVisible('toolTime')) {
|
|
1306
|
+
if (durations.length > 0) durations.push(' · ');
|
|
1307
|
+
durations.push(fieldSpan('toolTime', 'durt', metric('工具调用', formatDuration(statsProj.toolMs))));
|
|
1308
|
+
}
|
|
946
1309
|
if (durations.length > 0) group(durations);
|
|
947
1310
|
|
|
948
1311
|
const speeds = [];
|
|
949
1312
|
if (statsProj.ttftSteps > 0) speeds.push(metric('首 token 平均', formatDuration(statsProj.ttftMs / statsProj.ttftSteps)));
|
|
950
1313
|
if (statsProj.decodeMs > 0) speeds.push(' · ', num(formatTps(statsProj.decodeTokens / (statsProj.decodeMs / 1e3)) + ' tok/s'));
|
|
951
|
-
if (speeds.length > 0) group(speeds, HIDE_SPEED_FIELDS); // 不占可见版式,title
|
|
1314
|
+
if (speeds.length > 0) group(speeds, HIDE_SPEED_FIELDS); // 不占可见版式,title 浮窗保留(官方隐藏字段,非用户可配)
|
|
952
1315
|
|
|
953
1316
|
if (usageProj && (billedInput(usageProj) > 0 || (usageProj.outputTokens || 0) > 0)) {
|
|
954
1317
|
const denom = billedInput(usageProj);
|
|
955
1318
|
const hit = denom > 0 ? Math.round(((usageProj.cacheReadTokens || 0) / denom) * 100) : null;
|
|
956
|
-
if (hit != null) group([metric('缓存命中', hit + '%')]);
|
|
957
|
-
|
|
1319
|
+
if (hit != null && fieldVisible('cacheHit')) group([metric('缓存命中', hit + '%')], false, 'cacheHit');
|
|
1320
|
+
if (fieldVisible('tokensIO')) {
|
|
1321
|
+
group([metric('输入', formatTokens(billedInput(usageProj)) + ' tok'), ' · ', metric('输出', formatTokens(usageProj.outputTokens || 0) + ' tok')], false, 'tokensIO');
|
|
1322
|
+
}
|
|
958
1323
|
}
|
|
959
1324
|
|
|
960
1325
|
const nativeLine = ng.map(function (g) { return g.text; }).join(' | ');
|
|
@@ -964,9 +1329,22 @@ module.exports = {
|
|
|
964
1329
|
if (ng[i].hidden) continue; // 隐藏分组不占版式(title 仍含其文本)
|
|
965
1330
|
if (visCount > 0) ngNodes.push(React.createElement('span', { key: 'nsep' + i, className: 'bi-sep' }, '|'));
|
|
966
1331
|
visCount++;
|
|
967
|
-
ngNodes.push(React.createElement('span', {
|
|
1332
|
+
ngNodes.push(React.createElement('span', {
|
|
1333
|
+
key: 'ng' + i,
|
|
1334
|
+
'data-field': ng[i].fieldId || undefined,
|
|
1335
|
+
style: ng[i].fieldId ? fieldStyle(ng[i].fieldId) : undefined,
|
|
1336
|
+
}, ng[i].nodes));
|
|
968
1337
|
}
|
|
969
|
-
|
|
1338
|
+
// D6:原生组全部被隐藏(或全空)→ 原生行不渲染(不留空行/占位;hover 浮窗随之消失)
|
|
1339
|
+
if (ngNodes.length === 0) row1 = null;
|
|
1340
|
+
else row1 = React.createElement('div', { id: 'dsh-bottom-info-bar-native', className: 'bi-native-row', title: nativeLine }, ...ngNodes);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// D6 用户拍板:全部字段隐藏 = 底栏彻底移除——不渲染任何 DOM(无空行/占位高度/悬空分隔符),
|
|
1344
|
+
// density 点击因无 DOM 而天然无副作用、不报错。两条路径:①配置层面所有字段都被关闭;
|
|
1345
|
+
// ②渲染层面(数据条件导致)原生行/主行全空。
|
|
1346
|
+
if (infoBarShouldRemoveAll(FIELD_REGISTRY, fieldVisible) || (row1 === null && nodes.length === 0)) {
|
|
1347
|
+
return null;
|
|
970
1348
|
}
|
|
971
1349
|
|
|
972
1350
|
const animatedRow1 = row1 === null ? null : React.createElement('div', { className: 'bi-density-extra' },
|
|
@@ -998,5 +1376,488 @@ module.exports = {
|
|
|
998
1376
|
},
|
|
999
1377
|
};
|
|
1000
1378
|
|
|
1379
|
+
// Bottom Info Bar(底部信息栏插件)— client 设置页扩展(与 client-bundle.js 同一 bundle 作用域拼接)
|
|
1380
|
+
// v1.9.0 PR2:注册 DSH 设置面板 settings.section 插座,新增「信息底栏」设置页。
|
|
1381
|
+
// 设计规范:苹果 HIG(设置型界面改动即时生效)+ 融入 DSH 设置面板既有视觉(--dsw-alias-* 令牌、
|
|
1382
|
+
// 卡片圆角 12px、行标题 14px/说明 12px),全程键盘可达(roving tabindex 方向键)、
|
|
1383
|
+
// role=switch/radio + aria-checked、:focus-visible 焦点环、prefers-reduced-motion 降级。
|
|
1384
|
+
// 约定:本文件与 client-bundle.js 拼接进同一个 __ModuleLoader__ factory 作用域,
|
|
1385
|
+
// 顶层标识符一律 bibSet* / InfoBarSettings* 前缀,绝不与主包重名(React/rpc/FIELD_REGISTRY 等直接复用主包声明)。
|
|
1386
|
+
'use strict';
|
|
1387
|
+
|
|
1388
|
+
const BIB_SET_EVENT = 'dsh-bib-config-changed';
|
|
1389
|
+
const BIB_SET_PRESET_LABELS = { red: '红', green: '绿', blue: '蓝', purple: '紫', orange: '橙', neutral: '中性' };
|
|
1390
|
+
// 原生取色器(input[type=color])在未自定义时显示的代表色(浅色主题值;实际信息栏渲染仍按主题变量)
|
|
1391
|
+
const BIB_SET_PRESET_WELL_HEX = { red: '#D92D20', green: '#087F5B', blue: '#0044CC', purple: '#6941C6', orange: '#B54708', neutral: '#333333' };
|
|
1392
|
+
const BIB_SET_HEX_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
1393
|
+
// 「显示字段」两级分组(D6 用户拍板:原生在前、插件在后)——构建从 constants.js 注入(单一来源)
|
|
1394
|
+
const FIELD_GROUP_ORDER = ['native', 'plugin'];
|
|
1395
|
+
const FIELD_GROUP_LABELS = {
|
|
1396
|
+
native: '原生字段',
|
|
1397
|
+
plugin: '插件字段',
|
|
1398
|
+
};
|
|
1399
|
+
// 每行出现条件文案:modes → 中文(余额制/订阅制/账单制/原生统计行/通用)
|
|
1400
|
+
const BIB_SET_MODE_LABELS = { balance: '余额制', subscription: '订阅制', billing: '账单制', native: '原生统计行', common: '通用' };
|
|
1401
|
+
function bibSetModeText(field) {
|
|
1402
|
+
if (!field || !Array.isArray(field.modes) || field.modes.length === 0) return '';
|
|
1403
|
+
if (field.modes.length === 1 && field.modes[0] === 'native') return '原生统计行字段';
|
|
1404
|
+
const labels = [];
|
|
1405
|
+
for (let i = 0; i < field.modes.length; i++) {
|
|
1406
|
+
const label = BIB_SET_MODE_LABELS[field.modes[i]];
|
|
1407
|
+
if (label && labels.indexOf(label) === -1) labels.push(label);
|
|
1408
|
+
}
|
|
1409
|
+
return labels.length > 0 ? '出现于' + labels.join('、') : '';
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function bibSetDispatchChanged() {
|
|
1413
|
+
// 设置页保存成功后广播:信息栏监听并立即重拉配置(宿主内存缓存,即回)
|
|
1414
|
+
try { document.dispatchEvent(new CustomEvent(BIB_SET_EVENT)); } catch (err) { /* 事件总线不可用时静默:30s 周期校准兜底 */ }
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function bibSetOperationMessage(err) {
|
|
1418
|
+
return String((err && err.message) || err || '请稍后再试');
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ---------- 设置页样式(融入 DSH 设置面板:卡片/行布局/控件全部走 --dsw-alias-* 令牌) ----------
|
|
1422
|
+
function bibSetInstallStyles() {
|
|
1423
|
+
const id = 'dsh-bottom-info-bar-settings';
|
|
1424
|
+
const existing = document.querySelector('style[data-plugin-css="' + id + '"]');
|
|
1425
|
+
if (existing !== null) return function () {};
|
|
1426
|
+
const style = document.createElement('style');
|
|
1427
|
+
style.dataset.plugin = 'dsh-bottom-info-bar';
|
|
1428
|
+
style.dataset.pluginCss = id;
|
|
1429
|
+
style.textContent = `
|
|
1430
|
+
.bib-set-root { --bib-set-brand: var(--dsw-alias-brand-primary, #4d6bfe); }
|
|
1431
|
+
.bib-settings { max-width: 720px; box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; color: var(--dsw-alias-label-primary); }
|
|
1432
|
+
.bib-set-intro { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
1433
|
+
.bib-set-status { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 20px; }
|
|
1434
|
+
.bib-set-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; }
|
|
1435
|
+
.bib-set-card-header { display: flex; flex-direction: column; gap: 4px; padding: 14px 16px; }
|
|
1436
|
+
.bib-set-card-title { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); }
|
|
1437
|
+
.bib-set-card-desc { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
|
|
1438
|
+
.bib-set-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 0 0 6px; }
|
|
1439
|
+
.bib-set-group-title { margin: 12px 0 2px; font-size: 12px; font-weight: 500; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
1440
|
+
.bib-set-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
1441
|
+
.bib-set-row:last-child { border-bottom: none; }
|
|
1442
|
+
.bib-set-rowText { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 200px; }
|
|
1443
|
+
.bib-set-rowTitle { display: flex; align-items: center; gap: 6px; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
|
|
1444
|
+
.bib-set-keep { flex: none; padding: 0 6px; border-radius: 999px; background: var(--dsw-alias-fill-tsp-secondary, rgba(128,128,128,0.12)); color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 500; line-height: 16px; }
|
|
1445
|
+
.bib-set-rowDesc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
1446
|
+
/* 开关:iOS 原生质感(40×24 轨道 + 18px 圆钮),语义 = role:switch + aria-checked */
|
|
1447
|
+
.bib-set-switch { appearance: none; background: 0 0; border: 0; padding: 0; margin: 0; cursor: pointer; display: inline-flex; flex: none; border-radius: 12px; }
|
|
1448
|
+
.bib-set-switch:disabled { cursor: default; opacity: 0.5; }
|
|
1449
|
+
.bib-set-switch:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
1450
|
+
.bib-set-switch-track { position: relative; display: inline-block; box-sizing: border-box; width: 40px; height: 24px; border-radius: 12px; background: var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); transition: background-color 160ms var(--ds-ease-in-out, ease); }
|
|
1451
|
+
.bib-set-switch-track[data-on="true"] { background: var(--bib-set-brand); }
|
|
1452
|
+
.bib-set-switch-thumb { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgba(0,0,0,0.2); transition: transform 160ms var(--ds-ease-in-out, ease); }
|
|
1453
|
+
.bib-set-switch-track[data-on="true"] .bib-set-switch-thumb { transform: translateX(16px); }
|
|
1454
|
+
/* 色板圆点:role:radio + roving tabindex(方向键/Home/End 可达),选中态外圈描边 */
|
|
1455
|
+
.bib-set-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; }
|
|
1456
|
+
.bib-set-dots { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
1457
|
+
.bib-set-dot { appearance: none; width: 20px; height: 20px; padding: 0; margin: 0; border-radius: 50%; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); background: transparent; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
|
1458
|
+
.bib-set-dot:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
1459
|
+
.bib-set-dot[aria-checked="true"] { box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3, #fff), 0 0 0 4px var(--bib-set-brand); }
|
|
1460
|
+
.bib-set-dot-core { display: block; width: 14px; height: 14px; border-radius: 50%; }
|
|
1461
|
+
.bib-set-dot-default .bib-set-dot-core { background: transparent; border: 1.5px dashed var(--dsw-alias-label-quaternary, rgba(128,128,128,0.5)); }
|
|
1462
|
+
/* 原生取色器色井:保留系统行为,仅样式化为圆角色井 */
|
|
1463
|
+
.bib-set-well { display: inline-flex; flex: none; }
|
|
1464
|
+
.bib-set-well input[type="color"] { appearance: none; -webkit-appearance: none; box-sizing: border-box; width: 30px; height: 26px; padding: 2px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); border-radius: 8px; background: var(--dsw-alias-bg-layer-2, transparent); cursor: pointer; }
|
|
1465
|
+
.bib-set-well input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
|
|
1466
|
+
.bib-set-well input[type="color"]::-webkit-color-swatch { border: none; border-radius: 5px; }
|
|
1467
|
+
.bib-set-well input[type="color"]::-moz-color-swatch { border: none; border-radius: 5px; }
|
|
1468
|
+
.bib-set-well input[type="color"]:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
1469
|
+
/* hex 输入:等宽字体、即时校验(非法描红 + aria-invalid),Enter/失焦提交,非法回退 */
|
|
1470
|
+
.bib-set-hex { box-sizing: border-box; width: 88px; padding: 4px 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4)); border-radius: 8px; }
|
|
1471
|
+
.bib-set-hex:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 1px; }
|
|
1472
|
+
.bib-set-hex[data-invalid="true"] { border-color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-error, #d92d20)); }
|
|
1473
|
+
.bib-set-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; padding: 2px 0 10px; }
|
|
1474
|
+
.bib-set-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); background: 0 0; border-radius: 8px; padding: 5px 14px; font-size: 13px; line-height: 1.5; }
|
|
1475
|
+
.bib-set-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.08)); }
|
|
1476
|
+
.bib-set-btn:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
1477
|
+
.bib-set-btn:disabled { opacity: 0.5; cursor: default; }
|
|
1478
|
+
.bib-set-alert { margin: 0; color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-error, #d92d20)); font-size: 12px; line-height: 18px; flex: 1 1 auto; min-width: 0; }
|
|
1479
|
+
.bib-set-notice { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; flex: 1 1 auto; min-width: 0; }
|
|
1480
|
+
@media (prefers-reduced-motion: reduce) { .bib-set-switch-track, .bib-set-switch-thumb { transition: none; } }
|
|
1481
|
+
`;
|
|
1482
|
+
document.head.appendChild(style);
|
|
1483
|
+
return function () { style.remove(); };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// ---------- 控件 ----------
|
|
1487
|
+
function bibSetSwitch(props) {
|
|
1488
|
+
const checked = !!props.checked;
|
|
1489
|
+
return React.createElement('button', {
|
|
1490
|
+
type: 'button',
|
|
1491
|
+
className: 'bib-set-switch',
|
|
1492
|
+
role: 'switch',
|
|
1493
|
+
'aria-checked': checked,
|
|
1494
|
+
'aria-label': props.label,
|
|
1495
|
+
disabled: props.disabled === true,
|
|
1496
|
+
title: props.title,
|
|
1497
|
+
onClick: function () { if (props.onToggle) props.onToggle(!checked); },
|
|
1498
|
+
},
|
|
1499
|
+
React.createElement('span', { className: 'bib-set-switch-track', 'data-on': checked ? 'true' : 'false', 'aria-hidden': 'true' },
|
|
1500
|
+
React.createElement('span', { className: 'bib-set-switch-thumb' })));
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// 色板圆点组:默认 + 预设色名;role=radiogroup/radio + roving tabindex(方向键/Home/End)
|
|
1504
|
+
function bibSetPalette(props) {
|
|
1505
|
+
const options = ['default'].concat(PRESET_COLORS);
|
|
1506
|
+
const refs = React.useRef({});
|
|
1507
|
+
const isSelected = function (option) {
|
|
1508
|
+
return option === 'default' ? props.value === null : props.value === option;
|
|
1509
|
+
};
|
|
1510
|
+
const select = function (option) {
|
|
1511
|
+
if (props.onSelect) props.onSelect(option === 'default' ? null : option);
|
|
1512
|
+
};
|
|
1513
|
+
const move = function (from, step) {
|
|
1514
|
+
const next = (from + step + options.length) % options.length;
|
|
1515
|
+
const node = refs.current[next];
|
|
1516
|
+
if (node && typeof node.focus === 'function') node.focus();
|
|
1517
|
+
select(options[next]);
|
|
1518
|
+
};
|
|
1519
|
+
const onKey = function (event, index) {
|
|
1520
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { event.preventDefault(); move(index, 1); }
|
|
1521
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { event.preventDefault(); move(index, -1); }
|
|
1522
|
+
else if (event.key === 'Home') { event.preventDefault(); move(index, -index); }
|
|
1523
|
+
else if (event.key === 'End') { event.preventDefault(); move(index, options.length - 1 - index); }
|
|
1524
|
+
};
|
|
1525
|
+
const children = options.map(function (option, index) {
|
|
1526
|
+
const selected = isSelected(option);
|
|
1527
|
+
const isDefault = option === 'default';
|
|
1528
|
+
const coreStyle = {};
|
|
1529
|
+
if (!isDefault) coreStyle.background = 'var(--bi-palette-' + option + ')';
|
|
1530
|
+
const ariaLabel = isDefault ? '恢复默认颜色' : (BIB_SET_PRESET_LABELS[option] || option);
|
|
1531
|
+
return React.createElement('button', {
|
|
1532
|
+
key: option,
|
|
1533
|
+
type: 'button',
|
|
1534
|
+
ref: function (node) { refs.current[index] = node; },
|
|
1535
|
+
className: 'bib-set-dot' + (isDefault ? ' bib-set-dot-default' : ''),
|
|
1536
|
+
role: 'radio',
|
|
1537
|
+
'aria-checked': selected,
|
|
1538
|
+
'aria-label': ariaLabel,
|
|
1539
|
+
title: ariaLabel,
|
|
1540
|
+
tabIndex: selected ? 0 : -1,
|
|
1541
|
+
onClick: function () { select(option); },
|
|
1542
|
+
onKeyDown: function (event) { onKey(event, index); },
|
|
1543
|
+
}, React.createElement('span', { className: 'bib-set-dot-core', style: coreStyle, 'aria-hidden': 'true' }));
|
|
1544
|
+
});
|
|
1545
|
+
return React.createElement('span', { className: 'bib-set-dots', role: 'radiogroup', 'aria-label': props.label }, children);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// ---------- 页面组件 ----------
|
|
1549
|
+
function InfoBarSettingsSection(props) {
|
|
1550
|
+
void props; // 与官方 section 组件同签名(外壳会传入 renderSlot/close 等,本页不使用)
|
|
1551
|
+
const [snapshot, setSnapshot] = React.useState(null); // { fields, colors, configVersion }
|
|
1552
|
+
const [status, setStatus] = React.useState('loading');
|
|
1553
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
1554
|
+
const [opError, setOpError] = React.useState(null);
|
|
1555
|
+
const [notice, setNotice] = React.useState(null);
|
|
1556
|
+
const [saving, setSaving] = React.useState(false);
|
|
1557
|
+
const [hexDrafts, setHexDrafts] = React.useState({});
|
|
1558
|
+
const opSeqRef = React.useRef(0); // 版本号守卫:慢响应绝不覆盖更新的操作
|
|
1559
|
+
const savingCountRef = React.useRef(0);
|
|
1560
|
+
|
|
1561
|
+
React.useEffect(function () {
|
|
1562
|
+
let active = true;
|
|
1563
|
+
rpc('getFieldConfig').then(function (cfg) {
|
|
1564
|
+
if (!active) return;
|
|
1565
|
+
if (cfg && typeof cfg === 'object' && cfg.fields) {
|
|
1566
|
+
setSnapshot({ fields: cfg.fields, colors: cfg.colors || {}, configVersion: cfg.configVersion || 0 });
|
|
1567
|
+
setStatus('ready');
|
|
1568
|
+
} else {
|
|
1569
|
+
setLoadError('配置暂时无法读取'); setStatus('error');
|
|
1570
|
+
}
|
|
1571
|
+
}).catch(function (err) {
|
|
1572
|
+
if (!active) return;
|
|
1573
|
+
setLoadError(bibSetOperationMessage(err)); setStatus('error');
|
|
1574
|
+
});
|
|
1575
|
+
return function () { active = false; };
|
|
1576
|
+
}, []);
|
|
1577
|
+
|
|
1578
|
+
const beginOp = React.useCallback(function () {
|
|
1579
|
+
savingCountRef.current += 1;
|
|
1580
|
+
setSaving(true);
|
|
1581
|
+
}, []);
|
|
1582
|
+
const endOp = React.useCallback(function () {
|
|
1583
|
+
savingCountRef.current = Math.max(0, savingCountRef.current - 1);
|
|
1584
|
+
if (savingCountRef.current === 0) setSaving(false);
|
|
1585
|
+
}, []);
|
|
1586
|
+
|
|
1587
|
+
if (status === 'loading') {
|
|
1588
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
1589
|
+
React.createElement('p', { className: 'bib-set-status' }, '正在载入信息底栏设置…'));
|
|
1590
|
+
}
|
|
1591
|
+
if (status === 'error') {
|
|
1592
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
1593
|
+
React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, '信息底栏设置暂时无法读取:' + (loadError || '请稍后再试')));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const fieldsById = {};
|
|
1597
|
+
for (let i = 0; i < FIELD_REGISTRY.length; i++) fieldsById[FIELD_REGISTRY[i].id] = FIELD_REGISTRY[i];
|
|
1598
|
+
function fieldOn(id) { return snapshot.fields[id] !== false; }
|
|
1599
|
+
function colorOf(id) {
|
|
1600
|
+
const value = snapshot.colors[id];
|
|
1601
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
1602
|
+
}
|
|
1603
|
+
function labelOf(id) { const f = fieldsById[id]; return f ? f.label : id; }
|
|
1604
|
+
function makePair(key, value) { const pair = {}; pair[key] = value; return pair; }
|
|
1605
|
+
|
|
1606
|
+
// 服务端快照回写(configVersion 一并更新;persisted=false 时如实提示落盘失败)
|
|
1607
|
+
function applyServerResult(res) {
|
|
1608
|
+
if (!res || typeof res !== 'object') return;
|
|
1609
|
+
setSnapshot(function (prev) {
|
|
1610
|
+
return {
|
|
1611
|
+
fields: res.fields || (prev && prev.fields) || {},
|
|
1612
|
+
colors: res.colors || (prev && prev.colors) || {},
|
|
1613
|
+
configVersion: typeof res.configVersion === 'number' ? res.configVersion : ((prev && prev.configVersion) || 0),
|
|
1614
|
+
};
|
|
1615
|
+
});
|
|
1616
|
+
if (res.persisted === false) setNotice('改动已生效,但写入本地配置文件失败:' + (res.warning || '未知原因'));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// 乐观更新 + 失败回退(参照 density toggle)+ 版本号守卫
|
|
1620
|
+
function commit(patch, applyOptimistic, revertOptimistic, errorPrefix) {
|
|
1621
|
+
setOpError(null);
|
|
1622
|
+
setNotice(null);
|
|
1623
|
+
const seq = ++opSeqRef.current;
|
|
1624
|
+
applyOptimistic();
|
|
1625
|
+
beginOp();
|
|
1626
|
+
rpc('setFieldConfig', patch).then(function (res) {
|
|
1627
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
1628
|
+
endOp();
|
|
1629
|
+
applyServerResult(res);
|
|
1630
|
+
bibSetDispatchChanged();
|
|
1631
|
+
}).catch(function (err) {
|
|
1632
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
1633
|
+
endOp();
|
|
1634
|
+
revertOptimistic();
|
|
1635
|
+
setOpError(errorPrefix + '保存失败:' + bibSetOperationMessage(err));
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function setFieldFlag(id, next) {
|
|
1640
|
+
const previous = fieldOn(id);
|
|
1641
|
+
if (previous === next) return;
|
|
1642
|
+
commit({ fields: makePair(id, next) },
|
|
1643
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, next)) }); }); },
|
|
1644
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, previous)) }); }); },
|
|
1645
|
+
'「' + labelOf(id) + '」');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function setColor(id, next) {
|
|
1649
|
+
const previous = colorOf(id);
|
|
1650
|
+
if (previous === next) {
|
|
1651
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, undefined)); });
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
commit({ colors: makePair(id, next) },
|
|
1655
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, next)) }); }); },
|
|
1656
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, previous)) }); }); },
|
|
1657
|
+
'「' + labelOf(id) + '」颜色');
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// hex 输入:输入中仅标记非法(描红 + aria-invalid);Enter/失焦时合法才提交,非法回退当前值
|
|
1661
|
+
function hexDraftOf(id) {
|
|
1662
|
+
const draft = hexDrafts[id];
|
|
1663
|
+
return typeof draft === 'string' ? draft : null;
|
|
1664
|
+
}
|
|
1665
|
+
function committedHexText(id) {
|
|
1666
|
+
const value = colorOf(id);
|
|
1667
|
+
if (value === null) return '';
|
|
1668
|
+
return PRESET_COLOR_SET.has(value) ? value.toUpperCase() : value;
|
|
1669
|
+
}
|
|
1670
|
+
function onHexChange(id, raw) {
|
|
1671
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, raw)); });
|
|
1672
|
+
}
|
|
1673
|
+
function commitHex(id) {
|
|
1674
|
+
const draft = hexDraftOf(id);
|
|
1675
|
+
if (draft === null) return;
|
|
1676
|
+
const value = draft.trim();
|
|
1677
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, undefined)); });
|
|
1678
|
+
if (value.length === 0) return;
|
|
1679
|
+
if (!BIB_SET_HEX_PATTERN.test(value)) {
|
|
1680
|
+
setOpError('「' + labelOf(id) + '」颜色格式无效:请输入 #RRGGBB(例如 #0044CC)');
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
setColor(id, value.toUpperCase());
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function runReset(kind) {
|
|
1687
|
+
setOpError(null);
|
|
1688
|
+
setNotice(null);
|
|
1689
|
+
const seq = ++opSeqRef.current;
|
|
1690
|
+
beginOp();
|
|
1691
|
+
rpc(kind === 'colors' ? 'resetFieldColors' : 'resetFieldConfig').then(function (res) {
|
|
1692
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
1693
|
+
endOp();
|
|
1694
|
+
applyServerResult(res);
|
|
1695
|
+
bibSetDispatchChanged();
|
|
1696
|
+
setNotice(kind === 'colors' ? '已恢复默认颜色' : '已恢复默认标签');
|
|
1697
|
+
}).catch(function (err) {
|
|
1698
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
1699
|
+
endOp();
|
|
1700
|
+
setOpError('重置失败:' + bibSetOperationMessage(err));
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ---- 渲染 ----
|
|
1705
|
+
const groupsChildren = [];
|
|
1706
|
+
for (let g = 0; g < FIELD_GROUP_ORDER.length; g++) {
|
|
1707
|
+
const group = FIELD_GROUP_ORDER[g];
|
|
1708
|
+
const groupFields = FIELD_REGISTRY.filter(function (f) { return f.group === group; });
|
|
1709
|
+
if (groupFields.length === 0) continue;
|
|
1710
|
+
const rows = [];
|
|
1711
|
+
for (let i = 0; i < groupFields.length; i++) {
|
|
1712
|
+
const field = groupFields[i];
|
|
1713
|
+
const isAnchor = field.anchor === true;
|
|
1714
|
+
const descParts = [];
|
|
1715
|
+
// D6:锚点与其他字段同等可隐藏——「身份锚点」仅作为说明保留,不再恒开/禁用
|
|
1716
|
+
if (isAnchor) descParts.push('身份锚点:当前对话的服务商与模型标识。');
|
|
1717
|
+
const modeText = bibSetModeText(field);
|
|
1718
|
+
if (modeText) descParts.push(modeText + '。');
|
|
1719
|
+
if (field.note) descParts.push(field.note);
|
|
1720
|
+
if (field.suggestKeep) descParts.push('关闭后相应提示不再出现,建议保留。');
|
|
1721
|
+
rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
|
|
1722
|
+
React.createElement('div', { className: 'bib-set-rowText' },
|
|
1723
|
+
React.createElement('div', { className: 'bib-set-rowTitle' },
|
|
1724
|
+
field.label,
|
|
1725
|
+
field.suggestKeep ? React.createElement('span', { className: 'bib-set-keep' }, '建议保留') : null),
|
|
1726
|
+
React.createElement('div', { className: 'bib-set-rowDesc' }, descParts.join(''))),
|
|
1727
|
+
bibSetSwitch({
|
|
1728
|
+
label: '显示' + field.label,
|
|
1729
|
+
checked: fieldOn(field.id),
|
|
1730
|
+
title: fieldOn(field.id) ? '点击隐藏' : '点击显示',
|
|
1731
|
+
onToggle: function (next) { setFieldFlag(field.id, next); },
|
|
1732
|
+
})));
|
|
1733
|
+
}
|
|
1734
|
+
groupsChildren.push(React.createElement('div', { key: 'g-' + group },
|
|
1735
|
+
React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
|
|
1736
|
+
rows));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const colorChildren = [];
|
|
1740
|
+
for (let g = 0; g < FIELD_GROUP_ORDER.length; g++) {
|
|
1741
|
+
const group = FIELD_GROUP_ORDER[g];
|
|
1742
|
+
const groupFields = FIELD_REGISTRY.filter(function (f) { return f.group === group; });
|
|
1743
|
+
if (groupFields.length === 0) continue;
|
|
1744
|
+
const rows = [];
|
|
1745
|
+
for (let i = 0; i < groupFields.length; i++) {
|
|
1746
|
+
const field = groupFields[i];
|
|
1747
|
+
const value = colorOf(field.id);
|
|
1748
|
+
const isPreset = value !== null && PRESET_COLOR_SET.has(value);
|
|
1749
|
+
const isHex = value !== null && !isPreset;
|
|
1750
|
+
const draft = hexDraftOf(field.id);
|
|
1751
|
+
const hexValue = draft !== null ? draft : committedHexText(field.id);
|
|
1752
|
+
const hexInvalid = draft !== null && draft.trim().length > 0 && !BIB_SET_HEX_PATTERN.test(draft.trim());
|
|
1753
|
+
const wellValue = isHex ? value : (isPreset ? (BIB_SET_PRESET_WELL_HEX[value] || '#333333') : '#333333');
|
|
1754
|
+
const valueText = value === null ? '默认'
|
|
1755
|
+
: (isPreset ? (BIB_SET_PRESET_LABELS[value] || value) : value.toUpperCase());
|
|
1756
|
+
rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
|
|
1757
|
+
React.createElement('div', { className: 'bib-set-rowText' },
|
|
1758
|
+
React.createElement('div', { className: 'bib-set-rowTitle' }, field.label),
|
|
1759
|
+
React.createElement('div', { className: 'bib-set-rowDesc' }, '当前:' + valueText + (field.anchor === true ? '(身份锚点)' : ''))),
|
|
1760
|
+
React.createElement('div', { className: 'bib-set-controls' },
|
|
1761
|
+
bibSetPalette({
|
|
1762
|
+
label: field.label + '的预设颜色',
|
|
1763
|
+
value: value,
|
|
1764
|
+
onSelect: function (next) { setColor(field.id, next); },
|
|
1765
|
+
}),
|
|
1766
|
+
React.createElement('label', { className: 'bib-set-well' },
|
|
1767
|
+
React.createElement('input', {
|
|
1768
|
+
type: 'color',
|
|
1769
|
+
'aria-label': field.label + '的自定义颜色',
|
|
1770
|
+
title: '自定义颜色(打开系统取色器)',
|
|
1771
|
+
value: wellValue,
|
|
1772
|
+
onChange: function (event) {
|
|
1773
|
+
const picked = event && event.target ? event.target.value : null;
|
|
1774
|
+
if (picked && BIB_SET_HEX_PATTERN.test(picked)) setColor(field.id, picked.toUpperCase());
|
|
1775
|
+
},
|
|
1776
|
+
})),
|
|
1777
|
+
React.createElement('input', {
|
|
1778
|
+
type: 'text',
|
|
1779
|
+
className: 'bib-set-hex',
|
|
1780
|
+
'aria-label': field.label + '的十六进制颜色',
|
|
1781
|
+
'aria-invalid': hexInvalid ? 'true' : 'false',
|
|
1782
|
+
'data-invalid': hexInvalid ? 'true' : 'false',
|
|
1783
|
+
placeholder: '#RRGGBB',
|
|
1784
|
+
spellCheck: false,
|
|
1785
|
+
maxLength: 7,
|
|
1786
|
+
value: hexValue,
|
|
1787
|
+
onChange: function (event) { onHexChange(field.id, event && event.target ? event.target.value : ''); },
|
|
1788
|
+
onBlur: function () { commitHex(field.id); },
|
|
1789
|
+
onKeyDown: function (event) { if (event.key === 'Enter') { event.preventDefault(); commitHex(field.id); } },
|
|
1790
|
+
}))));
|
|
1791
|
+
}
|
|
1792
|
+
colorChildren.push(React.createElement('div', { key: 'c-' + group },
|
|
1793
|
+
React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
|
|
1794
|
+
rows));
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
const feedback = [];
|
|
1798
|
+
if (opError) feedback.push(React.createElement('p', { key: 'err', className: 'bib-set-alert', role: 'alert' }, opError));
|
|
1799
|
+
else if (notice) feedback.push(React.createElement('p', { key: 'notice', className: 'bib-set-notice', role: 'status' }, notice));
|
|
1800
|
+
else if (saving) feedback.push(React.createElement('p', { key: 'saving', className: 'bib-set-notice', 'aria-live': 'polite' }, '正在保存…'));
|
|
1801
|
+
|
|
1802
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
1803
|
+
React.createElement('p', { className: 'bib-set-intro' },
|
|
1804
|
+
'选择信息底栏显示哪些信息、为每个信息挑一个颜色。改动即时生效并自动保存,重启后仍然保留;隐藏信息不会删除任何记账数据。'),
|
|
1805
|
+
React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-fields-title' },
|
|
1806
|
+
React.createElement('div', { className: 'bib-set-card-header' },
|
|
1807
|
+
React.createElement('h2', { id: 'bib-set-fields-title', className: 'bib-set-card-title' }, '显示字段'),
|
|
1808
|
+
React.createElement('div', { className: 'bib-set-card-desc' },
|
|
1809
|
+
'关闭的字段立刻从信息栏隐藏且不占位;「刷新失败」「账单未保存」等提示建议保留,方便发现数据异常。')),
|
|
1810
|
+
React.createElement('div', { className: 'bib-set-body' }, groupsChildren)),
|
|
1811
|
+
React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-colors-title' },
|
|
1812
|
+
React.createElement('div', { className: 'bib-set-card-header' },
|
|
1813
|
+
React.createElement('h2', { id: 'bib-set-colors-title', className: 'bib-set-card-title' }, '字段颜色'),
|
|
1814
|
+
React.createElement('div', { className: 'bib-set-card-desc' },
|
|
1815
|
+
'挑选预设色或用取色器、十六进制自定义;颜色会自动适配浅色与深色主题,保持可读。未改动前与现在的外观完全一致。')),
|
|
1816
|
+
React.createElement('div', { className: 'bib-set-body' }, colorChildren)),
|
|
1817
|
+
React.createElement('div', { className: 'bib-set-footer' },
|
|
1818
|
+
feedback,
|
|
1819
|
+
React.createElement('button', {
|
|
1820
|
+
type: 'button', className: 'bib-set-btn', disabled: saving,
|
|
1821
|
+
onClick: function () { runReset('fields'); },
|
|
1822
|
+
}, '重置标签'),
|
|
1823
|
+
React.createElement('button', {
|
|
1824
|
+
type: 'button', className: 'bib-set-btn', disabled: saving,
|
|
1825
|
+
onClick: function () { runReset('colors'); },
|
|
1826
|
+
}, '重置颜色')));
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// 注册入口:在信息栏 apply 完成后调用(见文件尾部的 module.exports 包装)。
|
|
1830
|
+
async function applyInfoBarSettingsSection(ctx) {
|
|
1831
|
+
let slots = ctx.slots || (ctx.get ? ctx.get('slots') : undefined);
|
|
1832
|
+
for (let i = 0; slots === undefined && i < 60; i++) {
|
|
1833
|
+
await new Promise(function (resolve) { window.setTimeout(resolve, 300); });
|
|
1834
|
+
slots = ctx.slots || (ctx.get ? ctx.get('slots') : undefined);
|
|
1835
|
+
}
|
|
1836
|
+
if (slots === undefined) {
|
|
1837
|
+
console.warn('[dsh-bottom-info-bar] slots 服务 18s 内未就绪,信息底栏设置页未注册');
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
ctx.effect(function () {
|
|
1841
|
+
return bibSetInstallStyles();
|
|
1842
|
+
}, 'dsh-bottom-info-bar: settings styles');
|
|
1843
|
+
ctx.slots.inject('settings.section', function () {
|
|
1844
|
+
return ctx.slots.register(
|
|
1845
|
+
{ name: 'settings.section', id: 'bottom-info-bar', order: 100, label: '信息底栏' },
|
|
1846
|
+
InfoBarSettingsSection);
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ---- 设置页扩展:包装模块导出,在信息栏 apply 完成后注册 settings.section ----
|
|
1851
|
+
;(function () {
|
|
1852
|
+
const baseExports = module.exports;
|
|
1853
|
+
module.exports = {
|
|
1854
|
+
inject: baseExports.inject,
|
|
1855
|
+
apply: async function (ctx) {
|
|
1856
|
+
await baseExports.apply(ctx);
|
|
1857
|
+
await applyInfoBarSettingsSection(ctx);
|
|
1858
|
+
},
|
|
1859
|
+
};
|
|
1860
|
+
})();
|
|
1861
|
+
|
|
1001
1862
|
return module.exports;
|
|
1002
1863
|
} });
|