From ddc24fec90e93be39a8965dde1defb3ee88afe62 Mon Sep 17 00:00:00 2001 From: JackZhao10086 Date: Wed, 13 May 2026 14:09:53 +0800 Subject: [PATCH] fix(auth): clarify URL handling in auth messages and docs (#856) --- cmd/auth/login.go | 2 +- cmd/auth/login_messages.go | 4 +- cmd/auth/login_test.go | 105 ++++++++++++++++++++++++++++++++++++ skills/lark-shared/SKILL.md | 8 +-- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 5d1f7b62f..365b495ba 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -232,7 +232,7 @@ func authLoginRun(opts *LoginOptions) error { "verification_url": authResp.VerificationUriComplete, "device_code": authResp.DeviceCode, "expires_in": authResp.ExpiresIn, - "hint": fmt.Sprintf("Show verification_url to user, then immediately execute: lark-cli auth login --device-code %s (blocks until authorized or timeout). Do not instruct the user to run this command themselves.", authResp.DeviceCode), + "hint": fmt.Sprintf("Show verification_url to the user exactly as returned by the CLI and treat it as an opaque string. Do not URL-encode or decode it, do not normalize or rewrite it, do not add %%20, spaces, or punctuation, and do not wrap it as Markdown link text; prefer a fenced code block containing only the raw URL. Then immediately execute: lark-cli auth login --device-code %s (blocks until authorized or timeout). Do not instruct the user to run this command themselves.", authResp.DeviceCode), } encoder := json.NewEncoder(f.IOStreams.Out) encoder.SetEscapeHTML(false) diff --git a/cmd/auth/login_messages.go b/cmd/auth/login_messages.go index dd0f0402c..548d704b2 100644 --- a/cmd/auth/login_messages.go +++ b/cmd/auth/login_messages.go @@ -59,7 +59,7 @@ var loginMsgZh = &loginMsg{ OpenURL: "在浏览器中打开以下链接进行认证:\n\n", WaitingAuth: "等待用户授权...", - AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout ≥ 600s;如不支持长 timeout,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 后再用 `lark-cli auth login --device-code ` 续上轮询,**不要短 timeout 反复重试**——每次重启会作废上一轮的 device code,导致用户授权的链接失效。", + AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s;如不支持长 timeout,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 后再用 `lark-cli auth login --device-code ` 续上轮询。**不要短 timeout 反复重试**,每次重启会作废上一轮的 device code,导致用户授权链接失效。向用户展示授权链接时,必须逐字原样转发 CLI 返回的 URL,把它视为不可修改的 opaque string;不要做 URL 编码或解码,不要补 `%20`、空格或标点,不要改写成 Markdown 链接,建议用只包含该 URL 的代码块单独输出。", AuthSuccess: "已收到授权确认,正在获取用户信息并校验授权结果...", LoginSuccess: "授权成功! 用户: %s (%s)", AuthorizedUser: "当前授权账号: %s (%s)", @@ -95,7 +95,7 @@ var loginMsgEn = &loginMsg{ OpenURL: "Open this URL in your browser to authenticate:\n\n", WaitingAuth: "Waiting for user authorization...", - AgentTimeoutHint: "[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is ≥ 600s. If long timeouts are not supported, use `lark-cli auth login --no-wait --json` to get a device_code, then `lark-cli auth login --device-code ` to resume polling. **Do NOT retry with a short timeout** — each restart invalidates the previous device code, so any URL the user already authorized becomes useless.", + AgentTimeoutHint: "[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is >= 600s. If long timeouts are not supported, use `lark-cli auth login --no-wait --json` to get a device_code, then `lark-cli auth login --device-code ` to resume polling. **Do NOT retry with a short timeout**; each restart invalidates the previous device code and makes the earlier authorization URL useless. When showing the authorization URL to the user, copy the CLI-returned URL exactly as-is and treat it as an opaque string. Do not URL-encode or decode it, do not add `%20`, spaces, or punctuation, do not rewrite it as Markdown link text, and prefer a fenced code block containing only the raw URL.", AuthSuccess: "Authorization confirmed, fetching user info and validating granted scopes...", LoginSuccess: "Authorization successful! User: %s (%s)", AuthorizedUser: "Authorized account: %s (%s)", diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 82dc3cb30..ea9503464 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -879,6 +879,57 @@ func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) { } } +func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{ + ProfileName: "default", + AppID: "cli_test", + AppSecret: "secret", + Brand: core.BrandFeishu, + }) + + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: larkauth.PathDeviceAuthorization, + Body: map[string]interface{}{ + "device_code": "device-code", + "user_code": "user-code", + "verification_uri": "https://example.com/verify", + "verification_uri_complete": "https://example.com/verify?code=123", + "expires_in": 240, + "interval": 5, + }, + }) + + err := authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "im:message:send", + NoWait: true, + }) + if err != nil { + t.Fatalf("authLoginRun() error = %v", err) + } + + dec := json.NewDecoder(strings.NewReader(stdout.String())) + var data map[string]interface{} + if err := dec.Decode(&data); err != nil { + t.Fatalf("Decode(stdout first event) error = %v, stdout=%q", err, stdout.String()) + } + hint, _ := data["hint"].(string) + for _, want := range []string{ + "exactly as returned by the CLI", + "opaque string", + "Do not URL-encode or decode it", + "do not add %20, spaces, or punctuation", + "do not wrap it as Markdown link text", + "fenced code block containing only the raw URL", + } { + if !strings.Contains(hint, want) { + t.Fatalf("hint missing %q, got:\n%s", want, hint) + } + } +} + func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *testing.T) { f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{ ProfileName: "default", @@ -917,6 +968,60 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t * } } +func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{ + ProfileName: "default", + AppID: "cli_test", + AppSecret: "secret", + Brand: core.BrandFeishu, + }) + + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: larkauth.PathDeviceAuthorization, + Body: map[string]interface{}{ + "device_code": "device-code", + "user_code": "user-code", + "verification_uri": "https://example.com/verify", + "verification_uri_complete": "https://example.com/verify?code=123", + "expires_in": 240, + "interval": 5, + }, + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := authLoginRun(&LoginOptions{ + Factory: f, + Ctx: ctx, + Scope: "im:message:send", + JSON: true, + }) + if err == nil { + t.Fatal("expected error from cancelled context") + } + + dec := json.NewDecoder(strings.NewReader(stdout.String())) + var data map[string]interface{} + if err := dec.Decode(&data); err != nil { + t.Fatalf("Decode(stdout first event) error = %v, stdout=%q", err, stdout.String()) + } + hint, _ := data["agent_hint"].(string) + for _, want := range []string{ + "timeout >= 600s", + "逐字原样转发 CLI 返回的 URL", + "opaque string", + "不要做 URL 编码或解码", + "不要补 `%20`、空格或标点", + "不要改写成 Markdown 链接", + "只包含该 URL 的代码块单独输出", + } { + if !strings.Contains(hint, want) { + t.Fatalf("agent_hint missing %q, got:\n%s", want, hint) + } + } +} + func TestGetDomainMetadata_ExcludesEvent(t *testing.T) { domains := getDomainMetadata("zh") for _, dm := range domains { diff --git a/skills/lark-shared/SKILL.md b/skills/lark-shared/SKILL.md index 0a3f74fbf..d15831cef 100644 --- a/skills/lark-shared/SKILL.md +++ b/skills/lark-shared/SKILL.md @@ -12,7 +12,9 @@ description: "飞书/Lark CLI 共享基础:应用配置初始化、认证登 首次使用需运行 `lark-cli config init` 完成应用配置。 -当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户: +当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户。 + +**URL 转发规则**:当命令输出 `verification_url`、`verification_uri_complete`、`console_url` 等 URL 字段时,必须将 URL exactly as returned by the CLI 转发给用户,并把它视为不可修改的 opaque string;不要做 URL encode/decode,不要补 `%20`、空格或标点,不要重新拼接 query,不要改写成 Markdown link text,建议用只包含原始 URL 的代码块单独输出。 ```bash # 发起配置(该命令会阻塞直到用户打开链接并完成操作或过期) @@ -51,7 +53,7 @@ lark-cli config init --new #### Bot 身份(`--as bot`) -将错误中的 `console_url` 提供给用户,引导去后台开通 scope。**禁止**对 bot 执行 `auth login`。 +将错误中的 `console_url` 原样提供给用户,引导去后台开通 scope。**禁止**对 bot 执行 `auth login`。 #### User 身份(`--as user`) @@ -64,7 +66,7 @@ lark-cli auth login --scope "" # 按具体 scope 授权(推 #### Agent 代理发起认证(推荐) -当你作为 AI agent 需要帮用户完成认证时,使用background方式 执行以下命令发起授权流程, 并将授权链接发给用户: +当你作为 AI agent 需要帮用户完成认证时,使用background方式 执行以下命令发起授权流程, 并将授权链接原样发给用户: ```bash # 发起授权(阻塞直到用户授权完成或过期)