fix(deeplink): import usage scripts disabled and show their code

An imported usage script is JavaScript that runs whenever usage is
queried. Two things made it possible to acquire one without seeing it:

  - `usage_enabled.unwrap_or(!code.is_empty())` treated the presence of
    code as a decision to run it, so a link that simply carried a script
    got it enabled
  - the confirmation dialog rendered only an enabled/disabled badge; the
    script body was never displayed

Default to disabled. Enabling now requires `usageEnabled=true` in the
link -- which is the link author's request, not the user's consent. The
consent is the user pressing Import after seeing the full script body
and the badge, which is why both displays are load-bearing rather than
decorative.

The badge predicate moves from `!== false` to `=== true` to match the
new backend default. Left alone it would have started rendering "did not
say" as a green "Enabled" -- more optimistic than what would actually
happen.

Extracts the payload decode into `decodeDeeplinkPayload`, which falls
back to the raw string when decoding fails or yields empty. A dialog
whose job is to show what is about to be written must not let a payload
vanish just because it is malformed; empty reads as "there is no
script", which is exactly the wrong impression.
This commit is contained in:
Jason
2026-07-29 00:04:20 +08:00
parent 19bf236e58
commit cfa90f396a
10 changed files with 193 additions and 5 deletions
+2 -1
View File
@@ -114,7 +114,8 @@ pub struct DeepLinkImportRequest {
pub config_url: Option<String>,
// ============ Usage script fields (v3.9+) ============
/// Whether to enable usage query (default: true if usage_script is provided)
/// 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`.
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_enabled: Option<bool>,
/// Base64 encoded usage query script code
+12 -2
View File
@@ -254,8 +254,18 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<Provide
String::new()
};
// Determine enabled state: explicit param > has code > false
let enabled = request.usage_enabled.unwrap_or(!code.is_empty());
// Determine enabled state: explicit param only, defaulting to disabled.
//
// 「携带了代码」不构成用户的启用决定。此处的输入来自 deeplink——即第三方
// 构造、经浏览器抵达的不可信载荷——而 `code` 是一段会在查询用量时执行的
// JavaScript。若以 `!code.is_empty()` 作默认,一条链接就能让脚本在用户
// 从未勾选过的情况下进入启用态。
//
// 要启用,链接必须显式携带 `usageEnabled=true`。注意该参数是**链接作者**的
// 请求,不构成用户的同意;用户的同意体现在确认框展示了完整脚本正文与启用
// 徽章之后仍点了导入——所以那两处展示是本设计的承重部分,不可省略。
// 用户在应用内手动配置的脚本不走这条路径。
let enabled = request.usage_enabled.unwrap_or(false);
let usage_script = UsageScript {
enabled,
+81
View File
@@ -342,6 +342,87 @@ fn test_deeplink_usage_script_does_not_copy_provider_credentials() {
assert_eq!(script.base_url, None);
}
/// 构造一个只带用量脚本字段的 provider 请求,其余保持最小。
fn usage_script_request(code: &str, usage_enabled: Option<bool>) -> DeepLinkImportRequest {
DeepLinkImportRequest {
version: "v1".to_string(),
resource: "provider".to_string(),
app: Some("claude".to_string()),
name: Some("Test Claude".to_string()),
homepage: Some("https://example.com".to_string()),
endpoint: Some("https://api.example.com/v1/".to_string()),
api_key: Some("sk-main".to_string()),
icon: None,
model: None,
notes: None,
haiku_model: None,
sonnet_model: None,
opus_model: None,
config: None,
config_format: None,
config_url: None,
apps: None,
repo: None,
directory: None,
branch: None,
content: None,
description: None,
enabled: None,
usage_enabled,
usage_script: Some(BASE64_STANDARD.encode(code)),
usage_api_key: None,
usage_base_url: None,
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
}
}
#[test]
fn test_deeplink_usage_script_is_not_enabled_merely_by_carrying_code() {
use super::provider::build_provider_from_request;
// deeplink 是第三方构造、经浏览器抵达的不可信载荷。「带了代码」不是用户的
// 启用决定——否则一条链接就能让这段 JS 在用户从未勾选的情况下进入启用态。
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 still be created");
assert!(
!script.enabled,
"缺省必须是未启用;`带了代码`不构成用户的启用决定"
);
// 代码本身仍要保留:确认框要展示它,用户之后也可在应用内手动开启。
assert_eq!(script.code, code);
}
#[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));
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);
assert_eq!(script.code, code);
}
#[test]
fn test_deeplink_usage_script_omits_explicit_credentials_that_match_provider() {
use super::provider::build_provider_from_request;