@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,150 @@
|
|
|
1
|
+
export type BillingMode = 'subscription' | 'api' | 'hybrid' | 'unknown';
|
|
2
|
+
|
|
3
|
+
export interface QuotaWindow {
|
|
4
|
+
label: string;
|
|
5
|
+
remainingPercent: number;
|
|
6
|
+
resetMs: number | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface QuotaInfo {
|
|
10
|
+
provider: string;
|
|
11
|
+
windows: QuotaWindow[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BalanceValue {
|
|
15
|
+
amount: number;
|
|
16
|
+
currency: 'CNY' | 'USD';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProviderUsage {
|
|
20
|
+
mode: Exclude<BillingMode, 'unknown'>;
|
|
21
|
+
balance?: BalanceValue;
|
|
22
|
+
quota?: QuotaInfo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const QUERY_PROTOCOLS = ['sub2api', 'new-api', 'generic-balance', 'zenmux'] as const;
|
|
26
|
+
export type RelayQueryProtocol = (typeof QUERY_PROTOCOLS)[number];
|
|
27
|
+
|
|
28
|
+
/** 不猜中转接口;查询与推理地址不同源时须显式提供查询凭据,避免跨域复用推理 Key。 */
|
|
29
|
+
export interface ProviderQueryConfig {
|
|
30
|
+
id: string;
|
|
31
|
+
displayName?: string;
|
|
32
|
+
matchHosts: string[];
|
|
33
|
+
protocol: RelayQueryProtocol;
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
path?: string;
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
accessToken?: string;
|
|
38
|
+
userId?: string;
|
|
39
|
+
currency?: 'CNY' | 'USD';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProviderCredentials {
|
|
43
|
+
volcengine?: {
|
|
44
|
+
accessKeyId: string;
|
|
45
|
+
secretAccessKey: string;
|
|
46
|
+
};
|
|
47
|
+
zhipuTeam?: {
|
|
48
|
+
organizationId: string;
|
|
49
|
+
projectId: string;
|
|
50
|
+
};
|
|
51
|
+
openrouter?: {
|
|
52
|
+
managementKey: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ProviderConfigRule = 'object' | 'array' | 'nonEmptyArray' | 'nonEmptyString' | 'choice';
|
|
57
|
+
|
|
58
|
+
/** 不携带输入原文,避免宿主输出诊断时泄露凭据;宿主按规则码选择自己的文案。 */
|
|
59
|
+
export class ProviderConfigValidationError extends Error {
|
|
60
|
+
readonly field: string;
|
|
61
|
+
readonly rule: ProviderConfigRule;
|
|
62
|
+
|
|
63
|
+
constructor(field: string, rule: ProviderConfigRule) {
|
|
64
|
+
super(`${field}: ${rule}`);
|
|
65
|
+
this.name = 'ProviderConfigValidationError';
|
|
66
|
+
this.field = field;
|
|
67
|
+
this.rule = rule;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
72
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
73
|
+
throw new ProviderConfigValidationError(field, 'object');
|
|
74
|
+
}
|
|
75
|
+
return value as Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function optionalString(value: unknown, field: string, secret = false): string | undefined {
|
|
79
|
+
if (value === undefined) return undefined;
|
|
80
|
+
if (typeof value !== 'string' || !(secret ? value : value.trim())) {
|
|
81
|
+
throw new ProviderConfigValidationError(field, 'nonEmptyString');
|
|
82
|
+
}
|
|
83
|
+
return secret ? value : value.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function requiredString(value: unknown, field: string, secret = false): string {
|
|
87
|
+
const parsed = optionalString(value, field, secret);
|
|
88
|
+
if (parsed === undefined) throw new ProviderConfigValidationError(field, 'nonEmptyString');
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 缺省保留 undefined;普通字符串裁边,凭据原样保留,未知字段丢弃,以兼容既有配置。 */
|
|
93
|
+
export function parseProviderQueries(value: unknown, field: string): ProviderQueryConfig[] | undefined {
|
|
94
|
+
if (value === undefined) return undefined;
|
|
95
|
+
if (!Array.isArray(value)) throw new ProviderConfigValidationError(field, 'array');
|
|
96
|
+
return value.map((item, index) => {
|
|
97
|
+
const path = `${field}[${index}]`;
|
|
98
|
+
const query = requireRecord(item, path);
|
|
99
|
+
const id = requiredString(query.id, `${path}.id`);
|
|
100
|
+
if (!Array.isArray(query.matchHosts) || query.matchHosts.length === 0) {
|
|
101
|
+
throw new ProviderConfigValidationError(`${path}.matchHosts`, 'nonEmptyArray');
|
|
102
|
+
}
|
|
103
|
+
const matchHosts = query.matchHosts.map((host, hostIndex) => requiredString(host, `${path}.matchHosts[${hostIndex}]`));
|
|
104
|
+
if (typeof query.protocol !== 'string' || !QUERY_PROTOCOLS.includes(query.protocol as RelayQueryProtocol)) {
|
|
105
|
+
throw new ProviderConfigValidationError(`${path}.protocol`, 'choice');
|
|
106
|
+
}
|
|
107
|
+
if (query.currency !== undefined && query.currency !== 'CNY' && query.currency !== 'USD') {
|
|
108
|
+
throw new ProviderConfigValidationError(`${path}.currency`, 'choice');
|
|
109
|
+
}
|
|
110
|
+
const result: ProviderQueryConfig = { id, matchHosts, protocol: query.protocol as RelayQueryProtocol };
|
|
111
|
+
for (const key of ['displayName', 'baseUrl', 'path', 'apiKey', 'accessToken', 'userId'] as const) {
|
|
112
|
+
const parsed = optionalString(query[key], `${path}.${key}`, key === 'apiKey' || key === 'accessToken');
|
|
113
|
+
if (parsed !== undefined) result[key] = parsed;
|
|
114
|
+
}
|
|
115
|
+
if (query.currency !== undefined) result.currency = query.currency;
|
|
116
|
+
return result;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 缺省视为空凭据;仅校验显式配置的已知供应商,不要求同时配置所有供应商。 */
|
|
121
|
+
export function parseProviderCredentials(value: unknown, field: string): ProviderCredentials {
|
|
122
|
+
const credentials = value === undefined ? {} : requireRecord(value, field);
|
|
123
|
+
const volcengine = credentials.volcengine === undefined
|
|
124
|
+
? undefined : requireRecord(credentials.volcengine, `${field}.volcengine`);
|
|
125
|
+
const zhipuTeam = credentials.zhipuTeam === undefined
|
|
126
|
+
? undefined : requireRecord(credentials.zhipuTeam, `${field}.zhipuTeam`);
|
|
127
|
+
const openrouter = credentials.openrouter === undefined
|
|
128
|
+
? undefined : requireRecord(credentials.openrouter, `${field}.openrouter`);
|
|
129
|
+
return {
|
|
130
|
+
...(volcengine ? { volcengine: {
|
|
131
|
+
accessKeyId: requiredString(volcengine.accessKeyId, `${field}.volcengine.accessKeyId`, true),
|
|
132
|
+
secretAccessKey: requiredString(volcengine.secretAccessKey, `${field}.volcengine.secretAccessKey`, true),
|
|
133
|
+
} } : {}),
|
|
134
|
+
...(zhipuTeam ? { zhipuTeam: {
|
|
135
|
+
organizationId: requiredString(zhipuTeam.organizationId, `${field}.zhipuTeam.organizationId`),
|
|
136
|
+
projectId: requiredString(zhipuTeam.projectId, `${field}.zhipuTeam.projectId`),
|
|
137
|
+
} } : {}),
|
|
138
|
+
...(openrouter ? { openrouter: {
|
|
139
|
+
managementKey: requiredString(openrouter.managementKey, `${field}.openrouter.managementKey`, true),
|
|
140
|
+
} } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 查询入口的唯一凭据通道:调用方只交凭据,具体字段用不用由路由表按供应商决定。 */
|
|
145
|
+
export interface ProviderQueryCredentials extends ProviderCredentials {
|
|
146
|
+
/** 主凭据:API Key 或 OAuth Access Token,按供应商协议充当对应角色 */
|
|
147
|
+
token: string;
|
|
148
|
+
accountId?: string;
|
|
149
|
+
githubDomain?: string;
|
|
150
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { findProviderById } from './provider-catalog.ts';
|
|
2
|
+
|
|
3
|
+
const MODEL_PROVIDER_PATTERNS: readonly [RegExp, string][] = [
|
|
4
|
+
[/^deepseek(?:[-_/]|$)/, 'deepseek'],
|
|
5
|
+
[/^(?:kimi|moonshot)(?:[-_/]|$)/, 'kimi'],
|
|
6
|
+
[/^(?:glm|chatglm|zhipu)(?:[-_/]|$)/, 'zhipu'],
|
|
7
|
+
[/^(?:qwen|qwq)(?:[-_/]|$)/, 'qwen'],
|
|
8
|
+
[/^(?:minimax|abab)(?:[-_/]|$)/, 'minimax'],
|
|
9
|
+
[/^(?:doubao|seed)(?:[-_/]|$)/, 'doubao'],
|
|
10
|
+
[/^(?:gemini|gemma)(?:[-_/]|$)/, 'gemini'],
|
|
11
|
+
[/^(?:claude|anthropic)(?:[-_/]|$)/, 'anthropic'],
|
|
12
|
+
[/^(?:gpt|o[1-4])(?:[-_/]|$)/, 'openai'],
|
|
13
|
+
[/^(?:grok|xai)(?:[-_/]|$)/, 'xai'],
|
|
14
|
+
[/^(?:llama)(?:[-_/]|$)/, 'meta'],
|
|
15
|
+
[/^(?:mistral|codestral)(?:[-_/]|$)/, 'mistral'],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const QUERY_DISPLAY_NAMES: Readonly<Record<string, string>> = {
|
|
19
|
+
zhipu: 'Zhipu',
|
|
20
|
+
'z.ai': 'Zhipu',
|
|
21
|
+
kimi: 'Kimi',
|
|
22
|
+
qwen: 'Qwen',
|
|
23
|
+
doubao: 'Doubao',
|
|
24
|
+
gemini: 'Google',
|
|
25
|
+
anthropic: 'Anthropic',
|
|
26
|
+
openai: 'OpenAI',
|
|
27
|
+
xai: 'xAI',
|
|
28
|
+
meta: 'Meta',
|
|
29
|
+
mistral: 'Mistral',
|
|
30
|
+
minimax: 'MiniMax',
|
|
31
|
+
'minimax-cn': 'MiniMax',
|
|
32
|
+
'minimax-en': 'MiniMax',
|
|
33
|
+
zenmux: 'ZenMux',
|
|
34
|
+
deepseek: 'DeepSeek',
|
|
35
|
+
stepfun: 'StepFun',
|
|
36
|
+
siliconflow: 'SiliconFlow',
|
|
37
|
+
'siliconflow-cn': 'SiliconFlow',
|
|
38
|
+
'siliconflow-en': 'SiliconFlow',
|
|
39
|
+
openrouter: 'OpenRouter',
|
|
40
|
+
novita: 'Novita',
|
|
41
|
+
sub2api: 'Sub2API',
|
|
42
|
+
'apikey.fun': 'ApiKey',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 仅用于未知中转的界面标注,不能用于选择查询端点或发送凭据。 */
|
|
46
|
+
export function inferModelProviderName(modelId: string): string | undefined {
|
|
47
|
+
const normalized = modelId.trim().toLowerCase().split('/').pop() ?? '';
|
|
48
|
+
return MODEL_PROVIDER_PATTERNS.find(([pattern]) => pattern.test(normalized))?.[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 内部标识只用于逻辑;状态栏显示官方拉丁品牌名,未知值保留宿主原文。 */
|
|
52
|
+
export function displayProviderName(providerId: string): string {
|
|
53
|
+
const normalized = providerId.toLowerCase();
|
|
54
|
+
return QUERY_DISPLAY_NAMES[normalized] ?? findProviderById(normalized)?.displayName ?? providerId;
|
|
55
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// 供应商响应解析(纯函数,零 IO)。字段口径以 cc-switch 0b5da51 为基准,
|
|
2
|
+
// Sub2API /v1/usage 另按 ApiKey 现场响应与 Sub2API 上游实现适配。
|
|
3
|
+
// 解析器只由查询运行层按路由表绑定消费,不从 usage-core / usage-node 出口导出。
|
|
4
|
+
|
|
5
|
+
import type { BalanceValue, ProviderUsage, QuotaInfo, QuotaWindow } from './provider-contracts.ts';
|
|
6
|
+
|
|
7
|
+
export function numberValue(value: unknown): number | null {
|
|
8
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
|
|
9
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function currencyValue(value: unknown, fallback: 'CNY' | 'USD'): 'CNY' | 'USD' {
|
|
13
|
+
const currency = String(value).toUpperCase();
|
|
14
|
+
if (currency === 'USD' || currency === 'CNY') return currency;
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function clampPercent(value: number): number {
|
|
19
|
+
return Math.max(0, Math.min(100, value));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resetTime(value: unknown): number | null {
|
|
23
|
+
const numeric = numberValue(value);
|
|
24
|
+
if (numeric != null) {
|
|
25
|
+
if (numeric <= 0) return null;
|
|
26
|
+
return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== 'string') return null;
|
|
29
|
+
const parsed = Date.parse(value);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function remainingWindow(label: string, remainingPercent: number, reset: unknown): QuotaWindow {
|
|
34
|
+
return { label, remainingPercent: clampPercent(remainingPercent), resetMs: resetTime(reset) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function usageWindow(label: string, used: unknown, limit: unknown, reset: unknown): QuotaWindow | null {
|
|
38
|
+
const usedValue = numberValue(used);
|
|
39
|
+
const limitValue = numberValue(limit);
|
|
40
|
+
if (usedValue == null || limitValue == null || limitValue <= 0) return null;
|
|
41
|
+
return remainingWindow(label, 100 - (usedValue / limitValue) * 100, reset);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseStepFunBalance(json: any): BalanceValue | null {
|
|
45
|
+
const amount = numberValue(json?.balance);
|
|
46
|
+
return amount == null ? null : { amount, currency: 'CNY' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseSiliconFlowBalance(json: any, international: boolean): BalanceValue | null {
|
|
50
|
+
const amount = numberValue(json?.data?.totalBalance);
|
|
51
|
+
if (amount == null) return null;
|
|
52
|
+
return { amount, currency: international ? 'USD' : 'CNY' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseOpenRouterBalance(json: any): BalanceValue | null {
|
|
56
|
+
const data = json?.data ?? json;
|
|
57
|
+
const total = numberValue(data?.total_credits);
|
|
58
|
+
const used = numberValue(data?.total_usage);
|
|
59
|
+
if (total == null || used == null) return null;
|
|
60
|
+
return { amount: total - used, currency: 'USD' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseNovitaBalance(json: any): BalanceValue | null {
|
|
64
|
+
const units = numberValue(json?.availableBalance);
|
|
65
|
+
return units == null ? null : { amount: units / 10_000, currency: 'USD' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseKimiBalance(json: any, international = false): BalanceValue | null {
|
|
69
|
+
if ((json?.code !== 0 && json?.code !== '0') || !json?.data) return null;
|
|
70
|
+
const amount = numberValue(json.data.available_balance);
|
|
71
|
+
return amount == null
|
|
72
|
+
? null
|
|
73
|
+
: { amount, currency: international ? 'USD' : 'CNY' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseKimiQuota(json: any): QuotaInfo | null {
|
|
77
|
+
// 官方接口当前返回裸对象;兼容网关包裹在 data 下的同一响应,
|
|
78
|
+
// 但不把普通余额响应误认为套餐额度。
|
|
79
|
+
const payload = json?.data && (json.data.usage || json.data.limits) ? json.data : json;
|
|
80
|
+
const windows: QuotaWindow[] = [];
|
|
81
|
+
const detail = Array.isArray(payload?.limits)
|
|
82
|
+
? payload.limits.map((item: any) => item?.detail).find((item: any) => item && numberValue(item.limit) != null)
|
|
83
|
+
: null;
|
|
84
|
+
if (detail) {
|
|
85
|
+
const limit = numberValue(detail.limit);
|
|
86
|
+
const remaining = numberValue(detail.remaining);
|
|
87
|
+
if (limit != null && limit > 0 && remaining != null) {
|
|
88
|
+
windows.push(remainingWindow('5h', (remaining / limit) * 100, detail.resetTime));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const weeklyLimit = numberValue(payload?.usage?.limit);
|
|
92
|
+
const weeklyRemaining = numberValue(payload?.usage?.remaining);
|
|
93
|
+
if (weeklyLimit != null && weeklyLimit > 0 && weeklyRemaining != null) {
|
|
94
|
+
windows.push(remainingWindow('7d', (weeklyRemaining / weeklyLimit) * 100, payload.usage.resetTime));
|
|
95
|
+
}
|
|
96
|
+
return windows.length > 0 ? { provider: 'kimi', windows } : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseMiniMaxQuota(json: any): QuotaInfo | null {
|
|
100
|
+
if (numberValue(json?.base_resp?.status_code) != null && numberValue(json.base_resp.status_code) !== 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const item = Array.isArray(json?.model_remains)
|
|
104
|
+
? json.model_remains.find((entry: any) => entry?.model_name === 'general')
|
|
105
|
+
: null;
|
|
106
|
+
if (!item) return null;
|
|
107
|
+
|
|
108
|
+
const windows: QuotaWindow[] = [];
|
|
109
|
+
const interval = numberValue(item.current_interval_remaining_percent);
|
|
110
|
+
if (interval != null) windows.push(remainingWindow('5h', interval, item.end_time));
|
|
111
|
+
if (numberValue(item.current_weekly_status) === 1) {
|
|
112
|
+
const weekly = numberValue(item.current_weekly_remaining_percent);
|
|
113
|
+
if (weekly != null) windows.push(remainingWindow('7d', weekly, item.weekly_end_time));
|
|
114
|
+
}
|
|
115
|
+
return windows.length > 0 ? { provider: 'minimax', windows } : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseZenMuxQuota(json: any): QuotaInfo | null {
|
|
119
|
+
if (json?.success !== true || !json?.data) return null;
|
|
120
|
+
const windows: QuotaWindow[] = [];
|
|
121
|
+
for (const [field, label] of [
|
|
122
|
+
['quota_5_hour', '5h'],
|
|
123
|
+
['quota_7_day', '7d'],
|
|
124
|
+
] as const) {
|
|
125
|
+
const item = json.data[field];
|
|
126
|
+
const usedRatio = numberValue(item?.usage_percentage);
|
|
127
|
+
if (usedRatio != null) windows.push(remainingWindow(label, 100 - usedRatio * 100, item.resets_at));
|
|
128
|
+
}
|
|
129
|
+
return windows.length > 0 ? { provider: 'zenmux', windows } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sub2ApiSubscriptionWindows(subscription: any): QuotaWindow[] {
|
|
133
|
+
const weeklyStart = resetTime(subscription?.weekly_window_start);
|
|
134
|
+
const weeklyReset = weeklyStart == null ? null : weeklyStart + 7 * 86_400_000;
|
|
135
|
+
return [
|
|
136
|
+
usageWindow('1d', subscription?.daily_usage_usd, subscription?.daily_limit_usd, null),
|
|
137
|
+
usageWindow('7d', subscription?.weekly_usage_usd, subscription?.weekly_limit_usd, weeklyReset),
|
|
138
|
+
usageWindow('30d', subscription?.monthly_usage_usd, subscription?.monthly_limit_usd, subscription?.expires_at),
|
|
139
|
+
].filter((window): window is QuotaWindow => window != null);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sub2ApiRateWindows(rateLimits: any): QuotaWindow[] {
|
|
143
|
+
if (!Array.isArray(rateLimits)) return [];
|
|
144
|
+
return rateLimits
|
|
145
|
+
.map((item: any) => usageWindow(String(item?.window ?? ''), item?.used, item?.limit, item?.reset_at))
|
|
146
|
+
.filter((window): window is QuotaWindow => window != null && window.label.length > 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function parseSub2ApiUsage(json: any, provider: string): ProviderUsage | null {
|
|
150
|
+
if (!json || json.isValid === false || json.error) return null;
|
|
151
|
+
|
|
152
|
+
const subscriptionWindows = sub2ApiSubscriptionWindows(json.subscription);
|
|
153
|
+
const rateWindows = sub2ApiRateWindows(json.rate_limits);
|
|
154
|
+
const windows = [...subscriptionWindows, ...rateWindows];
|
|
155
|
+
const quotaRemaining = numberValue(json?.quota?.remaining);
|
|
156
|
+
const walletRemaining = numberValue(json?.remaining) ?? numberValue(json?.balance);
|
|
157
|
+
const amount = quotaRemaining ?? walletRemaining;
|
|
158
|
+
const balance =
|
|
159
|
+
amount == null
|
|
160
|
+
? undefined
|
|
161
|
+
: { amount, currency: currencyValue(json?.quota?.unit ?? json?.unit, 'USD') };
|
|
162
|
+
|
|
163
|
+
if (windows.length > 0) {
|
|
164
|
+
return {
|
|
165
|
+
mode: balance ? 'hybrid' : 'subscription',
|
|
166
|
+
balance,
|
|
167
|
+
quota: { provider, windows },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return balance ? { mode: 'api', balance } : null;
|
|
171
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// 火山方舟 Agent Plan / Coding Plan 控制面查询。
|
|
2
|
+
// 协议与签名规则移植自 cc-switch 0b5da51 coding_plan.rs;它使用 AK/SK,
|
|
3
|
+
// 不能复用推理接口的 Bearer Key。
|
|
4
|
+
|
|
5
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
6
|
+
import type { QuotaInfo, QuotaWindow } from './provider-contracts.ts';
|
|
7
|
+
|
|
8
|
+
const HOST = 'open.volcengineapi.com';
|
|
9
|
+
const VERSION = '2024-01-01';
|
|
10
|
+
const SERVICE = 'ark';
|
|
11
|
+
const CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
12
|
+
const SIGNED_HEADERS = 'host;x-date;x-content-sha256;content-type';
|
|
13
|
+
|
|
14
|
+
function sha256Hex(value: string): string {
|
|
15
|
+
return createHash('sha256').update(value).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hmac(key: string | Buffer, value: string): Buffer {
|
|
19
|
+
return createHmac('sha256', key).update(value).digest();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isoBasic(date: Date): string {
|
|
23
|
+
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shortDate(date: Date): string {
|
|
27
|
+
return isoBasic(date).slice(0, 8);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function encodeRfc3986(value: string): string {
|
|
31
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function volcengineRegion(baseUrl: string): string {
|
|
35
|
+
try {
|
|
36
|
+
return new URL(baseUrl).hostname.split('.').find((part) => /^(?:cn|ap)-/.test(part)) ?? 'cn-beijing';
|
|
37
|
+
} catch {
|
|
38
|
+
return 'cn-beijing';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function volcengineCanonicalQuery(action: string, region: string): string {
|
|
43
|
+
return [
|
|
44
|
+
['Action', action],
|
|
45
|
+
['Region', region],
|
|
46
|
+
['Version', VERSION],
|
|
47
|
+
]
|
|
48
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
49
|
+
.map(([key, value]) => `${encodeRfc3986(key)}=${encodeRfc3986(value)}`)
|
|
50
|
+
.join('&');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function signVolcengineRequest(
|
|
54
|
+
accessKeyId: string,
|
|
55
|
+
secretAccessKey: string,
|
|
56
|
+
region: string,
|
|
57
|
+
canonicalQuery: string,
|
|
58
|
+
now: Date,
|
|
59
|
+
): { authorization: string; xDate: string; contentSha256: string } {
|
|
60
|
+
const xDate = isoBasic(now);
|
|
61
|
+
const date = shortDate(now);
|
|
62
|
+
const contentSha256 = sha256Hex('');
|
|
63
|
+
const canonicalHeaders =
|
|
64
|
+
`host:${HOST}\n` +
|
|
65
|
+
`x-date:${xDate}\n` +
|
|
66
|
+
`x-content-sha256:${contentSha256}\n` +
|
|
67
|
+
`content-type:${CONTENT_TYPE}\n`;
|
|
68
|
+
const canonicalRequest =
|
|
69
|
+
`POST\n/\n${canonicalQuery}\n${canonicalHeaders}\n${SIGNED_HEADERS}\n${contentSha256}`;
|
|
70
|
+
const scope = `${date}/${region}/${SERVICE}/request`;
|
|
71
|
+
const stringToSign = `HMAC-SHA256\n${xDate}\n${scope}\n${sha256Hex(canonicalRequest)}`;
|
|
72
|
+
const kDate = hmac(secretAccessKey, date);
|
|
73
|
+
const kRegion = hmac(kDate, region);
|
|
74
|
+
const kService = hmac(kRegion, SERVICE);
|
|
75
|
+
const kSigning = hmac(kService, 'request');
|
|
76
|
+
const signature = hmac(kSigning, stringToSign).toString('hex');
|
|
77
|
+
return {
|
|
78
|
+
authorization:
|
|
79
|
+
`HMAC-SHA256 Credential=${accessKeyId}/${scope}, ` +
|
|
80
|
+
`SignedHeaders=${SIGNED_HEADERS}, Signature=${signature}`,
|
|
81
|
+
xDate,
|
|
82
|
+
contentSha256,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function numberValue(value: unknown): number | null {
|
|
87
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
|
|
88
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resetMs(value: unknown): number | null {
|
|
92
|
+
const numeric = numberValue(value);
|
|
93
|
+
if (numeric != null) return numeric <= 0 ? null : numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
|
|
94
|
+
if (typeof value !== 'string') return null;
|
|
95
|
+
const parsed = Date.parse(value);
|
|
96
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function window(label: string, usedPercent: number, reset: unknown): QuotaWindow {
|
|
100
|
+
return {
|
|
101
|
+
label,
|
|
102
|
+
remainingPercent: Math.max(0, Math.min(100, 100 - usedPercent)),
|
|
103
|
+
resetMs: resetMs(reset),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseVolcengineAgentPlan(json: any): QuotaInfo | null {
|
|
108
|
+
const result = json?.Result ?? json;
|
|
109
|
+
const windows: QuotaWindow[] = [];
|
|
110
|
+
for (const [field, label] of [
|
|
111
|
+
['AFPFiveHour', '5h'],
|
|
112
|
+
['AFPWeekly', '7d'],
|
|
113
|
+
['AFPMonthly', '30d'],
|
|
114
|
+
] as const) {
|
|
115
|
+
const item = result?.[field];
|
|
116
|
+
const quota = numberValue(item?.Quota);
|
|
117
|
+
const used = numberValue(item?.Used);
|
|
118
|
+
if (quota != null && quota > 0 && used != null) {
|
|
119
|
+
windows.push(window(label, (used / quota) * 100, item?.ResetTime));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return windows.length ? { provider: 'volcengine', windows } : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function codingWindow(value: unknown): string | null {
|
|
126
|
+
const label = String(value ?? '').toLowerCase();
|
|
127
|
+
if (['session', '5h', 'fivehour', 'five_hour', 'rolling_5h'].includes(label)) return '5h';
|
|
128
|
+
if (['weekly', 'week', '7d'].includes(label)) return '7d';
|
|
129
|
+
if (['monthly', 'month'].includes(label)) return '30d';
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function parseVolcengineCodingPlan(json: any): QuotaInfo | null {
|
|
134
|
+
const result = json?.Result ?? json;
|
|
135
|
+
const items = result?.QuotaUsage ?? result?.Usages ?? result?.Details;
|
|
136
|
+
if (!Array.isArray(items)) return null;
|
|
137
|
+
const windows = items.flatMap((item: any): QuotaWindow[] => {
|
|
138
|
+
const label = codingWindow(item?.Level ?? item?.Type ?? item?.Period ?? item?.Label ?? item?.Window);
|
|
139
|
+
const used = numberValue(item?.Percent ?? item?.UsedPercent ?? item?.UsagePercent);
|
|
140
|
+
return label && used != null
|
|
141
|
+
? [window(label, used, item?.ResetTime ?? item?.ResetTimestamp)]
|
|
142
|
+
: [];
|
|
143
|
+
});
|
|
144
|
+
return windows.length ? { provider: 'volcengine', windows } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function hasResponseError(json: any): boolean {
|
|
148
|
+
const error = json?.ResponseMetadata?.Error ?? json?.Error;
|
|
149
|
+
return Boolean(error?.Code || error?.Message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function callVolcengine(
|
|
153
|
+
action: string,
|
|
154
|
+
region: string,
|
|
155
|
+
accessKeyId: string,
|
|
156
|
+
secretAccessKey: string,
|
|
157
|
+
request: typeof fetch,
|
|
158
|
+
): Promise<any | null> {
|
|
159
|
+
const canonicalQuery = volcengineCanonicalQuery(action, region);
|
|
160
|
+
const signed = signVolcengineRequest(accessKeyId, secretAccessKey, region, canonicalQuery, new Date());
|
|
161
|
+
const response = await request(`https://${HOST}/?${canonicalQuery}`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: signed.authorization,
|
|
165
|
+
'Content-Type': CONTENT_TYPE,
|
|
166
|
+
'X-Date': signed.xDate,
|
|
167
|
+
'X-Content-Sha256': signed.contentSha256,
|
|
168
|
+
},
|
|
169
|
+
body: '',
|
|
170
|
+
signal: AbortSignal.timeout(15_000),
|
|
171
|
+
});
|
|
172
|
+
const json = await response.json().catch(() => null);
|
|
173
|
+
return response.ok && json && !hasResponseError(json) ? json : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function fetchVolcengineQuota(
|
|
177
|
+
baseUrl: string,
|
|
178
|
+
accessKeyId: string,
|
|
179
|
+
secretAccessKey: string,
|
|
180
|
+
request: typeof fetch = fetch,
|
|
181
|
+
plan: 'agent' | 'coding' = 'agent',
|
|
182
|
+
): Promise<QuotaInfo | null> {
|
|
183
|
+
if (!accessKeyId || !secretAccessKey) return null;
|
|
184
|
+
const region = volcengineRegion(baseUrl);
|
|
185
|
+
if (plan === 'coding') {
|
|
186
|
+
const coding = await callVolcengine('GetCodingPlanUsage', region, accessKeyId, secretAccessKey, request);
|
|
187
|
+
return parseVolcengineCodingPlan(coding);
|
|
188
|
+
}
|
|
189
|
+
const agent = await callVolcengine('GetAFPUsage', region, accessKeyId, secretAccessKey, request);
|
|
190
|
+
return parseVolcengineAgentPlan(agent);
|
|
191
|
+
}
|