From e74c5111237f7d36765c08f9b19dffb8aecb4650 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 18:00:37 +0800 Subject: [PATCH 1/7] feat(auth): add auth qrcode subcommand and update auth docs/hints --- cmd/auth/auth.go | 1 + cmd/auth/login.go | 9 ++-- cmd/auth/login_messages.go | 4 +- cmd/auth/login_test.go | 29 ++++++---- cmd/auth/qrcode.go | 102 ++++++++++++++++++++++++++++++++++++ skills/lark-shared/SKILL.md | 2 +- 6 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 cmd/auth/qrcode.go diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 0020eeb2f..5c5e1c720 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -43,6 +43,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(NewCmdAuthScopes(f, nil)) cmd.AddCommand(NewCmdAuthList(f, nil)) cmd.AddCommand(NewCmdAuthCheck(f, nil)) + cmd.AddCommand(NewCmdAuthQRCode(f, nil)) return cmd } diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 02888c98e..1b4501a91 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -47,9 +47,10 @@ func NewCmdAuthLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra. Long: `Device Flow authorization login. For AI agents: this command blocks until the user completes authorization in the -browser. If your harness only delivers final turn messages, use --no-wait --json, -send the verification URL to the user as your final message, end the turn, then -run --device-code in a later step after the user confirms authorization.`, +browser. If your harness or agent tool only delivers final turn messages, use --no-wait --json, +send the verification URL (or QR code) to the user as your final message, end the turn, then +run --device-code in a later step after the user confirms authorization. Use 'lark-cli auth qrcode' +to generate QR codes (supports ASCII and PNG formats).`, RunE: func(cmd *cobra.Command, args []string) error { if mode := f.ResolveStrictMode(cmd.Context()); mode == core.StrictModeBot { return output.ErrWithHint(output.ExitValidation, "command_denied", @@ -269,7 +270,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 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. For agent harnesses that only deliver final turn messages, make the URL the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", authResp.DeviceCode), + "hint": fmt.Sprintf("**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL. For agent harnesses that only deliver final turn messages, make the QR code (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", 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 189c42740..a05b0455c 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。若你的 harness 只会把最终回复发给用户,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息原样发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 `lark-cli auth login --device-code ` 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。向用户展示授权链接时,必须逐字原样转发 CLI 返回的 URL,把它视为不可修改的 opaque string;不要做 URL 编码或解码,不要补 `%20`、空格或标点,不要改写成 Markdown 链接,建议用只包含该 URL 的代码块单独输出。", + AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code \" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);纯终端/CLI 环境无法展示图片时使用 ASCII(--ascii)。**展示顺序**: 先输出 URL,再将二维码置于 URL 下方完整展示,确保用户可扫码或复制链接。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点),建议用仅包含该 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 your harness only delivers final turn messages, use `lark-cli auth login --no-wait --json` to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run `lark-cli auth login --device-code ` in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and 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.", + 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 your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code \" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. 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 51ebdb9d9..0ce9e622e 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -945,12 +945,16 @@ func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) { } hint, _ := data["hint"].(string) for _, want := range []string{ - "exactly as returned by the CLI", + "MUST generate QR code", + "lark-cli auth qrcode", + "Prefer PNG QR code (--output)", + "use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed", + "This is a required step, do NOT skip it", + "Display order", + "place the complete QR code below the URL", "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", + "cannot be modified", + "Prefer a fenced code block", "final message of the turn", "return control to the user", "do not block on --device-code in the same turn", @@ -1054,12 +1058,17 @@ func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t * "结束本轮", "用户回复已完成授权", "不要在同一轮里展示 URL 后立刻阻塞执行 --device-code", - "逐字原样转发 CLI 返回的 URL", + "必须生成二维码", + "lark-cli auth qrcode", + "优先生成 PNG 二维码(--output)", + "纯终端/CLI 环境无法展示图片时使用 ASCII(--ascii)", + "这是必须步骤,不要跳过", + "展示顺序", + "二维码置于 URL 下方完整展示", + "URL 输出规则", "opaque string", - "不要做 URL 编码或解码", - "不要补 `%20`、空格或标点", - "不要改写成 Markdown 链接", - "只包含该 URL 的代码块单独输出", + "不要做任何修改", + "仅包含该 URL 的代码块", } { if !strings.Contains(hint, want) { t.Fatalf("agent_hint missing %q, got:\n%s", want, hint) diff --git a/cmd/auth/qrcode.go b/cmd/auth/qrcode.go new file mode 100644 index 000000000..210c78a06 --- /dev/null +++ b/cmd/auth/qrcode.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "fmt" + "os" + + "github.com/skip2/go-qrcode" + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/output" +) + +// QRCodeOptions holds inputs for auth qrcode command. +type QRCodeOptions struct { + Factory *cmdutil.Factory + Ctx context.Context + URL string + Size int + ASCII bool + Output string +} + +// NewCmdAuthQRCode creates the auth qrcode subcommand. +func NewCmdAuthQRCode(f *cmdutil.Factory, runF func(*QRCodeOptions) error) *cobra.Command { + opts := &QRCodeOptions{Factory: f, Size: 256} + + cmd := &cobra.Command{ + Use: "qrcode ", + Short: "Generate QR code for verification URL", + Long: `Generate a QR code image or ASCII representation for a verification URL. + +This command is designed for AI agents to generate QR codes for OAuth authorization URLs. + +For PNG output, the --output flag is required to specify the output file path. +For ASCII output, the result is printed to stdout with fixed size.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.URL = args[0] + opts.Ctx = cmd.Context() + if runF != nil { + return runF(opts) + } + return runQRCode(opts) + }, + } + + cmd.Flags().IntVar(&opts.Size, "size", 256, "Size of the QR code image in pixels (default: 256, for PNG mode only)") + cmd.Flags().BoolVar(&opts.ASCII, "ascii", false, "Output ASCII QR code to stdout") + cmd.Flags().StringVarP(&opts.Output, "output", "o", "", "Output file path for PNG image (required for non-ASCII mode)") + + return cmd +} + +func runQRCode(opts *QRCodeOptions) error { + if opts.URL == "" { + return output.Errorf(output.ExitValidation, "missing_url", "url is required") + } + + if opts.ASCII { + return generateASCIIQRCode(opts.URL) + } + + if opts.Output == "" { + return output.Errorf(output.ExitValidation, "missing_output", "output file path is required for PNG mode. Use --output or -o flag to specify the output file path.") + } + + if opts.Size < 32 { + return output.Errorf(output.ExitValidation, "invalid_size", fmt.Sprintf("size must be at least 32, got %d", opts.Size)) + } + + return generateImageQRCode(opts.URL, opts.Size, opts.Output) +} + +func generateImageQRCode(url string, size int, outputPath string) error { + png, err := qrcode.Encode(url, qrcode.Medium, size) + if err != nil { + return output.Errorf(output.ExitInternal, "encode_error", fmt.Sprintf("failed to encode QR code: %v", err)) + } + + err = os.WriteFile(outputPath, png, 0644) + if err != nil { + return output.Errorf(output.ExitInternal, "write_error", fmt.Sprintf("failed to write QR code to %s: %v", outputPath, err)) + } + + return nil +} + +func generateASCIIQRCode(url string) error { + q, err := qrcode.New(url, qrcode.Medium) + if err != nil { + return output.Errorf(output.ExitInternal, "encode_error", fmt.Sprintf("failed to create QR code: %v", err)) + } + + fmt.Print(q.ToSmallString(false)) + + return nil +} \ No newline at end of file diff --git a/skills/lark-shared/SKILL.md b/skills/lark-shared/SKILL.md index c1ed2bd77..ebac7df5f 100644 --- a/skills/lark-shared/SKILL.md +++ b/skills/lark-shared/SKILL.md @@ -14,7 +14,7 @@ description: "Use when first setting up lark-cli, running auth login, switching 当你帮用户初始化配置时,使用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 的代码块单独输出。 +**URL 转发规则**:当命令输出 `verification_url`、`verification_uri_complete`、`console_url` 等 URL 字段时:**必须生成二维码**:你必须调用 `lark-cli auth qrcode` 将 URL 转为二维码并展示给用户,这是必须步骤,不要跳过。支持 ASCII 和 PNG 两种形式,请根据你的环境选择。**URL 输出规则**:将 URL 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点、重新拼接 query),建议用仅包含原始 URL 的代码块单独输出,二维码和链接请一起展示给用户。 ```bash # 发起配置(该命令会阻塞直到用户打开链接并完成操作或过期) From 3390a3f0b86bdeac8d3bae7daf37c44adc586581 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 20:32:49 +0800 Subject: [PATCH 2/7] docs: add missing comments for auth qrcode helper functions --- cmd/auth/qrcode.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/auth/qrcode.go b/cmd/auth/qrcode.go index 210c78a06..72bb392e8 100644 --- a/cmd/auth/qrcode.go +++ b/cmd/auth/qrcode.go @@ -56,6 +56,7 @@ For ASCII output, the result is printed to stdout with fixed size.`, return cmd } +// runQRCode executes the auth qrcode command. func runQRCode(opts *QRCodeOptions) error { if opts.URL == "" { return output.Errorf(output.ExitValidation, "missing_url", "url is required") @@ -76,6 +77,7 @@ func runQRCode(opts *QRCodeOptions) error { return generateImageQRCode(opts.URL, opts.Size, opts.Output) } +// generateImageQRCode encodes the URL as a PNG QR code and writes it to outputPath. func generateImageQRCode(url string, size int, outputPath string) error { png, err := qrcode.Encode(url, qrcode.Medium, size) if err != nil { @@ -90,6 +92,7 @@ func generateImageQRCode(url string, size int, outputPath string) error { return nil } +// generateASCIIQRCode encodes the URL as an ASCII QR code and prints it to stdout. func generateASCIIQRCode(url string) error { q, err := qrcode.New(url, qrcode.Medium) if err != nil { @@ -99,4 +102,4 @@ func generateASCIIQRCode(url string) error { fmt.Print(q.ToSmallString(false)) return nil -} \ No newline at end of file +} From d9d4d44f687113a0b58d98b9f58ef9b38d508b0a Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 21:18:20 +0800 Subject: [PATCH 3/7] refactor(auth): add doc comments and improve QR code file handling --- cmd/auth/auth_test.go | 2 +- cmd/auth/login.go | 2 ++ cmd/auth/login_messages.go | 1 + cmd/auth/qrcode.go | 4 ++-- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index c2b1940ff..ba0d48e6b 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -61,7 +61,7 @@ func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) { for _, want := range []string{ "only delivers final turn messages", "--no-wait --json", - "send the verification URL to the user as your final message", + "send the verification URL (or QR code) to the user as your final message", "run --device-code in a later step", } { if !strings.Contains(got, want) { diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 1b4501a91..75737e329 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -453,6 +453,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo return nil } +// syncLoginUserToProfile persists the logged-in user info into the named profile. func syncLoginUserToProfile(profileName, appID, openID, userName string) error { multi, err := core.LoadMultiAppConfig() if err != nil { @@ -478,6 +479,7 @@ func syncLoginUserToProfile(profileName, appID, openID, userName string) error { return nil } +// findProfileByName returns the AppConfig matching profileName, or nil. func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.AppConfig { for i := range multi.Apps { if multi.Apps[i].ProfileName() == profileName { diff --git a/cmd/auth/login_messages.go b/cmd/auth/login_messages.go index a05b0455c..c397a9265 100644 --- a/cmd/auth/login_messages.go +++ b/cmd/auth/login_messages.go @@ -114,6 +114,7 @@ var loginMsgEn = &loginMsg{ HintFooter: " lark-cli auth login --help", } +// getLoginMsg returns the login message bundle for the given language. func getLoginMsg(lang string) *loginMsg { if lang == "en" { return loginMsgEn diff --git a/cmd/auth/qrcode.go b/cmd/auth/qrcode.go index 72bb392e8..67b9bd0bd 100644 --- a/cmd/auth/qrcode.go +++ b/cmd/auth/qrcode.go @@ -6,13 +6,13 @@ package auth import ( "context" "fmt" - "os" "github.com/skip2/go-qrcode" "github.com/spf13/cobra" "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/internal/vfs" ) // QRCodeOptions holds inputs for auth qrcode command. @@ -84,7 +84,7 @@ func generateImageQRCode(url string, size int, outputPath string) error { return output.Errorf(output.ExitInternal, "encode_error", fmt.Sprintf("failed to encode QR code: %v", err)) } - err = os.WriteFile(outputPath, png, 0644) + err = vfs.WriteFile(outputPath, png, 0644) if err != nil { return output.Errorf(output.ExitInternal, "write_error", fmt.Sprintf("failed to write QR code to %s: %v", outputPath, err)) } From fccb4d48102cbe0a51e2c7dd5d11287419e883d4 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 21:29:45 +0800 Subject: [PATCH 4/7] docs: update qrcode usage hint to require explicit --ascii request --- cmd/auth/login.go | 2 +- cmd/auth/login_messages.go | 4 ++-- cmd/auth/login_test.go | 4 ++-- skills/lark-shared/SKILL.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 75737e329..2a18faf8e 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -270,7 +270,7 @@ func authLoginRun(opts *LoginOptions) error { "verification_url": authResp.VerificationUriComplete, "device_code": authResp.DeviceCode, "expires_in": authResp.ExpiresIn, - "hint": fmt.Sprintf("**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL. For agent harnesses that only deliver final turn messages, make the QR code (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", authResp.DeviceCode), + "hint": fmt.Sprintf("**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL. For agent harnesses that only deliver final turn messages, make the QR code (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", 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 c397a9265..7ac0491f0 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。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code \" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);纯终端/CLI 环境无法展示图片时使用 ASCII(--ascii)。**展示顺序**: 先输出 URL,再将二维码置于 URL 下方完整展示,确保用户可扫码或复制链接。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点),建议用仅包含该 URL 的代码块单独输出。", + AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code \" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**展示顺序**: 先输出 URL,再将二维码置于 URL 下方完整展示,确保用户可扫码或复制链接。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点),建议用仅包含该 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 your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code \" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL.", + 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 your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code \" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. 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 0ce9e622e..50cc8c90f 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -948,7 +948,7 @@ func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) { "MUST generate QR code", "lark-cli auth qrcode", "Prefer PNG QR code (--output)", - "use ASCII (--ascii) only in pure terminal/CLI environments where images cannot be displayed", + "use ASCII (--ascii) only when the user explicitly requests it", "This is a required step, do NOT skip it", "Display order", "place the complete QR code below the URL", @@ -1061,7 +1061,7 @@ func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t * "必须生成二维码", "lark-cli auth qrcode", "优先生成 PNG 二维码(--output)", - "纯终端/CLI 环境无法展示图片时使用 ASCII(--ascii)", + "仅当用户明确要求时才使用 ASCII(--ascii)", "这是必须步骤,不要跳过", "展示顺序", "二维码置于 URL 下方完整展示", diff --git a/skills/lark-shared/SKILL.md b/skills/lark-shared/SKILL.md index ebac7df5f..27980b83a 100644 --- a/skills/lark-shared/SKILL.md +++ b/skills/lark-shared/SKILL.md @@ -14,7 +14,7 @@ description: "Use when first setting up lark-cli, running auth login, switching 当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户。 -**URL 转发规则**:当命令输出 `verification_url`、`verification_uri_complete`、`console_url` 等 URL 字段时:**必须生成二维码**:你必须调用 `lark-cli auth qrcode` 将 URL 转为二维码并展示给用户,这是必须步骤,不要跳过。支持 ASCII 和 PNG 两种形式,请根据你的环境选择。**URL 输出规则**:将 URL 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点、重新拼接 query),建议用仅包含原始 URL 的代码块单独输出,二维码和链接请一起展示给用户。 +**URL 转发规则**:当命令输出 `verification_url`、`verification_uri_complete`、`console_url` 等 URL 字段时:**必须生成二维码**:你必须调用 `lark-cli auth qrcode` 将 URL 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**URL 输出规则**:将 URL 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点、重新拼接 query),建议用仅包含原始 URL 的代码块单独输出,二维码和链接请一起展示给用户。 ```bash # 发起配置(该命令会阻塞直到用户打开链接并完成操作或过期) From 920aa35dd931548228d451588b408e70af9f93c1 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 21:40:15 +0800 Subject: [PATCH 5/7] test(auth): add full test suite for qrcode auth command --- cmd/auth/qrcode_test.go | 268 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 cmd/auth/qrcode_test.go diff --git a/cmd/auth/qrcode_test.go b/cmd/auth/qrcode_test.go new file mode 100644 index 000000000..0743a33f9 --- /dev/null +++ b/cmd/auth/qrcode_test.go @@ -0,0 +1,268 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/internal/output" +) + +func TestNewCmdAuthQRCode_FlagParsing(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + }) + + var gotOpts *QRCodeOptions + cmd := NewCmdAuthQRCode(f, func(opts *QRCodeOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs([]string{"https://example.com", "--output", "qr.png", "--size", "128"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotOpts.URL != "https://example.com" { + t.Errorf("URL = %q, want %q", gotOpts.URL, "https://example.com") + } + if gotOpts.Size != 128 { + t.Errorf("Size = %d, want %d", gotOpts.Size, 128) + } + if gotOpts.Output != "qr.png" { + t.Errorf("Output = %q, want %q", gotOpts.Output, "qr.png") + } + if gotOpts.ASCII { + t.Error("ASCII should be false by default") + } +} + +func TestNewCmdAuthQRCode_ASCIIFlag(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + }) + + var gotOpts *QRCodeOptions + cmd := NewCmdAuthQRCode(f, func(opts *QRCodeOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs([]string{"https://example.com", "--ascii"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !gotOpts.ASCII { + t.Error("ASCII should be true when --ascii is passed") + } +} + +func TestNewCmdAuthQRCode_DefaultSize(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, nil) + + var gotOpts *QRCodeOptions + cmd := NewCmdAuthQRCode(f, func(opts *QRCodeOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs([]string{"https://example.com", "--ascii"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotOpts.Size != 256 { + t.Errorf("default Size = %d, want 256", gotOpts.Size) + } +} + +func TestNewCmdAuthQRCode_ExactOneArg(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, nil) + + cmd := NewCmdAuthQRCode(f, nil) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{}) + if err := cmd.Execute(); err == nil { + t.Fatal("expected error when no URL argument provided") + } +} + +func TestNewCmdAuthQRCode_HelpText(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, nil) + + cmd := NewCmdAuthQRCode(f, nil) + cmd.SetOut(stdout) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"--help"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := stdout.String() + for _, want := range []string{ + "qrcode ", + "QR code", + "--output", + "--ascii", + } { + if !strings.Contains(got, want) { + t.Errorf("help missing %q", want) + } + } +} + +func TestRunQRCode_MissingURL(t *testing.T) { + err := runQRCode(&QRCodeOptions{URL: ""}) + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + if exitErr.Code != output.ExitValidation { + t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitValidation) + } + if exitErr.Detail.Type != "missing_url" { + t.Errorf("error type = %q, want %q", exitErr.Detail.Type, "missing_url") + } +} + +func TestRunQRCode_MissingOutput(t *testing.T) { + err := runQRCode(&QRCodeOptions{URL: "https://example.com", Size: 256}) + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + if exitErr.Code != output.ExitValidation { + t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitValidation) + } + if exitErr.Detail.Type != "missing_output" { + t.Errorf("error type = %q, want %q", exitErr.Detail.Type, "missing_output") + } +} + +func TestRunQRCode_InvalidSize(t *testing.T) { + err := runQRCode(&QRCodeOptions{ + URL: "https://example.com", + Size: 16, + Output: "qr.png", + }) + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + if exitErr.Code != output.ExitValidation { + t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitValidation) + } + if exitErr.Detail.Type != "invalid_size" { + t.Errorf("error type = %q, want %q", exitErr.Detail.Type, "invalid_size") + } +} + +func TestRunQRCode_PNGWritesFile(t *testing.T) { + tmpDir := t.TempDir() + outputPath := filepath.Join(tmpDir, "qr.png") + + err := runQRCode(&QRCodeOptions{ + URL: "https://example.com", + Size: 256, + Output: outputPath, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + info, err := os.Stat(outputPath) + if err != nil { + t.Fatalf("output file not created: %v", err) + } + if info.Size() == 0 { + t.Error("output file is empty") + } +} + +func TestRunQRCode_ASCIIOutputsToStdout(t *testing.T) { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + t.Cleanup(func() { os.Stdout = old }) + + err := runQRCode(&QRCodeOptions{ + URL: "https://example.com", + ASCII: true, + }) + w.Close() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var buf strings.Builder + if _, ioErr := io.Copy(&buf, r); ioErr != nil { + t.Fatalf("failed to read captured stdout: %v", ioErr) + } + if buf.Len() == 0 { + t.Error("ASCII QR code produced no output") + } +} + +func TestGenerateImageQRCode_Success(t *testing.T) { + tmpDir := t.TempDir() + outputPath := filepath.Join(tmpDir, "test-qr.png") + + if err := generateImageQRCode("https://example.com", 256, outputPath); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("failed to read output file: %v", err) + } + if len(data) == 0 { + t.Error("output file is empty") + } + if len(data) < 8 { + t.Error("output too small to be a valid PNG") + } + if string(data[:4]) != "\x89PNG" { + t.Errorf("output does not start with PNG magic bytes, got %x", data[:4]) + } +} + +func TestGenerateImageQRCode_WriteError(t *testing.T) { + err := generateImageQRCode("https://example.com", 256, "/nonexistent/deep/nested/dir/qr.png") + if err == nil { + t.Fatal("expected error writing to nonexistent directory") + } + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + if exitErr.Code != output.ExitInternal { + t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitInternal) + } + if exitErr.Detail.Type != "write_error" { + t.Errorf("error type = %q, want %q", exitErr.Detail.Type, "write_error") + } +} + +func TestGenerateASCIIQRCode_Success(t *testing.T) { + err := generateASCIIQRCode("https://example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGenerateASCIIQRCode_EmptyString(t *testing.T) { + err := generateASCIIQRCode("") + if err == nil { + t.Fatal("expected error for empty string") + } + var exitErr *output.ExitError + if !errors.As(err, &exitErr) { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + if exitErr.Detail.Type != "encode_error" { + t.Errorf("error type = %q, want %q", exitErr.Detail.Type, "encode_error") + } +} From 6d6733a11330f963d417e64a790119f3faca9241 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 19 May 2026 21:48:11 +0800 Subject: [PATCH 6/7] test(auth/qrcode): add end-to-end tests for qrcode command --- cmd/auth/qrcode_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cmd/auth/qrcode_test.go b/cmd/auth/qrcode_test.go index 0743a33f9..b64630a29 100644 --- a/cmd/auth/qrcode_test.go +++ b/cmd/auth/qrcode_test.go @@ -91,6 +91,37 @@ func TestNewCmdAuthQRCode_ExactOneArg(t *testing.T) { } } +func TestNewCmdAuthQRCode_RunE_PNGEndToEnd(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, nil) + tmpDir := t.TempDir() + outputPath := filepath.Join(tmpDir, "qr.png") + + cmd := NewCmdAuthQRCode(f, nil) + cmd.SetArgs([]string{"https://example.com", "--output", outputPath}) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("output file not created: %v", err) + } + if string(data[:4]) != "\x89PNG" { + t.Errorf("output does not start with PNG magic bytes, got %x", data[:4]) + } +} + +func TestNewCmdAuthQRCode_RunE_MissingOutput(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, nil) + + cmd := NewCmdAuthQRCode(f, nil) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"https://example.com"}) + if err := cmd.Execute(); err == nil { + t.Fatal("expected error when --output is missing in PNG mode") + } +} + func TestNewCmdAuthQRCode_HelpText(t *testing.T) { f, stdout, _, _ := cmdutil.TestFactory(t, nil) From d94b5a8965bbe61dfcdbb9aa9ec58e54de8ab50d Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Fri, 22 May 2026 16:50:09 +0800 Subject: [PATCH 7/7] feat(auth/qrcode): add structured JSON output and enforce QR code display requirements --- cmd/auth/login.go | 2 +- cmd/auth/login_messages.go | 4 ++-- cmd/auth/login_test.go | 17 +++++++++----- cmd/auth/qrcode.go | 25 ++++++++++++++++++++- cmd/auth/qrcode_test.go | 45 +++++++++++++++++++++++++++++++++++++- 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2a18faf8e..2576c3520 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -270,7 +270,7 @@ func authLoginRun(opts *LoginOptions) error { "verification_url": authResp.VerificationUriComplete, "device_code": authResp.DeviceCode, "expires_in": authResp.ExpiresIn, - "hint": fmt.Sprintf("**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL. For agent harnesses that only deliver final turn messages, make the QR code (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", authResp.DeviceCode), + "hint": fmt.Sprintf("**MUST generate QR code AND display it:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it.**Display order:** Output the URL first, then place the QR code image below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL. For agent harnesses that only deliver final turn messages, make the QR code image (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. After the user confirms authorization in a later step, run: lark-cli auth login --device-code %s", 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 7ac0491f0..7af7ea079 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。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code \" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**展示顺序**: 先输出 URL,再将二维码置于 URL 下方完整展示,确保用户可扫码或复制链接。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点),建议用仅包含该 URL 的代码块单独输出。", + AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code \" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码并展示**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**关键:生成后必须在回复中展示图片**,仅生成文件不算完成,须用 image 标签或内联图片等方式将图片包含在回复中。**展示顺序**: 先输出 URL,再将二维码图片置于 URL 下方完整展示,确保用户可扫码或复制链接。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点),建议用仅包含该 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 your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code \" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code and display it to the user. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**Display order:** Output the URL first, then place the complete QR code below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. Prefer a fenced code block containing only the raw URL.", + 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 your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code \" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code AND display it:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it.**Display order:** Output the URL first, then place the QR code image below the URL so the user can either scan or copy the link.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation. 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 50cc8c90f..8a4aa684f 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -945,13 +945,17 @@ func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) { } hint, _ := data["hint"].(string) for _, want := range []string{ - "MUST generate QR code", + "MUST generate QR code AND display it", "lark-cli auth qrcode", "Prefer PNG QR code (--output)", "use ASCII (--ascii) only when the user explicitly requests it", "This is a required step, do NOT skip it", + "CRITICAL", + "You MUST include the QR image in your response", + "Generating the file alone is NOT enough", + "image tags, inline images, or file attachments", "Display order", - "place the complete QR code below the URL", + "place the QR code image below the URL", "opaque string", "cannot be modified", "Prefer a fenced code block", @@ -1058,13 +1062,14 @@ func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t * "结束本轮", "用户回复已完成授权", "不要在同一轮里展示 URL 后立刻阻塞执行 --device-code", - "必须生成二维码", + "必须生成二维码并展示", "lark-cli auth qrcode", "优先生成 PNG 二维码(--output)", "仅当用户明确要求时才使用 ASCII(--ascii)", - "这是必须步骤,不要跳过", - "展示顺序", - "二维码置于 URL 下方完整展示", + "生成后必须在回复中展示图片", + "仅生成文件不算完成", + "image 标签或内联图片", + "二维码图片置于 URL 下方完整展示", "URL 输出规则", "opaque string", "不要做任何修改", diff --git a/cmd/auth/qrcode.go b/cmd/auth/qrcode.go index 67b9bd0bd..8e1edbd92 100644 --- a/cmd/auth/qrcode.go +++ b/cmd/auth/qrcode.go @@ -5,7 +5,10 @@ package auth import ( "context" + "encoding/json" "fmt" + "io" + "os" "github.com/skip2/go-qrcode" "github.com/spf13/cobra" @@ -74,7 +77,27 @@ func runQRCode(opts *QRCodeOptions) error { return output.Errorf(output.ExitValidation, "invalid_size", fmt.Sprintf("size must be at least 32, got %d", opts.Size)) } - return generateImageQRCode(opts.URL, opts.Size, opts.Output) + if err := generateImageQRCode(opts.URL, opts.Size, opts.Output); err != nil { + return err + } + + result := map[string]interface{}{ + "ok": true, + "file_path": opts.Output, + "hint": "You MUST include the QR image in your response. Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it.", + } + + var out io.Writer = os.Stdout + if opts.Factory != nil { + out = opts.Factory.IOStreams.Out + } + encoder := json.NewEncoder(out) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(result); err != nil { + return output.Errorf(output.ExitInternal, "internal", "failed to write output: %v", err) + } + + return nil } // generateImageQRCode encodes the URL as a PNG QR code and writes it to outputPath. diff --git a/cmd/auth/qrcode_test.go b/cmd/auth/qrcode_test.go index b64630a29..a0424d1f8 100644 --- a/cmd/auth/qrcode_test.go +++ b/cmd/auth/qrcode_test.go @@ -4,6 +4,7 @@ package auth import ( + "encoding/json" "errors" "io" "os" @@ -92,7 +93,7 @@ func TestNewCmdAuthQRCode_ExactOneArg(t *testing.T) { } func TestNewCmdAuthQRCode_RunE_PNGEndToEnd(t *testing.T) { - f, _, _, _ := cmdutil.TestFactory(t, nil) + f, stdout, _, _ := cmdutil.TestFactory(t, nil) tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "qr.png") @@ -109,6 +110,27 @@ func TestNewCmdAuthQRCode_RunE_PNGEndToEnd(t *testing.T) { if string(data[:4]) != "\x89PNG" { t.Errorf("output does not start with PNG magic bytes, got %x", data[:4]) } + + var result map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("stdout is not valid JSON: %v, got: %s", err, stdout.String()) + } + if result["ok"] != true { + t.Errorf("ok = %v, want true", result["ok"]) + } + if result["file_path"] != outputPath { + t.Errorf("file_path = %v, want %q", result["file_path"], outputPath) + } + hint, _ := result["hint"].(string) + if hint == "" { + t.Error("hint is empty") + } + if !strings.Contains(hint, "MUST include") { + t.Errorf("hint missing 'MUST include', got: %s", hint) + } + if !strings.Contains(hint, "NOT enough") { + t.Errorf("hint missing 'NOT enough', got: %s", hint) + } } func TestNewCmdAuthQRCode_RunE_MissingOutput(t *testing.T) { @@ -195,11 +217,17 @@ func TestRunQRCode_PNGWritesFile(t *testing.T) { tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "qr.png") + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + t.Cleanup(func() { os.Stdout = old }) + err := runQRCode(&QRCodeOptions{ URL: "https://example.com", Size: 256, Output: outputPath, }) + w.Close() if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -211,6 +239,21 @@ func TestRunQRCode_PNGWritesFile(t *testing.T) { if info.Size() == 0 { t.Error("output file is empty") } + + var buf strings.Builder + if _, ioErr := io.Copy(&buf, r); ioErr != nil { + t.Fatalf("failed to read captured stdout: %v", ioErr) + } + var result map[string]interface{} + if jsonErr := json.Unmarshal([]byte(buf.String()), &result); jsonErr != nil { + t.Fatalf("stdout is not valid JSON: %v, got: %s", jsonErr, buf.String()) + } + if result["ok"] != true { + t.Errorf("ok = %v, want true", result["ok"]) + } + if result["file_path"] != outputPath { + t.Errorf("file_path = %v, want %q", result["file_path"], outputPath) + } } func TestRunQRCode_ASCIIOutputsToStdout(t *testing.T) {