dsh-token-gauge 0.1.6 → 0.1.8
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/README.md +14 -6
- package/lib/client.mjs +42 -70
- package/package.json +80 -82
- package/src/client.ts +302 -319
package/README.md
CHANGED
|
@@ -19,13 +19,19 @@ DeepSeek Harness(DSH)Web 侧边栏插件:以「机车风格仪表盘」实
|
|
|
19
19
|
## 安装
|
|
20
20
|
|
|
21
21
|
> **兼容性**:本插件面向 **DSH 0.1.1-rc.1 / 0.1.1-rc.2(稳定版)**。采用 `dsh.bundle.patch` 清单 + `cordis.patch.yml` 注册,符合稳定版的插件机制(不兼容新版 0.1.2-alpha 的 `dsh.host/client` 清单)。
|
|
22
|
-
>
|
|
22
|
+
> **前置依赖 `dsh-better-sidebar`**:本插件的 Tab 通过 `ctx.betterSidebar.registerTab` 注册到它的**右侧工作台**,所以它必须是 **profile 的直接依赖、且已挂进 `dsh.profile.bundles`**:
|
|
23
|
+
>
|
|
24
|
+
> `dsh plugin --profile web add dsh-better-sidebar`
|
|
25
|
+
>
|
|
26
|
+
> ⚠️ **陷阱(很常见)**:它若只是本插件的**传递依赖**,只会被装进 `node_modules` 而**不会**被挂成 bundle 层 —— 加载树里没有条目、`betterSidebar` 服务不会被 provide,表现就是本插件「已启用 / 已挂载」却没有任何侧边栏 Tab,**而且全程零报错**(0.1.7 起会在浏览器控制台打出明确指引)。同理请确认 `cordis.patch.yml` 与 `.dsh-market/state.json` 没有把它列为 `disabled`。
|
|
27
|
+
>
|
|
28
|
+
> 另注:Tab 注册后会出现在侧边栏 **「+ 新建 Tab」菜单**里(这是 better-sidebar 的设计:注册的是 tab *类型*,需你点开一次才会出现在标签栏),详见下方「排查」。
|
|
23
29
|
|
|
24
30
|
### 方式一:从市场安装(推荐)
|
|
25
31
|
|
|
26
|
-
> **强烈建议钉版本安装**:`dsh plugin add` 底层是 pnpm,受你本地 `.npmrc` 指向的镜像(如 npmmirror)同步延迟影响,裸 `add dsh-token-gauge` 在 `^0.1.0` 范围内可能解析到尚未带 `dsh.bundle` 的旧版 `0.1.0`(装上去只会是普通依赖、不进 bundle 层、侧边栏不出现)。**显式钉 `0.1.
|
|
32
|
+
> **强烈建议钉版本安装**:`dsh plugin add` 底层是 pnpm,受你本地 `.npmrc` 指向的镜像(如 npmmirror)同步延迟影响,裸 `add dsh-token-gauge` 在 `^0.1.0` 范围内可能解析到尚未带 `dsh.bundle` 的旧版 `0.1.0`(装上去只会是普通依赖、不进 bundle 层、侧边栏不出现)。**显式钉 `0.1.7` 可彻底规避这一坑**:
|
|
27
33
|
```bash
|
|
28
|
-
dsh plugin --profile web add dsh-token-gauge@0.1.
|
|
34
|
+
dsh plugin --profile web add dsh-token-gauge@0.1.7
|
|
29
35
|
dsh web
|
|
30
36
|
```
|
|
31
37
|
> 若想装最新版,可先 `dsh plugin --profile web update dsh-token-gauge` 升级,再确认 `node_modules/dsh-token-gauge/package.json` 的 `dsh.bundle.patch` 字段存在。
|
|
@@ -35,9 +41,9 @@ dsh web
|
|
|
35
41
|
若此前已误装 `0.1.0`(表现为 `dsh.profile.bundles` 未新增 `token-gauge` 层,并出现 `declares no dsh.bundle — installed as a plain dependency` 警告),按下面两步切换即可,无需手动改 bundles:
|
|
36
42
|
```bash
|
|
37
43
|
dsh plugin --profile web remove dsh-token-gauge
|
|
38
|
-
dsh plugin --profile web add dsh-token-gauge@0.1.
|
|
44
|
+
dsh plugin --profile web add dsh-token-gauge@0.1.7 # 钉版本,避免又解析回 0.1.0
|
|
39
45
|
```
|
|
40
|
-
装完后 `dsh.profile.bundles` 在 reconcile 时自动把 `token-gauge`
|
|
46
|
+
装完后 `dsh.profile.bundles` 在 reconcile 时自动把 `token-gauge` 加为最后一层,重启 `dsh web` 后,在侧边栏 **「+ 新建 Tab」菜单**里选「Token 仪表盘」即可打开。
|
|
41
47
|
|
|
42
48
|
### 方式二:本地源码安装
|
|
43
49
|
```bash
|
|
@@ -54,7 +60,7 @@ node build.mjs
|
|
|
54
60
|
dsh web --patch ./cordis.patch.yml
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
安装后**完整重启** DSH Web(进程内 client 缓存只在重启时清空,硬刷新不够),然后在侧边栏点 **「+ 新建 Tab」→「Token 仪表盘」** 打开它。开始对话产生 Token 后,仪表盘即实时更新。
|
|
58
64
|
|
|
59
65
|
### 排查:侧边栏 Tab 不出现
|
|
60
66
|
|
|
@@ -62,6 +68,8 @@ DSH 把插件拆成 host(Node 端)与 client(浏览器端)两半,走
|
|
|
62
68
|
|
|
63
69
|
- **host 半边是否正常**:浏览器访问 `http://<dsh-host>/plugins/dsh-token-gauge/state` 应返回 200 + 状态 JSON,且 `~/.dsh-token-gauge/` 目录已生成。host 不工作通常是 bundle 层没挂上(`dsh.profile.bundles` 缺 `token-gauge`)。
|
|
64
70
|
- **client 半边是否正常**:访问 `/plugins/dsh-token-gauge/client.js` 应返回 200(打包后的客户端脚本);首页 `window.__DSH_BOOT__` 里应包含该 client。若此处 404,常见原因是 `package.json` 的 `exports` 未导出 `./package.json`——DSH 的 client 加载器用 `require.resolve('<包名>/package.json')` 定位 client 包,缺少该子路径导出会被静默判为「非 client 包」且(进程内)缓存。**本包已显式导出 `"./package.json"`,如遇此问题请确认装的是 0.1.2+**。
|
|
71
|
+
- **`dsh-better-sidebar` 是否真的挂载了**(「已启用/已挂载却无 Tab」的头号原因):Tab 挂在它的右侧工作台上。检查 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` **和** `dependencies` 里是否**都**有 `dsh-better-sidebar` —— **只存在于 `node_modules` 不算**:作为传递依赖装进来的包不会被挂成 bundle 层,服务不会被 provide,于是本插件完全静默(0.1.7 起浏览器控制台会打出指引)。同时确认 `cordis.patch.yml` 与 `.dsh-market/state.json` 未把它标为 `disabled`。
|
|
72
|
+
- **类型已注册,但标签栏里看不到**:`registerTab` 注册的是 tab **类型**,不会自动出现在标签栏 —— 需到侧边栏 **「+ 新建 Tab」菜单**点选「Token 仪表盘」打开一次。若菜单里也找不到,去看侧边栏**设置面板的 Tab 列表**:列表里有 = 注册成功只是没打开;列表里也没有 = 服务没拿到(回到上一条)。
|
|
65
73
|
- 改动后务必**完整重启 `dsh web`**(进程内 client 缓存只在重启时清空),仅硬刷新不够。
|
|
66
74
|
|
|
67
75
|
## 数据来源与费用口径
|
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,85 +944,56 @@ var gaugeRef = (el) => {
|
|
|
943
944
|
mountGauge(el);
|
|
944
945
|
}
|
|
945
946
|
};
|
|
946
|
-
function
|
|
947
|
-
return import_react.default.createElement("div", {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
d: "M8 11.6 10.4 7.2",
|
|
967
|
-
stroke: "currentColor",
|
|
968
|
-
strokeWidth: 1.5,
|
|
969
|
-
strokeLinecap: "round"
|
|
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
|
+
}
|
|
970
967
|
});
|
|
971
|
-
|
|
972
|
-
};
|
|
973
|
-
var TAB_ORDER = 90;
|
|
968
|
+
}
|
|
974
969
|
var name = PLUGIN_ID;
|
|
970
|
+
var inject = ["slots"];
|
|
975
971
|
function apply(ctx) {
|
|
976
|
-
let
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
try {
|
|
982
|
-
unregister = sidebar.registerTab({
|
|
972
|
+
let disposeRegistration = void 0;
|
|
973
|
+
const doRegister = () => {
|
|
974
|
+
disposeRegistration = ctx.slots.register(
|
|
975
|
+
{
|
|
976
|
+
name: "shell.overlay",
|
|
983
977
|
id: PLUGIN_ID,
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
});
|
|
990
|
-
} catch (e) {
|
|
991
|
-
registered = false;
|
|
992
|
-
}
|
|
978
|
+
order: 100,
|
|
979
|
+
label: "Token \u4EEA\u8868\u76D8"
|
|
980
|
+
},
|
|
981
|
+
() => import_react.default.createElement(GaugePanel)
|
|
982
|
+
);
|
|
993
983
|
};
|
|
994
|
-
let
|
|
984
|
+
let stop = void 0;
|
|
995
985
|
try {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
const tryRead = () => {
|
|
1002
|
-
if (registered) return;
|
|
1003
|
-
try {
|
|
1004
|
-
const s = typeof ctx.get === "function" ? ctx.get("betterSidebar") : ctx.betterSidebar;
|
|
1005
|
-
register(s);
|
|
1006
|
-
} catch (e) {
|
|
986
|
+
if (typeof ctx.slots.inject === "function") {
|
|
987
|
+
stop = ctx.slots.inject("shell.overlay", doRegister);
|
|
988
|
+
} else {
|
|
989
|
+
doRegister();
|
|
1007
990
|
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
let timer = void 0;
|
|
1011
|
-
if (!registered) {
|
|
1012
|
-
timer = setInterval(() => {
|
|
1013
|
-
tryRead();
|
|
1014
|
-
if (registered && timer !== void 0) clearInterval(timer);
|
|
1015
|
-
}, 500);
|
|
991
|
+
} catch (e) {
|
|
992
|
+
console.error("[dsh-token-gauge] \u6CE8\u518C shell.overlay \u5931\u8D25", e);
|
|
1016
993
|
}
|
|
1017
|
-
const stop = setTimeout(() => {
|
|
1018
|
-
if (timer !== void 0) clearInterval(timer);
|
|
1019
|
-
}, 3e4);
|
|
1020
994
|
return () => {
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1023
|
-
if (typeof unregister === "function") unregister();
|
|
1024
|
-
if (typeof disposeInject === "function") disposeInject();
|
|
995
|
+
if (typeof disposeRegistration === "function") disposeRegistration();
|
|
996
|
+
if (typeof stop === "function") stop();
|
|
1025
997
|
};
|
|
1026
998
|
}
|
|
1027
999
|
; return module.exports; } });
|
package/package.json
CHANGED
|
@@ -1,82 +1,80 @@
|
|
|
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
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"dsh-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-token-gauge",
|
|
3
|
+
"version": "0.1.8",
|
|
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
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@deepseek-ai/cordis": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@deepseek-ai/cordis": "*",
|
|
75
|
+
"@types/react": "^18.2.0",
|
|
76
|
+
"esbuild": "^0.21.5",
|
|
77
|
+
"react": "^18.2.0",
|
|
78
|
+
"typescript": "^5.4.0"
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,319 +1,302 @@
|
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
st.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
titleEl.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
gear.
|
|
111
|
-
gear.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
empty.
|
|
119
|
-
empty
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
root
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
empty.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
void poll()
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
el
|
|
197
|
-
|
|
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
|
-
timer = setInterval(() => {
|
|
304
|
-
tryRead()
|
|
305
|
-
if (registered && timer !== void 0) clearInterval(timer as number)
|
|
306
|
-
}, 500)
|
|
307
|
-
}
|
|
308
|
-
const stop = setTimeout(() => {
|
|
309
|
-
if (timer !== void 0) clearInterval(timer as number)
|
|
310
|
-
}, 30000)
|
|
311
|
-
|
|
312
|
-
// 插件级 disposer
|
|
313
|
-
return () => {
|
|
314
|
-
if (timer !== void 0) clearInterval(timer as number)
|
|
315
|
-
clearTimeout(stop)
|
|
316
|
-
if (typeof unregister === 'function') (unregister as () => void)()
|
|
317
|
-
if (typeof disposeInject === 'function') (disposeInject as () => void)()
|
|
318
|
-
}
|
|
319
|
-
}
|
|
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
|
+
// 通过官方 shell.overlay 插槽独立挂载浮层面板(方案 A),不再依赖 dsh-better-sidebar。
|
|
8
|
+
// 为避免与运行时 React 产生双实例 hook 冲突,本组件不调用任何 hook,
|
|
9
|
+
// 仅在 ref 回调里挂载一次原生 DOM 仪表盘,并自行轮询刷新。
|
|
10
|
+
// ============================================================================
|
|
11
|
+
import React from 'react'
|
|
12
|
+
import { getStyle, applyPalette, buildCss } from './styles'
|
|
13
|
+
import { createGauge } from './gauge'
|
|
14
|
+
import { t } from './i18n'
|
|
15
|
+
import { downloadReport } from './export'
|
|
16
|
+
import { createSettingsModal } from './settings'
|
|
17
|
+
|
|
18
|
+
const PLUGIN_ID = 'dsh-token-gauge'
|
|
19
|
+
const ROUTE_STATE = '/plugins/dsh-token-gauge/state'
|
|
20
|
+
const ROUTE_CMD = '/plugins/dsh-token-gauge/cmd'
|
|
21
|
+
|
|
22
|
+
function totalOf(b: any): number {
|
|
23
|
+
return (b?.input || 0) + (b?.cacheRead || 0) + (b?.cacheWrite || 0) + (b?.output || 0)
|
|
24
|
+
}
|
|
25
|
+
function todayKey(d = new Date()): string {
|
|
26
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
27
|
+
}
|
|
28
|
+
function monthKey(d = new Date()): string {
|
|
29
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
|
30
|
+
}
|
|
31
|
+
function weekKey(d = new Date()): string {
|
|
32
|
+
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
|
|
33
|
+
const dayNum = (date.getUTCDay() + 6) % 7
|
|
34
|
+
date.setUTCDate(date.getUTCDate() - dayNum + 3)
|
|
35
|
+
const firstThursday = new Date(Date.UTC(date.getUTCFullYear(), 0, 4))
|
|
36
|
+
const week =
|
|
37
|
+
1 +
|
|
38
|
+
Math.round(
|
|
39
|
+
((date.getTime() - firstThursday.getTime()) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7,
|
|
40
|
+
)
|
|
41
|
+
return `${d.getFullYear()}-W${String(week).padStart(2, '0')}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fallbackState(): any {
|
|
45
|
+
return {
|
|
46
|
+
global: {},
|
|
47
|
+
daily: {},
|
|
48
|
+
weekly: {},
|
|
49
|
+
monthly: {},
|
|
50
|
+
sessions: {},
|
|
51
|
+
models: {},
|
|
52
|
+
activeSession: '',
|
|
53
|
+
settings: {
|
|
54
|
+
style: 'ev',
|
|
55
|
+
palette: 'neon',
|
|
56
|
+
lang: 'zh',
|
|
57
|
+
metric: 'global',
|
|
58
|
+
display: { usage: true, cost: true, io: true, cacheMiss: true },
|
|
59
|
+
limits: { global: null, daily: null, weekly: null, monthly: null },
|
|
60
|
+
pricing: {},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 计算当前指标的量程/费用
|
|
66
|
+
function computeMetric(s: any): { value: number; limit: number | null; cost: number } {
|
|
67
|
+
const metric = s.settings.metric
|
|
68
|
+
let entry: any = s.global
|
|
69
|
+
let limit: number | null = s.settings.limits.global
|
|
70
|
+
if (metric === 'session') {
|
|
71
|
+
entry = s.sessions[s.activeSession] || {}
|
|
72
|
+
limit = s.settings.limits.daily
|
|
73
|
+
} else if (metric === 'today' || metric === 'daily') {
|
|
74
|
+
entry = s.daily[todayKey()] || {}
|
|
75
|
+
limit = s.settings.limits.daily
|
|
76
|
+
} else if (metric === 'weekly') {
|
|
77
|
+
entry = s.weekly[weekKey()] || {}
|
|
78
|
+
limit = s.settings.limits.weekly
|
|
79
|
+
} else if (metric === 'monthly') {
|
|
80
|
+
entry = s.monthly[monthKey()] || {}
|
|
81
|
+
limit = s.settings.limits.monthly
|
|
82
|
+
}
|
|
83
|
+
return { value: totalOf(entry), limit, cost: entry.cost || 0 }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 把原生 DOM 仪表盘挂载进容器,并启动轮询
|
|
87
|
+
function mountGauge(container: HTMLElement): void {
|
|
88
|
+
// 注入一次全局样式
|
|
89
|
+
if (!document.getElementById('tg-gauge-style')) {
|
|
90
|
+
const st = document.createElement('style')
|
|
91
|
+
st.id = 'tg-gauge-style'
|
|
92
|
+
st.textContent = buildCss()
|
|
93
|
+
document.head.appendChild(st)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let state: any = null
|
|
97
|
+
const tr = (k: string) => t((state?.settings?.lang as 'zh' | 'en') || 'zh', k)
|
|
98
|
+
|
|
99
|
+
const root = container
|
|
100
|
+
root.className = 'tg-root tg-style-ev'
|
|
101
|
+
|
|
102
|
+
// 头部
|
|
103
|
+
const head = document.createElement('div')
|
|
104
|
+
head.className = 'tg-head'
|
|
105
|
+
const titleEl = document.createElement('div')
|
|
106
|
+
titleEl.className = 'tg-title'
|
|
107
|
+
titleEl.textContent = 'Token 仪表盘'
|
|
108
|
+
const gear = document.createElement('div')
|
|
109
|
+
gear.className = 'tg-gear'
|
|
110
|
+
gear.textContent = '⚙'
|
|
111
|
+
gear.title = '设置'
|
|
112
|
+
head.append(titleEl, gear)
|
|
113
|
+
root.appendChild(head)
|
|
114
|
+
|
|
115
|
+
// 空数据提示
|
|
116
|
+
const empty = document.createElement('div')
|
|
117
|
+
empty.className = 'tg-empty'
|
|
118
|
+
empty.style.display = 'none'
|
|
119
|
+
root.appendChild(empty)
|
|
120
|
+
|
|
121
|
+
// 仪表盘
|
|
122
|
+
const gauge = createGauge()
|
|
123
|
+
root.appendChild(gauge.el)
|
|
124
|
+
|
|
125
|
+
// 设置弹窗
|
|
126
|
+
const modal = createSettingsModal({
|
|
127
|
+
getLang: () => (state?.settings?.lang as 'zh' | 'en') || 'zh',
|
|
128
|
+
onSettings: (patch) => sendCmd('setSettings', patch),
|
|
129
|
+
onExport: (format) => {
|
|
130
|
+
if (state) downloadReport(state, (state.settings.lang as 'zh' | 'en') || 'zh', format)
|
|
131
|
+
},
|
|
132
|
+
onClear: () => sendCmd('clearUsage', undefined),
|
|
133
|
+
onReset: () => sendCmd('resetSettings', undefined),
|
|
134
|
+
})
|
|
135
|
+
root.appendChild(modal.el)
|
|
136
|
+
|
|
137
|
+
gear.addEventListener('click', () => modal.open(state || fallbackState()))
|
|
138
|
+
|
|
139
|
+
const applyState = (s: any): void => {
|
|
140
|
+
state = s
|
|
141
|
+
root.className = 'tg-root ' + getStyle(s.settings.style).className
|
|
142
|
+
applyPalette(root, s.settings.palette)
|
|
143
|
+
titleEl.textContent = tr('title')
|
|
144
|
+
const m = computeMetric(s)
|
|
145
|
+
gauge.update({ value: m.value, limit: m.limit, cost: m.cost, state: s, display: s.settings.display, t: tr, lang: s.settings.lang })
|
|
146
|
+
const hasData = totalOf(s.global) > 0
|
|
147
|
+
empty.style.display = hasData ? 'none' : ''
|
|
148
|
+
empty.textContent = tr('noData')
|
|
149
|
+
modal.sync(s)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 拉取最新状态
|
|
153
|
+
async function poll(): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
const r = await fetch(ROUTE_STATE, { headers: { Accept: 'application/json' } })
|
|
156
|
+
if (r.ok) {
|
|
157
|
+
const s = await r.json()
|
|
158
|
+
applyState(s)
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// 网络抖动忽略,下一轮重试
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 发送指令并刷新
|
|
166
|
+
async function sendCmd(type: string, payload: any): Promise<void> {
|
|
167
|
+
try {
|
|
168
|
+
await fetch(ROUTE_CMD, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: { 'Content-Type': 'application/json' },
|
|
171
|
+
body: JSON.stringify({ type, payload }),
|
|
172
|
+
})
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn(`[${PLUGIN_ID}] cmd ${type} failed`, e)
|
|
175
|
+
}
|
|
176
|
+
await poll()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 立即拉一次,并定时轮询
|
|
180
|
+
void poll()
|
|
181
|
+
const timer = window.setInterval(() => void poll(), 2000)
|
|
182
|
+
// 容器被移除时停止轮询
|
|
183
|
+
const mo = new MutationObserver(() => {
|
|
184
|
+
if (!document.body.contains(container)) {
|
|
185
|
+
window.clearInterval(timer)
|
|
186
|
+
mo.disconnect()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
mo.observe(document.body, { childList: true, subtree: true })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 稳定的 ref 回调:只在首次挂载时构建一次仪表盘
|
|
193
|
+
const gaugeRef = (el: HTMLElement | null): void => {
|
|
194
|
+
if (el && !el.dataset.tgMounted) {
|
|
195
|
+
el.dataset.tgMounted = '1'
|
|
196
|
+
mountGauge(el)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 浮层面板组件:固定在右下角、占视口高度 1/3(方案 A:shell.overlay 插槽)。
|
|
201
|
+
// 仍是零 hook、ref 回调挂原生 DOM,规避双 React 实例;外层用内联定位样式,
|
|
202
|
+
// 不被 mountGauge 内的 className 覆盖(style 优先级高于 class)。
|
|
203
|
+
function GaugePanel(): React.ReactElement {
|
|
204
|
+
return React.createElement('div', {
|
|
205
|
+
ref: gaugeRef,
|
|
206
|
+
className: 'tg-panel-root',
|
|
207
|
+
style: {
|
|
208
|
+
position: 'fixed',
|
|
209
|
+
right: '0',
|
|
210
|
+
bottom: '0',
|
|
211
|
+
width: '420px',
|
|
212
|
+
height: '33vh',
|
|
213
|
+
zIndex: 50,
|
|
214
|
+
pointerEvents: 'auto',
|
|
215
|
+
boxSizing: 'border-box',
|
|
216
|
+
overflow: 'hidden',
|
|
217
|
+
borderTop: '1px solid var(--dsw-alias-hairline)',
|
|
218
|
+
borderLeft: '1px solid var(--dsw-alias-hairline)',
|
|
219
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
220
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
221
|
+
boxShadow: '0 0 24px rgba(0,0,0,0.28)',
|
|
222
|
+
} as any,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 侧边栏图标:better-sidebar 以 `icon(size)` 形式调用(见其内置 tab:
|
|
227
|
+
// `icon: (size) => jsx(Icon, { size })`),故这里接收数字尺寸。用 currentColor
|
|
228
|
+
// 着色以自适应明暗主题,纯 SVG 零依赖。
|
|
229
|
+
const GaugeIcon = (size?: number): any => {
|
|
230
|
+
const s = typeof size === 'number' && size > 0 ? size : 16
|
|
231
|
+
const svgProps: any = {
|
|
232
|
+
width: s,
|
|
233
|
+
height: s,
|
|
234
|
+
viewBox: '0 0 16 16',
|
|
235
|
+
fill: 'none',
|
|
236
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
237
|
+
'aria-hidden': 'true',
|
|
238
|
+
}
|
|
239
|
+
// 仪表盘弧 + 指针
|
|
240
|
+
const arc = React.createElement('path', {
|
|
241
|
+
d: 'M2.6 11.6a5.6 5.6 0 1 1 10.8 0',
|
|
242
|
+
stroke: 'currentColor',
|
|
243
|
+
strokeWidth: 1.5,
|
|
244
|
+
strokeLinecap: 'round',
|
|
245
|
+
})
|
|
246
|
+
const needle = React.createElement('path', {
|
|
247
|
+
d: 'M8 11.6 10.4 7.2',
|
|
248
|
+
stroke: 'currentColor',
|
|
249
|
+
strokeWidth: 1.5,
|
|
250
|
+
strokeLinecap: 'round',
|
|
251
|
+
})
|
|
252
|
+
return React.createElement('svg', svgProps, arc, needle)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 在「+ 新建 Tab」菜单里的排序位。内置 tab 用 20/91/110 等,order 缺省为 100
|
|
256
|
+
// 会排到菜单最底部不易发现,这里显式给一个偏前的值。
|
|
257
|
+
const TAB_ORDER = 90
|
|
258
|
+
|
|
259
|
+
export const name = PLUGIN_ID
|
|
260
|
+
|
|
261
|
+
// 方案 A:通过官方 shell.overlay 插槽(list 型、additive、全框架级浮层)独立挂载仪表盘,
|
|
262
|
+
// 不再依赖第三方插件 dsh-better-sidebar。slots 是 layout 提供的框架级服务(root 早期即
|
|
263
|
+
// 就绪),故插件级 `inject: ['slots']` 不会触发此前 betterSidebar 的 pending / without-inject
|
|
264
|
+
// 问题——这正是脱离 better-sidebar 的核心收益。
|
|
265
|
+
export const inject = ['slots']
|
|
266
|
+
|
|
267
|
+
export function apply(ctx: any): (() => void) | void {
|
|
268
|
+
let disposeRegistration: unknown = void 0
|
|
269
|
+
|
|
270
|
+
const doRegister = (): void => {
|
|
271
|
+
// shell.overlay 是 click-through 层,entry 需自行 opt-in pointer events(已在
|
|
272
|
+
// GaugePanel 外层设 pointerEvents:'auto')。第二参是返回 React.Element 的工厂函数
|
|
273
|
+
//(照抄 dsh-cordis-client-runner 官方示例)。
|
|
274
|
+
disposeRegistration = ctx.slots.register(
|
|
275
|
+
{
|
|
276
|
+
name: 'shell.overlay',
|
|
277
|
+
id: PLUGIN_ID,
|
|
278
|
+
order: 100,
|
|
279
|
+
label: 'Token 仪表盘',
|
|
280
|
+
},
|
|
281
|
+
() => React.createElement(GaugePanel),
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let stop: unknown = void 0
|
|
286
|
+
try {
|
|
287
|
+
// slots.inject(seat, cb):等 shell.overlay 这个 seat 就绪再注册(root 已声明,
|
|
288
|
+
// 通常同步触发)。某些环境下若无 inject 方法,则直接 register 兜底。
|
|
289
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
290
|
+
stop = ctx.slots.inject('shell.overlay', doRegister)
|
|
291
|
+
} else {
|
|
292
|
+
doRegister()
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.error('[dsh-token-gauge] 注册 shell.overlay 失败', e)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
if (typeof disposeRegistration === 'function') (disposeRegistration as () => void)()
|
|
300
|
+
if (typeof stop === 'function') (stop as () => void)()
|
|
301
|
+
}
|
|
302
|
+
}
|