项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

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
},
}