dsh-superinput 0.7.0 → 0.12.1
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 +265 -0
- package/README.md +18 -7
- package/client.js +2356 -251
- package/modules/compaction.js +413 -0
- package/modules/cost.js +571 -3
- package/modules/prompts.client.js +1068 -57
- package/modules/prompts.js +65 -0
- package/package.json +2 -3
- package/scripts/build-suite.mjs +14 -4
- package/scripts/smoke.mjs +54 -3
- package/server.js +1057 -3
package/modules/cost.js
CHANGED
|
@@ -695,6 +695,465 @@ export const inject = []
|
|
|
695
695
|
/** 官方余额查询:host 白名单(安全约束——只允许官方域名,内网/环回不可能出现)。 */
|
|
696
696
|
const BALANCE_HOST = 'api.deepseek.com'
|
|
697
697
|
|
|
698
|
+
/* ------------------------------------------------------------------ */
|
|
699
|
+
/* Coding Plan 订阅配额(对标 dsh-cost-meter lib/coding-plans.js 的 */
|
|
700
|
+
/* Bearer 子集:端点/解析/降级链照抄上游,全部官方域名硬编码白名单) */
|
|
701
|
+
/* ------------------------------------------------------------------ */
|
|
702
|
+
|
|
703
|
+
/** 0-100 已用百分比(保留 1 位)。 */
|
|
704
|
+
const clampPct = (p) => Math.max(0, Math.min(100, Math.round(p * 10) / 10))
|
|
705
|
+
|
|
706
|
+
/** 归一化百分比:0-1 视为小数,>=1 视为已是百分数;非法 → null。 */
|
|
707
|
+
function normalizePercent(value) {
|
|
708
|
+
const n = Number(value)
|
|
709
|
+
if (!Number.isFinite(n) || n < 0) return null
|
|
710
|
+
return clampPct(n <= 1 ? n * 100 : n)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** 归一化重置时刻:unix 秒 / 毫秒 / ISO 字符串 → ISO 字符串;非法 → ''。 */
|
|
714
|
+
function normalizeResetAt(value) {
|
|
715
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
716
|
+
const ms = Date.parse(value)
|
|
717
|
+
if (Number.isFinite(ms)) return new Date(ms).toISOString()
|
|
718
|
+
const asNum = Number(value)
|
|
719
|
+
if (Number.isFinite(asNum) && asNum > 0) return new Date(asNum > 1e12 ? asNum : asNum * 1000).toISOString()
|
|
720
|
+
return ''
|
|
721
|
+
}
|
|
722
|
+
const n = Number(value)
|
|
723
|
+
if (!Number.isFinite(n) || n <= 0) return ''
|
|
724
|
+
return new Date(n > 1e12 ? n : n * 1000).toISOString()
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** 组装文本窗口(余额等无百分比的量):text 空 → null。 */
|
|
728
|
+
const textWindowOf = (text) => {
|
|
729
|
+
const s = String(text ?? '').trim()
|
|
730
|
+
return s.length > 0 ? { resetsAt: '', text: s } : null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** 单窗口:utilization(0-100 或 0-1)+ resets(unix 秒/毫秒/ISO)。 */
|
|
734
|
+
function windowOf(utilization, resetsAt) {
|
|
735
|
+
const percent = normalizePercent(utilization)
|
|
736
|
+
return percent === null ? null : { percent, resetsAt: normalizeResetAt(resetsAt) }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** Anthropic:{ five_hour: { utilization, resets_at }, seven_day: {...}, ... };子配额窗口丢弃。 */
|
|
740
|
+
function parseAnthropicUsage(data) {
|
|
741
|
+
if (data === null || typeof data !== 'object') return null
|
|
742
|
+
const windows = {}
|
|
743
|
+
for (const [name, raw] of Object.entries(data)) {
|
|
744
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) continue
|
|
745
|
+
if (/^(five[_ -]?hour|seven[_ -]?day)[_ -]/i.test(name)) continue
|
|
746
|
+
if (name === 'iguana_necktie') continue
|
|
747
|
+
const win = windowOf(raw.utilization ?? raw.used_percentage, raw.resets_at ?? raw.reset_at)
|
|
748
|
+
if (win !== null) windows[name] = win
|
|
749
|
+
}
|
|
750
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Z.ai / 智谱 GLM:monitor 端点 limits 数组(unit 3=小时档/6=周档)→ plans 数组 → 扁平窗口。 */
|
|
754
|
+
function parseZaiUsage(data) {
|
|
755
|
+
if (data === null || typeof data !== 'object') return null
|
|
756
|
+
const windows = {}
|
|
757
|
+
const limits = data.data !== null && typeof data.data === 'object' && Array.isArray(data.data.limits)
|
|
758
|
+
? data.data.limits : null
|
|
759
|
+
if (limits !== null) {
|
|
760
|
+
const monWindows = {}
|
|
761
|
+
const rest = []
|
|
762
|
+
for (const limit of limits) {
|
|
763
|
+
if (limit === null || typeof limit !== 'object' || (limit.type !== 'TOKENS_LIMIT' && limit.type !== 'CREDIT_LIMIT')) continue
|
|
764
|
+
const pct = limit.percentage !== undefined ? clampPct(Number(limit.percentage))
|
|
765
|
+
: Number.isFinite(Number(limit.usage)) && Number(limit.usage) > 0 && Number.isFinite(Number(limit.currentValue))
|
|
766
|
+
? clampPct((Number(limit.currentValue) / Number(limit.usage)) * 100)
|
|
767
|
+
: null
|
|
768
|
+
if (pct === null) continue
|
|
769
|
+
const resetsAt = normalizeResetAt(limit.nextResetTime)
|
|
770
|
+
const unit = Number(limit.unit)
|
|
771
|
+
if (unit === 3 && monWindows.fiveHour === undefined) monWindows.fiveHour = { percent: pct, resetsAt }
|
|
772
|
+
else if (unit === 6 && monWindows.weekly === undefined) monWindows.weekly = { percent: pct, resetsAt }
|
|
773
|
+
else if (!Number.isFinite(unit)) rest.push({ pct, resetsAt, resetMs: Number(limit.nextResetTime) })
|
|
774
|
+
}
|
|
775
|
+
if (rest.length > 0) {
|
|
776
|
+
rest.sort((a, b) => {
|
|
777
|
+
const av = Number.isFinite(a.resetMs) && a.resetMs > 0 ? a.resetMs : 0
|
|
778
|
+
const bv = Number.isFinite(b.resetMs) && b.resetMs > 0 ? b.resetMs : 0
|
|
779
|
+
return av - bv
|
|
780
|
+
})
|
|
781
|
+
for (const item of rest) {
|
|
782
|
+
if (monWindows.fiveHour === undefined) monWindows.fiveHour = { percent: item.pct, resetsAt: item.resetsAt }
|
|
783
|
+
else if (monWindows.weekly === undefined) monWindows.weekly = { percent: item.pct, resetsAt: item.resetsAt }
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (Object.keys(monWindows).length > 0) return monWindows
|
|
787
|
+
}
|
|
788
|
+
if (Array.isArray(data.plans)) {
|
|
789
|
+
for (const plan of data.plans) {
|
|
790
|
+
if (plan === null || typeof plan !== 'object') continue
|
|
791
|
+
const total = Number(plan.total_units)
|
|
792
|
+
const used = Number(plan.used_units)
|
|
793
|
+
const pct = Number.isFinite(total) && total > 0 && Number.isFinite(used)
|
|
794
|
+
? clampPct((used / total) * 100)
|
|
795
|
+
: normalizePercent(plan.utilization ?? plan.percent ?? plan.used_percentage)
|
|
796
|
+
if (pct === null) continue
|
|
797
|
+
const spanMs = Number(plan.period_end) * 1000 - Date.now()
|
|
798
|
+
const key = Number.isFinite(spanMs) && spanMs > 24 * 3600_000 ? 'weekly' : 'fiveHour'
|
|
799
|
+
windows[key] = { percent: pct, resetsAt: normalizeResetAt(plan.period_end) }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
for (const [name, raw] of Object.entries(data)) {
|
|
803
|
+
if (name === 'plans' || raw === null || typeof raw !== 'object' || Array.isArray(raw)) continue
|
|
804
|
+
const win = windowOf(raw.utilization ?? raw.percent ?? raw.used_percentage, raw.resets_at ?? raw.reset_at ?? raw.resetsAt)
|
|
805
|
+
if (win !== null) windows[name] = win
|
|
806
|
+
}
|
|
807
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** MiniMax 单条记录的剩余百分比(优先 *_remaining_percent,total 常为 0)。 */
|
|
811
|
+
function remainingPercentOf(row, remainPctKey, totalKey, usedKey, remainKey) {
|
|
812
|
+
if (row === null || typeof row !== 'object') return null
|
|
813
|
+
const numeric = (key) => (row[key] == null || row[key] === '' ? NaN : Number(row[key]))
|
|
814
|
+
const rp = numeric(remainPctKey)
|
|
815
|
+
if (Number.isFinite(rp)) return Math.min(100, Math.max(0, rp <= 1 ? rp * 100 : rp))
|
|
816
|
+
const total = numeric(totalKey)
|
|
817
|
+
const used = numeric(usedKey)
|
|
818
|
+
const remain = numeric(remainKey)
|
|
819
|
+
if (Number.isFinite(total) && total > 0) {
|
|
820
|
+
if (Number.isFinite(remain)) return (remain / total) * 100
|
|
821
|
+
if (Number.isFinite(used)) return ((total - used) / total) * 100
|
|
822
|
+
}
|
|
823
|
+
return null
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** MiniMax 单条记录 → 5h/7d 窗口;status=3 为不限量,保留显式 ∞。 */
|
|
827
|
+
function windowsFromMiniMaxRecord(row) {
|
|
828
|
+
if (row === null || typeof row !== 'object') return {}
|
|
829
|
+
const windows = {}
|
|
830
|
+
if (Number(row.current_interval_status) === 3) {
|
|
831
|
+
windows['5h'] = { unlimited: true, text: '∞', resetsAt: '' }
|
|
832
|
+
} else {
|
|
833
|
+
const remain = remainingPercentOf(row, 'current_interval_remaining_percent', 'current_interval_total_count', 'current_interval_usage_count', 'current_interval_remain_count')
|
|
834
|
+
if (remain !== null) {
|
|
835
|
+
windows['5h'] = { percent: clampPct(100 - remain), resetsAt: normalizeResetAt(row.end_time ?? row.reset_time ?? row.next_reset_time) }
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (Number(row.current_weekly_status) === 3) {
|
|
839
|
+
windows['7d'] = { unlimited: true, text: '∞', resetsAt: '' }
|
|
840
|
+
} else {
|
|
841
|
+
const remain = remainingPercentOf(row, 'current_weekly_remaining_percent', 'current_weekly_total_count', 'current_weekly_usage_count', 'current_weekly_remain_count')
|
|
842
|
+
if (remain !== null) {
|
|
843
|
+
windows['7d'] = { percent: clampPct(100 - remain), resetsAt: normalizeResetAt(row.weekly_end_time) }
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return windows
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** 选 MiniMax 通用额度行:general → MiniMax-M* → 第一条可解析行。 */
|
|
850
|
+
function pickMiniMaxModelRow(rows) {
|
|
851
|
+
const list = rows.filter((row) => row !== null && typeof row === 'object' && !/^(video|speech|audio|image)(?:$|[-_])/i.test(String(row.model_name ?? '')))
|
|
852
|
+
const byName = (name) => list.find((row) => String(row.model_name ?? '').toLowerCase() === name)
|
|
853
|
+
return byName('general')
|
|
854
|
+
?? list.find((row) => /^minimax-m/i.test(String(row.model_name ?? '')))
|
|
855
|
+
?? list.find((row) => Object.keys(windowsFromMiniMaxRecord(row)).length > 0)
|
|
856
|
+
?? list[0]
|
|
857
|
+
?? null
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** MiniMax:token_plan/remains(现行 model_remains)→ 平铺 → 旧数组形态。 */
|
|
861
|
+
function parseMiniMaxRemains(data) {
|
|
862
|
+
if (data === null || typeof data !== 'object') return null
|
|
863
|
+
const payload = data.data !== null && typeof data.data === 'object' && !Array.isArray(data.data) ? data.data : data
|
|
864
|
+
const pickArray = (...keys) => {
|
|
865
|
+
for (const key of keys) {
|
|
866
|
+
if (Array.isArray(data?.[key])) return data[key]
|
|
867
|
+
if (Array.isArray(data?.data?.[key])) return data.data[key]
|
|
868
|
+
}
|
|
869
|
+
return null
|
|
870
|
+
}
|
|
871
|
+
const modelRows = pickArray('model_remains')
|
|
872
|
+
if (modelRows !== null) {
|
|
873
|
+
const fromRow = windowsFromMiniMaxRecord(pickMiniMaxModelRow(modelRows))
|
|
874
|
+
if (Object.keys(fromRow).length > 0) return fromRow
|
|
875
|
+
// 旧计数制:无 remaining_percent、靠 total>0 汇总。
|
|
876
|
+
let total = 0
|
|
877
|
+
let used = 0
|
|
878
|
+
let found = false
|
|
879
|
+
for (const item of modelRows) {
|
|
880
|
+
if (item === null || typeof item !== 'object') continue
|
|
881
|
+
const t = Number(item.current_interval_total_count ?? item.total)
|
|
882
|
+
const u = Number(item.current_interval_usage_count ?? item.used)
|
|
883
|
+
if (!Number.isFinite(t) || t <= 0) continue
|
|
884
|
+
found = true
|
|
885
|
+
total += t
|
|
886
|
+
used += Number.isFinite(u) ? u : 0
|
|
887
|
+
}
|
|
888
|
+
if (found && total > 0) return { current: { percent: clampPct((used / total) * 100), resetsAt: '' } }
|
|
889
|
+
}
|
|
890
|
+
const flat = windowsFromMiniMaxRecord(payload)
|
|
891
|
+
if (Object.keys(flat).length > 0) return flat
|
|
892
|
+
const windows = {}
|
|
893
|
+
const planRows = pickArray('token_plan_remains', 'plan_remains', 'remains', 'windows')
|
|
894
|
+
if (planRows !== null) {
|
|
895
|
+
planRows.forEach((row, index) => {
|
|
896
|
+
if (row === null || typeof row !== 'object') return
|
|
897
|
+
const total = Number(row.current_interval_total_count ?? row.total_count ?? row.total ?? row.limit)
|
|
898
|
+
const used = Number(row.current_interval_usage_count ?? row.used_count ?? row.usage_count ?? row.used)
|
|
899
|
+
const remain = Number(row.current_interval_remain_count ?? row.remain_count ?? row.remain ?? row.remaining)
|
|
900
|
+
let pct = null
|
|
901
|
+
if (Number.isFinite(total) && total > 0 && Number.isFinite(used)) pct = (used / total) * 100
|
|
902
|
+
else if (Number.isFinite(total) && total > 0 && Number.isFinite(remain)) pct = ((total - remain) / total) * 100
|
|
903
|
+
else pct = normalizePercent(row.utilization ?? row.percent ?? row.used_percentage)
|
|
904
|
+
if (pct === null) return
|
|
905
|
+
const labelRaw = row.interval ?? row.interval_type ?? row.window_type ?? row.type ?? row.name
|
|
906
|
+
const label = typeof labelRaw === 'string' && labelRaw.length > 0 ? labelRaw : `window${index + 1}`
|
|
907
|
+
windows[label] = { percent: clampPct(pct), resetsAt: normalizeResetAt(row.reset_time ?? row.resets_at ?? row.next_reset_time ?? row.reset_at) }
|
|
908
|
+
})
|
|
909
|
+
}
|
|
910
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Kimi Code 订阅配额(api.kimi.com/coding/v1/usages,Kimi CLI 同款接口)。 */
|
|
914
|
+
function parseKimiCodingUsage(data) {
|
|
915
|
+
if (data === null || typeof data !== 'object') return null
|
|
916
|
+
const payload = data.data && typeof data.data === 'object' ? data.data : data
|
|
917
|
+
const quota = payload.quota && typeof payload.quota === 'object' ? payload.quota : payload
|
|
918
|
+
const windows = {}
|
|
919
|
+
const numeric = (value) => (value == null || typeof value === 'boolean' || String(value).trim() === '' ? NaN : Number(value))
|
|
920
|
+
const counter = (row) => {
|
|
921
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return null
|
|
922
|
+
const total = numeric(row.limit)
|
|
923
|
+
const used = numeric(row.used)
|
|
924
|
+
const remaining = numeric(row.remaining)
|
|
925
|
+
const pct = total > 0 && Number.isFinite(total)
|
|
926
|
+
? Number.isFinite(used) && used >= 0 ? (used / total) * 100
|
|
927
|
+
: Number.isFinite(remaining) && remaining >= 0 ? ((total - remaining) / total) * 100 : NaN
|
|
928
|
+
: NaN
|
|
929
|
+
return Number.isFinite(pct) ? { percent: clampPct(pct), resetsAt: normalizeResetAt(row.resetTime ?? row.reset_time ?? row.resetAt ?? row.resetsAt) } : null
|
|
930
|
+
}
|
|
931
|
+
const usages = quota.usages ?? {}
|
|
932
|
+
const shapes = [['5h', ['limit_5h', 'limit5h']], ['weekly', ['limit_7d', 'limit7d']], ['monthly', ['limit_month_total', 'limitMonthTotal']]]
|
|
933
|
+
for (const [name, aliases] of shapes) {
|
|
934
|
+
const raw = aliases.map((key) => usages[key]).find((row) => row && typeof row === 'object' && !Array.isArray(row))
|
|
935
|
+
if (!raw) continue
|
|
936
|
+
const counted = counter(raw)
|
|
937
|
+
const ratio = numeric(raw.usedRatio ?? raw.used_ratio)
|
|
938
|
+
if (counted) windows[name] = counted
|
|
939
|
+
else if (Number.isFinite(ratio) && ratio >= 0 && ratio <= 1) {
|
|
940
|
+
windows[name] = { percent: clampPct(ratio * 100), resetsAt: normalizeResetAt(raw.resetTime ?? raw.reset_time ?? raw.resetAt) }
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const usage = counter(quota.usage)
|
|
944
|
+
if (usage && (!Object.keys(usages).length || usages.limit_7d || usages.limit7d)) windows.weekly = usage
|
|
945
|
+
const limits = Array.isArray(quota.limits) ? quota.limits : []
|
|
946
|
+
limits.forEach((row, index) => {
|
|
947
|
+
const parsed = counter(row?.detail)
|
|
948
|
+
if (!parsed) return
|
|
949
|
+
const duration = numeric(row.window?.duration)
|
|
950
|
+
const unitRaw = String(row.window?.timeUnit ?? '').toLowerCase().replace(/^time_unit_/, '')
|
|
951
|
+
const unit = unitRaw.startsWith('hour') ? 'h' : unitRaw.startsWith('day') ? 'd'
|
|
952
|
+
: unitRaw.startsWith('week') ? 'w' : unitRaw.startsWith('minute') ? 'm'
|
|
953
|
+
: unitRaw.startsWith('month') ? 'mo' : unitRaw.slice(0, 4)
|
|
954
|
+
const name = unit === 'm' && duration === 300 ? '5h'
|
|
955
|
+
: Number.isFinite(duration) && duration > 0 && unit.length > 0 ? `${duration}${unit}` : `window${index + 1}`
|
|
956
|
+
windows[name] = parsed
|
|
957
|
+
})
|
|
958
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Kimi / Moonshot PAYG 余额(人民币分;<100 视为已是元)。 */
|
|
962
|
+
function parseKimiBalance(data) {
|
|
963
|
+
if (data === null || typeof data !== 'object') return null
|
|
964
|
+
const raw = data.available_balance ?? data.balance ?? data.cash_balance ?? data.data?.available_balance
|
|
965
|
+
const n = Number(raw)
|
|
966
|
+
if (!Number.isFinite(n) || n < 0) return null
|
|
967
|
+
const cny = n >= 100 ? n / 100 : n
|
|
968
|
+
const win = textWindowOf(`¥${(Math.round(cny * 100) / 100).toFixed(2)}`)
|
|
969
|
+
return win === null ? null : { balance: win }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** OpenRouter:{ data: { total_credits, total_usage } }(美元)→ 已用%。 */
|
|
973
|
+
function parseOpenRouterCredits(data) {
|
|
974
|
+
if (data === null || typeof data !== 'object') return null
|
|
975
|
+
const d = data.data !== null && typeof data.data === 'object' ? data.data : data
|
|
976
|
+
const total = Number(d.total_credits ?? d.credits)
|
|
977
|
+
const used = Number(d.total_usage ?? d.usage)
|
|
978
|
+
if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(used)) return null
|
|
979
|
+
return { credits: { percent: clampPct((used / total) * 100), resetsAt: normalizeResetAt(d.resets_at ?? d.next_reset_time) } }
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** SiliconFlow:balance/amount/remain(人民币)→ 文本窗口。 */
|
|
983
|
+
function parseSiliconFlowInfo(data) {
|
|
984
|
+
if (data === null || typeof data !== 'object') return null
|
|
985
|
+
const d = data.data !== null && typeof data.data === 'object' ? data.data : data
|
|
986
|
+
const n = Number(d.balance ?? d.amount ?? d.remain ?? d.remaining)
|
|
987
|
+
if (!Number.isFinite(n) || n < 0) return null
|
|
988
|
+
const win = textWindowOf(`¥${(Math.round(n * 100) / 100).toFixed(2)}`)
|
|
989
|
+
return win === null ? null : { balance: win }
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** CommandCode:windowLimits(used/cap)+ 月度 Credits 余额文本。 */
|
|
993
|
+
function parseCommandCodeCredits(data) {
|
|
994
|
+
if (data === null || typeof data !== 'object') return null
|
|
995
|
+
const windows = {}
|
|
996
|
+
const limits = data.windowLimits
|
|
997
|
+
if (limits !== null && typeof limits === 'object' && !Array.isArray(limits)) {
|
|
998
|
+
for (const [name, raw] of Object.entries(limits)) {
|
|
999
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) continue
|
|
1000
|
+
const used = Number(raw.used)
|
|
1001
|
+
const cap = Number(raw.cap)
|
|
1002
|
+
if (!Number.isFinite(used) || !Number.isFinite(cap) || cap <= 0 || used < 0) continue
|
|
1003
|
+
windows[name] = { percent: clampPct((used / cap) * 100), resetsAt: normalizeResetAt(raw.resetAt) }
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const monthly = Number(data.credits !== null && typeof data.credits === 'object' ? data.credits.monthlyCredits : undefined)
|
|
1007
|
+
if (Number.isFinite(monthly) && monthly >= 0) {
|
|
1008
|
+
const win = textWindowOf(`$${(Math.round(monthly * 100) / 100).toFixed(2)}`)
|
|
1009
|
+
if (win !== null) windows.monthly = win
|
|
1010
|
+
}
|
|
1011
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** Cline Pass:limits[].percentUsed 已是 0-100 已用百分比,不走 0-1 猜测。 */
|
|
1015
|
+
function parseClinePassUsage(data) {
|
|
1016
|
+
if (data === null || typeof data !== 'object') return null
|
|
1017
|
+
const limits = data.data !== null && typeof data.data === 'object' ? data.data.limits : undefined
|
|
1018
|
+
if (!Array.isArray(limits)) return null
|
|
1019
|
+
const names = new Map([['five_hour', 'fiveHour'], ['weekly', 'weekly'], ['monthly', 'monthly']])
|
|
1020
|
+
const windows = {}
|
|
1021
|
+
for (const entry of limits) {
|
|
1022
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue
|
|
1023
|
+
const name = names.get(entry.type)
|
|
1024
|
+
const raw = entry.percentUsed
|
|
1025
|
+
const percent = typeof raw === 'number'
|
|
1026
|
+
? raw
|
|
1027
|
+
: typeof raw === 'string' && /^[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/.test(raw.trim()) ? Number(raw) : Number.NaN
|
|
1028
|
+
if (name === undefined || !Number.isFinite(percent) || percent < 0) continue
|
|
1029
|
+
windows[name] = { percent: clampPct(percent), resetsAt: normalizeResetAt(entry.resetsAt) }
|
|
1030
|
+
}
|
|
1031
|
+
return Object.keys(windows).length > 0 ? windows : null
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const CODING_PLAN_PARSERS = {
|
|
1035
|
+
anthropic: parseAnthropicUsage,
|
|
1036
|
+
zai: parseZaiUsage,
|
|
1037
|
+
minimax: parseMiniMaxRemains,
|
|
1038
|
+
openrouter: parseOpenRouterCredits,
|
|
1039
|
+
siliconflow: parseSiliconFlowInfo,
|
|
1040
|
+
commandcode: parseCommandCodeCredits,
|
|
1041
|
+
clinepass: parseClinePassUsage,
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* 官方端点白名单(硬编码,配置不可改;MiniMax 是唯一例外——允许显式 HTTPS
|
|
1046
|
+
* origin 覆盖,且经 normalizeMiniMaxOrigin 校验为公网域名)。顺序即降级链:
|
|
1047
|
+
* 认证失败(401/403)与解析成功立即返回,其余错误换下一端点。
|
|
1048
|
+
*/
|
|
1049
|
+
const CODING_PLAN_ENDPOINTS = {
|
|
1050
|
+
anthropic: ['https://api.anthropic.com/api/oauth/usage'],
|
|
1051
|
+
zai: [
|
|
1052
|
+
'https://open.bigmodel.cn/api/monitor/usage/quota/limit',
|
|
1053
|
+
'https://api.z.ai/api/monitor/usage/quota/limit',
|
|
1054
|
+
'https://api.z.ai/api/coding/paas/v3/dashboard/billing/coding_plan/usage',
|
|
1055
|
+
'https://open.bigmodel.cn/api/coding/paas/v3/dashboard/billing/coding_plan/usage',
|
|
1056
|
+
],
|
|
1057
|
+
minimax: [
|
|
1058
|
+
'https://www.minimax.cn/v1/token_plan/remains',
|
|
1059
|
+
'https://www.minimax.io/v1/token_plan/remains',
|
|
1060
|
+
'https://www.minimaxi.com/v1/token_plan/remains',
|
|
1061
|
+
],
|
|
1062
|
+
kimi: [
|
|
1063
|
+
'https://api.kimi.com/coding/v1/usages',
|
|
1064
|
+
'https://api.kimi.com/coding/v1/usage',
|
|
1065
|
+
'https://api.moonshot.cn/v1/users/me/balance',
|
|
1066
|
+
],
|
|
1067
|
+
openrouter: ['https://openrouter.ai/api/v1/credits'],
|
|
1068
|
+
siliconflow: ['https://api.siliconflow.cn/v1/user/info'],
|
|
1069
|
+
commandcode: ['https://api.commandcode.ai/alpha/billing/credits'],
|
|
1070
|
+
clinepass: ['https://api.cline.bot/api/v1/users/me/plan/usage-limits'],
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const CODING_PLAN_IDS = Object.keys(CODING_PLAN_ENDPOINTS)
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* MiniMax 自定源归一化:只接受 HTTPS origin(无路径/凭据/空白),并拒绝
|
|
1077
|
+
* 环回/内网/链路本地/裸 IP —— SSRF 硬化比上游更严(上游只查 HTTPS origin)。
|
|
1078
|
+
*/
|
|
1079
|
+
function normalizeMiniMaxOrigin(value) {
|
|
1080
|
+
if (value === undefined || value === null || value === '') return ''
|
|
1081
|
+
if (typeof value !== 'string' || value.length > 2048) return null
|
|
1082
|
+
const text = value.trim()
|
|
1083
|
+
if (!text) return ''
|
|
1084
|
+
if (!/^https:\/\/[^/?#]+\/?$/i.test(text) || /[\s\\@?#]/.test(text)) return null
|
|
1085
|
+
let url
|
|
1086
|
+
try { url = new URL(text) } catch { return null }
|
|
1087
|
+
const host = url.hostname.toLowerCase()
|
|
1088
|
+
if (!host || url.username || url.password || url.pathname !== '/') return null
|
|
1089
|
+
if (host === '::1' || host.endsWith('.local') || host.endsWith('.internal')) return null
|
|
1090
|
+
if (/^(localhost|0\.|10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) return null
|
|
1091
|
+
if (/^\[?[0-9a-f:]+\]?$/i.test(host) && host.includes(':')) return null
|
|
1092
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return null
|
|
1093
|
+
return url.origin
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** 单端点抓取:Bearer 鉴权;Kimi 订阅端点须带官方 CLI 的 UA(上游实测约束)。 */
|
|
1097
|
+
async function fetchPlanJson(url, key, { kimiCoding = false, redirect } = {}) {
|
|
1098
|
+
const headers = {
|
|
1099
|
+
authorization: `Bearer ${key.trim()}`,
|
|
1100
|
+
accept: 'application/json',
|
|
1101
|
+
'user-agent': 'dsh-cost (DeepSeek Harness plugin)',
|
|
1102
|
+
}
|
|
1103
|
+
if (kimiCoding) headers['user-agent'] = 'KimiCLI/1.6'
|
|
1104
|
+
return fetch(url, { headers, redirect, signal: AbortSignal.timeout(15_000) })
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** 响应体有界 JSON(256KiB 上限,防异常端点拖内存)。 */
|
|
1108
|
+
async function readJsonBounded(res, cap = 262_144) {
|
|
1109
|
+
const text = await res.text()
|
|
1110
|
+
if (text.length > cap) throw new Error('response too large')
|
|
1111
|
+
return JSON.parse(text)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* 查询单家 Coding Plan 额度:按白名单端点顺序尝试。401/403 与解析成功立即
|
|
1116
|
+
* 定论,其余错误换下一端点;zai/minimax 双域 Key 不互通、kimi 订阅端点 401
|
|
1117
|
+
* 视为「非订阅 Key」继续降级到 PAYG 余额端点(上游 issue #42/#53 同语义)。
|
|
1118
|
+
* 全部错误均为 soft(未订阅/Key 无效属预期场景,面板中性提示)。
|
|
1119
|
+
*/
|
|
1120
|
+
async function queryCodingPlan(provider, key, origin) {
|
|
1121
|
+
const urls = provider === 'minimax'
|
|
1122
|
+
? (() => {
|
|
1123
|
+
const base = normalizeMiniMaxOrigin(origin)
|
|
1124
|
+
if (base === null) throw Object.assign(new Error('minimax: invalid HTTPS origin'), { soft: true })
|
|
1125
|
+
return base ? [`${base}/v1/token_plan/remains`, `${base}/v1/api/openplatform/coding_plan/remains`] : CODING_PLAN_ENDPOINTS.minimax
|
|
1126
|
+
})()
|
|
1127
|
+
: CODING_PLAN_ENDPOINTS[provider]
|
|
1128
|
+
let lastError = 'HTTP error'
|
|
1129
|
+
for (const url of urls) {
|
|
1130
|
+
const kimiCoding = provider === 'kimi' && new URL(url).hostname === 'api.kimi.com'
|
|
1131
|
+
let res
|
|
1132
|
+
try {
|
|
1133
|
+
res = await fetchPlanJson(url, key, {
|
|
1134
|
+
kimiCoding,
|
|
1135
|
+
redirect: provider === 'minimax' ? 'error' : provider === 'zai' ? 'manual' : undefined,
|
|
1136
|
+
})
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
lastError = String(error?.message ?? error).slice(0, 120)
|
|
1139
|
+
continue
|
|
1140
|
+
}
|
|
1141
|
+
if (res.status === 401 || res.status === 403) {
|
|
1142
|
+
lastError = `HTTP ${res.status}`
|
|
1143
|
+
if (provider === 'zai' || provider === 'minimax' || kimiCoding) continue
|
|
1144
|
+
throw Object.assign(new Error(lastError), { soft: true })
|
|
1145
|
+
}
|
|
1146
|
+
if (!res.ok) { lastError = `HTTP ${res.status}`; continue }
|
|
1147
|
+
let data = null
|
|
1148
|
+
try { data = await readJsonBounded(res) } catch { lastError = 'invalid JSON response'; continue }
|
|
1149
|
+
const windows = (kimiCoding ? parseKimiCodingUsage(data) : CODING_PLAN_PARSERS[provider](data))
|
|
1150
|
+
if (windows) return { windows, endpoint: url }
|
|
1151
|
+
lastError = 'no usage windows in response'
|
|
1152
|
+
}
|
|
1153
|
+
throw Object.assign(new Error(lastError), { soft: true })
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
|
|
698
1157
|
export const Config = Schema.object({
|
|
699
1158
|
enabled: Schema.boolean().default(true).description('是否启用费用统计'),
|
|
700
1159
|
/** 展示汇率(USD → CNY);金额账本恒存美元,展示时换算。 */
|
|
@@ -712,6 +1171,8 @@ export const Config = Schema.object({
|
|
|
712
1171
|
peakAlertNotify: Schema.boolean().default(false).description('同时发送系统通知'),
|
|
713
1172
|
/** DeepSeek 开放平台 API Key(选填):配置后费用弹卡显示官方账户余额。 */
|
|
714
1173
|
apiKey: Schema.string().default('').description('DeepSeek 开放平台 API Key(查官方余额用,可选)'),
|
|
1174
|
+
/** Coding Plan 订阅配额:{ 厂商: { key, origin? } },Key 在费用设置页配置、不回显。 */
|
|
1175
|
+
codingPlans: Schema.object({}).default({}).description('Coding Plan 订阅配额(在费用设置页配置)'),
|
|
715
1176
|
})
|
|
716
1177
|
|
|
717
1178
|
export function apply(ctx, config) {
|
|
@@ -723,7 +1184,7 @@ export function apply(ctx, config) {
|
|
|
723
1184
|
if (!live.enabled) return
|
|
724
1185
|
|
|
725
1186
|
// 设置持久化:只落与 config 不同的键,防抖 400ms 原子写。
|
|
726
|
-
const SETTINGS_KEYS = ['budgetEnabled', 'budgetAmount', 'budgetPeriod', 'peakAlertEnabled', 'peakAlertNotify', 'peakAlertLeadMinutes', 'exchangeRate', 'apiKey']
|
|
1187
|
+
const SETTINGS_KEYS = ['budgetEnabled', 'budgetAmount', 'budgetPeriod', 'peakAlertEnabled', 'peakAlertNotify', 'peakAlertLeadMinutes', 'exchangeRate', 'apiKey', 'codingPlans']
|
|
727
1188
|
const SETTINGS_PATH = join(homedir(), '.dsh', 'storages', 'dsh-cost', 'settings.json')
|
|
728
1189
|
try {
|
|
729
1190
|
const saved = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
|
|
@@ -745,9 +1206,38 @@ export function apply(ctx, config) {
|
|
|
745
1206
|
}, 400)
|
|
746
1207
|
}
|
|
747
1208
|
|
|
1209
|
+
/* -- Coding Plan 配额缓存与查询编排 -------------------------------- */
|
|
1210
|
+
|
|
1211
|
+
const PLAN_TTL_MS = 600_000
|
|
1212
|
+
const planCache = new Map() // provider → { at, result }
|
|
1213
|
+
const planKeyOf = (id) => {
|
|
1214
|
+
const entry = live.codingPlans?.[id]
|
|
1215
|
+
const key = typeof entry?.key === 'string' ? entry.key.trim() : ''
|
|
1216
|
+
return key.length > 0 && key.length <= 4096 ? key : ''
|
|
1217
|
+
}
|
|
1218
|
+
async function refreshCodingPlan(id, force = false) {
|
|
1219
|
+
const key = planKeyOf(id)
|
|
1220
|
+
if (!key) return { hasKey: false }
|
|
1221
|
+
const cached = planCache.get(id)
|
|
1222
|
+
if (!force && cached && Date.now() - cached.at < PLAN_TTL_MS) return cached.result
|
|
1223
|
+
let result
|
|
1224
|
+
try {
|
|
1225
|
+
const { windows } = await queryCodingPlan(id, key, live.codingPlans?.[id]?.origin)
|
|
1226
|
+
result = { hasKey: true, windows, at: Date.now() }
|
|
1227
|
+
void report(`coding plan ${id} refreshed: ${Object.keys(windows).join(',')}`)
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
result = { hasKey: true, error: String(error?.message ?? error).slice(0, 160), at: Date.now() }
|
|
1230
|
+
void report(`coding plan ${id} refresh failed: ${result.error}`)
|
|
1231
|
+
}
|
|
1232
|
+
planCache.set(id, { at: Date.now(), result })
|
|
1233
|
+
return result
|
|
1234
|
+
}
|
|
1235
|
+
|
|
748
1236
|
const lockPath = join(STORE_DIR, 'ledger.lock')
|
|
1237
|
+
// 存储目录可能不存在(全新安装):锁文件 openSync(wx) 要求父目录已建。
|
|
1238
|
+
try { mkdirSync(STORE_DIR, { recursive: true }) } catch { /* ignore */ }
|
|
749
1239
|
const lock = acquireLedgerLock(lockPath)
|
|
750
|
-
void report(`server boot v0.
|
|
1240
|
+
void report(`server boot v0.10.0 pid=${process.pid} ledgerOwner=${lock.owner} retention=${live.retentionDays} rate=${live.exchangeRate} budget=${live.budgetEnabled ? `${live.budgetAmount}/${live.budgetPeriod}` : 'off'} backfill=${live.backfillEnabled && lock.owner ? 'on' : 'off'} plans=${CODING_PLAN_IDS.filter((id) => planKeyOf(id)).length}`)
|
|
751
1241
|
|
|
752
1242
|
const ledger = createLedger(live.retentionDays)
|
|
753
1243
|
if (lock.owner) ledger.load()
|
|
@@ -1130,7 +1620,7 @@ export function apply(ctx, config) {
|
|
|
1130
1620
|
const daysLeft = daysInMonth - nowDate.getDate()
|
|
1131
1621
|
const projected = monthCost + avgDaily * daysLeft
|
|
1132
1622
|
|
|
1133
|
-
|
|
1623
|
+
send(200, {
|
|
1134
1624
|
monthCost,
|
|
1135
1625
|
monthCalls,
|
|
1136
1626
|
avgDaily,
|
|
@@ -1142,6 +1632,7 @@ export function apply(ctx, config) {
|
|
|
1142
1632
|
basis: n >= 3 ? 'recent' : 'insufficient',
|
|
1143
1633
|
recent,
|
|
1144
1634
|
})
|
|
1635
|
+
return
|
|
1145
1636
|
}
|
|
1146
1637
|
|
|
1147
1638
|
if (path === '/calls' && req.method === 'GET') {
|
|
@@ -1316,6 +1807,83 @@ export function apply(ctx, config) {
|
|
|
1316
1807
|
return
|
|
1317
1808
|
}
|
|
1318
1809
|
|
|
1810
|
+
// 热力图:近 N 天逐日 { cost, calls, tokens }(GitHub 贡献图样式)。
|
|
1811
|
+
if (path === '/heatmap' && req.method === 'GET') {
|
|
1812
|
+
const url = new URL(req.url ?? '/api/dsh-cost/heatmap', 'http://localhost')
|
|
1813
|
+
const days = Math.min(365, Math.max(28, Number(url.searchParams.get('days') ?? 182)))
|
|
1814
|
+
const snap = await ledgerSnapshotFresh()
|
|
1815
|
+
const since = localDayKey(Date.now() - days * 86400000)
|
|
1816
|
+
const rows = []
|
|
1817
|
+
for (const key of Object.keys(snap.days).sort()) {
|
|
1818
|
+
if (key < since) continue
|
|
1819
|
+
const day = snap.days[key]
|
|
1820
|
+
rows.push({
|
|
1821
|
+
date: key,
|
|
1822
|
+
cost: day.cost ?? 0,
|
|
1823
|
+
calls: day.calls ?? 0,
|
|
1824
|
+
tokens: (day.input ?? 0) + (day.output ?? 0) + (day.cacheRead ?? 0) + (day.cacheWrite ?? 0) + (day.reasoning ?? 0),
|
|
1825
|
+
})
|
|
1826
|
+
}
|
|
1827
|
+
send(200, { days: rows })
|
|
1828
|
+
return
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Coding Plan 配额:只下发窗口/错误/时间戳,Key 永不出服务端。
|
|
1832
|
+
const planState = () => {
|
|
1833
|
+
const providers = {}
|
|
1834
|
+
for (const id of CODING_PLAN_IDS) providers[id] = { hasKey: planKeyOf(id).length > 0 }
|
|
1835
|
+
for (const id of CODING_PLAN_IDS) {
|
|
1836
|
+
if (!providers[id].hasKey) continue
|
|
1837
|
+
const cached = planCache.get(id)
|
|
1838
|
+
if (cached) providers[id] = { ...providers[id], ...cached.result, stale: Date.now() - cached.at >= PLAN_TTL_MS }
|
|
1839
|
+
}
|
|
1840
|
+
return providers
|
|
1841
|
+
}
|
|
1842
|
+
if (path === '/coding-plans' && req.method === 'GET') {
|
|
1843
|
+
// 已配置且缓存过期的后台补刷:GET 不阻塞,下一轮轮询取到新值。
|
|
1844
|
+
for (const id of CODING_PLAN_IDS) {
|
|
1845
|
+
if (!planKeyOf(id)) continue
|
|
1846
|
+
const cached = planCache.get(id)
|
|
1847
|
+
if (!cached || Date.now() - cached.at >= PLAN_TTL_MS) void refreshCodingPlan(id)
|
|
1848
|
+
}
|
|
1849
|
+
send(200, { providers: planState() })
|
|
1850
|
+
return
|
|
1851
|
+
}
|
|
1852
|
+
if (path === '/coding-plans/refresh' && req.method === 'POST') {
|
|
1853
|
+
let raw = ''
|
|
1854
|
+
for await (const chunk of req) raw += chunk
|
|
1855
|
+
let body = {}
|
|
1856
|
+
try { body = raw ? JSON.parse(raw) : {} } catch { send(400, { error: 'invalid-json' }); return }
|
|
1857
|
+
const only = typeof body.provider === 'string' ? body.provider : ''
|
|
1858
|
+
const ids = only ? [only] : CODING_PLAN_IDS.filter((id) => planKeyOf(id))
|
|
1859
|
+
for (const id of ids) await refreshCodingPlan(id, true)
|
|
1860
|
+
send(200, { ok: true, providers: planState() })
|
|
1861
|
+
return
|
|
1862
|
+
}
|
|
1863
|
+
if (path === '/coding-plans/config' && req.method === 'POST') {
|
|
1864
|
+
let raw = ''
|
|
1865
|
+
for await (const chunk of req) raw += chunk
|
|
1866
|
+
let body
|
|
1867
|
+
try { body = JSON.parse(raw) } catch { send(400, { error: 'invalid-json' }); return }
|
|
1868
|
+
const id = typeof body.provider === 'string' ? body.provider : ''
|
|
1869
|
+
if (!CODING_PLAN_IDS.includes(id)) { send(400, { error: 'unknown-provider' }); return }
|
|
1870
|
+
const key = typeof body.key === 'string' ? body.key.trim().slice(0, 4096) : ''
|
|
1871
|
+
const merged = { ...(live.codingPlans ?? {}) }
|
|
1872
|
+
if (key) {
|
|
1873
|
+
const origin = normalizeMiniMaxOrigin(typeof body.origin === 'string' ? body.origin : undefined)
|
|
1874
|
+
if (origin === null) { send(400, { error: 'invalid-origin' }); return }
|
|
1875
|
+
merged[id] = origin ? { key, origin } : { key }
|
|
1876
|
+
} else {
|
|
1877
|
+
delete merged[id] // 空 key = 清除配置
|
|
1878
|
+
}
|
|
1879
|
+
live.codingPlans = merged
|
|
1880
|
+
planCache.delete(id)
|
|
1881
|
+
persistSettings()
|
|
1882
|
+
void report(`coding plan config updated: ${id} ${key ? 'set' : 'cleared'}`)
|
|
1883
|
+
send(200, { ok: true })
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1319
1887
|
send(404, { error: 'not-found' })
|
|
1320
1888
|
},
|
|
1321
1889
|
}), 'dsh-cost: /api/dsh-cost/*')
|