Skip to content

Commit ddc24fe

Browse files
fix(auth): clarify URL handling in auth messages and docs (#856)
1 parent 25454f4 commit ddc24fe

4 files changed

Lines changed: 113 additions & 6 deletions

File tree

cmd/auth/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func authLoginRun(opts *LoginOptions) error {
232232
"verification_url": authResp.VerificationUriComplete,
233233
"device_code": authResp.DeviceCode,
234234
"expires_in": authResp.ExpiresIn,
235-
"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),
235+
"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),
236236
}
237237
encoder := json.NewEncoder(f.IOStreams.Out)
238238
encoder.SetEscapeHTML(false)

cmd/auth/login_messages.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var loginMsgZh = &loginMsg{
5959

6060
OpenURL: "在浏览器中打开以下链接进行认证:\n\n",
6161
WaitingAuth: "等待用户授权...",
62-
AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout 600s;如不支持长 timeout,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 后再用 `lark-cli auth login --device-code <code>` 续上轮询**不要短 timeout 反复重试**——每次重启会作废上一轮的 device code,导致用户授权的链接失效。",
62+
AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s;如不支持长 timeout,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 后再用 `lark-cli auth login --device-code <code>` 续上轮询**不要短 timeout 反复重试**每次重启会作废上一轮的 device code,导致用户授权链接失效。向用户展示授权链接时,必须逐字原样转发 CLI 返回的 URL,把它视为不可修改的 opaque string;不要做 URL 编码或解码,不要补 `%20`、空格或标点,不要改写成 Markdown 链接,建议用只包含该 URL 的代码块单独输出。",
6363
AuthSuccess: "已收到授权确认,正在获取用户信息并校验授权结果...",
6464
LoginSuccess: "授权成功! 用户: %s (%s)",
6565
AuthorizedUser: "当前授权账号: %s (%s)",
@@ -95,7 +95,7 @@ var loginMsgEn = &loginMsg{
9595

9696
OpenURL: "Open this URL in your browser to authenticate:\n\n",
9797
WaitingAuth: "Waiting for user authorization...",
98-
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 <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.",
98+
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 <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.",
9999
AuthSuccess: "Authorization confirmed, fetching user info and validating granted scopes...",
100100
LoginSuccess: "Authorization successful! User: %s (%s)",
101101
AuthorizedUser: "Authorized account: %s (%s)",

cmd/auth/login_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,57 @@ func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) {
879879
}
880880
}
881881

882+
func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) {
883+
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
884+
ProfileName: "default",
885+
AppID: "cli_test",
886+
AppSecret: "secret",
887+
Brand: core.BrandFeishu,
888+
})
889+
890+
reg.Register(&httpmock.Stub{
891+
Method: "POST",
892+
URL: larkauth.PathDeviceAuthorization,
893+
Body: map[string]interface{}{
894+
"device_code": "device-code",
895+
"user_code": "user-code",
896+
"verification_uri": "https://example.com/verify",
897+
"verification_uri_complete": "https://example.com/verify?code=123",
898+
"expires_in": 240,
899+
"interval": 5,
900+
},
901+
})
902+
903+
err := authLoginRun(&LoginOptions{
904+
Factory: f,
905+
Ctx: context.Background(),
906+
Scope: "im:message:send",
907+
NoWait: true,
908+
})
909+
if err != nil {
910+
t.Fatalf("authLoginRun() error = %v", err)
911+
}
912+
913+
dec := json.NewDecoder(strings.NewReader(stdout.String()))
914+
var data map[string]interface{}
915+
if err := dec.Decode(&data); err != nil {
916+
t.Fatalf("Decode(stdout first event) error = %v, stdout=%q", err, stdout.String())
917+
}
918+
hint, _ := data["hint"].(string)
919+
for _, want := range []string{
920+
"exactly as returned by the CLI",
921+
"opaque string",
922+
"Do not URL-encode or decode it",
923+
"do not add %20, spaces, or punctuation",
924+
"do not wrap it as Markdown link text",
925+
"fenced code block containing only the raw URL",
926+
} {
927+
if !strings.Contains(hint, want) {
928+
t.Fatalf("hint missing %q, got:\n%s", want, hint)
929+
}
930+
}
931+
}
932+
882933
func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *testing.T) {
883934
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
884935
ProfileName: "default",
@@ -917,6 +968,60 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *
917968
}
918969
}
919970

971+
func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t *testing.T) {
972+
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
973+
ProfileName: "default",
974+
AppID: "cli_test",
975+
AppSecret: "secret",
976+
Brand: core.BrandFeishu,
977+
})
978+
979+
reg.Register(&httpmock.Stub{
980+
Method: "POST",
981+
URL: larkauth.PathDeviceAuthorization,
982+
Body: map[string]interface{}{
983+
"device_code": "device-code",
984+
"user_code": "user-code",
985+
"verification_uri": "https://example.com/verify",
986+
"verification_uri_complete": "https://example.com/verify?code=123",
987+
"expires_in": 240,
988+
"interval": 5,
989+
},
990+
})
991+
992+
ctx, cancel := context.WithCancel(context.Background())
993+
cancel()
994+
err := authLoginRun(&LoginOptions{
995+
Factory: f,
996+
Ctx: ctx,
997+
Scope: "im:message:send",
998+
JSON: true,
999+
})
1000+
if err == nil {
1001+
t.Fatal("expected error from cancelled context")
1002+
}
1003+
1004+
dec := json.NewDecoder(strings.NewReader(stdout.String()))
1005+
var data map[string]interface{}
1006+
if err := dec.Decode(&data); err != nil {
1007+
t.Fatalf("Decode(stdout first event) error = %v, stdout=%q", err, stdout.String())
1008+
}
1009+
hint, _ := data["agent_hint"].(string)
1010+
for _, want := range []string{
1011+
"timeout >= 600s",
1012+
"逐字原样转发 CLI 返回的 URL",
1013+
"opaque string",
1014+
"不要做 URL 编码或解码",
1015+
"不要补 `%20`、空格或标点",
1016+
"不要改写成 Markdown 链接",
1017+
"只包含该 URL 的代码块单独输出",
1018+
} {
1019+
if !strings.Contains(hint, want) {
1020+
t.Fatalf("agent_hint missing %q, got:\n%s", want, hint)
1021+
}
1022+
}
1023+
}
1024+
9201025
func TestGetDomainMetadata_ExcludesEvent(t *testing.T) {
9211026
domains := getDomainMetadata("zh")
9221027
for _, dm := range domains {

skills/lark-shared/SKILL.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ description: "飞书/Lark CLI 共享基础:应用配置初始化、认证登
1212

1313
首次使用需运行 `lark-cli config init` 完成应用配置。
1414

15-
当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户:
15+
当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户。
16+
17+
**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 的代码块单独输出。
1618

1719
```bash
1820
# 发起配置(该命令会阻塞直到用户打开链接并完成操作或过期)
@@ -51,7 +53,7 @@ lark-cli config init --new
5153

5254
#### Bot 身份(`--as bot`
5355

54-
将错误中的 `console_url` 提供给用户,引导去后台开通 scope。**禁止**对 bot 执行 `auth login`
56+
将错误中的 `console_url` 原样提供给用户,引导去后台开通 scope。**禁止**对 bot 执行 `auth login`
5557

5658
#### User 身份(`--as user`
5759

@@ -64,7 +66,7 @@ lark-cli auth login --scope "<missing_scope>" # 按具体 scope 授权(推
6466

6567
#### Agent 代理发起认证(推荐)
6668

67-
当你作为 AI agent 需要帮用户完成认证时,使用background方式 执行以下命令发起授权流程, 并将授权链接发给用户
69+
当你作为 AI agent 需要帮用户完成认证时,使用background方式 执行以下命令发起授权流程, 并将授权链接原样发给用户
6870

6971
```bash
7072
# 发起授权(阻塞直到用户授权完成或过期)

0 commit comments

Comments
 (0)