dsh-plugin-deepseek-usage 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +11 -0
- package/lib/client.js +146 -0
- package/lib/index.js +105 -0
- package/package.json +31 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# deepseek-usage — bundle patch layer.
|
|
2
|
+
#
|
|
3
|
+
# This file turns the package into a DSH *profile bundle* (see
|
|
4
|
+
# `package.json` → `dsh.bundle.patch`): the desktop app appends the package to
|
|
5
|
+
# the `web` profile's `dsh.profile.bundles` list on first launch, and this
|
|
6
|
+
# layer inserts the plugin row into the composed tree — the host half mounts
|
|
7
|
+
# the `/deepseek-usage` route, and the client-modules node half scans the entry
|
|
8
|
+
# and serves the sidebar balance button bundle.
|
|
9
|
+
- insert:
|
|
10
|
+
- id: deepseek-usage
|
|
11
|
+
name: 'dsh-plugin-deepseek-usage'
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-plugin-deepseek-usage",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
|
|
8
|
+
var React = require("react");
|
|
9
|
+
var createElement = React.createElement;
|
|
10
|
+
|
|
11
|
+
var css = [
|
|
12
|
+
".dshu-trigger{box-sizing:border-box;cursor:pointer;width:100%;height:34px;color:var(--dsw-alias-label-primary,#111);background:transparent;border:none;border-radius:12px;flex:none;display:flex;align-items:center;gap:8px;padding:6px 10px;font-family:inherit;font-size:14px}",
|
|
13
|
+
".dshu-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f0f0f0)}",
|
|
14
|
+
".dshu-trigger.dshu-rail{justify-content:center;gap:0;width:36px;height:36px;padding:0}",
|
|
15
|
+
".dshu-trigger.dshu-rail .dshu-status-dot{position:absolute;right:5px;bottom:5px}",
|
|
16
|
+
".dshu-trigger-label{white-space:nowrap;overflow:hidden}",
|
|
17
|
+
".dshu-amount{margin-left:auto;flex:none;background:rgba(46,160,90,.16);color:#2ea05a;border-radius:999px;padding:1px 8px;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
18
|
+
".dshu-amount-bad{background:rgba(211,60,60,.16);color:var(--dsw-alias-state-error-primary,#d33)}",
|
|
19
|
+
".dshu-status-dot{width:8px;height:8px;border-radius:50%;flex:none;display:inline-block}",
|
|
20
|
+
".dshu-status-dot.dshu-dot-ok{background:#2ea05a}",
|
|
21
|
+
".dshu-status-dot.dshu-dot-bad{background:var(--dsw-alias-state-error-primary,#d33)}",
|
|
22
|
+
".dshu-status-dot.dshu-dot-wait{background:var(--dsw-alias-label-tertiary,#999)}"
|
|
23
|
+
].join("\n");
|
|
24
|
+
|
|
25
|
+
var tagId = "dsh-plugin-deepseek-usage/usage.css";
|
|
26
|
+
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
|
|
27
|
+
var tag = document.createElement("style");
|
|
28
|
+
tag.dataset.plugin = "dsh-plugin-deepseek-usage";
|
|
29
|
+
tag.dataset.pluginCss = tagId;
|
|
30
|
+
tag.textContent = css;
|
|
31
|
+
document.head.appendChild(tag);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmt(value) {
|
|
35
|
+
if (value === undefined || value === null || value === "") return "—";
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 返回当前「用量」所属人:9:00–12:00、14:00–18:00 为梁文锋,其余时间为梁文谷。
|
|
41
|
+
* 边界按 [开始, 结束) 处理,即 12:00 / 18:00 起切换为梁文谷。
|
|
42
|
+
*/
|
|
43
|
+
function getActiveUser(now) {
|
|
44
|
+
var hour = new Date(now).getHours();
|
|
45
|
+
var wenfeng = (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
46
|
+
return wenfeng ? "梁文锋" : "梁文谷";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Sidebar footer button: shows the DeepSeek account balance directly. */
|
|
50
|
+
function BalanceButton(props) {
|
|
51
|
+
var wide = props.wide;
|
|
52
|
+
var stateRef = React.useState({ phase: "loading", total: null, currency: null, available: false, error: null });
|
|
53
|
+
var state = stateRef[0];
|
|
54
|
+
var setState = stateRef[1];
|
|
55
|
+
|
|
56
|
+
var nowRef = React.useState(Date.now());
|
|
57
|
+
var now = nowRef[0];
|
|
58
|
+
var setNow = nowRef[1];
|
|
59
|
+
|
|
60
|
+
var load = React.useCallback(function () {
|
|
61
|
+
fetch("/deepseek-usage", { method: "GET", cache: "no-store" })
|
|
62
|
+
.then(function (r) { return r.json(); })
|
|
63
|
+
.then(function (j) {
|
|
64
|
+
if (j && j.ok) {
|
|
65
|
+
var data = j.data || {};
|
|
66
|
+
var infos = Array.isArray(data.balance_infos) ? data.balance_infos : [];
|
|
67
|
+
var first = infos.length > 0 ? infos[0] : null;
|
|
68
|
+
setState({
|
|
69
|
+
phase: "ready",
|
|
70
|
+
total: first && first.total_balance !== undefined ? String(first.total_balance) : null,
|
|
71
|
+
currency: first && first.currency ? String(first.currency) : null,
|
|
72
|
+
available: data.is_available !== false,
|
|
73
|
+
error: null
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
setState({ phase: "error", total: null, currency: null, available: false, error: (j && (j.message || j.error)) || "未知错误" });
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.catch(function (e) {
|
|
80
|
+
setState({ phase: "error", total: null, currency: null, available: false, error: (e && e.message) || String(e) });
|
|
81
|
+
});
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
React.useEffect(function () {
|
|
85
|
+
load();
|
|
86
|
+
var timer = window.setInterval(load, 60000);
|
|
87
|
+
var nameTimer = window.setInterval(function () { setNow(Date.now()); }, 30000);
|
|
88
|
+
return function () {
|
|
89
|
+
window.clearInterval(timer);
|
|
90
|
+
window.clearInterval(nameTimer);
|
|
91
|
+
};
|
|
92
|
+
}, [load]);
|
|
93
|
+
|
|
94
|
+
var phase = state.phase;
|
|
95
|
+
var amount = state.total;
|
|
96
|
+
var userName = getActiveUser(now);
|
|
97
|
+
var titleText = "DeepSeek 余额(" + userName + ")";
|
|
98
|
+
if (phase === "error") titleText = "余额查询失败(" + userName + "):" + state.error;
|
|
99
|
+
else if (phase === "ready" && amount !== null) titleText = "DeepSeek 余额(" + userName + "):" + amount + (state.currency ? " " + state.currency : "") + (state.available ? "(可用)" : "(不可用)");
|
|
100
|
+
else if (phase === "ready") titleText = "DeepSeek 余额(" + userName + "):暂无数据";
|
|
101
|
+
else titleText = "DeepSeek 余额(" + userName + "):加载中…";
|
|
102
|
+
|
|
103
|
+
var dotClass = phase === "error" || (phase === "ready" && !state.available) ? "dshu-dot-bad"
|
|
104
|
+
: phase === "ready" && amount !== null ? "dshu-dot-ok"
|
|
105
|
+
: "dshu-dot-wait";
|
|
106
|
+
|
|
107
|
+
return createElement(
|
|
108
|
+
"button",
|
|
109
|
+
{
|
|
110
|
+
type: "button",
|
|
111
|
+
className: "dshu-trigger" + (wide ? "" : " dshu-rail"),
|
|
112
|
+
"aria-label": titleText,
|
|
113
|
+
title: titleText,
|
|
114
|
+
onClick: load
|
|
115
|
+
},
|
|
116
|
+
createElement(
|
|
117
|
+
"svg",
|
|
118
|
+
{ viewBox: "0 0 16 16", width: wide ? 14 : 18, height: wide ? 14 : 18, "aria-hidden": true },
|
|
119
|
+
createElement("path", { d: "M2 13h12M3 9h4l2-4 2 2h4", stroke: "currentColor", strokeWidth: "1.5", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
120
|
+
),
|
|
121
|
+
wide ? createElement("span", { className: "dshu-trigger-label" }, "用量 · " + userName) : null,
|
|
122
|
+
!wide ? createElement("span", { className: "dshu-status-dot " + dotClass }) : null,
|
|
123
|
+
wide && phase === "ready" && amount !== null
|
|
124
|
+
? createElement("span", { className: "dshu-amount" + (!state.available ? " dshu-amount-bad" : "") }, amount + (state.currency ? " " + state.currency : ""))
|
|
125
|
+
: null
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Services required by this client plugin. */
|
|
130
|
+
var inject = ["slots"];
|
|
131
|
+
|
|
132
|
+
function apply(ctx) {
|
|
133
|
+
ctx.slots.inject("sidebar.footer.action", function () {
|
|
134
|
+
return ctx.slots.register({
|
|
135
|
+
name: "sidebar.footer.action",
|
|
136
|
+
id: "deepseek-usage",
|
|
137
|
+
order: 10
|
|
138
|
+
}, BalanceButton);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.apply = apply;
|
|
143
|
+
exports.inject = inject;
|
|
144
|
+
return module.exports;
|
|
145
|
+
}
|
|
146
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* dsh-plugin-deepseek-usage — host half.
|
|
6
|
+
*
|
|
7
|
+
* Registers one exact HTTP route (`/deepseek-usage`) on the Web server that:
|
|
8
|
+
* 1. resolves the DeepSeek API key through `ctx.credentials` (per request,
|
|
9
|
+
* so a rotated key reaches the very next query without a restart),
|
|
10
|
+
* 2. calls the official balance endpoint `GET <baseURL>/user/balance`,
|
|
11
|
+
* 3. returns a small JSON envelope the browser half renders.
|
|
12
|
+
*
|
|
13
|
+
* The key never leaves the host and never crosses the wire: the route answers
|
|
14
|
+
* with balance figures only.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Stable Cordis plugin name. */
|
|
18
|
+
const name = "deepseek-usage";
|
|
19
|
+
|
|
20
|
+
/** Services required before the route can be registered. */
|
|
21
|
+
const inject = ["webServer", "credentials"];
|
|
22
|
+
|
|
23
|
+
/** Plugin config (schema defaults applied by the loader). */
|
|
24
|
+
const Config = z.object({
|
|
25
|
+
/** Credential reference (an env-var-shaped name) holding the DeepSeek key. */
|
|
26
|
+
apiKeyEnv: z.string().default("DEEPSEEK_API_KEY"),
|
|
27
|
+
/** DeepSeek API base; the balance endpoint is `<baseURL>/user/balance`. */
|
|
28
|
+
baseURL: z.string().default("https://api.deepseek.com")
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function writeJson(res, status, payload) {
|
|
32
|
+
const body = JSON.stringify(payload);
|
|
33
|
+
res.writeHead(status, {
|
|
34
|
+
"content-type": "application/json; charset=utf-8",
|
|
35
|
+
"cache-control": "no-store"
|
|
36
|
+
});
|
|
37
|
+
res.end(body);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function apply(ctx, config) {
|
|
41
|
+
const apiKeyEnv = config.apiKeyEnv;
|
|
42
|
+
const baseURL = config.baseURL.replace(/\/+$/, "");
|
|
43
|
+
|
|
44
|
+
ctx.effect(() => ctx.webServer.register({
|
|
45
|
+
kind: "exact",
|
|
46
|
+
path: "/deepseek-usage",
|
|
47
|
+
handler: async (req, res) => {
|
|
48
|
+
if (req.method !== "GET") {
|
|
49
|
+
writeJson(res, 405, { ok: false, error: "METHOD_NOT_ALLOWED" });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
|
|
54
|
+
if (hit === undefined || hit.value === "") {
|
|
55
|
+
writeJson(res, 200, {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: "MISSING_CREDENTIAL",
|
|
58
|
+
message: `API key not configured (${apiKeyEnv})`
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const upstream = await fetch(`${baseURL}/user/balance`, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${hit.value}`,
|
|
67
|
+
Accept: "application/json"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const text = await upstream.text();
|
|
72
|
+
let parsed = null;
|
|
73
|
+
try {
|
|
74
|
+
parsed = text ? JSON.parse(text) : null;
|
|
75
|
+
} catch {
|
|
76
|
+
parsed = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!upstream.ok) {
|
|
80
|
+
const upstreamMessage =
|
|
81
|
+
(parsed && (parsed.error?.message || parsed.message)) ||
|
|
82
|
+
text ||
|
|
83
|
+
`HTTP ${upstream.status}`;
|
|
84
|
+
writeJson(res, 200, {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: "UPSTREAM_ERROR",
|
|
87
|
+
status: upstream.status,
|
|
88
|
+
message: upstreamMessage
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
writeJson(res, 200, { ok: true, status: upstream.status, data: parsed });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
writeJson(res, 200, {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: "TRANSPORT",
|
|
98
|
+
message: err instanceof Error ? err.message : String(err)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}), "deepseek-usage: http route");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { Config, apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-deepseek-usage",
|
|
3
|
+
"description": "DeepSeek Harness plugin: show the current DeepSeek account balance right on the sidebar button, queried through the resolved API key",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"client": {
|
|
14
|
+
"inject": [
|
|
15
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
16
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
17
|
+
],
|
|
18
|
+
"platform": "web"
|
|
19
|
+
},
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
27
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
28
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
29
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
30
|
+
}
|
|
31
|
+
}
|