Skip to content

Commit bddee10

Browse files
riba2534claude
andcommitted
fix: address review findings from v1.10.0 changes
- fix(auth): pass --scopes to Device Flow (was silently ignored) - fix(auth): validate verification_uri in device authorization response - fix(im): add file size pre-check (30MB file / 10MB image) and use 5-minute timeout instead of 30s default for IM uploads - fix(msg): output upload progress to stderr to avoid polluting --output json; wrap os.Stat errors with original error - fix(export): unify file permission to 0600 (was 0644 in doc export) - docs(skills): update feishu-cli-auth, msg, export, perm, toolkit skills to reflect new features and fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 139e2aa commit bddee10

10 files changed

Lines changed: 81 additions & 25 deletions

File tree

cmd/auth_login.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Authorization Code Flow 前置条件:
6262

6363
switch method {
6464
case "device":
65-
return runDeviceFlow(cfg.AppID, cfg.AppSecret, cfg.BaseURL)
65+
return runDeviceFlow(cfg.AppID, cfg.AppSecret, cfg.BaseURL, scopes)
6666
case "code", "":
6767
// Authorization Code Flow,继续往下
6868
default:
@@ -104,8 +104,8 @@ Authorization Code Flow 前置条件:
104104
}
105105

106106
// runDeviceFlow 执行 Device Flow 授权(RFC 8628)
107-
func runDeviceFlow(appID, appSecret, baseURL string) error {
108-
deviceResp, err := auth.RequestDeviceAuthorization(appID, appSecret, baseURL, "")
107+
func runDeviceFlow(appID, appSecret, baseURL, scope string) error {
108+
deviceResp, err := auth.RequestDeviceAuthorization(appID, appSecret, baseURL, scope)
109109
if err != nil {
110110
return err
111111
}

cmd/export_markdown.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ var exportMarkdownCmd = &cobra.Command{
9595

9696
// Output
9797
if output != "" {
98-
if err := os.WriteFile(output, []byte(markdown), 0644); err != nil {
98+
if err := os.WriteFile(output, []byte(markdown), 0600); err != nil {
9999
return fmt.Errorf("写入输出文件失败: %w", err)
100100
}
101101
fmt.Printf("已导出到 %s\n", output)

cmd/send_message.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,20 @@ var sendMessageCmd = &cobra.Command{
117117
// 文件/图片路径预检查
118118
if filePath != "" {
119119
if _, err := os.Stat(filePath); err != nil {
120-
return fmt.Errorf("指定的文件不存在: %s", filePath)
120+
return fmt.Errorf("无法访问文件: %w", err)
121121
}
122122
}
123123
if imagePath != "" {
124124
if _, err := os.Stat(imagePath); err != nil {
125-
return fmt.Errorf("指定的图片文件不存在: %s", imagePath)
125+
return fmt.Errorf("无法访问图片文件: %w", err)
126126
}
127127
}
128128

129129
var msgContent string
130130
switch {
131131
case filePath != "":
132132
// Upload file via IM API, then send as file message
133-
fmt.Printf("正在上传文件: %s\n", filepath.Base(filePath))
133+
fmt.Fprintf(os.Stderr, "正在上传文件: %s\n", filepath.Base(filePath))
134134
fileKey, err := client.UploadIMFile(filePath, "")
135135
if err != nil {
136136
return err
@@ -141,7 +141,7 @@ var sendMessageCmd = &cobra.Command{
141141

142142
case imagePath != "":
143143
// Upload image via IM API, then send as image message
144-
fmt.Printf("正在上传图片: %s\n", filepath.Base(imagePath))
144+
fmt.Fprintf(os.Stderr, "正在上传图片: %s\n", filepath.Base(imagePath))
145145
imageKey, err := client.UploadIMImage(imagePath)
146146
if err != nil {
147147
return err

internal/auth/device_flow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func RequestDeviceAuthorization(appID, appSecret, baseURL, scope string) (*Devic
125125
expiresIn := int(deviceFlowToFloat64(raw["expires_in"], 240))
126126
interval := int(deviceFlowToFloat64(raw["interval"], 5))
127127

128-
if deviceCode == "" || userCode == "" {
128+
if deviceCode == "" || userCode == "" || verificationURI == "" {
129129
return nil, fmt.Errorf("设备授权响应缺少必要字段,响应: %s", deviceFlowTruncate(string(respBody), 300))
130130
}
131131

internal/client/im_upload.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
1010
)
1111

12+
const (
13+
maxIMFileSize = 30 << 20 // 30 MB,IM 文件上传限制
14+
maxIMImageSize = 10 << 20 // 10 MB,IM 图片上传限制
15+
)
16+
1217
// fileExtToIMType maps common file extensions to Feishu IM file_type values.
1318
// Falls back to "stream" for unknown extensions.
1419
func fileExtToIMType(filename string) string {
@@ -45,6 +50,14 @@ func UploadIMFile(filePath string, fileName string) (string, error) {
4550
}
4651
defer f.Close()
4752

53+
stat, err := f.Stat()
54+
if err != nil {
55+
return "", fmt.Errorf("获取文件信息失败: %w", err)
56+
}
57+
if stat.Size() > maxIMFileSize {
58+
return "", fmt.Errorf("文件大小 %s 超过 IM 上传限制(30MB),请使用 file upload 命令上传到云空间", formatSize(int(stat.Size())))
59+
}
60+
4861
if fileName == "" {
4962
fileName = filepath.Base(filePath)
5063
}
@@ -59,7 +72,7 @@ func UploadIMFile(filePath string, fileName string) (string, error) {
5972
Build()).
6073
Build()
6174

62-
resp, err := client.Im.File.Create(Context(), req)
75+
resp, err := client.Im.File.Create(ContextWithTimeout(downloadTimeout), req)
6376
if err != nil {
6477
return "", fmt.Errorf("上传文件失败: %w", err)
6578
}
@@ -89,14 +102,22 @@ func UploadIMImage(filePath string) (string, error) {
89102
}
90103
defer f.Close()
91104

105+
stat, err := f.Stat()
106+
if err != nil {
107+
return "", fmt.Errorf("获取图片信息失败: %w", err)
108+
}
109+
if stat.Size() > maxIMImageSize {
110+
return "", fmt.Errorf("图片大小 %s 超过 IM 上传限制(10MB)", formatSize(int(stat.Size())))
111+
}
112+
92113
req := larkim.NewCreateImageReqBuilder().
93114
Body(larkim.NewCreateImageReqBodyBuilder().
94115
ImageType("message").
95116
Image(f).
96117
Build()).
97118
Build()
98119

99-
resp, err := client.Im.Image.Create(Context(), req)
120+
resp, err := client.Im.Image.Create(ContextWithTimeout(downloadTimeout), req)
100121
if err != nil {
101122
return "", fmt.Errorf("上传图片失败: %w", err)
102123
}

skills/feishu-cli-auth/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,9 @@ feishu-cli auth login --manual --scopes "offline_access search:docs:read search:
282282

283283
# Device Flow:无需在飞书开放平台配置重定向 URL 白名单
284284
feishu-cli auth login --method device
285+
286+
# Device Flow + 指定 scope
287+
feishu-cli auth login --method device --scopes "offline_access search:docs:read"
285288
```
286289

287290
### Authorization Code Flow 前置条件
@@ -303,7 +306,7 @@ Device Flow(`--method device`)无需此配置。
303306
2. 终端显示用户码和验证链接,在浏览器中打开链接并输入用户码完成授权
304307
3. 命令自动轮询等待授权完成,成功后保存 Token
305308

306-
**注意**Device Flow 不支持指定 scope,服务端按应用已开通的权限授予。如需特定 scope,请改用标准 `auth login` 并配置重定向 URL 白名单
309+
Device Flow 支持 `--scopes` 参数指定 OAuth scope(会自动追加 `offline_access`)。未指定时默认请求 `offline_access`
307310

308311
---
309312

skills/feishu-cli-export/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ feishu-cli doc export-file <doc_token> --type pdf -o output.pdf
294294
| `<doc_token>` | 文档 Token | 必填 |
295295
| `--type` | 导出格式 | 必填 |
296296
| `-o, --output` | 输出文件路径 | 必填 |
297+
| `--user-access-token` | User Access Token(用于导出无 App 权限的文档) | 自动读取 |
297298

298299
### 示例
299300

@@ -303,6 +304,9 @@ feishu-cli doc export-file JKbxdRez1oNWEKxPz14cWMpBnKh --type pdf -o /tmp/report
303304

304305
# 导出为 Word
305306
feishu-cli doc export-file JKbxdRez1oNWEKxPz14cWMpBnKh --type docx -o /tmp/report.docx
307+
308+
# 使用 User Token 导出(无 App 权限的文档)
309+
feishu-cli doc export-file JKbxdRez1oNWEKxPz14cWMpBnKh --type pdf -o /tmp/report.pdf --user-access-token u-xxx
306310
```
307311

308312
---

skills/feishu-cli-msg/SKILL.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ allowed-tools: Bash, Read, Write
2828
| 输入方式 | 参数 | 适用场景 |
2929
|---------|------|---------|
3030
| 快捷文本 | `--text "内容"` | 纯文本消息,最简单 |
31+
| 发送文件 | `--file <路径>``-f` | 本地文件自动上传并发送(限 30MB) |
32+
| 发送图片 | `--image <路径>` | 本地图片自动上传并发送(限 10MB) |
3133
| 内联 JSON | `--content '{"key":"val"}'``-c` | 简单 JSON,一行搞定 |
3234
| JSON 文件 | `--content-file file.json` | 复杂消息(卡片、富文本等) |
3335

34-
**优先级**`--text` > `--content` > `--content-file`(同时指定时前者优先)
36+
**互斥**以上 5 种输入方式**只能指定一个**,同时指定会报错。
3537

3638
### 接收者类型
3739

@@ -93,9 +95,33 @@ feishu-cli msg send \
9395
--receive-id-type <type> \
9496
--receive-id <id> \
9597
[--msg-type <msg_type>] \
96-
[--text "<text>" | --content '<json>' | --content-file <file.json>]
98+
[--text "<text>" | --file <path> | --image <path> | --content '<json>' | --content-file <file.json>]
9799
```
98100

101+
### file 类型(直发文件)
102+
103+
```bash
104+
# 直接发送本地文件(自动上传,限 30MB)
105+
feishu-cli msg send \
106+
--receive-id-type email \
107+
--receive-id user@example.com \
108+
--file /path/to/report.pdf
109+
```
110+
111+
自动推断文件 MIME 类型(opus/mp4/pdf/doc/xls/ppt),未知类型使用 `stream`。超过 30MB 的文件请先用 `file upload` 上传到云空间,再用 `--msg-type file --content '{"file_key":"..."}'` 发送。
112+
113+
### image 类型(直发图片)
114+
115+
```bash
116+
# 直接发送本地图片(自动上传,限 10MB)
117+
feishu-cli msg send \
118+
--receive-id-type chat_id \
119+
--receive-id oc_xxx \
120+
--image /path/to/screenshot.png
121+
```
122+
123+
支持 JPEG、PNG、BMP、GIF、TIFF、WebP 格式。
124+
99125
### text 类型
100126

101127
```bash

skills/feishu-cli-perm/SKILL.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,18 @@ feishu-cli perm password delete <TOKEN> [--doc-type <DOC_TYPE>]
196196

197197
### member-type(协作者 ID 类型)
198198

199-
|| 说明 | 使用场景 | 示例 |
200-
|----|------|----------|------|
201-
| `email` | 邮箱 | **最常用**,精确到个人 | user@example.com |
202-
| `openid` | Open ID | 通过开放平台获取的用户 ID | ou_xxx |
203-
| `userid` | User ID | 企业内部用户 ID | 123456 |
204-
| `unionid` | Union ID | 跨应用统一 ID | on_xxx |
205-
| `openchat` | 群聊 ID | **按群聊授权**,群内所有成员获得权限 | oc_xxx |
206-
| `opendepartmentid` | 部门 ID | **按部门授权**,部门内所有成员获得权限 | od_xxx |
207-
| `groupid` | 群组 ID | 用户组 | gc_xxx |
208-
| `wikispaceid` | 知识空间 ID | 知识库空间 | ws_xxx |
199+
|| 别名(IM 风格) | 说明 | 使用场景 | 示例 |
200+
|----|-----------------|------|----------|------|
201+
| `email` || 邮箱 | **最常用**,精确到个人 | user@example.com |
202+
| `openid` | `open_id` | Open ID | 通过开放平台获取的用户 ID | ou_xxx |
203+
| `userid` | `user_id` | User ID | 企业内部用户 ID | 123456 |
204+
| `unionid` | `union_id` | Union ID | 跨应用统一 ID | on_xxx |
205+
| `openchat` | `chat_id` | 群聊 ID | **按群聊授权**,群内所有成员获得权限 | oc_xxx |
206+
| `opendepartmentid` || 部门 ID | **按部门授权**,部门内所有成员获得权限 | od_xxx |
207+
| `groupid` || 群组 ID | 用户组 | gc_xxx |
208+
| `wikispaceid` || 知识空间 ID | 知识库空间 | ws_xxx |
209+
210+
> IM API 风格别名(`open_id``user_id``union_id``chat_id`)会自动映射为标准值,两种写法等效。
209211
210212
### doc-type(云文档类型)
211213

skills/feishu-cli-toolkit/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ feishu-cli file move <file_token> --target <folder_token> --type <docx|sheet|fil
294294
feishu-cli file copy <file_token> --target <folder_token> --type <type> [--name "新名"]
295295
feishu-cli file delete <file_token> --type <type> # 移到回收站,30 天可恢复
296296

297-
# 下载/上传
297+
# 下载/上传(≤20MB 单步上传,>20MB 自动分片上传)
298298
feishu-cli file download <file_token> -o output.pdf
299299
feishu-cli file upload local_file.pdf --parent <FOLDER_TOKEN> [--name "自定义名"]
300300

0 commit comments

Comments
 (0)