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
7 changes: 7 additions & 0 deletions cmd/bitable_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"

"github.com/riba2534/feishu-cli/internal/client"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -32,6 +33,12 @@ var bitableCreateCmd = &cobra.Command{
if app.URL != "" {
fmt.Printf(" URL: %s\n", app.URL)
}

// Auto-grant owner permission
if err := grantOwnerPermission(app.AppToken, "bitable"); err != nil {
fmt.Fprintf(os.Stderr, "警告: %v\n", err)
}

return nil
},
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/config_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ var configGetCmd = &cobra.Command{
app_secret 应用密钥(出于安全仅显示前 4 位)
base_url API 地址
owner_email 文档所有者邮箱
owner_open_id 文档所有者 Open ID(优先于 owner_email)
transfer_ownership 创建文档后是否转移所有权
debug 调试模式

示例:
feishu-cli config get owner_email
feishu-cli config get owner_open_id
feishu-cli config get transfer_ownership`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
6 changes: 6 additions & 0 deletions cmd/copy_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"

"github.com/riba2534/feishu-cli/internal/client"
"github.com/riba2534/feishu-cli/internal/config"
Expand Down Expand Up @@ -66,6 +67,11 @@ var copyFileCmd = &cobra.Command{
}
}

// Auto-grant owner permission
if err := grantOwnerPermission(newToken, fileType); err != nil {
fmt.Fprintf(os.Stderr, "警告: %v\n", err)
}

return nil
},
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/create_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"

"github.com/riba2534/feishu-cli/internal/client"
"github.com/riba2534/feishu-cli/internal/config"
Expand Down Expand Up @@ -70,6 +71,11 @@ var createDocumentCmd = &cobra.Command{
fmt.Printf(" 链接: https://feishu.cn/docx/%s\n", documentID)
}

// Auto-grant owner permission
if err := grantOwnerPermission(documentID, "docx"); err != nil {
fmt.Fprintf(os.Stderr, "警告: %v\n", err)
}

return nil
},
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/create_folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"

"github.com/riba2534/feishu-cli/internal/client"
"github.com/riba2534/feishu-cli/internal/config"
Expand Down Expand Up @@ -57,6 +58,11 @@ var createFolderCmd = &cobra.Command{
}
}

// Auto-grant owner permission
if err := grantOwnerPermission(token, "folder"); err != nil {
fmt.Fprintf(os.Stderr, "警告: %v\n", err)
}

return nil
},
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/import_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -94,6 +95,11 @@ var importFileCmd = &cobra.Command{
fmt.Printf(" 链接: %s\n", url)
}

// Auto-grant owner permission
if err := grantOwnerPermission(docToken, targetType); err != nil {
fmt.Fprintf(os.Stderr, "警告: %v\n", err)
}

return nil
},
}
Expand Down
41 changes: 41 additions & 0 deletions cmd/owner_grant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"fmt"

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

// grantOwnerPermission automatically grants permission to the configured owner
// after document creation. It reads owner_open_id (priority) or owner_email from
// config, adds full_access permission, and optionally transfers ownership.
// If no owner is configured, it silently returns nil.
func grantOwnerPermission(docToken, docType string) error {
cfg := config.Get()
memberType, memberID := cfg.GetOwner()
if memberType == "" {
return nil
}

// Add full_access permission
member := client.PermissionMember{
MemberType: memberType,
MemberID: memberID,
Perm: "full_access",
}
if err := client.AddPermission(docToken, docType, member, true); err != nil {
return fmt.Errorf("自动授权失败: %w", err)
}
fmt.Printf(" 已授权 %s(%s) full_access 权限\n", memberID, memberType)

// Transfer ownership if configured
if cfg.TransferOwnership {
if err := client.TransferOwnership(docToken, docType, memberType, memberID, true, false, false, "full_access"); err != nil {
return fmt.Errorf("转移所有权失败: %w", err)
}
fmt.Printf(" 已转移所有权给 %s(%s)\n", memberID, memberType)
}

return nil
}
20 changes: 19 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Config struct {
UserAccessToken string `mapstructure:"user_access_token"`
BaseURL string `mapstructure:"base_url"`
OwnerEmail string `mapstructure:"owner_email"`
OwnerOpenID string `mapstructure:"owner_open_id"`
TransferOwnership bool `mapstructure:"transfer_ownership"`
Debug bool `mapstructure:"debug"`
Export ExportConfig `mapstructure:"export"`
Expand Down Expand Up @@ -56,6 +57,7 @@ func Init(cfgFile string) error {
// 2. 设置默认值
viper.SetDefault("base_url", "https://open.feishu.cn")
viper.SetDefault("owner_email", "")
viper.SetDefault("owner_open_id", "")
viper.SetDefault("transfer_ownership", false)
viper.SetDefault("debug", false)
viper.SetDefault("export.download_images", false)
Expand All @@ -72,6 +74,7 @@ func Init(cfgFile string) error {
_ = viper.BindEnv("user_access_token", "FEISHU_USER_ACCESS_TOKEN")
_ = viper.BindEnv("base_url", "FEISHU_BASE_URL")
_ = viper.BindEnv("owner_email", "FEISHU_OWNER_EMAIL")
_ = viper.BindEnv("owner_open_id", "FEISHU_OWNER_OPEN_ID")
_ = viper.BindEnv("transfer_ownership", "FEISHU_TRANSFER_OWNERSHIP")
_ = viper.BindEnv("debug", "FEISHU_DEBUG")

Expand All @@ -96,6 +99,7 @@ func Get() *Config {
return &Config{
BaseURL: "https://open.feishu.cn",
OwnerEmail: "",
OwnerOpenID: "",
TransferOwnership: false,
Export: ExportConfig{
AssetsDir: "./assets",
Expand All @@ -108,6 +112,19 @@ func Get() *Config {
return cfg
}

// GetOwner returns the owner's member type and ID from config.
// Priority: owner_open_id > owner_email.
// Returns empty strings if neither is configured.
func (c *Config) GetOwner() (memberType, memberID string) {
if c.OwnerOpenID != "" {
return "openid", c.OwnerOpenID
}
if c.OwnerEmail != "" {
return "email", c.OwnerEmail
}
return "", ""
}

// Validate validates the configuration
func Validate() error {
if cfg == nil {
Expand Down Expand Up @@ -153,7 +170,8 @@ app_id: ""
app_secret: ""
base_url: "https://open.feishu.cn"
owner_email: "" # 文档创建后自动授权的邮箱(环境变量: FEISHU_OWNER_EMAIL)
transfer_ownership: false # 创建文档后是否转移所有权给 owner_email(默认仅添加 full_access)
owner_open_id: "" # 文档创建后自动授权的 Open ID(环境变量: FEISHU_OWNER_OPEN_ID,优先于 owner_email)
transfer_ownership: false # 创建文档后是否转移所有权给 owner(默认仅添加 full_access)
debug: false

# 导出配置
Expand Down
90 changes: 90 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ func TestInit_ConfigDefaultsExposedToViper(t *testing.T) {
}

os.Unsetenv("FEISHU_OWNER_EMAIL")
os.Unsetenv("FEISHU_OWNER_OPEN_ID")
os.Unsetenv("FEISHU_TRANSFER_OWNERSHIP")

if err := Init(configFile); err != nil {
Expand All @@ -408,6 +409,9 @@ func TestInit_ConfigDefaultsExposedToViper(t *testing.T) {
if !viper.IsSet("owner_email") {
t.Fatal("owner_email 应被识别为已设置默认值")
}
if !viper.IsSet("owner_open_id") {
t.Fatal("owner_open_id 应被识别为已设置默认值")
}
if !viper.IsSet("transfer_ownership") {
t.Fatal("transfer_ownership 应被识别为已设置默认值")
}
Expand All @@ -416,7 +420,93 @@ func TestInit_ConfigDefaultsExposedToViper(t *testing.T) {
if c.OwnerEmail != "" {
t.Errorf("OwnerEmail = %q, 期望空字符串", c.OwnerEmail)
}
if c.OwnerOpenID != "" {
t.Errorf("OwnerOpenID = %q, 期望空字符串", c.OwnerOpenID)
}
if c.TransferOwnership {
t.Errorf("TransferOwnership = %v, 期望 false", c.TransferOwnership)
}
}

func TestInit_OwnerOpenIDFromEnv(t *testing.T) {
resetConfig()

os.Setenv("FEISHU_OWNER_OPEN_ID", "ou_test123")
defer os.Unsetenv("FEISHU_OWNER_OPEN_ID")

err := Init("")
if err != nil {
t.Fatalf("Init() 返回错误: %v", err)
}

c := Get()
if c.OwnerOpenID != "ou_test123" {
t.Errorf("OwnerOpenID = %q, 期望 %q", c.OwnerOpenID, "ou_test123")
}
}

func TestInit_OwnerOpenIDFromConfigFile(t *testing.T) {
resetConfig()

tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
content := `app_id: "test_id"
app_secret: "test_secret"
owner_open_id: "ou_file123"
`
if err := os.WriteFile(configFile, []byte(content), 0600); err != nil {
t.Fatalf("创建配置文件失败: %v", err)
}

os.Unsetenv("FEISHU_OWNER_OPEN_ID")

if err := Init(configFile); err != nil {
t.Fatalf("Init() 返回错误: %v", err)
}

c := Get()
if c.OwnerOpenID != "ou_file123" {
t.Errorf("OwnerOpenID = %q, 期望 %q", c.OwnerOpenID, "ou_file123")
}
}

func TestGetOwner_OpenIDPriority(t *testing.T) {
c := &Config{
OwnerOpenID: "ou_test",
OwnerEmail: "test@example.com",
}

memberType, memberID := c.GetOwner()
if memberType != "openid" {
t.Errorf("GetOwner() memberType = %q, 期望 %q", memberType, "openid")
}
if memberID != "ou_test" {
t.Errorf("GetOwner() memberID = %q, 期望 %q", memberID, "ou_test")
}
}

func TestGetOwner_EmailFallback(t *testing.T) {
c := &Config{
OwnerEmail: "test@example.com",
}

memberType, memberID := c.GetOwner()
if memberType != "email" {
t.Errorf("GetOwner() memberType = %q, 期望 %q", memberType, "email")
}
if memberID != "test@example.com" {
t.Errorf("GetOwner() memberID = %q, 期望 %q", memberID, "test@example.com")
}
}

func TestGetOwner_NeitherConfigured(t *testing.T) {
c := &Config{}

memberType, memberID := c.GetOwner()
if memberType != "" {
t.Errorf("GetOwner() memberType = %q, 期望空字符串", memberType)
}
if memberID != "" {
t.Errorf("GetOwner() memberID = %q, 期望空字符串", memberID)
}
}