From 4e2baf75e6287b40c0a2739a78dfcb217b5cb41e Mon Sep 17 00:00:00 2001 From: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:07:02 +0800 Subject: [PATCH 1/2] feat: wrap no-wait verification url as markdown autolink The auth login --no-wait JSON output emits verification_url for an AI agent to relay to the end user. URLs from the OAuth device-flow endpoint contain underscores in query parameters; when the agent embeds the URL into a markdown reply, the underscores are parsed as italic markers and the URL gets truncated by the renderer. Wrap the URL value in angle brackets so it is recognized as a markdown autolink. Renderers leave the inner content unparsed, the link stays clickable, and the user sees the full URL for OAuth review. Scoped to the --no-wait JSON path. Interactive --json output and plain-text stderr remain unchanged: those paths target programs and human users respectively, where bare URLs are appropriate. Change-Id: I80595b0fc63821e19fdd1032b1bb02a9eb224481 --- cmd/auth/login.go | 4 +- cmd/auth/login_test.go | 201 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 2 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index add8762bb..e506a5512 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -230,10 +230,10 @@ func authLoginRun(opts *LoginOptions) error { fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err) } data := map[string]interface{}{ - "verification_url": authResp.VerificationUriComplete, + "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 verbatim — it is wrapped in <> as a markdown autolink so it renders correctly even when the URL contains underscores. Do not unwrap or modify it. 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_test.go b/cmd/auth/login_test.go index a3b84af37..21849f496 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -904,6 +904,207 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t * } } +func TestAuthLoginRun_NoWaitJSONWrapsVerificationURLAsAutolink(t *testing.T) { + keyring.MockInit() + setupLoginConfigDir(t) + + f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{ + ProfileName: "default", + AppID: "cli_test", + AppSecret: "secret", + Brand: core.BrandFeishu, + }) + + const completeURL = "https://example.com/verify?state=abc_def_ghi&scope=mail:user_mailbox:readonly" + 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": completeURL, + "expires_in": 240, + "interval": 0, + }, + }) + + err := authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "im:message:send", + NoWait: true, + }) + if err != nil { + t.Fatalf("authLoginRun() error = %v", err) + } + + var payload map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { + t.Fatalf("unmarshal stdout: %v\nstdout: %s", err, stdout.String()) + } + got, ok := payload["verification_url"].(string) + if !ok { + t.Fatalf("verification_url missing or not string: %#v", payload) + } + want := "<" + completeURL + ">" + if got != want { + t.Fatalf("verification_url = %q, want %q (--no-wait wraps URL as markdown autolink)", got, want) + } + if _, exists := payload["verification_url_markdown"]; exists { + t.Fatalf("verification_url_markdown should not be a separate field, got payload: %#v", payload) + } +} + +func TestAuthLoginRun_NoWaitHintInstructsVerbatimDisplay(t *testing.T) { + keyring.MockInit() + setupLoginConfigDir(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?state=a_b_c", + "expires_in": 240, + "interval": 0, + }, + }) + + err := authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "im:message:send", + NoWait: true, + }) + if err != nil { + t.Fatalf("authLoginRun() error = %v", err) + } + + var payload map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { + t.Fatalf("unmarshal stdout: %v", err) + } + hint, _ := payload["hint"].(string) + for _, want := range []string{ + "verification_url", + "verbatim", + } { + if !strings.Contains(hint, want) { + t.Fatalf("hint missing %q, got: %s", want, hint) + } + } + if strings.Contains(hint, "verification_url_markdown") { + t.Fatalf("hint should not reference removed field verification_url_markdown, got: %s", hint) + } +} + +func TestAuthLoginRun_InteractiveJSONKeepsRawURL(t *testing.T) { + keyring.MockInit() + setupLoginConfigDir(t) + + original := pollDeviceToken + t.Cleanup(func() { pollDeviceToken = original }) + pollDeviceToken = func(_ context.Context, _ *http.Client, _, _ string, _ core.LarkBrand, _ string, _, _ int, _ io.Writer) *larkauth.DeviceFlowResult { + return &larkauth.DeviceFlowResult{OK: false, Message: "stub"} + } + + f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{ + ProfileName: "default", + AppID: "cli_test", + AppSecret: "secret", + Brand: core.BrandFeishu, + }) + + const completeURL = "https://example.com/verify?state=a_b_c&scope=mail:user_mailbox:readonly" + 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": completeURL, + "expires_in": 240, + "interval": 0, + }, + }) + + _ = authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "im:message:send", + JSON: true, + }) + + dec := json.NewDecoder(strings.NewReader(stdout.String())) + var first map[string]interface{} + if err := dec.Decode(&first); err != nil { + t.Fatalf("decode first JSON event: %v\nstdout: %s", err, stdout.String()) + } + if first["verification_uri_complete"] != completeURL { + t.Fatalf("verification_uri_complete = %v, want raw URL %q", first["verification_uri_complete"], completeURL) + } + if _, exists := first["verification_uri_complete_markdown"]; exists { + t.Fatalf("interactive --json must not add markdown field; only --no-wait wraps URL. payload: %#v", first) + } +} + +func TestAuthLoginRun_PlainTextStderrShowsBareURL(t *testing.T) { + keyring.MockInit() + setupLoginConfigDir(t) + + original := pollDeviceToken + t.Cleanup(func() { pollDeviceToken = original }) + pollDeviceToken = func(_ context.Context, _ *http.Client, _, _ string, _ core.LarkBrand, _ string, _, _ int, _ io.Writer) *larkauth.DeviceFlowResult { + return &larkauth.DeviceFlowResult{OK: false, Message: "stub"} + } + + f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{ + ProfileName: "default", + AppID: "cli_test", + AppSecret: "secret", + Brand: core.BrandFeishu, + }) + + const completeURL = "https://example.com/verify?state=a_b_c" + 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": completeURL, + "expires_in": 240, + "interval": 0, + }, + }) + + _ = authLoginRun(&LoginOptions{ + Factory: f, + Ctx: context.Background(), + Scope: "im:message:send", + }) + + got := stderr.String() + if !strings.Contains(got, " "+completeURL+"\n") { + t.Fatalf("stderr missing bare URL line, got:\n%s", got) + } + if strings.Contains(got, "<"+completeURL+">") { + t.Fatalf("stderr should not contain autolink form for human users, got:\n%s", got) + } +} + func TestGetDomainMetadata_ExcludesEvent(t *testing.T) { domains := getDomainMetadata("zh") for _, dm := range domains { From dd9d06fed1f7e6e828a99a68bfc5d5d8032017db Mon Sep 17 00:00:00 2001 From: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:00:51 +0800 Subject: [PATCH 2/2] fix: use [`URL`](URL) instead of for verification_url The autolink form () failed in two layers observed in practice: 1. LLM transcription strips <> as if it were an HTML tag remnant. 2. Some markdown renderers do not honor CommonMark autolink semantics and still apply emphasis parsing inside <>, truncating URLs that contain underscores. Switch to a double-defense form: the URL text wrapped in backticks gives inline-code semantics (emphasis cannot mangle the displayed text), and the outer markdown link gives clickability. Even if the renderer mishandles the link destination, the backtick-wrapped text remains intact and copy-pastable. LLM passthrough preserves both backticks (sacred code-block convention) and link syntax (the most trained markdown form), avoiding the strip seen with <>. Change-Id: I3ee9dae2d0d8ad4a6a13430a5ca3f60451507fa1 --- cmd/auth/login.go | 4 ++-- cmd/auth/login_test.go | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index e506a5512..4f96dfc70 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -230,10 +230,10 @@ func authLoginRun(opts *LoginOptions) error { fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err) } data := map[string]interface{}{ - "verification_url": "<" + authResp.VerificationUriComplete + ">", + "verification_url": "[`" + authResp.VerificationUriComplete + "`](" + authResp.VerificationUriComplete + ")", "device_code": authResp.DeviceCode, "expires_in": authResp.ExpiresIn, - "hint": fmt.Sprintf("Show verification_url to the user verbatim — it is wrapped in <> as a markdown autolink so it renders correctly even when the URL contains underscores. Do not unwrap or modify it. 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 verbatim. It is pre-formatted as a markdown link `[`URL`](URL)`: the inner backticks render the URL as inline code so emphasis parsing cannot mangle underscores, while the outer link wrapper keeps it clickable. Do not unwrap, escape, or rewrite any part. 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_test.go b/cmd/auth/login_test.go index 21849f496..e312f255e 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -904,7 +904,7 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t * } } -func TestAuthLoginRun_NoWaitJSONWrapsVerificationURLAsAutolink(t *testing.T) { +func TestAuthLoginRun_NoWaitJSONWrapsVerificationURLAsCodedLink(t *testing.T) { keyring.MockInit() setupLoginConfigDir(t) @@ -947,12 +947,9 @@ func TestAuthLoginRun_NoWaitJSONWrapsVerificationURLAsAutolink(t *testing.T) { if !ok { t.Fatalf("verification_url missing or not string: %#v", payload) } - want := "<" + completeURL + ">" + want := "[`" + completeURL + "`](" + completeURL + ")" if got != want { - t.Fatalf("verification_url = %q, want %q (--no-wait wraps URL as markdown autolink)", got, want) - } - if _, exists := payload["verification_url_markdown"]; exists { - t.Fatalf("verification_url_markdown should not be a separate field, got payload: %#v", payload) + t.Fatalf("verification_url = %q,\nwant %q\n(--no-wait wraps URL as [`URL`](URL): backticks shield text from emphasis parsing, link wrapper provides clickability)", got, want) } } @@ -998,14 +995,12 @@ func TestAuthLoginRun_NoWaitHintInstructsVerbatimDisplay(t *testing.T) { for _, want := range []string{ "verification_url", "verbatim", + "backtick", } { if !strings.Contains(hint, want) { t.Fatalf("hint missing %q, got: %s", want, hint) } } - if strings.Contains(hint, "verification_url_markdown") { - t.Fatalf("hint should not reference removed field verification_url_markdown, got: %s", hint) - } } func TestAuthLoginRun_InteractiveJSONKeepsRawURL(t *testing.T) {