@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/assembler.ts +127 -0
  4. package/config.ts +201 -0
  5. package/i18n.ts +693 -0
  6. package/index.ts +99 -0
  7. package/menu/items.ts +264 -0
  8. package/menu/panels.ts +91 -0
  9. package/menu/settings-list.ts +84 -0
  10. package/menu/theme.ts +31 -0
  11. package/menu/toolkit-menu.ts +139 -0
  12. package/module.ts +156 -0
  13. package/modules/eyes/chain.ts +173 -0
  14. package/modules/eyes/config.ts +319 -0
  15. package/modules/eyes/index.ts +161 -0
  16. package/modules/eyes/menu.ts +828 -0
  17. package/modules/eyes/pi-model-backend.ts +245 -0
  18. package/modules/eyes/resilience.ts +161 -0
  19. package/modules/eyes/vision-bridge.ts +232 -0
  20. package/modules/eyes/vision-cache.ts +86 -0
  21. package/modules/eyes/vision-json.ts +19 -0
  22. package/modules/eyes/vision-preprocess.ts +192 -0
  23. package/modules/eyes/vision-probe.ts +21 -0
  24. package/modules/eyes/vision-prompt.ts +90 -0
  25. package/modules/eyes/vision-tool.ts +59 -0
  26. package/modules/eyes/vision-types.ts +33 -0
  27. package/modules/index.ts +10 -0
  28. package/modules/subagents/agents/researcher.md +51 -0
  29. package/modules/subagents/agents/scout.md +40 -0
  30. package/modules/subagents/agents/worker.md +79 -0
  31. package/modules/subagents/config.json.example +6 -0
  32. package/modules/subagents/config.ts +144 -0
  33. package/modules/subagents/index.ts +91 -0
  34. package/modules/subagents/menu.ts +388 -0
  35. package/modules/subagents/src/activity.ts +511 -0
  36. package/modules/subagents/src/agents.ts +126 -0
  37. package/modules/subagents/src/command.ts +37 -0
  38. package/modules/subagents/src/dependencies.ts +246 -0
  39. package/modules/subagents/src/diagnostics.ts +13 -0
  40. package/modules/subagents/src/display.ts +94 -0
  41. package/modules/subagents/src/headless.ts +342 -0
  42. package/modules/subagents/src/herdr.ts +203 -0
  43. package/modules/subagents/src/index.ts +2798 -0
  44. package/modules/subagents/src/inspect-tool.ts +839 -0
  45. package/modules/subagents/src/launch-config.ts +196 -0
  46. package/modules/subagents/src/layout-budget.ts +26 -0
  47. package/modules/subagents/src/list-tool.ts +292 -0
  48. package/modules/subagents/src/message-tool.ts +808 -0
  49. package/modules/subagents/src/names.ts +17 -0
  50. package/modules/subagents/src/pane-layout.ts +49 -0
  51. package/modules/subagents/src/params.ts +145 -0
  52. package/modules/subagents/src/renderers.ts +181 -0
  53. package/modules/subagents/src/result.ts +43 -0
  54. package/modules/subagents/src/retention.ts +114 -0
  55. package/modules/subagents/src/route-error.ts +212 -0
  56. package/modules/subagents/src/routing.ts +235 -0
  57. package/modules/subagents/src/runtime-registry.ts +159 -0
  58. package/modules/subagents/src/session.ts +801 -0
  59. package/modules/subagents/src/status.ts +513 -0
  60. package/modules/subagents/src/stop-tool.ts +235 -0
  61. package/modules/subagents/src/subagent-done.ts +590 -0
  62. package/modules/subagents/src/subagent-tool.ts +1155 -0
  63. package/modules/subagents/src/surface.ts +355 -0
  64. package/modules/subagents/src/team-dispatch-tool.ts +219 -0
  65. package/modules/subagents/src/team.ts +232 -0
  66. package/modules/subagents/src/tmux.ts +210 -0
  67. package/modules/subagents/src/tools/safe-bash.ts +72 -0
  68. package/modules/subagents/src/types.ts +131 -0
  69. package/modules/tui/adapter/provider-usage.ts +143 -0
  70. package/modules/tui/config.ts +50 -0
  71. package/modules/tui/index.ts +120 -0
  72. package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
  73. package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
  74. package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
  75. package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
  76. package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
  77. package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
  78. package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
  79. package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
  80. package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
  81. package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
  82. package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
  83. package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
  84. package/modules/tui/kernel/usage-core.ts +2 -0
  85. package/modules/tui/kernel/usage-node.ts +2 -0
  86. package/modules/tui/menu.ts +418 -0
  87. package/modules/tui/plugin/editor.ts +396 -0
  88. package/modules/tui/plugin/footer.ts +161 -0
  89. package/modules/tui/plugin/index.ts +30 -0
  90. package/modules/tui/plugin/lifecycle.ts +637 -0
  91. package/modules/tui/plugin/package-order.ts +169 -0
  92. package/modules/tui/plugin/screen-transition.ts +203 -0
  93. package/modules/tui/plugin/settings-config.ts +297 -0
  94. package/modules/tui/plugin/status-sources.ts +49 -0
  95. package/modules/tui/plugin/transition-gate.ts +261 -0
  96. package/modules/tui/renderer/custom-header.ts +157 -0
  97. package/modules/tui/renderer/editor.ts +70 -0
  98. package/modules/tui/renderer/header.ts +72 -0
  99. package/modules/tui/renderer/icons.ts +149 -0
  100. package/modules/tui/renderer/pi-installer-logo.ts +194 -0
  101. package/modules/tui/status/auto-compaction.ts +67 -0
  102. package/modules/tui/status/project-status.ts +655 -0
  103. package/modules/tui/status/provider-status.ts +120 -0
  104. package/modules/tui/status/runtime-status.ts +307 -0
  105. package/modules/tui/status/session-status.ts +325 -0
  106. package/modules/tui/status/status-config.ts +95 -0
  107. package/modules/tui/status/status-segments.ts +263 -0
  108. package/modules/tui/status/turn-telemetry.ts +466 -0
  109. package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
  110. package/modules/tui/themes/UPSTREAM.md +7 -0
  111. package/modules/tui/themes/catppuccin-latte.json +80 -0
  112. package/modules/tui/themes/catppuccin-mocha.json +79 -0
  113. package/modules/tui/themes/crimson-noir.json +85 -0
  114. package/modules/tui/themes/dracula.json +79 -0
  115. package/modules/tui/themes/everforest-dark.json +85 -0
  116. package/modules/tui/themes/gruvbox-dark.json +85 -0
  117. package/modules/tui/themes/gruvbox-light.json +85 -0
  118. package/modules/tui/themes/matrix.json +85 -0
  119. package/modules/tui/themes/nord.json +85 -0
  120. package/modules/tui/themes/one-dark.json +85 -0
  121. package/modules/tui/themes/rose-pine-dawn.json +85 -0
  122. package/modules/tui/themes/rose-pine.json +85 -0
  123. package/modules/tui/themes/solarized-dark.json +85 -0
  124. package/modules/tui/themes/solarized-light.json +85 -0
  125. package/modules/tui/themes/tokyo-night-storm.json +79 -0
  126. package/modules/tui/themes/tokyo-night.json +79 -0
  127. package/package.json +28 -0
  128. package/services.ts +38 -0
  129. package/toolkit.ts +147 -0
@@ -0,0 +1,237 @@
1
+ // 官方订阅查询只处理协议与响应;OAuth 凭据从哪里读取由宿主适配器决定。
2
+
3
+ import type { ProviderUsage, QuotaWindow } from './provider-contracts.ts';
4
+
5
+ function numberValue(value: unknown): number | null {
6
+ const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
7
+ return Number.isFinite(parsed) ? parsed : null;
8
+ }
9
+
10
+ function resetTime(value: unknown): number | null {
11
+ if (typeof value === 'number' && Number.isFinite(value)) {
12
+ return value < 1_000_000_000_000 ? value * 1000 : value;
13
+ }
14
+ if (typeof value !== 'string') return null;
15
+ const parsed = Date.parse(value);
16
+ return Number.isFinite(parsed) ? parsed : null;
17
+ }
18
+
19
+ function claudeWindowLabel(key: string): string {
20
+ return (
21
+ {
22
+ five_hour: '5h',
23
+ seven_day: '7d',
24
+ seven_day_opus: '7d Opus',
25
+ seven_day_sonnet: '7d Sonnet',
26
+ } as Record<string, string>
27
+ )[key] ?? key;
28
+ }
29
+
30
+ export function parseClaudeSubscription(json: any): ProviderUsage | null {
31
+ if (!json || typeof json !== 'object') return null;
32
+ const windows: QuotaWindow[] = [];
33
+ for (const [key, item] of Object.entries(json)) {
34
+ if (key === 'extra_usage' || !item || typeof item !== 'object') continue;
35
+ const utilization = numberValue((item as any).utilization);
36
+ if (utilization == null) continue;
37
+ windows.push({
38
+ label: claudeWindowLabel(key),
39
+ remainingPercent: Math.max(0, Math.min(100, 100 - utilization)),
40
+ resetMs: resetTime((item as any).resets_at),
41
+ });
42
+ }
43
+
44
+ const extra = json.extra_usage;
45
+ const enabled = extra?.is_enabled === true;
46
+ const monthlyLimit = numberValue(extra?.monthly_limit);
47
+ const usedCredits = numberValue(extra?.used_credits);
48
+ const currency = String(extra?.currency ?? '').toUpperCase() === 'CNY' ? 'CNY' : 'USD';
49
+ const balance =
50
+ enabled && monthlyLimit != null && usedCredits != null
51
+ ? { amount: Math.max(0, monthlyLimit - usedCredits), currency: currency as 'CNY' | 'USD' }
52
+ : undefined;
53
+
54
+ if (!windows.length && !balance) return null;
55
+ return {
56
+ mode: windows.length && balance ? 'hybrid' : windows.length ? 'subscription' : 'api',
57
+ quota: windows.length ? { provider: 'anthropic', windows } : undefined,
58
+ balance,
59
+ };
60
+ }
61
+
62
+ export async function fetchClaudeSubscription(
63
+ accessToken: string,
64
+ request: typeof fetch = fetch,
65
+ ): Promise<ProviderUsage | null> {
66
+ if (!accessToken) return null;
67
+ const response = await request('https://api.anthropic.com/api/oauth/usage', {
68
+ headers: {
69
+ Authorization: `Bearer ${accessToken}`,
70
+ 'anthropic-beta': 'oauth-2025-04-20',
71
+ Accept: 'application/json',
72
+ },
73
+ signal: AbortSignal.timeout(15_000),
74
+ });
75
+ if (!response.ok) return null;
76
+ return parseClaudeSubscription(await response.json().catch(() => null));
77
+ }
78
+
79
+ function codexWindowLabel(seconds: unknown): string {
80
+ const value = numberValue(seconds);
81
+ if (value === 18_000) return '5h';
82
+ if (value === 604_800) return '7d';
83
+ if (value === 2_592_000) return '30d';
84
+ if (value == null) return 'unknown';
85
+ const hours = Math.floor(value / 3600);
86
+ return hours >= 24 ? `${Math.floor(hours / 24)}d` : `${hours}h`;
87
+ }
88
+
89
+ export function parseCodexSubscription(json: any): ProviderUsage | null {
90
+ const rateLimit = json?.rate_limit;
91
+ const windows = [rateLimit?.primary_window, rateLimit?.secondary_window].flatMap(
92
+ (item: any): QuotaWindow[] => {
93
+ const used = numberValue(item?.used_percent);
94
+ return used == null
95
+ ? []
96
+ : [
97
+ {
98
+ label: codexWindowLabel(item?.limit_window_seconds),
99
+ remainingPercent: Math.max(0, Math.min(100, 100 - used)),
100
+ resetMs: resetTime(item?.reset_at),
101
+ },
102
+ ];
103
+ },
104
+ );
105
+ return windows.length ? { mode: 'subscription', quota: { provider: 'codex', windows } } : null;
106
+ }
107
+
108
+ export async function fetchCodexSubscription(
109
+ accessToken: string,
110
+ accountId?: string,
111
+ request: typeof fetch = fetch,
112
+ ): Promise<ProviderUsage | null> {
113
+ if (!accessToken) return null;
114
+ const headers: Record<string, string> = {
115
+ Authorization: `Bearer ${accessToken}`,
116
+ 'User-Agent': 'codex-cli',
117
+ Accept: 'application/json',
118
+ };
119
+ if (accountId) headers['ChatGPT-Account-Id'] = accountId;
120
+ const response = await request('https://chatgpt.com/backend-api/wham/usage', {
121
+ headers,
122
+ signal: AbortSignal.timeout(15_000),
123
+ });
124
+ if (!response.ok) return null;
125
+ return parseCodexSubscription(await response.json().catch(() => null));
126
+ }
127
+
128
+ function geminiCategory(modelId: string): string {
129
+ if (modelId.includes('flash-lite')) return 'Gemini Flash Lite';
130
+ if (modelId.includes('flash')) return 'Gemini Flash';
131
+ if (modelId.includes('pro')) return 'Gemini Pro';
132
+ return modelId || 'unknown';
133
+ }
134
+
135
+ export function parseGeminiSubscription(json: any): ProviderUsage | null {
136
+ if (!Array.isArray(json?.buckets)) return null;
137
+ const categories = new Map<string, { remaining: number; reset: unknown }>();
138
+ for (const bucket of json.buckets) {
139
+ const remaining = numberValue(bucket?.remainingFraction);
140
+ if (remaining == null) continue;
141
+ const category = geminiCategory(String(bucket?.modelId ?? ''));
142
+ const current = categories.get(category);
143
+ if (!current || remaining < current.remaining) {
144
+ categories.set(category, { remaining: Math.max(0, Math.min(1, remaining)), reset: bucket?.resetTime });
145
+ }
146
+ }
147
+ const order = new Map([
148
+ ['Gemini Pro', 0],
149
+ ['Gemini Flash', 1],
150
+ ['Gemini Flash Lite', 2],
151
+ ]);
152
+ const windows = [...categories.entries()]
153
+ .sort(([left], [right]) => (order.get(left) ?? 3) - (order.get(right) ?? 3))
154
+ .map(([label, item]) => ({
155
+ label,
156
+ remainingPercent: item.remaining * 100,
157
+ resetMs: resetTime(item.reset),
158
+ }));
159
+ return windows.length ? { mode: 'subscription', quota: { provider: 'gemini', windows } } : null;
160
+ }
161
+
162
+ function geminiProjectId(value: any): string | null {
163
+ if (typeof value === 'string') return value;
164
+ if (!value || typeof value !== 'object') return null;
165
+ return typeof value.id === 'string'
166
+ ? value.id
167
+ : typeof value.projectId === 'string'
168
+ ? value.projectId
169
+ : null;
170
+ }
171
+
172
+ export async function fetchGeminiSubscription(
173
+ accessToken: string,
174
+ request: typeof fetch = fetch,
175
+ ): Promise<ProviderUsage | null> {
176
+ if (!accessToken) return null;
177
+ const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
178
+ const load = await request('https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', {
179
+ method: 'POST',
180
+ headers,
181
+ body: JSON.stringify({ metadata: { ideType: 'GEMINI_CLI', pluginType: 'GEMINI' } }),
182
+ signal: AbortSignal.timeout(15_000),
183
+ });
184
+ if (!load.ok) return null;
185
+ const loadJson: any = await load.json().catch(() => null);
186
+ const project = geminiProjectId(loadJson?.cloudaicompanionProject);
187
+ const quota = await request('https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota', {
188
+ method: 'POST',
189
+ headers,
190
+ body: JSON.stringify(project ? { project } : {}),
191
+ signal: AbortSignal.timeout(15_000),
192
+ });
193
+ if (!quota.ok) return null;
194
+ return parseGeminiSubscription(await quota.json().catch(() => null));
195
+ }
196
+
197
+ export function parseCopilotSubscription(json: any): ProviderUsage | null {
198
+ const premium = json?.quota_snapshots?.premium_interactions;
199
+ const remaining = numberValue(premium?.percent_remaining);
200
+ if (remaining == null) return null;
201
+ return {
202
+ mode: 'subscription',
203
+ quota: {
204
+ provider: 'github-copilot',
205
+ windows: [
206
+ {
207
+ label: 'Premium',
208
+ remainingPercent: Math.max(0, Math.min(100, remaining)),
209
+ resetMs: resetTime(json?.quota_reset_date),
210
+ },
211
+ ],
212
+ },
213
+ };
214
+ }
215
+
216
+ export async function fetchCopilotSubscription(
217
+ githubToken: string,
218
+ githubDomain = 'github.com',
219
+ request: typeof fetch = fetch,
220
+ ): Promise<ProviderUsage | null> {
221
+ if (!githubToken) return null;
222
+ const apiBase =
223
+ githubDomain === 'github.com' ? 'https://api.github.com' : `https://${githubDomain}/api/v3`;
224
+ const response = await request(`${apiBase}/copilot_internal/user`, {
225
+ headers: {
226
+ Authorization: `token ${githubToken}`,
227
+ 'Content-Type': 'application/json',
228
+ 'editor-version': 'vscode/1.110.1',
229
+ 'editor-plugin-version': 'copilot-chat/0.38.2',
230
+ 'user-agent': 'GitHubCopilotChat/0.38.2',
231
+ 'x-github-api-version': '2025-10-01',
232
+ },
233
+ signal: AbortSignal.timeout(15_000),
234
+ });
235
+ if (!response.ok) return null;
236
+ return parseCopilotSubscription(await response.json().catch(() => null));
237
+ }
@@ -0,0 +1,367 @@
1
+ // 共享供应商目录。品牌收录与查询能力是两件事:目录负责识别和分组,
2
+ // queryKind 只标记已经有确定协议实现的查询后端。
3
+ //
4
+ // 主目录以 cc-switch 0b5da510168914b251481654a568c3ffacd62cf4 的
5
+ // Claude Code 77 个原始预设为固定快照,再通过 brandId 归并品牌;
6
+ // ZenMux 来自同一提交的 coding_plan 实现。
7
+
8
+ export type ProviderGroup = 'official' | 'relay';
9
+
10
+ /** 内部维护分类;不参与 Pi 界面渲染。 */
11
+ export type ProviderCategory = 'official' | 'major-relay' | 'small-relay' | 'unknown';
12
+ export type MaintenancePriority = 'P0' | 'P1' | 'P2' | 'P3';
13
+ export type ProviderQueryStatus = 'implemented' | 'recognition-only';
14
+
15
+ export const UNKNOWN_PROVIDER_METADATA = Object.freeze({
16
+ category: 'unknown' as const,
17
+ maintenancePriority: 'P3' as const,
18
+ queryStatus: 'recognition-only' as const,
19
+ });
20
+
21
+ export type BuiltinQueryKind =
22
+ | 'claude-subscription'
23
+ | 'codex-subscription'
24
+ | 'gemini-subscription'
25
+ | 'copilot-subscription'
26
+ | 'grok-subscription'
27
+ | 'zhipu'
28
+ | 'kimi-api'
29
+ | 'kimi-coding'
30
+ | 'volcengine-agent'
31
+ | 'volcengine-coding'
32
+ | 'minimax-cn'
33
+ | 'minimax-en'
34
+ | 'deepseek'
35
+ | 'stepfun'
36
+ | 'siliconflow-cn'
37
+ | 'siliconflow-en'
38
+ | 'openrouter'
39
+ | 'novita'
40
+ | 'sub2api';
41
+
42
+ /** 查询所需凭据来源;none 表示目前只有品牌识别,没有确定的查询协议。 */
43
+ export type QueryAccess = 'api-key' | 'extra-credentials' | 'host-oauth' | 'generic' | 'none';
44
+
45
+ export interface ProviderRoute {
46
+ /** 精确主机名;以 *. 开头时允许该域及其子域。 */
47
+ host: string;
48
+ /** 同一主机承载多个产品时,用路径前缀消歧。 */
49
+ pathPrefix?: string;
50
+ }
51
+
52
+ export interface ProviderCatalogEntry {
53
+ /** 路由/产品变体标识;同一品牌的国内站、国际站或套餐可以各自拥有独立条目。 */
54
+ id: string;
55
+ /** 去重后的品牌标识;界面和跨宿主统计以此为准。 */
56
+ brandId: string;
57
+ displayName: string;
58
+ group: ProviderGroup;
59
+ /** 内部维护字段;不得传入状态栏或 Header 的展示数据。 */
60
+ category: Exclude<ProviderCategory, 'unknown'>;
61
+ maintenancePriority: Exclude<MaintenancePriority, 'P3'>;
62
+ queryStatus: ProviderQueryStatus;
63
+ /** 仅描述站点、套餐或鉴权差异,不作为供应商品牌展示。 */
64
+ variant?: string;
65
+ /** cc-switch 中的原始预设名称;非预设查询后端为空。 */
66
+ presetName?: string;
67
+ /** 其它宿主中指向同一计费服务的预设名称。 */
68
+ aliases?: readonly string[];
69
+ routes: readonly ProviderRoute[];
70
+ queryAccess: QueryAccess;
71
+ queryKind?: BuiltinQueryKind;
72
+ }
73
+
74
+ export interface ProviderBrand {
75
+ id: string;
76
+ displayName: string;
77
+ group: ProviderGroup;
78
+ variants: readonly ProviderCatalogEntry[];
79
+ }
80
+
81
+ const route = (host: string, pathPrefix?: string): ProviderRoute => ({ host, pathPrefix });
82
+
83
+ // 分类是维护者审核结果,不根据域名、价格或模型数量自动推断。
84
+ const BRAND_METADATA = {
85
+ anthropic: { displayName: 'Anthropic', category: 'official' },
86
+ kimi: { displayName: 'Kimi', category: 'official' },
87
+ volcengine: { displayName: 'Volcengine', category: 'official' },
88
+ byteplus: { displayName: 'BytePlus', category: 'official' },
89
+ 'doubao-seed': { displayName: 'Doubao', category: 'official' },
90
+ gemini: { displayName: 'Gemini', category: 'official' },
91
+ deepseek: { displayName: 'DeepSeek', category: 'official' },
92
+ zhipu: { displayName: 'Zhipu', category: 'official' },
93
+ 'baidu-qianfan': { displayName: 'Baidu', category: 'official' },
94
+ bailian: { displayName: 'Bailian', category: 'official' },
95
+ stepfun: { displayName: 'StepFun', category: 'official' },
96
+ 'kat-coder': { displayName: 'KAT-Coder', category: 'official' },
97
+ longcat: { displayName: 'LongCat', category: 'official' },
98
+ minimax: { displayName: 'MiniMax', category: 'official' },
99
+ bailing: { displayName: 'BaiLing', category: 'official' },
100
+ 'github-copilot': { displayName: 'GitHub', category: 'official' },
101
+ codex: { displayName: 'OpenAI', category: 'official' },
102
+ xai: { displayName: 'xAI', category: 'official' },
103
+ 'xiaomi-mimo': { displayName: 'Xiaomi', category: 'official' },
104
+ 'aws-bedrock': { displayName: 'AWS', category: 'official' },
105
+ openai: { displayName: 'OpenAI', category: 'official' },
106
+ 'azure-openai': { displayName: 'Azure', category: 'official' },
107
+ 'tencent-hunyuan': { displayName: 'Tencent', category: 'official' },
108
+ 'nous-research': { displayName: 'Nous Research', category: 'official' },
109
+ packycode: { displayName: 'PackyCode', category: 'small-relay' },
110
+ zetaapi: { displayName: 'ZetaAPI', category: 'small-relay' },
111
+ apinebula: { displayName: 'APINebula', category: 'small-relay' },
112
+ aicodemirror: { displayName: 'AICodeMirror', category: 'small-relay' },
113
+ patewayai: { displayName: 'PatewayAI', category: 'small-relay' },
114
+ fennoai: { displayName: 'FennoAI', category: 'small-relay' },
115
+ runapi: { displayName: 'RunAPI', category: 'small-relay' },
116
+ shengsuanyun: { displayName: 'Shengsuanyun', category: 'small-relay' },
117
+ aigocode: { displayName: 'AIGoCode', category: 'small-relay' },
118
+ qiniu: { displayName: 'Qiniu', category: 'small-relay' },
119
+ aicoding: { displayName: 'AICoding', category: 'small-relay' },
120
+ subrouter: { displayName: 'SubRouter', category: 'small-relay' },
121
+ apikey: { displayName: 'ApiKey', category: 'small-relay' },
122
+ claudeapi: { displayName: 'ClaudeAPI', category: 'small-relay' },
123
+ code0: { displayName: 'Code0', category: 'small-relay' },
124
+ teamorouter: { displayName: 'TeamoRouter', category: 'small-relay' },
125
+ ppio: { displayName: 'PPIO', category: 'small-relay' },
126
+ claudecn: { displayName: 'ClaudeCN', category: 'small-relay' },
127
+ siliconflow: { displayName: 'SiliconFlow', category: 'major-relay' },
128
+ a6api: { displayName: 'A6API', category: 'small-relay' },
129
+ atlascloud: { displayName: 'AtlasCloud', category: 'small-relay' },
130
+ compshare: { displayName: 'Compshare', category: 'small-relay' },
131
+ ccsub: { displayName: 'CCSub', category: 'small-relay' },
132
+ sssaicode: { displayName: 'SSSAiCode', category: 'small-relay' },
133
+ micu: { displayName: 'Micu', category: 'small-relay' },
134
+ rightcode: { displayName: 'RightCode', category: 'small-relay' },
135
+ etok: { displayName: 'ETok', category: 'small-relay' },
136
+ cubence: { displayName: 'Cubence', category: 'small-relay' },
137
+ crazyrouter: { displayName: 'CrazyRouter', category: 'small-relay' },
138
+ dmxapi: { displayName: 'DMXAPI', category: 'small-relay' },
139
+ sudocode: { displayName: 'SudoCode', category: 'small-relay' },
140
+ xycai: { displayName: 'XycAi', category: 'small-relay' },
141
+ amux: { displayName: 'Amux', category: 'small-relay' },
142
+ 'opencode-go': { displayName: 'OpenCode Go', category: 'small-relay' },
143
+ modelscope: { displayName: 'ModelScope', category: 'major-relay' },
144
+ aihubmix: { displayName: 'AiHubMix', category: 'major-relay' },
145
+ cherryin: { displayName: 'CherryIN', category: 'small-relay' },
146
+ relaxycode: { displayName: 'RelaxyCode', category: 'small-relay' },
147
+ eflowcode: { displayName: 'E-FlowCode', category: 'small-relay' },
148
+ openrouter: { displayName: 'OpenRouter', category: 'major-relay' },
149
+ therouter: { displayName: 'TheRouter', category: 'small-relay' },
150
+ novita: { displayName: 'Novita', category: 'major-relay' },
151
+ nvidia: { displayName: 'NVIDIA', category: 'small-relay' },
152
+ pipellm: { displayName: 'PIPELLM', category: 'small-relay' },
153
+ jiekou: { displayName: 'JieKou', category: 'small-relay' },
154
+ zenmux: { displayName: 'ZenMux', category: 'major-relay' },
155
+ 'together-ai': { displayName: 'Together', category: 'major-relay' },
156
+ 'new-api': { displayName: 'New API', category: 'small-relay' },
157
+ } as const satisfies Record<string, Pick<ProviderCatalogEntry, 'displayName' | 'category'>>;
158
+
159
+ type ProviderVariant = Pick<ProviderCatalogEntry, 'id' | 'routes' | 'presetName' | 'variant' | 'aliases' | 'queryKind'> & {
160
+ brandId: keyof typeof BRAND_METADATA;
161
+ queryAccess?: QueryAccess;
162
+ };
163
+
164
+ const PROVIDER_VARIANTS: readonly ProviderVariant[] = [
165
+ { id: 'anthropic', brandId: 'anthropic', presetName: 'Claude Official', routes: [route('api.anthropic.com')], queryAccess: 'host-oauth', queryKind: 'claude-subscription', aliases: ['Claude Desktop Official'] },
166
+ {
167
+ id: 'kimi-api-cn', brandId: 'kimi', presetName: 'Kimi', variant: 'API (China)',
168
+ routes: [route('api.moonshot.cn')], queryAccess: 'api-key', queryKind: 'kimi-api',
169
+ },
170
+ {
171
+ id: 'kimi-api-en', brandId: 'kimi', variant: 'API (International)',
172
+ routes: [route('api.moonshot.ai')], queryAccess: 'api-key', queryKind: 'kimi-api',
173
+ },
174
+ {
175
+ id: 'kimi', brandId: 'kimi', presetName: 'Kimi For Coding', variant: 'Coding Plan',
176
+ routes: [route('api.kimi.com', '/coding')], queryAccess: 'api-key', queryKind: 'kimi-coding',
177
+ },
178
+ {
179
+ id: 'volcengine-agent-plan', brandId: 'volcengine', presetName: '火山 Agent Plan', variant: 'Agent Plan',
180
+ routes: [route('ark.cn-beijing.volces.com', '/api/plan')], queryAccess: 'extra-credentials', queryKind: 'volcengine-agent', aliases: ['火山Agentplan'],
181
+ },
182
+ {
183
+ id: 'volcengine-coding-plan', brandId: 'volcengine', presetName: '火山 Coding Plan', variant: 'Coding Plan',
184
+ routes: [route('ark.cn-beijing.volces.com', '/api/coding')], queryAccess: 'extra-credentials', queryKind: 'volcengine-coding',
185
+ },
186
+ { id: 'byteplus', brandId: 'byteplus', presetName: 'BytePlus', routes: [route('ark.ap-southeast.bytepluses.com')] },
187
+ { id: 'doubao-seed', brandId: 'doubao-seed', presetName: 'DouBaoSeed', routes: [route('ark.cn-beijing.volces.com')] },
188
+ { id: 'gemini', brandId: 'gemini', presetName: 'Gemini Native', routes: [route('generativelanguage.googleapis.com')], queryAccess: 'host-oauth', queryKind: 'gemini-subscription', aliases: ['Google Official'] },
189
+ { id: 'deepseek', brandId: 'deepseek', presetName: 'DeepSeek', routes: [route('api.deepseek.com')], queryAccess: 'api-key', queryKind: 'deepseek' },
190
+ { id: 'zhipu', brandId: 'zhipu', presetName: 'Zhipu GLM', variant: 'China', routes: [route('*.bigmodel.cn')], queryAccess: 'api-key', queryKind: 'zhipu' },
191
+ { id: 'z.ai', brandId: 'zhipu', presetName: 'Zhipu GLM en', variant: 'International', routes: [route('api.z.ai')], queryAccess: 'api-key', queryKind: 'zhipu' },
192
+ { id: 'baidu-qianfan-coding', brandId: 'baidu-qianfan', presetName: 'Baidu Qianfan Coding Plan', variant: 'Coding Plan', routes: [route('qianfan.baidubce.com', '/anthropic/coding')] },
193
+ { id: 'baidu-qianfan-token-plan', brandId: 'baidu-qianfan', presetName: 'Baidu Qianfan Token Plan', variant: 'Token Plan', routes: [route('qianfan.baidubce.com', '/anthropic/tokenplan')] },
194
+ { id: 'bailian', brandId: 'bailian', presetName: 'Bailian', variant: 'API', routes: [route('dashscope.aliyuncs.com')], aliases: ['Qwen Coder'] },
195
+ { id: 'bailian-coding', brandId: 'bailian', presetName: 'Bailian For Coding', variant: 'Coding Plan', routes: [route('coding.dashscope.aliyuncs.com')] },
196
+ { id: 'stepfun-cn', brandId: 'stepfun', presetName: 'StepFun', variant: 'China', routes: [route('api.stepfun.com')], queryAccess: 'api-key', queryKind: 'stepfun', aliases: ['StepFun Step Plan'] },
197
+ { id: 'stepfun-en', brandId: 'stepfun', presetName: 'StepFun en', variant: 'International', routes: [route('api.stepfun.ai')], queryAccess: 'api-key', queryKind: 'stepfun' },
198
+ { id: 'kat-coder', brandId: 'kat-coder', presetName: 'KAT-Coder', routes: [route('vanchin.streamlake.ai')] },
199
+ { id: 'longcat', brandId: 'longcat', presetName: 'Longcat', routes: [route('api.longcat.chat')] },
200
+ { id: 'minimax-cn', brandId: 'minimax', presetName: 'MiniMax', variant: 'China', routes: [route('api.minimaxi.com')], queryAccess: 'api-key', queryKind: 'minimax-cn' },
201
+ { id: 'minimax-en', brandId: 'minimax', presetName: 'MiniMax en', variant: 'International', routes: [route('api.minimax.io')], queryAccess: 'api-key', queryKind: 'minimax-en' },
202
+ { id: 'bailing', brandId: 'bailing', presetName: 'BaiLing', routes: [route('api.tbox.cn')] },
203
+ { id: 'github-copilot', brandId: 'github-copilot', presetName: 'GitHub Copilot', routes: [route('api.githubcopilot.com')], queryAccess: 'host-oauth', queryKind: 'copilot-subscription' },
204
+ { id: 'codex', brandId: 'codex', presetName: 'Codex', routes: [route('chatgpt.com', '/backend-api')], queryAccess: 'host-oauth', queryKind: 'codex-subscription', aliases: ['openai-codex'] },
205
+ { id: 'xai', brandId: 'xai', presetName: 'xAI (Grok)', routes: [route('api.x.ai')], queryAccess: 'host-oauth', queryKind: 'grok-subscription', aliases: ['xAI (Grok) OAuth'] },
206
+ { id: 'xiaomi-mimo', brandId: 'xiaomi-mimo', presetName: 'Xiaomi MiMo', variant: 'API', routes: [route('api.xiaomimimo.com')] },
207
+ { id: 'xiaomi-mimo-token-plan', brandId: 'xiaomi-mimo', presetName: 'Xiaomi MiMo Token Plan (China)', variant: 'Token Plan (China)', routes: [route('token-plan-cn.xiaomimimo.com')] },
208
+ { id: 'aws-bedrock-aksk', brandId: 'aws-bedrock', presetName: 'AWS Bedrock (AKSK)', variant: 'AK/SK', routes: [route('bedrock-runtime.*.amazonaws.com')] },
209
+ { id: 'aws-bedrock-api-key', brandId: 'aws-bedrock', presetName: 'AWS Bedrock (API Key)', variant: 'API Key', routes: [route('bedrock-runtime.*.amazonaws.com')] },
210
+ // cc-switch 其它宿主相对 Claude 目录新增的第一方服务。
211
+ { id: 'openai', brandId: 'openai', routes: [route('api.openai.com')], queryAccess: 'host-oauth', aliases: ['OpenAI Official'] },
212
+ { id: 'azure-openai', brandId: 'azure-openai', routes: [route('*.openai.azure.com')], aliases: ['Azure OpenAI'] },
213
+ { id: 'tencent-hunyuan', brandId: 'tencent-hunyuan', routes: [route('tokenhub.tencentmaas.com')], aliases: ['Tencent Hunyuan'] },
214
+ { id: 'nous-research', brandId: 'nous-research', routes: [route('inference-api.nousresearch.com')], aliases: ['Nous Research'] },
215
+ { id: 'packycode', brandId: 'packycode', presetName: 'PackyCode', routes: [route('www.packyapi.ai')] },
216
+ { id: 'zetaapi', brandId: 'zetaapi', presetName: 'ZetaAPI', routes: [route('api.zetaapi.ai')] },
217
+ { id: 'apinebula', brandId: 'apinebula', presetName: 'APINebula', routes: [route('apinebula.ai')] },
218
+ { id: 'aicodemirror', brandId: 'aicodemirror', presetName: 'AICodeMirror', routes: [route('api.aicodemirror.ai')] },
219
+ { id: 'patewayai', brandId: 'patewayai', presetName: 'PatewayAI', routes: [route('api.pateway.ai')] },
220
+ { id: 'fennoai', brandId: 'fennoai', presetName: 'FennoAI', routes: [route('api.fenno.ai')] },
221
+ { id: 'runapi', brandId: 'runapi', presetName: 'RunAPI', routes: [route('runapi.host'), route('runapi.co')] },
222
+ { id: 'shengsuanyun', brandId: 'shengsuanyun', presetName: 'Shengsuanyun', routes: [route('router.shengsuanyun.com')] },
223
+ { id: 'aigocode', brandId: 'aigocode', presetName: 'AIGoCode', routes: [route('api.aigocode.app')] },
224
+ { id: 'qiniu', brandId: 'qiniu', presetName: 'Qiniu', routes: [route('api.qnaigc.com')] },
225
+ { id: 'aicoding', brandId: 'aicoding', presetName: 'AICoding', routes: [route('api.aicoding.inc')] },
226
+ { id: 'subrouter', brandId: 'subrouter', presetName: 'SubRouter', routes: [route('subrouter.ai')] },
227
+ { id: 'apikey', brandId: 'apikey', presetName: 'APIKEY.FUN', routes: [route('api.apikey.fun'), route('slb.apikey.fun')], queryAccess: 'api-key', queryKind: 'sub2api', aliases: ['apikey.fun'] },
228
+ { id: 'claudeapi', brandId: 'claudeapi', presetName: 'ClaudeAPI', routes: [route('gw.apito.ai')] },
229
+ { id: 'code0', brandId: 'code0', presetName: 'Code0', routes: [route('code0.ai')] },
230
+ { id: 'teamorouter', brandId: 'teamorouter', presetName: 'TeamoRouter', routes: [route('api.teamorouter.cn'), route('api.teamorouter.com')] },
231
+ { id: 'ppio', brandId: 'ppio', presetName: 'PPIO', routes: [route('api.ppio.com')] },
232
+ { id: 'claudecn', brandId: 'claudecn', presetName: 'ClaudeCN', routes: [route('claudecn.top')] },
233
+ { id: 'siliconflow-cn', brandId: 'siliconflow', presetName: 'SiliconFlow', variant: 'China', routes: [route('api.siliconflow.cn')], queryAccess: 'api-key', queryKind: 'siliconflow-cn' },
234
+ { id: 'siliconflow-en', brandId: 'siliconflow', presetName: 'SiliconFlow en', variant: 'International', routes: [route('api.siliconflow.com')], queryAccess: 'api-key', queryKind: 'siliconflow-en' },
235
+ { id: 'a6api', brandId: 'a6api', presetName: 'A6API', routes: [route('api.a6api.com')] },
236
+ { id: 'atlascloud', brandId: 'atlascloud', presetName: 'AtlasCloud', routes: [route('api.atlascloud.ai')] },
237
+ { id: 'compshare', brandId: 'compshare', presetName: 'Compshare', variant: 'API', routes: [route('api.modelverse.cn')] },
238
+ { id: 'compshare-coding', brandId: 'compshare', presetName: 'Compshare Coding Plan', variant: 'Coding Plan', routes: [route('cp.compshare.cn')] },
239
+ { id: 'ccsub', brandId: 'ccsub', presetName: 'CCSub', routes: [route('www.ccsub.net')] },
240
+ { id: 'sssaicode', brandId: 'sssaicode', presetName: 'SSSAiCode', routes: [route('node-hk.sssaicodeapi.com')] },
241
+ { id: 'micu', brandId: 'micu', presetName: 'Micu', routes: [route('www.micuapi.ai')] },
242
+ { id: 'rightcode', brandId: 'rightcode', presetName: 'RightCode', routes: [route('www.rightapi.ai')] },
243
+ { id: 'etok', brandId: 'etok', presetName: 'ETok.ai', routes: [route('api.etok.ai')] },
244
+ { id: 'cubence', brandId: 'cubence', presetName: 'Cubence', routes: [route('api.cubence.com')] },
245
+ { id: 'crazyrouter', brandId: 'crazyrouter', presetName: 'CrazyRouter', routes: [route('cn.crazyrouter.com')] },
246
+ { id: 'dmxapi', brandId: 'dmxapi', presetName: 'DMXAPI', routes: [route('www.dmxapi.cn')] },
247
+ { id: 'sudocode-chat', brandId: 'sudocode', presetName: 'SudoCode.chat', variant: '.chat', routes: [route('api.sudocode.chat')] },
248
+ { id: 'sudocode-us', brandId: 'sudocode', presetName: 'SudoCode.us', variant: '.us', routes: [route('sudocode.us')] },
249
+ { id: 'xycai', brandId: 'xycai', presetName: 'XycAi', routes: [route('apicdn.xycai.us')] },
250
+ { id: 'amux', brandId: 'amux', presetName: 'Amux', routes: [route('api.amux.ai')] },
251
+ { id: 'opencode-go', brandId: 'opencode-go', presetName: 'OpenCode Go', routes: [route('opencode.ai', '/zen/go')] },
252
+ { id: 'modelscope', brandId: 'modelscope', presetName: 'ModelScope', routes: [route('api-inference.modelscope.cn')] },
253
+ { id: 'aihubmix', brandId: 'aihubmix', presetName: 'AiHubMix', routes: [route('aihubmix.com')] },
254
+ { id: 'cherryin', brandId: 'cherryin', presetName: 'CherryIN', routes: [route('open.cherryin.net')] },
255
+ { id: 'relaxycode', brandId: 'relaxycode', presetName: 'RelaxyCode', routes: [route('www.relaxycode.com')] },
256
+ { id: 'eflowcode', brandId: 'eflowcode', presetName: 'E-FlowCode', routes: [route('e-flowcode.cc')] },
257
+ { id: 'openrouter', brandId: 'openrouter', presetName: 'OpenRouter', routes: [route('openrouter.ai')], queryAccess: 'api-key', queryKind: 'openrouter' },
258
+ { id: 'therouter', brandId: 'therouter', presetName: 'TheRouter', routes: [route('api.therouter.ai')] },
259
+ { id: 'novita', brandId: 'novita', presetName: 'Novita AI', routes: [route('api.novita.ai')], queryAccess: 'api-key', queryKind: 'novita' },
260
+ { id: 'nvidia', brandId: 'nvidia', presetName: 'Nvidia', routes: [route('integrate.api.nvidia.com')] },
261
+ { id: 'pipellm', brandId: 'pipellm', presetName: 'PIPELLM', routes: [route('cc-api.pipellm.ai')] },
262
+ { id: 'jiekou', brandId: 'jiekou', presetName: 'JieKou AI', routes: [route('api.jiekou.ai')] },
263
+ // 同一参考提交内有专用 Coding Plan 查询,但没有 Claude 预设。
264
+ { id: 'zenmux', brandId: 'zenmux', routes: [route('*.zenmux.ai')] },
265
+ // cc-switch 其它宿主相对 Claude 目录新增的聚合/网关服务。
266
+ { id: 'together-ai', brandId: 'together-ai', routes: [route('api.together.xyz')], aliases: ['Together AI'] },
267
+ { id: 'new-api', brandId: 'new-api', routes: [], aliases: ['NewAPI'] },
268
+ ];
269
+
270
+ function createCatalog(variants: readonly ProviderVariant[]): {
271
+ entries: readonly ProviderCatalogEntry[];
272
+ brands: readonly ProviderBrand[];
273
+ } {
274
+ const brands = new Map(Object.entries(BRAND_METADATA).map(([id, metadata]) => [id, {
275
+ id,
276
+ displayName: metadata.displayName,
277
+ group: (metadata.category === 'official' ? 'official' : 'relay') as ProviderGroup,
278
+ variants: [] as ProviderCatalogEntry[],
279
+ }]));
280
+ const ids = new Map<string, ProviderCatalogEntry>();
281
+ const entries = variants.map((variant): ProviderCatalogEntry => {
282
+ const brand = brands.get(variant.brandId);
283
+ if (!brand) throw new Error(`Unknown provider brand: ${variant.brandId}`);
284
+ const { category } = BRAND_METADATA[variant.brandId];
285
+ const entry: ProviderCatalogEntry = {
286
+ id: variant.id,
287
+ brandId: brand.id,
288
+ displayName: brand.displayName,
289
+ variant: variant.variant,
290
+ presetName: variant.presetName,
291
+ group: brand.group,
292
+ routes: variant.routes,
293
+ queryAccess: variant.queryAccess ?? (brand.group === 'official' ? 'none' : 'generic'),
294
+ queryKind: variant.queryKind,
295
+ aliases: variant.aliases ?? [],
296
+ category,
297
+ maintenancePriority: category === 'official' ? 'P0' : category === 'major-relay' ? 'P1' : 'P2',
298
+ queryStatus: variant.queryKind ? 'implemented' : 'recognition-only',
299
+ };
300
+ const id = entry.id.toLowerCase();
301
+ if (!id || id !== id.trim() || ids.has(id)) throw new Error(`Invalid or duplicate provider ID: ${entry.id}`);
302
+ ids.set(id, entry);
303
+ brand.variants.push(entry);
304
+ return entry;
305
+ });
306
+ // ID 和别名共用查找入口,必须一起检查,避免任一条目遮住另一条目的 ID 或别名。
307
+ const names = new Map(ids);
308
+ for (const entry of entries) {
309
+ for (const alias of entry.aliases ?? []) {
310
+ const name = alias.toLowerCase();
311
+ if (!name || name !== name.trim() || (names.has(name) && names.get(name) !== entry)) {
312
+ throw new Error(`Unreachable provider alias: ${alias} (${entry.id})`);
313
+ }
314
+ names.set(name, entry);
315
+ }
316
+ }
317
+ for (const brand of brands.values()) {
318
+ if (!brand.variants.length) throw new Error(`Provider brand has no variants: ${brand.id}`);
319
+ }
320
+ return { entries, brands: [...brands.values()] };
321
+ }
322
+
323
+ const catalog = createCatalog(PROVIDER_VARIANTS);
324
+
325
+ export const PROVIDER_CATALOG: readonly ProviderCatalogEntry[] = catalog.entries;
326
+ export const OFFICIAL_PROVIDERS: readonly ProviderCatalogEntry[] = PROVIDER_CATALOG.filter((entry) => entry.group === 'official');
327
+ export const RELAY_PROVIDERS: readonly ProviderCatalogEntry[] = PROVIDER_CATALOG.filter((entry) => entry.group === 'relay');
328
+ export const PROVIDER_BRANDS: readonly ProviderBrand[] = catalog.brands;
329
+
330
+ export const CC_SWITCH_CLAUDE_PRESET_COUNT = 77;
331
+
332
+ const PROVIDER_ROUTES = PROVIDER_CATALOG.flatMap((entry) =>
333
+ entry.routes.map((candidate) => ({ entry, candidate })),
334
+ ).sort((a, b) => Number(Boolean(b.candidate.pathPrefix)) - Number(Boolean(a.candidate.pathPrefix)));
335
+
336
+ function hostMatches(hostname: string, pattern: string): boolean {
337
+ if (!pattern.includes('*')) return hostname === pattern;
338
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.]+');
339
+ return new RegExp(`^${escaped}$`).test(hostname);
340
+ }
341
+
342
+ function routeMatches(url: URL, candidate: ProviderRoute): boolean {
343
+ if (!hostMatches(url.hostname.toLowerCase(), candidate.host.toLowerCase())) return false;
344
+ if (!candidate.pathPrefix) return true;
345
+ const prefix = candidate.pathPrefix.toLowerCase().replace(/\/+$/, '');
346
+ const path = url.pathname.toLowerCase().replace(/\/+$/, '');
347
+ return path === prefix || path.startsWith(`${prefix}/`);
348
+ }
349
+
350
+ /** 路径更具体的规则优先,避免火山 Plan 被同主机的 Doubao 通用规则抢占。 */
351
+ export function findProviderByUrl(baseUrl: string): ProviderCatalogEntry | null {
352
+ let url: URL;
353
+ try {
354
+ url = new URL(baseUrl);
355
+ } catch {
356
+ return null;
357
+ }
358
+ return PROVIDER_ROUTES.find(({ candidate }) => routeMatches(url, candidate))?.entry ?? null;
359
+ }
360
+
361
+ export function findProviderById(id: string): ProviderCatalogEntry | null {
362
+ const normalized = id.trim().toLowerCase();
363
+ return PROVIDER_CATALOG.find((entry) =>
364
+ entry.id.toLowerCase() === normalized ||
365
+ entry.aliases?.some((alias) => alias.toLowerCase() === normalized),
366
+ ) ?? null;
367
+ }