vc

package
v1.0.5 Latest Latest
Warning

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

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var VCNotes = common.Shortcut{
	Service:     "vc",
	Command:     "+notes",
	Description: "Query meeting notes (via meeting-ids, minute-tokens, or calendar-event-ids)",
	Risk:        "read",
	Scopes:      []string{"vc:note:read"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "meeting-ids", Desc: "meeting IDs, comma-separated for batch"},
		{Name: "minute-tokens", Desc: "minute tokens, comma-separated for batch"},
		{Name: "calendar-event-ids", Desc: "calendar event instance IDs, comma-separated for batch"},
		{Name: "output-dir", Desc: "output directory for artifact files (default: current dir)"},
		{Name: "overwrite", Type: "bool", Desc: "overwrite existing artifact files"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if err := common.ExactlyOne(runtime, "meeting-ids", "minute-tokens", "calendar-event-ids"); err != nil {
			return err
		}
		// batch input size limit
		const maxBatchSize = 50
		for _, flag := range []string{"meeting-ids", "minute-tokens", "calendar-event-ids"} {
			if v := runtime.Str(flag); v != "" {
				if ids := common.SplitCSV(v); len(ids) > maxBatchSize {
					return output.ErrValidation("--%s: too many IDs (%d), maximum is %d", flag, len(ids), maxBatchSize)
				}
			}
		}

		if outDir := runtime.Str("output-dir"); outDir != "" {
			if err := common.ValidateSafeOutputDir(outDir); err != nil {
				return err
			}
		}
		// dynamic scope check based on which flag is provided
		var required []string
		switch {
		case runtime.Str("meeting-ids") != "":
			required = scopesMeetingIDs
		case runtime.Str("minute-tokens") != "":
			required = scopesMinuteTokens
		case runtime.Str("calendar-event-ids") != "":
			required = scopesCalendarEventIDs
		default:

		}
		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, required); len(missing) > 0 {
				return output.ErrWithHint(output.ExitAuth, "missing_scope",
					fmt.Sprintf("missing required scope(s): %s", strings.Join(missing, ", ")),
					fmt.Sprintf("run `lark-cli auth login --scope \"%s\"` 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, " ")))
			}
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		if ids := runtime.Str("meeting-ids"); ids != "" {
			return common.NewDryRunAPI().
				GET("/open-apis/vc/v1/meetings/{meeting_id}").
				GET("/open-apis/vc/v1/notes/{note_id}").
				Set("meeting_ids", common.SplitCSV(ids)).
				Set("steps", "meeting.get → note_id → note detail API")
		}
		if tokens := runtime.Str("minute-tokens"); tokens != "" {
			return common.NewDryRunAPI().
				GET("/open-apis/minutes/v1/minutes/{minute_token}").
				GET("/open-apis/vc/v1/notes/{note_id}").
				GET("/open-apis/minutes/v1/minutes/{minute_token}/artifacts").
				GET("/open-apis/minutes/v1/minutes/{minute_token}/transcript").
				Set("minute_tokens", common.SplitCSV(tokens)).
				Set("steps", "minutes API → note detail + AI artifacts + transcript")
		}
		ids := runtime.Str("calendar-event-ids")
		return common.NewDryRunAPI().
			POST("/open-apis/calendar/v4/calendars/primary").
			POST("/open-apis/calendar/v4/calendars/{calendar_id}/events/mget_instance_relation_info").
			GET("/open-apis/vc/v1/meetings/{meeting_id}").
			GET("/open-apis/vc/v1/notes/{note_id}").
			Set("calendar_event_ids", common.SplitCSV(ids)).
			Set("steps", "primary calendar → mget_instance_relation_info → meeting_id → meeting.get → note detail API")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		errOut := runtime.IO().ErrOut
		var results []any

		const batchDelay = 100 * time.Millisecond

		if ids := runtime.Str("meeting-ids"); ids != "" {
			meetingIDs := common.SplitCSV(ids)
			fmt.Fprintf(errOut, "%s querying %d meeting_id(s)\n", logPrefix, len(meetingIDs))
			for i, id := range meetingIDs {
				if err := ctx.Err(); err != nil {
					return err
				}
				if i > 0 {
					time.Sleep(batchDelay)
				}
				fmt.Fprintf(errOut, "%s querying meeting_id=%s ...\n", logPrefix, sanitizeLogValue(id))
				results = append(results, fetchNoteByMeetingID(ctx, runtime, id))
			}
		} else if tokens := runtime.Str("minute-tokens"); tokens != "" {
			minuteTokens := common.SplitCSV(tokens)
			fmt.Fprintf(errOut, "%s querying %d minute_token(s)\n", logPrefix, 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", logPrefix, sanitizeLogValue(token))
				results = append(results, fetchNoteByMinuteToken(ctx, runtime, token))
			}
		} else {
			instanceIDs := common.SplitCSV(runtime.Str("calendar-event-ids"))
			fmt.Fprintf(errOut, "%s querying %d calendar_event_id(s)\n", logPrefix, len(instanceIDs))
			calendarID, err := getPrimaryCalendarID(runtime)
			if err != nil {
				return err
			}
			fmt.Fprintf(errOut, "%s primary calendar: %s\n", logPrefix, calendarID)
			for i, id := range instanceIDs {
				if err := ctx.Err(); err != nil {
					return err
				}
				if i > 0 {
					time.Sleep(batchDelay)
				}
				fmt.Fprintf(errOut, "%s querying calendar_event_id=%s ...\n", logPrefix, sanitizeLogValue(id))
				results = append(results, fetchNoteByCalendarEventID(ctx, runtime, id, calendarID))
			}
		}

		successCount := 0
		for _, r := range results {
			m, _ := r.(map[string]any)
			if m["error"] == nil {
				successCount++
			}
		}
		fmt.Fprintf(errOut, "%s done: %d total, %d succeeded, %d failed\n", logPrefix, len(results), successCount, len(results)-successCount)

		if successCount == 0 && len(results) > 0 {
			outData := map[string]any{"notes": results}
			runtime.OutFormat(outData, &output.Meta{Count: len(results)}, nil)
			return output.ErrAPI(0, fmt.Sprintf("all %d queries failed", len(results)), nil)
		}

		outData := map[string]any{"notes": results}
		runtime.OutFormat(outData, &output.Meta{Count: len(results)}, func(w io.Writer) {
			var rows []map[string]interface{}
			for _, r := range results {
				m, _ := r.(map[string]any)
				id, _ := m["meeting_id"].(string)
				if id == "" {
					id, _ = m["minute_token"].(string)
				}
				row := map[string]interface{}{"id": id}
				if errMsg, _ := m["error"].(string); errMsg != "" {
					row["status"] = "FAIL"
					row["error"] = errMsg
				} else {
					row["status"] = "OK"
					if v, _ := m["note_doc_token"].(string); v != "" {
						row["note_doc"] = v
					}
					if v, _ := m["verbatim_doc_token"].(string); v != "" {
						row["verbatim_doc"] = v
					}
					if v, _ := m["shared_doc_tokens"].([]string); len(v) > 0 {
						row["shared_docs"] = strings.Join(v, ", ")
					}
					if v, _ := m["source"].(string); v != "" {
						row["source"] = v
					}
					if v, _ := m["create_time"].(string); v != "" {
						row["create_time"] = v
					}
					if arts, _ := m["artifacts"].(map[string]any); arts != nil {
						if v, _ := arts["transcript_file"].(string); v != "" {
							row["transcript"] = v
						}
					}
				}
				rows = append(rows, row)
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%d note(s), %d succeeded, %d failed\n", len(results), successCount, len(results)-successCount)
		})
		return nil
	},
}

VCNotes queries meeting notes via meeting-ids, minute-tokens, or calendar-event-ids.

View Source
var VCSearch = common.Shortcut{
	Service:     "vc",
	Command:     "+search",
	Description: "Search meeting records (requires at least one filter)",
	Risk:        "read",
	Scopes:      []string{"vc:meeting.search:read"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "query", Desc: "search keyword"},
		{Name: "start", Desc: "start time (ISO 8601 or YYYY-MM-DD, e.g. 2026-03-24T00:00+08:00)"},
		{Name: "end", Desc: "end time (ISO 8601 or YYYY-MM-DD, e.g. 2026-03-25)"},
		{Name: "organizer-ids", Desc: "organizer open_id list, comma-separated"},
		{Name: "participant-ids", Desc: "participant open_id list, comma-separated"},
		{Name: "room-ids", Desc: "room_id list, comma-separated"},
		{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) > maxVCSearchQueryLen {
			return output.ErrValidation("--query: length must be between 1 and 50 characters")
		}
		if _, err := common.ValidatePageSize(runtime, "page-size", defaultVCSearchPageSize, 1, maxVCSearchPageSize); err != nil {
			return err
		}
		for _, flag := range []string{"query", "start", "end", "organizer-ids", "participant-ids", "room-ids"} {
			if strings.TrimSpace(runtime.Str(flag)) != "" {
				return nil
			}
		}
		return common.FlagErrorf("specify at least one of --query, --start, --end, --organizer-ids, --participant-ids, or --room-ids")
	},
	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 := buildSearchParams(runtime)
		dryRunParams := map[string]interface{}{}
		for key, values := range params {
			if len(values) == 1 {
				dryRunParams[key] = values[0]
			} else if len(values) > 1 {
				vs := make([]string, len(values))
				copy(vs, values)
				dryRunParams[key] = vs
			}
		}
		dryRun := common.NewDryRunAPI().
			POST("/open-apis/vc/v1/meetings/search")
		if len(dryRunParams) > 0 {
			dryRun.Params(dryRunParams)
		}
		return dryRun.Body(buildSearchBody(runtime, startTime, endTime))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		startTime, endTime, err := parseTimeRange(runtime)
		if err != nil {
			return err
		}
		data, err := runtime.DoAPIJSON("POST", "/open-apis/vc/v1/meetings/search", buildSearchParams(runtime), buildSearchBody(runtime, startTime, endTime))
		if err != nil {
			return err
		}
		if data == nil {
			data = map[string]interface{}{}
		}
		items := common.GetSlice(data, "items")
		outData := map[string]interface{}{
			"items":      items,
			"total":      data["total"],
			"has_more":   data["has_more"],
			"page_token": data["page_token"],
		}
		hasMore, _ := data["has_more"].(bool)
		runtime.OutFormat(outData, &output.Meta{Count: len(items)}, func(w io.Writer) {
			if len(items) == 0 {
				fmt.Fprintln(w, "No meetings.")
				return
			}
			var rows []map[string]interface{}
			common.EachMap(items, func(item map[string]interface{}) {
				rows = append(rows, map[string]interface{}{
					"id":           fmt.Sprintf("%v", item["id"]),
					"display_info": common.TruncateStr(meetingSearchDisplayInfo(item), 40),
					"meta_data":    common.TruncateStr(meetingSearchDescription(item), 80),
				})
			})
			output.PrintTable(w, rows)
		})

		if hasMore && runtime.Format != "json" && runtime.Format != "" {
			pt, _ := data["page_token"].(string)
			fmt.Fprintf(runtime.IO().Out, "\n(more available, page_token: %s)\n", pt)
		}
		return nil
	},
}

VCSearch searches historical meeting records with filters.

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all vc shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

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