diff --git a/cmd/bitable_create.go b/cmd/bitable_create.go index fb21a82..fe41d40 100644 --- a/cmd/bitable_create.go +++ b/cmd/bitable_create.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/riba2534/feishu-cli/internal/client" "github.com/spf13/cobra" @@ -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 }, } diff --git a/cmd/config_get.go b/cmd/config_get.go index 7d8e0fe..4c5f112 100644 --- a/cmd/config_get.go +++ b/cmd/config_get.go @@ -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 { diff --git a/cmd/copy_file.go b/cmd/copy_file.go index 965124f..132a067 100644 --- a/cmd/copy_file.go +++ b/cmd/copy_file.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/riba2534/feishu-cli/internal/client" "github.com/riba2534/feishu-cli/internal/config" @@ -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 }, } diff --git a/cmd/create_document.go b/cmd/create_document.go index 0a776a1..e47b30f 100644 --- a/cmd/create_document.go +++ b/cmd/create_document.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/riba2534/feishu-cli/internal/client" "github.com/riba2534/feishu-cli/internal/config" @@ -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 }, } diff --git a/cmd/create_folder.go b/cmd/create_folder.go index 280013e..06db911 100644 --- a/cmd/create_folder.go +++ b/cmd/create_folder.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/riba2534/feishu-cli/internal/client" "github.com/riba2534/feishu-cli/internal/config" @@ -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 }, } diff --git a/cmd/import_file.go b/cmd/import_file.go index 877077c..0b30b43 100644 --- a/cmd/import_file.go +++ b/cmd/import_file.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "path/filepath" "strings" @@ -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 }, } diff --git a/cmd/owner_grant.go b/cmd/owner_grant.go new file mode 100644 index 0000000..314ef78 --- /dev/null +++ b/cmd/owner_grant.go @@ -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 +} diff --git a/internal/config/config.go b/internal/config/config.go index 5c0a34b..5e20ff6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` @@ -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) @@ -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") @@ -96,6 +99,7 @@ func Get() *Config { return &Config{ BaseURL: "https://open.feishu.cn", OwnerEmail: "", + OwnerOpenID: "", TransferOwnership: false, Export: ExportConfig{ AssetsDir: "./assets", @@ -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 { @@ -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 # 导出配置 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1e20360..c4f0934 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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 { @@ -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 应被识别为已设置默认值") } @@ -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) + } +}