mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
A provider link can now carry `iconUrl`, an https address of the site's own logo. The confirmation dialog downloads it as soon as it opens and shows it at the top; importing stores it as the card's custom icon. No extra confirmation step. Any failure is silent: the card falls back to the built-in `icon` name, or to the name's initials. The image goes through the same path as a local upload: the backend returns a data URI, the frontend redraws it on a canvas into a 96x96 PNG, and the provider save validation checks it again. The address itself is never stored or rendered, and the import command never downloads. The download (`deeplink/icon_fetch.rs`): - Parsing drops addresses that are not https, use an IP or a non-default port, carry credentials or exceed 2048 characters; the rest of the link still imports. - Every resolved address must be public, and the connection is pinned to them so a second DNS answer cannot point it elsewhere. Redirects are followed by hand, up to 3, and each hop is checked again. - No system proxy, no cookies, no Referer, 5 s overall timeout, body read as a stream and capped at 256 KB. - Content-Type must be PNG, JPEG, WebP or SVG and match the file's magic bytes; bitmap dimensions are read from the header and capped at 4096. - 198.18.0.0/15 is allowed on purpose: Clash and Surge fake-ip mode resolve every domain into it. The trade-off is written next to `is_public_ipv4`. - Errors are logged without the URL, which may carry query tokens. Closes #8117
153 lines
4.0 KiB
TypeScript
153 lines
4.0 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
export type ResourceType = "provider" | "prompt" | "mcp" | "skill";
|
|
|
|
export interface DeepLinkImportRequest {
|
|
version: string;
|
|
resource: ResourceType;
|
|
|
|
// Common fields
|
|
app?:
|
|
| "claude"
|
|
| "codex"
|
|
| "gemini"
|
|
| "grokbuild"
|
|
| "opencode"
|
|
| "openclaw"
|
|
| "hermes"
|
|
| "pi";
|
|
name?: string;
|
|
enabled?: boolean;
|
|
|
|
// Provider fields
|
|
homepage?: string;
|
|
endpoint?: string;
|
|
apiKey?: string;
|
|
icon?: string;
|
|
/** 图标下载地址;确认框里经 `fetchDeeplinkIcon` 下载、重新编码后放进 `icon` */
|
|
iconUrl?: string;
|
|
model?: string;
|
|
notes?: string;
|
|
haikuModel?: string;
|
|
sonnetModel?: string;
|
|
opusModel?: string;
|
|
/** 接口格式,后端按应用换算成各自的字段 */
|
|
apiFormat?:
|
|
| "anthropic"
|
|
| "openai_chat"
|
|
| "openai_responses"
|
|
| "gemini_native";
|
|
|
|
// Prompt fields
|
|
content?: string;
|
|
description?: string;
|
|
|
|
// MCP fields
|
|
apps?: string; // Comma-separated application IDs
|
|
|
|
// Skill fields
|
|
repo?: string;
|
|
directory?: string;
|
|
branch?: string;
|
|
|
|
// Config file fields
|
|
config?: string;
|
|
configFormat?: string;
|
|
configUrl?: string;
|
|
|
|
// Usage script fields (v3.9+)
|
|
usageEnabled?: boolean;
|
|
usageScript?: string;
|
|
usageApiKey?: string;
|
|
usageBaseUrl?: string;
|
|
usageAccessToken?: string;
|
|
usageUserId?: string;
|
|
usageAutoInterval?: number;
|
|
}
|
|
|
|
export interface McpImportResult {
|
|
importedCount: number;
|
|
importedIds: string[];
|
|
failed: Array<{
|
|
id: string;
|
|
error: string;
|
|
}>;
|
|
}
|
|
|
|
export type ImportResult =
|
|
| { type: "provider"; id: string }
|
|
| { type: "prompt"; id: string }
|
|
| {
|
|
type: "mcp";
|
|
importedCount: number;
|
|
importedIds: string[];
|
|
failed: Array<{ id: string; error: string }>;
|
|
}
|
|
| { type: "skill"; key: string };
|
|
|
|
/** 后端暂存、等前端处理的深链接(解析结果或解析错误) */
|
|
export type PendingDeepLink =
|
|
| { kind: "import"; request: DeepLinkImportRequest }
|
|
| { kind: "error"; url: string; error: string };
|
|
|
|
/** 带编号的待处理深链接:编号用于确认清除和去重 */
|
|
export interface PendingDeepLinkEntry {
|
|
id: number;
|
|
link: PendingDeepLink;
|
|
}
|
|
|
|
export const deeplinkApi = {
|
|
/**
|
|
* Read the deep link waiting for the frontend; the backend keeps it until acked.
|
|
* Called on mount (cold start / rebuilt window) and on every `deeplink-pending` event.
|
|
*/
|
|
peekPendingDeeplink: async (): Promise<PendingDeepLinkEntry | null> => {
|
|
return invoke("peek_pending_deeplink");
|
|
},
|
|
|
|
/** Clear the pending deep link after it has been shown; a stale id is ignored */
|
|
ackPendingDeeplink: async (id: number): Promise<void> => {
|
|
return invoke("ack_pending_deeplink", { id });
|
|
},
|
|
|
|
/**
|
|
* Parse a deep link URL
|
|
* @param url The ccswitch:// URL to parse
|
|
* @returns Parsed deep link request
|
|
*/
|
|
parseDeeplink: async (url: string): Promise<DeepLinkImportRequest> => {
|
|
return invoke("parse_deeplink", { url });
|
|
},
|
|
|
|
/**
|
|
* Merge configuration from Base64/URL into a deep link request
|
|
* This is used to show the complete configuration in the confirmation dialog
|
|
* @param request The deep link import request
|
|
* @returns Merged deep link request with config fields populated
|
|
*/
|
|
mergeDeeplinkConfig: async (
|
|
request: DeepLinkImportRequest,
|
|
): Promise<DeepLinkImportRequest> => {
|
|
return invoke("merge_deeplink_config", { request });
|
|
},
|
|
|
|
/**
|
|
* 下载链接里 `iconUrl` 指向的图标,返回核对过类型的 data URI;
|
|
* 任何失败(地址不合规、超时、类型不符等)都返回 null,原因只进后端日志
|
|
*/
|
|
fetchDeeplinkIcon: async (url: string): Promise<string | null> => {
|
|
return invoke("fetch_deeplink_icon", { url });
|
|
},
|
|
|
|
/**
|
|
* Import a resource from a deep link request (unified handler)
|
|
* @param request The deep link import request
|
|
* @returns Import result based on resource type
|
|
*/
|
|
importFromDeeplink: async (
|
|
request: DeepLinkImportRequest,
|
|
): Promise<ImportResult> => {
|
|
return invoke("import_from_deeplink_unified", { request });
|
|
},
|
|
};
|