dsh-token-gauge 0.1.7 → 0.1.9
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.mjs +71 -44
- package/package.json +84 -84
- package/src/client.ts +341 -334
package/lib/client.mjs
CHANGED
|
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
32
32
|
var client_exports = {};
|
|
33
33
|
__export(client_exports, {
|
|
34
34
|
apply: () => apply,
|
|
35
|
+
inject: () => inject,
|
|
35
36
|
name: () => name
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(client_exports);
|
|
@@ -943,8 +944,41 @@ var gaugeRef = (el) => {
|
|
|
943
944
|
mountGauge(el);
|
|
944
945
|
}
|
|
945
946
|
};
|
|
947
|
+
function GaugePanel() {
|
|
948
|
+
return import_react.default.createElement("div", {
|
|
949
|
+
ref: gaugeRef,
|
|
950
|
+
className: "tg-panel-root",
|
|
951
|
+
style: {
|
|
952
|
+
position: "fixed",
|
|
953
|
+
right: "0",
|
|
954
|
+
bottom: "0",
|
|
955
|
+
width: "420px",
|
|
956
|
+
height: "33vh",
|
|
957
|
+
zIndex: 50,
|
|
958
|
+
pointerEvents: "auto",
|
|
959
|
+
boxSizing: "border-box",
|
|
960
|
+
overflow: "hidden",
|
|
961
|
+
borderTop: "1px solid var(--dsw-alias-hairline)",
|
|
962
|
+
borderLeft: "1px solid var(--dsw-alias-hairline)",
|
|
963
|
+
background: "var(--dsw-alias-bg-base)",
|
|
964
|
+
color: "var(--dsw-alias-label-primary)",
|
|
965
|
+
boxShadow: "0 0 24px rgba(0,0,0,0.28)"
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
946
969
|
function GaugeTab() {
|
|
947
|
-
return import_react.default.createElement("div", {
|
|
970
|
+
return import_react.default.createElement("div", {
|
|
971
|
+
ref: gaugeRef,
|
|
972
|
+
className: "tg-panel-root",
|
|
973
|
+
style: {
|
|
974
|
+
width: "100%",
|
|
975
|
+
height: "100%",
|
|
976
|
+
boxSizing: "border-box",
|
|
977
|
+
overflow: "hidden",
|
|
978
|
+
background: "var(--dsw-alias-bg-base)",
|
|
979
|
+
color: "var(--dsw-alias-label-primary)"
|
|
980
|
+
}
|
|
981
|
+
});
|
|
948
982
|
}
|
|
949
983
|
var GaugeIcon = (size) => {
|
|
950
984
|
const s = typeof size === "number" && size > 0 ? size : 16;
|
|
@@ -972,61 +1006,54 @@ var GaugeIcon = (size) => {
|
|
|
972
1006
|
};
|
|
973
1007
|
var TAB_ORDER = 90;
|
|
974
1008
|
var name = PLUGIN_ID;
|
|
1009
|
+
var inject = ["slots"];
|
|
975
1010
|
function apply(ctx) {
|
|
976
|
-
let
|
|
977
|
-
let
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1011
|
+
let unregisterTab = null;
|
|
1012
|
+
let overlayDispose = void 0;
|
|
1013
|
+
let registeredViaBetter = false;
|
|
1014
|
+
let fallbackTimer = void 0;
|
|
1015
|
+
const tryMountOverlay = (reason) => {
|
|
1016
|
+
if (registeredViaBetter || overlayDispose !== void 0) return;
|
|
1017
|
+
console.warn(`[dsh-token-gauge] ${reason}\uFF0C\u4EEA\u8868\u76D8\u6539\u4E3A\u53F3\u4E0B\u89D2\u6D6E\u5C42\u663E\u793A\uFF08\u4ECD\u53EF\u7528\uFF09\u3002`);
|
|
981
1018
|
try {
|
|
982
|
-
|
|
983
|
-
id: PLUGIN_ID,
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
order: TAB_ORDER,
|
|
987
|
-
icon: GaugeIcon,
|
|
988
|
-
component: GaugeTab
|
|
989
|
-
});
|
|
1019
|
+
overlayDispose = ctx.slots.register(
|
|
1020
|
+
{ name: "shell.overlay", id: PLUGIN_ID, order: 100, label: "Token \u4EEA\u8868\u76D8" },
|
|
1021
|
+
() => import_react.default.createElement(GaugePanel)
|
|
1022
|
+
);
|
|
990
1023
|
} catch (e) {
|
|
991
|
-
|
|
1024
|
+
console.error("[dsh-token-gauge] \u6D6E\u5C42\u515C\u5E95\u4E5F\u5931\u8D25", e);
|
|
992
1025
|
}
|
|
993
1026
|
};
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1027
|
+
const disposeSidebar = ctx.inject(["betterSidebar"], (sidebarCtx) => {
|
|
1028
|
+
const sidebar = sidebarCtx.betterSidebar;
|
|
1029
|
+
if (!sidebar || typeof sidebar.registerTab !== "function") return;
|
|
1030
|
+
unregisterTab = sidebar.registerTab({
|
|
1031
|
+
id: PLUGIN_ID,
|
|
1032
|
+
title: "Token \u4EEA\u8868\u76D8",
|
|
1033
|
+
single: true,
|
|
1034
|
+
order: TAB_ORDER,
|
|
1035
|
+
icon: GaugeIcon,
|
|
1036
|
+
component: () => import_react.default.createElement(GaugeTab)
|
|
998
1037
|
});
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const tryRead = () => {
|
|
1002
|
-
if (registered) return;
|
|
1038
|
+
registeredViaBetter = true;
|
|
1039
|
+
if (typeof fallbackTimer !== "undefined") clearTimeout(fallbackTimer);
|
|
1003
1040
|
try {
|
|
1004
|
-
|
|
1005
|
-
register(s);
|
|
1041
|
+
if (typeof sidebar.openTab === "function") sidebar.openTab({ type: PLUGIN_ID, single: true });
|
|
1006
1042
|
} catch (e) {
|
|
1007
1043
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}, 500);
|
|
1016
|
-
}
|
|
1017
|
-
const stop = setTimeout(() => {
|
|
1018
|
-
if (timer !== void 0) clearInterval(timer);
|
|
1019
|
-
if (!registered) {
|
|
1020
|
-
console.error(
|
|
1021
|
-
"[dsh-token-gauge] \u672A\u53D6\u5F97 dsh-better-sidebar \u670D\u52A1\uFF0C\u4FA7\u8FB9\u680F Tab \u672A\u6CE8\u518C\u3002\n \u8BF7\u628A\u5B83\u88C5\u6210 profile \u7684\u300C\u76F4\u63A5\u4F9D\u8D56\u300D\u5E76\u6302\u8F7D\uFF1A\n dsh plugin --profile web add dsh-better-sidebar\n \u7136\u540E\u5B8C\u6574\u91CD\u542F dsh web\u3002\n \u6CE8\u610F\uFF1A\u4EC5\u4F5C\u4E3A\u672C\u63D2\u4EF6\u7684\u4F20\u9012\u4F9D\u8D56\u5B58\u5728\u4E8E node_modules \u662F\u4E0D\u591F\u7684\u2014\u2014DSH \u4E0D\u4F1A\u628A\n \u4F20\u9012\u4F9D\u8D56\u6302\u6210 bundle \u5C42\uFF0C\u90A3\u79CD\u60C5\u51B5\u4E0B\u672C\u63D2\u4EF6\u4F1A\u9759\u9ED8\u5730\u6CA1\u6709 Tab\u3002"
|
|
1022
|
-
);
|
|
1044
|
+
return () => {
|
|
1045
|
+
if (unregisterTab) unregisterTab();
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
fallbackTimer = setTimeout(() => {
|
|
1049
|
+
if (!registeredViaBetter) {
|
|
1050
|
+
tryMountOverlay("\u672A\u53D6\u5F97 dsh-better-sidebar \u670D\u52A1\uFF08\u8BF7\u786E\u8BA4\u5176\u5DF2\u5B89\u88C5\u5E76\u542F\u7528\uFF09");
|
|
1023
1051
|
}
|
|
1024
1052
|
}, 3e4);
|
|
1025
1053
|
return () => {
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
if (typeof
|
|
1029
|
-
if (typeof disposeInject === "function") disposeInject();
|
|
1054
|
+
if (typeof fallbackTimer !== "undefined") clearTimeout(fallbackTimer);
|
|
1055
|
+
if (typeof disposeSidebar === "function") disposeSidebar();
|
|
1056
|
+
if (typeof overlayDispose === "function") overlayDispose();
|
|
1030
1057
|
};
|
|
1031
1058
|
}
|
|
1032
1059
|
; return module.exports; } });
|
package/package.json
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-token-gauge",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 机车风格 Token 用量/费用仪表盘侧边栏插件(稳定版 0.1.1-rc.x 兼容)",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.mjs",
|
|
7
|
-
"module": "lib/index.mjs",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./lib/index.mjs",
|
|
10
|
-
"./client": "./lib/client.mjs",
|
|
11
|
-
"./package.json": "./package.json"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"lib/index.mjs",
|
|
15
|
-
"lib/client.mjs",
|
|
16
|
-
"lib/LICENSE",
|
|
17
|
-
"src",
|
|
18
|
-
"preview",
|
|
19
|
-
"README.md",
|
|
20
|
-
"README.en.md",
|
|
21
|
-
"LICENSE",
|
|
22
|
-
"cordis.patch.yml",
|
|
23
|
-
"build.mjs",
|
|
24
|
-
"tsconfig.json"
|
|
25
|
-
],
|
|
26
|
-
"author": "WorkBuddy User",
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=20"
|
|
30
|
-
},
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/liumiaozhi/dsh-token-gauge.git"
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com/liumiaozhi/dsh-token-gauge#readme",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/liumiaozhi/dsh-token-gauge/issues"
|
|
38
|
-
},
|
|
39
|
-
"publishConfig": {
|
|
40
|
-
"registry": "https://registry.npmjs.org",
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
|
-
"dsh": {
|
|
44
|
-
"engines": {
|
|
45
|
-
"dsh": ">=0.1.1-rc.1"
|
|
46
|
-
},
|
|
47
|
-
"bundle": {
|
|
48
|
-
"patch": "./cordis.patch.yml"
|
|
49
|
-
},
|
|
50
|
-
"compatibility": {
|
|
51
|
-
"dshReleases": {
|
|
52
|
-
"0.1.1-rc.1": "compatible",
|
|
53
|
-
"0.1.1-rc.2": "compatible"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "node build.mjs",
|
|
59
|
-
"prepack": "node build.mjs"
|
|
60
|
-
},
|
|
61
|
-
"peerDependencies": {
|
|
62
|
-
"@deepseek-ai/cordis": "*",
|
|
63
|
-
"@deepseek-ai/dsh-host-webserver": "*",
|
|
64
|
-
"dsh-better-sidebar": ">=0.15.2"
|
|
65
|
-
},
|
|
66
|
-
"peerDependenciesMeta": {
|
|
67
|
-
"@deepseek-ai/cordis": {
|
|
68
|
-
"optional": true
|
|
69
|
-
},
|
|
70
|
-
"@deepseek-ai/dsh-host-webserver": {
|
|
71
|
-
"optional": true
|
|
72
|
-
},
|
|
73
|
-
"dsh-better-sidebar": {
|
|
74
|
-
"optional": true
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
"devDependencies": {
|
|
78
|
-
"@deepseek-ai/cordis": "*",
|
|
79
|
-
"@types/react": "^18.2.0",
|
|
80
|
-
"esbuild": "^0.21.5",
|
|
81
|
-
"react": "^18.2.0",
|
|
82
|
-
"typescript": "^5.4.0"
|
|
83
|
-
}
|
|
84
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-token-gauge",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "DSH 机车风格 Token 用量/费用仪表盘侧边栏插件(稳定版 0.1.1-rc.x 兼容)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"module": "lib/index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.mjs",
|
|
10
|
+
"./client": "./lib/client.mjs",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/index.mjs",
|
|
15
|
+
"lib/client.mjs",
|
|
16
|
+
"lib/LICENSE",
|
|
17
|
+
"src",
|
|
18
|
+
"preview",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.en.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"build.mjs",
|
|
24
|
+
"tsconfig.json"
|
|
25
|
+
],
|
|
26
|
+
"author": "WorkBuddy User",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/liumiaozhi/dsh-token-gauge.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/liumiaozhi/dsh-token-gauge#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/liumiaozhi/dsh-token-gauge/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dsh": {
|
|
44
|
+
"engines": {
|
|
45
|
+
"dsh": ">=0.1.1-rc.1"
|
|
46
|
+
},
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"compatibility": {
|
|
51
|
+
"dshReleases": {
|
|
52
|
+
"0.1.1-rc.1": "compatible",
|
|
53
|
+
"0.1.1-rc.2": "compatible"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "node build.mjs",
|
|
59
|
+
"prepack": "node build.mjs"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@deepseek-ai/cordis": "*",
|
|
63
|
+
"@deepseek-ai/dsh-host-webserver": "*",
|
|
64
|
+
"dsh-better-sidebar": ">=0.15.2"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@deepseek-ai/cordis": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"dsh-better-sidebar": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@deepseek-ai/cordis": "*",
|
|
79
|
+
"@types/react": "^18.2.0",
|
|
80
|
+
"esbuild": "^0.21.5",
|
|
81
|
+
"react": "^18.2.0",
|
|
82
|
+
"typescript": "^5.4.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,334 +1,341 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// DSH Client 半边:注册侧边栏 Tab,渲染机车风格仪表盘 + 设置弹窗
|
|
3
|
-
// 运行环境:浏览器(web 平台,与 webServer 同域)。纯原生 DOM/SVG 仪表盘,
|
|
4
|
-
// 通过同域 HTTP 与 Host 半边通信:
|
|
5
|
-
// GET /plugins/dsh-token-gauge/state -> 完整 State(JSON)
|
|
6
|
-
// POST /plugins/dsh-token-gauge/cmd -> {type,payload} -> 更新后 State
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// 为避免与运行时 React 产生双实例 hook 冲突,本组件不调用任何 hook,
|
|
10
|
-
// 仅在 ref 回调里挂载一次原生 DOM 仪表盘,并自行轮询刷新。
|
|
11
|
-
// ============================================================================
|
|
12
|
-
import React from 'react'
|
|
13
|
-
import { getStyle, applyPalette, buildCss } from './styles'
|
|
14
|
-
import { createGauge } from './gauge'
|
|
15
|
-
import { t } from './i18n'
|
|
16
|
-
import { downloadReport } from './export'
|
|
17
|
-
import { createSettingsModal } from './settings'
|
|
18
|
-
|
|
19
|
-
const PLUGIN_ID = 'dsh-token-gauge'
|
|
20
|
-
const ROUTE_STATE = '/plugins/dsh-token-gauge/state'
|
|
21
|
-
const ROUTE_CMD = '/plugins/dsh-token-gauge/cmd'
|
|
22
|
-
|
|
23
|
-
function totalOf(b: any): number {
|
|
24
|
-
return (b?.input || 0) + (b?.cacheRead || 0) + (b?.cacheWrite || 0) + (b?.output || 0)
|
|
25
|
-
}
|
|
26
|
-
function todayKey(d = new Date()): string {
|
|
27
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
28
|
-
}
|
|
29
|
-
function monthKey(d = new Date()): string {
|
|
30
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
31
|
-
}
|
|
32
|
-
function weekKey(d = new Date()): string {
|
|
33
|
-
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
|
|
34
|
-
const dayNum = (date.getUTCDay() + 6) % 7
|
|
35
|
-
date.setUTCDate(date.getUTCDate() - dayNum + 3)
|
|
36
|
-
const firstThursday = new Date(Date.UTC(date.getUTCFullYear(), 0, 4))
|
|
37
|
-
const week =
|
|
38
|
-
1 +
|
|
39
|
-
Math.round(
|
|
40
|
-
((date.getTime() - firstThursday.getTime()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7,
|
|
41
|
-
)
|
|
42
|
-
return `${d.getFullYear()}-W${String(week).padStart(2, '0')}`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function fallbackState(): any {
|
|
46
|
-
return {
|
|
47
|
-
global: {},
|
|
48
|
-
daily: {},
|
|
49
|
-
weekly: {},
|
|
50
|
-
monthly: {},
|
|
51
|
-
sessions: {},
|
|
52
|
-
models: {},
|
|
53
|
-
activeSession: '',
|
|
54
|
-
settings: {
|
|
55
|
-
style: 'ev',
|
|
56
|
-
palette: 'neon',
|
|
57
|
-
lang: 'zh',
|
|
58
|
-
metric: 'global',
|
|
59
|
-
display: { usage: true, cost: true, io: true, cacheMiss: true },
|
|
60
|
-
limits: { global: null, daily: null, weekly: null, monthly: null },
|
|
61
|
-
pricing: {},
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 计算当前指标的量程/费用
|
|
67
|
-
function computeMetric(s: any): { value: number; limit: number | null; cost: number } {
|
|
68
|
-
const metric = s.settings.metric
|
|
69
|
-
let entry: any = s.global
|
|
70
|
-
let limit: number | null = s.settings.limits.global
|
|
71
|
-
if (metric === 'session') {
|
|
72
|
-
entry = s.sessions[s.activeSession] || {}
|
|
73
|
-
limit = s.settings.limits.daily
|
|
74
|
-
} else if (metric === 'today' || metric === 'daily') {
|
|
75
|
-
entry = s.daily[todayKey()] || {}
|
|
76
|
-
limit = s.settings.limits.daily
|
|
77
|
-
} else if (metric === 'weekly') {
|
|
78
|
-
entry = s.weekly[weekKey()] || {}
|
|
79
|
-
limit = s.settings.limits.weekly
|
|
80
|
-
} else if (metric === 'monthly') {
|
|
81
|
-
entry = s.monthly[monthKey()] || {}
|
|
82
|
-
limit = s.settings.limits.monthly
|
|
83
|
-
}
|
|
84
|
-
return { value: totalOf(entry), limit, cost: entry.cost || 0 }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 把原生 DOM 仪表盘挂载进容器,并启动轮询
|
|
88
|
-
function mountGauge(container: HTMLElement): void {
|
|
89
|
-
// 注入一次全局样式
|
|
90
|
-
if (!document.getElementById('tg-gauge-style')) {
|
|
91
|
-
const st = document.createElement('style')
|
|
92
|
-
st.id = 'tg-gauge-style'
|
|
93
|
-
st.textContent = buildCss()
|
|
94
|
-
document.head.appendChild(st)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let state: any = null
|
|
98
|
-
const tr = (k: string) => t((state?.settings?.lang as 'zh' | 'en') || 'zh', k)
|
|
99
|
-
|
|
100
|
-
const root = container
|
|
101
|
-
root.className = 'tg-root tg-style-ev'
|
|
102
|
-
|
|
103
|
-
// 头部
|
|
104
|
-
const head = document.createElement('div')
|
|
105
|
-
head.className = 'tg-head'
|
|
106
|
-
const titleEl = document.createElement('div')
|
|
107
|
-
titleEl.className = 'tg-title'
|
|
108
|
-
titleEl.textContent = 'Token 仪表盘'
|
|
109
|
-
const gear = document.createElement('div')
|
|
110
|
-
gear.className = 'tg-gear'
|
|
111
|
-
gear.textContent = '⚙'
|
|
112
|
-
gear.title = '设置'
|
|
113
|
-
head.append(titleEl, gear)
|
|
114
|
-
root.appendChild(head)
|
|
115
|
-
|
|
116
|
-
// 空数据提示
|
|
117
|
-
const empty = document.createElement('div')
|
|
118
|
-
empty.className = 'tg-empty'
|
|
119
|
-
empty.style.display = 'none'
|
|
120
|
-
root.appendChild(empty)
|
|
121
|
-
|
|
122
|
-
// 仪表盘
|
|
123
|
-
const gauge = createGauge()
|
|
124
|
-
root.appendChild(gauge.el)
|
|
125
|
-
|
|
126
|
-
// 设置弹窗
|
|
127
|
-
const modal = createSettingsModal({
|
|
128
|
-
getLang: () => (state?.settings?.lang as 'zh' | 'en') || 'zh',
|
|
129
|
-
onSettings: (patch) => sendCmd('setSettings', patch),
|
|
130
|
-
onExport: (format) => {
|
|
131
|
-
if (state) downloadReport(state, (state.settings.lang as 'zh' | 'en') || 'zh', format)
|
|
132
|
-
},
|
|
133
|
-
onClear: () => sendCmd('clearUsage', undefined),
|
|
134
|
-
onReset: () => sendCmd('resetSettings', undefined),
|
|
135
|
-
})
|
|
136
|
-
root.appendChild(modal.el)
|
|
137
|
-
|
|
138
|
-
gear.addEventListener('click', () => modal.open(state || fallbackState()))
|
|
139
|
-
|
|
140
|
-
const applyState = (s: any): void => {
|
|
141
|
-
state = s
|
|
142
|
-
root.className = 'tg-root ' + getStyle(s.settings.style).className
|
|
143
|
-
applyPalette(root, s.settings.palette)
|
|
144
|
-
titleEl.textContent = tr('title')
|
|
145
|
-
const m = computeMetric(s)
|
|
146
|
-
gauge.update({ value: m.value, limit: m.limit, cost: m.cost, state: s, display: s.settings.display, t: tr, lang: s.settings.lang })
|
|
147
|
-
const hasData = totalOf(s.global) > 0
|
|
148
|
-
empty.style.display = hasData ? 'none' : ''
|
|
149
|
-
empty.textContent = tr('noData')
|
|
150
|
-
modal.sync(s)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 拉取最新状态
|
|
154
|
-
async function poll(): Promise<void> {
|
|
155
|
-
try {
|
|
156
|
-
const r = await fetch(ROUTE_STATE, { headers: { Accept: 'application/json' } })
|
|
157
|
-
if (r.ok) {
|
|
158
|
-
const s = await r.json()
|
|
159
|
-
applyState(s)
|
|
160
|
-
}
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// 网络抖动忽略,下一轮重试
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 发送指令并刷新
|
|
167
|
-
async function sendCmd(type: string, payload: any): Promise<void> {
|
|
168
|
-
try {
|
|
169
|
-
await fetch(ROUTE_CMD, {
|
|
170
|
-
method: 'POST',
|
|
171
|
-
headers: { 'Content-Type': 'application/json' },
|
|
172
|
-
body: JSON.stringify({ type, payload }),
|
|
173
|
-
})
|
|
174
|
-
} catch (e) {
|
|
175
|
-
console.warn(`[${PLUGIN_ID}] cmd ${type} failed`, e)
|
|
176
|
-
}
|
|
177
|
-
await poll()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// 立即拉一次,并定时轮询
|
|
181
|
-
void poll()
|
|
182
|
-
const timer = window.setInterval(() => void poll(), 2000)
|
|
183
|
-
// 容器被移除时停止轮询
|
|
184
|
-
const mo = new MutationObserver(() => {
|
|
185
|
-
if (!document.body.contains(container)) {
|
|
186
|
-
window.clearInterval(timer)
|
|
187
|
-
mo.disconnect()
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
mo.observe(document.body, { childList: true, subtree: true })
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 稳定的 ref 回调:只在首次挂载时构建一次仪表盘
|
|
194
|
-
const gaugeRef = (el: HTMLElement | null): void => {
|
|
195
|
-
if (el && !el.dataset.tgMounted) {
|
|
196
|
-
el.dataset.tgMounted = '1'
|
|
197
|
-
mountGauge(el)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// DSH Client 半边:注册侧边栏 Tab,渲染机车风格仪表盘 + 设置弹窗
|
|
3
|
+
// 运行环境:浏览器(web 平台,与 webServer 同域)。纯原生 DOM/SVG 仪表盘,
|
|
4
|
+
// 通过同域 HTTP 与 Host 半边通信:
|
|
5
|
+
// GET /plugins/dsh-token-gauge/state -> 完整 State(JSON)
|
|
6
|
+
// POST /plugins/dsh-token-gauge/cmd -> {type,payload} -> 更新后 State
|
|
7
|
+
// 优先把仪表盘注册到 dsh-better-sidebar 的右侧工作台 Tab(停靠侧边栏);若 better-sidebar
|
|
8
|
+
// 未就绪,回退到官方 shell.overlay 浮层(双模式,保证仪表盘永远可见)。
|
|
9
|
+
// 为避免与运行时 React 产生双实例 hook 冲突,本组件不调用任何 hook,
|
|
10
|
+
// 仅在 ref 回调里挂载一次原生 DOM 仪表盘,并自行轮询刷新。
|
|
11
|
+
// ============================================================================
|
|
12
|
+
import React from 'react'
|
|
13
|
+
import { getStyle, applyPalette, buildCss } from './styles'
|
|
14
|
+
import { createGauge } from './gauge'
|
|
15
|
+
import { t } from './i18n'
|
|
16
|
+
import { downloadReport } from './export'
|
|
17
|
+
import { createSettingsModal } from './settings'
|
|
18
|
+
|
|
19
|
+
const PLUGIN_ID = 'dsh-token-gauge'
|
|
20
|
+
const ROUTE_STATE = '/plugins/dsh-token-gauge/state'
|
|
21
|
+
const ROUTE_CMD = '/plugins/dsh-token-gauge/cmd'
|
|
22
|
+
|
|
23
|
+
function totalOf(b: any): number {
|
|
24
|
+
return (b?.input || 0) + (b?.cacheRead || 0) + (b?.cacheWrite || 0) + (b?.output || 0)
|
|
25
|
+
}
|
|
26
|
+
function todayKey(d = new Date()): string {
|
|
27
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
28
|
+
}
|
|
29
|
+
function monthKey(d = new Date()): string {
|
|
30
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
31
|
+
}
|
|
32
|
+
function weekKey(d = new Date()): string {
|
|
33
|
+
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
|
|
34
|
+
const dayNum = (date.getUTCDay() + 6) % 7
|
|
35
|
+
date.setUTCDate(date.getUTCDate() - dayNum + 3)
|
|
36
|
+
const firstThursday = new Date(Date.UTC(date.getUTCFullYear(), 0, 4))
|
|
37
|
+
const week =
|
|
38
|
+
1 +
|
|
39
|
+
Math.round(
|
|
40
|
+
((date.getTime() - firstThursday.getTime()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7,
|
|
41
|
+
)
|
|
42
|
+
return `${d.getFullYear()}-W${String(week).padStart(2, '0')}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fallbackState(): any {
|
|
46
|
+
return {
|
|
47
|
+
global: {},
|
|
48
|
+
daily: {},
|
|
49
|
+
weekly: {},
|
|
50
|
+
monthly: {},
|
|
51
|
+
sessions: {},
|
|
52
|
+
models: {},
|
|
53
|
+
activeSession: '',
|
|
54
|
+
settings: {
|
|
55
|
+
style: 'ev',
|
|
56
|
+
palette: 'neon',
|
|
57
|
+
lang: 'zh',
|
|
58
|
+
metric: 'global',
|
|
59
|
+
display: { usage: true, cost: true, io: true, cacheMiss: true },
|
|
60
|
+
limits: { global: null, daily: null, weekly: null, monthly: null },
|
|
61
|
+
pricing: {},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 计算当前指标的量程/费用
|
|
67
|
+
function computeMetric(s: any): { value: number; limit: number | null; cost: number } {
|
|
68
|
+
const metric = s.settings.metric
|
|
69
|
+
let entry: any = s.global
|
|
70
|
+
let limit: number | null = s.settings.limits.global
|
|
71
|
+
if (metric === 'session') {
|
|
72
|
+
entry = s.sessions[s.activeSession] || {}
|
|
73
|
+
limit = s.settings.limits.daily
|
|
74
|
+
} else if (metric === 'today' || metric === 'daily') {
|
|
75
|
+
entry = s.daily[todayKey()] || {}
|
|
76
|
+
limit = s.settings.limits.daily
|
|
77
|
+
} else if (metric === 'weekly') {
|
|
78
|
+
entry = s.weekly[weekKey()] || {}
|
|
79
|
+
limit = s.settings.limits.weekly
|
|
80
|
+
} else if (metric === 'monthly') {
|
|
81
|
+
entry = s.monthly[monthKey()] || {}
|
|
82
|
+
limit = s.settings.limits.monthly
|
|
83
|
+
}
|
|
84
|
+
return { value: totalOf(entry), limit, cost: entry.cost || 0 }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 把原生 DOM 仪表盘挂载进容器,并启动轮询
|
|
88
|
+
function mountGauge(container: HTMLElement): void {
|
|
89
|
+
// 注入一次全局样式
|
|
90
|
+
if (!document.getElementById('tg-gauge-style')) {
|
|
91
|
+
const st = document.createElement('style')
|
|
92
|
+
st.id = 'tg-gauge-style'
|
|
93
|
+
st.textContent = buildCss()
|
|
94
|
+
document.head.appendChild(st)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let state: any = null
|
|
98
|
+
const tr = (k: string) => t((state?.settings?.lang as 'zh' | 'en') || 'zh', k)
|
|
99
|
+
|
|
100
|
+
const root = container
|
|
101
|
+
root.className = 'tg-root tg-style-ev'
|
|
102
|
+
|
|
103
|
+
// 头部
|
|
104
|
+
const head = document.createElement('div')
|
|
105
|
+
head.className = 'tg-head'
|
|
106
|
+
const titleEl = document.createElement('div')
|
|
107
|
+
titleEl.className = 'tg-title'
|
|
108
|
+
titleEl.textContent = 'Token 仪表盘'
|
|
109
|
+
const gear = document.createElement('div')
|
|
110
|
+
gear.className = 'tg-gear'
|
|
111
|
+
gear.textContent = '⚙'
|
|
112
|
+
gear.title = '设置'
|
|
113
|
+
head.append(titleEl, gear)
|
|
114
|
+
root.appendChild(head)
|
|
115
|
+
|
|
116
|
+
// 空数据提示
|
|
117
|
+
const empty = document.createElement('div')
|
|
118
|
+
empty.className = 'tg-empty'
|
|
119
|
+
empty.style.display = 'none'
|
|
120
|
+
root.appendChild(empty)
|
|
121
|
+
|
|
122
|
+
// 仪表盘
|
|
123
|
+
const gauge = createGauge()
|
|
124
|
+
root.appendChild(gauge.el)
|
|
125
|
+
|
|
126
|
+
// 设置弹窗
|
|
127
|
+
const modal = createSettingsModal({
|
|
128
|
+
getLang: () => (state?.settings?.lang as 'zh' | 'en') || 'zh',
|
|
129
|
+
onSettings: (patch) => sendCmd('setSettings', patch),
|
|
130
|
+
onExport: (format) => {
|
|
131
|
+
if (state) downloadReport(state, (state.settings.lang as 'zh' | 'en') || 'zh', format)
|
|
132
|
+
},
|
|
133
|
+
onClear: () => sendCmd('clearUsage', undefined),
|
|
134
|
+
onReset: () => sendCmd('resetSettings', undefined),
|
|
135
|
+
})
|
|
136
|
+
root.appendChild(modal.el)
|
|
137
|
+
|
|
138
|
+
gear.addEventListener('click', () => modal.open(state || fallbackState()))
|
|
139
|
+
|
|
140
|
+
const applyState = (s: any): void => {
|
|
141
|
+
state = s
|
|
142
|
+
root.className = 'tg-root ' + getStyle(s.settings.style).className
|
|
143
|
+
applyPalette(root, s.settings.palette)
|
|
144
|
+
titleEl.textContent = tr('title')
|
|
145
|
+
const m = computeMetric(s)
|
|
146
|
+
gauge.update({ value: m.value, limit: m.limit, cost: m.cost, state: s, display: s.settings.display, t: tr, lang: s.settings.lang })
|
|
147
|
+
const hasData = totalOf(s.global) > 0
|
|
148
|
+
empty.style.display = hasData ? 'none' : ''
|
|
149
|
+
empty.textContent = tr('noData')
|
|
150
|
+
modal.sync(s)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 拉取最新状态
|
|
154
|
+
async function poll(): Promise<void> {
|
|
155
|
+
try {
|
|
156
|
+
const r = await fetch(ROUTE_STATE, { headers: { Accept: 'application/json' } })
|
|
157
|
+
if (r.ok) {
|
|
158
|
+
const s = await r.json()
|
|
159
|
+
applyState(s)
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// 网络抖动忽略,下一轮重试
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 发送指令并刷新
|
|
167
|
+
async function sendCmd(type: string, payload: any): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
await fetch(ROUTE_CMD, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ type, payload }),
|
|
173
|
+
})
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.warn(`[${PLUGIN_ID}] cmd ${type} failed`, e)
|
|
176
|
+
}
|
|
177
|
+
await poll()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 立即拉一次,并定时轮询
|
|
181
|
+
void poll()
|
|
182
|
+
const timer = window.setInterval(() => void poll(), 2000)
|
|
183
|
+
// 容器被移除时停止轮询
|
|
184
|
+
const mo = new MutationObserver(() => {
|
|
185
|
+
if (!document.body.contains(container)) {
|
|
186
|
+
window.clearInterval(timer)
|
|
187
|
+
mo.disconnect()
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
mo.observe(document.body, { childList: true, subtree: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 稳定的 ref 回调:只在首次挂载时构建一次仪表盘
|
|
194
|
+
const gaugeRef = (el: HTMLElement | null): void => {
|
|
195
|
+
if (el && !el.dataset.tgMounted) {
|
|
196
|
+
el.dataset.tgMounted = '1'
|
|
197
|
+
mountGauge(el)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 浮层面板组件:固定在右下角、占视口高度 1/3(方案 A:shell.overlay 插槽)。
|
|
202
|
+
// 仍是零 hook、ref 回调挂原生 DOM,规避双 React 实例;外层用内联定位样式,
|
|
203
|
+
// 不被 mountGauge 内的 className 覆盖(style 优先级高于 class)。
|
|
204
|
+
function GaugePanel(): React.ReactElement {
|
|
205
|
+
return React.createElement('div', {
|
|
206
|
+
ref: gaugeRef,
|
|
207
|
+
className: 'tg-panel-root',
|
|
208
|
+
style: {
|
|
209
|
+
position: 'fixed',
|
|
210
|
+
right: '0',
|
|
211
|
+
bottom: '0',
|
|
212
|
+
width: '420px',
|
|
213
|
+
height: '33vh',
|
|
214
|
+
zIndex: 50,
|
|
215
|
+
pointerEvents: 'auto',
|
|
216
|
+
boxSizing: 'border-box',
|
|
217
|
+
overflow: 'hidden',
|
|
218
|
+
borderTop: '1px solid var(--dsw-alias-hairline)',
|
|
219
|
+
borderLeft: '1px solid var(--dsw-alias-hairline)',
|
|
220
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
221
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
222
|
+
boxShadow: '0 0 24px rgba(0,0,0,0.28)',
|
|
223
|
+
} as any,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 停靠在 better-sidebar 工作台 Tab 内的面板:填满容器(非固定定位),同样零 hook、ref 挂原生 DOM。
|
|
228
|
+
function GaugeTab(): React.ReactElement {
|
|
229
|
+
return React.createElement('div', {
|
|
230
|
+
ref: gaugeRef,
|
|
231
|
+
className: 'tg-panel-root',
|
|
232
|
+
style: {
|
|
233
|
+
width: '100%',
|
|
234
|
+
height: '100%',
|
|
235
|
+
boxSizing: 'border-box',
|
|
236
|
+
overflow: 'hidden',
|
|
237
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
238
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
239
|
+
} as any,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 侧边栏图标:better-sidebar 以 `icon(size)` 形式调用(见其内置 tab:
|
|
244
|
+
// `icon: (size) => jsx(Icon, { size })`),故这里接收数字尺寸。用 currentColor
|
|
245
|
+
// 着色以自适应明暗主题,纯 SVG 零依赖。
|
|
246
|
+
const GaugeIcon = (size?: number): any => {
|
|
247
|
+
const s = typeof size === 'number' && size > 0 ? size : 16
|
|
248
|
+
const svgProps: any = {
|
|
249
|
+
width: s,
|
|
250
|
+
height: s,
|
|
251
|
+
viewBox: '0 0 16 16',
|
|
252
|
+
fill: 'none',
|
|
253
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
254
|
+
'aria-hidden': 'true',
|
|
255
|
+
}
|
|
256
|
+
// 仪表盘弧 + 指针
|
|
257
|
+
const arc = React.createElement('path', {
|
|
258
|
+
d: 'M2.6 11.6a5.6 5.6 0 1 1 10.8 0',
|
|
259
|
+
stroke: 'currentColor',
|
|
260
|
+
strokeWidth: 1.5,
|
|
261
|
+
strokeLinecap: 'round',
|
|
262
|
+
})
|
|
263
|
+
const needle = React.createElement('path', {
|
|
264
|
+
d: 'M8 11.6 10.4 7.2',
|
|
265
|
+
stroke: 'currentColor',
|
|
266
|
+
strokeWidth: 1.5,
|
|
267
|
+
strokeLinecap: 'round',
|
|
268
|
+
})
|
|
269
|
+
return React.createElement('svg', svgProps, arc, needle)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 在「+ 新建 Tab」菜单里的排序位。内置 tab 用 20/91/110 等,order 缺省为 100
|
|
273
|
+
// 会排到菜单最底部不易发现,这里显式给一个偏前的值。
|
|
274
|
+
const TAB_ORDER = 90
|
|
275
|
+
|
|
276
|
+
export const name = PLUGIN_ID
|
|
277
|
+
|
|
278
|
+
// slots 是 layout 提供的框架级服务(root 早期即就绪),用于浮层兜底,可安全声明顶层 inject。
|
|
279
|
+
// 注意:betterSidebar 是兄弟 bundle 提供的服务,不用顶层 inject(会触发 pending),改用
|
|
280
|
+
// apply 内的 ctx.inject 软注入回调。
|
|
281
|
+
export const inject = ['slots']
|
|
282
|
+
|
|
283
|
+
export function apply(ctx: any): (() => void) | void {
|
|
284
|
+
let unregisterTab: (() => void) | null = null
|
|
285
|
+
let overlayDispose: unknown = void 0
|
|
286
|
+
let registeredViaBetter = false
|
|
287
|
+
let fallbackTimer: any = void 0
|
|
288
|
+
|
|
289
|
+
// 浮层兜底:把仪表盘挂到官方 shell.overlay(全程右下角 33vh),保证即使 better-sidebar
|
|
290
|
+
// 缺失/未启用,仪表盘也始终可见。
|
|
291
|
+
const tryMountOverlay = (reason: string): void => {
|
|
292
|
+
if (registeredViaBetter || overlayDispose !== void 0) return
|
|
293
|
+
console.warn(`[dsh-token-gauge] ${reason},仪表盘改为右下角浮层显示(仍可用)。`)
|
|
294
|
+
try {
|
|
295
|
+
overlayDispose = ctx.slots.register(
|
|
296
|
+
{ name: 'shell.overlay', id: PLUGIN_ID, order: 100, label: 'Token 仪表盘' },
|
|
297
|
+
() => React.createElement(GaugePanel),
|
|
298
|
+
)
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error('[dsh-token-gauge] 浮层兜底也失败', e)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 主路径:注册到 better-sidebar 右侧工作台 Tab(停靠侧边栏,带图标、靠前排序)
|
|
305
|
+
const disposeSidebar = ctx.inject(['betterSidebar'], (sidebarCtx: any) => {
|
|
306
|
+
const sidebar = sidebarCtx.betterSidebar
|
|
307
|
+
if (!sidebar || typeof sidebar.registerTab !== 'function') return
|
|
308
|
+
unregisterTab = sidebar.registerTab({
|
|
309
|
+
id: PLUGIN_ID,
|
|
310
|
+
title: 'Token 仪表盘',
|
|
311
|
+
single: true,
|
|
312
|
+
order: TAB_ORDER,
|
|
313
|
+
icon: GaugeIcon,
|
|
314
|
+
component: () => React.createElement(GaugeTab),
|
|
315
|
+
})
|
|
316
|
+
registeredViaBetter = true
|
|
317
|
+
if (typeof fallbackTimer !== 'undefined') clearTimeout(fallbackTimer)
|
|
318
|
+
// 会话就绪时尝试自动打开 Tab(未就绪会静默 no-op,用户也可从「+ 新建 Tab」菜单手动打开)
|
|
319
|
+
try {
|
|
320
|
+
if (typeof sidebar.openTab === 'function') sidebar.openTab({ type: PLUGIN_ID, single: true })
|
|
321
|
+
} catch (e) {
|
|
322
|
+
/* ignore */
|
|
323
|
+
}
|
|
324
|
+
return () => {
|
|
325
|
+
if (unregisterTab) unregisterTab()
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// 兜底:30s 内 better-sidebar 仍未就绪 → 浮层
|
|
330
|
+
fallbackTimer = setTimeout(() => {
|
|
331
|
+
if (!registeredViaBetter) {
|
|
332
|
+
tryMountOverlay('未取得 dsh-better-sidebar 服务(请确认其已安装并启用)')
|
|
333
|
+
}
|
|
334
|
+
}, 30000)
|
|
335
|
+
|
|
336
|
+
return () => {
|
|
337
|
+
if (typeof fallbackTimer !== 'undefined') clearTimeout(fallbackTimer)
|
|
338
|
+
if (typeof disposeSidebar === 'function') (disposeSidebar as () => void)()
|
|
339
|
+
if (typeof overlayDispose === 'function') (overlayDispose as () => void)()
|
|
340
|
+
}
|
|
341
|
+
}
|