mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat: add minutes permission application shortcut
Change-Id: Ice3e0113a55d7fc4817de9978539fc03d81b7a9b
This commit is contained in:
75
shortcuts/minutes/minutes_apply_permission.go
Normal file
75
shortcuts/minutes/minutes_apply_permission.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package minutes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// MinutesApplyPermission applies for view or edit permission on a minute.
|
||||
var MinutesApplyPermission = common.Shortcut{
|
||||
Service: "minutes",
|
||||
Command: "+apply-permission",
|
||||
Description: "Apply for view or edit permission on a minute",
|
||||
Risk: "write",
|
||||
Scopes: []string{"minutes:permission:apply"},
|
||||
AuthTypes: []string{"user"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "minute-token", Desc: "minute token", Required: true},
|
||||
{Name: "perm", Desc: "permission to apply for", Required: true, Enum: []string{"view", "edit"}},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
minuteToken := strings.TrimSpace(runtime.Str("minute-token"))
|
||||
if minuteToken == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--minute-token is required").WithParam("--minute-token")
|
||||
}
|
||||
if err := validate.ResourceName(minuteToken, "--minute-token"); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--minute-token")
|
||||
}
|
||||
perm := strings.TrimSpace(runtime.Str("perm"))
|
||||
if perm == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--perm is required").WithParam("--perm")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
minuteToken := strings.TrimSpace(runtime.Str("minute-token"))
|
||||
return common.NewDryRunAPI().
|
||||
POST(minutesApplyPermissionPath(minuteToken)).
|
||||
Body(minutesApplyPermissionBody(runtime))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
minuteToken := strings.TrimSpace(runtime.Str("minute-token"))
|
||||
perm := strings.TrimSpace(runtime.Str("perm"))
|
||||
|
||||
_, err := runtime.CallAPITyped(http.MethodPost, minutesApplyPermissionPath(minuteToken), nil, map[string]interface{}{"perm": perm})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runtime.OutFormat(map[string]interface{}{
|
||||
"minute_token": minuteToken,
|
||||
"perm": perm,
|
||||
}, nil, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func minutesApplyPermissionPath(minuteToken string) string {
|
||||
return fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/permissions/apply", validate.EncodePathSegment(minuteToken))
|
||||
}
|
||||
|
||||
func minutesApplyPermissionBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"perm": strings.TrimSpace(runtime.Str("perm")),
|
||||
}
|
||||
}
|
||||
192
shortcuts/minutes/minutes_apply_permission_test.go
Normal file
192
shortcuts/minutes/minutes_apply_permission_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package minutes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const minutesApplyPermissionTestToken = "obcnexampleminute"
|
||||
|
||||
func TestMinutesApplyPermission_Validate(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "missing minute token",
|
||||
args: []string{"+apply-permission", "--perm", "view", "--as", "user"},
|
||||
wantErr: "required flag(s) \"minute-token\" not set",
|
||||
},
|
||||
{
|
||||
name: "missing perm",
|
||||
args: []string{"+apply-permission", "--minute-token", minutesApplyPermissionTestToken, "--as", "user"},
|
||||
wantErr: "required flag(s) \"perm\" not set",
|
||||
},
|
||||
{
|
||||
name: "invalid perm",
|
||||
args: []string{"+apply-permission", "--minute-token", minutesApplyPermissionTestToken, "--perm", "full_access", "--as", "user"},
|
||||
wantErr: "allowed: view, edit",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parent := &cobra.Command{Use: "minutes"}
|
||||
MinutesApplyPermission.Mount(parent, f)
|
||||
parent.SetArgs(tt.args)
|
||||
parent.SilenceErrors = true
|
||||
parent.SilenceUsage = true
|
||||
err := parent.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Errorf("error should contain %q, got: %s", tt.wantErr, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_ValidateTypedMinuteToken(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
parent := &cobra.Command{Use: "minutes"}
|
||||
MinutesApplyPermission.Mount(parent, f)
|
||||
parent.SetArgs([]string{"+apply-permission", "--minute-token", "..", "--perm", "view", "--as", "user"})
|
||||
parent.SilenceErrors = true
|
||||
parent.SilenceUsage = true
|
||||
err := parent.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
var ve *errs.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("want *errs.ValidationError, got %T", err)
|
||||
}
|
||||
if ve.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Errorf("subtype=%q", ve.Subtype)
|
||||
}
|
||||
if ve.Param != "--minute-token" {
|
||||
t.Errorf("param=%q", ve.Param)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_ValidateTypedPerm(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
parent := &cobra.Command{Use: "minutes"}
|
||||
MinutesApplyPermission.Mount(parent, f)
|
||||
parent.SetArgs([]string{"+apply-permission", "--minute-token", minutesApplyPermissionTestToken, "--perm", "full_access", "--as", "user"})
|
||||
parent.SilenceErrors = true
|
||||
parent.SilenceUsage = true
|
||||
err := parent.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
var ve *errs.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("want *errs.ValidationError, got %T", err)
|
||||
}
|
||||
if ve.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Errorf("subtype=%q", ve.Subtype)
|
||||
}
|
||||
if ve.Param != "--perm" {
|
||||
t.Errorf("param=%q", ve.Param)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_DryRun(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
warmTokenCache(t)
|
||||
|
||||
err := mountAndRun(t, MinutesApplyPermission, []string{
|
||||
"+apply-permission",
|
||||
"--minute-token", minutesApplyPermissionTestToken,
|
||||
"--perm", "view",
|
||||
"--dry-run", "--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "POST") {
|
||||
t.Errorf("expected POST method, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "/open-apis/minutes/v1/minutes/"+minutesApplyPermissionTestToken+"/permissions/apply") {
|
||||
t.Errorf("expected apply-permission endpoint, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, `"perm": "view"`) && !strings.Contains(out, `"perm":"view"`) {
|
||||
t.Errorf("expected perm body, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_Execute(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
warmTokenCache(t)
|
||||
|
||||
stub := &httpmock.Stub{
|
||||
Method: http.MethodPost,
|
||||
URL: "/open-apis/minutes/v1/minutes/" + minutesApplyPermissionTestToken + "/permissions/apply",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := mountAndRun(t, MinutesApplyPermission, []string{
|
||||
"+apply-permission",
|
||||
"--minute-token", minutesApplyPermissionTestToken,
|
||||
"--perm", "edit",
|
||||
"--format", "json", "--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var requestBody struct {
|
||||
Perm string `json:"perm"`
|
||||
}
|
||||
if err := json.Unmarshal(stub.CapturedBody, &requestBody); err != nil {
|
||||
t.Fatalf("unmarshal request body: %v", err)
|
||||
}
|
||||
if requestBody.Perm != "edit" {
|
||||
t.Errorf("request perm = %q, want edit", requestBody.Perm)
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
MinuteToken string `json:"minute_token"`
|
||||
Perm string `json:"perm"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("unmarshal stdout: %v", err)
|
||||
}
|
||||
if envelope.Data.MinuteToken != minutesApplyPermissionTestToken {
|
||||
t.Errorf("data.minute_token = %q, want %q", envelope.Data.MinuteToken, minutesApplyPermissionTestToken)
|
||||
}
|
||||
if envelope.Data.Perm != "edit" {
|
||||
t.Errorf("data.perm = %q, want edit", envelope.Data.Perm)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ func Shortcuts() []common.Shortcut {
|
||||
MinutesDownload,
|
||||
MinutesUpload,
|
||||
MinutesUpdate,
|
||||
MinutesApplyPermission,
|
||||
MinutesSummary,
|
||||
MinutesTodo,
|
||||
MinutesSpeakerReplace,
|
||||
|
||||
@@ -31,6 +31,7 @@ metadata:
|
||||
| [`+download`](references/lark-minutes-download.md) | 下载妙记音视频媒体文件 |
|
||||
| [`+upload`](references/lark-minutes-upload.md) | 上传 file_token 生成妙记 |
|
||||
| [`+update`](references/lark-minutes-update.md) | 更新妙记标题 |
|
||||
| `+apply-permission` | 申请妙记查看或编辑权限 |
|
||||
| [`+speaker-replace`](references/lark-minutes-speaker-replace.md) | 替换妙记逐字稿中的说话人(须先 `lark-cli api GET .../speakerlist` 取 `speaker_id`) |
|
||||
| `+word-replace` | 批量替换逐字稿关键词(详见 `lark-cli minutes +word-replace --help`) |
|
||||
| [`+summary`](references/lark-minutes-summary.md) | 替换妙记 AI 总结全文 |
|
||||
@@ -52,6 +53,7 @@ metadata:
|
||||
| 在妙记里增加 / 更改 / 删除 AI 待办 | `+todo`(**禁止走 lark-task**) |
|
||||
| 替换妙记的AI 总结 | `+summary` |
|
||||
| 重命名妙记/改妙记标题 | `+update` |
|
||||
| 申请妙记权限(查看/编辑) | `+apply-permission --perm view\|edit` |
|
||||
| 替换说话人/把 A 的发言改成 B/重新归属发言人/把外部(非飞书)说话人改成飞书用户" | 先 `lark-cli api GET .../transcript/speakerlist` 取 `speaker_id`,再 [`minutes +speaker-replace`](references/lark-minutes-speaker-replace.md);`--from-speaker-id` 只传 id,不传展示名 |
|
||||
| 批量替换逐字稿关键词 | `+word-replace` |
|
||||
| 用户同时提到"会议/开会"和"妙记" | 先 [lark-vc](../lark-vc/SKILL.md)(`+search` → `+recording`)获取 `minute_token`,再本 skill |
|
||||
@@ -75,8 +77,19 @@ metadata:
|
||||
2. 如果是会议 / 日程上下文中的妙记基础信息,先通过 VC/Calendar 链路拿到 `minute_token`,再调用 `minutes minutes get`。
|
||||
3. 用户意图不明确时,默认先给基础元信息,帮助确认是否命中目标妙记。
|
||||
|
||||
### 3. 申请妙记权限
|
||||
|
||||
### 3. 上传音视频文件生成妙记(并可继续获取纪要 / 逐字稿)
|
||||
只有当用户明确要求"申请查看权限"、"申请编辑权限"、"帮我申请这条妙记权限"时,才调用:
|
||||
|
||||
```bash
|
||||
lark-cli minutes +apply-permission --minute-token <token> --perm view|edit
|
||||
```
|
||||
|
||||
这是向妙记所有者发起权限申请,不代表立即获得权限。
|
||||
|
||||
**安全约束**:遇到无权限错误时,不要自动调用 `+apply-permission`;先把无权限事实告知用户,只有用户明确要求申请权限时才发起申请。
|
||||
|
||||
### 4. 上传音视频文件生成妙记(并可继续获取纪要 / 逐字稿)
|
||||
|
||||
1. 当用户说"把音视频文件转成纪要""把录音转成逐字稿/文字稿/撰写文字""把 mp4/mp3 转成总结/待办/章节"时,也先走这个入口。
|
||||
2. **处理流程**:
|
||||
|
||||
58
tests/cli_e2e/minutes/minutes_apply_permission_test.go
Normal file
58
tests/cli_e2e/minutes/minutes_apply_permission_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package minutes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMinutesApplyPermission_DryRun(t *testing.T) {
|
||||
setDryRunConfigEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"minutes", "+apply-permission",
|
||||
"--minute-token", "obcnexampleminute",
|
||||
"--perm", "view",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
output := result.Stdout
|
||||
assert.True(t, strings.Contains(output, "POST"), "dry-run should contain POST method, got: %s", output)
|
||||
assert.True(t, strings.Contains(output, "/open-apis/minutes/v1/minutes/obcnexampleminute/permissions/apply"), "dry-run should contain API path, got: %s", output)
|
||||
assert.True(t, strings.Contains(output, `"perm": "view"`) || strings.Contains(output, `"perm":"view"`), "dry-run should contain perm body, got: %s", output)
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_InvalidPerm(t *testing.T) {
|
||||
setDryRunConfigEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"minutes", "+apply-permission",
|
||||
"--minute-token", "obcnexampleminute",
|
||||
"--perm", "full_access",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 2)
|
||||
assert.True(t, strings.Contains(result.Stderr, "--perm"), "stderr should name --perm, got: %s", result.Stderr)
|
||||
assert.True(t, strings.Contains(result.Stderr, "view") && strings.Contains(result.Stderr, "edit"), "stderr should list allowed values, got: %s", result.Stderr)
|
||||
}
|
||||
Reference in New Issue
Block a user