@feiyang666/dsh-usage-plugin 1.12.2 → 1.13.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 +268 -229
- package/README.md +311 -308
- package/README.zh.md +4 -1
- package/lib/balance.js +117 -17
- package/lib/client.js +499 -256
- package/lib/index.js +143 -2
- package/package.json +80 -77
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
matchesModelProvider,
|
|
23
23
|
parseBalanceResponse,
|
|
24
24
|
providerList,
|
|
25
|
+
missingCredentialError,
|
|
25
26
|
resolveBalanceEndpoint
|
|
26
27
|
} from './balance.js'
|
|
27
28
|
|
|
@@ -508,6 +509,11 @@ export default {
|
|
|
508
509
|
if (provider === 'digital-ocean' || provider === 'digitalocean') {
|
|
509
510
|
try { await refreshFxRate(false) } catch (e) {}
|
|
510
511
|
}
|
|
512
|
+
// 兼容性:DeepSeek 系 provider 的 usage 不单独上报 cacheWriteTokens
|
|
513
|
+
// (缓存写入发生在未命中时,即 cacheWriteTokens == inputTokens)。
|
|
514
|
+
// 当上游未提供该字段时,用未命中 token 数(inputTokens)兜底,避免
|
|
515
|
+
// "缓存写入"列长期为空。
|
|
516
|
+
const cacheWrite = usage.cacheWriteTokens || usage.inputTokens || 0
|
|
511
517
|
records.push({
|
|
512
518
|
time: startedAt,
|
|
513
519
|
model,
|
|
@@ -516,7 +522,7 @@ export default {
|
|
|
516
522
|
inputTokens: usage.inputTokens || 0,
|
|
517
523
|
outputTokens: usage.outputTokens || 0,
|
|
518
524
|
cacheReadTokens: usage.cacheReadTokens || 0,
|
|
519
|
-
cacheWriteTokens:
|
|
525
|
+
cacheWriteTokens: cacheWrite,
|
|
520
526
|
reasoningTokens: usage.reasoningTokens || 0,
|
|
521
527
|
finishReason,
|
|
522
528
|
usdCnyRate: (provider === 'digital-ocean' || provider === 'digitalocean') ? (FX.rate || 0) : 0,
|
|
@@ -798,6 +804,124 @@ export default {
|
|
|
798
804
|
credentialName: DIGITALOCEAN_CREDENTIAL
|
|
799
805
|
}
|
|
800
806
|
}
|
|
807
|
+
// ── Qwen / 百炼 Token Plan (console-token) ────────────────────────────
|
|
808
|
+
// 复用 bl CLI 保存的百炼控制台 OAuth access_token(~/.bailian/config.json),
|
|
809
|
+
// 调用百炼内部门户网关查询 Token Plan 配额用量,不需要阿里云 AccessKey。
|
|
810
|
+
const QWEN_GATEWAYS = {
|
|
811
|
+
'cn-beijing-domestic': { host: 'bailian-cs.console.aliyun.com', action: 'BroadScopeAspnGateway' },
|
|
812
|
+
'cn-beijing-international': { host: 'bailian-cs.console.alibabacloud.com', action: 'BroadScopeAspnGateway' },
|
|
813
|
+
'ap-southeast-1-domestic': { host: 'modelstudio-cs.console.aliyun.com', action: 'IntlBroadScopeAspnGateway' },
|
|
814
|
+
'ap-southeast-1-international': { host: 'bailian-singapore-cs.alibabacloud.com', action: 'IntlBroadScopeAspnGateway' }
|
|
815
|
+
}
|
|
816
|
+
const QWEN_API = 'zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage'
|
|
817
|
+
|
|
818
|
+
function readBailianConfigPath() {
|
|
819
|
+
const base = process.env.BAILIAN_CONFIG_DIR || joinPath(os.homedir(), '.bailian')
|
|
820
|
+
return joinPath(base, 'config.json')
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function readQwenConsoleToken() {
|
|
824
|
+
const cfgPath = readBailianConfigPath()
|
|
825
|
+
let raw
|
|
826
|
+
try { raw = await nfsReadText(cfgPath) } catch (e) {
|
|
827
|
+
throw new Error('未找到 ' + cfgPath + '。请先运行 `bl auth login --console` 完成百炼控制台登录。')
|
|
828
|
+
}
|
|
829
|
+
let cfg
|
|
830
|
+
try { cfg = JSON.parse(raw) } catch (e) {
|
|
831
|
+
throw new Error(cfgPath + ' 不是合法的 JSON 配置')
|
|
832
|
+
}
|
|
833
|
+
let token = cfg.access_token || cfg.accessToken
|
|
834
|
+
const active = cfg.active_config || 'default'
|
|
835
|
+
const profile = cfg[active]
|
|
836
|
+
if (profile && typeof profile === 'object') {
|
|
837
|
+
token = token || profile.access_token || profile.accessToken
|
|
838
|
+
}
|
|
839
|
+
if (!token || !String(token).trim()) {
|
|
840
|
+
throw new Error('配置中没有 access_token,请先运行 `bl auth login --console` 完成百炼控制台登录。')
|
|
841
|
+
}
|
|
842
|
+
return { token: String(token).trim(), region: String(cfg.console_region || 'cn-beijing'), site: String(cfg.console_site || 'domestic') }
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function queryQwenTokenPlan() {
|
|
846
|
+
const { token, region, site } = await readQwenConsoleToken()
|
|
847
|
+
const gw = QWEN_GATEWAYS[region + '-' + site] || QWEN_GATEWAYS['cn-beijing-domestic']
|
|
848
|
+
if (!gw) throw new Error('不支持的控制台地域/站点:' + region + '/' + site)
|
|
849
|
+
const payload = {
|
|
850
|
+
Api: QWEN_API,
|
|
851
|
+
V: '1.0',
|
|
852
|
+
Data: {
|
|
853
|
+
cornerstoneParam: {
|
|
854
|
+
protocol: 'V2',
|
|
855
|
+
console: 'ONE_CONSOLE',
|
|
856
|
+
productCode: 'p_efm',
|
|
857
|
+
switchUserType: 3,
|
|
858
|
+
consoleSite: 'BAILIAN_ALIYUN'
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const body = 'params=' + encodeURIComponent(JSON.stringify(payload)) + '®ion=' + encodeURIComponent(region)
|
|
863
|
+
const url = 'https://' + gw.host + '/cli/api.json'
|
|
864
|
+
+ '?action=' + encodeURIComponent(gw.action)
|
|
865
|
+
+ '&product=sfm_bailian&api=' + encodeURIComponent(QWEN_API)
|
|
866
|
+
// 与 bl CLI 一致:用 undici 的 EnvHttpProxyAgent 自动读取 HTTPS_PROXY/https_proxy
|
|
867
|
+
// 环境变量,走代理转发;未设置代理时则直连(EnvHttpProxyAgent 内部处理)。
|
|
868
|
+
// 因为子进程不保证能按模块名解析到 undici,这里预先算出 undici 的绝对入口路径并传给子进程。
|
|
869
|
+
let undiciPath = ''
|
|
870
|
+
try {
|
|
871
|
+
undiciPath = new URL(import.meta.resolve('undici')).pathname
|
|
872
|
+
} catch (e) {
|
|
873
|
+
// undici 是插件依赖,正常安装后必然存在;这里留空交由子进程报清楚错误。
|
|
874
|
+
undiciPath = ''
|
|
875
|
+
}
|
|
876
|
+
const script = [
|
|
877
|
+
'const token=process.env.BAILIAN_TOKEN||"";',
|
|
878
|
+
'const url=process.env.BAILIAN_API_URL||"";',
|
|
879
|
+
'const body=process.env.BAILIAN_BODY||"";',
|
|
880
|
+
'const undiciPath=process.env.UNDICI_PATH||"";',
|
|
881
|
+
'if(!undiciPath){process.stdout.write(JSON.stringify({error:"undici 不可用,请确认插件依赖已安装"}));process.exit(0);}',
|
|
882
|
+
'const m=require(undiciPath);',
|
|
883
|
+
'try{m.setGlobalDispatcher(new m.EnvHttpProxyAgent())}catch(e){}',
|
|
884
|
+
'const timeout=setTimeout(()=>{process.stdout.write(JSON.stringify({error:"timeout"}));process.exit(0)},20000);',
|
|
885
|
+
'(async()=>{',
|
|
886
|
+
' try{',
|
|
887
|
+
' const res=await m.fetch(url,{method:"POST",headers:{Authorization:"Bearer "+token,Accept:"*/*","Content-Type":"application/x-www-form-urlencoded","User-Agent":"dsh-usage-plugin"},body:body});',
|
|
888
|
+
' clearTimeout(timeout);',
|
|
889
|
+
' const b=await res.text();',
|
|
890
|
+
' process.stdout.write(JSON.stringify({statusCode:res.status,contentType:String(res.headers.get("content-type")||""),body:b}));',
|
|
891
|
+
' }catch(e){',
|
|
892
|
+
' clearTimeout(timeout);',
|
|
893
|
+
' process.stdout.write(JSON.stringify({error:String(e&&e.message||e)}));',
|
|
894
|
+
' }',
|
|
895
|
+
'})();'
|
|
896
|
+
].join('\n')
|
|
897
|
+
const r = await spawnNode(script, null, {
|
|
898
|
+
BAILIAN_TOKEN: token,
|
|
899
|
+
BAILIAN_API_URL: url,
|
|
900
|
+
BAILIAN_BODY: body,
|
|
901
|
+
UNDICI_PATH: undiciPath
|
|
902
|
+
})
|
|
903
|
+
if (!r.ok) throw new Error(r.error || '请求失败')
|
|
904
|
+
let parsed
|
|
905
|
+
try { parsed = JSON.parse(r.out) } catch (e) { throw new Error('无法解析百炼网关输出') }
|
|
906
|
+
if (parsed.error) throw new Error(parsed.error)
|
|
907
|
+
if (parsed.statusCode !== 200) {
|
|
908
|
+
const hint = parsed.statusCode === 401 || parsed.statusCode === 403 ? ' 登录可能已过期,请重新运行 `bl auth login --console`。' : ''
|
|
909
|
+
throw new Error('接口返回 HTTP ' + parsed.statusCode + ':' + String(parsed.body || '').slice(0, 300) + hint)
|
|
910
|
+
}
|
|
911
|
+
// 网关返回的是带外层包装的 JSON(data.DataV2.data.data 才是配额字段),
|
|
912
|
+
// 这里解包后把扁平化的 JSON 文本交给 parseBalanceResponse。
|
|
913
|
+
let wrapper
|
|
914
|
+
try { wrapper = JSON.parse(parsed.body) } catch (e) { throw new Error('无法解析百炼网关响应') }
|
|
915
|
+
let inner = wrapper && wrapper.data
|
|
916
|
+
if (inner && inner.DataV2) {
|
|
917
|
+
const d2 = inner.DataV2.data
|
|
918
|
+
inner = (d2 && d2.data) != null ? d2.data : (d2 != null ? d2 : inner.DataV2)
|
|
919
|
+
} else if (inner) {
|
|
920
|
+
inner = inner.data || inner
|
|
921
|
+
}
|
|
922
|
+
if (!inner || typeof inner !== 'object') throw new Error('百炼网关响应格式不符合预期')
|
|
923
|
+
return JSON.stringify(inner)
|
|
924
|
+
}
|
|
801
925
|
|
|
802
926
|
async function queryBalance(providerId) {
|
|
803
927
|
const provider = getBalanceProvider(providerId)
|
|
@@ -805,6 +929,23 @@ export default {
|
|
|
805
929
|
if (provider.queryMode === 'unsupported') {
|
|
806
930
|
return balanceFailure(provider, 'AMD GPU Cloud 当前未公开可由推理 API Key 调用的余额查询端点;请在 AMD Developer Cloud 控制台查看 credits。', { unsupported: true, errorCode: 'unsupported' })
|
|
807
931
|
}
|
|
932
|
+
if (provider.queryMode === 'console-token') {
|
|
933
|
+
try {
|
|
934
|
+
const bodyText = await queryQwenTokenPlan()
|
|
935
|
+
const normalized = parseBalanceResponse(provider.id, bodyText)
|
|
936
|
+
if (!normalized.ok) {
|
|
937
|
+
normalized.provider = provider.id
|
|
938
|
+
normalized.providerName = provider.name
|
|
939
|
+
normalized.credentialHelpUrl = provider.credentialHelpUrl || ''
|
|
940
|
+
} else {
|
|
941
|
+
normalized.credentialName = '百炼控制台 token'
|
|
942
|
+
normalized.credentialSource = readBailianConfigPath()
|
|
943
|
+
}
|
|
944
|
+
return normalized
|
|
945
|
+
} catch (e) {
|
|
946
|
+
return balanceFailure(provider, msg(e), { errorCode: 'console-token-error' })
|
|
947
|
+
}
|
|
948
|
+
}
|
|
808
949
|
const credentials = ctx.get('credentials')
|
|
809
950
|
if (!credentials) return balanceFailure(provider, '凭据服务不可用', { errorCode: 'credentials-unavailable' })
|
|
810
951
|
const modelProfile = await configuredModelProvider(provider)
|
|
@@ -825,7 +966,7 @@ export default {
|
|
|
825
966
|
? '尚未保存 DigitalOcean 账户 Personal Access Token。请在此页面输入 dop_v1_ Token,保存后查询。'
|
|
826
967
|
: provider.id === 'siliconflow'
|
|
827
968
|
? '模型提供商 ' + modelProfile.route + ' 引用了 ' + modelProfile.apiKeyEnv + ',但该凭据未配置。请在“设置 → 模型”中重新填写 API Key 并保存。'
|
|
828
|
-
:
|
|
969
|
+
: missingCredentialError(provider).error
|
|
829
970
|
return balanceFailure(provider, message, { errorCode: 'missing-credential', modelProviderRoute: modelProfile ? modelProfile.route : '' })
|
|
830
971
|
}
|
|
831
972
|
const endpoint = resolveBalanceEndpoint(provider.id, modelProfile && modelProfile.baseURL)
|
package/package.json
CHANGED
|
@@ -1,77 +1,80 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@feiyang666/dsh-usage-plugin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DeepSeek Harness usage & cost tracker plugin: per-call token/cache-hit stats, peak/off-peak billing, DeepSeek balance query, CSV/JSON/PNG export with custom destination, and persistent local storage. Ships a host half plus a web client half in one npm package; installs into a DSH profile as a dsh.bundle with one command (dsh plugin --profile web add @feiyang666/dsh-usage-plugin).",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"deepseek",
|
|
7
|
-
"harness",
|
|
8
|
-
"dsh",
|
|
9
|
-
"plugin",
|
|
10
|
-
"usage",
|
|
11
|
-
"cost",
|
|
12
|
-
"tokens",
|
|
13
|
-
"cache",
|
|
14
|
-
"balance"
|
|
15
|
-
],
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"type": "module",
|
|
18
|
-
"main": "lib/index.js",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/feiyang-dev/dsh-usage-plugin.git"
|
|
22
|
-
},
|
|
23
|
-
"contributors": [
|
|
24
|
-
{
|
|
25
|
-
"name": "Martin-soaring-dev",
|
|
26
|
-
"url": "https://github.com/Martin-soaring-dev"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"name": "liu3734",
|
|
30
|
-
"url": "https://github.com/liu3734"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"name": "mumuer1024",
|
|
34
|
-
"url": "https://github.com/mumuer1024"
|
|
35
|
-
}
|
|
36
|
-
],
|
|
37
|
-
"homepage": "https://github.com/feiyang-dev/dsh-usage-plugin#readme",
|
|
38
|
-
"bugs": {
|
|
39
|
-
"url": "https://github.com/feiyang-dev/dsh-usage-plugin/issues"
|
|
40
|
-
},
|
|
41
|
-
"exports": {
|
|
42
|
-
".": "./lib/index.js",
|
|
43
|
-
"./client": "./lib/client.js",
|
|
44
|
-
"./package.json": "./package.json"
|
|
45
|
-
},
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=18"
|
|
48
|
-
},
|
|
49
|
-
"dsh": {
|
|
50
|
-
"bundle": {
|
|
51
|
-
"patch": "./cordis.patch.yml"
|
|
52
|
-
},
|
|
53
|
-
"client": {
|
|
54
|
-
"platform": "web",
|
|
55
|
-
"inject": [
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-conversation"
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
"files": [
|
|
61
|
-
"lib",
|
|
62
|
-
"scripts",
|
|
63
|
-
"cordis.patch.yml",
|
|
64
|
-
"README.md",
|
|
65
|
-
"README.zh.md",
|
|
66
|
-
"CHANGELOG.md"
|
|
67
|
-
],
|
|
68
|
-
"scripts": {
|
|
69
|
-
"wire": "node scripts/wire.js",
|
|
70
|
-
"check": "node scripts/check-package.js",
|
|
71
|
-
"prepublishOnly": "node scripts/check-package.js",
|
|
72
|
-
"pack": "npm pack"
|
|
73
|
-
},
|
|
74
|
-
"peerDependencies": {
|
|
75
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
76
|
-
}
|
|
77
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@feiyang666/dsh-usage-plugin",
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"description": "DeepSeek Harness usage & cost tracker plugin: per-call token/cache-hit stats, peak/off-peak billing, DeepSeek balance query, CSV/JSON/PNG export with custom destination, and persistent local storage. Ships a host half plus a web client half in one npm package; installs into a DSH profile as a dsh.bundle with one command (dsh plugin --profile web add @feiyang666/dsh-usage-plugin).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek",
|
|
7
|
+
"harness",
|
|
8
|
+
"dsh",
|
|
9
|
+
"plugin",
|
|
10
|
+
"usage",
|
|
11
|
+
"cost",
|
|
12
|
+
"tokens",
|
|
13
|
+
"cache",
|
|
14
|
+
"balance"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "lib/index.js",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/feiyang-dev/dsh-usage-plugin.git"
|
|
22
|
+
},
|
|
23
|
+
"contributors": [
|
|
24
|
+
{
|
|
25
|
+
"name": "Martin-soaring-dev",
|
|
26
|
+
"url": "https://github.com/Martin-soaring-dev"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "liu3734",
|
|
30
|
+
"url": "https://github.com/liu3734"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "mumuer1024",
|
|
34
|
+
"url": "https://github.com/mumuer1024"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/feiyang-dev/dsh-usage-plugin#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/feiyang-dev/dsh-usage-plugin/issues"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./lib/index.js",
|
|
43
|
+
"./client": "./lib/client.js",
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"dsh": {
|
|
50
|
+
"bundle": {
|
|
51
|
+
"patch": "./cordis.patch.yml"
|
|
52
|
+
},
|
|
53
|
+
"client": {
|
|
54
|
+
"platform": "web",
|
|
55
|
+
"inject": [
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"lib",
|
|
62
|
+
"scripts",
|
|
63
|
+
"cordis.patch.yml",
|
|
64
|
+
"README.md",
|
|
65
|
+
"README.zh.md",
|
|
66
|
+
"CHANGELOG.md"
|
|
67
|
+
],
|
|
68
|
+
"scripts": {
|
|
69
|
+
"wire": "node scripts/wire.js",
|
|
70
|
+
"check": "node scripts/check-package.js",
|
|
71
|
+
"prepublishOnly": "node scripts/check-package.js",
|
|
72
|
+
"pack": "npm pack"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"undici": "^7.0.0"
|
|
79
|
+
}
|
|
80
|
+
}
|