Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
View Source
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"}, 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 }, }
MinutesApplyPermission applies for view or edit permission on a minute.
View Source
var MinutesDetail = common.Shortcut{ Service: "minutes", Command: "+detail", Description: "Query minute details with selective artifact flags (summary, todo, chapter, transcript, keyword)", Risk: "read", Scopes: []string{"minutes:minutes.basic:read", "minutes:minutes.artifacts:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-tokens", Desc: "minute tokens, comma-separated for batch", Required: true}, {Name: "summary", Type: "bool", Desc: "include summary"}, {Name: "todo", Type: "bool", Desc: "include todos"}, {Name: "chapter", Type: "bool", Desc: "include chapters"}, {Name: "transcript", Type: "bool", Desc: "include transcript (saved to file)"}, {Name: "keyword", Type: "bool", Desc: "include keywords"}, {Name: "output-dir", Desc: "output directory for transcript files (default: ./minutes/{minute_token}/)"}, {Name: "overwrite", Type: "bool", Desc: "overwrite existing transcript files"}, {Name: "wait-ready", Type: "bool", Desc: "wait until minute metadata/artifacts are ready", Hidden: true}, {Name: "wait-timeout-seconds", Type: "int", Default: "300", Desc: "maximum seconds to wait for readiness", Hidden: true}, {Name: "wait-interval-seconds", Type: "int", Default: "15", Desc: "seconds between readiness checks", Hidden: true}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { tokens := common.SplitCSV(runtime.Str("minute-tokens")) const maxBatchSize = 50 if len(tokens) > maxBatchSize { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--minute-tokens: too many tokens (%d), maximum is %d", len(tokens), maxBatchSize).WithParam("--minute-tokens") } for _, token := range tokens { if !validMinuteTokenDetail.MatchString(token) { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid minute token %q: must contain only lowercase alphanumeric characters", token).WithParam("--minute-tokens") } } if outDir := runtime.Str("output-dir"); outDir != "" { if err := common.ValidateSafePathTyped(runtime.FileIO(), outDir); err != nil { return err } } result, err := runtime.Factory.Credential.ResolveToken(ctx, credential.NewTokenSpec(runtime.As(), runtime.Config.AppID)) if err == nil && result != nil && result.Scopes != "" { if missing := auth.MissingScopes(result.Scopes, scopesDetailMinuteTokens); len(missing) > 0 { return errs.NewPermissionError(errs.SubtypeMissingScope, "missing required scope(s): %s", strings.Join(missing, ", ")). WithHint("run `lark-cli auth login --scope %q` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", strings.Join(missing, " ")). WithMissingScopes(missing...). WithIdentity(string(runtime.As())) } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { tokens := runtime.Str("minute-tokens") d := common.NewDryRunAPI(). GET("/open-apis/minutes/v1/minutes/{minute_token}"). Set("minute_tokens", common.SplitCSV(tokens)) if runtime.Bool("summary") || runtime.Bool("todo") || runtime.Bool("chapter") || runtime.Bool("transcript") || runtime.Bool("keyword") { d.GET("/open-apis/minutes/v1/minutes/{minute_token}/artifacts") } return d }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { errOut := runtime.IO().ErrOut minuteTokens := common.SplitCSV(runtime.Str("minute-tokens")) results := make([]*minuteDetailItem, 0, len(minuteTokens)) const batchDelay = 100 * time.Millisecond fmt.Fprintf(errOut, "%s querying %d minute_token(s)\n", minutesDetailLogPrefix, len(minuteTokens)) for i, token := range minuteTokens { if err := ctx.Err(); err != nil { return err } if i > 0 { time.Sleep(batchDelay) } fmt.Fprintf(errOut, "%s querying minute_token=%s ...\n", minutesDetailLogPrefix, token) results = append(results, fetchMinuteDetail(ctx, runtime, token)) } successCount := 0 for _, r := range results { if r.Error == "" { successCount++ } } fmt.Fprintf(errOut, "%s done: %d total, %d succeeded, %d failed\n", minutesDetailLogPrefix, len(results), successCount, len(results)-successCount) if successCount == 0 && len(results) > 0 { return runtime.OutPartialFailure(map[string]any{"minutes": results}, &output.Meta{Count: len(results)}) } outData := map[string]any{"minutes": results} runtime.OutFormat(outData, &output.Meta{Count: len(results)}, func(w io.Writer) { if len(results) == 0 { fmt.Fprintln(w, "No minutes.") return } var rows []map[string]interface{} for _, r := range results { row := map[string]interface{}{"minute_token": r.MinuteToken} if r.Error != "" { if r.Status == "processing" { row["status"] = "PROCESSING" } else { row["status"] = "FAIL" } row["error"] = r.Error if r.NextCommand != "" { row["next_command"] = r.NextCommand } } else { row["status"] = "OK" row["title"] = r.Title row["note_id"] = r.NoteID if len(r.Artifacts) > 0 { var parts []string if _, ok := r.Artifacts["summary"]; ok { parts = append(parts, "summary") } if _, ok := r.Artifacts["todos"]; ok { parts = append(parts, "todo") } if _, ok := r.Artifacts["chapters"]; ok { parts = append(parts, "chapter") } if _, ok := r.Artifacts["keywords"]; ok { parts = append(parts, "keyword") } if _, ok := r.Artifacts["transcript_file"]; ok { parts = append(parts, "transcript") } if len(parts) > 0 { row["artifacts"] = strings.Join(parts, ", ") } } } rows = append(rows, row) } output.PrintTable(w, rows) fmt.Fprintf(w, "\n%d minute(s), %d succeeded, %d failed\n", len(results), successCount, len(results)-successCount) }) return nil }, }
MinutesDetail queries minute details with selective artifact flags.
View Source
var MinutesDownload = common.Shortcut{ Service: "minutes", Command: "+download", Description: "Download audio/video media file of a minute", Risk: "read", Scopes: []string{"minutes:minutes.media:export"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-tokens", Desc: "minute tokens, comma-separated for batch download (max 50)", Required: true}, {Name: "output", Desc: "output file path (single token)"}, {Name: "output-dir", Desc: "output directory (default: ./minutes/{minute_token}/)"}, {Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"}, {Name: "url-only", Type: "bool", Desc: "only print the download URL(s) without downloading"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { tokens := common.SplitCSV(runtime.Str("minute-tokens")) if len(tokens) == 0 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--minute-tokens is required").WithParam("--minute-tokens") } if len(tokens) > maxBatchSize { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--minute-tokens: too many tokens (%d), maximum is %d", len(tokens), maxBatchSize).WithParam("--minute-tokens") } for _, token := range tokens { if !validMinuteToken.MatchString(token) { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid minute token %q: must contain only lowercase alphanumeric characters (e.g. obcnq3b9jl72l83w4f149w9c)", token).WithParam("--minute-tokens") } } out := runtime.Str("output") outDir := runtime.Str("output-dir") if out != "" && outDir != "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output and --output-dir cannot both be set").WithParam("--output") } if out != "" { if err := common.ValidateSafePathTyped(runtime.FileIO(), out); err != nil { return err } } if outDir != "" { if err := common.ValidateSafePathTyped(runtime.FileIO(), outDir); err != nil { return err } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { tokens := common.SplitCSV(runtime.Str("minute-tokens")) return common.NewDryRunAPI(). GET("/open-apis/minutes/v1/minutes/:minute_token/media"). Set("minute_tokens", tokens) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { tokens := common.SplitCSV(runtime.Str("minute-tokens")) rawOutput := runtime.Str("output") rawOutputDir := runtime.Str("output-dir") overwrite := runtime.Bool("overwrite") urlOnly := runtime.Bool("url-only") errOut := runtime.IO().ErrOut single := len(tokens) == 1 explicitOutputPath := rawOutput explicitOutputDir := rawOutputDir if explicitOutputPath != "" { fi, statErr := runtime.FileIO().Stat(explicitOutputPath) switch { case statErr == nil && fi.IsDir(): explicitOutputDir = explicitOutputPath explicitOutputPath = "" case statErr == nil && !fi.IsDir(): if !single { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output %q is a file; batch mode expects a directory (use --output-dir)", explicitOutputPath).WithParam("--output") } case errors.Is(statErr, fs.ErrNotExist): if !single { explicitOutputDir = explicitOutputPath explicitOutputPath = "" } default: return errs.NewInternalError(errs.SubtypeFileIO, "cannot access --output %q: %s", explicitOutputPath, statErr).WithCause(statErr) } } useDefaultLayout := explicitOutputPath == "" && explicitOutputDir == "" if !single { fmt.Fprintf(errOut, "[minutes +download] batch: %d token(s)\n", len(tokens)) } type result struct { MinuteToken string `json:"minute_token"` ArtifactType string `json:"artifact_type,omitempty"` SavedPath string `json:"saved_path,omitempty"` SizeBytes int64 `json:"size_bytes,omitempty"` DownloadURL string `json:"download_url,omitempty"` Error string `json:"error,omitempty"` err error // raw typed error for single-mode passthrough } results := make([]result, len(tokens)) seen := make(map[string]int) usedNames := make(map[string]bool) baseClient, err := runtime.Factory.HttpClient() if err != nil { return errs.NewNetworkError(errs.SubtypeNetworkTransport, "failed to get HTTP client: %s", err).WithCause(err) } clonedClient := *baseClient clonedClient.Timeout = disableClientTimeout clonedClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { if len(via) >= maxDownloadRedirects { return fmt.Errorf("too many redirects") } if len(via) > 0 { prev := via[len(via)-1] if strings.EqualFold(prev.URL.Scheme, "https") && strings.EqualFold(req.URL.Scheme, "http") { return fmt.Errorf("redirect from https to http is not allowed") } } return validate.ValidateDownloadSourceURL(req.Context(), req.URL.String()) } dlClient := &clonedClient ticker := time.NewTicker(time.Second / 5) defer ticker.Stop() for i, token := range tokens { if i > 0 { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } } if err := validate.ResourceName(token, "--minute-tokens"); err != nil { results[i] = result{MinuteToken: token, Error: err.Error()} continue } if firstIdx, dup := seen[token]; dup { results[i] = result{MinuteToken: token, Error: fmt.Sprintf("duplicate token, same as index %d", firstIdx)} continue } seen[token] = i downloadURL, err := fetchDownloadURL(ctx, runtime, token) if err != nil { results[i] = result{MinuteToken: token, Error: err.Error(), err: err} continue } if urlOnly { results[i] = result{MinuteToken: token, DownloadURL: downloadURL} continue } fmt.Fprintf(errOut, "Downloading media: %s\n", common.MaskToken(token)) opts := downloadOpts{fio: runtime.FileIO(), overwrite: overwrite} switch { case useDefaultLayout: opts.outputDir = common.DefaultMinuteArtifactDir(token) case explicitOutputPath != "" && single: opts.outputPath = explicitOutputPath default: opts.outputDir = explicitOutputDir if !single { opts.usedNames = usedNames } } dl, err := downloadMediaFile(ctx, dlClient, downloadURL, token, opts) if err != nil { results[i] = result{MinuteToken: token, Error: err.Error(), err: err} continue } results[i] = result{ MinuteToken: token, ArtifactType: common.ArtifactTypeRecording, SavedPath: dl.savedPath, SizeBytes: dl.sizeBytes, } } if single { r := results[0] if r.Error != "" { if r.err != nil { return r.err } return runtime.OutPartialFailure(map[string]interface{}{"downloads": results}, &output.Meta{Count: len(results)}) } if urlOnly { runtime.Out(map[string]interface{}{ "minute_token": r.MinuteToken, "download_url": r.DownloadURL, }, nil) } else { runtime.Out(map[string]interface{}{ "minute_token": r.MinuteToken, "artifact_type": r.ArtifactType, "saved_path": r.SavedPath, "size_bytes": r.SizeBytes, }, nil) } return nil } successCount := 0 for _, r := range results { if r.Error == "" { successCount++ } } fmt.Fprintf(errOut, "[minutes +download] done: %d total, %d succeeded, %d failed\n", len(results), successCount, len(results)-successCount) outData := map[string]interface{}{"downloads": results} meta := &output.Meta{Count: len(results)} if successCount == 0 && len(results) > 0 { return runtime.OutPartialFailure(outData, meta) } runtime.OutFormat(outData, meta, nil) return nil }, }
View Source
var MinutesSearch = common.Shortcut{ Service: "minutes", Command: "+search", Description: "Search minutes by keyword, owners, participants, and time range", Risk: "read", Scopes: []string{"minutes:minutes.search:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "query", Desc: "search keyword"}, {Name: "owner-ids", Desc: "owner open_id list, comma-separated (use \"me\" for current user)"}, {Name: "participant-ids", Desc: "participant open_id list, comma-separated (use \"me\" for current user)"}, {Name: "start", Desc: "time lower bound (ISO 8601 or YYYY-MM-DD)"}, {Name: "end", Desc: "time upper bound (ISO 8601 or YYYY-MM-DD)"}, {Name: "page-token", Desc: "page token for next page"}, {Name: "page-size", Default: "15", Desc: "page size, 1-30 (default 15)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, _, err := parseTimeRange(runtime); err != nil { return err } if q := strings.TrimSpace(runtime.Str("query")); q != "" && utf8.RuneCountInString(q) > maxMinutesSearchQueryLen { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--query: length must be between 1 and 50 characters").WithParam("--query") } if _, err := common.ValidatePageSizeTyped(runtime, "page-size", defaultMinutesSearchPageSize, 1, maxMinutesSearchPageSize); err != nil { return err } ownerIDs, err := common.ResolveOpenIDsTyped("--owner-ids", common.SplitCSV(runtime.Str("owner-ids")), runtime) if err != nil { return err } for _, id := range ownerIDs { if _, err := common.ValidateUserIDTyped("--owner-ids", id); err != nil { return err } } participantIDs, err := common.ResolveOpenIDsTyped("--participant-ids", common.SplitCSV(runtime.Str("participant-ids")), runtime) if err != nil { return err } for _, id := range participantIDs { if _, err := common.ValidateUserIDTyped("--participant-ids", id); err != nil { return err } } for _, flag := range []string{"query", "owner-ids", "participant-ids", "start", "end"} { if strings.TrimSpace(runtime.Str(flag)) != "" { return nil } } return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify at least one of --query, --owner-ids, --participant-ids, --start, or --end") }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { startTime, endTime, err := parseTimeRange(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } params := buildMinutesSearchParams(runtime) body, err := buildMinutesSearchBody(runtime, startTime, endTime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } dryRun := common.NewDryRunAPI(). POST("/open-apis/minutes/v1/minutes/search") if len(params) > 0 { dryRun.Params(params) } return dryRun.Body(body) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { startTime, endTime, err := parseTimeRange(runtime) if err != nil { return err } body, err := buildMinutesSearchBody(runtime, startTime, endTime) if err != nil { return err } data, err := runtime.CallAPITyped(http.MethodPost, "/open-apis/minutes/v1/minutes/search", buildMinutesSearchParams(runtime), body) if err != nil { return err } if data == nil { data = map[string]interface{}{} } items := minuteSearchItems(data) stripAvatarFromItems(items) hasMore, _ := data["has_more"].(bool) pageToken, _ := data["page_token"].(string) rows := buildMinuteSearchRows(items) outData := map[string]interface{}{ "items": items, "has_more": data["has_more"], "page_token": data["page_token"], } if notice, _ := data["notice"].(string); notice != "" { outData["notice"] = notice } runtime.OutFormat(outData, &output.Meta{Count: len(rows)}, func(w io.Writer) { if len(rows) == 0 { fmt.Fprintln(w, "No minutes.") return } output.PrintTable(w, rows) }) if hasMore && runtime.Format != "json" && runtime.Format != "" { fmt.Fprintf(runtime.IO().Out, "\n(more available, page_token: %s)\n", pageToken) } return nil }, }
MinutesSearch searches minutes by keyword, owners, participants, and time range.
View Source
var MinutesSpeakerReplace = common.Shortcut{ Service: "minutes", Command: "+speaker-replace", Description: "Replace a speaker in a minute's transcript (rebind from one user to another)", Risk: "write", Scopes: []string{"minutes:minutes:readonly", "minutes:minutes:update"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-token", Desc: "minute token", Required: true}, {Name: "from-speaker-id", Desc: "speaker to replace: opaque speaker_id from transcript speakerlist API (do not pass display names)"}, {Name: "from-user-id", Desc: "deprecated: open_id of the speaker to replace; prefer --from-speaker-id", Hidden: true}, {Name: "to-user-id", Desc: "new speaker, must be an open_id starting with 'ou_'", Required: true}, }, 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") } fromSpeakerID := strings.TrimSpace(runtime.Str("from-speaker-id")) fromUserID := strings.TrimSpace(runtime.Str("from-user-id")) if fromSpeakerID == "" && fromUserID == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--from-speaker-id is required").WithParam("--from-speaker-id") } toUserID := strings.TrimSpace(runtime.Str("to-user-id")) if toUserID == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--to-user-id is required").WithParam("--to-user-id") } if _, err := common.ValidateUserIDTyped("--to-user-id", toUserID); err != nil { return err } if fromSpeakerID == "" { if _, err := common.ValidateUserIDTyped("--from-user-id", fromUserID); err != nil { return err } if fromUserID == toUserID { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--from-user-id and --to-user-id must be different").WithParam("--to-user-id") } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) return common.NewDryRunAPI(). PUT(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/transcript/speaker", validate.EncodePathSegment(minuteToken))). Body(buildSpeakerReplaceRequestBody(runtime)) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) fromSpeakerID := strings.TrimSpace(runtime.Str("from-speaker-id")) fromUserID := strings.TrimSpace(runtime.Str("from-user-id")) toUserID := strings.TrimSpace(runtime.Str("to-user-id")) _, err := runtime.CallAPITyped(http.MethodPut, fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/transcript/speaker", validate.EncodePathSegment(minuteToken)), map[string]interface{}{"user_id_type": "open_id"}, buildSpeakerReplaceRequestBodyResolved(fromSpeakerID, fromUserID, toUserID)) if err != nil { return minutesSpeakerReplaceError(err, minuteToken, speakerReplaceSourceLabel(fromSpeakerID, fromUserID)) } runtime.OutFormat(buildSpeakerReplaceOutputData(minuteToken, fromSpeakerID, fromUserID, toUserID), nil, nil) return nil }, }
MinutesSpeakerReplace replaces a speaker in a minute's transcript.
View Source
var MinutesSummary = common.Shortcut{ Service: "minutes", Command: "+summary", Description: "Replace the AI summary of a minute", Risk: "write", Scopes: []string{"minutes:minutes:update"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-token", Desc: "minute token", Required: true}, {Name: "summary", Desc: "replacement summary text (Markdown subset renders best in Minutes)", Required: true, Input: []string{common.File, common.Stdin}}, }, Tips: []string{ minutesSummaryMarkdownTip, "Use `lark-cli minutes +detail --minute-tokens <token> --summary` to read the current summary before replacing it.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := 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") } summary := strings.TrimSpace(runtime.Str("summary")) if summary == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--summary is required").WithParam("--summary") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). PUT(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/summary", validate.EncodePathSegment(runtime.Str("minute-token")))). Body(map[string]interface{}{"summary": "<summary markdown>"}) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := runtime.Str("minute-token") summary := strings.TrimSpace(runtime.Str("summary")) path := fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/summary", validate.EncodePathSegment(minuteToken)) body := map[string]interface{}{ "summary": summary, } if _, err := runtime.CallAPITyped(http.MethodPut, path, nil, body); err != nil { return err } runtime.OutFormat(map[string]interface{}{ "minute_token": minuteToken, "updated": true, }, nil, nil) return nil }, }
MinutesSummary replaces the AI summary of a minute.
View Source
var MinutesTodo = common.Shortcut{ Service: "minutes", Command: "+todo", Description: "Add, update, or delete todo item(s) on a minute", Risk: "write", Scopes: []string{"minutes:minutes:update"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-token", Desc: "minute token (required)", Required: true}, {Name: "operation", Desc: "operation for a single todo (required unless --todos)", Enum: []string{"add", "update", "delete"}}, {Name: "todo", Desc: "todo plain-text content; required by single add/update", Input: []string{common.File, common.Stdin}}, {Name: "is-done", Type: "bool", Desc: "completion flag; required by single add/update"}, {Name: "todo-id", Desc: "id of an existing todo; required by single update/delete"}, { Name: "todos", Desc: `batch todo_items JSON array; each item has operation add|update|delete (supports @file / @-)`, Input: []string{common.File, common.Stdin}, }, }, Tips: []string{ "Single todo: `--operation add --todo \"...\" --is-done=false`.", "Batch: `--todos '[{\"operation\":\"add\",\"content\":\"...\",\"is_done\":false}, ...]'` or `--todos @todos.json`.", "Batch can mix add, update, and delete in one request; array order is preserved in the API body.", "Update: `--operation update --todo-id <id> --todo \"...\" --is-done`.", "Delete: `--operation delete --todo-id <id>`.", "`content` is plain text only; markdown formatting is not supported.", "Use `lark-cli minutes +detail --minute-tokens <token> --todo` to read current todos before writing.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := 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") } if _, err := resolveMinuteTodoOps(runtime); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { api := common.NewDryRunAPI(). POST(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/todo", validate.EncodePathSegment(runtime.Str("minute-token")))) ops, err := resolveMinuteTodoOps(runtime) if err != nil { return api.Body(map[string]interface{}{ "todo_items": "<todo_items array>", }) } return api.Body(map[string]interface{}{ "todo_items": todoItemsFromOps(ops), }) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := runtime.Str("minute-token") ops, err := resolveMinuteTodoOps(runtime) if err != nil { return err } path := fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/todo", validate.EncodePathSegment(minuteToken)) body := map[string]interface{}{ "todo_items": todoItemsFromOps(ops), } if _, err := runtime.CallAPITyped(http.MethodPost, path, nil, body); err != nil { return minutesTodoError(err, minuteToken) } out := map[string]interface{}{ "minute_token": minuteToken, "count": len(ops), "updated": true, } if len(ops) == 1 { out["operation"] = ops[0].operation } runtime.OutFormat(out, nil, nil) return nil }, }
MinutesTodo adds, updates, or deletes todo item(s) on a minute.
View Source
var MinutesUpdate = common.Shortcut{ Service: "minutes", Command: "+update", Description: "Update a minute's title", Risk: "write", Scopes: []string{"minutes:minutes:update"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-token", Desc: "minute token", Required: true}, {Name: "topic", Desc: "new minute title", Required: true}, }, 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") } if strings.TrimSpace(runtime.Str("topic")) == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--topic is required").WithParam("--topic") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) return common.NewDryRunAPI(). PATCH(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s", validate.EncodePathSegment(minuteToken))). Body(map[string]interface{}{"topic": runtime.Str("topic")}) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) topic := runtime.Str("topic") body := map[string]interface{}{ "topic": topic, } _, err := runtime.CallAPITyped(http.MethodPatch, fmt.Sprintf("/open-apis/minutes/v1/minutes/%s", validate.EncodePathSegment(minuteToken)), nil, body) if err != nil { return minutesUpdateError(err, minuteToken) } outData := map[string]interface{}{ "minute_token": minuteToken, "topic": topic, } runtime.OutFormat(outData, nil, nil) return nil }, }
MinutesUpdate updates the title (topic) of a minute.
View Source
var MinutesUpload = common.Shortcut{ Service: "minutes", Command: "+upload", Description: "Upload a media file token to generate a minute", Risk: "write", Scopes: []string{"minutes:minutes.upload:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "file-token", Desc: "file_token of a supported audio/video file already uploaded to Drive", Required: true}, }, Tips: []string{ "This shortcut only accepts --file-token. Upload the local media file to Drive first with `lark-cli drive +upload`.", minutesUploadSupportedFormatsTip, minutesUploadLimitsTip, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { fileToken := runtime.Str("file-token") if fileToken == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--file-token is required").WithParam("--file-token") } if err := validate.ResourceName(fileToken, "--file-token"); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--file-token") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST("/open-apis/minutes/v1/minutes/upload"). Body(map[string]interface{}{"file_token": runtime.Str("file-token")}) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { fileToken := runtime.Str("file-token") body := map[string]interface{}{ "file_token": fileToken, } data, err := runtime.CallAPITyped("POST", "/open-apis/minutes/v1/minutes/upload", nil, body) if err != nil { return err } minuteURL := common.GetString(data, "minute_url") outData := map[string]interface{}{ "minute_url": minuteURL, } if minuteToken := extractUploadedMinuteToken(minuteURL); minuteToken != "" { outData["minute_token"] = minuteToken } runtime.OutFormat(outData, nil, nil) return nil }, }
MinutesUpload uploads a media file token to generate a minute.
View Source
var MinutesWordReplace = common.Shortcut{ Service: "minutes", Command: "+word-replace", Description: "Batch replace words in a minute's transcript", Risk: "write", Scopes: []string{"minutes:minutes:update"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "minute-token", Desc: "minute token", Required: true}, { Name: "replace-words", Desc: `JSON array of replacements, e.g. [{"source_word":"old","target_word":"new"}]`, Required: true, Input: []string{common.File, common.Stdin}, }, }, 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") } if _, err := parseReplaceWords(runtime.Str("replace-words")); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) replaceWords, _ := parseReplaceWords(runtime.Str("replace-words")) return common.NewDryRunAPI(). PUT(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/transcript/word", validate.EncodePathSegment(minuteToken))). Body(map[string]interface{}{ "minute_token": minuteToken, "replace_words": replaceWords, }) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { minuteToken := strings.TrimSpace(runtime.Str("minute-token")) replaceWords, err := parseReplaceWords(runtime.Str("replace-words")) if err != nil { return err } body := map[string]interface{}{ "minute_token": minuteToken, "replace_words": replaceWords, } _, err = runtime.CallAPITyped(http.MethodPut, fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/transcript/word", validate.EncodePathSegment(minuteToken)), nil, body) if err != nil { return minutesWordReplaceError(err, minuteToken) } outData := map[string]interface{}{ "minute_token": minuteToken, "replace_words": replaceWords, } runtime.OutFormat(outData, nil, nil) return nil }, }
MinutesWordReplace batch-replaces words in a minute's transcript.
Functions ¶
Types ¶
This section is empty.
Click to show internal directories.
Click to hide internal directories.