dsh-bottom-info-bar 1.9.0 → 1.9.2
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 +1350 -1353
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -413,1451 +413,1448 @@ function installStyles() {
|
|
|
413
413
|
return function () { style.remove(); };
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
416
|
+
// ---------- v1.9.0 PR2 → v1.9.1 设置页(并入本文件,单文件化) ----------
|
|
417
|
+
// 原 plugin/src/client-settings.js 整体迁移至此(v1.9.1 拆除 build 跨文件拼接与 module.exports 重写机制):
|
|
418
|
+
// 与信息栏同一 factory 作用域,直接复用 React/rpc/FIELD_REGISTRY/PRESET_COLORS/PRESET_COLOR_SET;
|
|
419
|
+
// 注册动作进 apply(),与信息栏共用等好的 slots 变量、走同一 slots.inject 路径
|
|
420
|
+
// (官方 dsh-client-ui-settings-general 同型写法),不再有 async 串接。
|
|
421
|
+
// 约定:本段顶层标识符一律 bibSet* / InfoBarSettings* 前缀,绝不与主包重名。
|
|
422
|
+
const BIB_SET_EVENT = 'dsh-bib-config-changed';
|
|
423
|
+
const BIB_SET_PRESET_LABELS = { red: '红', green: '绿', blue: '蓝', purple: '紫', orange: '橙', neutral: '中性' };
|
|
424
|
+
// 原生取色器(input[type=color])在未自定义时显示的代表色(浅色主题值;实际信息栏渲染仍按主题变量)
|
|
425
|
+
const BIB_SET_PRESET_WELL_HEX = { red: '#D92D20', green: '#087F5B', blue: '#0044CC', purple: '#6941C6', orange: '#B54708', neutral: '#333333' };
|
|
426
|
+
const BIB_SET_HEX_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
427
|
+
// 「显示字段」两级分组(D6 用户拍板:原生在前、插件在后)——构建从 constants.js 注入(单一来源)
|
|
428
|
+
const FIELD_GROUP_ORDER = ['native', 'plugin'];
|
|
429
|
+
const FIELD_GROUP_LABELS = {
|
|
430
|
+
native: '原生字段',
|
|
431
|
+
plugin: '插件字段',
|
|
432
|
+
};
|
|
433
|
+
// 每行出现条件文案:modes → 中文(余额制/订阅制/账单制/原生统计行/通用)
|
|
434
|
+
const BIB_SET_MODE_LABELS = { balance: '余额制', subscription: '订阅制', billing: '账单制', native: '原生统计行', common: '通用' };
|
|
435
|
+
function bibSetModeText(field) {
|
|
436
|
+
if (!field || !Array.isArray(field.modes) || field.modes.length === 0) return '';
|
|
437
|
+
if (field.modes.length === 1 && field.modes[0] === 'native') return '原生统计行字段';
|
|
438
|
+
const labels = [];
|
|
439
|
+
for (let i = 0; i < field.modes.length; i++) {
|
|
440
|
+
const label = BIB_SET_MODE_LABELS[field.modes[i]];
|
|
441
|
+
if (label && labels.indexOf(label) === -1) labels.push(label);
|
|
442
|
+
}
|
|
443
|
+
return labels.length > 0 ? '出现于' + labels.join('、') : '';
|
|
444
|
+
}
|
|
435
445
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
let occupantDispose = null;
|
|
441
|
-
const densityListeners = new Set();
|
|
442
|
-
const densityBusyListeners = new Set();
|
|
443
|
-
// Survives composer remounts, so returning to an already visited session
|
|
444
|
-
// does not require even one paint of an intermediate state.
|
|
445
|
-
const sessionModelCache = new Map();
|
|
446
|
-
function applyMode() {
|
|
447
|
-
if (occupantDispose) { occupantDispose(); occupantDispose = null; }
|
|
448
|
-
occupantDispose = slots.register(
|
|
449
|
-
// 静态注册无动态沙箱的优先级自动分配:显式给低 priority(最低者渲染)以遮蔽原生 stats 栏(priority 0)
|
|
450
|
-
{ name: 'conversation.composer.dock', id: 'stats', priority: -1000 },
|
|
451
|
-
function (slotProps) {
|
|
452
|
-
return React.createElement(BottomInfoBar, Object.assign({}, slotProps, { density: density, onToggleDensity: onToggleDensity }));
|
|
453
|
-
}
|
|
454
|
-
);
|
|
455
|
-
}
|
|
446
|
+
function bibSetDispatchChanged() {
|
|
447
|
+
// 设置页保存成功后广播:信息栏监听并立即重拉配置(宿主内存缓存,即回)
|
|
448
|
+
try { document.dispatchEvent(new CustomEvent(BIB_SET_EVENT)); } catch (err) { /* 事件总线不可用时静默:30s 周期校准兜底 */ }
|
|
449
|
+
}
|
|
456
450
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
densityListeners.forEach(function (listener) { listener(next); });
|
|
461
|
-
}
|
|
451
|
+
function bibSetOperationMessage(err) {
|
|
452
|
+
return String((err && err.message) || err || '请稍后再试');
|
|
453
|
+
}
|
|
462
454
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
455
|
+
// ---------- 设置页样式(融入 DSH 设置面板:卡片/行布局/控件全部走 --dsw-alias-* 令牌) ----------
|
|
456
|
+
function bibSetInstallStyles() {
|
|
457
|
+
const id = 'dsh-bottom-info-bar-settings';
|
|
458
|
+
const existing = document.querySelector('style[data-plugin-css="' + id + '"]');
|
|
459
|
+
if (existing !== null) return function () {};
|
|
460
|
+
const style = document.createElement('style');
|
|
461
|
+
style.dataset.plugin = 'dsh-bottom-info-bar';
|
|
462
|
+
style.dataset.pluginCss = id;
|
|
463
|
+
style.textContent = `
|
|
464
|
+
.bib-set-root { --bib-set-brand: var(--dsw-alias-brand-primary, #4d6bfe); }
|
|
465
|
+
/* 页面标题行(M2 首渲骨架):数据未到也先渲染标题,绝不白屏 */
|
|
466
|
+
.bib-set-page-title { margin: 0 0 4px; font-size: 17px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); }
|
|
467
|
+
.bib-settings { max-width: 720px; box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; color: var(--dsw-alias-label-primary); }
|
|
468
|
+
.bib-set-intro { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
469
|
+
.bib-set-status { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 20px; }
|
|
470
|
+
.bib-set-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; }
|
|
471
|
+
.bib-set-card-header { display: flex; flex-direction: column; gap: 4px; padding: 14px 16px; }
|
|
472
|
+
.bib-set-card-title { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); }
|
|
473
|
+
.bib-set-card-desc { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
|
|
474
|
+
.bib-set-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 0 0 6px; }
|
|
475
|
+
.bib-set-group-title { margin: 12px 0 2px; font-size: 12px; font-weight: 500; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
476
|
+
.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); }
|
|
477
|
+
.bib-set-row:last-child { border-bottom: none; }
|
|
478
|
+
.bib-set-rowText { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 200px; }
|
|
479
|
+
.bib-set-rowTitle { display: flex; align-items: center; gap: 6px; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
|
|
480
|
+
.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; }
|
|
481
|
+
.bib-set-rowDesc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
482
|
+
/* 开关:iOS 原生质感(40×24 轨道 + 18px 圆钮),语义 = role:switch + aria-checked */
|
|
483
|
+
.bib-set-switch { appearance: none; background: 0 0; border: 0; padding: 0; margin: 0; cursor: pointer; display: inline-flex; flex: none; border-radius: 12px; }
|
|
484
|
+
.bib-set-switch:disabled { cursor: default; opacity: 0.5; }
|
|
485
|
+
.bib-set-switch:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
486
|
+
.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); }
|
|
487
|
+
.bib-set-switch-track[data-on="true"] { background: var(--bib-set-brand); }
|
|
488
|
+
.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); }
|
|
489
|
+
.bib-set-switch-track[data-on="true"] .bib-set-switch-thumb { transform: translateX(16px); }
|
|
490
|
+
/* 色板圆点:role:radio + roving tabindex(方向键/Home/End 可达),选中态外圈描边 */
|
|
491
|
+
.bib-set-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; }
|
|
492
|
+
.bib-set-dots { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
493
|
+
.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; }
|
|
494
|
+
.bib-set-dot:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
495
|
+
.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); }
|
|
496
|
+
.bib-set-dot-core { display: block; width: 14px; height: 14px; border-radius: 50%; }
|
|
497
|
+
.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)); }
|
|
498
|
+
/* 原生取色器色井:保留系统行为,仅样式化为圆角色井 */
|
|
499
|
+
.bib-set-well { display: inline-flex; flex: none; }
|
|
500
|
+
.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; }
|
|
501
|
+
.bib-set-well input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
|
|
502
|
+
.bib-set-well input[type="color"]::-webkit-color-swatch { border: none; border-radius: 5px; }
|
|
503
|
+
.bib-set-well input[type="color"]::-moz-color-swatch { border: none; border-radius: 5px; }
|
|
504
|
+
.bib-set-well input[type="color"]:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
505
|
+
/* hex 输入:等宽字体、即时校验(非法描红 + aria-invalid),Enter/失焦提交,非法回退 */
|
|
506
|
+
.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; }
|
|
507
|
+
.bib-set-hex:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 1px; }
|
|
508
|
+
.bib-set-hex[data-invalid="true"] { border-color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-error, #d92d20)); }
|
|
509
|
+
.bib-set-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; padding: 2px 0 10px; }
|
|
510
|
+
.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; }
|
|
511
|
+
.bib-set-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.08)); }
|
|
512
|
+
.bib-set-btn:focus-visible { outline: 2px solid var(--bib-set-brand); outline-offset: 2px; }
|
|
513
|
+
.bib-set-btn:disabled { opacity: 0.5; cursor: default; }
|
|
514
|
+
.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; }
|
|
515
|
+
.bib-set-notice { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; flex: 1 1 auto; min-width: 0; }
|
|
516
|
+
@media (prefers-reduced-motion: reduce) { .bib-set-switch-track, .bib-set-switch-thumb { transition: none; } }
|
|
517
|
+
`;
|
|
518
|
+
document.head.appendChild(style);
|
|
519
|
+
return function () { style.remove(); };
|
|
520
|
+
}
|
|
467
521
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
522
|
+
// ---------- 控件 ----------
|
|
523
|
+
function bibSetSwitch(props) {
|
|
524
|
+
const checked = !!props.checked;
|
|
525
|
+
return React.createElement('button', {
|
|
526
|
+
type: 'button',
|
|
527
|
+
className: 'bib-set-switch',
|
|
528
|
+
role: 'switch',
|
|
529
|
+
'aria-checked': checked,
|
|
530
|
+
'aria-label': props.label,
|
|
531
|
+
disabled: props.disabled === true,
|
|
532
|
+
title: props.title,
|
|
533
|
+
onClick: function () { if (props.onToggle) props.onToggle(!checked); },
|
|
534
|
+
},
|
|
535
|
+
React.createElement('span', { className: 'bib-set-switch-track', 'data-on': checked ? 'true' : 'false', 'aria-hidden': 'true' },
|
|
536
|
+
React.createElement('span', { className: 'bib-set-switch-thumb' })));
|
|
537
|
+
}
|
|
485
538
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
539
|
+
// 色板圆点组:默认 + 预设色名;role=radiogroup/radio + roving tabindex(方向键/Home/End)
|
|
540
|
+
function bibSetPalette(props) {
|
|
541
|
+
const options = ['default'].concat(PRESET_COLORS);
|
|
542
|
+
const refs = React.useRef({});
|
|
543
|
+
const isSelected = function (option) {
|
|
544
|
+
return option === 'default' ? props.value === null : props.value === option;
|
|
545
|
+
};
|
|
546
|
+
const select = function (option) {
|
|
547
|
+
if (props.onSelect) props.onSelect(option === 'default' ? null : option);
|
|
548
|
+
};
|
|
549
|
+
const move = function (from, step) {
|
|
550
|
+
const next = (from + step + options.length) % options.length;
|
|
551
|
+
const node = refs.current[next];
|
|
552
|
+
if (node && typeof node.focus === 'function') node.focus();
|
|
553
|
+
select(options[next]);
|
|
554
|
+
};
|
|
555
|
+
const onKey = function (event, index) {
|
|
556
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { event.preventDefault(); move(index, 1); }
|
|
557
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { event.preventDefault(); move(index, -1); }
|
|
558
|
+
else if (event.key === 'Home') { event.preventDefault(); move(index, -index); }
|
|
559
|
+
else if (event.key === 'End') { event.preventDefault(); move(index, options.length - 1 - index); }
|
|
560
|
+
};
|
|
561
|
+
const children = options.map(function (option, index) {
|
|
562
|
+
const selected = isSelected(option);
|
|
563
|
+
const isDefault = option === 'default';
|
|
564
|
+
const coreStyle = {};
|
|
565
|
+
if (!isDefault) coreStyle.background = 'var(--bi-palette-' + option + ')';
|
|
566
|
+
const ariaLabel = isDefault ? '恢复默认颜色' : (BIB_SET_PRESET_LABELS[option] || option);
|
|
567
|
+
return React.createElement('button', {
|
|
568
|
+
key: option,
|
|
569
|
+
type: 'button',
|
|
570
|
+
ref: function (node) { refs.current[index] = node; },
|
|
571
|
+
className: 'bib-set-dot' + (isDefault ? ' bib-set-dot-default' : ''),
|
|
572
|
+
role: 'radio',
|
|
573
|
+
'aria-checked': selected,
|
|
574
|
+
'aria-label': ariaLabel,
|
|
575
|
+
title: ariaLabel,
|
|
576
|
+
tabIndex: selected ? 0 : -1,
|
|
577
|
+
onClick: function () { select(option); },
|
|
578
|
+
onKeyDown: function (event) { onKey(event, index); },
|
|
579
|
+
}, React.createElement('span', { className: 'bib-set-dot-core', style: coreStyle, 'aria-hidden': 'true' }));
|
|
580
|
+
});
|
|
581
|
+
return React.createElement('span', { className: 'bib-set-dots', role: 'radiogroup', 'aria-label': props.label }, children);
|
|
582
|
+
}
|
|
490
583
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
setDensity(cfg.infoDensity);
|
|
497
|
-
}
|
|
498
|
-
} catch (err) { /* 默认完整 */ }
|
|
584
|
+
// ---------- 页面组件 ----------
|
|
585
|
+
// M2 首渲骨架:页面标题行——任何状态下都先输出可见标题(数据未到、加载失败也一样有东西看)
|
|
586
|
+
function bibSetPageTitle() {
|
|
587
|
+
return React.createElement('h1', { className: 'bib-set-page-title' }, '信息底栏设置');
|
|
588
|
+
}
|
|
499
589
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
590
|
+
function InfoBarSettingsSection(props) {
|
|
591
|
+
void props; // 与官方 section 组件同签名(外壳会传入 renderSlot/close 等,本页不使用)
|
|
592
|
+
const [snapshot, setSnapshot] = React.useState(null); // { fields, colors, configVersion }
|
|
593
|
+
const [status, setStatus] = React.useState('loading');
|
|
594
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
595
|
+
const [opError, setOpError] = React.useState(null);
|
|
596
|
+
const [notice, setNotice] = React.useState(null);
|
|
597
|
+
const [saving, setSaving] = React.useState(false);
|
|
598
|
+
const [hexDrafts, setHexDrafts] = React.useState({});
|
|
599
|
+
const opSeqRef = React.useRef(0); // 版本号守卫:慢响应绝不覆盖更新的操作
|
|
600
|
+
const savingCountRef = React.useRef(0);
|
|
509
601
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const [modelSource, setModelSource] = React.useState('pending');
|
|
527
|
-
const [displayDensity, setDisplayDensity] = React.useState(props.density);
|
|
528
|
-
const [isDensitySaving, setIsDensitySaving] = React.useState(toggling);
|
|
602
|
+
React.useEffect(function () {
|
|
603
|
+
let active = true;
|
|
604
|
+
rpc('getFieldConfig').then(function (cfg) {
|
|
605
|
+
if (!active) return;
|
|
606
|
+
if (cfg && typeof cfg === 'object' && cfg.fields) {
|
|
607
|
+
setSnapshot({ fields: cfg.fields, colors: cfg.colors || {}, configVersion: cfg.configVersion || 0 });
|
|
608
|
+
setStatus('ready');
|
|
609
|
+
} else {
|
|
610
|
+
setLoadError('配置暂时无法读取'); setStatus('error');
|
|
611
|
+
}
|
|
612
|
+
}).catch(function (err) {
|
|
613
|
+
if (!active) return;
|
|
614
|
+
setLoadError(bibSetOperationMessage(err)); setStatus('error');
|
|
615
|
+
});
|
|
616
|
+
return function () { active = false; };
|
|
617
|
+
}, []);
|
|
529
618
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
densityListeners.delete(setDisplayDensity);
|
|
539
|
-
densityBusyListeners.delete(setIsDensitySaving);
|
|
540
|
-
};
|
|
541
|
-
}, []);
|
|
619
|
+
const beginOp = React.useCallback(function () {
|
|
620
|
+
savingCountRef.current += 1;
|
|
621
|
+
setSaving(true);
|
|
622
|
+
}, []);
|
|
623
|
+
const endOp = React.useCallback(function () {
|
|
624
|
+
savingCountRef.current = Math.max(0, savingCountRef.current - 1);
|
|
625
|
+
if (savingCountRef.current === 0) setSaving(false);
|
|
626
|
+
}, []);
|
|
542
627
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
628
|
+
// M2 屏显防护(铁律:绝不允许白屏):渲染主体整体包进 try/catch——
|
|
629
|
+
// 任何渲染期异常都在页面内渲染为可见红色错误框(role=alert,含错误信息),而不是中断成白屏
|
|
630
|
+
try {
|
|
631
|
+
// 首渲骨架:即便配置尚未取回,也立刻渲染「信息底栏设置」标题行 + 载入说明
|
|
632
|
+
if (status === 'loading') {
|
|
633
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
634
|
+
bibSetPageTitle(),
|
|
635
|
+
React.createElement('p', { className: 'bib-set-status' }, '正在载入信息底栏设置…'));
|
|
636
|
+
}
|
|
637
|
+
// RPC 加载失败:照旧显示可见错误文字(角色 alert),页面不空白
|
|
638
|
+
if (status === 'error') {
|
|
639
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
640
|
+
bibSetPageTitle(),
|
|
641
|
+
React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, '信息底栏设置暂时无法读取:' + (loadError || '请稍后再试')));
|
|
642
|
+
}
|
|
553
643
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const sessions = ctx.get ? ctx.get('sessions') : null;
|
|
564
|
-
const cur = sessions && sessions.list && sessions.list.getSnapshot().current;
|
|
565
|
-
if (cur) return cur;
|
|
566
|
-
} catch (e) { /* 拿不到则返回空串,host 端对空串返回 null(显示 ¥0.000) */ }
|
|
567
|
-
return '';
|
|
568
|
-
}, []);
|
|
569
|
-
const sessionId = resolveSessionId();
|
|
644
|
+
const fieldsById = {};
|
|
645
|
+
for (let i = 0; i < FIELD_REGISTRY.length; i++) fieldsById[FIELD_REGISTRY[i].id] = FIELD_REGISTRY[i];
|
|
646
|
+
function fieldOn(id) { return snapshot.fields[id] !== false; }
|
|
647
|
+
function colorOf(id) {
|
|
648
|
+
const value = snapshot.colors[id];
|
|
649
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
650
|
+
}
|
|
651
|
+
function labelOf(id) { const f = fieldsById[id]; return f ? f.label : id; }
|
|
652
|
+
function makePair(key, value) { const pair = {}; pair[key] = value; return pair; }
|
|
570
653
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
setModelSource('unavailable');
|
|
584
|
-
return function () {};
|
|
585
|
-
}
|
|
586
|
-
setModelSource('available');
|
|
587
|
-
const directory = directories.directoryFor(sessionId);
|
|
588
|
-
const publish = function () {
|
|
589
|
-
if (!active || !directory.store || typeof directory.store.getSnapshot !== 'function') return;
|
|
590
|
-
const snapshot = directory.store.getSnapshot();
|
|
591
|
-
const selected = snapshot && snapshot.current;
|
|
592
|
-
if (!selected || typeof selected.provider !== 'string' || typeof selected.model !== 'string') return;
|
|
593
|
-
const group = Array.isArray(snapshot.groups) ? snapshot.groups.find(function (g) { return g && g.id === selected.provider; }) : null;
|
|
594
|
-
const model = group && Array.isArray(group.models) ? group.models.find(function (m) { return m && m.id === selected.model; }) : null;
|
|
595
|
-
const inputModalities = model && Array.isArray(model.inputModalities) ? model.inputModalities : null;
|
|
596
|
-
const value = {
|
|
597
|
-
sessionId: sessionId,
|
|
598
|
-
provider: selected.provider,
|
|
599
|
-
model: selected.model,
|
|
600
|
-
providerDisplay: group && typeof group.name === 'string' ? group.name : selected.provider,
|
|
601
|
-
modelDisplay: model && typeof model.name === 'string' ? model.name : selected.model,
|
|
602
|
-
// true/false 来自 DSH 的明确目录 metadata;null 表示仍待 host 查询,不能误画成文本模型。
|
|
603
|
-
acceptsImageInput: inputModalities === null ? null : inputModalities.indexOf('image') !== -1,
|
|
604
|
-
};
|
|
605
|
-
sessionModelCache.set(sessionId, value);
|
|
606
|
-
setSessionModel(value);
|
|
607
|
-
};
|
|
608
|
-
publish();
|
|
609
|
-
if (directory.store && typeof directory.store.subscribe === 'function') stop = directory.store.subscribe(publish);
|
|
610
|
-
} catch (err) { setModelSource('unavailable'); /* old DSH: retain the RPC fallback */ }
|
|
611
|
-
return function () { active = false; if (typeof stop === 'function') stop(); };
|
|
612
|
-
}, [sessionId]);
|
|
654
|
+
// 服务端快照回写(configVersion 一并更新;persisted=false 时如实提示落盘失败)
|
|
655
|
+
function applyServerResult(res) {
|
|
656
|
+
if (!res || typeof res !== 'object') return;
|
|
657
|
+
setSnapshot(function (prev) {
|
|
658
|
+
return {
|
|
659
|
+
fields: res.fields || (prev && prev.fields) || {},
|
|
660
|
+
colors: res.colors || (prev && prev.colors) || {},
|
|
661
|
+
configVersion: typeof res.configVersion === 'number' ? res.configVersion : ((prev && prev.configVersion) || 0),
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
if (res.persisted === false) setNotice('改动已生效,但写入本地配置文件失败:' + (res.warning || '未知原因'));
|
|
665
|
+
}
|
|
613
666
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
667
|
+
// 乐观更新 + 失败回退(参照 density toggle)+ 版本号守卫
|
|
668
|
+
function commit(patch, applyOptimistic, revertOptimistic, errorPrefix) {
|
|
669
|
+
setOpError(null);
|
|
670
|
+
setNotice(null);
|
|
671
|
+
const seq = ++opSeqRef.current;
|
|
672
|
+
applyOptimistic();
|
|
673
|
+
beginOp();
|
|
674
|
+
rpc('setFieldConfig', patch).then(function (res) {
|
|
675
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
676
|
+
endOp();
|
|
677
|
+
applyServerResult(res);
|
|
678
|
+
bibSetDispatchChanged();
|
|
679
|
+
}).catch(function (err) {
|
|
680
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
681
|
+
endOp();
|
|
682
|
+
revertOptimistic();
|
|
683
|
+
setOpError(errorPrefix + '保存失败:' + bibSetOperationMessage(err));
|
|
684
|
+
});
|
|
685
|
+
}
|
|
621
686
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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; }
|
|
634
|
-
const signal = abortRef.current ? abortRef.current.signal : null;
|
|
635
|
-
// 逐接口容错:allSettled 等全部 settle(最坏 20s 超时兜底),任一失败只降级该端点,
|
|
636
|
-
// 不拖垮其他成功数据;合并逻辑在 mergeLoadResults(失败端点保留旧值 + 记录错误)
|
|
637
|
-
Promise.allSettled([
|
|
638
|
-
rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider, force: force } : (force ? { force: true } : null), signal),
|
|
639
|
-
rpc('getPricing', selectionArgs, signal),
|
|
640
|
-
rpc('getUsageSummary', Object.assign({ sessionId: sessionId }, selectionArgs), signal),
|
|
641
|
-
rpc('getBillingMode', selectionArgs, signal),
|
|
642
|
-
rpc('getSubscriptionSnapshot', selectionArgs, signal),
|
|
643
|
-
rpc('getBillingStatus', selectionArgs, signal),
|
|
644
|
-
]).then(function (results) {
|
|
645
|
-
// Do not allow a late A response to overwrite newly active B.
|
|
646
|
-
if ((signal && signal.aborted) || requestVersion !== loadVersionRef.current) return;
|
|
647
|
-
setState(function (s) { return mergeLoadResults(s, results); });
|
|
648
|
-
});
|
|
649
|
-
}, [resolveSessionId, activeSessionModel, sessionId]);
|
|
687
|
+
function setFieldFlag(id, next) {
|
|
688
|
+
const previous = fieldOn(id);
|
|
689
|
+
if (previous === next) return;
|
|
690
|
+
commit({ fields: makePair(id, next) },
|
|
691
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, next)) }); }); },
|
|
692
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { fields: Object.assign({}, s.fields, makePair(id, previous)) }); }); },
|
|
693
|
+
'「' + labelOf(id) + '」');
|
|
694
|
+
}
|
|
650
695
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
696
|
+
function setColor(id, next) {
|
|
697
|
+
const previous = colorOf(id);
|
|
698
|
+
if (previous === next) {
|
|
699
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, undefined)); });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
commit({ colors: makePair(id, next) },
|
|
703
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, next)) }); }); },
|
|
704
|
+
function () { setSnapshot(function (s) { return Object.assign({}, s, { colors: Object.assign({}, s.colors, makePair(id, previous)) }); }); },
|
|
705
|
+
'「' + labelOf(id) + '」颜色');
|
|
706
|
+
}
|
|
656
707
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
708
|
+
// hex 输入:输入中仅标记非法(描红 + aria-invalid);Enter/失焦时合法才提交,非法回退当前值
|
|
709
|
+
function hexDraftOf(id) {
|
|
710
|
+
const draft = hexDrafts[id];
|
|
711
|
+
return typeof draft === 'string' ? draft : null;
|
|
712
|
+
}
|
|
713
|
+
function committedHexText(id) {
|
|
714
|
+
const value = colorOf(id);
|
|
715
|
+
if (value === null) return '';
|
|
716
|
+
return PRESET_COLOR_SET.has(value) ? value.toUpperCase() : value;
|
|
717
|
+
}
|
|
718
|
+
function onHexChange(id, raw) {
|
|
719
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, raw)); });
|
|
720
|
+
}
|
|
721
|
+
function commitHex(id) {
|
|
722
|
+
const draft = hexDraftOf(id);
|
|
723
|
+
if (draft === null) return;
|
|
724
|
+
const value = draft.trim();
|
|
725
|
+
setHexDrafts(function (drafts) { return Object.assign({}, drafts, makePair(id, undefined)); });
|
|
726
|
+
if (value.length === 0) return;
|
|
727
|
+
if (!BIB_SET_HEX_PATTERN.test(value)) {
|
|
728
|
+
setOpError('「' + labelOf(id) + '」颜色格式无效:请输入 #RRGGBB(例如 #0044CC)');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
setColor(id, value.toUpperCase());
|
|
732
|
+
}
|
|
665
733
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
734
|
+
function runReset(kind) {
|
|
735
|
+
setOpError(null);
|
|
736
|
+
setNotice(null);
|
|
737
|
+
const seq = ++opSeqRef.current;
|
|
738
|
+
beginOp();
|
|
739
|
+
rpc(kind === 'colors' ? 'resetFieldColors' : 'resetFieldConfig').then(function (res) {
|
|
740
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
741
|
+
endOp();
|
|
742
|
+
applyServerResult(res);
|
|
743
|
+
bibSetDispatchChanged();
|
|
744
|
+
setNotice(kind === 'colors' ? '已恢复默认颜色' : '已恢复默认标签');
|
|
745
|
+
}).catch(function (err) {
|
|
746
|
+
if (seq !== opSeqRef.current) { endOp(); return; }
|
|
747
|
+
endOp();
|
|
748
|
+
setOpError('重置失败:' + bibSetOperationMessage(err));
|
|
749
|
+
});
|
|
750
|
+
}
|
|
671
751
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
752
|
+
// ---- 渲染 ----
|
|
753
|
+
const groupsChildren = [];
|
|
754
|
+
for (let g = 0; g < FIELD_GROUP_ORDER.length; g++) {
|
|
755
|
+
const group = FIELD_GROUP_ORDER[g];
|
|
756
|
+
const groupFields = FIELD_REGISTRY.filter(function (f) { return f.group === group; });
|
|
757
|
+
if (groupFields.length === 0) continue;
|
|
758
|
+
const rows = [];
|
|
759
|
+
for (let i = 0; i < groupFields.length; i++) {
|
|
760
|
+
const field = groupFields[i];
|
|
761
|
+
const isAnchor = field.anchor === true;
|
|
762
|
+
const descParts = [];
|
|
763
|
+
// D6:锚点与其他字段同等可隐藏——「身份锚点」仅作为说明保留,不再恒开/禁用
|
|
764
|
+
if (isAnchor) descParts.push('身份锚点:当前对话的服务商与模型标识。');
|
|
765
|
+
const modeText = bibSetModeText(field);
|
|
766
|
+
if (modeText) descParts.push(modeText + '。');
|
|
767
|
+
if (field.note) descParts.push(field.note);
|
|
768
|
+
if (field.suggestKeep) descParts.push('关闭后相应提示不再出现,建议保留。');
|
|
769
|
+
rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
|
|
770
|
+
React.createElement('div', { className: 'bib-set-rowText' },
|
|
771
|
+
React.createElement('div', { className: 'bib-set-rowTitle' },
|
|
772
|
+
field.label,
|
|
773
|
+
field.suggestKeep ? React.createElement('span', { className: 'bib-set-keep' }, '建议保留') : null),
|
|
774
|
+
React.createElement('div', { className: 'bib-set-rowDesc' }, descParts.join(''))),
|
|
775
|
+
bibSetSwitch({
|
|
776
|
+
label: '显示' + field.label,
|
|
777
|
+
checked: fieldOn(field.id),
|
|
778
|
+
title: fieldOn(field.id) ? '点击隐藏' : '点击显示',
|
|
779
|
+
onToggle: function (next) { setFieldFlag(field.id, next); },
|
|
780
|
+
})));
|
|
781
|
+
}
|
|
782
|
+
groupsChildren.push(React.createElement('div', { key: 'g-' + group },
|
|
783
|
+
React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
|
|
784
|
+
rows));
|
|
785
|
+
}
|
|
684
786
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
787
|
+
const colorChildren = [];
|
|
788
|
+
for (let g = 0; g < FIELD_GROUP_ORDER.length; g++) {
|
|
789
|
+
const group = FIELD_GROUP_ORDER[g];
|
|
790
|
+
const groupFields = FIELD_REGISTRY.filter(function (f) { return f.group === group; });
|
|
791
|
+
if (groupFields.length === 0) continue;
|
|
792
|
+
const rows = [];
|
|
793
|
+
for (let i = 0; i < groupFields.length; i++) {
|
|
794
|
+
const field = groupFields[i];
|
|
795
|
+
const value = colorOf(field.id);
|
|
796
|
+
const isPreset = value !== null && PRESET_COLOR_SET.has(value);
|
|
797
|
+
const isHex = value !== null && !isPreset;
|
|
798
|
+
const draft = hexDraftOf(field.id);
|
|
799
|
+
const hexValue = draft !== null ? draft : committedHexText(field.id);
|
|
800
|
+
const hexInvalid = draft !== null && draft.trim().length > 0 && !BIB_SET_HEX_PATTERN.test(draft.trim());
|
|
801
|
+
const wellValue = isHex ? value : (isPreset ? (BIB_SET_PRESET_WELL_HEX[value] || '#333333') : '#333333');
|
|
802
|
+
const valueText = value === null ? '默认'
|
|
803
|
+
: (isPreset ? (BIB_SET_PRESET_LABELS[value] || value) : value.toUpperCase());
|
|
804
|
+
rows.push(React.createElement('div', { key: field.id, className: 'bib-set-row' },
|
|
805
|
+
React.createElement('div', { className: 'bib-set-rowText' },
|
|
806
|
+
React.createElement('div', { className: 'bib-set-rowTitle' }, field.label),
|
|
807
|
+
React.createElement('div', { className: 'bib-set-rowDesc' }, '当前:' + valueText + (field.anchor === true ? '(身份锚点)' : ''))),
|
|
808
|
+
React.createElement('div', { className: 'bib-set-controls' },
|
|
809
|
+
React.createElement(bibSetPalette, {
|
|
810
|
+
label: field.label + '的预设颜色',
|
|
811
|
+
value: value,
|
|
812
|
+
onSelect: function (next) { setColor(field.id, next); },
|
|
813
|
+
}),
|
|
814
|
+
React.createElement('label', { className: 'bib-set-well' },
|
|
815
|
+
React.createElement('input', {
|
|
816
|
+
type: 'color',
|
|
817
|
+
'aria-label': field.label + '的自定义颜色',
|
|
818
|
+
title: '自定义颜色(打开系统取色器)',
|
|
819
|
+
value: wellValue,
|
|
820
|
+
onChange: function (event) {
|
|
821
|
+
const picked = event && event.target ? event.target.value : null;
|
|
822
|
+
if (picked && BIB_SET_HEX_PATTERN.test(picked)) setColor(field.id, picked.toUpperCase());
|
|
823
|
+
},
|
|
824
|
+
})),
|
|
825
|
+
React.createElement('input', {
|
|
826
|
+
type: 'text',
|
|
827
|
+
className: 'bib-set-hex',
|
|
828
|
+
'aria-label': field.label + '的十六进制颜色',
|
|
829
|
+
'aria-invalid': hexInvalid ? 'true' : 'false',
|
|
830
|
+
'data-invalid': hexInvalid ? 'true' : 'false',
|
|
831
|
+
placeholder: '#RRGGBB',
|
|
832
|
+
spellCheck: false,
|
|
833
|
+
maxLength: 7,
|
|
834
|
+
value: hexValue,
|
|
835
|
+
onChange: function (event) { onHexChange(field.id, event && event.target ? event.target.value : ''); },
|
|
836
|
+
onBlur: function () { commitHex(field.id); },
|
|
837
|
+
onKeyDown: function (event) { if (event.key === 'Enter') { event.preventDefault(); commitHex(field.id); } },
|
|
838
|
+
}))));
|
|
839
|
+
}
|
|
840
|
+
colorChildren.push(React.createElement('div', { key: 'c-' + group },
|
|
841
|
+
React.createElement('div', { className: 'bib-set-group-title' }, FIELD_GROUP_LABELS[group] || group),
|
|
842
|
+
rows));
|
|
843
|
+
}
|
|
689
844
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const visiblePricing = activeSessionModel && (!state.pricing
|
|
695
|
-
|| state.pricing.provider !== activeSessionModel.provider || state.pricing.model !== activeSessionModel.model)
|
|
696
|
-
? { provider: activeSessionModel.provider, model: activeSessionModel.model, providerDisplay: activeSessionModel.providerDisplay, modelDisplay: activeSessionModel.modelDisplay, mode: 'unknown', acceptsImageInput: activeSessionModel.acceptsImageInput }
|
|
697
|
-
: (waitForSessionModel ? null : state.pricing);
|
|
698
|
-
// v1.6 T7:订阅 provider 集合提取为共享常量,消除两端硬编码漂移
|
|
699
|
-
var SUBSCRIPTION_PROVIDERS = ['codex', 'chatgpt', 'opencode-go', 'opencode', 'openai-codex', 'zai', 'zai-coding-cn', 'xiaomi-token-plan-cn', 'xiaomi-token-plan-sgp', 'xiaomi-token-plan-ams'];
|
|
700
|
-
// v1.7 FR-14:云账单 provider 集合(账单型显示,与余额/额度互斥)
|
|
701
|
-
var BILLING_PROVIDERS = ['together', 'fireworks', 'amazon-bedrock', 'cloudflare-ai-gateway', 'cloudflare-workers-ai'];
|
|
702
|
-
const visibleBillingMode = activeSessionModel && (!state.billingMode
|
|
703
|
-
|| state.billingMode.provider !== activeSessionModel.provider || state.billingMode.model !== activeSessionModel.model)
|
|
704
|
-
? { provider: activeSessionModel.provider, model: activeSessionModel.model, mode: SUBSCRIPTION_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'subscription' : (BILLING_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'billing' : 'balance') }
|
|
705
|
-
: (waitForSessionModel ? null : state.billingMode);
|
|
706
|
-
// ---- 与原生一致格式工具 ----
|
|
707
|
-
function formatTokens(n) {
|
|
708
|
-
const scaled = function (v) { return v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10); };
|
|
709
|
-
if (n < 1e3) return String(n);
|
|
710
|
-
if (n < 1e6) return scaled(n / 1e3) + 'K';
|
|
711
|
-
return scaled(n / 1e6) + 'M';
|
|
712
|
-
}
|
|
713
|
-
function formatDuration(ms) {
|
|
714
|
-
const s = ms / 1e3;
|
|
715
|
-
if (s < 60) return Math.round(s * 10) / 10 + 's';
|
|
716
|
-
const whole = Math.round(s);
|
|
717
|
-
const sec = whole % 60;
|
|
718
|
-
return Math.floor(whole / 60) + 'm' + String(sec).padStart(2, '0') + 's';
|
|
719
|
-
}
|
|
720
|
-
function formatTps(tps) {
|
|
721
|
-
const clamped = Math.max(0, tps);
|
|
722
|
-
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
723
|
-
}
|
|
724
|
-
function billedInput(usage) {
|
|
725
|
-
return (usage.uncachedInputTokens || 0) + (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0);
|
|
726
|
-
}
|
|
727
|
-
function fmt(n, digits) {
|
|
728
|
-
if (n == null || isNaN(n)) return '—';
|
|
729
|
-
return n.toFixed(digits == null ? 2 : digits);
|
|
730
|
-
}
|
|
731
|
-
function fmtCountdown(ms) {
|
|
732
|
-
if (ms == null || ms <= 0) return '00:00';
|
|
733
|
-
const totalSec = Math.floor(ms / 1000);
|
|
734
|
-
const h = Math.floor(totalSec / 3600);
|
|
735
|
-
const m = Math.floor((totalSec % 3600) / 60);
|
|
736
|
-
const s = totalSec % 60;
|
|
737
|
-
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
738
|
-
return h > 0 ? h + 'h' + p(m) + 'm' : p(m) + ':' + p(s);
|
|
739
|
-
}
|
|
740
|
-
// 订阅窗口重置时刻(本地时区,hover 浮窗用)
|
|
741
|
-
function formatDateTime(ms) {
|
|
742
|
-
const d = new Date(ms);
|
|
743
|
-
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
744
|
-
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
745
|
-
}
|
|
746
|
-
// 订阅窗口重置倒计时(天级格式):≥1 天 → '1d 21h';≥1 小时 → '3h 12m';<1 小时 → '12:34'
|
|
747
|
-
function fmtResetCountdown(ms) {
|
|
748
|
-
if (ms == null || ms <= 0) return '00:00';
|
|
749
|
-
const totalSec = Math.floor(ms / 1000);
|
|
750
|
-
const d = Math.floor(totalSec / 86400);
|
|
751
|
-
const h = Math.floor((totalSec % 86400) / 3600);
|
|
752
|
-
const m = Math.floor((totalSec % 3600) / 60);
|
|
753
|
-
if (d > 0) return d + 'd ' + h + 'h';
|
|
754
|
-
if (h > 0) return h + 'h ' + String(m).padStart(2, '0') + 'm';
|
|
755
|
-
return String(m).padStart(2, '0') + ':' + String(totalSec % 60).padStart(2, '0');
|
|
756
|
-
}
|
|
845
|
+
const feedback = [];
|
|
846
|
+
if (opError) feedback.push(React.createElement('p', { key: 'err', className: 'bib-set-alert', role: 'alert' }, opError));
|
|
847
|
+
else if (notice) feedback.push(React.createElement('p', { key: 'notice', className: 'bib-set-notice', role: 'status' }, notice));
|
|
848
|
+
else if (saving) feedback.push(React.createElement('p', { key: 'saving', className: 'bib-set-notice', 'aria-live': 'polite' }, '正在保存…'));
|
|
757
849
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
850
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
851
|
+
bibSetPageTitle(),
|
|
852
|
+
React.createElement('p', { className: 'bib-set-intro' },
|
|
853
|
+
'选择信息底栏显示哪些信息、为每个信息挑一个颜色。改动即时生效并自动保存,重启后仍然保留;隐藏信息不会删除任何记账数据。'),
|
|
854
|
+
React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-fields-title' },
|
|
855
|
+
React.createElement('div', { className: 'bib-set-card-header' },
|
|
856
|
+
React.createElement('h2', { id: 'bib-set-fields-title', className: 'bib-set-card-title' }, '显示字段'),
|
|
857
|
+
React.createElement('div', { className: 'bib-set-card-desc' },
|
|
858
|
+
'关闭的字段立刻从信息栏隐藏且不占位;「刷新失败」「账单未保存」等提示建议保留,方便发现数据异常。')),
|
|
859
|
+
React.createElement('div', { className: 'bib-set-body' }, groupsChildren)),
|
|
860
|
+
React.createElement('section', { className: 'bib-set-card', 'aria-labelledby': 'bib-set-colors-title' },
|
|
861
|
+
React.createElement('div', { className: 'bib-set-card-header' },
|
|
862
|
+
React.createElement('h2', { id: 'bib-set-colors-title', className: 'bib-set-card-title' }, '字段颜色'),
|
|
863
|
+
React.createElement('div', { className: 'bib-set-card-desc' },
|
|
864
|
+
'挑选预设色或用取色器、十六进制自定义;颜色会自动适配浅色与深色主题,保持可读。未改动前与现在的外观完全一致。')),
|
|
865
|
+
React.createElement('div', { className: 'bib-set-body' }, colorChildren)),
|
|
866
|
+
React.createElement('div', { className: 'bib-set-footer' },
|
|
867
|
+
feedback,
|
|
868
|
+
React.createElement('button', {
|
|
869
|
+
type: 'button', className: 'bib-set-btn', disabled: saving,
|
|
870
|
+
onClick: function () { runReset('fields'); },
|
|
871
|
+
}, '重置标签'),
|
|
872
|
+
React.createElement('button', {
|
|
873
|
+
type: 'button', className: 'bib-set-btn', disabled: saving,
|
|
874
|
+
onClick: function () { runReset('colors'); },
|
|
875
|
+
}, '重置颜色')));
|
|
876
|
+
} catch (err) {
|
|
877
|
+
// M2:渲染期异常绝不以白屏收场——改为页面内可见的红色错误框(role=alert,含错误信息)
|
|
878
|
+
return React.createElement('div', { className: 'bib-set-root bib-settings' },
|
|
879
|
+
bibSetPageTitle(),
|
|
880
|
+
React.createElement('p', { className: 'bib-set-alert', role: 'alert' }, '信息底栏设置渲染出错:' + bibSetOperationMessage(err)));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
769
883
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
884
|
+
module.exports = {
|
|
885
|
+
inject: ['slots'],
|
|
886
|
+
async apply(ctx) {
|
|
887
|
+
// slots 服务可能晚于 apply 就绪:优先 ctx.slots(inject 注入属性),回退 ctx.get('slots');
|
|
888
|
+
// 仍不可用则轮询等待(最多 60×300ms ≈ 18s),绝不提前退出导致注册丢失
|
|
889
|
+
let slots = ctx.slots || ctx.get('slots');
|
|
890
|
+
for (let i = 0; slots === undefined && i < 60; i++) {
|
|
891
|
+
await new Promise(function (resolve) { window.setTimeout(resolve, 300); });
|
|
892
|
+
slots = ctx.slots || ctx.get('slots');
|
|
893
|
+
}
|
|
894
|
+
if (slots === undefined) {
|
|
895
|
+
console.warn('[dsh-bottom-info-bar] slots 服务 18s 内未就绪,信息栏未注册');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
774
898
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
React.createElement('span', { className: 'bi-metric-data' }, num(value, extraClass)));
|
|
780
|
-
}
|
|
899
|
+
ctx.effect(function () {
|
|
900
|
+
const disposeStyles = installStyles();
|
|
901
|
+
return function () { disposeStyles(); };
|
|
902
|
+
}, 'dsh-bottom-info-bar: styles');
|
|
781
903
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
904
|
+
// ---------- 注册:一体替换(同 id 'stats') ----------
|
|
905
|
+
let density = 'full';
|
|
906
|
+
let toggling = false; // 持久化期间禁止重复切换(只允许 full/compact 两态)
|
|
907
|
+
let densityVersion = 0;
|
|
908
|
+
let occupantDispose = null;
|
|
909
|
+
const densityListeners = new Set();
|
|
910
|
+
const densityBusyListeners = new Set();
|
|
911
|
+
// Survives composer remounts, so returning to an already visited session
|
|
912
|
+
// does not require even one paint of an intermediate state.
|
|
913
|
+
const sessionModelCache = new Map();
|
|
914
|
+
function applyMode() {
|
|
915
|
+
if (occupantDispose) { occupantDispose(); occupantDispose = null; }
|
|
916
|
+
occupantDispose = slots.register(
|
|
917
|
+
// 静态注册无动态沙箱的优先级自动分配:显式给低 priority(最低者渲染)以遮蔽原生 stats 栏(priority 0)
|
|
918
|
+
{ name: 'conversation.composer.dock', id: 'stats', priority: -1000 },
|
|
919
|
+
function (slotProps) {
|
|
920
|
+
return React.createElement(BottomInfoBar, Object.assign({}, slotProps, { density: density, onToggleDensity: onToggleDensity }));
|
|
786
921
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
React.createElement('span', { className: 'bi-vision-model' }, modelLabel),
|
|
790
|
-
React.createElement('span', { className: 'bi-vision-kind' }, '视觉'));
|
|
791
|
-
}
|
|
922
|
+
);
|
|
923
|
+
}
|
|
792
924
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
925
|
+
// 不重新注册 slot:保留同一个 React 树,CSS 才能连续地收合/展开行高。
|
|
926
|
+
function setDensity(next) {
|
|
927
|
+
density = next;
|
|
928
|
+
densityListeners.forEach(function (listener) { listener(next); });
|
|
929
|
+
}
|
|
796
930
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
931
|
+
function setDensitySaving(next) {
|
|
932
|
+
toggling = next;
|
|
933
|
+
densityBusyListeners.forEach(function (listener) { listener(next); });
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function onToggleDensity() {
|
|
937
|
+
if (toggling) return; // 切换进行中,忽略连点
|
|
938
|
+
const requestVersion = ++densityVersion;
|
|
939
|
+
setDensitySaving(true);
|
|
940
|
+
const previous = density;
|
|
941
|
+
const next = density === 'full' ? 'compact' : 'full';
|
|
942
|
+
// 交互反馈不等网络;写入失败才回退,避免慢网络让点击看似没有生效。
|
|
943
|
+
setDensity(next);
|
|
944
|
+
rpc('setInfoDensity', { density: next }).then(function () {
|
|
945
|
+
if (requestVersion === densityVersion) setDensitySaving(false);
|
|
946
|
+
}).catch(function (err) {
|
|
947
|
+
if (requestVersion !== densityVersion) return;
|
|
948
|
+
setDensitySaving(false);
|
|
949
|
+
if (density === next) setDensity(previous);
|
|
950
|
+
console.error('Bottom Info Bar 切换信息密度失败', err);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
801
953
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
// 只有目录以分隔符明确写成“服务商 + 模型”时才去重;例如 OpenAICode 不是 OpenAI 的重复前缀。
|
|
807
|
-
if (!/^[\s·._/-]+/.test(suffix)) return modelLabel;
|
|
808
|
-
return suffix.replace(/^[\s·._/-]+/, '') || modelLabel;
|
|
809
|
-
}
|
|
954
|
+
slots.inject('conversation.composer.dock', function () {
|
|
955
|
+
applyMode();
|
|
956
|
+
return function () { if (occupantDispose) occupantDispose(); };
|
|
957
|
+
});
|
|
810
958
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const provTitle = '服务商:' + provLabel + ' ' + modelLabel + '\n'
|
|
823
|
-
+ (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(工作日高峰 9-12、14-18 点;周末全天空闲)'
|
|
824
|
-
: (pr && pr.mode === 'flat' ? '定价:固定价' : '定价:未收录,按默认计'))
|
|
825
|
-
+ versionLine;
|
|
826
|
-
return React.createElement('span', { key: 'prov', className: 'bi-model-group', title: provTitle },
|
|
827
|
-
React.createElement('b', { className: 'bi-model-provider' }, provLabel),
|
|
828
|
-
modelDetail(pr, modelName));
|
|
829
|
-
}
|
|
959
|
+
// v1.9.0 PR2 信息底栏设置页(v1.9.1 单文件化):与信息栏同一 apply、复用已等好的 slots 变量,
|
|
960
|
+
// 直接注册 settings.section——官方 dsh-client-ui-settings-general 同型写法(name/id/order/label + 组件),
|
|
961
|
+
// 不再有跨文件拼接、module.exports 重写与 async 串接。
|
|
962
|
+
ctx.effect(function () {
|
|
963
|
+
return bibSetInstallStyles();
|
|
964
|
+
}, 'dsh-bottom-info-bar: settings styles');
|
|
965
|
+
slots.inject('settings.section', function () {
|
|
966
|
+
return slots.register(
|
|
967
|
+
{ name: 'settings.section', id: 'bottom-info-bar', order: 100, label: '信息底栏' },
|
|
968
|
+
InfoBarSettingsSection);
|
|
969
|
+
});
|
|
830
970
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (provider === 'opencode-go' || provider === 'opencode') return 'OpenCode Go';
|
|
838
|
-
if (provider === 'zai' || provider === 'zai-coding-cn') return '智谱';
|
|
839
|
-
if (provider === 'xiaomi-token-plan-cn' || provider === 'xiaomi-token-plan-sgp' || provider === 'xiaomi-token-plan-ams') return '小米 MiMo';
|
|
840
|
-
return '订阅';
|
|
971
|
+
const initialDensityVersion = densityVersion;
|
|
972
|
+
try {
|
|
973
|
+
const cfg = await rpc('getConfig');
|
|
974
|
+
// 用户已经作出新选择时,绝不让启动阶段的旧配置覆盖它。
|
|
975
|
+
if (initialDensityVersion === densityVersion && cfg && (cfg.infoDensity === 'full' || cfg.infoDensity === 'compact') && cfg.infoDensity !== density) {
|
|
976
|
+
setDensity(cfg.infoDensity);
|
|
841
977
|
}
|
|
978
|
+
} catch (err) { /* 默认完整 */ }
|
|
842
979
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
980
|
+
// v1.9.0 PR2:字段配置与密度同型——启动拉取一次;设置页保存成功后派发 CustomEvent 即时同步;
|
|
981
|
+
// load() 周期顺带校准(宿主常驻内存缓存,拉取即回)
|
|
982
|
+
refreshFieldConfig();
|
|
983
|
+
ctx.effect(function () {
|
|
984
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined;
|
|
985
|
+
const onConfigChanged = function () { refreshFieldConfig(); };
|
|
986
|
+
document.addEventListener('dsh-bib-config-changed', onConfigChanged);
|
|
987
|
+
return function () { document.removeEventListener('dsh-bib-config-changed', onConfigChanged); };
|
|
988
|
+
}, 'dsh-bottom-info-bar: field config sync');
|
|
851
989
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
990
|
+
// ---------- 组件 ----------
|
|
991
|
+
function BottomInfoBar(props) {
|
|
992
|
+
// 原生/会话投影(hooks 无条件调用)
|
|
993
|
+
const statsProj = props.useProjection ? props.useProjection('sessionStats') : undefined;
|
|
994
|
+
const usageProj = props.useProjection ? props.useProjection('tokenUsage') : undefined;
|
|
858
995
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
996
|
+
const [state, setState] = React.useState({
|
|
997
|
+
loading: true, balance: null, pricing: null, usage: null, billingMode: null, sub: null, billing: null,
|
|
998
|
+
errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null, billing: null },
|
|
999
|
+
});
|
|
1000
|
+
// 版本信息由 host 在启动时从 package.json 读取;无论是否有新版,都用于服务商/模型 hover 展示。
|
|
1001
|
+
const [updateInfo, setUpdateInfo] = React.useState(null);
|
|
1002
|
+
const [now, setNow] = React.useState(Date.now());
|
|
1003
|
+
// This state is owned by DSH's per-session model selector, not by the
|
|
1004
|
+
// process-wide default for newly-created Agents.
|
|
1005
|
+
const [sessionModel, setSessionModel] = React.useState(null);
|
|
1006
|
+
const [modelSource, setModelSource] = React.useState('pending');
|
|
1007
|
+
const [displayDensity, setDisplayDensity] = React.useState(props.density);
|
|
1008
|
+
const [isDensitySaving, setIsDensitySaving] = React.useState(toggling);
|
|
866
1009
|
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
? '\n插件版本:' + updateInfo.current : '';
|
|
880
|
-
const planLine = subSnapshot && subSnapshot.plan ? '\n套餐:' + subSnapshot.plan : '';
|
|
881
|
-
const expiryLine = subSnapshot && subSnapshot.expiryAt ? '\n到期:' + formatDate(subSnapshot.expiryAt) + '(本地时区)' : '';
|
|
882
|
-
const title = '订阅服务:' + serviceName + '\n模型:' + rawModelLabel + planLine + expiryLine + versionLine;
|
|
883
|
-
return React.createElement('span', { key: 'subprov', className: 'bi-model-group', title: title },
|
|
884
|
-
React.createElement('b', { className: 'bi-model-provider' }, serviceName),
|
|
885
|
-
modelDetail(pr, modelName),
|
|
886
|
-
);
|
|
887
|
-
}
|
|
1010
|
+
// 外层持有持久化后的密度;组件只订阅数值变化,避免 slot 卸载重建打断动画。
|
|
1011
|
+
React.useEffect(function () {
|
|
1012
|
+
densityListeners.add(setDisplayDensity);
|
|
1013
|
+
densityBusyListeners.add(setIsDensitySaving);
|
|
1014
|
+
// 订阅前后没有异步间隙:立即回读,避免首次 effect 建立前的更新丢失。
|
|
1015
|
+
setDisplayDensity(density);
|
|
1016
|
+
setIsDensitySaving(toggling);
|
|
1017
|
+
return function () {
|
|
1018
|
+
densityListeners.delete(setDisplayDensity);
|
|
1019
|
+
densityBusyListeners.delete(setIsDensitySaving);
|
|
1020
|
+
};
|
|
1021
|
+
}, []);
|
|
888
1022
|
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1023
|
+
// v1.9.0 PR2:字段配置订阅——设置页保存(CustomEvent)或周期校准更新配置时重渲染。
|
|
1024
|
+
// 配置本体存模块级单例,组件只记版本号;订阅建立时立即回读避免首帧用过期配置。
|
|
1025
|
+
const [fieldConfigTick, setFieldConfigTick] = React.useState(fieldConfigVersion);
|
|
1026
|
+
React.useEffect(function () {
|
|
1027
|
+
const listener = function () { setFieldConfigTick(fieldConfigVersion); };
|
|
1028
|
+
fieldConfigListeners.add(listener);
|
|
1029
|
+
setFieldConfigTick(fieldConfigVersion);
|
|
1030
|
+
return function () { fieldConfigListeners.delete(listener); };
|
|
1031
|
+
}, []);
|
|
1032
|
+
void fieldConfigTick;
|
|
893
1033
|
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1034
|
+
// 当前会话 ID 多路获取:slotProps 标准 kit → session 快照 → 运行时 sessions 服务
|
|
1035
|
+
// (DSH 各版本注入方式不同,任一路可用即拿到真实会话 ID,避免回退到上一会话的账)
|
|
1036
|
+
const propsRef = React.useRef(props);
|
|
1037
|
+
propsRef.current = props;
|
|
1038
|
+
const resolveSessionId = React.useCallback(function () {
|
|
1039
|
+
const p = propsRef.current;
|
|
1040
|
+
try {
|
|
1041
|
+
if (p.sessionId) return p.sessionId;
|
|
1042
|
+
if (p.session && p.session.sessionId) return p.session.sessionId;
|
|
1043
|
+
const sessions = ctx.get ? ctx.get('sessions') : null;
|
|
1044
|
+
const cur = sessions && sessions.list && sessions.list.getSnapshot().current;
|
|
1045
|
+
if (cur) return cur;
|
|
1046
|
+
} catch (e) { /* 拿不到则返回空串,host 端对空串返回 null(显示 ¥0.000) */ }
|
|
1047
|
+
return '';
|
|
1048
|
+
}, []);
|
|
1049
|
+
const sessionId = resolveSessionId();
|
|
904
1050
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
'未配置 ' + credName + ' → 设置→模型 填写')));
|
|
920
|
-
}
|
|
921
|
-
} else if (bal && bal.data) {
|
|
922
|
-
const symbol = bal.currency === 'USD' ? '$' : '¥';
|
|
923
|
-
const balTitle = bal.estimate
|
|
924
|
-
? '估算余额:' + symbol + fmt(bal.data.total)
|
|
925
|
-
: '余额:' + symbol + fmt(bal.data.total);
|
|
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
|
-
}
|
|
933
|
-
// host 快照失败(bal.error)或本次 RPC 失败(errors.balance)→ 均保留旧数据 + 降级标记
|
|
934
|
-
if (fieldVisible('balanceError') && (bal.error || errors.balance)) {
|
|
935
|
-
trailingErrorGroups.push(fieldSpan('balanceError', 'balerr',
|
|
936
|
-
React.createElement('span', { className: 'bi-stale', title: '余额暂不可用;正在显示上次数据并自动重试。' }, '刷新失败')));
|
|
937
|
-
}
|
|
938
|
-
} else if (bal && bal.error) {
|
|
939
|
-
if (fieldVisible('balanceError')) {
|
|
940
|
-
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
941
|
-
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
942
|
-
}
|
|
943
|
-
} else if (errors.balance) {
|
|
944
|
-
// 本次 RPC 失败且无旧数据:只降级余额块,其余端点数据照常渲染
|
|
945
|
-
if (fieldVisible('balanceError')) {
|
|
946
|
-
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
947
|
-
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
1051
|
+
// Subscribe to the exact store the native model seat uses. A session
|
|
1052
|
+
// activation or a successful model switch publishes here immediately;
|
|
1053
|
+
// no polling and no host HTTP request sit on the display path.
|
|
1054
|
+
React.useEffect(function () {
|
|
1055
|
+
let stop = null;
|
|
1056
|
+
let active = true;
|
|
1057
|
+
setSessionModel(null);
|
|
1058
|
+
setModelSource('pending');
|
|
1059
|
+
if (!sessionId) { setModelSource('unavailable'); return function () {}; }
|
|
1060
|
+
try {
|
|
1061
|
+
const directories = ctx.get ? ctx.get('modelDirectories') : null;
|
|
1062
|
+
if (!directories || typeof directories.directoryFor !== 'function') {
|
|
1063
|
+
setModelSource('unavailable');
|
|
1064
|
+
return function () {};
|
|
948
1065
|
}
|
|
949
|
-
|
|
1066
|
+
setModelSource('available');
|
|
1067
|
+
const directory = directories.directoryFor(sessionId);
|
|
1068
|
+
const publish = function () {
|
|
1069
|
+
if (!active || !directory.store || typeof directory.store.getSnapshot !== 'function') return;
|
|
1070
|
+
const snapshot = directory.store.getSnapshot();
|
|
1071
|
+
const selected = snapshot && snapshot.current;
|
|
1072
|
+
if (!selected || typeof selected.provider !== 'string' || typeof selected.model !== 'string') return;
|
|
1073
|
+
const group = Array.isArray(snapshot.groups) ? snapshot.groups.find(function (g) { return g && g.id === selected.provider; }) : null;
|
|
1074
|
+
const model = group && Array.isArray(group.models) ? group.models.find(function (m) { return m && m.id === selected.model; }) : null;
|
|
1075
|
+
const inputModalities = model && Array.isArray(model.inputModalities) ? model.inputModalities : null;
|
|
1076
|
+
const value = {
|
|
1077
|
+
sessionId: sessionId,
|
|
1078
|
+
provider: selected.provider,
|
|
1079
|
+
model: selected.model,
|
|
1080
|
+
providerDisplay: group && typeof group.name === 'string' ? group.name : selected.provider,
|
|
1081
|
+
modelDisplay: model && typeof model.name === 'string' ? model.name : selected.model,
|
|
1082
|
+
// true/false 来自 DSH 的明确目录 metadata;null 表示仍待 host 查询,不能误画成文本模型。
|
|
1083
|
+
acceptsImageInput: inputModalities === null ? null : inputModalities.indexOf('image') !== -1,
|
|
1084
|
+
};
|
|
1085
|
+
sessionModelCache.set(sessionId, value);
|
|
1086
|
+
setSessionModel(value);
|
|
1087
|
+
};
|
|
1088
|
+
publish();
|
|
1089
|
+
if (directory.store && typeof directory.store.subscribe === 'function') stop = directory.store.subscribe(publish);
|
|
1090
|
+
} catch (err) { setModelSource('unavailable'); /* old DSH: retain the RPC fallback */ }
|
|
1091
|
+
return function () { active = false; if (typeof stop === 'function') stop(); };
|
|
1092
|
+
}, [sessionId]);
|
|
950
1093
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
+ '/M · 输出 ¥' + (p.output != null ? p.output : '?') + '/M';
|
|
959
|
-
groups.push(fieldSpan('period', 'period', React.createElement('span', { className: peakNow ? 'bi-peak' : 'bi-offpeak', title: periodTitle },
|
|
960
|
-
peakNow ? '高峰价' : '空闲价')));
|
|
961
|
-
}
|
|
1094
|
+
// 组件生命周期 AbortSignal:卸载时中止所有在途 RPC(配合 rpc 20s 超时,双保险防旧响应写 state)
|
|
1095
|
+
const abortRef = React.useRef(null);
|
|
1096
|
+
React.useEffect(function () {
|
|
1097
|
+
const controller = new AbortController();
|
|
1098
|
+
abortRef.current = controller;
|
|
1099
|
+
return function () { controller.abort(); };
|
|
1100
|
+
}, []);
|
|
962
1101
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1102
|
+
const loadVersionRef = React.useRef(0);
|
|
1103
|
+
const cachedSessionModel = sessionId ? sessionModelCache.get(sessionId) : null;
|
|
1104
|
+
const activeSessionModel = sessionModel && sessionModel.sessionId === sessionId ? sessionModel : (cachedSessionModel || null);
|
|
1105
|
+
const load = React.useCallback(function (selection) {
|
|
1106
|
+
// v1.9.0 PR2:周期顺带校准字段配置(宿主内存缓存,即回;设置页变更另有 CustomEvent 即时通道)
|
|
1107
|
+
refreshFieldConfig();
|
|
1108
|
+
const requestVersion = ++loadVersionRef.current;
|
|
1109
|
+
const activeSelection = selection || activeSessionModel;
|
|
1110
|
+
const selectionArgs = activeSelection ? { selection: { provider: activeSelection.provider, model: activeSelection.model } } : {};
|
|
1111
|
+
// 首启窗口内(打开/刷新网页头几秒)→ 快照类请求强制重查;之后的 30s 周期轮询走常规缓存节奏
|
|
1112
|
+
const force = Date.now() - BOOT_AT < FORCE_REFRESH_WINDOW_MS;
|
|
1113
|
+
if (force) { selectionArgs.force = true; }
|
|
1114
|
+
const signal = abortRef.current ? abortRef.current.signal : null;
|
|
1115
|
+
// 逐接口容错:allSettled 等全部 settle(最坏 20s 超时兜底),任一失败只降级该端点,
|
|
1116
|
+
// 不拖垮其他成功数据;合并逻辑在 mergeLoadResults(失败端点保留旧值 + 记录错误)
|
|
1117
|
+
Promise.allSettled([
|
|
1118
|
+
rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider, force: force } : (force ? { force: true } : null), signal),
|
|
1119
|
+
rpc('getPricing', selectionArgs, signal),
|
|
1120
|
+
rpc('getUsageSummary', Object.assign({ sessionId: sessionId }, selectionArgs), signal),
|
|
1121
|
+
rpc('getBillingMode', selectionArgs, signal),
|
|
1122
|
+
rpc('getSubscriptionSnapshot', selectionArgs, signal),
|
|
1123
|
+
rpc('getBillingStatus', selectionArgs, signal),
|
|
1124
|
+
]).then(function (results) {
|
|
1125
|
+
// Do not allow a late A response to overwrite newly active B.
|
|
1126
|
+
if ((signal && signal.aborted) || requestVersion !== loadVersionRef.current) return;
|
|
1127
|
+
setState(function (s) { return mergeLoadResults(s, results); });
|
|
1128
|
+
});
|
|
1129
|
+
}, [resolveSessionId, activeSessionModel, sessionId]);
|
|
970
1130
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1131
|
+
React.useEffect(function () {
|
|
1132
|
+
load();
|
|
1133
|
+
const id = window.setInterval(load, 30000);
|
|
1134
|
+
return function () { window.clearInterval(id); };
|
|
1135
|
+
}, [load]);
|
|
975
1136
|
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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: '花费暂不可用;不会影响对话。' }, '花费获取失败')));
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1137
|
+
// 版本检查由 host 在进程启动时完成;这里仅读取一次缓存,不轮询 NPM。
|
|
1138
|
+
React.useEffect(function () {
|
|
1139
|
+
let active = true;
|
|
1140
|
+
rpc('getUpdateInfo').then(function (info) {
|
|
1141
|
+
if (active && info && typeof info.current === 'string') setUpdateInfo(info);
|
|
1142
|
+
}).catch(function () { /* 版本检查失败静默,不影响信息栏 */ });
|
|
1143
|
+
return function () { active = false; };
|
|
1144
|
+
}, []);
|
|
1004
1145
|
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const serviceName = subscriptionServiceName(source);
|
|
1011
|
-
const message = error && typeof error.message === 'string' ? error.message : '';
|
|
1012
|
-
const statusMatch = message.match(/HTTP (\d{3})/);
|
|
1013
|
-
const status = statusMatch ? statusMatch[1] : '';
|
|
1014
|
-
if (kind === 'no-key') return '未找到 ' + serviceName + ' 登录凭证。请重新授权。';
|
|
1015
|
-
if (kind === 'auth' || status === '401') return serviceName + ' 登录凭证已失效。请重新授权。';
|
|
1016
|
-
if (status === '403') return serviceName + ' 拒绝访问。请重新授权或稍后再试。';
|
|
1017
|
-
if (status === '429') return serviceName + ' 请求过于频繁。请稍后再试。';
|
|
1018
|
-
if (kind === 'timeout' || /timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
|
|
1019
|
-
if (kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
|
|
1020
|
-
return serviceName + ' 暂不可用。请检查网络后再试。';
|
|
1021
|
-
}
|
|
1146
|
+
// Model text comes from sessionModel synchronously. The following load
|
|
1147
|
+
// only refreshes secondary data in the background.
|
|
1148
|
+
React.useEffect(function () {
|
|
1149
|
+
if (activeSessionModel) load(activeSessionModel);
|
|
1150
|
+
}, [load, activeSessionModel]);
|
|
1022
1151
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if (!sub) {
|
|
1036
|
-
if (errors.sub && fieldVisible('refreshFailure')) {
|
|
1037
|
-
// 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
|
|
1038
|
-
trailingErrorGroups.push(fieldSpan('refreshFailure', 'suberr',
|
|
1039
|
-
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint({ kind: 'exception', message: String(errors.sub) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
|
|
1040
|
-
}
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
const rawWindows = Array.isArray(sub.windows) ? sub.windows : [];
|
|
1044
|
-
// v1.9.0 PR2:逐窗口显隐过滤(5h/周/月各自独立开关),后续优先窗口/重置倒计时都基于过滤后的集合
|
|
1045
|
-
const windows = rawWindows.filter(function (w) {
|
|
1046
|
-
return w && typeof w.usedPercent === 'number' && windowFieldVisible(w.key);
|
|
1047
|
-
});
|
|
1048
|
-
const hasData = windows.length > 0;
|
|
1049
|
-
// 错误分支:无旧数据时给出明确引导 / 错误文案;有旧数据时走下方渲染并附"刷新失败"标记。
|
|
1050
|
-
// no-key(无令牌/缺 access_token)与 auth(令牌失效 401)→ 统一"未绑定/重新绑定"引导——
|
|
1051
|
-
// 令牌由独立插件 dsh-chatgpt-subscription 维护,本插件只读令牌显示额度,不自行绑定/续期
|
|
1052
|
-
if (sub.error && !hasData) {
|
|
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
|
-
}
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
// 窗口缺失(如 Codex 无 5 小时窗口)→ 跳过窗口组,不占位、不报错
|
|
1079
|
-
if (hasData) {
|
|
1080
|
-
// 简洁模式下选择"时间最短且有重置时刻"的窗口(刷新最快,用户最需关注):
|
|
1081
|
-
// 优先级:5小时 > 周 > 月(按窗口时长排序,而非已用百分比)
|
|
1082
|
-
const windowPriority = { five_hour: 1, seven_day: 2, monthly: 3 };
|
|
1083
|
-
const windowsWithReset = windows.filter(function (w) { return w.resetsAt; });
|
|
1084
|
-
const displayWindow = windowsWithReset.length > 0
|
|
1085
|
-
? windowsWithReset.slice().sort(function (a, b) {
|
|
1086
|
-
const pa = windowPriority[a.key] || 99;
|
|
1087
|
-
const pb = windowPriority[b.key] || 99;
|
|
1088
|
-
return pa - pb;
|
|
1089
|
-
})[0]
|
|
1090
|
-
: null;
|
|
1152
|
+
// 会话统计变化(回复中 turns/steps/tokens 增长,回复完成时停止)→ 防抖后即时刷新花费,
|
|
1153
|
+
// 不等下一个 30s 轮询:用户回复一结束即可看到真实金额
|
|
1154
|
+
React.useEffect(function () {
|
|
1155
|
+
if (!statsProj) return undefined;
|
|
1156
|
+
const timer = window.setTimeout(load, 800);
|
|
1157
|
+
return function () { window.clearTimeout(timer); };
|
|
1158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1159
|
+
}, [load,
|
|
1160
|
+
statsProj && statsProj.turns,
|
|
1161
|
+
statsProj && statsProj.steps,
|
|
1162
|
+
statsProj && statsProj.decodeTokens,
|
|
1163
|
+
]);
|
|
1091
1164
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1165
|
+
React.useEffect(function () {
|
|
1166
|
+
const id = window.setInterval(function () { setNow(Date.now()); }, 1000);
|
|
1167
|
+
return function () { window.clearInterval(id); };
|
|
1168
|
+
}, []);
|
|
1094
1169
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 均保留旧数据 + 降级标记
|
|
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)) }, '刷新失败')));
|
|
1121
|
-
}
|
|
1122
|
-
// 距重置倒计时(与显示的窗口一致,确保额度与倒计时匹配)
|
|
1123
|
-
if (displayWindow && displayWindow.resetsAt && fieldVisible('resetCountdown')) {
|
|
1124
|
-
const cdTitle = displayWindow.label + '窗口 剩余 ' + remainingPercent(displayWindow)
|
|
1125
|
-
+ '%(已用 ' + displayWindow.usedPercent + '%) · 重置 ' + formatDateTime(displayWindow.resetsAt);
|
|
1126
|
-
groups.push(fieldSpan('resetCountdown', 'subcd', React.createElement('span', { title: cdTitle },
|
|
1127
|
-
metric('距重置', fmtResetCountdown(displayWindow.resetsAt - now)))));
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1170
|
+
// While background RPCs catch up, render the newly activated session's
|
|
1171
|
+
// model and suppress details from the prior session rather than showing
|
|
1172
|
+
// a convincing but wrong provider/model combination.
|
|
1173
|
+
const waitForSessionModel = !!sessionId && modelSource !== 'unavailable' && !activeSessionModel;
|
|
1174
|
+
const visiblePricing = activeSessionModel && (!state.pricing
|
|
1175
|
+
|| state.pricing.provider !== activeSessionModel.provider || state.pricing.model !== activeSessionModel.model)
|
|
1176
|
+
? { provider: activeSessionModel.provider, model: activeSessionModel.model, providerDisplay: activeSessionModel.providerDisplay, modelDisplay: activeSessionModel.modelDisplay, mode: 'unknown', acceptsImageInput: activeSessionModel.acceptsImageInput }
|
|
1177
|
+
: (waitForSessionModel ? null : state.pricing);
|
|
1178
|
+
// v1.6 T7:订阅 provider 集合提取为共享常量,消除两端硬编码漂移
|
|
1179
|
+
var SUBSCRIPTION_PROVIDERS = ['codex', 'chatgpt', 'opencode-go', 'opencode', 'openai-codex', 'zai', 'zai-coding-cn', 'xiaomi-token-plan-cn', 'xiaomi-token-plan-sgp', 'xiaomi-token-plan-ams'];
|
|
1180
|
+
// v1.7 FR-14:云账单 provider 集合(账单型显示,与余额/额度互斥)
|
|
1181
|
+
var BILLING_PROVIDERS = ['together', 'fireworks', 'amazon-bedrock', 'cloudflare-ai-gateway', 'cloudflare-workers-ai'];
|
|
1182
|
+
const visibleBillingMode = activeSessionModel && (!state.billingMode
|
|
1183
|
+
|| state.billingMode.provider !== activeSessionModel.provider || state.billingMode.model !== activeSessionModel.model)
|
|
1184
|
+
? { provider: activeSessionModel.provider, model: activeSessionModel.model, mode: SUBSCRIPTION_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'subscription' : (BILLING_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'billing' : 'balance') }
|
|
1185
|
+
: (waitForSessionModel ? null : state.billingMode);
|
|
1186
|
+
// ---- 与原生一致格式工具 ----
|
|
1187
|
+
function formatTokens(n) {
|
|
1188
|
+
const scaled = function (v) { return v >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10); };
|
|
1189
|
+
if (n < 1e3) return String(n);
|
|
1190
|
+
if (n < 1e6) return scaled(n / 1e3) + 'K';
|
|
1191
|
+
return scaled(n / 1e6) + 'M';
|
|
1130
1192
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
const statusMatch = message.match(/HTTP (\d{3})/);
|
|
1138
|
-
const status = statusMatch ? statusMatch[1] : '';
|
|
1139
|
-
if (error && error.kind === 'no-key') return '未配置 ' + message.replace('未配置 ', '') + '。请在"设置 → 模型"中填写。';
|
|
1140
|
-
if (status === '403' || /缺少 .*权限/.test(message)) return serviceName + ' 拒绝访问:Token 可能缺少账单读取权限。';
|
|
1141
|
-
if (error && error.kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
|
|
1142
|
-
if (/timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
|
|
1143
|
-
return serviceName + ' 账单暂不可用。请检查网络与权限后再试。';
|
|
1193
|
+
function formatDuration(ms) {
|
|
1194
|
+
const s = ms / 1e3;
|
|
1195
|
+
if (s < 60) return Math.round(s * 10) / 10 + 's';
|
|
1196
|
+
const whole = Math.round(s);
|
|
1197
|
+
const sec = whole % 60;
|
|
1198
|
+
return Math.floor(whole / 60) + 'm' + String(sec).padStart(2, '0') + 's';
|
|
1144
1199
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const billAnchor = billingProviderGroup();
|
|
1149
|
-
groups.push(React.cloneElement(billAnchor, { 'data-field': 'billingServiceGroup', style: fieldStyle('billingServiceGroup') }));
|
|
1150
|
-
}
|
|
1151
|
-
const bill = state.billing;
|
|
1152
|
-
const errors = state.errors || {};
|
|
1153
|
-
if (!bill) {
|
|
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) }, '刷新失败')));
|
|
1157
|
-
}
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
const d = bill.data;
|
|
1161
|
-
const hasSpend = !!(d && (d.currentPeriodSpend != null || d.usage != null));
|
|
1162
|
-
if (bill.error && !hasSpend) {
|
|
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
|
-
}
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
if (hasSpend) {
|
|
1170
|
-
const symbol = d.currency === 'CNY' ? '¥' : '$';
|
|
1171
|
-
const titleLines = ['账单源:' + billingServiceName(visibleBillingMode && visibleBillingMode.provider)]
|
|
1172
|
-
.concat(d.note ? [d.note] : [])
|
|
1173
|
-
.concat(d.currentPeriodSpend != null ? ['本月花费:' + symbol + fmt(d.currentPeriodSpend, 2)] : [])
|
|
1174
|
-
.concat(d.budgetPercent != null ? ['预算使用:' + fmt(d.budgetPercent, 0) + '%'] : [])
|
|
1175
|
-
.concat(d.freeRemaining != null ? ['每日免费额度剩余:' + fmt(d.freeRemaining, 0)] : []);
|
|
1176
|
-
// v1.9.0 PR2:本月/预算/免费额度三片段各自显隐,分隔符只在可见片段之间
|
|
1177
|
-
const nodes = [];
|
|
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
|
-
}
|
|
1187
|
-
// 免费额度仅当接口显式给出(freeRemaining/resetsAt 同时存在)才显示,绝不编造
|
|
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))));
|
|
1191
|
-
}
|
|
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)) }, '刷新失败')));
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
+
function formatTps(tps) {
|
|
1201
|
+
const clamped = Math.max(0, tps);
|
|
1202
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
1200
1203
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
function billingProviderGroup() {
|
|
1204
|
-
const pr = visiblePricing;
|
|
1205
|
-
const serviceName = billingServiceName(visibleBillingMode && visibleBillingMode.provider);
|
|
1206
|
-
const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
1207
|
-
: (pr && pr.model ? pr.model : '未知模型');
|
|
1208
|
-
const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
|
|
1209
|
-
const versionLine = updateInfo && typeof updateInfo.current === 'string'
|
|
1210
|
-
? '\n插件版本:' + updateInfo.current : '';
|
|
1211
|
-
const title = '账单服务:' + serviceName + '\n模型:' + modelLabel + versionLine;
|
|
1212
|
-
return React.createElement('span', { key: 'billprov', className: 'bi-model-group', title: title },
|
|
1213
|
-
React.createElement('b', { className: 'bi-model-provider' }, serviceName),
|
|
1214
|
-
modelDetail(pr, modelName),
|
|
1215
|
-
);
|
|
1204
|
+
function billedInput(usage) {
|
|
1205
|
+
return (usage.uncachedInputTokens || 0) + (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0);
|
|
1216
1206
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const trailingErrorGroups = [];
|
|
1221
|
-
// 两态严格判定:density 只能是 'full' 或 'compact'(host 校验 + 本地防抖保证)
|
|
1222
|
-
const full = displayDensity === 'full';
|
|
1223
|
-
// 模式互斥:订阅制渲染订阅版 row2,账单制渲染账单版 row2,余额制渲染 v1.0.0 现状——三态绝不叠加(FR-14)
|
|
1224
|
-
const isSub = !!(visibleBillingMode && visibleBillingMode.mode === 'subscription');
|
|
1225
|
-
const isBilling = !!(visibleBillingMode && visibleBillingMode.mode === 'billing');
|
|
1226
|
-
// Never paint a loading placeholder. Before the active session's model
|
|
1227
|
-
// is available, leave this compact row empty rather than briefly showing
|
|
1228
|
-
// either a generic loading label or data from the previous session.
|
|
1229
|
-
if (waitForSessionModel) {
|
|
1230
|
-
// Intentionally empty: session model publish fills the row immediately.
|
|
1231
|
-
} else if (isBilling) {
|
|
1232
|
-
pushBillingGroups(groups, trailingErrorGroups);
|
|
1233
|
-
} else if (isSub) {
|
|
1234
|
-
pushSubscriptionGroups(groups, trailingErrorGroups);
|
|
1235
|
-
} else {
|
|
1236
|
-
pushBalanceGroups(groups, trailingErrorGroups);
|
|
1207
|
+
function fmt(n, digits) {
|
|
1208
|
+
if (n == null || isNaN(n)) return '—';
|
|
1209
|
+
return n.toFixed(digits == null ? 2 : digits);
|
|
1237
1210
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (errors.sub) failedLabels.push('订阅额度');
|
|
1247
|
-
if (errors.billing) failedLabels.push('账单');
|
|
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('、') + '暂不可用;正在保留上次数据并自动重试。' }, '刷新失败')));
|
|
1211
|
+
function fmtCountdown(ms) {
|
|
1212
|
+
if (ms == null || ms <= 0) return '00:00';
|
|
1213
|
+
const totalSec = Math.floor(ms / 1000);
|
|
1214
|
+
const h = Math.floor(totalSec / 3600);
|
|
1215
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
1216
|
+
const s = totalSec % 60;
|
|
1217
|
+
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
1218
|
+
return h > 0 ? h + 'h' + p(m) + 'm' : p(m) + ':' + p(s);
|
|
1252
1219
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1220
|
+
// 订阅窗口重置时刻(本地时区,hover 浮窗用)
|
|
1221
|
+
function formatDateTime(ms) {
|
|
1222
|
+
const d = new Date(ms);
|
|
1223
|
+
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
1224
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
1225
|
+
}
|
|
1226
|
+
// 订阅窗口重置倒计时(天级格式):≥1 天 → '1d 21h';≥1 小时 → '3h 12m';<1 小时 → '12:34'
|
|
1227
|
+
function fmtResetCountdown(ms) {
|
|
1228
|
+
if (ms == null || ms <= 0) return '00:00';
|
|
1229
|
+
const totalSec = Math.floor(ms / 1000);
|
|
1230
|
+
const d = Math.floor(totalSec / 86400);
|
|
1231
|
+
const h = Math.floor((totalSec % 86400) / 3600);
|
|
1232
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
1233
|
+
if (d > 0) return d + 'd ' + h + 'h';
|
|
1234
|
+
if (h > 0) return h + 'h ' + String(m).padStart(2, '0') + 'm';
|
|
1235
|
+
return String(m).padStart(2, '0') + ':' + String(totalSec % 60).padStart(2, '0');
|
|
1262
1236
|
}
|
|
1263
1237
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1238
|
+
// 订阅窗口剩余百分比(剩余 = 100 - 已用;钳制 ≥0 防接口异常值)
|
|
1239
|
+
function remainingPercent(w) {
|
|
1240
|
+
return Math.max(0, 100 - w.usedPercent);
|
|
1241
|
+
}
|
|
1242
|
+
// 订阅窗口紧凑行标签(5小时 → '5h',周 → '周',月 → '月');hover 明细仍用完整标签
|
|
1243
|
+
function compactWindowLabel(key) {
|
|
1244
|
+
if (key === 'five_hour') return '5h';
|
|
1245
|
+
if (key === 'seven_day') return '周';
|
|
1246
|
+
if (key === 'monthly') return '月';
|
|
1247
|
+
return '窗口';
|
|
1248
|
+
}
|
|
1269
1249
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1250
|
+
// 数字统一加粗(仅数字本身)
|
|
1251
|
+
function num(t, extraClass) {
|
|
1252
|
+
return React.createElement('b', { className: 'bi-num' + (extraClass ? ' ' + extraClass : '') }, String(t));
|
|
1253
|
+
}
|
|
1273
1254
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const texts = [];
|
|
1281
|
-
function textOf(part) {
|
|
1282
|
-
if (part == null) return '';
|
|
1283
|
-
if (typeof part === 'string' || typeof part === 'number') return String(part);
|
|
1284
|
-
if (Array.isArray(part)) return part.map(textOf).join('');
|
|
1285
|
-
if (part.props && part.props['data-metric-text']) return part.props['data-metric-text'];
|
|
1286
|
-
return part.props ? textOf(part.props.children) : '';
|
|
1287
|
-
}
|
|
1288
|
-
for (let i = 0; i < parts.length; i++) {
|
|
1289
|
-
const p = parts[i];
|
|
1290
|
-
if (typeof p === 'string') { nodesArr.push(p); texts.push(p); }
|
|
1291
|
-
else if (Array.isArray(p)) { for (let j = 0; j < p.length; j++) nodesArr.push(p[j]); texts.push(textOf(p)); }
|
|
1292
|
-
else { nodesArr.push(p); texts.push(textOf(p)); }
|
|
1293
|
-
}
|
|
1294
|
-
ng.push({ nodes: nodesArr, text: texts.join(''), hidden: !!hidden, fieldId: fieldId || null });
|
|
1295
|
-
}
|
|
1255
|
+
// 统一数值语法:标签保持常规,数值与紧随的单位/货币符号作为一个加粗的数据令牌。
|
|
1256
|
+
function metric(label, value, extraClass) {
|
|
1257
|
+
return React.createElement('span', { className: 'bi-metric', 'data-metric-text': label + ' ' + value },
|
|
1258
|
+
React.createElement('span', { className: 'bi-metric-label' }, label),
|
|
1259
|
+
React.createElement('span', { className: 'bi-metric-data' }, num(value, extraClass)));
|
|
1260
|
+
}
|
|
1296
1261
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1262
|
+
// 仅在 DSH 模型目录明确声明 inputModalities 包含 image 时,将“完整模型名 视觉”合并为一个椭圆。
|
|
1263
|
+
function modelLabelWithCapability(pr, modelLabel) {
|
|
1264
|
+
if (pr && pr.acceptsImageInput === null) {
|
|
1265
|
+
return React.createElement('span', { className: 'bi-model-capability-pending', 'aria-hidden': 'true' }, modelLabel);
|
|
1301
1266
|
}
|
|
1267
|
+
if (!pr || pr.acceptsImageInput !== true) return React.createElement('span', { className: 'bi-model-name' }, modelLabel);
|
|
1268
|
+
return React.createElement('span', { className: 'bi-vision', title: '支持图像输入。' },
|
|
1269
|
+
React.createElement('span', { className: 'bi-vision-model' }, modelLabel),
|
|
1270
|
+
React.createElement('span', { className: 'bi-vision-kind' }, '视觉'));
|
|
1271
|
+
}
|
|
1302
1272
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
if (durations.length > 0) durations.push(' · ');
|
|
1307
|
-
durations.push(fieldSpan('toolTime', 'durt', metric('工具调用', formatDuration(statsProj.toolMs))));
|
|
1308
|
-
}
|
|
1309
|
-
if (durations.length > 0) group(durations);
|
|
1273
|
+
function modelSeparator() {
|
|
1274
|
+
return React.createElement('span', { className: 'bi-model-dot', 'aria-hidden': 'true' }, '·');
|
|
1275
|
+
}
|
|
1310
1276
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1277
|
+
function modelDetail(pr, modelName) {
|
|
1278
|
+
return React.createElement('span', { className: 'bi-model-detail' },
|
|
1279
|
+
modelSeparator(), modelLabelWithCapability(pr, modelName));
|
|
1280
|
+
}
|
|
1315
1281
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1282
|
+
// 参考图的视觉标签将服务商显示在椭圆外;仅移除重复的服务商前缀,不截断真实模型名。
|
|
1283
|
+
function modelLabelWithoutProvider(modelLabel, providerLabel) {
|
|
1284
|
+
if (modelLabel.toLowerCase().indexOf(providerLabel.toLowerCase()) !== 0) return modelLabel;
|
|
1285
|
+
const suffix = modelLabel.slice(providerLabel.length);
|
|
1286
|
+
// 只有目录以分隔符明确写成“服务商 + 模型”时才去重;例如 OpenAICode 不是 OpenAI 的重复前缀。
|
|
1287
|
+
if (!/^[\s·._/-]+/.test(suffix)) return modelLabel;
|
|
1288
|
+
return suffix.replace(/^[\s·._/-]+/, '') || modelLabel;
|
|
1289
|
+
}
|
|
1324
1290
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1291
|
+
// 服务商 + 具体模型(两种模式共用;纯显示,不拦截点击——点击冒泡到整条信息栏触发密度切换;hover 展示定价模式)
|
|
1292
|
+
// 模型目录名可能重复服务商前缀(如 DeepSeek V4 Flash);始终拆分为“DeepSeek · V4 Flash”,
|
|
1293
|
+
// 既保留服务商信息,也不让模型名重复前缀。
|
|
1294
|
+
function providerGroup() {
|
|
1295
|
+
const pr = visiblePricing;
|
|
1296
|
+
const provLabel = (pr && pr.providerDisplay) ? pr.providerDisplay : '未知';
|
|
1297
|
+
const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
1298
|
+
: (pr && pr.model ? pr.model : '未知模型');
|
|
1299
|
+
const modelName = modelLabelWithoutProvider(modelLabel, provLabel);
|
|
1300
|
+
const versionLine = updateInfo && typeof updateInfo.current === 'string'
|
|
1301
|
+
? '\n插件版本:' + updateInfo.current : '';
|
|
1302
|
+
const provTitle = '服务商:' + provLabel + ' ' + modelLabel + '\n'
|
|
1303
|
+
+ (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(工作日高峰 9-12、14-18 点;周末全天空闲)'
|
|
1304
|
+
: (pr && pr.mode === 'flat' ? '定价:固定价' : '定价:未收录,按默认计'))
|
|
1305
|
+
+ versionLine;
|
|
1306
|
+
return React.createElement('span', { key: 'prov', className: 'bi-model-group', title: provTitle },
|
|
1307
|
+
React.createElement('b', { className: 'bi-model-provider' }, provLabel),
|
|
1308
|
+
modelDetail(pr, modelName));
|
|
1341
1309
|
}
|
|
1342
1310
|
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
return
|
|
1311
|
+
// 订阅服务名(订阅制模式下"服务商"指订阅服务本身,不是模型厂商)
|
|
1312
|
+
// Codex 与 ChatGPT 已合并:实际 provider openai-codex / chatgpt 均显示 ChatGPT;codex 保持 Codex
|
|
1313
|
+
// v1.6 T7:新增 zai/zai-coding-cn → '智谱';v1.7:小米 Token Plan → '小米 MiMo'
|
|
1314
|
+
function subscriptionServiceName(provider) {
|
|
1315
|
+
if (provider === 'chatgpt' || provider === 'openai-codex') return 'ChatGPT';
|
|
1316
|
+
if (provider === 'codex') return 'Codex';
|
|
1317
|
+
if (provider === 'opencode-go' || provider === 'opencode') return 'OpenCode Go';
|
|
1318
|
+
if (provider === 'zai' || provider === 'zai-coding-cn') return '智谱';
|
|
1319
|
+
if (provider === 'xiaomi-token-plan-cn' || provider === 'xiaomi-token-plan-sgp' || provider === 'xiaomi-token-plan-ams') return '小米 MiMo';
|
|
1320
|
+
return '订阅';
|
|
1348
1321
|
}
|
|
1349
1322
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
event.preventDefault();
|
|
1359
|
-
props.onToggleDensity();
|
|
1360
|
-
}
|
|
1361
|
-
},
|
|
1362
|
-
role: 'button',
|
|
1363
|
-
tabIndex: 0,
|
|
1364
|
-
'aria-labelledby': full && row1 !== null ? 'dsh-bottom-info-bar-native dsh-bottom-info-bar-primary' : 'dsh-bottom-info-bar-primary',
|
|
1365
|
-
'aria-describedby': 'dsh-bottom-info-bar-action',
|
|
1366
|
-
'aria-pressed': full,
|
|
1367
|
-
'aria-busy': isDensitySaving,
|
|
1368
|
-
'aria-disabled': isDensitySaving,
|
|
1369
|
-
'data-density': displayDensity,
|
|
1370
|
-
'data-density-saving': isDensitySaving,
|
|
1371
|
-
title: isDensitySaving ? '正在保存切换…' : '单击切换 完整/简洁',
|
|
1372
|
-
}, animatedRow1, row2,
|
|
1373
|
-
React.createElement('span', { id: 'dsh-bottom-info-bar-action', className: 'bi-sr-only' },
|
|
1374
|
-
full ? '按 Enter 或空格切换为简洁模式。' : '按 Enter 或空格切换为完整模式。'));
|
|
1375
|
-
}
|
|
1376
|
-
},
|
|
1377
|
-
};
|
|
1323
|
+
// 账单型服务名(v1.7:云账单 provider 显示品牌名,未知保持兜底)
|
|
1324
|
+
function billingServiceName(provider) {
|
|
1325
|
+
if (provider === 'together') return 'Together';
|
|
1326
|
+
if (provider === 'fireworks') return 'Fireworks';
|
|
1327
|
+
if (provider === 'amazon-bedrock') return 'AWS Bedrock';
|
|
1328
|
+
if (provider === 'cloudflare-ai-gateway' || provider === 'cloudflare-workers-ai') return 'Cloudflare';
|
|
1329
|
+
return '云账单';
|
|
1330
|
+
}
|
|
1378
1331
|
|
|
1379
|
-
//
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
// 顶层标识符一律 bibSet* / InfoBarSettings* 前缀,绝不与主包重名(React/rpc/FIELD_REGISTRY 等直接复用主包声明)。
|
|
1386
|
-
'use strict';
|
|
1332
|
+
// 套餐档位短名(JWT 订阅卡:plus/pro/team/enterprise → Plus/Pro/Team/Enterprise;未知返回 null 显示模型名)
|
|
1333
|
+
function subscriptionPlanShort(planType) {
|
|
1334
|
+
if (typeof planType !== 'string' || planType.length === 0) return null;
|
|
1335
|
+
const map = { plus: 'Plus', pro: 'Pro', team: 'Team', enterprise: 'Enterprise' };
|
|
1336
|
+
return map[planType.toLowerCase()] || null;
|
|
1337
|
+
}
|
|
1387
1338
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
}
|
|
1339
|
+
// 本地时区 YYYY-MM-DD(订阅到期日)
|
|
1340
|
+
function formatDate(ms) {
|
|
1341
|
+
if (ms == null || isNaN(ms)) return '—';
|
|
1342
|
+
const d = new Date(ms);
|
|
1343
|
+
const p = function (x) { return String(x).padStart(2, '0'); };
|
|
1344
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
1345
|
+
}
|
|
1411
1346
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1347
|
+
// 订阅制模型组:订阅服务名 · 具体模型(如 `OpenCode Go · V4 Flash`、`Codex · GPT 5 Codex`)
|
|
1348
|
+
// v1.7 FR-8:codex 源有 JWT 套餐档位时,模型位显示套餐档位(如 `ChatGPT · Plus`),真实信息来自 id_token claims
|
|
1349
|
+
function subscriptionProviderGroup() {
|
|
1350
|
+
const pr = visiblePricing;
|
|
1351
|
+
const serviceName = subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider);
|
|
1352
|
+
const subSnapshot = state.sub;
|
|
1353
|
+
const planShort = subSnapshot && subSnapshot.planType ? subscriptionPlanShort(subSnapshot.planType) : null;
|
|
1354
|
+
const rawModelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
1355
|
+
: (pr && pr.model ? pr.model : '未知模型');
|
|
1356
|
+
const modelLabel = planShort ? planShort : rawModelLabel;
|
|
1357
|
+
const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
|
|
1358
|
+
const versionLine = updateInfo && typeof updateInfo.current === 'string'
|
|
1359
|
+
? '\n插件版本:' + updateInfo.current : '';
|
|
1360
|
+
const planLine = subSnapshot && subSnapshot.plan ? '\n套餐:' + subSnapshot.plan : '';
|
|
1361
|
+
const expiryLine = subSnapshot && subSnapshot.expiryAt ? '\n到期:' + formatDate(subSnapshot.expiryAt) + '(本地时区)' : '';
|
|
1362
|
+
const title = '订阅服务:' + serviceName + '\n模型:' + rawModelLabel + planLine + expiryLine + versionLine;
|
|
1363
|
+
return React.createElement('span', { key: 'subprov', className: 'bi-model-group', title: title },
|
|
1364
|
+
React.createElement('b', { className: 'bi-model-provider' }, serviceName),
|
|
1365
|
+
modelDetail(pr, modelName),
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1416
1368
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1369
|
+
// 字段包装:带 data-field 与(可选)颜色变量的容器 span,供 CSS 按字段着色(未自定义时变量不存在,零回归)
|
|
1370
|
+
function fieldSpan(id, key, children) {
|
|
1371
|
+
return React.createElement('span', { key: key, 'data-field': id, style: fieldStyle(id) }, children);
|
|
1372
|
+
}
|
|
1420
1373
|
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
}
|
|
1374
|
+
// ---- 余额制模式(v1.0.0 现状,完全不动):服务商+模型 → 余额 → 时段 → 倒计时 → 本会话花费 ----
|
|
1375
|
+
// v1.9.0 PR2:每个渲染片段按设置过滤(fieldVisible);隐藏不占位,组间分隔符由组装层自动收合
|
|
1376
|
+
function pushBalanceGroups(groups, trailingErrorGroups) {
|
|
1377
|
+
const bal = state.balance;
|
|
1378
|
+
const errors = state.errors || {};
|
|
1379
|
+
const alertActive = !!(bal && bal.alert && bal.alert.active);
|
|
1380
|
+
if (fieldVisible('anchorGroup')) {
|
|
1381
|
+
const anchor = providerGroup();
|
|
1382
|
+
groups.push(React.cloneElement(anchor, { 'data-field': 'anchorGroup', style: fieldStyle('anchorGroup') }));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// v1.6 T7:未适配账户渲染"未适配"弱提示
|
|
1386
|
+
if (bal && bal.unmapped) {
|
|
1387
|
+
if (fieldVisible('unmapped')) {
|
|
1388
|
+
trailingErrorGroups.push(fieldSpan('unmapped', 'unmapped',
|
|
1389
|
+
React.createElement('span', { className: 'bi-muted', title: '该服务商暂未适配余额查询,后续版本将支持。' }, '未适配')));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
// 余额(纯金额;hover 仅展示余额,不显示充值/赠金)
|
|
1393
|
+
// v1.6 T7:未配置提示改为按账户显示凭据名(去掉写死的 DeepSeek 文案)
|
|
1394
|
+
else if (bal && bal.error && bal.error.kind === 'no-key') {
|
|
1395
|
+
if (fieldVisible('noKeyHint')) {
|
|
1396
|
+
const credName = bal.error.message ? String(bal.error.message).replace('未配置 ', '') : 'API_KEY';
|
|
1397
|
+
trailingErrorGroups.push(fieldSpan('noKeyHint', 'nokey',
|
|
1398
|
+
React.createElement('span', { className: 'bi-err', title: '未配置 ' + credName + '。请在"设置 → 模型"中填写。' },
|
|
1399
|
+
'未配置 ' + credName + ' → 设置→模型 填写')));
|
|
1400
|
+
}
|
|
1401
|
+
} else if (bal && bal.data) {
|
|
1402
|
+
const symbol = bal.currency === 'USD' ? '$' : '¥';
|
|
1403
|
+
const balTitle = bal.estimate
|
|
1404
|
+
? '估算余额:' + symbol + fmt(bal.data.total)
|
|
1405
|
+
: '余额:' + symbol + fmt(bal.data.total);
|
|
1406
|
+
if (fieldVisible('balance')) {
|
|
1407
|
+
groups.push(fieldSpan('balance', 'bal', React.createElement('span', { title: balTitle },
|
|
1408
|
+
metric('余额', symbol + fmt(bal.data.total), alertActive ? 'bi-alert-num' : ''),
|
|
1409
|
+
alertActive ? React.createElement('span', { className: 'bi-low-status' }, '低') : null,
|
|
1410
|
+
bal.estimate ? React.createElement('span', { className: 'bi-muted' }, '(估算)') : null,
|
|
1411
|
+
)));
|
|
1412
|
+
}
|
|
1413
|
+
// host 快照失败(bal.error)或本次 RPC 失败(errors.balance)→ 均保留旧数据 + 降级标记
|
|
1414
|
+
if (fieldVisible('balanceError') && (bal.error || errors.balance)) {
|
|
1415
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'balerr',
|
|
1416
|
+
React.createElement('span', { className: 'bi-stale', title: '余额暂不可用;正在显示上次数据并自动重试。' }, '刷新失败')));
|
|
1417
|
+
}
|
|
1418
|
+
} else if (bal && bal.error) {
|
|
1419
|
+
if (fieldVisible('balanceError')) {
|
|
1420
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
1421
|
+
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
1422
|
+
}
|
|
1423
|
+
} else if (errors.balance) {
|
|
1424
|
+
// 本次 RPC 失败且无旧数据:只降级余额块,其余端点数据照常渲染
|
|
1425
|
+
if (fieldVisible('balanceError')) {
|
|
1426
|
+
trailingErrorGroups.push(fieldSpan('balanceError', 'berr',
|
|
1427
|
+
React.createElement('span', { className: 'bi-err', title: '余额获取失败。请检查网络和 API Key。' }, '余额获取失败')));
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1485
1430
|
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
-
}
|
|
1431
|
+
// 时段:仅峰谷价服务商显示"高峰价/空闲价"(flat/unknown 服务商不显示;hover 展示具体价格)
|
|
1432
|
+
const pr = visiblePricing;
|
|
1433
|
+
if (pr && pr.mode === 'peak-valley' && fieldVisible('period')) {
|
|
1434
|
+
const peakNow = pr.period === 'peak';
|
|
1435
|
+
const p = pr.prices || {};
|
|
1436
|
+
const periodTitle = '北京时间 ' + (peakNow ? '高峰价' : '空闲价') + ':输入 ¥' + (p.inputCacheMiss != null ? p.inputCacheMiss : '?')
|
|
1437
|
+
+ '/M · 缓存 ¥' + (p.inputCacheHit != null ? p.inputCacheHit : '?')
|
|
1438
|
+
+ '/M · 输出 ¥' + (p.output != null ? p.output : '?') + '/M';
|
|
1439
|
+
groups.push(fieldSpan('period', 'period', React.createElement('span', { className: peakNow ? 'bi-peak' : 'bi-offpeak', title: periodTitle },
|
|
1440
|
+
peakNow ? '高峰价' : '空闲价')));
|
|
1441
|
+
}
|
|
1502
1442
|
|
|
1503
|
-
//
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
}
|
|
1443
|
+
// 倒计时:仅峰谷价服务商显示"距高峰/距空闲"(hover 展示下次切换时刻;数字加粗)
|
|
1444
|
+
if (pr && pr.mode === 'peak-valley' && pr.nextSwitch && fieldVisible('countdown')) {
|
|
1445
|
+
const peakNow = pr.period === 'peak';
|
|
1446
|
+
const countdownTitle = '北京时间 ' + pr.nextSwitch.atLabel + ' 切换为' + (peakNow ? '空闲价' : '高峰价') + '。';
|
|
1447
|
+
groups.push(fieldSpan('countdown', 'countdown', React.createElement('span', { title: countdownTitle },
|
|
1448
|
+
metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now)))));
|
|
1449
|
+
}
|
|
1547
1450
|
|
|
1548
|
-
//
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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);
|
|
1451
|
+
// 本会话花费(公共小部件 pushSessionCost:只显示钱;hover 显示 今天/近一月/全部)
|
|
1452
|
+
// 始终显示:新会话/对话刚开始尚无记账时显示 ¥0.000,hover 仍可查看持久化的 今天/近一月/全部
|
|
1453
|
+
pushSessionCost(groups, trailingErrorGroups, !!(bal && bal.currency === 'USD'));
|
|
1454
|
+
}
|
|
1560
1455
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1456
|
+
// 本会话花费块(余额制 与 订阅·充值余额 形态共用的小部件):
|
|
1457
|
+
// 只显示钱;hover 浮窗显示 含子代理说明 + 今天 / 近一月 / 全部;金额数字加粗。
|
|
1458
|
+
// usdSymbol:账户币种为美元时,hover 汇总行用 $ 前缀(本会话单值仍按其真实计价币种)
|
|
1459
|
+
function pushSessionCost(groups, trailingErrorGroups, usdSymbol) {
|
|
1460
|
+
const usg = state.usage;
|
|
1461
|
+
const errs = state.errors || {};
|
|
1462
|
+
if (usg) {
|
|
1463
|
+
// 隐藏不占位(错误提示属于独立字段 usageError,两支互斥不受影响)
|
|
1464
|
+
if (fieldVisible('sessionCost')) {
|
|
1465
|
+
const cs = usg.currentSession;
|
|
1466
|
+
const costCNY = cs && cs.costs && cs.costs.CNY != null ? cs.costs.CNY : null;
|
|
1467
|
+
const costUSD = cs && cs.costs && cs.costs.USD != null ? cs.costs.USD : null;
|
|
1468
|
+
const symbol = usdSymbol ? '$' : '¥';
|
|
1469
|
+
const costTxt = costCNY != null ? '¥' + costCNY.toFixed(3)
|
|
1470
|
+
: (costUSD != null ? '$' + costUSD.toFixed(3) : symbol + (0).toFixed(3));
|
|
1471
|
+
const today = usg.todaySpend != null ? '今天 ' + symbol + fmt(usg.todaySpend, 3) : '';
|
|
1472
|
+
const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
|
|
1473
|
+
const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
|
|
1474
|
+
const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
|
|
1475
|
+
groups.push(fieldSpan('sessionCost', 'convo', React.createElement('span', {
|
|
1476
|
+
title: '本会话 ' + costTxt + '(含子代理)' + (detail ? '\n' + detail : '') },
|
|
1477
|
+
metric('本会话', costTxt))));
|
|
1478
|
+
}
|
|
1479
|
+
} else if (errs.usage && fieldVisible('usageError')) {
|
|
1480
|
+
trailingErrorGroups.push(fieldSpan('usageError', 'usageerr',
|
|
1481
|
+
React.createElement('span', { className: 'bi-err', title: '花费暂不可用;不会影响对话。' }, '花费获取失败')));
|
|
1482
|
+
}
|
|
1570
1483
|
}
|
|
1571
|
-
}).catch(function (err) {
|
|
1572
|
-
if (!active) return;
|
|
1573
|
-
setLoadError(bibSetOperationMessage(err)); setStatus('error');
|
|
1574
|
-
});
|
|
1575
|
-
return function () { active = false; };
|
|
1576
|
-
}, []);
|
|
1577
1484
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1485
|
+
// ---- 订阅制模式(互斥替换余额制版):
|
|
1486
|
+
// 套餐额度型:订阅服务+模型 → 三窗口额度 → 距重置倒计时(余额/时段/花费/token 不显示)
|
|
1487
|
+
// 充值余额型(如智谱按量账户,windows 空且有 sub.balance):服务商+模型 → 余额 → 本会话花费 ----
|
|
1488
|
+
function subscriptionFailureHint(error, source) {
|
|
1489
|
+
const kind = error && error.kind;
|
|
1490
|
+
const serviceName = subscriptionServiceName(source);
|
|
1491
|
+
const message = error && typeof error.message === 'string' ? error.message : '';
|
|
1492
|
+
const statusMatch = message.match(/HTTP (\d{3})/);
|
|
1493
|
+
const status = statusMatch ? statusMatch[1] : '';
|
|
1494
|
+
if (kind === 'no-key') return '未找到 ' + serviceName + ' 登录凭证。请重新授权。';
|
|
1495
|
+
if (kind === 'auth' || status === '401') return serviceName + ' 登录凭证已失效。请重新授权。';
|
|
1496
|
+
if (status === '403') return serviceName + ' 拒绝访问。请重新授权或稍后再试。';
|
|
1497
|
+
if (status === '429') return serviceName + ' 请求过于频繁。请稍后再试。';
|
|
1498
|
+
if (kind === 'timeout' || /timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
|
|
1499
|
+
if (kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
|
|
1500
|
+
return serviceName + ' 暂不可用。请检查网络后再试。';
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function pushSubscriptionGroups(groups, trailingErrorGroups) {
|
|
1504
|
+
if (fieldVisible('subServiceGroup')) {
|
|
1505
|
+
const subAnchor = subscriptionProviderGroup();
|
|
1506
|
+
groups.push(React.cloneElement(subAnchor, { 'data-field': 'subServiceGroup', style: fieldStyle('subServiceGroup') }));
|
|
1507
|
+
}
|
|
1508
|
+
const sub = state.sub;
|
|
1509
|
+
const errors = state.errors || {};
|
|
1510
|
+
// v1.7 FR-8:JWT 订阅卡——真实套餐到期日(纯本地解码;无登录态/解析失败不显示此处)
|
|
1511
|
+
if (sub && sub.planType && sub.expiryAt && fieldVisible('expiry')) {
|
|
1512
|
+
groups.push(fieldSpan('expiry', 'subexp', React.createElement('span', { title: '订阅到期:' + formatDate(sub.expiryAt) + '(本地时区)' },
|
|
1513
|
+
metric('到期', formatDate(sub.expiryAt)))));
|
|
1514
|
+
}
|
|
1515
|
+
if (!sub) {
|
|
1516
|
+
if (errors.sub && fieldVisible('refreshFailure')) {
|
|
1517
|
+
// 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
|
|
1518
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'suberr',
|
|
1519
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint({ kind: 'exception', message: String(errors.sub) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const rawWindows = Array.isArray(sub.windows) ? sub.windows : [];
|
|
1524
|
+
// v1.9.0 PR2:逐窗口显隐过滤(5h/周/月各自独立开关),后续优先窗口/重置倒计时都基于过滤后的集合
|
|
1525
|
+
const windows = rawWindows.filter(function (w) {
|
|
1526
|
+
return w && typeof w.usedPercent === 'number' && windowFieldVisible(w.key);
|
|
1527
|
+
});
|
|
1528
|
+
const hasData = windows.length > 0;
|
|
1529
|
+
// 错误分支:无旧数据时给出明确引导 / 错误文案;有旧数据时走下方渲染并附"刷新失败"标记。
|
|
1530
|
+
// no-key(无令牌/缺 access_token)与 auth(令牌失效 401)→ 统一"未绑定/重新绑定"引导——
|
|
1531
|
+
// 令牌由独立插件 dsh-chatgpt-subscription 维护,本插件只读令牌显示额度,不自行绑定/续期
|
|
1532
|
+
if (sub.error && !hasData) {
|
|
1533
|
+
if (fieldVisible('refreshFailure')) {
|
|
1534
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1535
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
// v1.8:充值余额模式——无额度窗口但有 balance 字段(如智谱普通 API 余额用户)
|
|
1540
|
+
// 显示"余额 ¥XX.XX",与 Coding Plan 额度窗口互斥
|
|
1541
|
+
if (!hasData && typeof sub.balance === 'number' && isFinite(sub.balance)) {
|
|
1542
|
+
if (fieldVisible('subBalance')) {
|
|
1543
|
+
const balTxt = '¥' + fmt(sub.balance, 2);
|
|
1544
|
+
const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + '(' + (sub.plan || '充值余额') + ')',
|
|
1545
|
+
'可用余额:' + balTxt];
|
|
1546
|
+
groups.push(fieldSpan('subBalance', 'subbal', React.createElement('span', { title: titleLines.join('\n') },
|
|
1547
|
+
metric('余额', balTxt))));
|
|
1548
|
+
}
|
|
1549
|
+
// 充值余额用户按量付费,花销与余额同等重要 → 追加公共花费块(含子代理聚合)
|
|
1550
|
+
pushSessionCost(groups, trailingErrorGroups, false);
|
|
1551
|
+
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 保留旧数据 + 降级标记
|
|
1552
|
+
if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
|
|
1553
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1554
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1555
|
+
}
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
// 窗口缺失(如 Codex 无 5 小时窗口)→ 跳过窗口组,不占位、不报错
|
|
1559
|
+
if (hasData) {
|
|
1560
|
+
// 简洁模式下选择"时间最短且有重置时刻"的窗口(刷新最快,用户最需关注):
|
|
1561
|
+
// 优先级:5小时 > 周 > 月(按窗口时长排序,而非已用百分比)
|
|
1562
|
+
const windowPriority = { five_hour: 1, seven_day: 2, monthly: 3 };
|
|
1563
|
+
const windowsWithReset = windows.filter(function (w) { return w.resetsAt; });
|
|
1564
|
+
const displayWindow = windowsWithReset.length > 0
|
|
1565
|
+
? windowsWithReset.slice().sort(function (a, b) {
|
|
1566
|
+
const pa = windowPriority[a.key] || 99;
|
|
1567
|
+
const pb = windowPriority[b.key] || 99;
|
|
1568
|
+
return pa - pb;
|
|
1569
|
+
})[0]
|
|
1570
|
+
: null;
|
|
1586
1571
|
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
}
|
|
1572
|
+
// 完整模式显示全部窗口;简洁模式只显示选中的那个窗口
|
|
1573
|
+
const visible = full ? windows : (displayWindow ? [displayWindow] : []);
|
|
1595
1574
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1575
|
+
// 预警触发条件:已用 ≥80%(= 剩余 ≤20%)→ 鲜红色文字;正常额度使用中性文字。
|
|
1576
|
+
const LOW_QUOTA_PERCENT = 20;
|
|
1577
|
+
const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + (sub.plan ? '(' + sub.plan + ')' : '')]
|
|
1578
|
+
.concat(windows.map(function (w) {
|
|
1579
|
+
return w.label + '窗口:剩余 ' + remainingPercent(w) + '%(已用 ' + w.usedPercent + '%)'
|
|
1580
|
+
+ (w.resetsAt ? ' · 重置 ' + formatDateTime(w.resetsAt) + ' · 距重置 ' + fmtResetCountdown(w.resetsAt - now) : '');
|
|
1581
|
+
}));
|
|
1582
|
+
const winNodes = [];
|
|
1583
|
+
for (let i = 0; i < visible.length; i++) {
|
|
1584
|
+
const w = visible[i];
|
|
1585
|
+
if (i > 0) winNodes.push(' · ');
|
|
1586
|
+
const remaining = remainingPercent(w);
|
|
1587
|
+
const numberClass = remaining <= LOW_QUOTA_PERCENT ? 'bi-quota-low' : '';
|
|
1588
|
+
// 每个窗口独立 data-field(subWindow5h/Week/Month),色变量按字段注入;「低」字标签保留
|
|
1589
|
+
winNodes.push(fieldSpan(WINDOW_FIELD_IDS[w.key] || 'subWindow5h', 'w' + i,
|
|
1590
|
+
metric(compactWindowLabel(w.key), remaining + '%', numberClass)));
|
|
1591
|
+
if (remaining <= LOW_QUOTA_PERCENT) winNodes.push(React.createElement('span', { key: 'low' + i, className: 'bi-low-status' }, '低'));
|
|
1592
|
+
}
|
|
1593
|
+
// 全部窗口被隐藏(或紧凑模式无候选)→ 整组不推送,分隔符由组装层正确收合
|
|
1594
|
+
if (winNodes.length > 0) {
|
|
1595
|
+
groups.push(React.createElement('span', { key: 'subwin', title: titleLines.join('\n') }, ...winNodes));
|
|
1596
|
+
}
|
|
1597
|
+
// host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 均保留旧数据 + 降级标记
|
|
1598
|
+
if (fieldVisible('refreshFailure') && (sub.error || errors.sub)) {
|
|
1599
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'substale',
|
|
1600
|
+
React.createElement('span', { className: 'bi-stale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1601
|
+
}
|
|
1602
|
+
// 距重置倒计时(与显示的窗口一致,确保额度与倒计时匹配)
|
|
1603
|
+
if (displayWindow && displayWindow.resetsAt && fieldVisible('resetCountdown')) {
|
|
1604
|
+
const cdTitle = displayWindow.label + '窗口 剩余 ' + remainingPercent(displayWindow)
|
|
1605
|
+
+ '%(已用 ' + displayWindow.usedPercent + '%) · 重置 ' + formatDateTime(displayWindow.resetsAt);
|
|
1606
|
+
groups.push(fieldSpan('resetCountdown', 'subcd', React.createElement('span', { title: cdTitle },
|
|
1607
|
+
metric('距重置', fmtResetCountdown(displayWindow.resetsAt - now)))));
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1605
1611
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1612
|
+
// ---- v1.7 账单型模式(FR-10~13/FR-14,互斥第三态):
|
|
1613
|
+
// 账单服务+模型 → 本月真实花费 →(预算%)→(免费额度+重置倒计时);余额/额度/本会话花费均不显示 ----
|
|
1614
|
+
function billingFailureHint(error, provider) {
|
|
1615
|
+
const serviceName = billingServiceName(provider);
|
|
1616
|
+
const message = error && typeof error.message === 'string' ? error.message : '';
|
|
1617
|
+
const statusMatch = message.match(/HTTP (\d{3})/);
|
|
1618
|
+
const status = statusMatch ? statusMatch[1] : '';
|
|
1619
|
+
if (error && error.kind === 'no-key') return '未配置 ' + message.replace('未配置 ', '') + '。请在"设置 → 模型"中填写。';
|
|
1620
|
+
if (status === '403' || /缺少 .*权限/.test(message)) return serviceName + ' 拒绝访问:Token 可能缺少账单读取权限。';
|
|
1621
|
+
if (error && error.kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
|
|
1622
|
+
if (/timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
|
|
1623
|
+
return serviceName + ' 账单暂不可用。请检查网络与权限后再试。';
|
|
1624
|
+
}
|
|
1618
1625
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1626
|
+
function pushBillingGroups(groups, trailingErrorGroups) {
|
|
1627
|
+
if (fieldVisible('billingServiceGroup')) {
|
|
1628
|
+
const billAnchor = billingProviderGroup();
|
|
1629
|
+
groups.push(React.cloneElement(billAnchor, { 'data-field': 'billingServiceGroup', style: fieldStyle('billingServiceGroup') }));
|
|
1630
|
+
}
|
|
1631
|
+
const bill = state.billing;
|
|
1632
|
+
const errors = state.errors || {};
|
|
1633
|
+
if (!bill) {
|
|
1634
|
+
if (errors.billing && fieldVisible('refreshFailure')) {
|
|
1635
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billerr',
|
|
1636
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint({ kind: 'exception', message: String(errors.billing) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败')));
|
|
1637
|
+
}
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const d = bill.data;
|
|
1641
|
+
const hasSpend = !!(d && (d.currentPeriodSpend != null || d.usage != null));
|
|
1642
|
+
if (bill.error && !hasSpend) {
|
|
1643
|
+
if (fieldVisible('refreshFailure')) {
|
|
1644
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
|
|
1645
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1646
|
+
}
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (hasSpend) {
|
|
1650
|
+
const symbol = d.currency === 'CNY' ? '¥' : '$';
|
|
1651
|
+
const titleLines = ['账单源:' + billingServiceName(visibleBillingMode && visibleBillingMode.provider)]
|
|
1652
|
+
.concat(d.note ? [d.note] : [])
|
|
1653
|
+
.concat(d.currentPeriodSpend != null ? ['本月花费:' + symbol + fmt(d.currentPeriodSpend, 2)] : [])
|
|
1654
|
+
.concat(d.budgetPercent != null ? ['预算使用:' + fmt(d.budgetPercent, 0) + '%'] : [])
|
|
1655
|
+
.concat(d.freeRemaining != null ? ['每日免费额度剩余:' + fmt(d.freeRemaining, 0)] : []);
|
|
1656
|
+
// v1.9.0 PR2:本月/预算/免费额度三片段各自显隐,分隔符只在可见片段之间
|
|
1657
|
+
const nodes = [];
|
|
1658
|
+
if (d.currentPeriodSpend != null && fieldVisible('billingSpend')) {
|
|
1659
|
+
nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月', symbol + fmt(d.currentPeriodSpend, 2))));
|
|
1660
|
+
} else if (d.usage != null && fieldVisible('billingSpend')) {
|
|
1661
|
+
nodes.push(fieldSpan('billingSpend', 'billspend', metric('本月用量', fmt(d.usage, 2) + (d.usageUnit ? ' ' + d.usageUnit : ''))));
|
|
1662
|
+
}
|
|
1663
|
+
if (d.budgetPercent != null && fieldVisible('budget')) {
|
|
1664
|
+
if (nodes.length > 0) nodes.push(' · ');
|
|
1665
|
+
nodes.push(fieldSpan('budget', 'billbudget', metric('预算', fmt(d.budgetPercent, 0) + '%')));
|
|
1666
|
+
}
|
|
1667
|
+
// 免费额度仅当接口显式给出(freeRemaining/resetsAt 同时存在)才显示,绝不编造
|
|
1668
|
+
if (d.freeRemaining != null && d.resetsAt && fieldVisible('freeQuota')) {
|
|
1669
|
+
if (nodes.length > 0) nodes.push(' · ');
|
|
1670
|
+
nodes.push(fieldSpan('freeQuota', 'billfree', metric('免费', fmt(d.freeRemaining, 0) + ' · 距重置 ' + fmtResetCountdown(d.resetsAt - now))));
|
|
1671
|
+
}
|
|
1672
|
+
if (nodes.length > 0) {
|
|
1673
|
+
groups.push(React.createElement('span', { key: 'bill', title: titleLines.join('\n') }, ...nodes));
|
|
1674
|
+
}
|
|
1675
|
+
if (fieldVisible('refreshFailure') && (bill.error || errors.billing)) {
|
|
1676
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'billstale',
|
|
1677
|
+
React.createElement('span', { className: 'bi-stale', title: billingFailureHint(bill.error || { kind: 'exception', message: String(errors.billing || '') }, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败')));
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1638
1681
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1682
|
+
// 账单型模型组:账单服务名 · 具体模型(如 `AWS Bedrock · Claude`)
|
|
1683
|
+
function billingProviderGroup() {
|
|
1684
|
+
const pr = visiblePricing;
|
|
1685
|
+
const serviceName = billingServiceName(visibleBillingMode && visibleBillingMode.provider);
|
|
1686
|
+
const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
|
|
1687
|
+
: (pr && pr.model ? pr.model : '未知模型');
|
|
1688
|
+
const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
|
|
1689
|
+
const versionLine = updateInfo && typeof updateInfo.current === 'string'
|
|
1690
|
+
? '\n插件版本:' + updateInfo.current : '';
|
|
1691
|
+
const title = '账单服务:' + serviceName + '\n模型:' + modelLabel + versionLine;
|
|
1692
|
+
return React.createElement('span', { key: 'billprov', className: 'bi-model-group', title: title },
|
|
1693
|
+
React.createElement('b', { className: 'bi-model-provider' }, serviceName),
|
|
1694
|
+
modelDetail(pr, modelName),
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1647
1697
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1698
|
+
const groups = [];
|
|
1699
|
+
// 报错不打断主要信息的阅读顺序:统一延后到整行最右侧。
|
|
1700
|
+
const trailingErrorGroups = [];
|
|
1701
|
+
// 两态严格判定:density 只能是 'full' 或 'compact'(host 校验 + 本地防抖保证)
|
|
1702
|
+
const full = displayDensity === 'full';
|
|
1703
|
+
// 模式互斥:订阅制渲染订阅版 row2,账单制渲染账单版 row2,余额制渲染 v1.0.0 现状——三态绝不叠加(FR-14)
|
|
1704
|
+
const isSub = !!(visibleBillingMode && visibleBillingMode.mode === 'subscription');
|
|
1705
|
+
const isBilling = !!(visibleBillingMode && visibleBillingMode.mode === 'billing');
|
|
1706
|
+
// Never paint a loading placeholder. Before the active session's model
|
|
1707
|
+
// is available, leave this compact row empty rather than briefly showing
|
|
1708
|
+
// either a generic loading label or data from the previous session.
|
|
1709
|
+
if (waitForSessionModel) {
|
|
1710
|
+
// Intentionally empty: session model publish fills the row immediately.
|
|
1711
|
+
} else if (isBilling) {
|
|
1712
|
+
pushBillingGroups(groups, trailingErrorGroups);
|
|
1713
|
+
} else if (isSub) {
|
|
1714
|
+
pushSubscriptionGroups(groups, trailingErrorGroups);
|
|
1715
|
+
} else {
|
|
1716
|
+
pushBalanceGroups(groups, trailingErrorGroups);
|
|
1717
|
+
}
|
|
1659
1718
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1719
|
+
// 全局降级提示:任一端点失败 → 旧数据照常渲染 + 角落提示(title 列出失败项),仅失败项降级
|
|
1720
|
+
const errors = state.errors || {};
|
|
1721
|
+
const failedLabels = [];
|
|
1722
|
+
if (errors.balance) failedLabels.push('余额');
|
|
1723
|
+
if (errors.pricing) failedLabels.push('定价');
|
|
1724
|
+
if (errors.usage) failedLabels.push('花费');
|
|
1725
|
+
if (errors.billingMode) failedLabels.push('模式');
|
|
1726
|
+
if (errors.sub) failedLabels.push('订阅额度');
|
|
1727
|
+
if (errors.billing) failedLabels.push('账单');
|
|
1728
|
+
if (failedLabels.length > 0 && fieldVisible('refreshFailure')) {
|
|
1729
|
+
trailingErrorGroups.push(fieldSpan('refreshFailure', 'degraded',
|
|
1730
|
+
React.createElement('span', { className: 'bi-stale', key: 'degraded',
|
|
1731
|
+
title: failedLabels.join('、') + '暂不可用;正在保留上次数据并自动重试。' }, '刷新失败')));
|
|
1732
|
+
}
|
|
1733
|
+
const persistence = state.usage && state.usage.persistence;
|
|
1734
|
+
if (persistence && persistence.state && persistence.state !== 'ok' && fieldVisible('persistWarning')) {
|
|
1735
|
+
const snapshotOnly = persistence.state === 'snapshot-stale';
|
|
1736
|
+
trailingErrorGroups.push(fieldSpan('persistWarning', 'ledger-save', React.createElement('span', {
|
|
1737
|
+
className: snapshotOnly ? 'bi-stale' : 'bi-err',
|
|
1738
|
+
title: snapshotOnly
|
|
1739
|
+
? '账单流水已保存,但可直接查看的账单文件暂未更新:' + (persistence.message || '未知原因')
|
|
1740
|
+
: '本次账单未保存,不会计入金额:' + (persistence.message || '未知原因'),
|
|
1741
|
+
}, snapshotOnly ? '账单待整理' : '账单未保存')));
|
|
1742
|
+
}
|
|
1685
1743
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
}
|
|
1744
|
+
if (updateInfo && updateInfo.available === true && fieldVisible('updateNotice')) {
|
|
1745
|
+
groups.push(fieldSpan('updateNotice', 'update', React.createElement('span', {
|
|
1746
|
+
className: 'bi-update', title: '请告知你的 Agent 将本插件更新到“' + updateInfo.latest + '”版本。',
|
|
1747
|
+
}, '新版本提醒')));
|
|
1748
|
+
}
|
|
1703
1749
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
}
|
|
1750
|
+
// ---- 组装(分隔符收合与「刷新失败」去重见模块级 assembleInfoBarRow) ----
|
|
1751
|
+
const nodes = assembleInfoBarRow(groups, trailingErrorGroups, React.createElement);
|
|
1752
|
+
const row2 = React.createElement('div', { id: 'dsh-bottom-info-bar-primary', className: 'bi-row2' }, ...nodes);
|
|
1738
1753
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
}
|
|
1754
|
+
let row1 = null;
|
|
1755
|
+
if (statsProj) {
|
|
1756
|
+
// 每组:{ nodes: React 节点数组(数字用 num 加粗), text: 纯文本(title 用), fieldId: 字段 id(着色用) }
|
|
1757
|
+
const ng = [];
|
|
1758
|
+
function group(parts, hidden, fieldId) {
|
|
1759
|
+
const nodesArr = [];
|
|
1760
|
+
const texts = [];
|
|
1761
|
+
function textOf(part) {
|
|
1762
|
+
if (part == null) return '';
|
|
1763
|
+
if (typeof part === 'string' || typeof part === 'number') return String(part);
|
|
1764
|
+
if (Array.isArray(part)) return part.map(textOf).join('');
|
|
1765
|
+
if (part.props && part.props['data-metric-text']) return part.props['data-metric-text'];
|
|
1766
|
+
return part.props ? textOf(part.props.children) : '';
|
|
1767
|
+
}
|
|
1768
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1769
|
+
const p = parts[i];
|
|
1770
|
+
if (typeof p === 'string') { nodesArr.push(p); texts.push(p); }
|
|
1771
|
+
else if (Array.isArray(p)) { for (let j = 0; j < p.length; j++) nodesArr.push(p[j]); texts.push(textOf(p)); }
|
|
1772
|
+
else { nodesArr.push(p); texts.push(textOf(p)); }
|
|
1773
|
+
}
|
|
1774
|
+
ng.push({ nodes: nodesArr, text: texts.join(''), hidden: !!hidden, fieldId: fieldId || null });
|
|
1775
|
+
}
|
|
1796
1776
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1777
|
+
// v1.9.0 PR2:原生统计行字段按设置过滤(隐藏组完全不进 ng,不占版式也不进 title)
|
|
1778
|
+
if (fieldVisible('turnsSteps')) {
|
|
1779
|
+
// 中文界面遵循数字与汉字混排留白:数值与量词视觉上分开,便于快速扫读。
|
|
1780
|
+
group([num(statsProj.turns + ' 轮'), ' · ', num(statsProj.steps + ' 步')], false, 'turnsSteps');
|
|
1781
|
+
}
|
|
1801
1782
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
}
|
|
1783
|
+
const durations = [];
|
|
1784
|
+
if (statsProj.llmMs > 0 && fieldVisible('llmTime')) durations.push(fieldSpan('llmTime', 'durl', metric('LLM', formatDuration(statsProj.llmMs))));
|
|
1785
|
+
if (statsProj.toolMs > 0 && fieldVisible('toolTime')) {
|
|
1786
|
+
if (durations.length > 0) durations.push(' · ');
|
|
1787
|
+
durations.push(fieldSpan('toolTime', 'durt', metric('工具调用', formatDuration(statsProj.toolMs))));
|
|
1788
|
+
}
|
|
1789
|
+
if (durations.length > 0) group(durations);
|
|
1828
1790
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
-
}
|
|
1791
|
+
const speeds = [];
|
|
1792
|
+
if (statsProj.ttftSteps > 0) speeds.push(metric('首 token 平均', formatDuration(statsProj.ttftMs / statsProj.ttftSteps)));
|
|
1793
|
+
if (statsProj.decodeMs > 0) speeds.push(' · ', num(formatTps(statsProj.decodeTokens / (statsProj.decodeMs / 1e3)) + ' tok/s'));
|
|
1794
|
+
if (speeds.length > 0) group(speeds, HIDE_SPEED_FIELDS); // 不占可见版式,title 浮窗保留(官方隐藏字段,非用户可配)
|
|
1849
1795
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1796
|
+
if (usageProj && (billedInput(usageProj) > 0 || (usageProj.outputTokens || 0) > 0)) {
|
|
1797
|
+
const denom = billedInput(usageProj);
|
|
1798
|
+
const hit = denom > 0 ? Math.round(((usageProj.cacheReadTokens || 0) / denom) * 100) : null;
|
|
1799
|
+
if (hit != null && fieldVisible('cacheHit')) group([metric('缓存命中', hit + '%')], false, 'cacheHit');
|
|
1800
|
+
if (fieldVisible('tokensIO')) {
|
|
1801
|
+
group([metric('输入', formatTokens(billedInput(usageProj)) + ' tok'), ' · ', metric('输出', formatTokens(usageProj.outputTokens || 0) + ' tok')], false, 'tokensIO');
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const nativeLine = ng.map(function (g) { return g.text; }).join(' | ');
|
|
1806
|
+
const ngNodes = [];
|
|
1807
|
+
let visCount = 0;
|
|
1808
|
+
for (let i = 0; i < ng.length; i++) {
|
|
1809
|
+
if (ng[i].hidden) continue; // 隐藏分组不占版式(title 仍含其文本)
|
|
1810
|
+
if (visCount > 0) ngNodes.push(React.createElement('span', { key: 'nsep' + i, className: 'bi-sep' }, '|'));
|
|
1811
|
+
visCount++;
|
|
1812
|
+
ngNodes.push(React.createElement('span', {
|
|
1813
|
+
key: 'ng' + i,
|
|
1814
|
+
'data-field': ng[i].fieldId || undefined,
|
|
1815
|
+
style: ng[i].fieldId ? fieldStyle(ng[i].fieldId) : undefined,
|
|
1816
|
+
}, ng[i].nodes));
|
|
1817
|
+
}
|
|
1818
|
+
// D6:原生组全部被隐藏(或全空)→ 原生行不渲染(不留空行/占位;hover 浮窗随之消失)
|
|
1819
|
+
if (ngNodes.length === 0) row1 = null;
|
|
1820
|
+
else row1 = React.createElement('div', { id: 'dsh-bottom-info-bar-native', className: 'bi-native-row', title: nativeLine }, ...ngNodes);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// D6 用户拍板:全部字段隐藏 = 底栏彻底移除——不渲染任何 DOM(无空行/占位高度/悬空分隔符),
|
|
1824
|
+
// density 点击因无 DOM 而天然无副作用、不报错。两条路径:①配置层面所有字段都被关闭;
|
|
1825
|
+
// ②渲染层面(数据条件导致)原生行/主行全空。
|
|
1826
|
+
if (infoBarShouldRemoveAll(FIELD_REGISTRY, fieldVisible) || (row1 === null && nodes.length === 0)) {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const animatedRow1 = row1 === null ? null : React.createElement('div', { className: 'bi-density-extra' },
|
|
1831
|
+
React.createElement('div', { className: 'bi-density-extra-inner' }, row1));
|
|
1832
|
+
const rootCls = 'bi-root';
|
|
1833
|
+
return React.createElement('div', {
|
|
1834
|
+
className: rootCls,
|
|
1835
|
+
onClick: function () { props.onToggleDensity(); },
|
|
1836
|
+
onKeyDown: function (event) {
|
|
1837
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1838
|
+
event.preventDefault();
|
|
1839
|
+
props.onToggleDensity();
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
role: 'button',
|
|
1843
|
+
tabIndex: 0,
|
|
1844
|
+
'aria-labelledby': full && row1 !== null ? 'dsh-bottom-info-bar-native dsh-bottom-info-bar-primary' : 'dsh-bottom-info-bar-primary',
|
|
1845
|
+
'aria-describedby': 'dsh-bottom-info-bar-action',
|
|
1846
|
+
'aria-pressed': full,
|
|
1847
|
+
'aria-busy': isDensitySaving,
|
|
1848
|
+
'aria-disabled': isDensitySaving,
|
|
1849
|
+
'data-density': displayDensity,
|
|
1850
|
+
'data-density-saving': isDensitySaving,
|
|
1851
|
+
title: isDensitySaving ? '正在保存切换…' : '单击切换 完整/简洁',
|
|
1852
|
+
}, animatedRow1, row2,
|
|
1853
|
+
React.createElement('span', { id: 'dsh-bottom-info-bar-action', className: 'bi-sr-only' },
|
|
1854
|
+
full ? '按 Enter 或空格切换为简洁模式。' : '按 Enter 或空格切换为完整模式。'));
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
};
|
|
1861
1858
|
|
|
1862
1859
|
return module.exports;
|
|
1863
1860
|
} });
|