@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/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: usage.cacheWriteTokens || 0,
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)) + '&region=' + 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
- : '未找到 ' + provider.credentialHint + ',请配置后重试'
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.12.2",
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
+ }