fix(vc): align meeting query scopes by identity (#1850)

* fix(vc): align meeting query scopes by identity

* docs(vc): simplify meeting query scope guidance

* fix: align meeting query scopes by identity

* fix: harden vc meeting query scope preflight

* test: assert vc meeting query permission category

* fix: declare empty vc meeting query scopes

* fix: align vc meeting query scope metadata

* docs: simplify vc meeting query scope guidance

* fix: preflight vc meeting query tat scopes

* fix: make vc scope metadata lookup best effort

* fix(vc): accept compatible meeting query scopes

* test(vc): cover meeting query validate scope checks

* refactor(vc): align meeting query precheck with framework

* fix(vc): clarify meeting query scope recovery

* docs(vc): use gray access as permission fallback

* fix(vc): use user-only scope preflight for meeting queries

* fix(vc): route meeting scope hints by error code

* fix(vc): clarify compatible scope application hint

* fix(vc): simplify meeting scope recovery

* chore(vc): centralize meeting scope guidance

* fix(vc): preserve upstream meeting scope messages

* fix(vc): align meeting scope guidance by identity

* docs(vc): clarify meeting gray access guidance

* docs(vc): scope meeting query permission guidance

* refactor(vc): simplify meeting permission hints

* refactor(vc): remove unreachable permission guard

* fix(vc): guard missing meeting permission runtime

* fix(vc): guard typed nil meeting permission errors

* fix(vc): preserve app scope console URL

* refactor(vc): preserve original permission errors

* docs(vc): prioritize permission recovery hints

* docs(vc): simplify permission guidance

* docs(vc): align permission check order

* fix(vc): clarify meeting permission messages

* docs(vc): prioritize meeting permission guidance

* fix(vc): align meeting scope application link

* docs(vc): scope user permission guidance to queries

* fix(vc): narrow meeting missing scopes by identity
This commit is contained in:
zhicong666-bytedance
2026-07-15 19:40:26 +08:00
committed by GitHub
parent 64e10a0954
commit 64caef1526
9 changed files with 371 additions and 16 deletions

50
shortcuts/vc/helpers.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"errors"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/registry"
"github.com/larksuite/cli/shortcuts/common"
)
const (
meetingQueryUserScope = "vc:meeting.meetingevent:read"
meetingQueryBotScope = "vc:meeting.bot.join:write"
)
func normalizeMeetingQueryPermissionError(runtime *common.RuntimeContext, err error) error {
if runtime == nil {
return err
}
var permissionErr *errs.PermissionError
if !errors.As(err, &permissionErr) || permissionErr == nil {
return err
}
switch {
case runtime.As() == core.AsUser && permissionErr.Code == output.LarkErrUserScopeInsufficient:
permissionErr.Message = "access denied for user identity; recommended scope: " + meetingQueryUserScope
permissionErr.WithHint("for user identity, 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.", meetingQueryUserScope)
permissionErr.WithMissingScopes(meetingQueryUserScope)
return err
case runtime.As() == core.AsBot && permissionErr.Code == output.LarkErrAppScopeNotEnabled:
permissionErr.Message = "access denied for bot identity; recommended scope: " + meetingQueryBotScope
permissionErr.WithHint("ask the app developer to enable scope %s", meetingQueryBotScope)
permissionErr.WithMissingScopes(meetingQueryBotScope)
if runtime.Config != nil {
consoleURL := registry.BuildConsoleScopeURL(runtime.Config.Brand, runtime.Config.AppID, meetingQueryBotScope)
if consoleURL != "" {
permissionErr.WithConsoleURL(consoleURL)
}
}
return err
default:
return err
}
}

View File

@@ -0,0 +1,207 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"errors"
"net/url"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)
func bareMeetingQueryRuntime(as core.Identity) *common.RuntimeContext {
return common.TestNewRuntimeContextWithIdentity(&cobra.Command{Use: "test"}, defaultConfig(), as)
}
func TestNormalizeMeetingQueryPermissionError_NilRuntimeReturnsOriginalError(t *testing.T) {
original := errs.NewPermissionError(errs.SubtypeMissingScope, "permission failure").
WithCode(output.LarkErrUserScopeInsufficient)
if got := normalizeMeetingQueryPermissionError(nil, original); got != original {
t.Fatalf("got %v, want original error %v", got, original)
}
}
func TestNormalizeMeetingQueryPermissionError_TypedNilReturnsOriginalError(t *testing.T) {
var permissionErr *errs.PermissionError
var original error = permissionErr
if got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(core.AsUser), original); got != original {
t.Fatalf("got %v, want original error %v", got, original)
}
}
func assertMeetingQueryPermissionError(t *testing.T, err error, identity core.Identity, code int) {
t.Helper()
var pe *errs.PermissionError
if !errors.As(err, &pe) {
t.Fatalf("expected *errs.PermissionError, got %T: %v", err, err)
}
if pe.Category != errs.CategoryAuthorization {
t.Fatalf("Category = %q, want %q", pe.Category, errs.CategoryAuthorization)
}
if pe.Subtype != errs.SubtypeMissingScope && pe.Subtype != errs.SubtypeAppScopeNotApplied {
t.Fatalf("Subtype = %q, want a missing-scope subtype", pe.Subtype)
}
if pe.Identity != string(identity) {
t.Fatalf("Identity = %q, want %q", pe.Identity, identity)
}
wantScope := meetingQueryUserScope
if identity.IsBot() {
wantScope = meetingQueryBotScope
}
if !strings.Contains(pe.Hint, wantScope) {
t.Fatalf("Hint = %q, want recommended scope %q", pe.Hint, wantScope)
}
if len(pe.MissingScopes) != 1 || pe.MissingScopes[0] != wantScope {
t.Fatalf("MissingScopes = %v, want only recommended scope %q", pe.MissingScopes, wantScope)
}
if strings.Contains(pe.Hint, "either compatible scope") {
t.Fatalf("Hint = %q, must not repeat the OR-scope explanation from message", pe.Hint)
}
switch code {
case output.LarkErrAppScopeNotEnabled:
if strings.Contains(pe.Hint, "auth login") {
t.Fatalf("Hint = %q, app-scope error must not recommend user login", pe.Hint)
}
if !strings.Contains(pe.Hint, "app developer") {
t.Fatalf("Hint = %q, want app developer guidance", pe.Hint)
}
if pe.ConsoleURL == "" {
t.Fatal("ConsoleURL is empty, want identity-specific developer-console URL")
}
if strings.Contains(pe.ConsoleURL, url.QueryEscape(meetingQueryUserScope)) || !strings.Contains(pe.ConsoleURL, url.QueryEscape(meetingQueryBotScope)) {
t.Fatalf("ConsoleURL = %q, want only bot scope", pe.ConsoleURL)
}
case output.LarkErrUserScopeInsufficient:
if !strings.Contains(pe.Hint, "auth login --scope") {
t.Fatalf("Hint = %q, want auth login guidance", pe.Hint)
}
if pe.ConsoleURL != "" {
t.Fatalf("ConsoleURL = %q, user-scope error must not expose a developer-console URL", pe.ConsoleURL)
}
default:
t.Fatalf("unexpected code %d", code)
}
}
func TestNormalizeMeetingQueryPermissionError_RecommendsScopeForMatchingIdentity(t *testing.T) {
cases := []struct {
name string
identity core.Identity
code int
subtype errs.Subtype
}{
{name: "user_with_user_scope_error", identity: core.AsUser, code: output.LarkErrUserScopeInsufficient, subtype: errs.SubtypeMissingScope},
{name: "bot_with_app_scope_error", identity: core.AsBot, code: output.LarkErrAppScopeNotEnabled, subtype: errs.SubtypeAppScopeNotApplied},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
wantScope := meetingQueryUserScope
if tc.identity == core.AsBot {
wantScope = meetingQueryBotScope
}
wantMessage := "access denied for " + string(tc.identity) + " identity; recommended scope: " + wantScope
original := errs.NewPermissionError(tc.subtype, "upstream permission failure").
WithCode(tc.code).
WithLogID("log-id").
WithRetryable().
WithIdentity(string(tc.identity)).
WithMissingScopes(meetingQueryUserScope, meetingQueryBotScope).
WithRequestedScopes("requested:scope").
WithGrantedScopes("granted:scope")
if tc.identity == core.AsBot {
original.ConsoleURL = "https://example.com/scopes"
}
original.Troubleshooter = "https://example.com/troubleshoot"
got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(tc.identity), original)
var pe *errs.PermissionError
if !errors.As(got, &pe) {
t.Fatalf("got %T, want *errs.PermissionError", got)
}
if got != original || pe != original {
t.Fatal("normalizer did not return the original permission error")
}
if pe.Code != tc.code || pe.Subtype != tc.subtype || pe.LogID != "log-id" || !pe.Retryable {
t.Fatalf("diagnostics changed: %+v", pe.Problem)
}
if pe.Troubleshooter != original.Troubleshooter {
t.Fatalf("Troubleshooter = %q, want %q", pe.Troubleshooter, original.Troubleshooter)
}
if pe.Message != wantMessage {
t.Fatalf("Message = %q, want %q", pe.Message, wantMessage)
}
if tc.identity == core.AsBot {
consoleURL, err := url.Parse(pe.ConsoleURL)
if err != nil {
t.Fatalf("ConsoleURL = %q is invalid: %v", pe.ConsoleURL, err)
}
if consoleURL.Host == "" || consoleURL.Query().Get("clientID") != "test-app" || consoleURL.Query().Get("scopes") != meetingQueryBotScope {
t.Fatalf("ConsoleURL = %q, want test-app and only bot scope", pe.ConsoleURL)
}
} else if pe.ConsoleURL != "" {
t.Fatalf("ConsoleURL = %q, user-scope error must not expose a developer-console URL", pe.ConsoleURL)
}
assertMeetingQueryPermissionError(t, got, tc.identity, tc.code)
})
}
}
func TestNormalizeMeetingQueryPermissionError_PassesThroughNonMatchingErrors(t *testing.T) {
cases := []struct {
name string
identity core.Identity
err error
}{
{
name: "user_with_app_scope_error",
identity: core.AsUser,
err: errs.NewPermissionError(errs.SubtypeAppScopeNotApplied, "app scope error").
WithCode(output.LarkErrAppScopeNotEnabled),
},
{
name: "bot_with_user_scope_error",
identity: core.AsBot,
err: errs.NewPermissionError(errs.SubtypeMissingScope, "user scope error").
WithCode(output.LarkErrUserScopeInsufficient),
},
{
name: "auto_with_user_scope_error",
identity: core.AsAuto,
err: errs.NewPermissionError(errs.SubtypeMissingScope, "auto identity").
WithCode(output.LarkErrUserScopeInsufficient),
},
{
name: "bot_not_in_meeting",
err: errs.NewPermissionError(errs.SubtypePermissionDenied, "not in meeting").WithCode(10005),
},
{
name: "not_in_gray",
err: errs.NewPermissionError(errs.SubtypePermissionDenied, "not in gray").
WithCode(20017),
},
{name: "plain_error", err: errors.New("boom")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
identity := tc.identity
if identity == "" {
identity = core.AsBot
}
if got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(identity), tc.err); got != tc.err {
t.Fatalf("got %T %v, want original error %T %v", got, got, tc.err, tc.err)
}
})
}
}

View File

@@ -52,9 +52,13 @@ var VCMeetingEvents = common.Shortcut{
Command: "+meeting-events",
Description: "List meeting events by meeting ID",
Risk: "read",
Scopes: []string{"vc:meeting.meetingevent:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
// UAT exposes user-granted scopes, so the framework can preflight the user
// recommendation. TAT has no scope metadata; keep the bot recommendation
// conditional so it is available to diagnostics without a local preflight.
UserScopes: []string{meetingQueryUserScope},
ConditionalBotScopes: []string{meetingQueryBotScope},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "meeting-id", Required: true, Desc: "meeting ID to query"},
{Name: "start", Desc: "time lower bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"},
@@ -101,7 +105,7 @@ var VCMeetingEvents = common.Shortcut{
}
data, events, hasMore, pageToken, err := fetchMeetingEvents(ctx, runtime, startTime, endTime)
if err != nil {
return err
return normalizeMeetingQueryPermissionError(runtime, err)
}
events = compactMeetingEvents(events)
identity, identityWarning := meetingEventsCurrentIdentity(runtime)

View File

@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"errors"
"net/url"
"reflect"
"strings"
"testing"
@@ -17,6 +18,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)
@@ -418,6 +420,21 @@ func TestMeetingEvents_Validation_PageAllIgnoresInvalidPageSize(t *testing.T) {
}
}
func TestMeetingEvents_UsesUserScopePreflightAndBotScopeHint(t *testing.T) {
if got := VCMeetingEvents.ScopesForIdentity("user"); !reflect.DeepEqual(got, []string{meetingQueryUserScope}) {
t.Fatalf("ScopesForIdentity(user) = %v, want %v", got, []string{meetingQueryUserScope})
}
if got := VCMeetingEvents.ScopesForIdentity("bot"); len(got) != 0 {
t.Fatalf("ScopesForIdentity(bot) = %v, want no bot preflight scopes", got)
}
if got := VCMeetingEvents.DeclaredScopesForIdentity("user"); !reflect.DeepEqual(got, []string{meetingQueryUserScope}) {
t.Fatalf("DeclaredScopesForIdentity(user) = %v, want %v", got, []string{meetingQueryUserScope})
}
if got := VCMeetingEvents.DeclaredScopesForIdentity("bot"); !reflect.DeepEqual(got, []string{meetingQueryBotScope}) {
t.Fatalf("DeclaredScopesForIdentity(bot) = %v, want %v", got, []string{meetingQueryBotScope})
}
}
func TestMeetingEvents_Validation_InvalidPageSizeReturnsFlagError(t *testing.T) {
runtime := newMeetingEventsRuntime()
mustSetMeetingEventsFlag(t, runtime, "meeting-id", "7628568141510692381")
@@ -637,6 +654,63 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
}
}
func TestMeetingEvents_Execute_NormalizesMeetingScopeError(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: vcMeetingEventsAPIPath,
Status: 400,
Body: map[string]interface{}{
"code": output.LarkErrAppScopeNotEnabled,
"msg": "access denied",
"error": map[string]interface{}{
"permission_violations": []interface{}{
map[string]interface{}{"subject": meetingQueryUserScope},
map[string]interface{}{"subject": meetingQueryBotScope},
},
},
},
})
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
"--meeting-id", "7628568141510692381",
"--format", "json",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatal("expected permission error")
}
reg.Verify(t)
var permissionErr *errs.PermissionError
if !errors.As(err, &permissionErr) {
t.Fatalf("error = %T %v, want *errs.PermissionError", err, err)
}
if permissionErr.Code != output.LarkErrAppScopeNotEnabled {
t.Fatalf("Code = %d, want %d", permissionErr.Code, output.LarkErrAppScopeNotEnabled)
}
if permissionErr.Identity != "bot" {
t.Fatalf("Identity = %q, want bot", permissionErr.Identity)
}
wantMessage := "access denied for bot identity; recommended scope: " + meetingQueryBotScope
if permissionErr.Message != wantMessage {
t.Fatalf("Message = %q, want %q", permissionErr.Message, wantMessage)
}
if !strings.Contains(permissionErr.Hint, meetingQueryBotScope) {
t.Fatalf("Hint = %q, want bot scope %q", permissionErr.Hint, meetingQueryBotScope)
}
if len(permissionErr.MissingScopes) != 1 || permissionErr.MissingScopes[0] != meetingQueryBotScope {
t.Fatalf("MissingScopes = %v, want only bot scope %q", permissionErr.MissingScopes, meetingQueryBotScope)
}
if permissionErr.ConsoleURL == "" {
t.Fatal("ConsoleURL is empty, want identity-specific developer-console URL")
}
if strings.Contains(permissionErr.ConsoleURL, url.QueryEscape(meetingQueryUserScope)) || !strings.Contains(permissionErr.ConsoleURL, url.QueryEscape(meetingQueryBotScope)) {
t.Fatalf("ConsoleURL = %q, want only bot scope", permissionErr.ConsoleURL)
}
}
func TestMeetingEvents_ExecuteJSON_BotIdentityErrorDoesNotBlockEvents(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))

View File

@@ -23,9 +23,13 @@ var VCMeetingListActive = common.Shortcut{
Command: "+meeting-list-active",
Description: "List active meetings for the current identity or target user",
Risk: "read",
Scopes: []string{"vc:meeting.meetingevent:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
// UAT exposes user-granted scopes, so the framework can preflight the user
// recommendation. TAT has no scope metadata; keep the bot recommendation
// conditional so it is available to diagnostics without a local preflight.
UserScopes: []string{meetingQueryUserScope},
ConditionalBotScopes: []string{meetingQueryBotScope},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "user-id", Desc: "target user ID when using bot identity"},
},
@@ -50,7 +54,7 @@ var VCMeetingListActive = common.Shortcut{
}
data, err := runtime.CallAPITyped(http.MethodGet, vcMeetingListActiveAPIPath, params, nil)
if err != nil {
return err
return normalizeMeetingQueryPermissionError(runtime, err)
}
if data == nil {
data = map[string]interface{}{}

View File

@@ -608,9 +608,18 @@ func TestMeetingListActive_DryRun_UserIdentity(t *testing.T) {
}
}
func TestMeetingListActive_ScopeMatchesEventReadPermission(t *testing.T) {
if len(VCMeetingListActive.Scopes) != 1 || VCMeetingListActive.Scopes[0] != "vc:meeting.meetingevent:read" {
t.Fatalf("scopes = %#v, want [vc:meeting.meetingevent:read]", VCMeetingListActive.Scopes)
func TestMeetingListActive_UsesUserScopePreflightAndBotScopeHint(t *testing.T) {
if got := VCMeetingListActive.ScopesForIdentity("user"); len(got) != 1 || got[0] != meetingQueryUserScope {
t.Fatalf("ScopesForIdentity(user) = %v, want [%s]", got, meetingQueryUserScope)
}
if got := VCMeetingListActive.ScopesForIdentity("bot"); len(got) != 0 {
t.Fatalf("ScopesForIdentity(bot) = %v, want no bot preflight scopes", got)
}
if got := VCMeetingListActive.DeclaredScopesForIdentity("user"); len(got) != 1 || got[0] != meetingQueryUserScope {
t.Fatalf("DeclaredScopesForIdentity(user) = %v, want [%s]", got, meetingQueryUserScope)
}
if got := VCMeetingListActive.DeclaredScopesForIdentity("bot"); len(got) != 1 || got[0] != meetingQueryBotScope {
t.Fatalf("DeclaredScopesForIdentity(bot) = %v, want [%s]", got, meetingQueryBotScope)
}
}

View File

@@ -21,6 +21,13 @@ metadata:
- 如果 `lark-cli` 提示 `missing required scope(s)` / `missing_scopes`,不要走普通权限申请流程;先提示用户加入早鸟群确认内测权限已开通,再按“应用身份权限配置检查”处理应用权限、安装和数据范围。
- 如果 `lark-cli` 返回 `error.code=20017` / `ErrNotInGray`,提示用户加入早鸟群:`https://go.larkoffice.com/join-chat/2f4nb0e1-fe00-4f67-bed7-25beaf533fbd`
## 会议查询权限
`+meeting-list-active``+meeting-events` 缺少权限时,先按上面的内测提示确认功能已开通,再读取 CLI 错误中的 `hint`,并根据当前调用身份处理:
- 用户身份 `--as user`:按 CLI 提示为当前用户授权 `vc:meeting.meetingevent:read`
- 应用身份 `--as bot`:请应用开发者开通 `vc:meeting.bot.join:write`,不要执行 `auth login`;随后按“应用身份权限配置检查”确认应用发布、安装和数据范围。
## 定位
本 skill 与 [`lark-vc`](../lark-vc/SKILL.md) 并列:
@@ -172,7 +179,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
应用身份 `--as bot``no permission``missing required scope(s)``missing_scopes``ErrNotInGray``20017` 时,不要引导用户执行 `auth login`。按顺序检查:
1. 以 CLI 返回的 metadata / error envelope 为准,确认提示的 VC Agent 相关权限已开通。常见读取 active meeting / events 需要会中事件读取权限;应用机器人入会 / 离会需要 bot 入会写权限
1. 确认内测权限后,按 CLI 错误中的 `hint` 处理;返回 `console_url` 时将其原样提供给用户
2. 应用已发布并安装到当前租户。
3. 开放平台“权限可访问的数据范围”已开通并保存。
4. 数据范围选择“按条件筛选”,条件配置为:**会议的归属者 包含 与应用的可用范围一致**。
@@ -180,7 +187,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
## 用户身份被拒绝时
用户身份 `--as user` 报权限或身份不支持类错误时,不要反复引导用户执行 `auth login`。先以 CLI 返回的 metadata / error envelope 为准判断:如果错误表明当前接口不支持用户身份访问,再按用户意图切换处理:
用户身份 `--as user` 调用 `+meeting-list-active``+meeting-events` 报普通 scope 缺失时,按“会议查询权限”处理;其他 shortcut 的 scope 缺失按各自 CLI `hint` 处理。普通 scope 缺失不表示接口不支持用户身份,只有 CLI 明确表明当前接口不支持用户身份访问时,才按用户意图切换处理:
1. 如果用户只是查询当前登录用户所在的进行中会议,说明当前接口链路不支持用户身份访问,改用应用身份流程;需要目标用户 open_id并要求应用机器人已在会中或先按用户确认执行入会。
2. 如果用户明确要求应用机器人入会、旁听、代参会或读取应用机器人可见事件,直接切到 `--as bot`,并按上面的应用身份权限配置检查处理。

View File

@@ -290,7 +290,7 @@ lark-cli vc +meeting-events \
| 用户身份无权限 / 不可见 | 当前用户不是该会议的可见参与者,或 `meeting_id` 不是从用户身份路径获得 | 不要反复执行 `auth login`。先确认 `meeting_id` 是否来自 `+meeting-list-active --as user`;如果用户明确要切到应用身份,再通过 `+meeting-list-active --as bot --user-id <user_open_id>` 获取应用身份可读的 `meeting_id`,或在用户明确同意后让应用机器人入会,再用 `+meeting-events --as bot` 读取 |
| `20001 meeting_status_MEETING_END` | 会议已结束且已超出后端允许的 5 分钟宽限窗口 | 本接口不再适合继续拉取事件。先用 `lark-cli vc +detail --meeting-ids <meeting.id>` 获取会议产物信息,再根据 `note_display_type` / `note_id` / `minute_token` 和用户意图选择纪要正文、逐字稿或妙记;参会人请用 `lark-cli vc meeting get --params '{"meeting_id":"<meeting.id>"}' --with-participants` |
| `20002 meeting not exist` | `meeting_id` 错误,或会议实例当前已不可获取(常见于把 9 位会议号当 meeting_id 传) | 确认传入的是长数字 `meeting_id`,不是 9 位会议号 |
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 |
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`请应用开发者开通 `vc:meeting.bot.join:write`;再检查应用发布/安装权限可访问的数据范围,均正确仍失败再排查内测灰度权限 |
| `HTTP 404` / `HTTP 500` | 服务端当前无法找到或处理该会议实例 | 换一个正在进行且 bot 可见的 meeting_id或排查后端问题 |
## 提示

View File

@@ -80,10 +80,10 @@ lark-cli vc +meeting-list-active --as bot --user-id <user_open_id> --format json
|---------|---------|---------|
| `--user-id is required when --as bot` | 应用身份未传目标用户 | 传入目标用户 open_id |
| 用户身份返回空列表 | 当前登录用户没有可见的进行中会议 | 确认用户是否在会中,或是否切错身份 |
| 用户身份无权限 / 不可见 | 当前登录用户没有可见的进行中会议,或当前身份无法读取该会议 | 不要反复执行 `auth login`确认当前登录用户是否在会中、是否切错 profile如果用户明确要查询应用机器人可见的会议,再拿目标用户 open_id 执行 `+meeting-list-active --as bot --user-id <user_open_id>`,并按应用身份权限配置检查应用权限、安装、数据范围和灰度 |
| 用户身份无权限 / 不可见 | 当前登录用户没有可见的进行中会议,或当前身份无法读取该会议 | 不要反复执行 `auth login`。确认用户是否在会中、是否切错 profile用户明确要查询应用机器人可见的会议,再拿目标用户 open_id 执行 `+meeting-list-active --as bot --user-id <user_open_id>` |
| 应用身份返回空列表 | 没有满足“目标用户在会中且应用机器人也在会中”的当前会 | 先让应用机器人入会,或确认 `user_id` 和会议状态 |
| `--user-id` 格式错误 | 传入了 internal user_id 或其他非 `ou_...` 值 | 改传目标用户 open_id |
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 |
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`请应用开发者开通 `vc:meeting.bot.join:write`;再检查应用发布/安装权限可访问的数据范围,均正确仍失败再排查内测灰度权限 |
## 参考