Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 项目概述

`feishu-cli` 是一个功能完整的飞书开放平台命令行工具,**核心功能是 Markdown ↔ 飞书文档双向转换**,支持文档操作、消息发送、权限管理、知识库操作、文件管理、评论管理等功能。
`feishu-cli` 是一个功能完整的飞书开放平台命令行工具,**核心功能是 Markdown ↔ 飞书文档双向转换**,支持文档操作、消息发送、权限管理、审批查询、知识库操作、文件管理、评论管理等功能。

## 技术栈

Expand All @@ -26,6 +26,10 @@ feishu-cli/
│ ├── auth_login.go # OAuth 登录
│ ├── auth_status.go # 查看授权状态
│ ├── auth_logout.go # 退出登录
│ ├── approval.go # 审批命令组
│ ├── approval_get.go # 审批定义详情查询
│ ├── approval_task.go # 审批任务命令组
│ ├── approval_task_query.go # 审批任务查询
│ ├── search_docs.go # 文档搜索
│ ├── wiki.go # 知识库命令组
│ ├── msg.go # 消息命令组
Expand All @@ -39,11 +43,13 @@ feishu-cli/
│ │ ├── oauth.go # OAuth 流程(Login, ExchangeToken, Refresh)
│ │ ├── resolve.go # Token 优先级链(ResolveUserAccessToken)
│ │ ├── token.go # Token 持久化(Load/Save/Delete)
│ │ ├── user_cache.go # 当前登录用户缓存(user_profile.json)
│ │ └── browser.go # 浏览器打开、环境检测
│ ├── client/ # 飞书 API 封装
│ │ ├── client.go # 客户端初始化、Context()
│ │ ├── helpers.go # 工具函数(StringVal/BoolVal/IsRateLimitError 等)
│ │ ├── docx.go # 文档 API(含 FillTableCells)
│ │ ├── approval.go # 审批 API(定义详情、任务查询)
│ │ ├── board.go # 画板 API(Mermaid/PlantUML 导入)
│ │ ├── sheets.go # 电子表格 API
│ │ ├── bitable.go # 多维表格 API(Base)
Expand Down Expand Up @@ -107,7 +113,7 @@ app_secret: "xxx"

### OAuth 认证

通过 OAuth 2.0 Authorization Code Flow 获取 User Access Token,用于搜索等需要用户授权的功能
通过 OAuth 2.0 Authorization Code Flow 获取 User Access Token,用于搜索、审批任务查询等需要用户授权的功能

**流程**:`auth login` → 浏览器授权 → 回调获取 code → 换取 Token → 保存到 `~/.feishu-cli/token.json`

Expand All @@ -119,6 +125,12 @@ app_secret: "xxx"
2. `FEISHU_USER_ACCESS_TOKEN` 环境变量
3. `~/.feishu-cli/token.json`(access_token 有效直接返回;过期则用 refresh_token 自动刷新)
4. `config.yaml` 中的 `user_access_token` 静态配置
- **审批任务查询**:`approval task query` 会先通过 `resolveRequiredUserToken` 获取当前用户 Token,再调用 `/authen/v1/user_info` 推断当前登录用户的 `open_id`;用户资料会缓存到 `~/.feishu-cli/user_profile.json`,登录态变化或 `auth logout` 时自动清理

**审批相关输出模式**:
- 不传 `--output`:输出便于阅读的文本摘要
- `--output json`:输出 CLI 归一化后的 JSON,部分字段会做拍平和字符串化处理
- `--output raw-json`:输出飞书 API 原始响应,便于排查字段差异

**前置条件**:在飞书开放平台 → 应用详情 → 安全设置 → 重定向 URL 中添加 `http://127.0.0.1:9768/callback`

Expand Down Expand Up @@ -190,6 +202,14 @@ feishu-cli auth status # 查看授权状态
feishu-cli auth status -o json # JSON 格式输出授权状态
feishu-cli auth logout # 退出登录

# === 审批 ===
feishu-cli approval get <approval_code> # 查询审批模板/流程定义
feishu-cli approval get <approval_code> --output json
feishu-cli approval get <approval_code> --output raw-json
feishu-cli approval task query --topic todo
feishu-cli approval task query --topic started --output json
feishu-cli approval task query --topic started --output raw-json

# === 文档操作 ===
feishu-cli doc create --title "测试"
feishu-cli doc get <doc_id>
Expand Down Expand Up @@ -533,6 +553,8 @@ feishu-cli search docs "产品需求" --user-access-token <token>
| 日历 | `calendar:calendar` | 需单独申请 |
| 任务 | `task:task:read`, `task:task:write` | 需单独申请 |
| 任务列表 | `task:tasklist:read`, `task:tasklist:write` | 任务列表管理 |
| 审批定义查询 | `approval:approval:readonly` | 用于 `approval get` |
| 审批任务查询 | `approval:task` | 用于 `approval task query`,并需完成用户授权 |
| 搜索消息/应用 | 需要 User Access Token | 通过 `auth login` 或手动获取 |
| 搜索文档 | 需要 User Access Token | 通过 `auth login` 或手动获取,支持 `search:read` 权限 |

Expand Down
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ feishu-cli doc import large-doc.md --title "大文档" \
| **群聊** | 创建、获取、更新、删除、分享链接、成员管理 |
| **日历** | 日历列表、主日历、日程增删改查、搜索、回复邀请、参与者管理、忙闲查询 |
| **任务** | 创建、查看、完成、删除、子任务、成员管理、提醒管理、任务列表 |
| **审批** | 审批定义详情查询、当前登录用户审批任务查询(待办 / 已办 / 已发起 / 抄送) |
| **权限** | 添加 / 更新 / 删除协作者、批量添加、公开权限管理、分享密码、权限检查、转移所有权 |
| **文件** | 云空间文件列表、创建、移动、复制、删除、上传、下载、版本管理、元数据、统计 |
| **素材** | 上传 / 下载(图片、文件、音视频) |
Expand Down Expand Up @@ -183,7 +184,7 @@ export FEISHU_APP_SECRET="xxx"
feishu-cli config init
```

4. (可选)如果需要使用搜索功能,还需完成 OAuth 用户授权:
4. (可选)如果需要使用搜索、审批任务查询等需要用户身份的功能,还需完成 OAuth 用户授权:

```bash
# 前置条件:在飞书开放平台 → 应用详情 → 安全设置 → 重定向 URL 中添加:
Expand Down Expand Up @@ -222,6 +223,7 @@ Commands:
dept 部门操作(详情、子部门列表)
board 画板操作(导入图表、下载图片)
comment 评论操作(列出、添加、解决/恢复、回复管理)
approval 审批操作(定义详情、当前登录用户任务查询)
search 搜索操作(消息、应用、文档)
auth 身份认证(OAuth 登录、状态、退出)
config 配置管理
Expand Down Expand Up @@ -461,10 +463,51 @@ feishu-cli search docs "产品需求"

</details>

<details>
<summary>审批操作</summary>

`approval get` 使用应用权限查询审批定义(审批模板/流程定义),支持 `--output raw-json` 查看飞书 API 原始响应,需要开通 `approval:approval:readonly`;`approval task query` 会优先根据当前 `auth login` 登录态自动识别当前用户,需要开通 `approval:task` 并完成用户授权。

输出说明:
- 不传 `--output`:输出便于阅读的文本摘要
- `--output json`:输出 CLI 归一化后的 JSON,部分字段会做拍平和字符串化处理
- `--output raw-json`:输出飞书 API 原始响应,便于排查字段差异

```bash
# 查询审批定义详情(审批模板/流程定义)
feishu-cli approval get <approval_code>

# 输出完整 JSON
feishu-cli approval get <approval_code> --output json

# 输出飞书 API 原始响应
feishu-cli approval get <approval_code> --output raw-json

# 查询当前登录用户的待我审批
feishu-cli approval task query --topic todo

# 查询我已审批的任务
feishu-cli approval task query --topic done

# 查询我发起的审批
feishu-cli approval task query --topic started --output json

# 输出飞书 API 原始响应
feishu-cli approval task query --topic started --output raw-json

# 翻页查询
feishu-cli approval task query --topic todo --page-size 20 --page-token <token>

# 显式指定 User Access Token
feishu-cli approval task query --topic cc-unread --user-access-token <token>
```

</details>

<details>
<summary>身份认证</summary>

通过 OAuth 2.0 获取 User Access Token,用于搜索等需要用户授权的功能
通过 OAuth 2.0 获取 User Access Token,用于搜索、审批待办查询等需要用户授权的功能

**前置条件**:在飞书开放平台 → 应用详情 → 安全设置 → 重定向 URL 中添加 `http://127.0.0.1:9768/callback`

Expand Down Expand Up @@ -498,6 +541,7 @@ feishu-cli auth logout
- Access Token 过期时自动使用 Refresh Token 刷新(Refresh Token 有效期 30 天)
- **重要**:登录时需包含 `offline_access` scope 才会返回 Refresh Token,否则 2 小时后需重新登录
- Token 优先级:`--user-access-token` 参数 > `FEISHU_USER_ACCESS_TOKEN` 环境变量 > `token.json` > `config.yaml`
- 审批任务查询会缓存当前登录用户资料到 `~/.feishu-cli/user_profile.json`,登录态变化或执行 `auth logout` 时会自动清理

</details>

Expand Down Expand Up @@ -648,6 +692,8 @@ npx skills add riba2534/feishu-cli --global --yes --agent claude-code --copy
| 日历 | `calendar:calendar` | 需单独申请 |
| 任务 | `task:task:read`, `task:task:write` | 需单独申请 |
| 任务列表 | `task:tasklist:read`, `task:tasklist:write` | 任务列表管理 |
| 审批定义查询 | `approval:approval:readonly` | 用于 `approval get` |
| 审批任务查询 | `approval:task` | 用于 `approval task query`,并需完成用户授权 |
| 搜索消息/应用 | 需要 User Access Token | 通过 `auth login` 或手动获取 |
| 搜索文档 | 需要 User Access Token | 通过 `auth login` 或手动获取,支持 `search:read` 权限 |
| 读取他人文档(User 身份) | `docx:document:readonly`(User scope) | 通过 `feishu-cli auth login --scopes "docx:document:readonly offline_access"` 授权 |
Expand All @@ -659,6 +705,8 @@ npx skills add riba2534/feishu-cli --global --yes --agent claude-code --copy
{
"scopes": {
"tenant": [
"approval:approval:readonly",
"approval:task",
"board:whiteboard:node:create",
"board:whiteboard:node:read",
"board:whiteboard:node:update",
Expand Down Expand Up @@ -722,6 +770,8 @@ npx skills add riba2534/feishu-cli --global --yes --agent claude-code --copy
{
"scopes": {
"tenant": [
"approval:approval:readonly",
"approval:task",
"board:whiteboard:node:create",
"board:whiteboard:node:delete",
"board:whiteboard:node:read",
Expand Down
24 changes: 24 additions & 0 deletions cmd/approval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmd

import "github.com/spf13/cobra"

var approvalCmd = &cobra.Command{
Use: "approval",
Short: "审批相关命令",
Long: `审批相关命令,用于查看审批定义、实例和任务。

当前已提供:
- 审批定义查询(approval get)
- 审批任务查询(approval task query)

示例:
# 查看审批定义详情
feishu-cli approval get <approval_code>

# 查看当前登录用户的待我审批任务
feishu-cli approval task query --topic todo`,
}

func init() {
rootCmd.AddCommand(approvalCmd)
}
108 changes: 108 additions & 0 deletions cmd/approval_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cmd

import (
"fmt"
"strings"

"github.com/riba2534/feishu-cli/internal/client"
"github.com/riba2534/feishu-cli/internal/config"
"github.com/spf13/cobra"
)

var approvalGetCmd = &cobra.Command{
Use: "get <approval_code>",
Short: "获取审批定义详情",
Long: `获取指定审批定义的详细信息,包括名称、状态、表单结构和审批节点。

参数:
approval_code 审批定义 Code(必填)
--output, -o 输出格式,可选:json、raw-json

示例:
# 获取审批定义详情
feishu-cli approval get <approval_code>

# 获取完整 JSON 输出
feishu-cli approval get <approval_code> --output json

# 获取原始 API 响应
feishu-cli approval get <approval_code> --output raw-json

# 指定语言
feishu-cli approval get <approval_code> --locale zh-CN`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := config.Validate(); err != nil {
return err
}

approvalCode := args[0]
if err := validateApprovalCode(approvalCode); err != nil {
return err
}

locale, _ := cmd.Flags().GetString("locale")
withAdminID, _ := cmd.Flags().GetBool("with-admin-id")
withOption, _ := cmd.Flags().GetBool("with-option")
output, _ := cmd.Flags().GetString("output")

opts := client.GetApprovalOptions{
Locale: locale,
WithAdminID: withAdminID,
WithOption: withOption,
}

if output == "raw-json" {
raw, err := client.GetApprovalDefinitionRaw(approvalCode, opts)
if err != nil {
return err
}
fmt.Println(string(raw))
return nil
}

approval, err := client.GetApprovalDefinition(approvalCode, opts)
if err != nil {
return err
}

if output == "json" {
return printJSON(approval)
}

fmt.Printf("审批定义详情:\n")
fmt.Printf(" Code: %s\n", approval.ApprovalCode)
fmt.Printf(" 名称: %s\n", approval.ApprovalName)
fmt.Printf(" 状态: %s\n", approval.Status)
fmt.Printf(" 节点数: %d\n", len(approval.NodeList))
fmt.Printf(" 可见人数量: %d\n", len(approval.Viewers))
if len(approval.ApprovalAdminIDs) > 0 {
fmt.Printf(" 管理员 ID: %s\n", strings.Join(approval.ApprovalAdminIDs, ", "))
}
if len(approval.NodeList) > 0 {
fmt.Printf(" 节点列表:\n")
for idx, node := range approval.NodeList {
fmt.Printf(" %d. %s [%s]\n", idx+1, node.Name, node.NodeType)
}
}
fmt.Printf(" 使用 --output json 查看归一化结果,或 --output raw-json 查看飞书 API 原始响应\n")

return nil
},
}

func validateApprovalCode(approvalCode string) error {
if !isValidToken(approvalCode) {
return fmt.Errorf("无效的审批定义 code: %s", approvalCode)
}
return nil
}

func init() {
approvalCmd.AddCommand(approvalGetCmd)

approvalGetCmd.Flags().StringP("output", "o", "", "输出格式(json/raw-json)")
approvalGetCmd.Flags().String("locale", "zh-CN", "返回结果语言,如 zh-CN、en-US")
approvalGetCmd.Flags().Bool("with-admin-id", false, "返回有数据权限的审批流程管理员 ID")
approvalGetCmd.Flags().Bool("with-option", false, "返回外部数据源和假勤控件选项")
}
63 changes: 63 additions & 0 deletions cmd/approval_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmd

import (
"strings"
"testing"
)

func TestValidateApprovalCode(t *testing.T) {
tests := []struct {
name string
code string
wantErr bool
}{
{
name: "valid uuid-like code",
code: "7C468A54-8745-2245-9675-08B7C63E7A85",
wantErr: false,
},
{
name: "valid short code",
code: "approval_123",
wantErr: false,
},
{
name: "invalid blank code",
code: "",
wantErr: true,
},
{
name: "invalid code with slash",
code: "approval/123",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateApprovalCode(tt.code)
if (err != nil) != tt.wantErr {
t.Fatalf("validateApprovalCode(%q) error = %v, wantErr %v", tt.code, err, tt.wantErr)
}
})
}
}

func TestApprovalGetOutputFlagSupportsRawJSON(t *testing.T) {
flag := approvalGetCmd.Flags().Lookup("output")
if flag == nil {
t.Fatal("approvalGetCmd should register --output")
}
if got := flag.Usage; got != "输出格式(json/raw-json)" {
t.Fatalf("--output usage = %q, want %q", got, "输出格式(json/raw-json)")
}
}

func TestApprovalGetLongHelpMentionsRawJSONAndAlignedExamples(t *testing.T) {
if !strings.Contains(approvalGetCmd.Long, "--output raw-json") {
t.Fatalf("approvalGetCmd.Long should mention raw-json, got %q", approvalGetCmd.Long)
}
if !strings.Contains(approvalGetCmd.Long, "\n示例:\n") {
t.Fatalf("approvalGetCmd.Long should align 示例 header with other commands, got %q", approvalGetCmd.Long)
}
}
Loading