diff --git a/shortcuts/vc/vc_notes.go b/shortcuts/vc/vc_notes.go index 5938146ef..5a0170394 100644 --- a/shortcuts/vc/vc_notes.go +++ b/shortcuts/vc/vc_notes.go @@ -90,17 +90,28 @@ func getPrimaryCalendarID(runtime *common.RuntimeContext) (string, error) { return calID, nil } +// eventRelationInfo holds the resolved relation info from mget_instance_relation_info API. +type eventRelationInfo struct { + MeetingIDs []string // meeting IDs (one event may spawn multiple meetings) + MeetingNotes []string // user-bound meeting note doc tokens +} + // resolveMeetingIDsFromCalendarEvent resolves a calendar event instance to its -// associated meeting IDs via the mget_instance_relation_info API. +// associated meeting IDs and optionally note doc tokens via the mget_instance_relation_info API. +// When needNotes is true, meeting_notes are also requested. // Shared by +notes and +recording for the --calendar-event-ids path. -func resolveMeetingIDsFromCalendarEvent(runtime *common.RuntimeContext, instanceID string, calendarID string) ([]string, error) { +func resolveMeetingIDsFromCalendarEvent(runtime *common.RuntimeContext, instanceID string, calendarID string, needNotes bool) (*eventRelationInfo, error) { + body := map[string]any{ + "instance_ids": []string{instanceID}, + "need_meeting_instance_ids": true, + } + if needNotes { + body["need_meeting_notes"] = true + } data, err := runtime.DoAPIJSON(http.MethodPost, fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/mget_instance_relation_info", validate.EncodePathSegment(calendarID)), nil, - map[string]any{ - "instance_ids": []string{instanceID}, - "need_meeting_instance_ids": true, - }) + body) if err != nil { return nil, fmt.Errorf("failed to query event relation info: %w", err) } @@ -116,7 +127,7 @@ func resolveMeetingIDsFromCalendarEvent(runtime *common.RuntimeContext, instance return nil, fmt.Errorf("no associated video meeting for this event") } - var ids []string + result := &eventRelationInfo{} for _, mid := range rawIDs { if mid == nil { continue @@ -130,35 +141,101 @@ func resolveMeetingIDsFromCalendarEvent(runtime *common.RuntimeContext, instance default: meetingID = fmt.Sprintf("%v", v) } - ids = append(ids, meetingID) + result.MeetingIDs = append(result.MeetingIDs, meetingID) } - return ids, nil + + result.MeetingNotes = extractStringSlice(info, "meeting_notes") + + return result, nil +} + +// extractStringSlice extracts a []string from a JSON array field in a map. +func extractStringSlice(m map[string]any, key string) []string { + raw, _ := m[key].([]any) + var out []string + for _, v := range raw { + if s, ok := v.(string); ok && s != "" { + out = append(out, s) + } + } + return out } // fetchNoteByCalendarEventID queries notes via calendar event instance ID. -// Chain: primary calendar → mget_instance_relation_info → meeting_id → meeting.get → note_id +// Two sources of doc tokens are collected and deduplicated: +// - mget_instance_relation_info: meeting_notes (user-bound note doc tokens) +// - meeting_id chain: meeting.get → note detail (note_doc_token, verbatim_doc_token, shared_doc_tokens) func fetchNoteByCalendarEventID(ctx context.Context, runtime *common.RuntimeContext, instanceID string, calendarID string) map[string]any { errOut := runtime.IO().ErrOut - meetingIDs, err := resolveMeetingIDsFromCalendarEvent(runtime, instanceID, calendarID) + relInfo, err := resolveMeetingIDsFromCalendarEvent(runtime, instanceID, calendarID, true) if err != nil { return map[string]any{"calendar_event_id": instanceID, "error": err.Error()} } - if len(meetingIDs) > 1 { - fmt.Fprintf(errOut, "%s event %s has %d meetings, trying each\n", logPrefix, sanitizeLogValue(instanceID), len(meetingIDs)) + result := map[string]any{"calendar_event_id": instanceID} + + // source 1: user-bound meeting note doc tokens from mget_instance_relation_info + if len(relInfo.MeetingNotes) > 0 { + result["meeting_notes"] = relInfo.MeetingNotes + } + + // source 2: meeting_id → meeting.get → note detail (for shared_doc_tokens etc.) + if len(relInfo.MeetingIDs) > 1 { + fmt.Fprintf(errOut, "%s event %s has %d meetings, trying each\n", logPrefix, sanitizeLogValue(instanceID), len(relInfo.MeetingIDs)) } - // try each associated meeting until one has notes - for _, meetingID := range meetingIDs { + for _, meetingID := range relInfo.MeetingIDs { fmt.Fprintf(errOut, "%s event %s → meeting_id=%s\n", logPrefix, sanitizeLogValue(instanceID), sanitizeLogValue(meetingID)) - result := fetchNoteByMeetingID(ctx, runtime, meetingID) - if result["error"] == nil { + noteResult := fetchNoteByMeetingID(ctx, runtime, meetingID) + if noteResult["error"] == nil { + for k, v := range noteResult { + result[k] = v + } + deduplicateDocTokens(result) return result } - fmt.Fprintf(errOut, "%s meeting_id=%s: %s, trying next\n", logPrefix, sanitizeLogValue(meetingID), result["error"]) + fmt.Fprintf(errOut, "%s meeting_id=%s: %s, trying next\n", logPrefix, sanitizeLogValue(meetingID), noteResult["error"]) } - return map[string]any{"calendar_event_id": instanceID, "error": "no notes found in any associated meeting"} + + // meeting chain failed, but still succeed if relation info returned note tokens + if len(relInfo.MeetingNotes) > 0 { + return result + } + result["error"] = "no notes found in any associated meeting" + return result +} + +// deduplicateDocTokens removes meeting_notes entries that duplicate note detail fields. +func deduplicateDocTokens(result map[string]any) { + seen := map[string]bool{} + if v, _ := result["note_doc_token"].(string); v != "" { + seen[v] = true + } + if v, _ := result["verbatim_doc_token"].(string); v != "" { + seen[v] = true + } + for _, tok := range asStringSlice(result["shared_doc_tokens"]) { + seen[tok] = true + } + + var filtered []string + for _, tok := range asStringSlice(result["meeting_notes"]) { + if !seen[tok] { + filtered = append(filtered, tok) + } + } + if len(filtered) > 0 { + result["meeting_notes"] = filtered + } else { + delete(result, "meeting_notes") + } +} + +// asStringSlice casts v to []string; returns nil for non-[]string or nil values. +func asStringSlice(v any) []string { + ss, _ := v.([]string) + return ss } // fetchNoteByMeetingID queries notes via meeting_id. @@ -569,6 +646,9 @@ var VCNotes = common.Shortcut{ if id == "" { id, _ = m["minute_token"].(string) } + if id == "" { + id, _ = m["calendar_event_id"].(string) + } row := map[string]interface{}{"id": id} if errMsg, _ := m["error"].(string); errMsg != "" { row["status"] = "FAIL" @@ -584,6 +664,9 @@ var VCNotes = common.Shortcut{ if v, _ := m["shared_doc_tokens"].([]string); len(v) > 0 { row["shared_docs"] = strings.Join(v, ", ") } + if v := asStringSlice(m["meeting_notes"]); len(v) > 0 { + row["meeting_notes"] = strings.Join(v, ", ") + } if v, _ := m["source"].(string); v != "" { row["source"] = v } diff --git a/shortcuts/vc/vc_notes_test.go b/shortcuts/vc/vc_notes_test.go index c202813e4..83569d240 100644 --- a/shortcuts/vc/vc_notes_test.go +++ b/shortcuts/vc/vc_notes_test.go @@ -372,3 +372,270 @@ func TestParseArtifactType_AllBranches(t *testing.T) { t.Errorf("nil: got %d, want 0", got) } } + +// --------------------------------------------------------------------------- +// Unit tests for new calendar-to-notes functions +// --------------------------------------------------------------------------- + +func TestExtractStringSlice(t *testing.T) { + m := map[string]any{ + "tokens": []any{"a", "b", "", "c"}, + "empty": []any{}, + "missing": nil, + "mixed": []any{"x", float64(123), nil, "y"}, + } + if got := extractStringSlice(m, "tokens"); len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" { + t.Errorf("tokens: got %v, want [a b c]", got) + } + if got := extractStringSlice(m, "empty"); got != nil { + t.Errorf("empty: got %v, want nil", got) + } + if got := extractStringSlice(m, "missing"); got != nil { + t.Errorf("missing: got %v, want nil", got) + } + if got := extractStringSlice(m, "nonexistent"); got != nil { + t.Errorf("nonexistent: got %v, want nil", got) + } + if got := extractStringSlice(m, "mixed"); len(got) != 2 || got[0] != "x" || got[1] != "y" { + t.Errorf("mixed: got %v, want [x y]", got) + } +} + +func TestAsStringSlice(t *testing.T) { + if got := asStringSlice(nil); got != nil { + t.Errorf("nil: got %v, want nil", got) + } + if got := asStringSlice([]string{"a", "b"}); len(got) != 2 || got[0] != "a" { + t.Errorf("[]string: got %v", got) + } + if got := asStringSlice("not a slice"); got != nil { + t.Errorf("string: got %v, want nil", got) + } +} + +func TestDeduplicateDocTokens(t *testing.T) { + // case 1: meeting_notes overlap with note_doc_token + result := map[string]any{ + "note_doc_token": "doc_main", + "verbatim_doc_token": "doc_verb", + "shared_doc_tokens": []string{"doc_shared"}, + "meeting_notes": []string{"doc_main", "unique_note"}, + } + deduplicateDocTokens(result) + mn := asStringSlice(result["meeting_notes"]) + if len(mn) != 1 || mn[0] != "unique_note" { + t.Errorf("meeting_notes: got %v, want [unique_note]", mn) + } + + // case 2: no overlap + result2 := map[string]any{ + "note_doc_token": "doc_a", + "meeting_notes": []string{"doc_b"}, + } + deduplicateDocTokens(result2) + mn2 := asStringSlice(result2["meeting_notes"]) + if len(mn2) != 1 || mn2[0] != "doc_b" { + t.Errorf("no overlap: got %v, want [doc_b]", mn2) + } + + // case 3: empty meeting_notes + result3 := map[string]any{ + "note_doc_token": "doc_a", + } + deduplicateDocTokens(result3) + if _, exists := result3["meeting_notes"]; exists { + t.Errorf("should not have meeting_notes key") + } + + // case 4: all meeting_notes are duplicates + result4 := map[string]any{ + "note_doc_token": "doc_a", + "shared_doc_tokens": []string{"doc_b"}, + "meeting_notes": []string{"doc_a", "doc_b"}, + } + deduplicateDocTokens(result4) + if _, exists := result4["meeting_notes"]; exists { + t.Errorf("case4: meeting_notes should be removed (all duplicates), got %v", result4["meeting_notes"]) + } +} + +// --------------------------------------------------------------------------- +// Integration: calendar-event-ids path with meeting_notes + dedup +// --------------------------------------------------------------------------- + +func calendarRelationStub(calendarID, instanceID string, meetingIDs []string, meetingNotes []string) *httpmock.Stub { + infos := map[string]interface{}{ + "instance_id": instanceID, + } + mIDs := make([]interface{}, len(meetingIDs)) + for i, id := range meetingIDs { + mIDs[i] = id + } + infos["meeting_instance_ids"] = mIDs + if len(meetingNotes) > 0 { + notes := make([]interface{}, len(meetingNotes)) + for i, n := range meetingNotes { + notes[i] = n + } + infos["meeting_notes"] = notes + } + return &httpmock.Stub{ + Method: "POST", + URL: fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/mget_instance_relation_info", calendarID), + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "instance_relation_infos": []interface{}{infos}, + }, + }, + } +} + +func primaryCalendarStub(calendarID string) *httpmock.Stub { + return &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/calendar/v4/calendars/primary", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "calendars": []interface{}{ + map[string]interface{}{ + "calendar": map[string]interface{}{ + "calendar_id": calendarID, + }, + }, + }, + }, + }, + } +} + +func TestNotes_CalendarPath_MeetingNotesDedup(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + calID := "cal_test" + reg.Register(primaryCalendarStub(calID)) + // mget returns meeting_notes=["doc_main","unique_note"], doc_main overlaps with note_doc_token + reg.Register(calendarRelationStub(calID, "evt_001", []string{"m001"}, []string{"doc_main", "unique_note"})) + reg.Register(meetingGetStub("m001", "note_001")) + reg.Register(noteDetailStub("note_001")) + + err := mountAndRun(t, VCNotes, []string{"+notes", "--calendar-event-ids", "evt_001", "--as", "user"}, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var resp map[string]any + if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse output: %v", err) + } + data, _ := resp["data"].(map[string]any) + notes, _ := data["notes"].([]any) + if len(notes) != 1 { + t.Fatalf("expected 1 note, got %d", len(notes)) + } + note, _ := notes[0].(map[string]any) + + // doc_main should be deduplicated (exists in note_doc_token) + // only "unique_note" should remain in meeting_notes + mn, _ := note["meeting_notes"].([]any) + if len(mn) != 1 { + t.Fatalf("meeting_notes: expected 1 after dedup, got %d: %v", len(mn), mn) + } + if mn[0] != "unique_note" { + t.Errorf("meeting_notes[0] = %v, want unique_note", mn[0]) + } +} + +func TestNotes_CalendarPath_FallbackWhenMeetingChainFails(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + calID := "cal_test" + reg.Register(primaryCalendarStub(calID)) + // mget returns note tokens but meeting chain will fail + reg.Register(calendarRelationStub(calID, "evt_002", []string{"m_bad"}, []string{"fallback_note"})) + // meeting.get returns error + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/meetings/m_bad", + Body: map[string]interface{}{"code": 121004, "msg": "data not found"}, + }) + + err := mountAndRun(t, VCNotes, []string{"+notes", "--calendar-event-ids", "evt_002", "--as", "user"}, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var resp map[string]any + if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse output: %v", err) + } + data, _ := resp["data"].(map[string]any) + notes, _ := data["notes"].([]any) + if len(notes) != 1 { + t.Fatalf("expected 1 note, got %d", len(notes)) + } + note, _ := notes[0].(map[string]any) + + // should succeed via fallback (meeting chain failed but mget had tokens) + if _, hasErr := note["error"]; hasErr { + t.Errorf("expected no error (fallback), got error: %v", note["error"]) + } + mn, _ := note["meeting_notes"].([]any) + if len(mn) != 1 || mn[0] != "fallback_note" { + t.Errorf("meeting_notes: got %v, want [fallback_note]", mn) + } +} + +func TestNotes_CalendarPath_NeedNotes_RequestBody(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + warmTokenCache(t) + + stub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/calendar/v4/calendars/cal_001/events/mget_instance_relation_info", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "instance_relation_infos": []interface{}{ + map[string]interface{}{ + "meeting_instance_ids": []interface{}{"m001"}, + }, + }, + }, + }, + } + reg.Register(stub) + + s := common.Shortcut{ + Service: "test", + Command: "+need-notes-test", + AuthTypes: []string{"bot"}, + Execute: func(_ context.Context, rctx *common.RuntimeContext) error { + _, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", true) + return err + }, + } + parent := &cobra.Command{Use: "vc"} + s.Mount(parent, f) + parent.SetArgs([]string{"+need-notes-test"}) + parent.SilenceErrors = true + parent.SilenceUsage = true + if err := parent.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(stub.CapturedBody) == 0 { + t.Fatal("request body was not captured") + } + var body map[string]any + if err := json.Unmarshal(stub.CapturedBody, &body); err != nil { + t.Fatalf("failed to parse captured body: %v", err) + } + if v, ok := body["need_meeting_notes"]; !ok || v != true { + t.Errorf("need_meeting_notes: got %v, want true", v) + } + if _, ok := body["need_ai_meeting_notes"]; ok { + t.Errorf("need_ai_meeting_notes should not be requested") + } +} diff --git a/shortcuts/vc/vc_recording.go b/shortcuts/vc/vc_recording.go index 480d248e6..425bdebdf 100644 --- a/shortcuts/vc/vc_recording.go +++ b/shortcuts/vc/vc_recording.go @@ -179,13 +179,13 @@ var VCRecording = common.Shortcut{ time.Sleep(batchDelay) } fmt.Fprintf(errOut, "%s resolving calendar_event_id=%s ...\n", recordingLogPrefix, sanitizeLogValue(instanceID)) - meetingIDs, resolveErr := resolveMeetingIDsFromCalendarEvent(runtime, instanceID, calendarID) + relInfo, resolveErr := resolveMeetingIDsFromCalendarEvent(runtime, instanceID, calendarID, false) if resolveErr != nil { results = append(results, map[string]any{"calendar_event_id": instanceID, "error": resolveErr.Error()}) continue } found := false - for _, meetingID := range meetingIDs { + for _, meetingID := range relInfo.MeetingIDs { fmt.Fprintf(errOut, "%s event %s → meeting_id=%s\n", recordingLogPrefix, sanitizeLogValue(instanceID), sanitizeLogValue(meetingID)) result := fetchRecordingByMeetingID(ctx, runtime, meetingID) if result["error"] == nil { diff --git a/shortcuts/vc/vc_recording_test.go b/shortcuts/vc/vc_recording_test.go index c8f879e71..0f5fe2cdf 100644 --- a/shortcuts/vc/vc_recording_test.go +++ b/shortcuts/vc/vc_recording_test.go @@ -357,10 +357,11 @@ func TestResolveMeetingIDs_TypeCoercion(t *testing.T) { Command: "+resolve-test", AuthTypes: []string{"bot"}, Execute: func(_ context.Context, rctx *common.RuntimeContext) error { - ids, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001") + relInfo, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", false) if err != nil { return err } + ids := relInfo.MeetingIDs if len(ids) != 2 { t.Errorf("expected 2 IDs (nil skipped), got %d: %v", len(ids), ids) } @@ -408,7 +409,7 @@ func TestResolveMeetingIDs_NoMeetings(t *testing.T) { Command: "+resolve-no-meetings", AuthTypes: []string{"bot"}, Execute: func(_ context.Context, rctx *common.RuntimeContext) error { - _, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001") + _, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", false) if err == nil { t.Error("expected error for no meetings") } @@ -449,7 +450,7 @@ func TestResolveMeetingIDs_NoRelationInfo(t *testing.T) { Command: "+resolve-no-info", AuthTypes: []string{"bot"}, Execute: func(_ context.Context, rctx *common.RuntimeContext) error { - _, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001") + _, err := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", false) if err == nil { t.Error("expected error for no relation info") } @@ -581,15 +582,15 @@ func TestRecording_Execute_CalendarPath_ResolveAndFetch(t *testing.T) { }) err := botExec(t, "cal-resolve", f, func(ctx context.Context, rctx *common.RuntimeContext) error { - ids, resolveErr := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001") + relInfo, resolveErr := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", false) if resolveErr != nil { t.Fatalf("resolve failed: %v", resolveErr) } - if len(ids) != 1 || ids[0] != "m001" { - t.Fatalf("expected [m001], got %v", ids) + if len(relInfo.MeetingIDs) != 1 || relInfo.MeetingIDs[0] != "m001" { + t.Fatalf("expected [m001], got %v", relInfo.MeetingIDs) } - result := fetchRecordingByMeetingID(ctx, rctx, ids[0]) + result := fetchRecordingByMeetingID(ctx, rctx, relInfo.MeetingIDs[0]) if result["error"] != nil { t.Errorf("fetch should succeed, got: %v", result["error"]) } @@ -641,17 +642,17 @@ func TestRecording_Execute_CalendarPath_MultiMeetingFallback(t *testing.T) { }) err := botExec(t, "cal-fallback", f, func(ctx context.Context, rctx *common.RuntimeContext) error { - ids, resolveErr := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001") + relInfo, resolveErr := resolveMeetingIDsFromCalendarEvent(rctx, "evt_001", "cal_001", false) if resolveErr != nil { t.Fatalf("resolve failed: %v", resolveErr) } - if len(ids) != 2 { - t.Fatalf("expected 2 meeting IDs, got %d", len(ids)) + if len(relInfo.MeetingIDs) != 2 { + t.Fatalf("expected 2 meeting IDs, got %d", len(relInfo.MeetingIDs)) } // simulate fallback: try each until success var found bool - for _, meetingID := range ids { + for _, meetingID := range relInfo.MeetingIDs { result := fetchRecordingByMeetingID(ctx, rctx, meetingID) if result["error"] == nil { if result["minute_token"] != "obcnfallback" { diff --git a/skills/lark-vc/SKILL.md b/skills/lark-vc/SKILL.md index a6712fe9e..9ae314d22 100644 --- a/skills/lark-vc/SKILL.md +++ b/skills/lark-vc/SKILL.md @@ -18,7 +18,8 @@ metadata: - **会议记录(Meeting Record)**:视频会议结束后生成的记录,支持通过关键词、时间段、参会人、组织者、会议室等筛选条件搜索会议室。 - **会议纪要(Note)**:视频会议结束后生成的结构化文档,包含纪要文档(包含总结、待办、章节)和逐字稿文档。 - **妙记(Minutes)**:来源于飞书视频会议的录制产物或用户上传的音视频文件,支持视频/音频的转写和会议纪要,通过 minute\_token 标识。 -- **纪要文档(MainDoc)**:会议纪要的主文档,包含 AI 生成的总结和待办。 +- **纪要文档(MainDoc)**:AI 智能纪要的主文档,包含 AI 生成的总结和待办,对应 `note_doc_token`。 +- **用户会议纪要(MeetingNotes)**:用户主动绑定到会议的纪要文档,对应 `meeting_notes`。仅通过 `--calendar-event-ids` 路径返回。 - **逐字稿(VerbatimDoc)**:会议的逐句文字记录,包含说话人和时间戳。 ## 核心场景 @@ -42,10 +43,12 @@ lark-cli docs +media-download --type whiteboard --token --out ``` > **产物目录规范**:同一会议的所有下载产物(封面图、逐字稿等)统一放到 `artifact-/` 目录下,不要散落在当前工作目录。 -> **`note_doc_token` vs `verbatim_doc_token` — 两份不同的文档,根据用户意图选择:** -> - `note_doc_token` → **智能纪要**(AI 总结 + 待办 + 章节)— 用户说"纪要""总结""待办""纪要内容"时用这个 +> **纪要相关文档 — 根据用户意图选择:** +> - `note_doc_token` → **AI 智能纪要**(AI 总结 + 待办 + 章节) +> - `meeting_notes` → **用户绑定的会议纪要**(用户主动关联到会议的文档,仅 `--calendar-event-ids` 路径返回) > - `verbatim_doc_token` → **逐字稿**(完整的逐句文字记录,含说话人和时间戳)— 用户说"逐字稿""完整记录""谁说了什么"时用这个 -> - 用户意图不明确时,应展示两个文档链接让用户选择,而不是替用户决定 +> - 用户说"纪要""总结""纪要内容"时,应同时返回 `note_doc_token` 和 `meeting_notes`(如有) +> - 用户意图不明确时,应展示所有文档链接让用户选择,而不是替用户决定 ### 3. 纪要文档与逐字稿链接 1. 纪要文档、逐字稿文档与关联的共享文档默认使用文档 Token 返回。 @@ -68,8 +71,9 @@ lark-cli docs +fetch --doc <doc_token> ``` Meeting (视频会议) ├── Note (会议纪要) -│ ├── MainDoc (主纪要文档) -│ ├── VerbatimDoc (逐字稿) +│ ├── MainDoc (AI 智能纪要文档, note_doc_token) +│ ├── MeetingNotes (用户绑定的会议纪要文档, meeting_notes) +│ ├── VerbatimDoc (逐字稿, verbatim_doc_token) │ └── SharedDoc (会中共享文档) └── Minutes (妙记) ← minute_token 标识,+recording 从 meeting_id 获取 ├── Transcript (文字记录) diff --git a/skills/lark-vc/references/lark-vc-notes.md b/skills/lark-vc/references/lark-vc-notes.md index 53aeb880d..7e082f9cc 100644 --- a/skills/lark-vc/references/lark-vc-notes.md +++ b/skills/lark-vc/references/lark-vc-notes.md @@ -75,13 +75,14 @@ lark-cli vc +notes --meeting-ids 69xxxxxxxxxxxxx28 --dry-run | 字段 | 说明 | |------|------| -| `note_doc_token` | **智能纪要**文档 Token — 包含 AI 总结、待办、章节(用户说"纪要"时用这个) | -| `verbatim_doc_token` | **逐字稿**文档 Token — 完整的逐句文字记录,含说话人和时间戳(用户说"逐字稿"时才用这个) | +| `note_doc_token` | **AI 智能纪要**文档 Token — AI 生成的总结、待办、章节 | +| `meeting_notes` | **用户绑定的会议纪要**文档 Token 列表 — 用户主动关联到会议的文档(仅 `--calendar-event-ids` 路径返回) | +| `verbatim_doc_token` | **逐字稿**文档 Token — 完整的逐句文字记录,含说话人和时间戳 | | `shared_doc_tokens` | 会中共享文档 Token 列表 | | `creator_id` | 创建者 ID | | `create_time` | 创建时间(格式化) | -> **选择哪个 token?** 用户说"会议纪要""总结""待办""纪要内容" → 用 `note_doc_token`。用户说"逐字稿""完整记录""谁说了什么" → 用 `verbatim_doc_token`。意图不明确时,展示两个文档链接让用户选择。 +> **选择哪个 token?** 用户说"会议纪要""总结""待办""纪要内容" → 返回 `note_doc_token` 和 `meeting_notes`(如有)。用户说"逐字稿""完整记录""谁说了什么" → 用 `verbatim_doc_token`。意图不明确时,展示所有文档链接让用户选择。 ### minute-tokens 路径的 AI 产物