dsh-deepseek-account 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/CHANGELOG.md +9 -0
- package/CONTRIBUTING.md +5 -0
- package/LICENSE +21 -0
- package/README.en.md +32 -0
- package/README.md +34 -0
- package/SECURITY.md +5 -0
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/deepseek-account.patch.yml +3 -0
- package/dist/client/client.js +188 -0
- package/dist/host/index.mjs +10 -0
- package/dist/internal/account-balance.mjs +107 -0
- package/dist/internal/account-rpc.mjs +24 -0
- package/docs/releases/v0.1.0.md +50 -0
- package/package.json +78 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-08-30
|
|
4
|
+
|
|
5
|
+
- Create an independent DeepSeek Harness account plugin with a loopback-only balance RPC and a credential-isolated Host boundary.
|
|
6
|
+
- Display CNY/USD total, granted, and topped-up balances in the sidebar and a dedicated settings page.
|
|
7
|
+
- Open the fixed official DeepSeek Platform balance page for web sign-in and top-up without receiving cookies, payment data, or payment authority.
|
|
8
|
+
- Render the DeepSeek card for DeepSeek and unknown custom Providers, hide it for Grok and Codex, and avoid balance requests while another Provider is selected.
|
|
9
|
+
- Bound balance response size and schema, preserve decimal strings, coalesce concurrent reads, rate-limit refreshes, and retain a marked stale snapshot after transient failure.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Please open an issue before a substantial change. Do not include API keys, cookies, OAuth codes, payment details, account screenshots, or private balance responses in issues, tests, commits, or logs.
|
|
4
|
+
|
|
5
|
+
Development branches must use the `yukiryou/` prefix. Run `npm run check` before opening a pull request and keep Host credential handling separate from browser code.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YukiRyou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dsh-deepseek-account
|
|
2
|
+
|
|
3
|
+
An independent DeepSeek Harness account plugin that:
|
|
4
|
+
|
|
5
|
+
- shows the balance associated with the configured DeepSeek API key when the current conversation selects DeepSeek or an unknown custom Provider;
|
|
6
|
+
- presents CNY/USD total, granted, and topped-up balances in a dedicated settings page;
|
|
7
|
+
- opens the official DeepSeek Platform for web sign-in and top-up.
|
|
8
|
+
|
|
9
|
+
The sidebar component returns no content for Grok or Codex, allowing those Provider plugins to display their own usage limits. This plugin never receives web cookies or payment details and never submits a payment.
|
|
10
|
+
|
|
11
|
+
## Security boundary
|
|
12
|
+
|
|
13
|
+
`DEEPSEEK_API_KEY` is resolved only by the Host through the Harness credential service. The browser can request only a validated balance snapshot through a loopback-only RPC. The build fails if the client artifact contains credential resolution or an Authorization header.
|
|
14
|
+
|
|
15
|
+
The top-up destination is fixed to `https://platform.deepseek.com/balance`; sign-in and payment happen entirely on the official DeepSeek website.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
After the formal release, install the exact version:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
dsh plugin --profile web add dsh-deepseek-account@0.1.0
|
|
23
|
+
dsh web
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm run check
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The release branch is `yukiryou/v0.1.0`. `dist/` is generated during build and packaging and is not committed.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# dsh-deepseek-account
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 的独立账户插件,负责:
|
|
4
|
+
|
|
5
|
+
- 在当前对话选择 DeepSeek 或无法识别的自定义 provider 时显示 API Key 所属账户余额;
|
|
6
|
+
- 在设置页展示 CNY/USD 总余额、赠送余额和充值余额;
|
|
7
|
+
- 打开 DeepSeek 官方平台完成网页登录与充值。
|
|
8
|
+
|
|
9
|
+
选择 Grok 或 Codex 时,本插件的侧栏卡片返回空内容,由对应 provider 插件展示自己的使用额度。插件不接收网页 Cookie 或支付资料,也不会代替用户提交付款。
|
|
10
|
+
|
|
11
|
+
## 安全边界
|
|
12
|
+
|
|
13
|
+
`DEEPSEEK_API_KEY` 只在 Host 侧通过 Harness credential service 解析。浏览器侧只能调用 loopback RPC 获取经过校验的余额快照,构建脚本会拒绝包含凭据解析或 Authorization header 的客户端产物。
|
|
14
|
+
|
|
15
|
+
充值入口固定为 `https://platform.deepseek.com/balance`,登录和付款完全发生在 DeepSeek 官方网页。
|
|
16
|
+
|
|
17
|
+
## 开发
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run check
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
仓库开发分支为 `yukiryou/v0.1.0`。`dist/` 由构建和打包流程生成,不提交到 Git。
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
正式发布后安装精确版本:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile web add dsh-deepseek-account@0.1.0
|
|
31
|
+
dsh web
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
英文说明见 [README.en.md](README.en.md)。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Report vulnerabilities privately through GitHub Security Advisories for this repository. Do not open a public issue containing credentials, cookies, account data, payment details, or an exploitable proof of concept.
|
|
4
|
+
|
|
5
|
+
Supported releases receive security fixes on the latest stable version. The plugin intentionally has no payment authority: sign-in and payment remain on the fixed official DeepSeek website.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* global document, window */
|
|
2
|
+
|
|
3
|
+
window.__ModuleLoader__.load({
|
|
4
|
+
id: "dsh-deepseek-account",
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
const module = { exports: {} }
|
|
7
|
+
const React = require("react")
|
|
8
|
+
const namespace = "settings.deepseekAccount"
|
|
9
|
+
const channel = "/dsh-deepseek-account"
|
|
10
|
+
const topUpUrl = "https://platform.deepseek.com/balance"
|
|
11
|
+
const dictionaries = {
|
|
12
|
+
zh: {
|
|
13
|
+
nav: "DeepSeek 账户", title: "DeepSeek 账户", description: "查看当前 API Key 所属账户余额,并前往 DeepSeek 官方平台充值。",
|
|
14
|
+
balance: "账户余额", total: "总余额", granted: "赠送余额", toppedUp: "充值余额", refresh: "刷新", refreshing: "刷新中…",
|
|
15
|
+
topUp: "登录并充值", officialNote: "登录和付款仅在 DeepSeek 官方网页完成;插件不读取网页 Cookie,也不会代为提交付款。",
|
|
16
|
+
updated: "数据更新时间", loading: "正在读取余额…", unavailable: "暂时无法读取余额。", unconfigured: "请先在模型设置中配置 DeepSeek API Key。",
|
|
17
|
+
unauthorized: "当前 API Key 无效或已失效。", rateLimited: "查询过于频繁,请稍后重试。", stale: "当前展示上一次成功读取的余额。",
|
|
18
|
+
},
|
|
19
|
+
en: {
|
|
20
|
+
nav: "DeepSeek account", title: "DeepSeek account", description: "View the balance of the account associated with the current API key and open the official DeepSeek Platform to top up.",
|
|
21
|
+
balance: "Account balance", total: "Total", granted: "Granted", toppedUp: "Topped up", refresh: "Refresh", refreshing: "Refreshing…",
|
|
22
|
+
topUp: "Sign in and top up", officialNote: "Sign-in and payment happen only on the official DeepSeek website. The plugin never reads web cookies or submits payment.",
|
|
23
|
+
updated: "Updated", loading: "Reading balance…", unavailable: "The balance is temporarily unavailable.", unconfigured: "Configure a DeepSeek API key in model settings first.",
|
|
24
|
+
unauthorized: "The current API key was rejected.", rateLimited: "Too many balance requests. Try again later.", stale: "Showing the last successfully verified balance.",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function accountClient(connection) {
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
async read(force, signal) {
|
|
31
|
+
const result = await connection.rpc.call(channel, "read", { force: force === true }, signal)
|
|
32
|
+
if (!plainRecord(result) || result.ok !== true) throw new Error("DeepSeek account RPC failed")
|
|
33
|
+
const snapshot = safeSnapshot(result.value)
|
|
34
|
+
if (snapshot === undefined) throw new Error("DeepSeek account RPC returned invalid data")
|
|
35
|
+
return snapshot
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function safeSnapshot(value) {
|
|
41
|
+
if (!plainRecord(value)) return undefined
|
|
42
|
+
if (value.status === "ready") return safeReady(value)
|
|
43
|
+
if (value.status !== "unavailable" || !["credential-unconfigured", "credential-unauthorized", "rate-limited", "network", "invalid-response"].includes(value.reason)) return undefined
|
|
44
|
+
const lastGood = value.lastGood === undefined ? undefined : safeReady(value.lastGood)
|
|
45
|
+
if (value.lastGood !== undefined && lastGood === undefined) return undefined
|
|
46
|
+
return lastGood === undefined ? { status: "unavailable", reason: value.reason } : { status: "unavailable", reason: value.reason, lastGood }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function safeReady(value) {
|
|
50
|
+
if (!plainRecord(value) || value.status !== "ready" || typeof value.isAvailable !== "boolean" || typeof value.stale !== "boolean" || !validTime(value.fetchedAt) || !Array.isArray(value.balances) || value.balances.length > 2) return undefined
|
|
51
|
+
const balances = []
|
|
52
|
+
const currencies = new Set()
|
|
53
|
+
for (const item of value.balances) {
|
|
54
|
+
if (!plainRecord(item) || (item.currency !== "CNY" && item.currency !== "USD") || currencies.has(item.currency)) return undefined
|
|
55
|
+
if (![item.total, item.granted, item.toppedUp].every(validDecimal)) return undefined
|
|
56
|
+
currencies.add(item.currency)
|
|
57
|
+
balances.push({ currency: item.currency, total: item.total, granted: item.granted, toppedUp: item.toppedUp })
|
|
58
|
+
}
|
|
59
|
+
return { status: "ready", isAvailable: value.isAvailable, balances, fetchedAt: value.fetchedAt, stale: value.stale }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function useAccount(client, enabled = true) {
|
|
63
|
+
const [state, setState] = React.useState({ status: "loading" })
|
|
64
|
+
const [busy, setBusy] = React.useState(false)
|
|
65
|
+
const refresh = React.useCallback(async (force = false) => {
|
|
66
|
+
setBusy(true)
|
|
67
|
+
try { setState(await client.read(force)) } catch { setState((previous) => previous.status === "ready" ? { status: "unavailable", reason: "network", lastGood: { ...previous, stale: true } } : { status: "unavailable", reason: "network" }) }
|
|
68
|
+
finally { setBusy(false) }
|
|
69
|
+
}, [client])
|
|
70
|
+
React.useEffect(() => { if (enabled) void refresh(false) }, [enabled, refresh])
|
|
71
|
+
return { state, busy, refresh }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function useActiveAccountProvider(ctx) {
|
|
75
|
+
const [provider, setProvider] = React.useState("deepseek")
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
let stopDirectory = () => {}
|
|
78
|
+
let active = true
|
|
79
|
+
const publish = (directory) => {
|
|
80
|
+
const selected = directory?.store.getSnapshot().current?.provider
|
|
81
|
+
if (selected === undefined || !active) return
|
|
82
|
+
setProvider(accountProvider(selected))
|
|
83
|
+
}
|
|
84
|
+
const bind = () => {
|
|
85
|
+
stopDirectory()
|
|
86
|
+
stopDirectory = () => {}
|
|
87
|
+
const sessionId = ctx.sessions.list.getSnapshot().current
|
|
88
|
+
if (sessionId === undefined) {
|
|
89
|
+
setProvider("deepseek")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
let directory
|
|
93
|
+
try { directory = ctx.modelDirectories.directoryFor(sessionId) } catch { return }
|
|
94
|
+
publish(directory)
|
|
95
|
+
stopDirectory = directory.store.subscribe(() => publish(directory))
|
|
96
|
+
if (directory.store.getSnapshot().current === null) void directory.load().catch(() => {})
|
|
97
|
+
}
|
|
98
|
+
const stopSessions = ctx.sessions.list.subscribe(bind)
|
|
99
|
+
bind()
|
|
100
|
+
return () => { active = false; stopSessions(); stopDirectory() }
|
|
101
|
+
}, [ctx])
|
|
102
|
+
return provider
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function accountProvider(selected) {
|
|
106
|
+
return selected === "grok" ? "grok" : selected === "dsh-codex" ? "codex" : "deepseek"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function shownSnapshot(state) { return state.status === "ready" ? state : state.lastGood }
|
|
110
|
+
function mainBalance(snapshot) { return snapshot?.balances.find((item) => item.currency === "CNY") ?? snapshot?.balances[0] }
|
|
111
|
+
function money(balance) { return balance === undefined ? "—" : `${balance.currency === "CNY" ? "¥" : "$"}${balance.total}` }
|
|
112
|
+
|
|
113
|
+
function SidebarBalance({ wide, ctx, client, t }) {
|
|
114
|
+
const provider = useActiveAccountProvider(ctx)
|
|
115
|
+
const { state, busy, refresh } = useAccount(client, provider === "deepseek")
|
|
116
|
+
if (provider !== "deepseek") return null
|
|
117
|
+
const summary = money(mainBalance(shownSnapshot(state)))
|
|
118
|
+
return React.createElement("button", {
|
|
119
|
+
type: "button", className: wide ? "dsh-deepseek-account-card" : "dsh-deepseek-account-rail",
|
|
120
|
+
disabled: busy, onClick: () => void refresh(true), "aria-label": `${t("balance")} ${summary}`,
|
|
121
|
+
}, wide
|
|
122
|
+
? React.createElement(React.Fragment, null,
|
|
123
|
+
React.createElement("span", { className: "dsh-deepseek-account-symbol", "aria-hidden": "true" }, "¥"),
|
|
124
|
+
React.createElement("span", { className: "dsh-deepseek-account-copy" }, React.createElement("span", null, t("balance")), React.createElement("small", null, summary)),
|
|
125
|
+
React.createElement("span", { "aria-hidden": "true" }, busy ? "…" : "↻"))
|
|
126
|
+
: React.createElement("span", null, summary))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function AccountSettings({ client, t }) {
|
|
130
|
+
const { state, busy, refresh } = useAccount(client)
|
|
131
|
+
const shown = shownSnapshot(state)
|
|
132
|
+
const reason = state.status === "unavailable" ? ({
|
|
133
|
+
"credential-unconfigured": "unconfigured", "credential-unauthorized": "unauthorized", "rate-limited": "rateLimited",
|
|
134
|
+
}[state.reason] ?? "unavailable") : undefined
|
|
135
|
+
return React.createElement("section", { className: "dsh-deepseek-account-page" },
|
|
136
|
+
React.createElement("h2", null, t("title")), React.createElement("p", { className: "dsh-deepseek-account-description" }, t("description")),
|
|
137
|
+
React.createElement("article", { className: "dsh-deepseek-account-panel" },
|
|
138
|
+
React.createElement("div", { className: "dsh-deepseek-account-head" }, React.createElement("h3", null, t("balance")),
|
|
139
|
+
React.createElement("button", { type: "button", disabled: busy, onClick: () => void refresh(true) }, busy ? t("refreshing") : t("refresh"))),
|
|
140
|
+
shown === undefined ? React.createElement("p", null, state.status === "loading" ? t("loading") : t(reason))
|
|
141
|
+
: React.createElement("div", { className: "dsh-deepseek-account-balances" }, shown.balances.map((balance) =>
|
|
142
|
+
React.createElement("div", { className: "dsh-deepseek-account-balance", key: balance.currency },
|
|
143
|
+
React.createElement("strong", null, `${balance.currency === "CNY" ? "¥" : "$"}${balance.total}`),
|
|
144
|
+
React.createElement("span", null, `${t("granted")} ${balance.granted}`),
|
|
145
|
+
React.createElement("span", null, `${t("toppedUp")} ${balance.toppedUp}`)))),
|
|
146
|
+
state.status === "unavailable" && shown !== undefined && React.createElement("p", { className: "dsh-deepseek-account-warning" }, t("stale")),
|
|
147
|
+
shown?.fetchedAt && React.createElement("small", null, `${t("updated")}:${new Date(shown.fetchedAt).toLocaleString()}`)),
|
|
148
|
+
React.createElement("article", { className: "dsh-deepseek-account-panel" },
|
|
149
|
+
React.createElement("a", { className: "dsh-deepseek-account-topup", href: topUpUrl, target: "_blank", rel: "noreferrer" }, t("topUp")),
|
|
150
|
+
React.createElement("p", null, t("officialNote"))))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function installStyle() {
|
|
154
|
+
if (document.querySelector('style[data-dsh-deepseek-account]')) return () => {}
|
|
155
|
+
const style = document.createElement("style")
|
|
156
|
+
style.dataset.dshDeepseekAccount = ""
|
|
157
|
+
style.textContent = ".dsh-deepseek-account-card{display:grid;width:100%;min-height:48px;margin:3px 0;padding:4px 3px;border:0;border-radius:8px;grid-template-columns:22px minmax(0,1fr) 16px;align-items:center;gap:10px;color:inherit;background:transparent;cursor:pointer;font:inherit;text-align:left}.dsh-deepseek-account-card:hover,.dsh-deepseek-account-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsh-deepseek-account-symbol{font-weight:650;text-align:center}.dsh-deepseek-account-copy{display:flex;min-width:0;flex-direction:column}.dsh-deepseek-account-copy small{overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsh-deepseek-account-rail{display:grid;width:36px;height:36px;margin:4px auto;border:0;border-radius:8px;place-items:center;color:inherit;background:transparent;cursor:pointer;font-size:10px;font-weight:650}.dsh-deepseek-account-page{box-sizing:border-box;width:min(860px,100%);padding:8px 4px 40px;color:var(--dsw-alias-label-primary)}.dsh-deepseek-account-page h2{margin:0}.dsh-deepseek-account-description{color:var(--dsw-alias-label-secondary)}.dsh-deepseek-account-panel{margin-top:14px;padding:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-1)}.dsh-deepseek-account-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsh-deepseek-account-head h3{margin:0}.dsh-deepseek-account-head button,.dsh-deepseek-account-topup{min-height:34px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 13px;color:inherit;background:transparent;font:inherit}.dsh-deepseek-account-balances{display:grid;margin-top:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.dsh-deepseek-account-balance{display:flex;flex-direction:column;gap:5px;padding:14px;border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-interactive-bg-hover))}.dsh-deepseek-account-balance strong{font-size:22px}.dsh-deepseek-account-balance span,.dsh-deepseek-account-panel p,.dsh-deepseek-account-panel small{color:var(--dsw-alias-label-secondary);font-size:12px}.dsh-deepseek-account-warning{color:var(--dsw-alias-state-warn-label)!important}.dsh-deepseek-account-topup{display:inline-flex;align-items:center;color:#fff;text-decoration:none;background:var(--dsw-alias-state-business-primary)}"
|
|
158
|
+
document.head.append(style)
|
|
159
|
+
return () => style.remove()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inject = ["slots", "locale", "connection", "sessions", "modelDirectories"]
|
|
163
|
+
function apply(ctx) {
|
|
164
|
+
ctx.effect(() => ctx.locale.register(namespace, dictionaries), "deepseek-account: dictionaries")
|
|
165
|
+
ctx.effect(() => installStyle(), "deepseek-account: styles")
|
|
166
|
+
const t = ctx.locale.bind(namespace)
|
|
167
|
+
const client = accountClient(ctx.connection)
|
|
168
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
169
|
+
name: "sidebar.footer.action", id: "deepseek-account", order: 35, label: t("balance"),
|
|
170
|
+
inject: () => ({ ctx, client, t }),
|
|
171
|
+
}, SidebarBalance))
|
|
172
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
173
|
+
name: "settings.section", id: "deepseek-account", order: 44, label: () => t("nav"), locale: namespace,
|
|
174
|
+
inject: () => ({ client, t }),
|
|
175
|
+
}, AccountSettings))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports.inject = inject
|
|
179
|
+
module.exports.apply = apply
|
|
180
|
+
module.exports.accountProvider = accountProvider
|
|
181
|
+
module.exports.safeSnapshot = safeSnapshot
|
|
182
|
+
return module.exports
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
function plainRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
|
|
187
|
+
function validDecimal(value) { return typeof value === "string" && value.length <= 64 && /^\d+(?:\.\d+)?$/u.test(value) }
|
|
188
|
+
function validTime(value) { return typeof value === "string" && Number.isFinite(Date.parse(value)) }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createDeepSeekAccountBalance } from "../internal/account-balance.mjs"
|
|
2
|
+
import { registerDeepSeekAccountRpc } from "../internal/account-rpc.mjs"
|
|
3
|
+
|
|
4
|
+
export const name = "deepseek-account"
|
|
5
|
+
export const inject = ["credentials"]
|
|
6
|
+
|
|
7
|
+
export function apply(ctx) {
|
|
8
|
+
const accountBalance = createDeepSeekAccountBalance({ credentials: ctx.credentials })
|
|
9
|
+
ctx.inject(["connection"], (connectionCtx) => registerDeepSeekAccountRpc(connectionCtx, accountBalance))
|
|
10
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const AUTOMATIC_TTL_MS = 5 * 60 * 1000
|
|
2
|
+
const MANUAL_TTL_MS = 30 * 1000
|
|
3
|
+
const MAX_RESPONSE_BYTES = 32 * 1024
|
|
4
|
+
const API_URL = "https://api.deepseek.com/user/balance"
|
|
5
|
+
|
|
6
|
+
export function createDeepSeekAccountBalance({ credentials, fetchImpl = fetch, now = Date.now }) {
|
|
7
|
+
if (typeof credentials?.resolve !== "function" || typeof fetchImpl !== "function" || typeof now !== "function") {
|
|
8
|
+
throw new TypeError("Invalid DeepSeek account balance dependencies")
|
|
9
|
+
}
|
|
10
|
+
let lastGood
|
|
11
|
+
let attemptedAt = 0
|
|
12
|
+
let inFlight
|
|
13
|
+
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
read({ force = false } = {}) {
|
|
16
|
+
const ttl = force ? MANUAL_TTL_MS : AUTOMATIC_TTL_MS
|
|
17
|
+
if (lastGood !== undefined && now() - attemptedAt < ttl) return Promise.resolve(lastGood)
|
|
18
|
+
if (inFlight !== undefined) return inFlight
|
|
19
|
+
attemptedAt = now()
|
|
20
|
+
inFlight = load().finally(() => { inFlight = undefined })
|
|
21
|
+
return inFlight
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
async function load() {
|
|
26
|
+
const credential = await credentials.resolve("DEEPSEEK_API_KEY")
|
|
27
|
+
if (credential === undefined) return unavailable("credential-unconfigured")
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetchImpl(API_URL, {
|
|
30
|
+
method: "GET",
|
|
31
|
+
redirect: "error",
|
|
32
|
+
headers: { accept: "application/json", authorization: `Bearer ${credential.value}` },
|
|
33
|
+
signal: AbortSignal.timeout(5_000),
|
|
34
|
+
})
|
|
35
|
+
if (response.status === 401 || response.status === 403) return unavailable("credential-unauthorized")
|
|
36
|
+
if (response.status === 429) return unavailable("rate-limited")
|
|
37
|
+
if (!response.ok) return unavailable(response.status >= 500 ? "network" : "invalid-response")
|
|
38
|
+
const parsed = parseDeepSeekBalance(JSON.parse(await readBoundedBody(response)), new Date(now()).toISOString())
|
|
39
|
+
if (parsed === undefined) return unavailable("invalid-response")
|
|
40
|
+
lastGood = parsed
|
|
41
|
+
return parsed
|
|
42
|
+
} catch {
|
|
43
|
+
return unavailable("network")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unavailable(reason) {
|
|
48
|
+
return lastGood === undefined
|
|
49
|
+
? Object.freeze({ status: "unavailable", reason })
|
|
50
|
+
: Object.freeze({ status: "unavailable", reason, lastGood: Object.freeze({ ...lastGood, stale: true }) })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseDeepSeekBalance(value, fetchedAt) {
|
|
55
|
+
if (!isRecord(value) || typeof value.is_available !== "boolean" || !Array.isArray(value.balance_infos) || value.balance_infos.length > 2) return undefined
|
|
56
|
+
const balances = []
|
|
57
|
+
const currencies = new Set()
|
|
58
|
+
for (const item of value.balance_infos) {
|
|
59
|
+
if (!isRecord(item) || (item.currency !== "CNY" && item.currency !== "USD") || currencies.has(item.currency)) return undefined
|
|
60
|
+
const parts = [item.total_balance, item.granted_balance, item.topped_up_balance]
|
|
61
|
+
if (!parts.every(validDecimal)) return undefined
|
|
62
|
+
currencies.add(item.currency)
|
|
63
|
+
balances.push(Object.freeze({
|
|
64
|
+
currency: item.currency,
|
|
65
|
+
total: item.total_balance,
|
|
66
|
+
granted: item.granted_balance,
|
|
67
|
+
toppedUp: item.topped_up_balance,
|
|
68
|
+
}))
|
|
69
|
+
}
|
|
70
|
+
if (typeof fetchedAt !== "string" || !Number.isFinite(Date.parse(fetchedAt))) return undefined
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
status: "ready",
|
|
73
|
+
isAvailable: value.is_available,
|
|
74
|
+
balances: Object.freeze(balances),
|
|
75
|
+
fetchedAt,
|
|
76
|
+
stale: false,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readBoundedBody(response) {
|
|
81
|
+
const declared = Number(response.headers.get("content-length"))
|
|
82
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) throw new Error("response too large")
|
|
83
|
+
const reader = response.body?.getReader()
|
|
84
|
+
if (reader === undefined) return ""
|
|
85
|
+
const decoder = new TextDecoder()
|
|
86
|
+
let length = 0
|
|
87
|
+
let body = ""
|
|
88
|
+
while (true) {
|
|
89
|
+
const { done, value } = await reader.read()
|
|
90
|
+
if (done) break
|
|
91
|
+
length += value.byteLength
|
|
92
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
93
|
+
await reader.cancel()
|
|
94
|
+
throw new Error("response too large")
|
|
95
|
+
}
|
|
96
|
+
body += decoder.decode(value, { stream: true })
|
|
97
|
+
}
|
|
98
|
+
return body + decoder.decode()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validDecimal(value) {
|
|
102
|
+
return typeof value === "string" && value.length <= 64 && /^\d+(?:\.\d+)?$/u.test(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
107
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DEEPSEEK_ACCOUNT_RPC_CHANNEL = "/dsh-deepseek-account"
|
|
2
|
+
|
|
3
|
+
export function registerDeepSeekAccountRpc(ctx, accountBalance) {
|
|
4
|
+
if (typeof ctx?.connection?.rpc?.handle !== "function" || typeof accountBalance?.read !== "function") {
|
|
5
|
+
throw new TypeError("Invalid DeepSeek account RPC dependencies")
|
|
6
|
+
}
|
|
7
|
+
return ctx.connection.rpc.handle(
|
|
8
|
+
DEEPSEEK_ACCOUNT_RPC_CHANNEL,
|
|
9
|
+
async (endpoint, payload) => {
|
|
10
|
+
if (endpoint !== "read" || !isRecord(payload) || Reflect.ownKeys(payload).some((key) => key !== "force")) {
|
|
11
|
+
return { ok: false, error: "invalid-request" }
|
|
12
|
+
}
|
|
13
|
+
if (payload.force !== undefined && typeof payload.force !== "boolean") {
|
|
14
|
+
return { ok: false, error: "invalid-request" }
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, value: await accountBalance.read({ force: payload.force === true }) }
|
|
17
|
+
},
|
|
18
|
+
{ authority: "loopback" },
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## 中文
|
|
2
|
+
|
|
3
|
+
`0.1.0` 是 `dsh-deepseek-account` 的首个正式版本,将 DeepSeek 账户余额从桌面应用内置能力迁移为可独立安装和升级的 Harness 插件。
|
|
4
|
+
|
|
5
|
+
### 功能
|
|
6
|
+
|
|
7
|
+
- 通过 Host credential service 读取当前 `DEEPSEEK_API_KEY`,调用官方 `/user/balance` 并严格投影 CNY/USD 总余额、赠送余额和充值余额。
|
|
8
|
+
- 当前会话选择 DeepSeek 或无法识别的自定义 Provider 时显示 DeepSeek 卡片;选择 Grok 或 Codex 时不渲染。
|
|
9
|
+
- 设置页提供固定的 DeepSeek 官方网页登录与充值入口。
|
|
10
|
+
- 并发查询单飞、自动/手动刷新限频、32 KiB 响应上限和失败后已标记的旧快照回退。
|
|
11
|
+
|
|
12
|
+
### 安全与限制
|
|
13
|
+
|
|
14
|
+
- API Key 只存在于 Host;浏览器产物不包含凭据解析或 Authorization header。
|
|
15
|
+
- 插件不读取网页 Cookie、账户密码或支付资料,也不会代替用户提交付款。
|
|
16
|
+
- 本次候选只验证接口、构建、打包和隔离边界,未执行真实账户余额查询或实际充值。
|
|
17
|
+
|
|
18
|
+
安装:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
dsh plugin --profile web add dsh-deepseek-account@0.1.0
|
|
22
|
+
dsh web
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary>English release notes</summary>
|
|
27
|
+
|
|
28
|
+
`0.1.0` is the first formal `dsh-deepseek-account` release. It moves DeepSeek account balance handling out of the desktop application and into an independently installable Harness plugin.
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- Resolves the current `DEEPSEEK_API_KEY` through the Host credential service, calls the official `/user/balance` endpoint, and strictly projects CNY/USD total, granted, and topped-up balances.
|
|
33
|
+
- Shows the DeepSeek card for DeepSeek and unknown custom Providers and returns no sidebar content for Grok or Codex.
|
|
34
|
+
- Provides a fixed official DeepSeek web sign-in and top-up entry in settings.
|
|
35
|
+
- Coalesces concurrent reads, rate-limits automatic/manual refreshes, bounds responses to 32 KiB, and retains an explicitly stale last-good snapshot after transient failure.
|
|
36
|
+
|
|
37
|
+
### Security and limits
|
|
38
|
+
|
|
39
|
+
- The API key remains Host-only; the browser artifact contains neither credential resolution nor an Authorization header.
|
|
40
|
+
- The plugin never reads web cookies, account passwords, or payment details and never submits a payment.
|
|
41
|
+
- Candidate validation covers contracts, build, packaging, and isolation boundaries only. No real-account balance query or real top-up was performed.
|
|
42
|
+
|
|
43
|
+
Install:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
dsh plugin --profile web add dsh-deepseek-account@0.1.0
|
|
47
|
+
dsh web
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
</details>
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-deepseek-account",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek account balance and official top-up entry for DeepSeek Harness",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "YukiRyou",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/yoshino-xiao7/dsh-deepseek-account.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/yoshino-xiao7/dsh-deepseek-account#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/yoshino-xiao7/dsh-deepseek-account/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.19.0 <25"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/host/index.mjs",
|
|
21
|
+
"./client": "./dist/client/client.js",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "deepseek-account.patch.yml"
|
|
27
|
+
},
|
|
28
|
+
"client": {
|
|
29
|
+
"inject": [
|
|
30
|
+
"@deepseek-ai/dsh-client-connection",
|
|
31
|
+
"@deepseek-ai/dsh-client-locale",
|
|
32
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
36
|
+
],
|
|
37
|
+
"platform": "web"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"docs",
|
|
43
|
+
"deepseek-account.patch.yml",
|
|
44
|
+
"README.md",
|
|
45
|
+
"README.en.md",
|
|
46
|
+
"CHANGELOG.md",
|
|
47
|
+
"CONTRIBUTING.md",
|
|
48
|
+
"SECURITY.md",
|
|
49
|
+
"THIRD_PARTY_NOTICES.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "node scripts/build.mjs",
|
|
54
|
+
"test": "node --test test/*.test.mjs",
|
|
55
|
+
"check": "npm run test && npm run build",
|
|
56
|
+
"pack:check": "npm pack --dry-run --json",
|
|
57
|
+
"prepack": "npm run check"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.1-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-credentials": "0.1.1-rc.2"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@deepseek-ai/dsh-client-connection": { "optional": true },
|
|
71
|
+
"@deepseek-ai/dsh-client-locale": { "optional": true },
|
|
72
|
+
"@deepseek-ai/dsh-client-runtime": { "optional": true },
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-model-selection": { "optional": true },
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-settings": { "optional": true },
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-sidebar": { "optional": true },
|
|
76
|
+
"@deepseek-ai/dsh-credentials": { "optional": true }
|
|
77
|
+
}
|
|
78
|
+
}
|