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.
This commit is contained in:
Jason committed 2026-10-12 23:26:43 +08:00
1 parent 98222d19e3
commit 450bc039f0
9 files changed
+98 -31

No files matched your search

+1 -1
View File
@@ -1072,7 +1072,7 @@
<div class="form-group">
<label>启用用量查询</label>
<select id="usageEnabled">
<option value="">不配置</option>
<option value="">不指定(带脚本即启用)</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
+1 -1
View File
@@ -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 |
+1 -1
View File
@@ -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` | いいえ | 使用量クエリアクセストークン |
+1 -1
View File
@@ -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` | 否 | 用量查询访问令牌 |
+2 -2
View File
@@ -166,8 +166,8 @@ pub struct DeepLinkImportRequest {
pub config_url: Option<String>,
// ============ 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<bool>,
/// Base64 encoded usage query script code
+9 -10
View File
@@ -264,18 +264,17 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<Provide
String::new()
};
// Determine enabled state: explicit param only, defaulting to disabled.
// 启用状态:链接写了 `usageEnabled` 就照办;没写时,带了脚本就启用。
//
// 「携带了代码」不构成用户的启用决定。此处的输入来自 deeplink——即第三方
// 构造、经浏览器抵达的不可信载荷——而 `code` 是一段会在查询用量时执行的
// JavaScript。若以 `!code.is_empty()` 作默认,一条链接就能让脚本在用户
// 从未勾选过的情况下进入启用态。
// 站长放脚本就是为了让用户看到余额,导入后还要再手动打开一次是多余的一步。
// 默认启用的代价可控:脚本跑在受限的 QuickJS 里,只能发一个请求,且必须是
// 与 base_url 同源的 HTTPS(`usage_script::validate_request_url`;深链接导入
// 的脚本没有 template_type,不算自定义模板,这项检查不会被跳过)。
//
// 要启用,链接必须显式携带 `usageEnabled=true`。注意该参数是**链接作者**的
// 请求,不构成用户的同意;用户的同意体现在确认框展示了完整脚本正文与启用
// 徽章之后仍点了导入——所以那两处展示是本设计的承重部分,不可省略。
// 用户在应用内手动配置的脚本不走这条路径。
let enabled = request.usage_enabled.unwrap_or(false);
// 用户的同意仍然体现在确认框:完整的脚本正文和启用徽章都展示过之后才点导入,
// 所以那两处展示是本设计的承重部分,不可省略。前端徽章的判断必须和这里一致。
// 链接想导入但不启用,就显式写 `usageEnabled=false`。
let enabled = request.usage_enabled.unwrap_or(!code.is_empty());
let usage_script = UsageScript {
enabled,
+43 -10
View File
@@ -451,14 +451,32 @@ fn usage_script_request(code: &str, usage_enabled: Option<bool>) -> 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));
+9 -5
View File
@@ -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() {
</div>
<div className="col-span-2 text-sm">
{/*
判据是 `=== true`,与后端 `usage_enabled.unwrap_or(false)`
严格对齐。此前用的 `!== false` 会把"链接没说"渲染成绿色的
「已启用」——徽章必须显示实际会发生的事,不能比后端更乐观。
与后端 `usage_enabled.unwrap_or(!code.is_empty())` 严格对齐:
链接写了就照办,没写时带脚本即启用。徽章必须显示实际会发生
的事,不能比后端更乐观,也不能更保守。
*/}
<span
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium ${
request.usageEnabled === true
usageWillBeEnabled
? "bg-success-soft text-success-text"
: "bg-subtle text-fg-2"
}`}
>
{request.usageEnabled === true
{usageWillBeEnabled
? t("deeplink.usageScriptEnabled", {
defaultValue: "已启用",
})
@@ -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(<DeepLinkImportDialog />, { 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({