@buyi1net/pi-toolkit 0.0.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/LICENSE +21 -0
- package/README.md +45 -0
- package/assembler.ts +127 -0
- package/config.ts +201 -0
- package/i18n.ts +693 -0
- package/index.ts +99 -0
- package/menu/items.ts +264 -0
- package/menu/panels.ts +91 -0
- package/menu/settings-list.ts +84 -0
- package/menu/theme.ts +31 -0
- package/menu/toolkit-menu.ts +139 -0
- package/module.ts +156 -0
- package/modules/eyes/chain.ts +173 -0
- package/modules/eyes/config.ts +319 -0
- package/modules/eyes/index.ts +161 -0
- package/modules/eyes/menu.ts +828 -0
- package/modules/eyes/pi-model-backend.ts +245 -0
- package/modules/eyes/resilience.ts +161 -0
- package/modules/eyes/vision-bridge.ts +232 -0
- package/modules/eyes/vision-cache.ts +86 -0
- package/modules/eyes/vision-json.ts +19 -0
- package/modules/eyes/vision-preprocess.ts +192 -0
- package/modules/eyes/vision-probe.ts +21 -0
- package/modules/eyes/vision-prompt.ts +90 -0
- package/modules/eyes/vision-tool.ts +59 -0
- package/modules/eyes/vision-types.ts +33 -0
- package/modules/index.ts +10 -0
- package/modules/subagents/agents/researcher.md +51 -0
- package/modules/subagents/agents/scout.md +40 -0
- package/modules/subagents/agents/worker.md +79 -0
- package/modules/subagents/config.json.example +6 -0
- package/modules/subagents/config.ts +144 -0
- package/modules/subagents/index.ts +91 -0
- package/modules/subagents/menu.ts +388 -0
- package/modules/subagents/src/activity.ts +511 -0
- package/modules/subagents/src/agents.ts +126 -0
- package/modules/subagents/src/command.ts +37 -0
- package/modules/subagents/src/dependencies.ts +246 -0
- package/modules/subagents/src/diagnostics.ts +13 -0
- package/modules/subagents/src/display.ts +94 -0
- package/modules/subagents/src/headless.ts +342 -0
- package/modules/subagents/src/herdr.ts +203 -0
- package/modules/subagents/src/index.ts +2798 -0
- package/modules/subagents/src/inspect-tool.ts +839 -0
- package/modules/subagents/src/launch-config.ts +196 -0
- package/modules/subagents/src/layout-budget.ts +26 -0
- package/modules/subagents/src/list-tool.ts +292 -0
- package/modules/subagents/src/message-tool.ts +808 -0
- package/modules/subagents/src/names.ts +17 -0
- package/modules/subagents/src/pane-layout.ts +49 -0
- package/modules/subagents/src/params.ts +145 -0
- package/modules/subagents/src/renderers.ts +181 -0
- package/modules/subagents/src/result.ts +43 -0
- package/modules/subagents/src/retention.ts +114 -0
- package/modules/subagents/src/route-error.ts +212 -0
- package/modules/subagents/src/routing.ts +235 -0
- package/modules/subagents/src/runtime-registry.ts +159 -0
- package/modules/subagents/src/session.ts +801 -0
- package/modules/subagents/src/status.ts +513 -0
- package/modules/subagents/src/stop-tool.ts +235 -0
- package/modules/subagents/src/subagent-done.ts +590 -0
- package/modules/subagents/src/subagent-tool.ts +1155 -0
- package/modules/subagents/src/surface.ts +355 -0
- package/modules/subagents/src/team-dispatch-tool.ts +219 -0
- package/modules/subagents/src/team.ts +232 -0
- package/modules/subagents/src/tmux.ts +210 -0
- package/modules/subagents/src/tools/safe-bash.ts +72 -0
- package/modules/subagents/src/types.ts +131 -0
- package/modules/tui/adapter/provider-usage.ts +143 -0
- package/modules/tui/config.ts +50 -0
- package/modules/tui/index.ts +120 -0
- package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
- package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
- package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
- package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
- package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
- package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
- package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
- package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
- package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
- package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
- package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
- package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
- package/modules/tui/kernel/usage-core.ts +2 -0
- package/modules/tui/kernel/usage-node.ts +2 -0
- package/modules/tui/menu.ts +418 -0
- package/modules/tui/plugin/editor.ts +396 -0
- package/modules/tui/plugin/footer.ts +161 -0
- package/modules/tui/plugin/index.ts +30 -0
- package/modules/tui/plugin/lifecycle.ts +637 -0
- package/modules/tui/plugin/package-order.ts +169 -0
- package/modules/tui/plugin/screen-transition.ts +203 -0
- package/modules/tui/plugin/settings-config.ts +297 -0
- package/modules/tui/plugin/status-sources.ts +49 -0
- package/modules/tui/plugin/transition-gate.ts +261 -0
- package/modules/tui/renderer/custom-header.ts +157 -0
- package/modules/tui/renderer/editor.ts +70 -0
- package/modules/tui/renderer/header.ts +72 -0
- package/modules/tui/renderer/icons.ts +149 -0
- package/modules/tui/renderer/pi-installer-logo.ts +194 -0
- package/modules/tui/status/auto-compaction.ts +67 -0
- package/modules/tui/status/project-status.ts +655 -0
- package/modules/tui/status/provider-status.ts +120 -0
- package/modules/tui/status/runtime-status.ts +307 -0
- package/modules/tui/status/session-status.ts +325 -0
- package/modules/tui/status/status-config.ts +95 -0
- package/modules/tui/status/status-segments.ts +263 -0
- package/modules/tui/status/turn-telemetry.ts +466 -0
- package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
- package/modules/tui/themes/UPSTREAM.md +7 -0
- package/modules/tui/themes/catppuccin-latte.json +80 -0
- package/modules/tui/themes/catppuccin-mocha.json +79 -0
- package/modules/tui/themes/crimson-noir.json +85 -0
- package/modules/tui/themes/dracula.json +79 -0
- package/modules/tui/themes/everforest-dark.json +85 -0
- package/modules/tui/themes/gruvbox-dark.json +85 -0
- package/modules/tui/themes/gruvbox-light.json +85 -0
- package/modules/tui/themes/matrix.json +85 -0
- package/modules/tui/themes/nord.json +85 -0
- package/modules/tui/themes/one-dark.json +85 -0
- package/modules/tui/themes/rose-pine-dawn.json +85 -0
- package/modules/tui/themes/rose-pine.json +85 -0
- package/modules/tui/themes/solarized-dark.json +85 -0
- package/modules/tui/themes/solarized-light.json +85 -0
- package/modules/tui/themes/tokyo-night-storm.json +79 -0
- package/modules/tui/themes/tokyo-night.json +79 -0
- package/package.json +28 -0
- package/services.ts +38 -0
- package/toolkit.ts +147 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// 供应商查询运行层:按共享领域层的路由表执行查询(查表 → 按记录逐步执行)。
|
|
2
|
+
// 简单 JSON 端点由通用执行器直接发请求;OAuth 订阅、HMAC 签名、gRPC 等复杂协议
|
|
3
|
+
// 以连接器标识登记在这里。凭据只经 ProviderQueryCredentials 一个通道进入,
|
|
4
|
+
// 调用方无需按 kind 记忆传参规则。
|
|
5
|
+
|
|
6
|
+
import { findProviderByUrl, type BuiltinQueryKind } from "../shared/provider-catalog.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ProviderQueryConfig,
|
|
9
|
+
ProviderQueryCredentials,
|
|
10
|
+
ProviderUsage,
|
|
11
|
+
} from "../shared/provider-contracts.ts";
|
|
12
|
+
import {
|
|
13
|
+
numberValue,
|
|
14
|
+
parseKimiBalance,
|
|
15
|
+
parseKimiQuota,
|
|
16
|
+
parseMiniMaxQuota,
|
|
17
|
+
parseNovitaBalance,
|
|
18
|
+
parseOpenRouterBalance,
|
|
19
|
+
parseSiliconFlowBalance,
|
|
20
|
+
parseStepFunBalance,
|
|
21
|
+
parseSub2ApiUsage,
|
|
22
|
+
parseZenMuxQuota,
|
|
23
|
+
} from "../shared/provider-parsers.ts";
|
|
24
|
+
import {
|
|
25
|
+
fetchDeepSeekBalance,
|
|
26
|
+
fetchZhipuBalance,
|
|
27
|
+
fetchZhipuQuota,
|
|
28
|
+
fetchZhipuTeamQuota,
|
|
29
|
+
} from "../shared/zhipu.ts";
|
|
30
|
+
import { fetchVolcengineQuota } from "../shared/volcengine.ts";
|
|
31
|
+
import {
|
|
32
|
+
fetchClaudeSubscription,
|
|
33
|
+
fetchCodexSubscription,
|
|
34
|
+
fetchCopilotSubscription,
|
|
35
|
+
fetchGeminiSubscription,
|
|
36
|
+
} from "../shared/official-subscription.ts";
|
|
37
|
+
import { fetchGrokSubscription } from "../shared/grok-subscription.ts";
|
|
38
|
+
import {
|
|
39
|
+
findProviderRoute,
|
|
40
|
+
type ConnectorId,
|
|
41
|
+
type CredentialSource,
|
|
42
|
+
type JsonParserId,
|
|
43
|
+
type JsonQueryStep,
|
|
44
|
+
type ProviderRouteStep,
|
|
45
|
+
} from "../usage-core/provider-routes.ts";
|
|
46
|
+
|
|
47
|
+
export type ProviderKind = BuiltinQueryKind | "unknown";
|
|
48
|
+
|
|
49
|
+
export function detectProviderKind(baseUrl: string): ProviderKind {
|
|
50
|
+
return findProviderByUrl(baseUrl)?.queryKind ?? "unknown";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 查询入口参数:凭据只此一个通道,具体用途由路由表按供应商决定。 */
|
|
54
|
+
export interface ProviderUsageQuery {
|
|
55
|
+
/** 宿主身份只用于 Grok 请求头;未注入时不显式设置,不参与凭据或缓存身份。 */
|
|
56
|
+
userAgent?: string;
|
|
57
|
+
kind: ProviderKind;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
credentials: ProviderQueryCredentials;
|
|
60
|
+
request?: typeof fetch;
|
|
61
|
+
/** 显式中转查询配置;存在时优先于 kind 路由。 */
|
|
62
|
+
query?: ProviderQueryConfig | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ConfiguredProviderUsageQuery {
|
|
66
|
+
config: ProviderQueryConfig;
|
|
67
|
+
/** 推理端点地址;仅用于同源判定与缺省查询地址。 */
|
|
68
|
+
baseUrl: string;
|
|
69
|
+
credentials: ProviderQueryCredentials;
|
|
70
|
+
request?: typeof fetch;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface QueryContext {
|
|
74
|
+
userAgent?: string;
|
|
75
|
+
kind: string;
|
|
76
|
+
baseUrl: string;
|
|
77
|
+
credentials: ProviderQueryCredentials;
|
|
78
|
+
request: typeof fetch;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hostIs(baseUrl: string, hostname: string): boolean {
|
|
82
|
+
try {
|
|
83
|
+
return new URL(baseUrl).hostname === hostname;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function requestJson(url: string, apiKey: string, request: typeof fetch): Promise<any | null> {
|
|
90
|
+
const response = await request(url, {
|
|
91
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
92
|
+
signal: AbortSignal.timeout(15_000),
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) return null;
|
|
95
|
+
return response.json().catch(() => null);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 简单 JSON 端点的解析器登记表;标识由路由表引用,宿主不可见。 */
|
|
99
|
+
const JSON_PARSERS: Record<
|
|
100
|
+
JsonParserId,
|
|
101
|
+
(json: any, ctx: QueryContext) => ProviderUsage | null
|
|
102
|
+
> = {
|
|
103
|
+
"stepfun-balance": (json) => {
|
|
104
|
+
const balance = parseStepFunBalance(json);
|
|
105
|
+
return balance ? { mode: "api", balance } : null;
|
|
106
|
+
},
|
|
107
|
+
"siliconflow-balance": (json, ctx) => {
|
|
108
|
+
const balance = parseSiliconFlowBalance(json, ctx.kind === "siliconflow-en");
|
|
109
|
+
return balance ? { mode: "api", balance } : null;
|
|
110
|
+
},
|
|
111
|
+
"openrouter-balance": (json) => {
|
|
112
|
+
const balance = parseOpenRouterBalance(json);
|
|
113
|
+
return balance ? { mode: "api", balance } : null;
|
|
114
|
+
},
|
|
115
|
+
"novita-balance": (json) => {
|
|
116
|
+
const balance = parseNovitaBalance(json);
|
|
117
|
+
return balance ? { mode: "api", balance } : null;
|
|
118
|
+
},
|
|
119
|
+
// Kimi 国际站余额以美元计价
|
|
120
|
+
"kimi-balance": (json, ctx) => {
|
|
121
|
+
const balance = parseKimiBalance(json, hostIs(ctx.baseUrl, "api.moonshot.ai"));
|
|
122
|
+
return balance ? { mode: "api", balance } : null;
|
|
123
|
+
},
|
|
124
|
+
"kimi-quota": (json) => {
|
|
125
|
+
const quota = parseKimiQuota(json);
|
|
126
|
+
return quota ? { mode: "subscription", quota } : null;
|
|
127
|
+
},
|
|
128
|
+
"minimax-quota": (json) => {
|
|
129
|
+
const quota = parseMiniMaxQuota(json);
|
|
130
|
+
return quota ? { mode: "subscription", quota } : null;
|
|
131
|
+
},
|
|
132
|
+
"sub2api-usage": (json) => parseSub2ApiUsage(json, "apikey.fun"),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** 专用协议连接器登记表;标识由路由表引用,宿主不可见。 */
|
|
136
|
+
const CONNECTORS: Record<ConnectorId, (ctx: QueryContext) => Promise<ProviderUsage | null>> = {
|
|
137
|
+
"claude-subscription": (ctx) =>
|
|
138
|
+
fetchClaudeSubscription(ctx.credentials.token, ctx.request).catch(() => null),
|
|
139
|
+
"codex-subscription": (ctx) =>
|
|
140
|
+
fetchCodexSubscription(ctx.credentials.token, ctx.credentials.accountId, ctx.request).catch(() => null),
|
|
141
|
+
"gemini-subscription": (ctx) =>
|
|
142
|
+
fetchGeminiSubscription(ctx.credentials.token, ctx.request).catch(() => null),
|
|
143
|
+
"copilot-subscription": (ctx) =>
|
|
144
|
+
fetchCopilotSubscription(ctx.credentials.token, ctx.credentials.githubDomain ?? "github.com", ctx.request).catch(() => null),
|
|
145
|
+
"grok-subscription": (ctx) =>
|
|
146
|
+
fetchGrokSubscription(ctx.credentials.token, ctx.request, ctx.userAgent).catch(() => null),
|
|
147
|
+
"zhipu-quota": async (ctx) => {
|
|
148
|
+
// 团队套餐与个人套餐是同一计费产品的两种入口:配置了团队凭据就走团队端点,
|
|
149
|
+
// 否则走个人端点;两端都失败才轮到路由表里的余额回退。
|
|
150
|
+
const team = ctx.credentials.zhipuTeam;
|
|
151
|
+
const quota = team
|
|
152
|
+
? await fetchZhipuTeamQuota(ctx.credentials.token, team.organizationId, team.projectId, ctx.request).catch(() => null)
|
|
153
|
+
: await fetchZhipuQuota(ctx.baseUrl, ctx.credentials.token, ctx.request).catch(() => null);
|
|
154
|
+
// 订阅接口只要返回有效窗口(包括剩余 0%),就优先展示并停止后续余额查询。
|
|
155
|
+
return quota ? { mode: "subscription", quota } : null;
|
|
156
|
+
},
|
|
157
|
+
"zhipu-balance": async (ctx) => {
|
|
158
|
+
const amount = await fetchZhipuBalance(ctx.credentials.token, ctx.request).catch(() => null);
|
|
159
|
+
return amount == null ? null : { mode: "api", balance: { amount, currency: "CNY" } };
|
|
160
|
+
},
|
|
161
|
+
"deepseek-balance": async (ctx) => {
|
|
162
|
+
const balance = await fetchDeepSeekBalance(ctx.credentials.token, ctx.request).catch(() => null);
|
|
163
|
+
return balance ? { mode: "api", balance } : null;
|
|
164
|
+
},
|
|
165
|
+
"volcengine-quota": async (ctx) => {
|
|
166
|
+
const credentials = ctx.credentials.volcengine;
|
|
167
|
+
if (!credentials) return null;
|
|
168
|
+
const quota = await fetchVolcengineQuota(
|
|
169
|
+
ctx.baseUrl,
|
|
170
|
+
credentials.accessKeyId,
|
|
171
|
+
credentials.secretAccessKey,
|
|
172
|
+
ctx.request,
|
|
173
|
+
ctx.kind === "volcengine-agent" ? "agent" : "coding",
|
|
174
|
+
).catch(() => null);
|
|
175
|
+
return quota ? { mode: "subscription", quota } : null;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function credentialReady(source: CredentialSource, credentials: ProviderQueryCredentials): boolean {
|
|
180
|
+
switch (source) {
|
|
181
|
+
case "token":
|
|
182
|
+
return Boolean(credentials.token);
|
|
183
|
+
case "openrouter-management":
|
|
184
|
+
return Boolean(credentials.openrouter?.managementKey);
|
|
185
|
+
case "volcengine-aksk":
|
|
186
|
+
return Boolean(credentials.volcengine?.accessKeyId && credentials.volcengine?.secretAccessKey);
|
|
187
|
+
case "zhipu-team":
|
|
188
|
+
return Boolean(credentials.zhipuTeam?.organizationId && credentials.zhipuTeam?.projectId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function jsonStepCredential(step: JsonQueryStep, credentials: ProviderQueryCredentials): string {
|
|
193
|
+
return step.credential === "openrouter-management"
|
|
194
|
+
? credentials.openrouter?.managementKey ?? ""
|
|
195
|
+
: credentials.token;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function stepUrl(step: JsonQueryStep, baseUrl: string): string | null {
|
|
199
|
+
if ("fixed" in step.url) return step.url.fixed;
|
|
200
|
+
try {
|
|
201
|
+
return `${new URL(baseUrl).origin}${step.url.originPath}`;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runRouteStep(step: ProviderRouteStep, ctx: QueryContext): Promise<ProviderUsage | null> {
|
|
208
|
+
if (step.onlyWhen?.hostNot != null && hostIs(ctx.baseUrl, step.onlyWhen.hostNot)) return null;
|
|
209
|
+
if (step.type === "connector") {
|
|
210
|
+
if (!(step.requires ?? []).every((source) => credentialReady(source, ctx.credentials))) return null;
|
|
211
|
+
return CONNECTORS[step.connector](ctx);
|
|
212
|
+
}
|
|
213
|
+
const credential = jsonStepCredential(step, ctx.credentials);
|
|
214
|
+
if (!credential) return null;
|
|
215
|
+
const url = stepUrl(step, ctx.baseUrl);
|
|
216
|
+
if (!url) return null;
|
|
217
|
+
const json = await requestJson(url, credential, ctx.request).catch(() => null);
|
|
218
|
+
return JSON_PARSERS[step.parser](json, ctx);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** kind 路由查询:查路由表,按记录的回退链依次执行,首个成功结果即返回。 */
|
|
222
|
+
export async function fetchProviderUsage(query: ProviderUsageQuery): Promise<ProviderUsage | null> {
|
|
223
|
+
if (query.query) {
|
|
224
|
+
return fetchConfiguredProviderUsage({
|
|
225
|
+
config: query.query,
|
|
226
|
+
baseUrl: query.baseUrl,
|
|
227
|
+
credentials: query.credentials,
|
|
228
|
+
request: query.request,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const route = findProviderRoute(query.kind);
|
|
232
|
+
if (!route) return null;
|
|
233
|
+
const ctx: QueryContext = {
|
|
234
|
+
userAgent: query.userAgent,
|
|
235
|
+
kind: query.kind,
|
|
236
|
+
baseUrl: query.baseUrl,
|
|
237
|
+
credentials: query.credentials,
|
|
238
|
+
request: query.request ?? fetch,
|
|
239
|
+
};
|
|
240
|
+
for (const step of route.steps) {
|
|
241
|
+
const usage = await runRouteStep(step, ctx);
|
|
242
|
+
if (usage) return usage;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function matchesConfiguredHost(hostname: string, pattern: string): boolean {
|
|
248
|
+
const normalized = pattern.trim().toLowerCase();
|
|
249
|
+
if (!normalized) return false;
|
|
250
|
+
if (!normalized.startsWith("*.")) return hostname === normalized;
|
|
251
|
+
const parent = normalized.slice(2);
|
|
252
|
+
return hostname === parent || hostname.endsWith(`.${parent}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 显式配置按当前推理主机匹配;不根据品牌名或 URL 子串猜测。 */
|
|
256
|
+
export function findProviderQueryConfig(
|
|
257
|
+
baseUrl: string,
|
|
258
|
+
configs: readonly ProviderQueryConfig[],
|
|
259
|
+
): ProviderQueryConfig | null {
|
|
260
|
+
let hostname: string;
|
|
261
|
+
try {
|
|
262
|
+
hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return (
|
|
267
|
+
configs.find(
|
|
268
|
+
(config) =>
|
|
269
|
+
Array.isArray(config?.matchHosts) &&
|
|
270
|
+
config.matchHosts.some((pattern) =>
|
|
271
|
+
typeof pattern === "string" ? matchesConfiguredHost(hostname, pattern) : false,
|
|
272
|
+
),
|
|
273
|
+
) ?? null
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function appendQueryPath(baseUrl: string, path: string): string | null {
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(baseUrl);
|
|
280
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
281
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
282
|
+
url.pathname = `${basePath}${suffix}`.replace(/\/{2,}/g, "/");
|
|
283
|
+
url.search = "";
|
|
284
|
+
url.hash = "";
|
|
285
|
+
return url.toString();
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function sameOrigin(left: string, right: string): boolean {
|
|
292
|
+
try {
|
|
293
|
+
return new URL(left).origin === new URL(right).origin;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function requestConfiguredJson(
|
|
300
|
+
url: string,
|
|
301
|
+
headers: Record<string, string>,
|
|
302
|
+
request: typeof fetch,
|
|
303
|
+
): Promise<any | null> {
|
|
304
|
+
const response = await request(url, {
|
|
305
|
+
headers: { Accept: "application/json", ...headers },
|
|
306
|
+
signal: AbortSignal.timeout(15_000),
|
|
307
|
+
});
|
|
308
|
+
if (!response.ok) return null;
|
|
309
|
+
return response.json().catch(() => null);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** 显式中转协议查询(new-api / zenmux / sub2api / generic-balance)。 */
|
|
313
|
+
export async function fetchConfiguredProviderUsage(
|
|
314
|
+
query: ConfiguredProviderUsageQuery,
|
|
315
|
+
): Promise<ProviderUsage | null> {
|
|
316
|
+
const { config, credentials } = query;
|
|
317
|
+
const request = query.request ?? fetch;
|
|
318
|
+
const inferenceApiKey = credentials.token;
|
|
319
|
+
const queryBaseUrl = String(config.baseUrl || query.baseUrl).replace(/\/+$/, "");
|
|
320
|
+
try {
|
|
321
|
+
if (new URL(queryBaseUrl).protocol !== "https:") return null;
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const providerId = String(config.id || findProviderByUrl(query.baseUrl)?.id || "relay");
|
|
326
|
+
const providerLabel = String(config.displayName || providerId);
|
|
327
|
+
|
|
328
|
+
if (config.protocol === "new-api") {
|
|
329
|
+
if (!config.accessToken || !config.userId) return null;
|
|
330
|
+
const url = appendQueryPath(queryBaseUrl, config.path || "/api/user/self");
|
|
331
|
+
if (!url) return null;
|
|
332
|
+
const json = await requestConfiguredJson(
|
|
333
|
+
url,
|
|
334
|
+
{
|
|
335
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
"New-Api-User": config.userId,
|
|
338
|
+
},
|
|
339
|
+
request,
|
|
340
|
+
);
|
|
341
|
+
if (json?.success !== true || !json?.data) return null;
|
|
342
|
+
const amount = numberValue(json.data.quota);
|
|
343
|
+
if (amount == null) return null;
|
|
344
|
+
return {
|
|
345
|
+
mode: "api",
|
|
346
|
+
balance: { amount: amount / 500_000, currency: config.currency ?? "USD" },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (config.protocol === "zenmux") {
|
|
351
|
+
if (!config.baseUrl) return null;
|
|
352
|
+
const apiKey = config.apiKey || (sameOrigin(queryBaseUrl, query.baseUrl) ? inferenceApiKey : "");
|
|
353
|
+
if (!apiKey) return null;
|
|
354
|
+
const json = await requestConfiguredJson(queryBaseUrl, { Authorization: `Bearer ${apiKey}` }, request);
|
|
355
|
+
const quota = parseZenMuxQuota(json);
|
|
356
|
+
return quota ? { mode: "subscription", quota } : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 只有同源查询可以复用推理 Key;跨域查询必须在配置里单独提供查询 Key。
|
|
360
|
+
const apiKey = config.apiKey || (sameOrigin(queryBaseUrl, query.baseUrl) ? inferenceApiKey : "");
|
|
361
|
+
if (!apiKey) return null;
|
|
362
|
+
|
|
363
|
+
if (config.protocol === "sub2api") {
|
|
364
|
+
let protocolBase = queryBaseUrl;
|
|
365
|
+
try {
|
|
366
|
+
protocolBase = new URL(queryBaseUrl).origin;
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const url = appendQueryPath(protocolBase, config.path || "/v1/usage");
|
|
371
|
+
if (!url) return null;
|
|
372
|
+
const json = await requestConfiguredJson(url, { Authorization: `Bearer ${apiKey}` }, request);
|
|
373
|
+
return parseSub2ApiUsage(json, providerLabel);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (config.protocol === "generic-balance") {
|
|
377
|
+
const url = appendQueryPath(queryBaseUrl, config.path || "/user/balance");
|
|
378
|
+
if (!url) return null;
|
|
379
|
+
const json = await requestConfiguredJson(url, { Authorization: `Bearer ${apiKey}` }, request);
|
|
380
|
+
if (!json || json.is_active === false || json.isValid === false) return null;
|
|
381
|
+
const amount = numberValue(json.balance ?? json?.data?.balance);
|
|
382
|
+
return amount == null
|
|
383
|
+
? null
|
|
384
|
+
: { mode: "api", balance: { amount, currency: config.currency ?? "USD" } };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return null;
|
|
388
|
+
}
|