From 450bc039f0b9784508f7824a3b5fea6cd3f804b9 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 12 Oct 2026 09:59:59 +0800 Subject: [PATCH] feat(deeplink): enable imported usage scripts by default A provider link that carries a usage script now enables usage query unless it says `usageEnabled=false`. Site owners attach the script so users see their balance; making them switch it on again after import was an extra step with little safety benefit: the script runs in the QuickJS sandbox, sends a single request, and deep-link scripts have no template type, so that request must be same-origin HTTPS with the base URL. Links without a script still import usage fields disabled. The confirmation badge follows the same rule as the backend, and the script body is still shown in full before the user imports. --- deplink.html | 2 +- docs/user-manual/en/5-faq/5.3-deeplink.md | 2 +- docs/user-manual/ja/5-faq/5.3-deeplink.md | 2 +- docs/user-manual/zh/5-faq/5.3-deeplink.md | 2 +- src-tauri/src/deeplink/mod.rs | 4 +- src-tauri/src/deeplink/provider.rs | 19 ++++--- src-tauri/src/deeplink/tests.rs | 53 +++++++++++++++---- src/components/DeepLinkImportDialog.tsx | 14 +++-- .../components/DeepLinkImportDialog.test.tsx | 31 +++++++++++ 9 files changed, 98 insertions(+), 31 deletions(-) diff --git a/deplink.html b/deplink.html index a02d8d66c..2c4dd788a 100644 --- a/deplink.html +++ b/deplink.html @@ -1072,7 +1072,7 @@
diff --git a/docs/user-manual/en/5-faq/5.3-deeplink.md b/docs/user-manual/en/5-faq/5.3-deeplink.md index 20aaf88c5..4fece72fe 100644 --- a/docs/user-manual/en/5-faq/5.3-deeplink.md +++ b/docs/user-manual/en/5-faq/5.3-deeplink.md @@ -60,7 +60,7 @@ ccswitch://v1/import?resource={type}&app={app}&name={name}&... | `configUrl` | No | Remote configuration URL | | `enabled` | No | Whether to enable (boolean) | | `usageScript` | No | Usage query script | -| `usageEnabled` | No | Whether to enable usage query (**default false**). The script body is shown in full in the import confirmation dialog; without an explicit `true` the script is imported but left disabled, and can be enabled in the app | +| `usageEnabled` | No | Whether to enable usage query. When omitted, it is enabled if `usageScript` is present and disabled otherwise; pass `false` to import the script without enabling it (it can be enabled later in the app). The script body is shown in full in the import confirmation dialog | | `usageApiKey` | No | Usage query API Key | | `usageBaseUrl` | No | Usage query base URL | | `usageAccessToken` | No | Usage query access token | diff --git a/docs/user-manual/ja/5-faq/5.3-deeplink.md b/docs/user-manual/ja/5-faq/5.3-deeplink.md index 1fa8bd188..37204199d 100644 --- a/docs/user-manual/ja/5-faq/5.3-deeplink.md +++ b/docs/user-manual/ja/5-faq/5.3-deeplink.md @@ -60,7 +60,7 @@ ccswitch://v1/import?resource={type}&app={app}&name={name}&... | `configUrl` | いいえ | リモート設定 URL | | `enabled` | いいえ | 有効にするかどうか(ブール値) | | `usageScript` | いいえ | 使用量クエリスクリプト | -| `usageEnabled` | いいえ | 使用量クエリを有効にするか(**デフォルト false**)。スクリプト本文はインポート確認ダイアログに全文表示されます。明示的に `true` を指定しない場合はインポートされるだけで有効化されず、アプリ内で手動で有効にできます | +| `usageEnabled` | いいえ | 使用量クエリを有効にするか。省略時は `usageScript` があれば有効、なければ無効です。`false` を指定するとインポートのみ行い有効化しません(アプリ内で後から有効にできます)。スクリプト本文はインポート確認ダイアログに全文表示されます | | `usageApiKey` | いいえ | 使用量クエリ専用 API Key | | `usageBaseUrl` | いいえ | 使用量クエリ専用アドレス | | `usageAccessToken` | いいえ | 使用量クエリアクセストークン | diff --git a/docs/user-manual/zh/5-faq/5.3-deeplink.md b/docs/user-manual/zh/5-faq/5.3-deeplink.md index 2f009ffa0..57e529fbf 100644 --- a/docs/user-manual/zh/5-faq/5.3-deeplink.md +++ b/docs/user-manual/zh/5-faq/5.3-deeplink.md @@ -60,7 +60,7 @@ ccswitch://v1/import?resource={type}&app={app}&name={name}&... | `configUrl` | 否 | 远程配置 URL | | `enabled` | 否 | 是否启用(布尔值) | | `usageScript` | 否 | 用量查询脚本 | -| `usageEnabled` | 否 | 是否启用用量查询(**默认 false**)。脚本正文会完整展示在导入确认框中;未显式传 `true` 时仅导入不启用,可在应用内手动开启 | +| `usageEnabled` | 否 | 是否启用用量查询。未传时:带了 `usageScript` 就启用,否则不启用;传 `false` 则只导入不启用,可在应用内手动开启。脚本正文会完整展示在导入确认框中 | | `usageApiKey` | 否 | 用量查询专用 API Key | | `usageBaseUrl` | 否 | 用量查询专用地址 | | `usageAccessToken` | 否 | 用量查询访问令牌 | diff --git a/src-tauri/src/deeplink/mod.rs b/src-tauri/src/deeplink/mod.rs index 495a33552..413615780 100644 --- a/src-tauri/src/deeplink/mod.rs +++ b/src-tauri/src/deeplink/mod.rs @@ -166,8 +166,8 @@ pub struct DeepLinkImportRequest { pub config_url: Option, // ============ Usage script fields (v3.9+) ============ - /// Whether to enable usage query. Defaults to **disabled** — carrying a script - /// is not itself a decision to run it; the link must say `usageEnabled=true`. + /// Whether to enable usage query. When omitted, defaults to enabled if the link + /// carries a usage script, disabled otherwise; `usageEnabled=false` opts out. #[serde(skip_serializing_if = "Option::is_none")] pub usage_enabled: Option, /// Base64 encoded usage query script code diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 0ffa6c8af..10cc69b36 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -264,18 +264,17 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result) -> DeepLinkImpo } #[test] -fn test_deeplink_usage_script_is_not_enabled_merely_by_carrying_code() { +fn test_deeplink_usage_script_is_enabled_by_default_when_the_link_carries_code() { use super::provider::build_provider_from_request; - // deeplink 是第三方构造、经浏览器抵达的不可信载荷。「带了代码」不是用户的 - // 启用决定——否则一条链接就能让这段 JS 在用户从未勾选的情况下进入启用态。 + // 站长放脚本就是为了让用户看到余额;链接没写 usageEnabled 时,带了脚本即启用。 + // 前端确认框的「已启用」徽章按同一规则显示。 let code = "export async function query() { return { cost: 0 }; }"; let request = usage_script_request(code, None); + let provider = build_provider_from_request(&AppType::Claude, &request).unwrap(); + let script = provider + .meta + .as_ref() + .and_then(|meta| meta.usage_script.as_ref()) + .expect("usage script should be created"); + + assert!(script.enabled, "带了脚本、没写 usageEnabled 时应默认启用"); + assert_eq!(script.code, code); +} + +#[test] +fn test_deeplink_usage_script_stays_disabled_when_the_link_opts_out() { + use super::provider::build_provider_from_request; + + let code = "export async function query() { return { cost: 0 }; }"; + let request = usage_script_request(code, Some(false)); + let provider = build_provider_from_request(&AppType::Claude, &request).unwrap(); let script = provider .meta @@ -466,21 +484,36 @@ fn test_deeplink_usage_script_is_not_enabled_merely_by_carrying_code() { .and_then(|meta| meta.usage_script.as_ref()) .expect("usage script should still be created"); - assert!( - !script.enabled, - "缺省必须是未启用;`带了代码`不构成用户的启用决定" - ); - // 代码本身仍要保留:确认框要展示它,用户之后也可在应用内手动开启。 + assert!(!script.enabled, "usageEnabled=false 必须照办"); + // 代码仍要保留:用户之后可在应用内手动开启。 assert_eq!(script.code, code); } +#[test] +fn test_deeplink_usage_fields_without_a_script_are_not_enabled_by_default() { + use super::provider::build_provider_from_request; + + // 没有脚本就没有东西可跑:只带凭据类字段的链接不应默认启用。 + let mut request = usage_script_request("", None); + request.usage_script = None; + request.usage_access_token = Some("token".to_string()); + + let provider = build_provider_from_request(&AppType::Claude, &request).unwrap(); + let script = provider + .meta + .as_ref() + .and_then(|meta| meta.usage_script.as_ref()) + .expect("usage fields should still be stored"); + + assert!(!script.enabled); +} + #[test] fn test_deeplink_usage_script_honors_an_explicit_enable_request_from_the_link() { use super::provider::build_provider_from_request; // `usageEnabled=true` 是**链接作者**的请求,不是用户的选择——用户的同意体现在 - // 看过确认框里完整的脚本正文与启用状态之后点了导入。收紧默认值不能顺手把这条 - // 正常通路改坏:合作伙伴的预设链接靠它一次性配好用量查询。 + // 看过确认框里完整的脚本正文与启用状态之后点了导入。 let code = "export async function query() { return { cost: 0 }; }"; let request = usage_script_request(code, Some(true)); diff --git a/src/components/DeepLinkImportDialog.tsx b/src/components/DeepLinkImportDialog.tsx index 46fb67f23..b71f58af6 100644 --- a/src/components/DeepLinkImportDialog.tsx +++ b/src/components/DeepLinkImportDialog.tsx @@ -286,6 +286,10 @@ export function DeepLinkImportDialog() { ? `${request.apiKey.substring(0, 4)}${"*".repeat(20)}` : "****"; + // 导入后用量查询是否启用:链接写了 usageEnabled 就照办,没写时带脚本即启用(与后端一致) + const usageWillBeEnabled = + request?.usageEnabled ?? Boolean(request?.usageScript); + // Check if config file is present const hasConfigFile = !!(request?.config || request?.configUrl); const configSource = request?.config @@ -678,18 +682,18 @@ export function DeepLinkImportDialog() {
{/* - 判据是 `=== true`,与后端 `usage_enabled.unwrap_or(false)` - 严格对齐。此前用的 `!== false` 会把"链接没说"渲染成绿色的 - 「已启用」——徽章必须显示实际会发生的事,不能比后端更乐观。 + 与后端 `usage_enabled.unwrap_or(!code.is_empty())` 严格对齐: + 链接写了就照办,没写时带脚本即启用。徽章必须显示实际会发生 + 的事,不能比后端更乐观,也不能更保守。 */} - {request.usageEnabled === true + {usageWillBeEnabled ? t("deeplink.usageScriptEnabled", { defaultValue: "已启用", }) diff --git a/tests/components/DeepLinkImportDialog.test.tsx b/tests/components/DeepLinkImportDialog.test.tsx index 627a8963f..fe8157b90 100644 --- a/tests/components/DeepLinkImportDialog.test.tsx +++ b/tests/components/DeepLinkImportDialog.test.tsx @@ -110,6 +110,37 @@ describe("DeepLinkImportDialog", () => { expect(screen.queryByText("脚本代码")).not.toBeInTheDocument(); }); + // 徽章要和后端 `usage_enabled.unwrap_or(!code.is_empty())` 一致: + // 链接写了 usageEnabled 就照办,没写时带脚本即启用 + it.each([ + { usageEnabled: undefined, expected: "已启用" }, + { usageEnabled: true, expected: "已启用" }, + { usageEnabled: false, expected: "未启用" }, + ])( + "shows the usage badge as $expected when usageEnabled is $usageEnabled and a script is attached", + async ({ usageEnabled, expected }) => { + render(, { wrapper: Wrapper }); + + act(() => { + deliverDeepLink({ + version: "v1", + resource: "provider", + app: "claude", + name: "Usage Provider", + homepage: "https://example.com", + endpoint: "https://api.example.com", + apiKey: "sk-provider-key", + usageScript: btoa("console.log('usage');"), + ...(usageEnabled === undefined ? {} : { usageEnabled }), + }); + }); + + await waitFor(() => { + expect(screen.getByText(expected)).toBeInTheDocument(); + }); + }, + ); + it("shows a link that arrived before the dialog mounted (cold start)", async () => { // 冷启动时链接早于前端监听到达,后端已暂存;组件挂载后必须主动取一次 setPendingDeepLink({