minutes

package
v1.0.72 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 24 Imported by: 0

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

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all minutes shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL