larksuite--cli
bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
217 行
7.2 KiB
Go
217 行
7.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// vc +detail — get meeting details including note_id and minute_token
|
|
|
|
package vc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/auth"
|
|
"github.com/larksuite/cli/internal/credential"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const detailLogPrefix = "[vc +detail]"
|
|
|
|
var scopesDetailMeetingIDs = []string{
|
|
"vc:meeting.meetingevent:read",
|
|
"vc:record:readonly",
|
|
}
|
|
|
|
// meetingDetailItem represents a single meeting detail result.
|
|
type meetingDetailItem struct {
|
|
MeetingID string `json:"meeting_id"`
|
|
MeetingNo string `json:"meeting_no,omitempty"`
|
|
Topic string `json:"topic"`
|
|
StartTime string `json:"start_time,omitempty"`
|
|
EndTime string `json:"end_time,omitempty"`
|
|
NoteID string `json:"note_id,omitempty"`
|
|
MinuteToken string `json:"minute_token,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Hint string `json:"hint,omitempty"`
|
|
}
|
|
|
|
// fetchMeetingDetail queries meeting.get and recording API to return a
|
|
// consolidated view of meeting metadata, note_id, and minute_token.
|
|
// Error is only set when an API call actually fails; note_id and minute_token
|
|
// are always present (empty string when not available).
|
|
func fetchMeetingDetail(ctx context.Context, runtime *common.RuntimeContext, meetingID string) *meetingDetailItem {
|
|
result := &meetingDetailItem{MeetingID: meetingID}
|
|
|
|
// Step 1: query meeting detail
|
|
data, err := runtime.CallAPITyped(http.MethodGet,
|
|
fmt.Sprintf("/open-apis/vc/v1/meetings/%s", validate.EncodePathSegment(meetingID)),
|
|
map[string]interface{}{"with_participants": "false", "query_mode": "0"}, nil)
|
|
if err != nil {
|
|
result.Error = fmt.Sprintf("failed to query meeting detail: %v", err)
|
|
return result
|
|
}
|
|
|
|
meeting, _ := data["meeting"].(map[string]any)
|
|
if meeting == nil {
|
|
result.Error = "meeting not found in response"
|
|
return result
|
|
}
|
|
|
|
if v, ok := meeting["meeting_no"].(string); ok {
|
|
result.MeetingNo = v
|
|
}
|
|
if v, ok := meeting["topic"].(string); ok {
|
|
result.Topic = v
|
|
}
|
|
if v := common.FormatTime(meeting["start_time"]); v != "" {
|
|
result.StartTime = v
|
|
}
|
|
if v := common.FormatTime(meeting["end_time"]); v != "" {
|
|
result.EndTime = v
|
|
}
|
|
if v, ok := meeting["note_id"].(string); ok && v != "" {
|
|
result.NoteID = v
|
|
}
|
|
|
|
// Step 2: query minute_token via recording API
|
|
minuteToken, minuteHint, minuteErr := fetchMeetingMinuteToken(runtime, meetingID)
|
|
if minuteErr != nil {
|
|
// Recording API failed — surface the error but keep data from step 1
|
|
result.Error = fmt.Sprintf("failed to query minutes: %v", minuteErr)
|
|
minuteHint = ""
|
|
}
|
|
if minuteToken != "" {
|
|
result.MinuteToken = minuteToken
|
|
}
|
|
|
|
// Add hints for empty resources (not errors, just informational)
|
|
var emptyFields []string
|
|
if result.NoteID == "" {
|
|
emptyFields = append(emptyFields, "note_id")
|
|
}
|
|
if result.MinuteToken == "" && minuteErr == nil && minuteHint == "" {
|
|
emptyFields = append(emptyFields, "minute_token")
|
|
}
|
|
if len(emptyFields) > 0 {
|
|
result.Hint = fmt.Sprintf("%s not found for this meeting", strings.Join(emptyFields, ", "))
|
|
}
|
|
if minuteHint != "" {
|
|
if result.Hint != "" {
|
|
result.Hint += "; " + minuteHint
|
|
} else {
|
|
result.Hint = minuteHint
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// VCDetail gets meeting details including note_id and minute_token.
|
|
var VCDetail = common.Shortcut{
|
|
Service: "vc",
|
|
Command: "+detail",
|
|
Description: "Get meeting details including note_id and minute_token by meeting IDs",
|
|
Risk: "read",
|
|
Scopes: []string{"vc:meeting.meetingevent:read", "vc:record:readonly"},
|
|
AuthTypes: []string{"user"},
|
|
HasFormat: true,
|
|
Flags: []common.Flag{
|
|
{Name: "meeting-ids", Desc: "meeting IDs, comma-separated for batch", Required: true},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
ids := common.SplitCSV(runtime.Str("meeting-ids"))
|
|
const maxBatchSize = 50
|
|
if len(ids) > maxBatchSize {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--meeting-ids: too many IDs (%d), maximum is %d", len(ids), maxBatchSize).WithParam("--meeting-ids")
|
|
}
|
|
// dynamic scope check
|
|
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, scopesDetailMeetingIDs); 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 {
|
|
ids := runtime.Str("meeting-ids")
|
|
return common.NewDryRunAPI().
|
|
GET("/open-apis/vc/v1/meetings/{meeting_id}").
|
|
GET("/open-apis/vc/v1/meetings/{meeting_id}/recording").
|
|
Set("meeting_ids", common.SplitCSV(ids)).
|
|
Set("steps", "meeting.get → note_id + recording API → minute_token")
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
errOut := runtime.IO().ErrOut
|
|
meetingIDs := common.SplitCSV(runtime.Str("meeting-ids"))
|
|
results := make([]*meetingDetailItem, 0, len(meetingIDs))
|
|
|
|
const batchDelay = 100 * time.Millisecond
|
|
fmt.Fprintf(errOut, "%s querying %d meeting_id(s)\n", detailLogPrefix, 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", detailLogPrefix, sanitizeLogValue(id))
|
|
results = append(results, fetchMeetingDetail(ctx, runtime, id))
|
|
}
|
|
|
|
successCount := 0
|
|
for _, r := range results {
|
|
if r.Error == "" {
|
|
successCount++
|
|
}
|
|
}
|
|
fmt.Fprintf(errOut, "%s done: %d total, %d succeeded, %d failed\n", detailLogPrefix, len(results), successCount, len(results)-successCount)
|
|
|
|
if successCount == 0 && len(results) > 0 {
|
|
return runtime.OutPartialFailure(map[string]any{"meetings": results}, &output.Meta{Count: len(results)})
|
|
}
|
|
|
|
outData := map[string]any{"meetings": results}
|
|
runtime.OutFormat(outData, &output.Meta{Count: len(results)}, func(w io.Writer) {
|
|
if len(results) == 0 {
|
|
fmt.Fprintln(w, "No meetings.")
|
|
return
|
|
}
|
|
var rows []map[string]interface{}
|
|
for _, r := range results {
|
|
row := map[string]interface{}{"meeting_id": r.MeetingID}
|
|
if r.Error != "" {
|
|
row["status"] = "FAIL"
|
|
row["error"] = r.Error
|
|
} else {
|
|
row["status"] = "OK"
|
|
}
|
|
if r.NoteID != "" {
|
|
row["note_id"] = r.NoteID
|
|
}
|
|
if r.MinuteToken != "" {
|
|
row["minute_token"] = r.MinuteToken
|
|
}
|
|
row["topic"] = r.Topic
|
|
if r.Hint != "" {
|
|
row["hint"] = r.Hint
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
output.PrintTable(w, rows)
|
|
fmt.Fprintf(w, "\n%d meeting(s), %d succeeded, %d failed\n", len(results), successCount, len(results)-successCount)
|
|
})
|
|
return nil
|
|
},
|
|
}
|