dsh-whale-widget 0.3.14 → 0.3.15
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 +15 -0
- package/lib/index.js +137 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -306,6 +306,19 @@ MeteorNOX/DeepSeek-Balance-Whale-Widget,或者我本地已经有这个插件
|
|
|
306
306
|
|
|
307
307
|
> **不需要** `DEEPSEEK_PLATFORM_TOKEN`。早期版本的"实时·令牌"模式已下线,今日已用统一由**小鲸鱼记账**(余额差 + 会话事件)计算,零令牌开箱即用。
|
|
308
308
|
|
|
309
|
+
### 安全边界(0.3.15 起,**请务必一读**)
|
|
310
|
+
|
|
311
|
+
插件会把凭据交给谁、以及谁能改这件事的规则:
|
|
312
|
+
|
|
313
|
+
1. **凭据只会发往"该厂商内置模板里写死的端点"**。
|
|
314
|
+
自定义模型里如果填了一个**不在该厂商模板内**的接口地址(或者模板地址里用 `{base}` 指向你自己填的地址),插件**默认不会把凭据发过去**,而是返回一条明确提示。
|
|
315
|
+
- 自建网关(New API / 自托管 / Ollama 等)确实需要自定义地址:请**在这台机器本机**打开插件面板,对该模型勾选「**允许把凭据发送到自定义地址**」。这是按模型保存的一个显式标志。
|
|
316
|
+
2. **配置与凭据的改动只能来自本机(回环地址)**。
|
|
317
|
+
所有写请求(保存模型、写/删凭据、改外观设置等)如果来源不是 `127.0.0.1` / `localhost` / `[::1]`,一律 `403`。
|
|
318
|
+
- 为什么:任意持有 DSH Web 会话的人如果能写模型配置,就等于能决定"凭据被发往哪里" —— 那会把你的 API key 直接送到对方服务器上。**只读接口不受影响**(局域网里照常能看挂件)。
|
|
319
|
+
- 需要远端管理?用环境变量显式声明:`DSHW_ADMIN_HOSTS=192.0.2.10:3080,myhost.lan`(逗号分隔,可带端口;默认空)。
|
|
320
|
+
3. 接口地址不允许携带用户名/密码(`https://user:pass@host/`),凭据名只允许字母、数字与下划线。
|
|
321
|
+
|
|
309
322
|
添加自定义模型时,还会按需用到各自厂商的凭据名(都可不配,用到哪个配哪个):
|
|
310
323
|
|
|
311
324
|
| 凭据名 | 用途 |
|
|
@@ -393,6 +406,8 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
|
|
|
393
406
|
- **图片/音效不显示、没声音**:确认插件包内 `assets/` 完整(`DSniang1.png`、`*.mp3`、`minecraft-exp-orb.wav` 等);缺失时相关功能静默降级。
|
|
394
407
|
- **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 凭据里配置 API key;**或者**用「设置 → 账号与余额」登录 DeepSeek 账号(较新 DSH 支持)—— 没配 key 时插件会自动走账号态,不需要去 platform 建 key。
|
|
395
408
|
- **余额拿到了、但提示「账本不可用 / 已停止写入以保护原记录」**:账本文件(`.dshw-usage.json`)被外部改坏或结构异常,插件**拒绝覆盖**它以免丢数据。先把它备份改名让插件重建,再按需用「余额校正」补账。
|
|
409
|
+
- **保存设置报 `403` / 在别的机器上改了配置不生效**:0.3.15 起**写操作只能在运行 DSH 的那台机器上、用 `127.0.0.1`(本机地址)打开界面进行**。从局域网或反向代理访问时,读没问题、写会被拒。需要远端管理就用 `DSHW_ADMIN_HOSTS` 显式授权那台机器(见「凭据 → 安全边界」)。
|
|
410
|
+
- **提示「该地址不是 … 的内置端点,已拒绝把凭据发出去」**:这是 0.3.15 的安全策略 —— **自定义接口地址默认拿不到凭据**。确认那是你自己的网关(New API / 自托管等)后,在**本机**打开面板,对该模型勾选「允许把凭据发送到自定义地址」。
|
|
396
411
|
- **今日已用显示 `--`**:先等一次成功的余额观测;统计从该观测时刻开始,起点之前的消费不在此区间内。
|
|
397
412
|
- **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 → DeepSeek(内置)→ 设置 → 余额校正」填写实际到账金额。余额接口只返回余额快照、不提供充值流水,充值与消费发生在同一次刷新间隔时需要实际到账金额才能校正。
|
|
398
413
|
- **充值后消费数字没变**:这是预期行为——充值不会增加消费;数字上方会出现「待核对余额调整」,提示你补充本统计区间的累计到账金额。
|
package/lib/index.js
CHANGED
|
@@ -692,6 +692,38 @@ export default {
|
|
|
692
692
|
+ 'DSHW_TRUSTED_HOSTS 声明允许的 Host(逗号分隔,可带端口)。')
|
|
693
693
|
} catch (err) {}
|
|
694
694
|
}
|
|
695
|
+
// v761(安全修复 S1):**写请求(POST/PUT/PATCH/DELETE)必须来自本机(回环)**。
|
|
696
|
+
// 为什么:任意持有 DSH Web 会话的人都能 `POST /dsh-whale/api-models.json` 写一个自定义模型,
|
|
697
|
+
// 而余额 / 探活 / 额度会**带着解析出来的真实凭据**去请求模型里那个 URL ⇒ 凭据外带。
|
|
698
|
+
// 「改配置」等于「决定把凭据发往哪里」,所以它必须与"坐在这台机器前"绑定;
|
|
699
|
+
// **只读接口不受影响**(局域网里仍能正常看挂件)。
|
|
700
|
+
// 运维若确实需要远端管理,显式声明 DSHW_ADMIN_HOSTS(逗号分隔,可带端口),默认为空。
|
|
701
|
+
const ADMIN_AUTHORITIES = String(process.env.DSHW_ADMIN_HOSTS || '')
|
|
702
|
+
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
703
|
+
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
|
|
704
|
+
function writeRejection(req) {
|
|
705
|
+
try {
|
|
706
|
+
const method = String((req && req.method) || 'GET').toUpperCase()
|
|
707
|
+
if (!WRITE_METHODS.has(method)) return null
|
|
708
|
+
const headers = (req && req.headers) || {}
|
|
709
|
+
let hostUrl = null
|
|
710
|
+
try { hostUrl = new URL('http://' + String(headers.host || '')) } catch (err) { return 403 }
|
|
711
|
+
if (isLoopbackHostname(hostUrl.hostname)) return null
|
|
712
|
+
const authority = hostUrl.host.toLowerCase()
|
|
713
|
+
const hostname = hostUrl.hostname.toLowerCase()
|
|
714
|
+
if (ADMIN_AUTHORITIES.some((e) => (e.indexOf(':') >= 0 ? e === authority : e === hostname))) return null
|
|
715
|
+
if (!writeRejection.warned) {
|
|
716
|
+
writeRejection.warned = true
|
|
717
|
+
try {
|
|
718
|
+
console.warn('[whale-balance] 已拒绝一个来自非本机的**写请求**(Host=' + authority + ')。'
|
|
719
|
+
+ '自 0.3.15 起,配置与凭据的改动只能在**本机**(回环地址)打开界面进行 —— '
|
|
720
|
+
+ '否则任何拿到 Web 会话的人都能把凭据改指向自己的服务器,从而把真实 API key 带走。'
|
|
721
|
+
+ '确需远端管理,请用环境变量 DSHW_ADMIN_HOSTS 声明允许的管理主机。')
|
|
722
|
+
} catch (err) {}
|
|
723
|
+
}
|
|
724
|
+
return 403
|
|
725
|
+
} catch (err) { return 403 } // 自身出错 → 拒绝(fail-closed)
|
|
726
|
+
}
|
|
695
727
|
function rejected(req, res) {
|
|
696
728
|
const deny = (code) => {
|
|
697
729
|
try { res.statusCode = code || 403; res.end() } catch (err) {}
|
|
@@ -701,6 +733,9 @@ export default {
|
|
|
701
733
|
// 先算栅栏可用性,再自校验:非回环 Host 在"栅栏可用"时交给栅栏判(它知道 --trusted-host)
|
|
702
734
|
const self = selfRejection(req, { fenceAvailable: !!conn })
|
|
703
735
|
if (self !== null) return deny(self)
|
|
736
|
+
// v761(安全修复 S1):写完再放行 —— 读请求不受影响
|
|
737
|
+
const write = writeRejection(req)
|
|
738
|
+
if (write !== null) return deny(write)
|
|
704
739
|
if (!conn) return false // 已通过自校验;此刻没有宿主栅栏可委托(加载时已 warn)
|
|
705
740
|
let code
|
|
706
741
|
try {
|
|
@@ -1153,7 +1188,7 @@ export default {
|
|
|
1153
1188
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
1154
1189
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
1155
1190
|
return {
|
|
1156
|
-
ok: true, version: '0.3.
|
|
1191
|
+
ok: true, version: '0.3.15', today: todayData, days7,
|
|
1157
1192
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
1158
1193
|
all: {
|
|
1159
1194
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -1732,6 +1767,71 @@ export default {
|
|
|
1732
1767
|
if (a === 0) throw new Error('禁止访问该地址')
|
|
1733
1768
|
}
|
|
1734
1769
|
if (/^fe[89ab][0-9a-f]:/i.test(host)) throw new Error('禁止访问 link-local 地址')
|
|
1770
|
+
// v761(安全修复 S1):URL 里不允许带 userinfo(`https://user:pass@host/`)——
|
|
1771
|
+
// 它是把凭据塞进 URL 的另一种写法,也会让日志/报错里出现凭据
|
|
1772
|
+
if (parsed.username || parsed.password) throw new Error('接口地址不允许携带用户名/密码')
|
|
1773
|
+
}
|
|
1774
|
+
// —— v761(安全修复 S1):**凭据目的地白名单** ——
|
|
1775
|
+
// 规则:只有当目标 origin **恰好等于该厂商内置模板里写死的端点**时,才允许携带凭据;
|
|
1776
|
+
// 其它地址(用户自定义 URL、以及由 `{base}` 展开出来的地址)都必须带 `model.allowCustomHost === true`
|
|
1777
|
+
// 才放行 —— 而该标志**只能由本机(回环)来源的写请求设置**(见 writeRejection),
|
|
1778
|
+
// 因此远端会话即使改了配置,也拿不到凭据。
|
|
1779
|
+
// 注意:**含 `{base}` 的模板 URL 不算可信**(base 是用户可填字段),一律按自定义地址处理。
|
|
1780
|
+
function originOfUrl(u) {
|
|
1781
|
+
try {
|
|
1782
|
+
const p = new URL(String(u))
|
|
1783
|
+
if (p.protocol !== 'http:' && p.protocol !== 'https:') return ''
|
|
1784
|
+
return p.origin.toLowerCase()
|
|
1785
|
+
} catch (err) { return '' }
|
|
1786
|
+
}
|
|
1787
|
+
function templateOrigins(tpl) {
|
|
1788
|
+
const out = new Set()
|
|
1789
|
+
const add = (u) => {
|
|
1790
|
+
const s = String(u == null ? '' : u)
|
|
1791
|
+
if (!s || s.indexOf('{base}') >= 0) return
|
|
1792
|
+
const o = originOfUrl(s.replace('{key}', 'placeholder'))
|
|
1793
|
+
if (o) out.add(o)
|
|
1794
|
+
}
|
|
1795
|
+
add(tpl && tpl.probeUrl)
|
|
1796
|
+
add(tpl && tpl.balance && tpl.balance.url)
|
|
1797
|
+
add(tpl && tpl.balance && tpl.balance.usage && tpl.balance.usage.url)
|
|
1798
|
+
add(tpl && tpl.quota && tpl.quota.url)
|
|
1799
|
+
return out
|
|
1800
|
+
}
|
|
1801
|
+
function credentialMayGoTo(model, tpl, url) {
|
|
1802
|
+
const o = originOfUrl(url)
|
|
1803
|
+
if (!o) return false
|
|
1804
|
+
if (templateOrigins(tpl).has(o)) return true
|
|
1805
|
+
return !!(model && model.allowCustomHost === true)
|
|
1806
|
+
}
|
|
1807
|
+
function warnCredDestinationOnce(url) {
|
|
1808
|
+
try {
|
|
1809
|
+
const o = originOfUrl(url) || '(无效地址)'
|
|
1810
|
+
if (!warnCredDestinationOnce.seen) warnCredDestinationOnce.seen = new Set()
|
|
1811
|
+
if (warnCredDestinationOnce.seen.has(o)) return
|
|
1812
|
+
warnCredDestinationOnce.seen.add(o)
|
|
1813
|
+
console.warn('[whale-balance] 已拒绝把凭据发送到非内置端点:' + o
|
|
1814
|
+
+ '(若这是你自建/自托管的网关,请在**本机**打开插件面板,对该模型勾选「允许把凭据发送到自定义地址」)')
|
|
1815
|
+
} catch (err) {}
|
|
1816
|
+
}
|
|
1817
|
+
// 解析凭据 + 目的地校验:所有"带凭据发请求"的地方都必须走这里
|
|
1818
|
+
async function keyForDestination(model, tpl, url) {
|
|
1819
|
+
let key = ''
|
|
1820
|
+
try {
|
|
1821
|
+
const cred = await ctx.credentials.resolve(model.keyRef || tpl.keyRef || '')
|
|
1822
|
+
key = cred && cred.value ? String(cred.value) : ''
|
|
1823
|
+
} catch (err) { key = '' }
|
|
1824
|
+
if (!key) return { key: '', code: 'NO_KEY', error: '未配置 ' + (model.keyRef || tpl.keyRef || 'API key') }
|
|
1825
|
+
if (!credentialMayGoTo(model, tpl, url)) {
|
|
1826
|
+
warnCredDestinationOnce(url)
|
|
1827
|
+
return {
|
|
1828
|
+
key: '', code: 'DEST_NOT_ALLOWED',
|
|
1829
|
+
error: '该地址不是 ' + String((tpl && tpl.name) || model.provider || '该厂商') + ' 的内置端点,'
|
|
1830
|
+
+ '已拒绝把凭据发出去。若这是你自建/自托管的网关,请**在本机**打开插件面板,'
|
|
1831
|
+
+ '对该模型勾选「允许把凭据发送到自定义地址」后重试。',
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return { key, code: '', error: '' }
|
|
1735
1835
|
}
|
|
1736
1836
|
async function apiFetchJson(url, auth, key) {
|
|
1737
1837
|
const headers = {}
|
|
@@ -1774,16 +1874,14 @@ export default {
|
|
|
1774
1874
|
}
|
|
1775
1875
|
return { ok: false, error: (cs && cs.error) || '未找到 Codex 会话目录' }
|
|
1776
1876
|
}
|
|
1777
|
-
let key = ''
|
|
1778
|
-
try {
|
|
1779
|
-
const cred = await ctx.credentials.resolve(model.keyRef || tpl.keyRef || '')
|
|
1780
|
-
key = cred && cred.value ? String(cred.value) : ''
|
|
1781
|
-
} catch (err) { key = '' }
|
|
1782
|
-
if (!key) return { ok: false, error: '未配置 ' + (model.keyRef || tpl.keyRef || 'API key') }
|
|
1783
1877
|
const b = mergeNonEmpty(tpl.balance, model.balance) // v724:模型侧空串不再覆盖模板
|
|
1784
1878
|
const base = String(model.baseUrl || '').replace(/\/+$/, '')
|
|
1785
1879
|
const url = String(tpl.probeUrl || b.url || (base ? base + '/v1/models' : '')).replace('{base}', base)
|
|
1786
1880
|
if (!url) return { ok: false, error: '没有可测试的接口(请填余额接口或 Base URL)' }
|
|
1881
|
+
// v761(安全修复 S1):只在目的地可信时才带上凭据
|
|
1882
|
+
const k = await keyForDestination(model, tpl, url)
|
|
1883
|
+
if (!k.key) return { ok: false, code: k.code, error: k.error }
|
|
1884
|
+
const key = k.key
|
|
1787
1885
|
try {
|
|
1788
1886
|
const data = await apiFetchJson(url, b.auth, key)
|
|
1789
1887
|
const bizErr = apiBusinessError(data)
|
|
@@ -1805,14 +1903,12 @@ export default {
|
|
|
1805
1903
|
const tpl = apiTemplateOf(model.provider) || {}
|
|
1806
1904
|
const q = tpl.quota
|
|
1807
1905
|
if (!q || !q.url) return { ok: false, code: 'NO_QUOTA', error: '该厂商没有订阅额度接口' }
|
|
1808
|
-
let key = ''
|
|
1809
|
-
try {
|
|
1810
|
-
const cred = await ctx.credentials.resolve(model.keyRef || tpl.keyRef || '')
|
|
1811
|
-
key = cred && cred.value ? String(cred.value) : ''
|
|
1812
|
-
} catch (err) { key = '' }
|
|
1813
|
-
if (!key) return { ok: false, code: 'NO_KEY', error: '未配置 ' + (model.keyRef || tpl.keyRef || 'API key') }
|
|
1814
1906
|
const base = String(model.baseUrl || '').replace(/\/+$/, '')
|
|
1815
1907
|
const url = String(q.url).replace('{base}', base)
|
|
1908
|
+
// v761(安全修复 S1):额度 URL 虽来自模板,但 `{base}` 会被用户字段改写,同样要校验目的地
|
|
1909
|
+
const k = await keyForDestination(model, tpl, url)
|
|
1910
|
+
if (!k.key) return { ok: false, code: k.code, error: k.error }
|
|
1911
|
+
const key = k.key
|
|
1816
1912
|
try {
|
|
1817
1913
|
const data = await apiFetchJson(url, q.auth, key)
|
|
1818
1914
|
const bizErr = apiBusinessError(data)
|
|
@@ -1899,14 +1995,12 @@ export default {
|
|
|
1899
1995
|
const tpl = apiTemplateOf(model.provider) || {}
|
|
1900
1996
|
const b = mergeNonEmpty(tpl.balance, model.balance) // v724:模型侧空串不再覆盖模板
|
|
1901
1997
|
if (!b.url) return { ok: false, code: 'NO_URL', error: '未配置余额接口地址' }
|
|
1902
|
-
let key = ''
|
|
1903
|
-
try {
|
|
1904
|
-
const cred = await ctx.credentials.resolve(model.keyRef || tpl.keyRef || '')
|
|
1905
|
-
key = cred && cred.value ? String(cred.value) : ''
|
|
1906
|
-
} catch (err) { key = '' }
|
|
1907
|
-
if (!key) return { ok: false, code: 'NO_KEY', error: '未配置 ' + (model.keyRef || tpl.keyRef || 'API key') }
|
|
1908
1998
|
const base = String(model.baseUrl || '').replace(/\/+$/, '')
|
|
1909
1999
|
const url = String(b.url).replace('{base}', base)
|
|
2000
|
+
// v761(安全修复 S1):凭据只在目的地可信(厂商内置端点 / 本机确认过的自定义地址)时才带上
|
|
2001
|
+
const k = await keyForDestination(model, tpl, url)
|
|
2002
|
+
if (!k.key) return { ok: false, code: k.code, error: k.error }
|
|
2003
|
+
const key = k.key
|
|
1910
2004
|
try {
|
|
1911
2005
|
const data = await apiFetchJson(url, b.auth, key)
|
|
1912
2006
|
const j = b.json || {}
|
|
@@ -1918,9 +2012,13 @@ export default {
|
|
|
1918
2012
|
if (j.used) { const v = apiNum(pickJsonPath(data, j.used), j.scale); if (v !== null) used += v }
|
|
1919
2013
|
if (b.usage && b.usage.url) {
|
|
1920
2014
|
const u = b.usage
|
|
1921
|
-
|
|
2015
|
+
// v761(安全修复 S1):usage 是可选增强 —— 目的地不可信就跳过,绝不把凭据发出去
|
|
2016
|
+
const usageUrl = String(u.url).replace('{base}', base)
|
|
2017
|
+
if (credentialMayGoTo(model, tpl, usageUrl)) {
|
|
2018
|
+
const d2 = await apiFetchJson(usageUrl, u.auth || b.auth, key)
|
|
1922
2019
|
const j2 = u.json || {}
|
|
1923
2020
|
if (j2.used) { const v = apiNum(pickJsonPath(d2, j2.used), j2.scale); if (v !== null) used += v }
|
|
2021
|
+
} else { warnCredDestinationOnce(usageUrl) }
|
|
1924
2022
|
}
|
|
1925
2023
|
if (remaining === null && total !== null) remaining = total - used
|
|
1926
2024
|
if (remaining === null) return { ok: false, code: 'SHAPE', error: '接口返回里没找到配置的字段(检查 JSON 路径)' }
|
|
@@ -2124,6 +2222,8 @@ export default {
|
|
|
2124
2222
|
if (!tpl) return { ok: false, error: '未知的厂商模板' }
|
|
2125
2223
|
const name = String(p.name || '').trim().slice(0, 30) || tpl.name
|
|
2126
2224
|
const keyRef = String(p.keyRef || tpl.keyRef || '').trim().slice(0, 64) || tpl.keyRef
|
|
2225
|
+
// v761(安全修复 S1):凭据名加白名单 —— 原实现接受任意字符串,理论上能写到别的键名上
|
|
2226
|
+
if (!/^[A-Za-z0-9_]{1,64}$/.test(keyRef)) return { ok: false, error: '凭据名只能包含字母、数字与下划线(≤64 字符)' }
|
|
2127
2227
|
const id = p.id ? String(p.id) : ('api_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7))
|
|
2128
2228
|
const reg = readApiRegistry()
|
|
2129
2229
|
let model = reg.models.find((m) => m && m.id === id)
|
|
@@ -2135,6 +2235,11 @@ export default {
|
|
|
2135
2235
|
model.provider = p.provider
|
|
2136
2236
|
model.currency = String(p.currency || tpl.currency || 'CNY').toUpperCase().slice(0, 8)
|
|
2137
2237
|
model.keyRef = keyRef
|
|
2238
|
+
// v761(安全修复 S1):**允许把凭据发往自定义地址**(自建/自托管网关需要)。
|
|
2239
|
+
// 该标志是"凭据目的地白名单"的唯一例外,而所有写请求都被限制为**本机来源**(见 writeRejection),
|
|
2240
|
+
// 所以远端会话即使能改配置,也无法给自己开这个口子。缺省不动(前端局部更新不会误抹)。
|
|
2241
|
+
if (p.allowCustomHost === true) model.allowCustomHost = true
|
|
2242
|
+
else if (p.allowCustomHost === false) delete model.allowCustomHost
|
|
2138
2243
|
if (p.baseUrl !== undefined) model.baseUrl = String(p.baseUrl || '').slice(0, 300)
|
|
2139
2244
|
if (p.balance && typeof p.balance === 'object') {
|
|
2140
2245
|
// v724:空字段不写入(stripEmptyDeep)→ 模板默认值继续生效;
|
|
@@ -2214,6 +2319,17 @@ export default {
|
|
|
2214
2319
|
let entry = {
|
|
2215
2320
|
id: m.id, name: m.name, provider: m.provider, currency: m.currency,
|
|
2216
2321
|
keyRef: m.keyRef, builtin: !!m.builtin, baseUrl: m.baseUrl || '',
|
|
2322
|
+
// v761(安全修复 S1):该模型当前的目的地拿不到凭据(非厂商内置端点且未在本机确认过),
|
|
2323
|
+
// 前端据此提示用户"要去本机面板确认",而不是让余额莫名其妙地空着。
|
|
2324
|
+
needsHostConfirm: (() => {
|
|
2325
|
+
try {
|
|
2326
|
+
const tpl2 = apiTemplateOf(m.provider) || {}
|
|
2327
|
+
const b2 = mergeNonEmpty(tpl2.balance, m.balance) || {}
|
|
2328
|
+
const base2 = String(m.baseUrl || "").replace(/\/+$/, "")
|
|
2329
|
+
const u2 = String(tpl2.probeUrl || b2.url || "").replace("{base}", base2)
|
|
2330
|
+
return !!u2 && !credentialMayGoTo(m, tpl2, u2)
|
|
2331
|
+
} catch (err) { return false }
|
|
2332
|
+
})(),
|
|
2217
2333
|
canAdjustBalance: canAdjustBuiltinBalance(m),
|
|
2218
2334
|
matchIds: m.matchIds || [], settings: settings.models && settings.models[m.id] ? settings.models[m.id] : null,
|
|
2219
2335
|
price: m.price || null,
|
|
@@ -2353,7 +2469,7 @@ export default {
|
|
|
2353
2469
|
const summary = daySummary(led, todayKey())
|
|
2354
2470
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
2355
2471
|
return {
|
|
2356
|
-
...visible, version: '0.3.
|
|
2472
|
+
...visible, version: '0.3.15', isPeak: isPeakTime(nowSec),
|
|
2357
2473
|
// 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
|
|
2358
2474
|
peakNextChangeAt: nextPeakChangeAt(nowSec),
|
|
2359
2475
|
peakHolidays: HOLIDAY_VALLEY_LIST,
|
package/package.json
CHANGED