From 4710a294f5868a70f9eacb507e75bfe2774ebca2 Mon Sep 17 00:00:00 2001 From: liangshuo-1 Date: Tue, 2 Jun 2026 16:10:35 +0800 Subject: [PATCH 1/4] refactor(transport): own all HTTP transport in internal/transport, fix util layering inversion (#1213) internal/util imported internal/proxyplugin (SharedTransport, FallbackTransport, NewHTTPClient, and WarnIfProxied via proxyPluginStatus), so a foundational util package depended up into a feature package, pulling binding/core/vfs into the transitive cone of every util importer. Move internal/proxyplugin -> internal/transport and make it the single owner of outbound transport: fold the two SharedTransport functions into one Shared() (proxy-plugin override -> LARK_CLI_NO_PROXY -> http.DefaultTransport), and move Fallback/NewHTTPClient/WarnIfProxied/DetectProxyEnv/noProxyTransport out of the now-deleted internal/util/proxy.go into the new package. The proxy-plugin probe is demoted to a private pluginTransport(); the duplicate redactProxyURL collapses to one. internal/util keeps no proxy code and is a leaf again. Re-point all consumers (registry, doctor, config, auth, cmdutil, update) to internal/transport. Behavior-preserving: package move + symbol rename + dedup. Two new tests lock the fail-closed contract (plugin overrides NO_PROXY; malformed config never falls through to direct egress). --- cmd/config/init_interactive.go | 4 +- cmd/doctor/doctor.go | 4 +- internal/auth/transport.go | 4 +- internal/cmdutil/factory_default.go | 20 +- internal/cmdutil/transport.go | 10 +- internal/cmdutil/transport_test.go | 2 +- internal/registry/remote.go | 4 +- internal/{proxyplugin => transport}/config.go | 30 +-- .../{proxyplugin => transport}/config_test.go | 2 +- internal/transport/shared.go | 83 ++++++++ internal/transport/shared_test.go | 156 +++++++++++++++ internal/{proxyplugin => transport}/tls_ca.go | 2 +- .../{proxyplugin => transport}/tls_ca_test.go | 2 +- .../{proxyplugin => transport}/transport.go | 12 +- .../transport_test.go | 52 ++--- internal/transport/warn.go | 104 ++++++++++ .../proxy_test.go => transport/warn_test.go} | 137 +------------ internal/update/update.go | 4 +- internal/util/proxy.go | 180 ------------------ 19 files changed, 420 insertions(+), 392 deletions(-) rename internal/{proxyplugin => transport}/config.go (90%) rename internal/{proxyplugin => transport}/config_test.go (99%) create mode 100644 internal/transport/shared.go create mode 100644 internal/transport/shared_test.go rename internal/{proxyplugin => transport}/tls_ca.go (99%) rename internal/{proxyplugin => transport}/tls_ca_test.go (99%) rename internal/{proxyplugin => transport}/transport.go (89%) rename internal/{proxyplugin => transport}/transport_test.go (76%) create mode 100644 internal/transport/warn.go rename internal/{util/proxy_test.go => transport/warn_test.go} (63%) delete mode 100644 internal/util/proxy.go diff --git a/cmd/config/init_interactive.go b/cmd/config/init_interactive.go index 1d1592755..0f17000a8 100644 --- a/cmd/config/init_interactive.go +++ b/cmd/config/init_interactive.go @@ -16,7 +16,7 @@ import ( "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/output" - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" ) // configInitResult holds the result of the interactive config init flow. @@ -179,7 +179,7 @@ func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride cor // Step 1: Request app registration (begin) // Use the shared proxy-plugin-aware transport so registration traffic is not // a bypass of proxy plugin mode. - httpClient := util.NewHTTPClient(0) + httpClient := transport.NewHTTPClient(0) authResp, err := larkauth.RequestAppRegistration(httpClient, larkBrand, f.IOStreams.ErrOut) if err != nil { return nil, errs.NewConfigError(errs.SubtypeInvalidClient, "app registration failed: %v", err).WithCause(err) diff --git a/cmd/doctor/doctor.go b/cmd/doctor/doctor.go index f96cdb6a6..2a5265b12 100644 --- a/cmd/doctor/doctor.go +++ b/cmd/doctor/doctor.go @@ -19,8 +19,8 @@ import ( "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/identitydiag" "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/internal/transport" "github.com/larksuite/cli/internal/update" - "github.com/larksuite/cli/internal/util" ) // DoctorOptions holds inputs for the doctor command. @@ -155,7 +155,7 @@ func networkChecks(ctx context.Context, opts *DoctorOptions, ep core.Endpoints) // Use the shared proxy-plugin-aware transport so connectivity checks reflect // the real egress path (and are blocked when proxy plugin fails closed). - httpClient := util.NewHTTPClient(0) + httpClient := transport.NewHTTPClient(0) mcpURL := ep.MCP + "/mcp" type probeResult struct { diff --git a/internal/auth/transport.go b/internal/auth/transport.go index b6d422899..95b2c6cd4 100644 --- a/internal/auth/transport.go +++ b/internal/auth/transport.go @@ -14,7 +14,7 @@ import ( "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/errclass" - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" ) // SecurityPolicyTransport is an http.RoundTripper that intercepts all responses @@ -28,7 +28,7 @@ func (t *SecurityPolicyTransport) base() http.RoundTripper { if t.Base != nil { return t.Base } - return util.FallbackTransport() + return transport.Fallback() } // RoundTrip implements http.RoundTripper. diff --git a/internal/cmdutil/factory_default.go b/internal/cmdutil/factory_default.go index f18a816b3..514aaf93f 100644 --- a/internal/cmdutil/factory_default.go +++ b/internal/cmdutil/factory_default.go @@ -23,7 +23,7 @@ import ( "github.com/larksuite/cli/internal/keychain" "github.com/larksuite/cli/internal/registry" _ "github.com/larksuite/cli/internal/security/contentsafety" // register content safety provider - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" _ "github.com/larksuite/cli/internal/vfs/localfileio" // register default FileIO provider ) @@ -102,15 +102,15 @@ func safeRedirectPolicy(req *http.Request, via []*http.Request) error { func cachedHttpClientFunc(f *Factory) func() (*http.Client, error) { return sync.OnceValues(func() (*http.Client, error) { - util.WarnIfProxied(f.IOStreams.ErrOut) + transport.WarnIfProxied(f.IOStreams.ErrOut) - var transport http.RoundTripper = util.SharedTransport() - transport = &RetryTransport{Base: transport} - transport = &SecurityHeaderTransport{Base: transport} - transport = &auth.SecurityPolicyTransport{Base: transport} // Add our global response interceptor - transport = wrapWithExtension(transport) + var rt http.RoundTripper = transport.Shared() + rt = &RetryTransport{Base: rt} + rt = &SecurityHeaderTransport{Base: rt} + rt = &auth.SecurityPolicyTransport{Base: rt} // Add our global response interceptor + rt = wrapWithExtension(rt) client := &http.Client{ - Transport: transport, + Transport: rt, Timeout: 30 * time.Second, CheckRedirect: safeRedirectPolicy, } @@ -129,7 +129,7 @@ func cachedLarkClientFunc(f *Factory) func() (*lark.Client, error) { lark.WithLogLevel(larkcore.LogLevelError), lark.WithHeaders(BaseSecurityHeaders()), } - util.WarnIfProxied(f.IOStreams.ErrOut) + transport.WarnIfProxied(f.IOStreams.ErrOut) opts = append(opts, lark.WithHttpClient(&http.Client{ Transport: buildSDKTransport(), CheckRedirect: safeRedirectPolicy, @@ -141,7 +141,7 @@ func cachedLarkClientFunc(f *Factory) func() (*lark.Client, error) { } func buildSDKTransport() http.RoundTripper { - var sdkTransport http.RoundTripper = util.SharedTransport() + var sdkTransport http.RoundTripper = transport.Shared() sdkTransport = &RetryTransport{Base: sdkTransport} sdkTransport = &UserAgentTransport{Base: sdkTransport} sdkTransport = &BuildHeaderTransport{Base: sdkTransport} diff --git a/internal/cmdutil/transport.go b/internal/cmdutil/transport.go index ccb661194..351eeb853 100644 --- a/internal/cmdutil/transport.go +++ b/internal/cmdutil/transport.go @@ -9,7 +9,7 @@ import ( "time" exttransport "github.com/larksuite/cli/extension/transport" - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" ) // RetryTransport is an http.RoundTripper that retries on 5xx responses @@ -24,7 +24,7 @@ func (t *RetryTransport) base() http.RoundTripper { if t.Base != nil { return t.Base } - return util.FallbackTransport() + return transport.Fallback() } func (t *RetryTransport) delay() time.Duration { @@ -69,7 +69,7 @@ func (t *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error if t.Base != nil { return t.Base.RoundTrip(req) } - return util.FallbackTransport().RoundTrip(req) + return transport.Fallback().RoundTrip(req) } // BuildHeaderTransport is an http.RoundTripper that force-writes the @@ -87,7 +87,7 @@ func (t *BuildHeaderTransport) RoundTrip(req *http.Request) (*http.Response, err if t.Base != nil { return t.Base.RoundTrip(req) } - return util.FallbackTransport().RoundTrip(req) + return transport.Fallback().RoundTrip(req) } // SecurityHeaderTransport is an http.RoundTripper that injects CLI security @@ -100,7 +100,7 @@ func (t *SecurityHeaderTransport) base() http.RoundTripper { if t.Base != nil { return t.Base } - return util.FallbackTransport() + return transport.Fallback() } // RoundTrip implements http.RoundTripper. diff --git a/internal/cmdutil/transport_test.go b/internal/cmdutil/transport_test.go index f76b0c326..9cd061021 100644 --- a/internal/cmdutil/transport_test.go +++ b/internal/cmdutil/transport_test.go @@ -332,7 +332,7 @@ func TestBuildHeaderTransport_OverridesEvenWithoutTamper(t *testing.T) { // TestBuildHeaderTransport_NilBase_UsesFallback verifies that when Base is nil, // the transport still sets X-Cli-Build and routes the request through -// util.FallbackTransport rather than panicking. This covers the fallback +// transport.Fallback rather than panicking. This covers the fallback // branch in RoundTrip that is otherwise unreachable with a non-nil Base. func TestBuildHeaderTransport_NilBase_UsesFallback(t *testing.T) { var receivedBuild string diff --git a/internal/registry/remote.go b/internal/registry/remote.go index 4bc9d6a8d..e13ec66d0 100644 --- a/internal/registry/remote.go +++ b/internal/registry/remote.go @@ -17,7 +17,7 @@ import ( "github.com/larksuite/cli/internal/build" "github.com/larksuite/cli/internal/core" - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/internal/vfs" ) @@ -181,7 +181,7 @@ func saveCachedMerged(data []byte, meta CacheMeta) error { func fetchRemoteMerged(localVersion string) (data []byte, reg *MergedRegistry, err error) { // Route through the shared proxy-plugin-aware transport so remote API // definition fetches honor proxy plugin mode instead of bypassing it. - client := util.NewHTTPClient(fetchTimeout) + client := transport.NewHTTPClient(fetchTimeout) req, err := http.NewRequest("GET", remoteMetaURL(localVersion), nil) if err != nil { return nil, nil, err diff --git a/internal/proxyplugin/config.go b/internal/transport/config.go similarity index 90% rename from internal/proxyplugin/config.go rename to internal/transport/config.go index 46c160d14..009e5f2c8 100644 --- a/internal/proxyplugin/config.go +++ b/internal/transport/config.go @@ -1,14 +1,15 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -// Package proxyplugin implements the ~/.lark-cli/proxy_config.json based security proxy plugin mode. +// Package transport owns how the CLI assembles its outbound HTTP transport: the +// shared base RoundTripper (Shared/Fallback/NewHTTPClient), the LARK_CLI_NO_PROXY +// direct-egress clone, and the ~/.lark-cli/proxy_config.json proxy-plugin mode. // -// It supports: -// - forcing all outbound HTTP(S) requests through a fixed HTTP proxy -// - trusting an additional root CA PEM bundle for MITM/inspection proxies -// -// Environment variables override matching values from proxy_config.json. -package proxyplugin +// Proxy-plugin mode forces all outbound HTTP(S) requests through a fixed loopback +// proxy, optionally trusting an extra root CA PEM bundle for TLS-inspection +// proxies, and fails closed on misconfiguration. Environment variables override +// matching values from proxy_config.json. +package transport import ( "encoding/json" @@ -222,21 +223,6 @@ func (c *Config) proxyURL() (*url.URL, error) { return u, nil } -// redactProxyURL masks userinfo (username:password) in a proxy URL. -// Handles both scheme-prefixed ("http://user:pass@host") and bare formats. -func redactProxyURL(raw string) string { - u, err := url.Parse(raw) - if err == nil && u.User != nil { - u.User = url.User("***") - return u.String() - } - // Fallback: handle "user:pass@proxy:8080" - if at := strings.LastIndex(raw, "@"); at > 0 { - return "***@" + raw[at+1:] - } - return raw -} - // ApplyToTransport clones base and applies proxy plugin settings to the clone. // Caller owns the returned *http.Transport. func (c *Config) ApplyToTransport(base *http.Transport) (*http.Transport, error) { diff --git a/internal/proxyplugin/config_test.go b/internal/transport/config_test.go similarity index 99% rename from internal/proxyplugin/config_test.go rename to internal/transport/config_test.go index c17c3a49e..24159950a 100644 --- a/internal/proxyplugin/config_test.go +++ b/internal/transport/config_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package proxyplugin +package transport import ( "net/http" diff --git a/internal/transport/shared.go b/internal/transport/shared.go new file mode 100644 index 000000000..9fb7041d5 --- /dev/null +++ b/internal/transport/shared.go @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package transport + +import ( + "net/http" + "os" + "sync" + "time" +) + +// Shared returns the base http.RoundTripper for all CLI HTTP clients. +// +// Precedence (highest first): +// 1. proxy-plugin mode — force traffic through a fixed loopback proxy; +// FAIL-CLOSED when the plugin config exists but is invalid. +// 2. LARK_CLI_NO_PROXY — direct egress, proxy disabled. +// 3. http.DefaultTransport — the stdlib process-wide singleton (honors +// HTTP(S)_PROXY), so every client shares one connection pool / TLS cache. +// +// The returned RoundTripper MUST NOT be mutated. Callers that need a customized +// transport should assert to *http.Transport and Clone() it. A shared base is +// required so persistConn read/write goroutines are reused; cloning per call +// leaks them until IdleConnTimeout (~90s) fires. +func Shared() http.RoundTripper { + // Proxy-plugin mode overrides everything, INCLUDING LARK_CLI_NO_PROXY. When + // the plugin config exists but is invalid, pluginTransport returns a + // fail-closed transport with ok=true and we return it here — we MUST NOT + // fall through to the NO_PROXY / DefaultTransport direct-egress paths below. + if t, ok := pluginTransport(); ok { + return t + } + if os.Getenv(EnvNoProxy) != "" { + return noProxyTransport() + } + return http.DefaultTransport +} + +// Fallback returns a shared *http.Transport. It is a thin wrapper over Shared +// retained so modules already on the leak-free singleton path (internal/auth, +// internal/cmdutil transport decorators) do not have to migrate. New code +// should prefer Shared and treat the base as an http.RoundTripper. +// +// Fail-closed invariant: pluginTransport always expresses its blocked transport +// as a concrete *http.Transport (see failClosedTransport), so the assertion +// below preserves the block. The noProxyTransport() fallback is therefore only +// reached when no proxy plugin is configured and some external code replaced +// http.DefaultTransport with a non-*http.Transport — a case with no fail-closed +// intent, where a proxy-disabled transport is acceptable. +func Fallback() *http.Transport { + if t, ok := Shared().(*http.Transport); ok { + return t + } + return noProxyTransport() +} + +// NewHTTPClient returns an *http.Client whose Transport is the shared, +// proxy-plugin-aware base (see Shared). Prefer this over a bare &http.Client{} +// for outbound requests: a bare client falls back to http.DefaultTransport and +// therefore silently bypasses proxy plugin mode (fixed proxy + trusted CA, or +// fail-closed), creating an audit blind spot. +// +// A zero timeout means no client-level timeout (callers relying on context +// deadlines pass 0). +func NewHTTPClient(timeout time.Duration) *http.Client { + return &http.Client{ + Transport: Shared(), + Timeout: timeout, + } +} + +// noProxyTransport is a proxy-disabled clone of http.DefaultTransport, lazily +// built the first time LARK_CLI_NO_PROXY is observed set. +var noProxyTransport = sync.OnceValue(func() *http.Transport { + def, ok := http.DefaultTransport.(*http.Transport) + if !ok { + return &http.Transport{} + } + t := def.Clone() + t.Proxy = nil + return t +}) diff --git a/internal/transport/shared_test.go b/internal/transport/shared_test.go new file mode 100644 index 000000000..fe960034d --- /dev/null +++ b/internal/transport/shared_test.go @@ -0,0 +1,156 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package transport + +import ( + "net/http" + "net/url" + "testing" + "time" +) + +// TestShared_DefaultReturnsStdlibSingleton verifies the default shared transport. +func TestShared_DefaultReturnsStdlibSingleton(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + t.Setenv(EnvNoProxy, "") + if Shared() != http.DefaultTransport { + t.Error("Shared should return http.DefaultTransport when LARK_CLI_NO_PROXY is unset") + } +} + +// TestShared_NoProxyReturnsClone verifies that disabling proxying returns a cloned transport. +func TestShared_NoProxyReturnsClone(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + t.Setenv(EnvNoProxy, "1") + tr := Shared() + if tr == http.DefaultTransport { + t.Fatal("Shared should return a clone, not DefaultTransport, when LARK_CLI_NO_PROXY is set") + } + ht, ok := tr.(*http.Transport) + if !ok { + t.Fatalf("expected *http.Transport, got %T", tr) + } + if ht.Proxy != nil { + t.Error("no-proxy transport should have Proxy == nil") + } +} + +// TestShared_NoProxyIsCachedSingleton verifies singleton caching for the no-proxy transport. +func TestShared_NoProxyIsCachedSingleton(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + t.Setenv(EnvNoProxy, "1") + if Shared() != Shared() { + t.Error("repeated Shared calls with LARK_CLI_NO_PROXY set must return the same instance") + } +} + +// TestShared_EnvUnsetAfterSetFallsBackToDefault verifies fallback to the stdlib +// transport after unsetting LARK_CLI_NO_PROXY. +func TestShared_EnvUnsetAfterSetFallsBackToDefault(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + // Simulate a process that first runs with LARK_CLI_NO_PROXY=1 (populating + // the no-proxy singleton), then unsets it. Subsequent calls must return + // http.DefaultTransport, NOT the cached no-proxy clone. + t.Setenv(EnvNoProxy, "1") + if Shared() == http.DefaultTransport { + t.Fatal("precondition: first call with env set should not return DefaultTransport") + } + + t.Setenv(EnvNoProxy, "") + if after := Shared(); after != http.DefaultTransport { + t.Errorf("after unsetting LARK_CLI_NO_PROXY, Shared must return http.DefaultTransport, got %T", after) + } +} + +// TestShared_NoProxyOverridesSystemProxy verifies that LARK_CLI_NO_PROXY disables system proxies. +func TestShared_NoProxyOverridesSystemProxy(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + t.Setenv("HTTPS_PROXY", "http://should-be-ignored:8888") + t.Setenv(EnvNoProxy, "1") + + ht, ok := Shared().(*http.Transport) + if !ok { + t.Fatalf("expected *http.Transport, got %T", Shared()) + } + if ht.Proxy != nil { + t.Error("LARK_CLI_NO_PROXY should override system proxy settings") + } +} + +// TestNewHTTPClient verifies the factory wires the shared proxy-plugin-aware +// transport (instead of a bare client that bypasses proxy plugin mode). +func TestNewHTTPClient(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + resetProxyPluginState() + t.Setenv(EnvNoProxy, "") + + c := NewHTTPClient(7 * time.Second) + if c.Transport == nil { + t.Fatal("NewHTTPClient transport is nil; want shared transport") + } + if c.Transport != Shared() { + t.Errorf("NewHTTPClient transport = %v, want Shared()", c.Transport) + } + if c.Timeout != 7*time.Second { + t.Errorf("NewHTTPClient timeout = %v, want 7s", c.Timeout) + } +} + +// TestShared_PluginOverridesNoProxy locks the contract that proxy-plugin mode wins +// over LARK_CLI_NO_PROXY: even with NO_PROXY set, an enabled plugin forces the proxy. +func TestShared_PluginOverridesNoProxy(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + t.Setenv(EnvNoProxy, "1") // NO_PROXY set, but the plugin must win + resetProxyPluginState() + + writeFile(t, Path(), []byte(`{ + "LARKSUITE_CLI_PROXY_ENABLE": true, + "LARKSUITE_CLI_PROXY_ADDRESS": "http://127.0.0.1:3128" +}`), 0600) + + tr, ok := Shared().(*http.Transport) + if !ok { + t.Fatalf("Shared() = %T, want proxy *http.Transport, not the NO_PROXY clone", tr) + } + u, err := tr.Proxy(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}}) + if err != nil || u == nil || u.String() != "http://127.0.0.1:3128" { + t.Fatalf("Proxy() = %v, %v; plugin must override NO_PROXY with the fixed proxy", u, err) + } +} + +// TestShared_MalformedConfigFailsClosedEvenWithNoProxy locks the most dangerous +// invariant of the fold: a malformed proxy_config.json must FAIL CLOSED, never +// fall through to direct egress — not even to the LARK_CLI_NO_PROXY clone. +func TestShared_MalformedConfigFailsClosedEvenWithNoProxy(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + unsetProxyPluginEnv(t) + t.Setenv(EnvNoProxy, "1") + resetProxyPluginState() + + writeFile(t, Path(), []byte(`{`), 0600) // malformed + + rt := Shared() + if rt == http.DefaultTransport { + t.Fatal("malformed config returned http.DefaultTransport — fail OPEN") + } + if rt == noProxyTransport() { + t.Fatal("malformed config fell through to the NO_PROXY direct-egress clone — fail OPEN") + } + resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}}) + if err == nil { + t.Fatalf("RoundTrip() err = nil (resp=%v); malformed config must fail closed", resp) + } +} diff --git a/internal/proxyplugin/tls_ca.go b/internal/transport/tls_ca.go similarity index 99% rename from internal/proxyplugin/tls_ca.go rename to internal/transport/tls_ca.go index c55dd7963..079cb6f1d 100644 --- a/internal/proxyplugin/tls_ca.go +++ b/internal/transport/tls_ca.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package proxyplugin +package transport import ( "crypto/tls" diff --git a/internal/proxyplugin/tls_ca_test.go b/internal/transport/tls_ca_test.go similarity index 99% rename from internal/proxyplugin/tls_ca_test.go rename to internal/transport/tls_ca_test.go index 31e25c23b..9fd1b9e7e 100644 --- a/internal/proxyplugin/tls_ca_test.go +++ b/internal/transport/tls_ca_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package proxyplugin +package transport import ( "crypto/rand" diff --git a/internal/proxyplugin/transport.go b/internal/transport/transport.go similarity index 89% rename from internal/proxyplugin/transport.go rename to internal/transport/transport.go index 9179c468f..88b2b1164 100644 --- a/internal/proxyplugin/transport.go +++ b/internal/transport/transport.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package proxyplugin +package transport import ( "fmt" @@ -16,7 +16,7 @@ var proxyPluginTransport = sync.OnceValue(buildProxyPluginTransport) // cachedBlockedTransport is a fail-closed transport cached on first use when // the proxy plugin config exists but is invalid. This avoids cloning -// http.DefaultTransport on every SharedTransport call. +// http.DefaultTransport on every pluginTransport call. var cachedBlockedTransport = sync.OnceValue(buildBlockedTransport) func buildBlockedTransport() http.RoundTripper { @@ -28,7 +28,7 @@ func buildProxyPluginTransport() http.RoundTripper { if !ok { // Cannot clone the stdlib transport. Fail closed with a concrete // *http.Transport (not a bare RoundTripper) so downcasting callers such - // as util.FallbackTransport cannot silently degrade this into a + // as Fallback cannot silently degrade this into a // direct-egress transport. return failClosedTransport(fmt.Errorf("proxy plugin transport unavailable: http.DefaultTransport is %T, want *http.Transport", http.DefaultTransport)) } @@ -51,9 +51,9 @@ func buildProxyPluginTransport() http.RoundTripper { return t } -// SharedTransport returns the proxy plugin transport when proxy plugin mode is +// pluginTransport returns the proxy plugin transport when proxy plugin mode is // configured. The bool return is false when the plugin is not configured or not enabled. -func SharedTransport() (http.RoundTripper, bool) { +func pluginTransport() (http.RoundTripper, bool) { cfg, err := Load() if err != nil { return cachedBlockedTransport(), true @@ -68,7 +68,7 @@ func SharedTransport() (http.RoundTripper, bool) { // err. It clones http.DefaultTransport when possible (preserving dial/timeout // tuning); otherwise it builds a minimal transport. Returning a concrete // *http.Transport (rather than a bare RoundTripper) is required so downcasting -// callers such as util.FallbackTransport cannot silently degrade a fail-closed +// callers such as Fallback cannot silently degrade a fail-closed // signal into a direct-egress transport. func failClosedTransport(err error) *http.Transport { if def, ok := http.DefaultTransport.(*http.Transport); ok { diff --git a/internal/proxyplugin/transport_test.go b/internal/transport/transport_test.go similarity index 76% rename from internal/proxyplugin/transport_test.go rename to internal/transport/transport_test.go index c4414f8b5..ad6d8987b 100644 --- a/internal/proxyplugin/transport_test.go +++ b/internal/transport/transport_test.go @@ -1,7 +1,7 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package proxyplugin +package transport import ( "io" @@ -20,21 +20,21 @@ func resetProxyPluginState() { cachedBlockedTransport = sync.OnceValue(buildBlockedTransport) } -func TestSharedTransport_NotConfigured(t *testing.T) { +func TestPluginTransport_NotConfigured(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) resetProxyPluginState() - tr, ok := SharedTransport() + tr, ok := pluginTransport() if ok { - t.Fatalf("SharedTransport() ok = true, want false") + t.Fatalf("pluginTransport() ok = true, want false") } if tr != nil { - t.Fatalf("SharedTransport() transport = %T, want nil", tr) + t.Fatalf("pluginTransport() transport = %T, want nil", tr) } } -func TestSharedTransport_EnabledReturnsFixedProxy(t *testing.T) { +func TestPluginTransport_EnabledReturnsFixedProxy(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) resetProxyPluginState() @@ -46,13 +46,13 @@ func TestSharedTransport_EnabledReturnsFixedProxy(t *testing.T) { "LARKSUITE_CLI_CA_PATH": "" }`), 0600) - rt, ok := SharedTransport() + rt, ok := pluginTransport() if !ok { - t.Fatal("SharedTransport() ok = false, want true") + t.Fatal("pluginTransport() ok = false, want true") } tr, ok := rt.(*http.Transport) if !ok { - t.Fatalf("SharedTransport() = %T, want *http.Transport", rt) + t.Fatalf("pluginTransport() = %T, want *http.Transport", rt) } u, err := tr.Proxy(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}}) if err != nil { @@ -63,7 +63,7 @@ func TestSharedTransport_EnabledReturnsFixedProxy(t *testing.T) { } } -func TestSharedTransport_InvalidConfigWithNonTransportDefaultFailsClosed(t *testing.T) { +func TestPluginTransport_InvalidConfigWithNonTransportDefaultFailsClosed(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) resetProxyPluginState() @@ -72,12 +72,12 @@ func TestSharedTransport_InvalidConfigWithNonTransportDefaultFailsClosed(t *test writeFile(t, Path(), []byte(`{`), 0600) - rt, ok := SharedTransport() + rt, ok := pluginTransport() if !ok { - t.Fatal("SharedTransport() ok = false, want true") + t.Fatal("pluginTransport() ok = false, want true") } if rt == http.DefaultTransport { - t.Fatalf("SharedTransport() returned http.DefaultTransport, want fail-closed transport") + t.Fatalf("pluginTransport() returned http.DefaultTransport, want fail-closed transport") } resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}}) if err == nil { @@ -88,23 +88,23 @@ func TestSharedTransport_InvalidConfigWithNonTransportDefaultFailsClosed(t *test } } -func TestSharedTransport_InvalidConfigReturnsCachedInstance(t *testing.T) { +func TestPluginTransport_InvalidConfigReturnsCachedInstance(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) resetProxyPluginState() writeFile(t, Path(), []byte(`{`), 0600) - a, ok := SharedTransport() + a, ok := pluginTransport() if !ok { - t.Fatal("SharedTransport() ok = false, want true") + t.Fatal("pluginTransport() ok = false, want true") } - b, ok := SharedTransport() + b, ok := pluginTransport() if !ok { - t.Fatal("SharedTransport() ok = false, want true") + t.Fatal("pluginTransport() ok = false, want true") } if a != b { - t.Fatalf("SharedTransport() returned different instances on repeated calls; blocked transport must be cached") + t.Fatalf("pluginTransport() returned different instances on repeated calls; blocked transport must be cached") } } @@ -148,13 +148,13 @@ func TestBuildProxyPluginTransport_NonTransportDefaultFailsClosed(t *testing.T) } } -// TestSharedTransport_InvalidConfigBlockerIsConcreteTransport guards the -// fail-closed invariant that util.FallbackTransport relies on: even when +// TestPluginTransport_InvalidConfigBlockerIsConcreteTransport guards the +// fail-closed invariant that Fallback relies on: even when // http.DefaultTransport is not an *http.Transport, an invalid proxy config must // produce a blocked transport that is itself a concrete *http.Transport. If it -// were a bare RoundTripper, util.FallbackTransport would downcast-fail and +// were a bare RoundTripper, Fallback would downcast-fail and // silently degrade it into a direct-egress transport. -func TestSharedTransport_InvalidConfigBlockerIsConcreteTransport(t *testing.T) { +func TestPluginTransport_InvalidConfigBlockerIsConcreteTransport(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) resetProxyPluginState() @@ -163,12 +163,12 @@ func TestSharedTransport_InvalidConfigBlockerIsConcreteTransport(t *testing.T) { writeFile(t, Path(), []byte(`{`), 0600) - rt, ok := SharedTransport() + rt, ok := pluginTransport() if !ok { - t.Fatal("SharedTransport() ok = false, want true") + t.Fatal("pluginTransport() ok = false, want true") } if _, isTransport := rt.(*http.Transport); !isTransport { - t.Fatalf("SharedTransport() blocked transport = %T, want *http.Transport so FallbackTransport cannot degrade it to direct egress", rt) + t.Fatalf("pluginTransport() blocked transport = %T, want *http.Transport so Fallback cannot degrade it to direct egress", rt) } // Must remain fail-closed. resp, err := rt.RoundTrip(&http.Request{URL: &url.URL{Scheme: "https", Host: "open.feishu.cn"}}) diff --git a/internal/transport/warn.go b/internal/transport/warn.go new file mode 100644 index 000000000..cac050f72 --- /dev/null +++ b/internal/transport/warn.go @@ -0,0 +1,104 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package transport + +import ( + "fmt" + "io" + "net/url" + "os" + "strings" + "sync" + + "github.com/larksuite/cli/internal/envvars" +) + +// Proxy environment constants control shared transport proxy behavior. +const ( + // EnvNoProxy disables automatic proxy support when set to any non-empty value. + EnvNoProxy = "LARK_CLI_NO_PROXY" +) + +// proxyEnvKeys lists environment variables that Go's ProxyFromEnvironment reads. +var proxyEnvKeys = []string{ + "HTTPS_PROXY", "https_proxy", + "HTTP_PROXY", "http_proxy", + "ALL_PROXY", "all_proxy", +} + +// DetectProxyEnv returns the first proxy-related environment variable that is set, +// or empty strings if none are configured. +func DetectProxyEnv() (key, value string) { + for _, k := range proxyEnvKeys { + if v := os.Getenv(k); v != "" { + return k, v + } + } + return "", "" +} + +// proxyWarningOnce ensures proxy environment warnings are emitted at most once. +var proxyWarningOnce sync.Once + +// proxyPluginStatus reports the configured proxy plugin address, the extra +// trusted CA path (if any), and whether proxy plugin mode is enabled. It is +// indirected through a package variable so tests can simulate plugin-enabled +// mode without the process-global Load() sync.Once cache. +var proxyPluginStatus = func() (addr, caPath string, enabled bool) { + cfg, err := Load() + if err != nil || !cfg.Enabled() { + return "", "", false + } + return cfg.Proxy, cfg.CAPath, true +} + +// redactProxyURL masks userinfo (username:password) in a proxy URL. +// Handles both scheme-prefixed ("http://user:pass@host") and bare ("user:pass@host") formats. +func redactProxyURL(raw string) string { + // Try standard url.Parse first (works when scheme is present) + u, err := url.Parse(raw) + if err == nil && u.User != nil { + return u.Scheme + "://***@" + u.Host + u.RequestURI() + } + + // Fallback: handle bare URLs without scheme (e.g. "user:pass@proxy:8080") + if at := strings.LastIndex(raw, "@"); at > 0 { + return "***@" + raw[at+1:] + } + + return raw +} + +// WarnIfProxied prints a one-time warning to w when a proxy environment variable +// is detected and proxy is not disabled via LARK_CLI_NO_PROXY. Proxy credentials +// are redacted. Safe to call multiple times; only the first call prints. +func WarnIfProxied(w io.Writer) { + proxyWarningOnce.Do(func() { + // Proxy plugin mode overrides env proxies and LARK_CLI_NO_PROXY (see + // Shared), so its warning and disable instructions take precedence. + // Emitting the env-proxy warning here would be misleading: it tells the + // user to set LARK_CLI_NO_PROXY=1, which does NOT disable the plugin proxy. + if addr, caPath, enabled := proxyPluginStatus(); enabled { + fmt.Fprintf(w, "[lark-cli] [WARN] proxy plugin enabled: all requests (including credentials) are forced through %s. To disable, set %s=false or remove %s.\n", + redactProxyURL(addr), envvars.CliProxyEnable, Path()) + if strings.TrimSpace(caPath) != "" { + // A custom CA means upstream TLS can be intercepted/inspected by + // the proxy (MITM). Surface it so the operator is aware traffic + // (including Bearer tokens) is decryptable on this host. + fmt.Fprintf(w, "[lark-cli] [WARN] proxy plugin trusts a custom CA (%s); TLS to upstreams can be intercepted/inspected by this proxy.\n", + caPath) + } + return + } + if os.Getenv(EnvNoProxy) != "" { + return + } + key, val := DetectProxyEnv() + if key == "" { + return + } + fmt.Fprintf(w, "[lark-cli] [WARN] proxy detected: %s=%s — requests (including credentials) will transit through this proxy. Set %s=1 to disable proxy.\n", + key, redactProxyURL(val), EnvNoProxy) + }) +} diff --git a/internal/util/proxy_test.go b/internal/transport/warn_test.go similarity index 63% rename from internal/util/proxy_test.go rename to internal/transport/warn_test.go index ae23215e8..13708ca7e 100644 --- a/internal/util/proxy_test.go +++ b/internal/transport/warn_test.go @@ -1,44 +1,17 @@ // Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT -package util +package transport import ( "bytes" - "net/http" - "os" "strings" "sync" "testing" - "time" "github.com/larksuite/cli/internal/envvars" ) -// unsetEnv clears key for the duration of the test and restores its original value. -func unsetEnv(t *testing.T, key string) { - t.Helper() - old, had := os.LookupEnv(key) - _ = os.Unsetenv(key) - t.Cleanup(func() { - if had { - _ = os.Setenv(key, old) - } else { - _ = os.Unsetenv(key) - } - }) -} - -// unsetProxyPluginEnv clears proxy-related environment variables for deterministic tests. -func unsetProxyPluginEnv(t *testing.T) { - t.Helper() - // Ensure developer machine env doesn't accidentally enable proxy plugin mode - // and change expectations for SharedTransport(). - unsetEnv(t, envvars.CliProxyEnable) - unsetEnv(t, envvars.CliProxyAddress) - unsetEnv(t, envvars.CliCAPath) -} - // TestDetectProxyEnv verifies proxy environment detection priority and empty-state behavior. func TestDetectProxyEnv(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) @@ -61,88 +34,11 @@ func TestDetectProxyEnv(t *testing.T) { } } -// TestSharedTransport_DefaultReturnsStdlibSingleton verifies the default shared transport. -func TestSharedTransport_DefaultReturnsStdlibSingleton(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - t.Setenv(EnvNoProxy, "") - tr := SharedTransport() - if tr != http.DefaultTransport { - t.Error("SharedTransport should return http.DefaultTransport when LARK_CLI_NO_PROXY is unset") - } -} - -// TestSharedTransport_NoProxyReturnsClone verifies that disabling proxying returns a cloned transport. -func TestSharedTransport_NoProxyReturnsClone(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - t.Setenv(EnvNoProxy, "1") - tr := SharedTransport() - if tr == http.DefaultTransport { - t.Fatal("SharedTransport should return a clone, not DefaultTransport, when LARK_CLI_NO_PROXY is set") - } - ht, ok := tr.(*http.Transport) - if !ok { - t.Fatalf("expected *http.Transport, got %T", tr) - } - if ht.Proxy != nil { - t.Error("no-proxy transport should have Proxy == nil") - } -} - -// TestSharedTransport_NoProxyIsCachedSingleton verifies singleton caching for the no-proxy transport. -func TestSharedTransport_NoProxyIsCachedSingleton(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - t.Setenv(EnvNoProxy, "1") - a := SharedTransport() - b := SharedTransport() - if a != b { - t.Error("repeated SharedTransport calls with LARK_CLI_NO_PROXY set must return the same instance") - } -} - -// TestSharedTransport_EnvUnsetAfterSetFallsBackToDefault verifies fallback to the stdlib transport after unsetting EnvNoProxy. -func TestSharedTransport_EnvUnsetAfterSetFallsBackToDefault(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - // Simulate a process that first runs with LARK_CLI_NO_PROXY=1 (populating - // the no-proxy singleton), then unsets it. Subsequent calls must return - // http.DefaultTransport, NOT the cached no-proxy clone. - t.Setenv(EnvNoProxy, "1") - noProxy := SharedTransport() - if noProxy == http.DefaultTransport { - t.Fatal("precondition: first call with env set should not return DefaultTransport") - } - - t.Setenv(EnvNoProxy, "") - after := SharedTransport() - if after != http.DefaultTransport { - t.Errorf("after unsetting LARK_CLI_NO_PROXY, SharedTransport must return http.DefaultTransport, got %T (%p)", after, after) - } -} - -// TestSharedTransport_NoProxyOverridesSystemProxy verifies that EnvNoProxy disables system proxies. -func TestSharedTransport_NoProxyOverridesSystemProxy(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - t.Setenv("HTTPS_PROXY", "http://should-be-ignored:8888") - t.Setenv(EnvNoProxy, "1") - - ht, ok := SharedTransport().(*http.Transport) - if !ok { - t.Fatalf("expected *http.Transport, got %T", SharedTransport()) - } - if ht.Proxy != nil { - t.Error("LARK_CLI_NO_PROXY should override system proxy settings") - } -} - // TestWarnIfProxied_WithProxy verifies that proxy detection emits a warning. func TestWarnIfProxied_WithProxy(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) - // Reset the once guard for this test + resetProxyPluginState() proxyWarningOnce = sync.Once{} t.Setenv("HTTPS_PROXY", "http://corp-proxy:3128") @@ -166,6 +62,7 @@ func TestWarnIfProxied_WithProxy(t *testing.T) { func TestWarnIfProxied_WithoutProxy(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) + resetProxyPluginState() proxyWarningOnce = sync.Once{} for _, k := range proxyEnvKeys { @@ -180,10 +77,11 @@ func TestWarnIfProxied_WithoutProxy(t *testing.T) { } } -// TestWarnIfProxied_SilentWhenDisabled verifies that EnvNoProxy suppresses warnings. +// TestWarnIfProxied_SilentWhenDisabled verifies that LARK_CLI_NO_PROXY suppresses warnings. func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) + resetProxyPluginState() proxyWarningOnce = sync.Once{} t.Setenv("HTTPS_PROXY", "http://proxy:8080") @@ -201,6 +99,7 @@ func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) { func TestWarnIfProxied_OnlyOnce(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) + resetProxyPluginState() proxyWarningOnce = sync.Once{} t.Setenv("HTTP_PROXY", "http://proxy:1234") @@ -257,7 +156,7 @@ func TestWarnIfProxied_ProxyPluginEnabled(t *testing.T) { } // TestWarnIfProxied_ProxyPluginCustomCAWarns verifies that when a custom CA is -// trusted, the warning surfaces the TLS-interception capability (V3). +// trusted, the warning surfaces the TLS-interception capability. func TestWarnIfProxied_ProxyPluginCustomCAWarns(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) @@ -284,25 +183,6 @@ func TestWarnIfProxied_ProxyPluginCustomCAWarns(t *testing.T) { } } -// TestNewHTTPClient verifies the factory wires the shared proxy-plugin-aware -// transport (instead of a bare client that bypasses proxy plugin mode). -func TestNewHTTPClient(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) - t.Setenv(EnvNoProxy, "") - - c := NewHTTPClient(7 * time.Second) - if c.Transport == nil { - t.Fatal("NewHTTPClient transport is nil; want shared transport") - } - if c.Transport != SharedTransport() { - t.Errorf("NewHTTPClient transport = %v, want SharedTransport()", c.Transport) - } - if c.Timeout != 7*time.Second { - t.Errorf("NewHTTPClient timeout = %v, want 7s", c.Timeout) - } -} - // TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials verifies the plugin // warning never leaks credentials embedded in the configured proxy address. func TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials(t *testing.T) { @@ -331,8 +211,6 @@ func TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials(t *testing.T) { // TestRedactProxyURL verifies redaction of proxy credentials across supported formats. func TestRedactProxyURL(t *testing.T) { - t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) - unsetProxyPluginEnv(t) tests := []struct { input string want string @@ -359,6 +237,7 @@ func TestRedactProxyURL(t *testing.T) { func TestWarnIfProxied_RedactsCredentials(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) unsetProxyPluginEnv(t) + resetProxyPluginState() proxyWarningOnce = sync.Once{} t.Setenv("HTTPS_PROXY", "http://admin:s3cret@proxy:8080") diff --git a/internal/update/update.go b/internal/update/update.go index c3509b3d6..31fef085e 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -17,7 +17,7 @@ import ( "time" "github.com/larksuite/cli/internal/core" - "github.com/larksuite/cli/internal/util" + "github.com/larksuite/cli/internal/transport" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/internal/vfs" ) @@ -64,7 +64,7 @@ func httpClient() *http.Client { } return &http.Client{ Timeout: fetchTimeout, - Transport: util.SharedTransport(), + Transport: transport.Shared(), } } diff --git a/internal/util/proxy.go b/internal/util/proxy.go deleted file mode 100644 index d78134e13..000000000 --- a/internal/util/proxy.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package util - -import ( - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - "github.com/larksuite/cli/internal/envvars" - "github.com/larksuite/cli/internal/proxyplugin" -) - -// Proxy environment constants control shared transport proxy behavior. -const ( - // EnvNoProxy disables automatic proxy support when set to any non-empty value. - EnvNoProxy = "LARK_CLI_NO_PROXY" -) - -// proxyEnvKeys lists environment variables that Go's ProxyFromEnvironment reads. -var proxyEnvKeys = []string{ - "HTTPS_PROXY", "https_proxy", - "HTTP_PROXY", "http_proxy", - "ALL_PROXY", "all_proxy", -} - -// DetectProxyEnv returns the first proxy-related environment variable that is set, -// or empty strings if none are configured. -func DetectProxyEnv() (key, value string) { - for _, k := range proxyEnvKeys { - if v := os.Getenv(k); v != "" { - return k, v - } - } - return "", "" -} - -// proxyWarningOnce ensures proxy environment warnings are emitted at most once. -var proxyWarningOnce sync.Once - -// proxyPluginStatus reports the configured proxy plugin address, the extra -// trusted CA path (if any), and whether proxy plugin mode is enabled. It is -// indirected through a package variable so tests can simulate plugin-enabled -// mode without the process-global proxyplugin.Load() sync.Once cache. -var proxyPluginStatus = func() (addr, caPath string, enabled bool) { - cfg, err := proxyplugin.Load() - if err != nil || !cfg.Enabled() { - return "", "", false - } - return cfg.Proxy, cfg.CAPath, true -} - -// redactProxyURL masks userinfo (username:password) in a proxy URL. -// Handles both scheme-prefixed ("http://user:pass@host") and bare ("user:pass@host") formats. -func redactProxyURL(raw string) string { - // Try standard url.Parse first (works when scheme is present) - u, err := url.Parse(raw) - if err == nil && u.User != nil { - return u.Scheme + "://***@" + u.Host + u.RequestURI() - } - - // Fallback: handle bare URLs without scheme (e.g. "user:pass@proxy:8080") - if at := strings.LastIndex(raw, "@"); at > 0 { - return "***@" + raw[at+1:] - } - - return raw -} - -// WarnIfProxied prints a one-time warning to w when a proxy environment variable -// is detected and proxy is not disabled via LARK_CLI_NO_PROXY. Proxy credentials -// are redacted. Safe to call multiple times; only the first call prints. -func WarnIfProxied(w io.Writer) { - proxyWarningOnce.Do(func() { - // Proxy plugin mode overrides env proxies and LARK_CLI_NO_PROXY (see - // SharedTransport), so its warning and disable instructions take - // precedence. Emitting the env-proxy warning here would be misleading: - // it tells the user to set LARK_CLI_NO_PROXY=1, which does NOT disable - // the plugin proxy. - if addr, caPath, enabled := proxyPluginStatus(); enabled { - fmt.Fprintf(w, "[lark-cli] [WARN] proxy plugin enabled: all requests (including credentials) are forced through %s. To disable, set %s=false or remove %s.\n", - redactProxyURL(addr), envvars.CliProxyEnable, proxyplugin.Path()) - if strings.TrimSpace(caPath) != "" { - // A custom CA means upstream TLS can be intercepted/inspected by - // the proxy (MITM). Surface it so the operator is aware traffic - // (including Bearer tokens) is decryptable on this host. - fmt.Fprintf(w, "[lark-cli] [WARN] proxy plugin trusts a custom CA (%s); TLS to upstreams can be intercepted/inspected by this proxy.\n", - caPath) - } - return - } - if os.Getenv(EnvNoProxy) != "" { - return - } - key, val := DetectProxyEnv() - if key == "" { - return - } - fmt.Fprintf(w, "[lark-cli] [WARN] proxy detected: %s=%s — requests (including credentials) will transit through this proxy. Set %s=1 to disable proxy.\n", - key, redactProxyURL(val), EnvNoProxy) - }) -} - -// noProxyTransport is a proxy-disabled clone of http.DefaultTransport, -// lazily built the first time LARK_CLI_NO_PROXY is observed set. -var noProxyTransport = sync.OnceValue(func() *http.Transport { - def, ok := http.DefaultTransport.(*http.Transport) - if !ok { - return &http.Transport{} - } - t := def.Clone() - t.Proxy = nil - return t -}) - -// SharedTransport returns the base http.RoundTripper for CLI HTTP clients. -// -// By default it returns http.DefaultTransport — the stdlib-provided -// process-wide singleton — so every HTTP client in the process shares one -// TCP connection pool, TLS session cache, and HTTP/2 state. When -// LARK_CLI_NO_PROXY is set it returns a separate proxy-disabled singleton -// clone; LARK_CLI_NO_PROXY is checked on every call, but the clone is built -// at most once. -// -// The returned RoundTripper MUST NOT be mutated. Callers that need a -// customized transport should assert to *http.Transport and Clone() it. -// Using a shared base is required so persistConn readLoop/writeLoop -// goroutines are reused; cloning per call leaks them until IdleConnTimeout -// (~90s) fires. -func SharedTransport() http.RoundTripper { - // proxy plugin mode overrides all other proxy behavior (env proxies and - // LARK_CLI_NO_PROXY), per operator intent. - if t, ok := proxyplugin.SharedTransport(); ok { - return t - } - if os.Getenv(EnvNoProxy) != "" { - return noProxyTransport() - } - return http.DefaultTransport -} - -// FallbackTransport returns a shared *http.Transport singleton. It is a -// thin wrapper over SharedTransport retained so modules that were already -// on the leak-free singleton path (internal/auth, internal/cmdutil -// transport decorators) do not have to migrate. New code should prefer -// SharedTransport and treat the base as an http.RoundTripper. -// -// Fail-closed invariant: proxyplugin always expresses its blocked/fail-closed -// transport as a concrete *http.Transport (see proxyplugin.failClosedTransport), -// so the assertion below preserves the block. The noProxyTransport() fallback is -// therefore only reached when no proxy plugin is configured and some external -// code replaced http.DefaultTransport with a non-*http.Transport — a case with -// no fail-closed intent, where a proxy-disabled transport is acceptable. -func FallbackTransport() *http.Transport { - if t, ok := SharedTransport().(*http.Transport); ok { - return t - } - return noProxyTransport() -} - -// NewHTTPClient returns an *http.Client whose Transport is the shared, -// proxy-plugin-aware base (see SharedTransport). Prefer this over a bare -// &http.Client{} for outbound requests: a bare client falls back to -// http.DefaultTransport and therefore silently bypasses proxy plugin mode -// (fixed proxy + trusted CA, or fail-closed), creating an audit blind spot. -// -// A zero timeout means no client-level timeout (callers relying on -// context deadlines pass 0). -func NewHTTPClient(timeout time.Duration) *http.Client { - return &http.Client{ - Transport: SharedTransport(), - Timeout: timeout, - } -} From 925ae5ecd604d613a783fd5d4a88d330930e9531 Mon Sep 17 00:00:00 2001 From: YH-1600 Date: Tue, 2 Jun 2026 16:28:25 +0800 Subject: [PATCH 2/4] docs: add lark drive knowledge organization workflow (#1028) Change-Id: I2343fcdf26ceefb898cc8d4faeae4b17384cfea8 --- skills/lark-drive/SKILL.md | 1 + ...ve-workflow-knowledge-organize-analysis.md | 222 ++++++++++++ ...e-workflow-knowledge-organize-discovery.md | 205 ++++++++++++ ...e-workflow-knowledge-organize-execution.md | 200 +++++++++++ ...ve-workflow-knowledge-organize-planning.md | 316 ++++++++++++++++++ ...ve-workflow-knowledge-organize-rollback.md | 308 +++++++++++++++++ .../lark-drive-workflow-knowledge-organize.md | 224 +++++++++++++ skills/lark-wiki/SKILL.md | 1 + 8 files changed, 1477 insertions(+) create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md create mode 100644 skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md diff --git a/skills/lark-drive/SKILL.md b/skills/lark-drive/SKILL.md index 678749fac..72aa540c9 100644 --- a/skills/lark-drive/SKILL.md +++ b/skills/lark-drive/SKILL.md @@ -18,6 +18,7 @@ metadata: ## 快速决策 +- 用户要**整理云盘 / 文件夹 / 文档库 / 知识库 / 个人文档库**,或要“盘点目录结构、找出未归档/临时/重复/空目录、生成整理方案”,必须先阅读 [`references/lark-drive-workflow-knowledge-organize.md`](references/lark-drive-workflow-knowledge-organize.md)。默认只生成方案;创建目录、移动资源、申请权限都必须单独确认。 - 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间(云盘/云存储)对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"(→ `--mine`,实为 owner 语义)、"最近一周我打开过的 xxx"、"某人 owner 的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。 - 用户要把本地 `.xlsx` / `.csv` / `.base` 导入成 Base / 多维表格 / bitable,第一步必须使用 `lark-cli drive +import --type bitable`。 - 用户要把本地 `.md` / `.docx` / `.doc` / `.txt` / `.html` 导入成在线文档,使用 `lark-cli drive +import --type docx`。 diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md new file mode 100644 index 000000000..c9f062176 --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md @@ -0,0 +1,222 @@ +# 知识整理工作流:Analysis + +Loaded by states: `CONTENT_READ`, `ISSUE_ANALYSIS`, `RULE_GENERATION`. + +This file owns low-confidence partial reads, issue analysis, classification rules, and target tree generation. It MUST NOT create execution plans, ask for execution confirmation, or perform write operations. + +## Required Context + +Before executing rules in this file: + +1. `resource_items` MUST already exist from [`lark-drive-workflow-knowledge-organize-discovery.md`](lark-drive-workflow-knowledge-organize-discovery.md). +2. For document partial reads, follow [`../../lark-doc/SKILL.md`](../../lark-doc/SKILL.md) and [`../../lark-doc/references/lark-doc-fetch.md`](../../lark-doc/references/lark-doc-fetch.md). +3. For sheet / bitable down-drill, follow [`../../lark-sheets/SKILL.md`](../../lark-sheets/SKILL.md) or [`../../lark-base/SKILL.md`](../../lark-base/SKILL.md) only when title and path are insufficient. + +## State: CONTENT_READ + +Entry: `resource_items` exists. + +MUST: + +1. Build `low_confidence_items`. +2. Apply `Low-Confidence Partial Read`. +3. Read only supported docs through `lark-doc-fetch`. +4. Switch to `lark-sheets` / `lark-base` only when sheet / bitable title and path are insufficient. +5. Record read evidence for classification. +6. Continue reading low-confidence resources in internal batches until all supported low-confidence resources in the current inventory are processed or a blocker occurs. +7. Output progress / summary without asking the user to continue between batches. + +Exit: low-confidence items are classified or marked `needs_review=true`. + +### Low-Confidence Partial Read + +Low-confidence resources include: + +- 标题为空 +- 标题为 `test` / `测试` / 纯数字 / 无意义短词 +- 标题、路径、类型之间没有足够分类线索 +- 同一标题或相似标题出现在多个候选分类中 +- 用户要求按项目 / 客户 / 业务线归类,但标题和路径没有明确项目 / 客户 / 业务线名称 + +| Condition | Agent MUST Do | Agent MUST NOT Do | +|-----------|---------------|-------------------| +| Title / path / type clearly determine classification | Classify directly | Do not perform content read | +| Resource is low-confidence and docs-fetch-supported | Read outline via `lark-doc-fetch` | Do not skip partial read | +| Candidate project / customer / business / document-type terms exist | After outline, run keyword partial read with candidate terms | Do not use broad generic keywords | +| Partial read returns usable block id and classification is still unclear | Read the relevant section via `lark-doc-fetch` | Do not read the full document | +| Partial read still cannot classify | Set `needs_review=true`; classify to manual confirmation target | Do not invent classification | +| Read fails or permission is insufficient | Set `needs_review=true`; record failure reason | Do not retry indefinitely | + +### Partial Read Limits + +| Limit | Default | +|-------|---------| +| `batch_size` | 20 resources per internal batch | +| `progress_report_interval` | 50 low-confidence resources | +| `max_attempts_per_resource` | 3 partial reads: outline, keyword, section | + +Batching rules: + +1. Sort low-confidence resources by impact before reading: root-level loose items, duplicated titles, project/customer ambiguity, then empty or meaningless titles. +2. Read supported low-confidence resources across internal batches without asking the user to continue after each batch. +3. Process reads in internal batches of `batch_size`; do not ask the user between internal batches unless auth, permission, or API errors block progress. +4. After each internal batch, update `low_confidence_items` with read evidence or `needs_review=true`. +5. After every `progress_report_interval` processed resources, output a progress summary and continue automatically. +6. If unread low-confidence resources remain because of auth, permission, API, unsupported type, or tool budget blockers, set `partial=true`, report unread count, and default remaining unread items to `needs_review=true` with target path set to manual confirmation target. +7. Never bypass these limits by reading full documents. + +### Low-Confidence Read Start Notice + +When `low_confidence_total > 100`, output this notice before reading: + +```text +低置信度资源较多,共 项。我会分批做轻量读取并定期汇报进度;不会读取全文,也不会执行移动或创建。 +``` + +### Low-Confidence Read Summary + +Use this as progress / final summary output. Do not ask the user to continue unless a blocker occurs. + +```text +低置信度内容读取进度 + +- 低置信度资源总数: +- 已读取:/ +- 已补充证据并完成分类: +- 暂入待人工确认: +- 失败: + +继续分析整理问题。 +``` + +Output this summary: + +- After every 50 processed low-confidence resources. +- Once after low-confidence reading finishes. + +## State: ISSUE_ANALYSIS + +Entry: `resource_items` and partial-read evidence are ready. + +MUST: + +1. Detect problems from organization perspective only. Do not generate research conclusions. +2. Generate an organization approach based on inventory, low-confidence read evidence, and detected problems. +3. Include how non-reused source containers will be handled after their contents are moved. +4. Output `Inventory And Organization Approach Decision`. +5. Stop and wait for the user to confirm the approach before `RULE_GENERATION`. + +Problem rules: + +| Problem | Detection Rule | +|---------|----------------| +| 根目录堆积 | 根目录直接资源过多,或超过总资源的明显比例 | +| 同类文件分散 | 标题 / 类型相似的资源分布在多个无关路径 | +| 命名不统一 | 同类资源日期、客户、项目命名格式明显不一致 | +| 临时内容过多 | 标题 / 路径含 `临时`、`测试`、`tmp`、`draft`、`转移`、`未整理` | +| 空目录 | 目录类节点无后代资源 | +| 重复目录 | 目录名归一化后相同或高度相似 | +| 过旧归档内容 | 旧年份资源仍散落在活跃目录 | + +MUST output evidence count or example paths. Do not output only abstract judgment. + +### Problem Pagination + +| Output Area | Rule | +|-------------|------| +| Problem overview | Show at most 5 problem types per page | +| Problem examples | Show at most 3 example paths per problem type | +| Pagination | Affects display only; complete `issue_summary` MUST remain internal | + +### Inventory And Organization Approach Decision + +```text +盘点与整理思路 + +盘点结果: +| 指标 | 数量 | +|------|------| +| 总资源数 | | +| 各类型资源数 | | +| 一级目录数量 | | +| 根目录直接资源数 | | +| 空目录数量 | | +| 低置信度资源数 | | +| 已完成低置信度读取 | | +| 待人工确认 | | +| partial | | + +共发现 类问题,当前展示第 / 页。 + +| 问题 | 证据数量 | 样例路径 | 说明 | +|------|----------|----------|------| + +整理思路: +- +- +- 对证据不足、读取失败或权限不足的资源放入"待人工确认" +- 如存在不再复用的来源目录,内容迁出后将目录本体收起到 `待人工确认/待清理旧目录`,避免整理后一级目录仍杂乱 +- 不删除、不重命名、不修改权限 + +是否基于这个整理思路生成目标目录和移动 / 创建计划? + +你可以选择: +A. 基于这个思路生成目标目录和计划 +B. 调整整理思路 +C. 查看问题详情 +D. 取消本次整理 +``` + +## State: RULE_GENERATION + +Entry: user confirms the organization approach. + +MUST: + +1. Generate `classification_rules`. +2. Generate `target_tree`. +3. Generate `target_tree` to at least two levels; include third level when needed for project / customer / document-type grouping. +4. Reuse existing clear structure when possible. +5. Identify reused top-level containers and non-reused source containers, and set `source_container_disposition`. +6. For non-reused source containers, ensure `target_tree` includes a source-container cleanup target, defaulting to `待人工确认/待清理旧目录`, unless the user explicitly asks to keep source containers in place. +7. Ensure target tree can contain every planned `target_path`. +8. Ensure the target tree contains a manual confirmation target named `待人工确认` unless the user explicitly provides an equivalent name. +9. Continue to `PLAN_GENERATION` without a separate target-tree-only confirmation. + +### Classification + +| Condition | Agent MUST Do | +|-----------|---------------| +| Existing structure is clear | Reuse existing directory names and hierarchy | +| Title / path / type is enough | Classify without content read | +| Item remains uncertain after mandatory partial read | Put into manual confirmation target and set `needs_review=true` | +| Item is temporary / test / draft | Prefer temporary / test target | +| Root has many loose resources | Prefer organizing root-level obvious items first | +| User asks project / customer grouping | Use project / customer names from title, path, and partial read evidence | +| Naming is inconsistent | Report the issue with examples only; do not generate rename actions | + +### Adaptive Classification + +The agent MUST NOT start from a fixed default category list. A fixed taxonomy can bias classification and confuse users when category names or numeric prefixes do not match their resources. + +Derive categories from the current `resource_items` and partial-read evidence: + +1. First group resources by clear signals from title, current path, type, and mandatory partial-read evidence. +2. Prefer category names that appear in the user's own content, such as project names, customer names, business lines, document types, years, or existing folder / Wiki node names. +3. Create a category only when there is enough evidence for at least one resource. +4. Do not create generic buckets such as archive, temporary, test, meeting, dashboard, or operations unless the current resources contain matching evidence. +5. Do not add numeric prefixes to category names unless the user explicitly asks for ordered naming. +6. Always keep a manual confirmation target named `待人工确认` or an equivalent user-specified name for unresolved items. + +### Target Tree + +`target_tree` is generated in this state but shown together with the move / create plan in `PLAN_GENERATION`. Do not stop after displaying a target tree alone. + +## Analysis Failure Handling + +| Failure / Blocker | Agent MUST Do | Agent MUST NOT Do | +|-------------------|---------------|-------------------| +| Missing API scope | Follow `lark-shared` permission handling and stop | Do not retry the same command repeatedly | +| Resource access denied | Stop and follow the main workflow `Permission Request Gate` | Do not request permission automatically or in batch | +| Partial document read fails for a low-confidence item | Mark item `needs_review=true`, record reason, and route to manual confirmation target | Do not classify by guessing | +| Item remains ambiguous after partial read | Mark `needs_review=true` and route to manual confirmation target | Do not invent classification | diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md new file mode 100644 index 000000000..a6d8db3b5 --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md @@ -0,0 +1,205 @@ +# 知识整理工作流:Discovery + +Loaded by states: `PARSE_SCOPE`, `INVENTORY`. + +This file owns target parsing, scope clarification, resource inventory, ResourceItem normalization, dedupe, and partial inventory handling. It MUST NOT generate classification rules, execution plans, or perform write operations. + +## Required Context + +Before executing rules in this file: + +1. Follow [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for identity, auth, and permission handling. +2. For Wiki / personal library targets, follow [`../../lark-wiki/SKILL.md`](../../lark-wiki/SKILL.md). +3. For Drive search targets, follow [`lark-drive-search.md`](lark-drive-search.md). +4. For URL / token inspection, follow [`lark-drive-inspect.md`](lark-drive-inspect.md) and [`../../lark-wiki/references/lark-wiki-node-get.md`](../../lark-wiki/references/lark-wiki-node-get.md). + +## State: PARSE_SCOPE + +Entry: workflow triggered. + +MUST: + +1. Identify `target_scope`, `environment_profile`, and `identity`. +2. Apply `Scope Parsing`. +3. Output `Scope Confirmation`. +4. Stop and wait for user confirmation before `INVENTORY`. + +Exit: user confirms target scope. + +### Scope Parsing + +| Condition | Agent MUST Do | Set `target_scope` | Next State | +|-----------|---------------|--------------------|------------| +| Input is `/wiki/` URL | Resolve the Wiki node and preserve both node identity and object identity | Wiki node | `INVENTORY` after user confirms scope | +| Input is Wiki space name / `space_id` | Resolve the Wiki space; 0 matches -> stop and ask; 1 exact match -> continue; multiple matches -> show candidates and wait for user selection; do not treat `my_library` as a normal listed space | Wiki space | `INVENTORY` after user confirms scope | +| Input has Personal Library Intent | Treat as Wiki personal library / `my_library`; resolve real `space_id` before root write; do not treat it as Drive root or owned Drive document search | Personal doc library | `INVENTORY` after user confirms scope | +| Input is `/drive/folder/` URL | Extract `folder_token` | Drive folder | `INVENTORY` after user confirms scope | +| Input has Drive Folder Intent but no concrete folder URL, token, or unique folder name | Ask for folder URL / token / name; if a concrete folder name exists, search folder candidates and wait for user selection when 0 or multiple matches exist | Unknown or Drive folder candidate | Stay in `PARSE_SCOPE` until scope is confirmed | +| Input has Broad Cloud Drive Intent without explicit owned-document search request | Ask the user to choose concrete scope: Drive folder URL / token, Drive root, owned Drive document search, or another explicit search filter; do not default to `drive +search --mine` | Unknown | Stay in `PARSE_SCOPE` until scope is confirmed | +| Input is single cloud resource URL | Resolve the resource type; if not folder / Wiki scope, do not expand automatically | Single resource | Ask whether scope is this resource, parent folder, owning Wiki, or related search results | +| Input is real keyword / name | Search with the real keyword according to `lark-drive-search` | Search scope | `INVENTORY` after user confirms scope | +| Input is range browsing / statistical description with no real keyword | Search by filters / empty-query browsing according to `lark-drive-search` | Search scope | `INVENTORY` after user confirms scope | +| Input is ambiguous | Ask the minimum clarification question and stop | Unknown | Stay in `PARSE_SCOPE` | + +Personal Library Intent means the user is referring to the current user's own Feishu document library / personal document library / personal knowledge library, such as `个人文档库`, `飞书个人文档库`, `我的文档库`, `个人知识库`, `我的知识库`, `My Document Library`, or `my_library`. + +When this intent is detected, use Wiki personal library semantics. Do not use Drive root, `drive +search --mine`, or broad owned-document search unless the user explicitly asks to search owned Drive documents. + +Drive Folder Intent means the user wants to organize a specific Drive folder or Drive folder tree. A Drive folder scope requires a concrete folder URL, folder token, or user-selected folder candidate. + +When this intent is detected without a concrete folder identity, stop in `PARSE_SCOPE` and ask for clarification. Do not use Drive root, `drive +search --mine`, or broad owned-document search unless the user explicitly asks for Drive root or owned-document search. + +Broad Cloud Drive Intent means the user refers to a broad cloud-drive-level scope such as `我的飞书云盘`, `我的云盘`, `我的云空间`, `我的空间`, or `整理云盘`, without a concrete folder URL / token / unique folder name. + +This intent is broader than Drive Folder Intent and MUST NOT be silently converted to owned-document search. Ask the user to choose one of: + +1. A specific Drive folder URL / token. +2. Drive root, only when the user explicitly accepts root-level scope. +3. Owned Drive document search, only when the user explicitly asks to organize documents owned / managed by the current user. +4. Another explicit search filter, such as keyword, type, time range, or folder token. + +### Stop Conditions + +Stop and ask for clarification when: + +1. 用户只说"整理文件夹"、"整理目录"、"整理资料"、"整理文档"、"我的文档",且没有 URL、token、知识库名称、Personal Library Intent、concrete Drive folder identity 或明确搜索范围。 +2. 用户说"我的文件夹"、"我的目录"、"我的空间"、"我的云盘"、"我的飞书云盘"、"我的云空间",但无法唯一判断是具体 Drive 文件夹、Drive 根目录、owned Drive document search、个人文档库还是某个 Wiki 节点。 +3. 用户给的是单个资源 URL,但要求"整理一批文档"或"整理相关资料"。 +4. 用户目标环境不明确,且上下文中同时存在线上、BOE、PRE 或多个 profile。 + +Clarification template: + +```text +请提供要整理的 Drive 文件夹链接、Wiki 节点 / 知识库链接,或明确说明要整理"我的文档库";如果只想按关键词搜索整理,也请给出关键词或范围。 +``` + +### Scope Confirmation + +```text +我先确认本次整理范围。 + +目标: +范围: +环境 / profile: +身份: +预计操作:先盘点并生成整理方案,不执行移动或创建。 + +请确认是否按这个范围继续? +``` + +## State: INVENTORY + +Entry: `target_scope` confirmed. + +MUST: + +1. Recursively list resources according to target type. +2. Generate `path` during traversal. +3. Normalize all results to `ResourceItem`. +4. Track pagination, depth, and item limits. +5. Set `partial=true` when limits are hit. +6. Output `Inventory Summary`. +7. Continue to `CONTENT_READ` without asking the user unless auth, permission, API, target scope, or environment blockers occur. + +### Inventory Limits + +| Scope | Default Limit | If Limit Is Hit | +|-------|---------------|-----------------| +| Wiki recursion | `max_depth=3`, `max_items=500`; follow `lark-wiki-node-list` pagination | Set `partial=true`; list covered paths and suggested next first-level directories | +| Drive folder recursion | `max_depth=3`, `max_items=500`, max 10 pages per folder, `page_size=50` | Set `partial=true`; list folders not drilled into | +| Search discovery | `page_size=20`, `max_items=500`; continue pages until `has_more=false` or `max_items` is reached | Set `partial=true`; report collected_count, service_total when available, page_count, and continuation information | + +If the user explicitly asks for full processing, batch by first-level directory, Wiki space, or time window. Do not remove all limits in one run. + +### Wiki Inventory Rules + +1. Follow [`../../lark-wiki/references/lark-wiki-node-list.md`](../../lark-wiki/references/lark-wiki-node-list.md) traversal semantics. +2. Generate stable paths from parent-child traversal. +3. Preserve Wiki node identity fields needed by `ResourceItem`. +4. Treat `my_library` as Wiki personal library, not Drive root. + +### Drive Inventory Rules + +1. Use CLI command family `drive files list` according to `lark-drive` API rules; its schema path is `drive.files.list`. +2. Recurse only into `folder` items. +3. Use `drive metas batch_query` when URL, owner, created time, or updated time is needed. +4. Continue pages by feeding `next_page_token` into request param `page_token`. +5. Prefer explicit `folder_token`; querying root with empty `folder_token` may return broad root data and may not paginate as expected. + +### Search Inventory Rules + +1. Search results may be normalized directly only when they include stable identity fields required by `ResourceItem`. +2. If a search result is a Wiki item and lacks `node_token`, resolve it with `drive +inspect` or `wiki +node-get` before dedupe. +3. If Wiki identity still cannot be resolved, keep the item, set `needs_review=true`, and record `needs_review_reason`. +4. For search scope, use `page_size=20` unless a lower value is required by the command. +5. Continue fetching pages until `has_more=false` or `max_items` is reached. +6. Do not stop at an arbitrary sample size such as first 5 pages unless the user explicitly asks for sampling or auth, permission, API, environment, or tool-budget blockers occur. +7. If `service_total` / result total is greater than collected items, set `partial=true` and show collected_count, service_total, page_count, and continuation information. +8. Do not present a partial search sample as complete inventory. Before generating a full organization plan from partial search results, ask whether to continue fetching more pages or proceed with sample-based planning. + +## ResourceItem + +Agent MUST normalize Wiki, Drive, and search results into `ResourceItem`. Later statistics, classification, and planning MUST use this model rather than raw API responses. + +```json +{ + "source": "wiki|drive|search", + "title": "资源标题", + "type": "doc|docx|sheet|bitable|mindnote|file|wiki|folder|slides|shortcut|catalog", + "path": "当前路径/资源标题", + "depth": 2, + "url": "https://...", + "token": "canonical_token", + "node_token": "wiki_node_token_or_empty", + "obj_token": "wiki_obj_token_or_drive_file_token", + "node_type": "origin|shortcut|empty", + "origin_node_token": "wiki_origin_node_token_or_empty", + "space_id": "wiki_space_id_or_empty", + "parent_token": "parent_node_or_folder_token", + "has_child": false, + "dedupe_key": "wiki::|drive::|search::", + "created_at": "optional", + "updated_at": "optional", + "needs_review": false, + "needs_review_reason": "" +} +``` + +ResourceItem rules: + +1. `path` MUST be generated by recursion. Do not use title alone as path. +2. Wiki URL token may not be the underlying document token. Preserve both `node_token` and `obj_token`. +3. `type` MUST come from API fields such as `obj_type` / `doc_type`. +4. Wiki organization is by node instance. Prefer `wiki::` as `dedupe_key`. +5. MUST NOT dedupe Wiki nodes only by `obj_token`; one document can appear under different Wiki paths or shortcuts. +6. If `node_type=shortcut` or dedupe is uncertain, use `wiki +node-get` to supplement `origin_node_token`; if unavailable, leave empty and set `needs_review=true`. +7. Drive folder tree dedupes by `drive::`. +8. Search results may merge with recursive results only by exact identity: Wiki by same `node_token`, Drive by same `type + token`. + +## Inventory Summary + +```text +已完成盘点。 + +| 指标 | 数量 | +|------|------| +| 总资源数 | | +| 各类型资源数 | | +| 一级目录数量 | | +| 根目录直接资源数 | | +| 空目录数量 | | +| 疑似临时 / 测试 / 未整理资源数 | | +| 低置信度待确认资源数 | | + +下一步将自动读取低置信度资源并分析整理问题;不会执行移动或创建。 +``` + +## Discovery Failure Handling + +| Failure / Blocker | Agent MUST Do | Agent MUST NOT Do | +|-------------------|---------------|-------------------| +| Target scope is ambiguous | Ask the minimum scope clarification question and stop | Do not choose a whole cloud drive / personal library by default | +| Environment / profile is ambiguous | Ask user to confirm prod / BOE / PRE and profile | Do not cross environment boundaries | +| Missing API scope | Follow `lark-shared` permission handling and stop | Do not retry the same command repeatedly | +| Resource access denied | Stop and follow the main workflow `Permission Request Gate` | Do not request permission automatically or in batch | +| Pagination / depth / item limit reached | Set `partial=true`; record uncovered range and continuation command | Do not claim full coverage | diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md new file mode 100644 index 000000000..9469f79e8 --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md @@ -0,0 +1,200 @@ +# 知识整理工作流:Execution + +Loaded by states: `EXECUTE`, `VERIFY`. + +This file owns confirmed write execution, PathTokenMap, progress reporting, verification, next suggestions, and execution-stage failure handling. It MUST NOT generate or revise plans. + +## Required Context + +Before executing rules in this file: + +1. `active_plan_items` and `execution_scope` MUST already exist from [`lark-drive-workflow-knowledge-organize-planning.md`](lark-drive-workflow-knowledge-organize-planning.md). +2. `target_tree` and `resource_items` MUST already exist. +3. Use the `PlanItem` structure already produced by the planning phase. Do not regenerate or revise plans in this file. +4. Follow command syntax, scope requirements, and confirmation behavior from referenced shortcut docs. +5. Follow `Non-goals` from the main workflow entry. Do not execute excluded operations from this file. +6. Maintain internal `rollback_snapshot` and `execution_journal` during writes, but do not mention recovery on the normal successful path. + +## State: EXECUTE + +Entry: user explicitly confirmed execution scope. + +Allowed writes only: + +- 创建 Drive 文件夹:`drive +create-folder` +- 移动 Drive 文件 / 文件夹:`drive +move` +- 创建 Wiki 节点:`wiki +node-create` +- 移动已有 Wiki 节点:`wiki +move --node-token` +- 续跑异步移动任务:`drive +task_result` +- 单个资源权限申请:`drive +apply-permission` + +MUST: + +1. Resolve all target paths through `PathTokenMap`. +2. Build internal `rollback_snapshot` for all move items in confirmed scope before any write operation. +3. Initialize `execution_journal` before any write operation. +4. Create target folders / nodes shallow to deep. +5. Save returned tokens immediately. +6. Append an `execution_journal` entry after every create / move / async continuation. +7. Apply parent-child source move ordering. +8. Execute only confirmed scope. +9. Record success/failure per `PlanItem`. +10. Apply `Progress Reporting`. + +MUST NOT: + +- Execute operations listed in `Non-goals`. +- Rename or patch resource titles. +- Move using path string instead of token. +- Use `wiki +move` docs-to-wiki mode. +- Output rollback snapshot, rollback readiness, or execution journal on the normal successful path. + +### Internal Recovery Hooks + +These hooks are mandatory internal state maintenance. They do not create user-facing output on the normal path. + +Rules: + +1. Build `rollback_snapshot` before the first write command. +2. Include only compact fields needed for recovery. Do not store full API responses. +3. Append `execution_journal` immediately after each write attempt, including successful creates, successful moves, failed moves, and async continuation results. +4. If snapshot or journal cannot be maintained, stop before further writes and report the blocker. +5. If a write blocker occurs after one or more successful moves, report the blocker and ask whether the user wants to try restoring to `整理前的位置`. +6. Do not load the rollback phase or execute recovery until the user explicitly chooses to try restore. + +Recovery question template: + +```text +执行暂停:已成功移动 项,失败 项。 + +执行出现错误,已有部分资源移动成功。是否需要尝试恢复到整理前的位置? +``` + +### Progress Reporting + +Small execution means `created_total + moved_total <= 50`. + +For small executions, a final execution result is enough. For larger or long-running executions (`created_total + moved_total > 50`), the agent MUST periodically report progress by elapsed time and meaningful stage boundaries rather than by operation count alone. + +Progress reports SHOULD be stage-specific. Include only fields relevant to the current stage. Do not output empty, unknown, or irrelevant fields. + +Required fields by stage: + +- Start: total create count, total move count, reporting cadence. +- Create stage finished: created count, failed count. +- Move stage progress / finished: current moved count as `/`, failed count, optional recent item. +- Blocked: current stage, completed count, blocker, required next action. + +Examples: + +- `执行开始:本次将创建 个目录 / 节点,移动 个资源。任务较大,我会约每 60 秒汇报一次进度。` +- `执行进度:移动资源 /,失败 。` +- `执行暂停: 阶段遇到 ,已完成 /,需要 。` + +Rules: + +1. When `created_total + moved_total > 50`, output one progress notice when execution starts. +2. After the create stage finishes, output one progress report when `created_total > 0`. +3. During the move stage, output a progress report about every 60 seconds, and once more when the move stage finishes. +4. Every move-stage progress report MUST include the current moved count as `/` and failed count, even if fewer than 50 additional moves completed since the previous report. +5. If execution is blocked by auth, permission, unresolved token, or API error, output the current progress and blocker before stopping. +6. Do not mention operation-count milestones such as "not yet reached 50 moves"; progress is time-based. +7. Do not output filler messages such as "still running", "no failure yet", or "not yet reached the next progress point" without current counts. +8. Do not report progress after every item unless the user explicitly asks for verbose execution logs. + +## PathTokenMap + +`PathTokenMap` maps target paths to real tokens before execution. + +| Scope | Mapping | +|-------|---------| +| Drive | `target_path -> folder_token` | +| Wiki | `target_path -> node_token`, with `space_id` retained | + +Rules: + +1. Scan existing target folders / nodes before creating new ones. +2. Create planned folders / nodes from shallow to deep. +3. Save returned `folder_token` or `node_token` immediately after each successful create. +4. Execute `move` only when `target_parent_path` resolves to a token. +5. If same-name target ambiguity exists, inspect existing children when possible; otherwise mark `needs_review=true`. +6. Before writing to `my_library` root, resolve the real `space_id` according to `lark-wiki`. + +## State: VERIFY + +Entry: execution finished. + +MUST: + +1. Rescan target scope. +2. Compare each executed `PlanItem` with actual path/token. +3. Verify items covered by `covered_by_parent_move=true`. +4. Output success/failure/manual-confirmation counts. +5. Report mismatches with expected vs actual path/token. +6. Verify non-reused source containers planned for cleanup are no longer left in their original top-level position. +7. Verify reused target containers remain in place. +8. If serious mismatches exist, ask whether the user wants to try restoring to `整理前的位置`. +9. Do not load the rollback phase or execute recovery until the user explicitly chooses to try restore. + +Verification table: + +| plan_id | 动作 | 标题 | 预期目标 | 实际目标 | 预期 token | 实际 token | 状态 | 失败原因 | +|---------|------|------|----------|----------|------------|------------|------|----------| + +### Verification Result + +```text +执行完成。 + +| 项目 | 数量 | +|------|------| +| 创建成功 | | +| 移动成功 | | +| 待人工确认 | | +| 失败 | | + +| plan_id | 动作 | 预期目标 | 实际目标 | 状态 | 失败原因 | +|---------|------|----------|----------|------|----------| +``` + +Serious mismatch recovery question template: + +```text +验证发现 项结果与计划不一致。 + +是否需要尝试恢复到整理前的位置? +``` + +### Next Suggestions + +Only output `建议下一步` when at least one trigger exists. Do not add generic suggestions when execution and verification are clean. + +Triggers: + +- `partial=true`: inventory or content read was incomplete. +- Manual confirmation or low-confidence items remain. +- Failed items exist. +- One target folder / Wiki node contains more than 100 direct child resources after organization. +- Root-level loose resources remain. +- Non-reused source containers remain in their original top-level position after cleanup was planned. +- Verification found mismatches. + +Template: + +```text +建议下一步: +- +``` + +## Execution Failure Handling + +| Failure / Blocker | Agent MUST Do | Agent MUST NOT Do | +|-------------------|---------------|-------------------| +| Missing API scope | Follow `lark-shared` permission handling and stop | Do not retry the same command repeatedly | +| Resource access denied | Stop and follow the main workflow `Permission Request Gate` | Do not request permission automatically or in batch | +| Target path cannot resolve to token | Mark affected plan item failed or `needs_review=true` | Do not execute move with a path string | +| Target path has same-name ambiguity | Read existing children if possible; otherwise mark `needs_review=true` | Do not create duplicate target blindly | +| Async move returns `ready=false` or `next_command` | Follow the returned async continuation command | Do not assume completion | +| Parent-child move conflict | Follow source-depth ordering; move divergent children before parent | Do not move parent first when child target differs | +| Verification mismatch | Report expected vs actual path/token and failure reason | Do not silently mark success | +| Write blocker after successful moves | Report current progress and ask whether to try restoring to `整理前的位置` | Do not load rollback phase or execute recovery without explicit user choice | diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md new file mode 100644 index 000000000..3e0ef022f --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md @@ -0,0 +1,316 @@ +# 知识整理工作流:Planning + +Loaded by states: `PLAN_GENERATION`, `EXEC_CONFIRM`. + +This file owns plan generation, plan revision, user-facing pagination, and execution confirmation. It MUST NOT perform write operations. + +## Required Context + +Before executing rules in this file: + +1. `resource_items`, `classification_rules`, and `target_tree` MUST already exist. +2. Follow command syntax, scope requirements, and confirmation behavior from referenced shortcut docs. +3. Follow `Non-goals` from the main workflow entry. Do not execute excluded operations from this file. + +## State: PLAN_GENERATION + +Entry: `target_tree` exists after the user confirmed the organization approach. + +MUST: + +1. Generate complete internal `plan_items`. +2. Build `DisplayItem` only for user-facing pages. +3. Apply `Plan Generation`. +4. Apply `Plan Pagination`. +5. Set `active_plan_items` to the latest complete plan. +6. Keep complete plan internally even if only one page is displayed. +7. Output `Target Tree And Plan Overview` or requested plan page, then wait. + +### Plan Generation + +| Condition | Agent MUST Do | +|-----------|---------------| +| Target path appears in any plan item | Ensure the path exists in `target_tree` | +| Source parent and descendants share same target subtree | Move parent only; mark descendants `covered_by_parent_move=true` | +| A child target differs from parent target | Move divergent child before parent; order by `source_depth` from deep to shallow | +| Target directory / node does not exist | Add `create_folder` / `create_node` before move | +| Resource is root-level and target path differs from current path | Add a `move` plan item; do not leave root-level resources in place by default | +| Resource has `needs_review=true` because classification evidence is insufficient | Set `target_path` to manual confirmation target, set `action=move`, and preserve `needs_review_reason` | +| Top-level folder / Wiki node has descendants that share the same target subtree | Move the parent folder / node only; descendants are covered by parent move | +| Top-level folder / Wiki node has descendants with divergent target subtrees | Move divergent descendants first; then move the parent only if it still has a target path or needs manual confirmation | +| Source container is reused as a target container | Keep the container in place; do not move it as source-container cleanup | +| Non-reused source container has descendants moved elsewhere | Add an explicit folder / node move plan item after descendant moves; target defaults to the source-container cleanup target | +| Source container handling is ambiguous | Move it to the manual confirmation target or mark `needs_review=true`; do not leave it in the root by default | +| Target parent token unresolved | Keep plan item but block execution until token is resolved | +| Resource title is poor or inconsistent | Report the naming issue only; do not create rename or title-patch plan items | + +## PlanItem + +`PlanItem` is for internal execution. It may contain tokens and internal enums. + +| Field | Meaning | +|-------|---------| +| `plan_id` | Stable unique ID for plan / verification, such as `P001` | +| `source_path` | Current path | +| `title` | Resource title | +| `type` | Resource type | +| `source_token` | Drive token or normal resource token | +| `source_node_token` | Wiki node token; empty for non-Wiki resources | +| `source_parent_token` | Current parent folder token or parent Wiki node token | +| `source_depth` | Original depth in source tree | +| `target_path` | Target path | +| `target_parent_path` | Target parent path | +| `target_parent_token` | Target parent token; may be empty during planning, MUST be resolved before execution | +| `action` | Internal enum: `keep` / `create_folder` / `create_node` / `move` | +| `covered_by_parent_move` | Whether an ancestor move already covers this item | +| `reason` | Classification reason | +| `evidence_paths` | Evidence paths | +| `evidence_count` | Evidence count or hit count | +| `confidence` | Internal enum: `high` / `medium` / `low` | +| `needs_review` | Whether human review is required | +| `needs_review_reason` | Reason requiring human review | +| `rollback_origin_kind` | Internal recovery origin marker: `drive_folder` / `drive_root` / `wiki_node` / `wiki_space_root` / `unknown` | +| `rollback_origin_token` | Original parent token when applicable; empty for root markers | +| `rollback_origin_space_id` | Original Wiki space ID when `rollback_origin_kind=wiki_space_root` | +| `rollback_supported` | Whether this move item can be restored automatically if recovery is requested | +| `rollback_blocker` | Internal reason when `rollback_supported=false` | + +### Rollback Origin Readiness + +This is an internal execution-safety rule. Do not expose rollback readiness on the normal user-facing execution confirmation path. + +Rules: + +1. `action=move` items entering execution SHOULD have `rollback_origin_kind`. +2. `rollback_origin_kind` can be: + - `drive_folder`: original Drive parent folder token is known. + - `drive_root`: original location is the Drive root. + - `wiki_node`: original Wiki parent node token is known. + - `wiki_space_root`: original location is the Wiki space root and `rollback_origin_space_id` is known. +3. If `rollback_origin_kind` is missing or `unknown`, the agent MUST try to resolve it before execution from `ResourceItem.parent_token`, traversal context, `source_path`, `space_id`, or `wiki +node-get` for Wiki resources. +4. If the origin is still unresolved, set `rollback_supported=false` and `rollback_blocker`, but do not block the entire execution solely because recovery is unsupported. +5. Target resolution remains mandatory: a move item with unresolved `target_parent_token` MUST NOT execute. +6. Internal recovery metadata MUST NOT change `DisplayItem` output on the normal successful path. + +## DisplayItem + +`DisplayItem` is for user-facing output. It MUST NOT expose raw internal enum values. + +| Display Field | Source | +|---------------|--------| +| `序号` | Page-local row number | +| `当前位置` | `source_path` | +| `标题` | `title` | +| `类型` | Human-readable `type` when possible; raw type is acceptable only when there is no clearer label | +| `目标位置` | `target_path` | +| `动作` | Convert from `action` using action display map | +| `原因` | `reason` | +| `置信度` | Convert from `confidence` using confidence display map | +| `待确认原因` | `needs_review_reason` | + +Action display map: + +| Internal Enum | User-Facing Label | +|---------------|-------------------| +| `keep` | 保持不变 | +| `create_folder` | 创建文件夹 | +| `create_node` | 创建知识库节点 | +| `move` | 移动到目标目录 | + +`needs_review=true` is a review state, not an action. A review item MUST still use `action=move` when its target is the manual confirmation target. + +### Manual Confirmation Target + +Resources with insufficient classification evidence MUST be moved to the manual confirmation target after the user confirms execution. + +Rules: + +1. The target tree MUST include `待人工确认` or an equivalent user-specified manual confirmation path. +2. For Drive scopes, the manual confirmation target is a Drive folder. +3. For Wiki scopes, the manual confirmation target is a Wiki node. +4. Plan items for these resources MUST set `needs_review=true`, preserve `needs_review_reason`, set `target_path` to the manual confirmation target, and set `action=move`. +5. Do not leave these items in their original location by default. + +Confidence display map: + +| Internal Enum | User-Facing Label | +|---------------|-------------------| +| `high` | 高,证据明确 | +| `medium` | 中,有依据但建议确认 | +| `low` | 低,需要人工确认 | + +### Plan Pagination + +| Output Area | Rule | +|-------------|------| +| Plan details | Show at most 20 plan items per page | +| Plan item count > 20 | MUST paginate; do not output all details at once | +| Plan item count > 500 | First response MUST show overview and filters only; no detail rows until user asks | +| Pagination | Affects display only; complete `plan_items` MUST remain internal | + +### Target Tree And Plan Overview + +```text +建议目标目录结构 + + + +移动 / 创建计划总览 + +本次计划共 项: +- 创建目录 / 节点: 项 +- 移动资源: 项(其中来源目录本体: 项) +- 保持不变: 项 +- 待人工确认: 项 +- 高置信度: 项 +- 中置信度: 项 +- 低置信度: 项 + +你可以选择: +- 查看第 1 页明细 +- 只看将创建的目录 / 节点 +- 只看待人工确认项 +- 只看高置信度移动项 +- 进入执行确认 +``` + +If `total_count > 500`, say: + +```text +计划较大,我先只展示总览。 +``` + +### Plan Revision Protocol + +When the user corrects or adjusts the plan in `PLAN_GENERATION` or `EXEC_CONFIRM`, the agent MUST treat it as a full-plan revision unless the user explicitly asks to execute only the corrected items. + +Revision triggers include: + +- Adjusting classification rules. +- Adjusting target folder / Wiki node structure. +- Changing one or more resources' target paths. +- Excluding resources from movement. +- Restricting execution to high-confidence items. +- Moving a whole category to another target. +- Changing manual confirmation handling. +- Changing source container cleanup or retention handling. + +Internal rules: + +1. Record the user correction in `last_user_correction`. +2. Mark the previous `plan_items` as stale. +3. Recompute `classification_rules`, `target_tree`, and complete `plan_items` when needed. +4. Increment `plan_version`. +5. Set `active_plan_items` to the complete revised plan. +6. Append a short internal summary to `plan_revision_history`. +7. Do not execute stale `plan_items`. +8. Do not execute only the delta unless the user explicitly asks for partial execution. + +User-facing output: + +```text +已按你的修改重新生成完整计划。 + +已应用的修改: +- +- + +当前完整计划: +- 创建目录 / 节点: 项 +- 移动资源: 项 +- 保持不变: 项 +- 待人工确认: 项 + +说明:后续执行默认基于这份完整修正版计划,不是只执行刚才的修正项。 + +你可以选择: +A. 查看修正版计划总览 +B. 查看本次修改涉及的资源 +C. 进入执行确认 +D. 继续调整 +``` + +If the user explicitly asks to execute only the corrected items, ask for confirmation before execution: + +```text +你明确要求只执行本次修改涉及的 项。其余计划项不会执行。 +请确认是否只执行这些项? +``` + +### Plan Detail Page + +```text +移动 / 创建计划,第 / 页,每页 20 项 + +| 序号 | 当前位置 | 标题 | 类型 | 目标位置 | 动作 | 原因 | 置信度 | 待确认原因 | +|------|----------|------|------|----------|------|------|--------|------------| + +还有 页未展示。 + +你可以回复: +- 继续看下一页 +- 只看待人工确认项 +- 只看低置信度项 +- 进入执行确认 +``` + +## State: EXEC_CONFIRM + +Entry: user asks to execute. + +MUST: + +1. Show write-operation summary: + - 将创建哪些目录 / 节点 + - 将移动哪些资源 + - 将移动哪些来源目录本体(如有) + - 哪些资源仍需人工确认 + - 预计影响范围 +2. Use `active_plan_items` from the latest complete plan. +3. Show `Permission Inheritance Notice`. +4. Ask for execution scope using `Execution Confirmation`. +5. Reference `Non-goals` for operations excluded from this workflow. +6. Wait for explicit confirmation. + +### Permission Inheritance Notice + +Before execution confirmation, MUST show this notice: + +```text +权限提示:移动资源后,资源权限可能随目标位置变化,可见范围或协作权限可能变化。本 workflow 不会自动修改权限。 +``` + +### Execution Confirmation + +When the user wants execution, ask for execution scope: + +Execution confirmation options MUST be renumbered by currently available choices. Do not show disabled choices, and do not ask the user to reply with skipped letters. + +If a plan detail page is currently active: + +```text +请确认执行范围: + +A. 执行完整计划: 项 +B. 只执行当前页: 项 +C. 只执行高置信度项: 项 +D. 暂不执行,只保留方案 + +本 workflow 只执行已确认范围内的创建、移动和必要的单资源权限申请;不会重命名任何资源。 +``` + +If no plan detail page is currently active: + +```text +请确认执行范围: + +A. 执行完整计划: 项 +B. 只执行高置信度项: 项 +C. 暂不执行,只保留方案 + +如需只执行某一页,请先查看计划明细页。 + +本 workflow 只执行已确认范围内的创建、移动和必要的单资源权限申请;不会重命名任何资源。 +``` + +If there is no pagination, still state the total number of plan items covered by confirmation. diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md new file mode 100644 index 000000000..173cdac10 --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md @@ -0,0 +1,308 @@ +# 知识整理工作流:Rollback + +Loaded by states: `ROLLBACK_CONFIRM`, `ROLLBACK`, `ROLLBACK_VERIFY`, `ROLLBACK_CLEANUP_CONFIRM`, `ROLLBACK_CLEANUP`, `ROLLBACK_CLEANUP_VERIFY`. + +This file owns recovery plan generation, recovery confirmation, recovery execution, recovery verification, cleanup confirmation, cleanup execution, and cleanup verification. It also defines the internal `rollback_snapshot` and `execution_journal` contracts. + +It MUST NOT generate organization plans, revise classification rules, execute unconfirmed deletes, rename resources, modify permissions, or use `wiki +move` docs-to-wiki mode. + +User-facing language should use "恢复到整理前的位置" / "恢复". Internal state and field names may use `rollback`. + +## Required Context + +Before executing rules in this file: + +1. `active_plan_items`, `execution_scope`, `target_scope`, and `path_token_map` MUST already exist. +2. `rollback_snapshot` MUST have been built before the first write operation in `EXECUTE`. +3. `execution_journal` MUST contain write-operation results from `EXECUTE`. +4. Follow command syntax and risk behavior from referenced shortcut docs. +5. Follow `Non-goals` from the main workflow entry. + +## Normal Path Visibility + +Do not mention rollback, recovery readiness, snapshot, or journal on the normal successful execution path. + +Load this file only after: + +1. Execution failed after one or more successful moves and the user chose to try restoring. +2. Verification found serious mismatches and the user chose to try restoring. +3. The user explicitly asks to rollback / recover the previous organization run. + +## Internal State Contracts + +### RollbackSnapshot + +`rollback_snapshot` records original locations before any write command. + +Fields: + +| Field | Meaning | +|-------|---------| +| `plan_id` | Matching `PlanItem.plan_id` | +| `source_kind` | `drive` / `wiki` | +| `title` | Resource title | +| `type` | Resource type used by move commands | +| `original_token` | Original Drive token when applicable | +| `original_node_token` | Original Wiki node token when applicable | +| `original_parent_kind` | `drive_folder` / `drive_root` / `wiki_node` / `wiki_space_root` / `unknown` | +| `original_parent_token` | Original parent token; empty for root markers | +| `original_space_id` | Original Wiki space ID when restoring to Wiki space root | +| `original_path` | Original path before organization | +| `planned_target_parent_token` | Planned target parent token | +| `planned_target_path` | Planned target path | +| `rollback_supported` | Whether this item can be restored automatically | +| `rollback_blocker` | Reason when `rollback_supported=false` | + +Rules: + +1. Store compact fields only. Do not store full API responses. +2. `drive_root` and `wiki_space_root` are valid origins; do not treat empty parent token as missing when the root marker is known. +3. Items without reliable origin can still execute, but MUST be marked `rollback_supported=false`. + +### ExecutionJournal + +`execution_journal` records every write attempt. + +Fields: + +| Field | Meaning | +|-------|---------| +| `journal_id` | Stable journal row ID | +| `plan_id` | Matching `PlanItem.plan_id` when applicable | +| `operation` | `create_folder` / `create_node` / `move_drive` / `move_wiki_node` / `delete_created_folder` / `delete_created_node` | +| `status` | `success` / `failed` / `pending` | +| `input_token` | Token supplied to the command | +| `input_node_token` | Wiki node token supplied to the command | +| `input_parent_token` | Source parent token when known | +| `target_parent_token` | Target parent token supplied to the command | +| `returned_token` | Token returned by the command | +| `returned_node_token` | Wiki node token returned by the command | +| `returned_parent_token` | Parent token returned by the command | +| `task_id` | Async task ID when returned | +| `next_command` | Async continuation command when returned | +| `error` | Error summary when failed | +| `created_by_workflow` | Whether the resource was created by this workflow run | +| `rollback_eligible` | Whether this successful operation can be included in `rollback_plan` | + +Rules: + +1. Append a journal entry immediately after each write attempt. +2. `create_folder` and `create_node` entries MUST set `created_by_workflow=true`. +3. Successful `move_drive` and `move_wiki_node` entries may set `rollback_eligible=true` only when matching snapshot origin is supported. +4. Failed or pending moves MUST NOT enter automatic recovery execution. +5. Async operations are `pending` until `drive +task_result` proves completion. + +## State: ROLLBACK_CONFIRM + +Entry: user chose to try restoring after execution failure / verification mismatch, or explicitly asked to rollback. + +MUST: + +1. Generate `rollback_plan` from successful eligible move journal entries. +2. Use `execution_journal` current token / current node token as the recovery source. +3. Use `rollback_snapshot` original origin as the recovery target. +4. Generate recovery items in reverse successful move order. +5. Exclude failed, pending, and unsupported items from executable recovery. +6. Do not include delete actions in `rollback_plan`. +7. Ask for explicit restore execution confirmation. + +Confirmation output: + +```text +可恢复范围如下: + +| 项目 | 数量 | +|------|------| +| 可尝试恢复到原位置 | | +| 无法安全自动恢复 | | +| 未完成 / 等待中的移动 | | +| 本次新建目录 / 节点 | | + +恢复操作只会尝试把已成功移动的资源移回原位置,不会删除、重命名或修改权限。是否执行恢复? +``` + +If no move can be restored automatically, report that no automatic restore is available and move to `DONE`. + +## Recovery Command Rules + +Use only these command forms: + +```bash +# Drive resource back to original parent folder +lark-cli drive +move \ + --file-token \ + --type \ + --folder-token + +# Drive resource back to root +lark-cli drive +move \ + --file-token \ + --type + +# Wiki node back to original parent node +lark-cli wiki +move \ + --node-token \ + --target-parent-token + +# Wiki node back to original space root +lark-cli wiki +move \ + --node-token \ + --target-space-id +``` + +MUST NOT: + +- Use `wiki +move` docs-to-wiki mode. +- Move using path strings. +- Recover failed or pending moves as if they succeeded. +- Delete created folders / nodes in `ROLLBACK`. + +## State: ROLLBACK + +Entry: user explicitly confirmed restore execution. + +MUST: + +1. Execute only confirmed `rollback_plan` items. +2. Execute reverse moves in reverse successful move order. +3. Continue async move tasks with `drive +task_result` when needed. +4. Record recovery success / failure per rollback item. +5. Stop on blockers that make following recovery items unsafe. + +Progress output should stay concise: + +```text +恢复进度:已尝试 / 项,失败 项。 +``` + +## State: ROLLBACK_VERIFY + +Entry: recovery execution finished. + +MUST: + +1. Rescan the relevant Drive folder / Wiki nodes. +2. Compare each rollback item with its original origin. +3. Mark status per item. +4. If cleanup candidates clearly remain from this workflow run, transition to `ROLLBACK_CLEANUP_CONFIRM`. +5. Do not ask for deletion confirmation in this state. + +Verification table: + +| plan_id | 标题 | 原位置 | 当前实际位置 | 状态 | 失败原因 | +|---------|------|--------|--------------|------|----------| + +Status values: + +| Status | Meaning | +|--------|---------| +| `rollback_success` | Resource is back under the original parent / root | +| `rollback_failed` | Resource is still outside the original origin | +| `missing` | Resource cannot be found | +| `needs_manual_review` | Actual state is ambiguous or affected by external changes | + +Do not delete anything from this state. + +## State: ROLLBACK_CLEANUP_CONFIRM + +Entry: cleanup candidates exist after recovery, or user asks to view / perform cleanup after recovery. + +Cleanup is optional and separate from recovery. It may delete resources, so it requires separate confirmation. + +Candidate rules: + +1. Candidate MUST have `created_by_workflow=true` in `execution_journal`. +2. Candidate MUST be a Drive folder or Wiki node created by this workflow run. +3. Candidate MUST currently be empty, or contain only workflow-created cleanup candidates that are themselves safe to delete. +4. Candidate MUST NOT contain original resources, unknown resources, rollback-failed resources, or user-created resources. +5. If child origin is uncertain, mark the candidate `cleanup_blocked`. + +Generate `rollback_cleanup_plan` with: + +| Field | Meaning | +|-------|---------| +| `cleanup_id` | Stable cleanup row ID | +| `type` | `drive_folder` / `wiki_node` | +| `path` | Current path | +| `token` | Folder token or node token | +| `depth` | Current path depth | +| `safe_to_delete` | Whether deletion is allowed after confirmation | +| `blocker` | Reason when deletion is blocked | + +Confirmation output: + +```text +恢复已完成。本次整理新建的部分空目录 / 节点如下,是否需要删除? + +| 项目 | 数量 | +|------|------| +| 可删除的新建空目录 / 节点 | | +| 不可删除,需人工确认 | | + +注:删除只会作用于本次 workflow 新建且当前可安全清理的空目录 / 节点。 +``` + +If the user wants details, paginate cleanup items at 20 rows per page. + +## State: ROLLBACK_CLEANUP + +Entry: user explicitly confirmed cleanup deletion. + +MUST: + +1. Delete only `safe_to_delete=true` cleanup items. +2. Delete deepest paths first. +3. Record delete results in `rollback_cleanup_results`. +4. Continue async delete tasks with `drive +task_result` when needed. + +Command forms: + +```bash +# Delete workflow-created Drive folder +lark-cli drive +delete \ + --file-token \ + --type folder \ + --yes + +# Delete workflow-created Wiki node +lark-cli wiki +node-delete \ + --node-token \ + --obj-type wiki \ + --include-children=true \ + --yes +``` + +`--yes` is allowed only after the user explicitly confirmed cleanup deletion. + +MUST NOT: + +- Delete original resources. +- Delete unknown resources. +- Delete rollback-failed resources. +- Delete non-empty folders / nodes that contain anything outside cleanup candidates. +- Delete a knowledge space. + +## State: ROLLBACK_CLEANUP_VERIFY + +Entry: cleanup deletion finished. + +MUST: + +1. Verify each confirmed cleanup target is gone. +2. Report failed or pending deletes. +3. Stop after reporting cleanup results. + +Verification table: + +| 类型 | 路径 | token | 状态 | 失败原因 | +|------|------|-------|------|----------| + +Status values: + +| Status | Meaning | +|--------|---------| +| `deleted` | Cleanup target was deleted | +| `delete_pending` | Async deletion is still pending | +| `delete_failed` | Delete command failed | +| `still_exists` | Target still exists after deletion attempt | +| `skipped` | Target was not safe to delete or user did not confirm it | diff --git a/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md new file mode 100644 index 000000000..75e9370dd --- /dev/null +++ b/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md @@ -0,0 +1,224 @@ +# 知识整理工作流 + +This file is the single entry point for the knowledge organization workflow. It defines the global contract, state machine, and progressive loading map. Stage-specific rules live in phase files and MUST be loaded only when the workflow reaches the corresponding state. + +Phase files are references for this workflow, not independent skills. Do not route user requests directly to a phase file. + +## Required Context + +Before running this workflow, MUST read [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) for identity, authentication, permission handling, and write-operation confirmation rules. + +Load other skills / references progressively: + +- Wiki / personal library target: [`../../lark-wiki/SKILL.md`](../../lark-wiki/SKILL.md) +- Content read required: [`../../lark-doc/SKILL.md`](../../lark-doc/SKILL.md) and [`../../lark-doc/references/lark-doc-fetch.md`](../../lark-doc/references/lark-doc-fetch.md) +- Sheet down-drill required: [`../../lark-sheets/SKILL.md`](../../lark-sheets/SKILL.md) +- Base down-drill required: [`../../lark-base/SKILL.md`](../../lark-base/SKILL.md) + +## Agent Contract + +When this workflow is triggered, the agent MUST: + +1. Follow the `Execution State Machine` in order. +2. Maintain the fields in `Runtime State`. +3. Before executing a state, read the phase file listed in `Progressive Load Map`. +4. Do not pre-load all phase files. Load only the current state's required phase file unless a transition requires the next state. +5. Stop and wait whenever a state has `wait_for_user=true`. +6. Keep complete internal state even when user-facing output is paginated. +7. Never perform organization write operations before `EXEC_CONFIRM`; never perform recovery or cleanup writes before the corresponding explicit confirmation state. +8. Execute only commands allowed by `Command Map`. +9. Use command syntax, scope requirements, and API parameter rules from referenced skills / shortcut docs. +10. Convert internal enum values to natural-language Chinese labels in user-facing tables. +11. Do not invent recovery behavior; follow the active phase file's failure handling. +12. Maintain internal recovery state during execution, but do not mention recovery on the normal successful path. + +## Scope + +本 workflow 用于对指定 Drive 文件夹、Wiki 知识库、个人文档库或搜索范围做知识整理。默认只生成可审阅方案;只有用户明确确认执行范围后,才创建目录 / 节点或移动资源。 + +适用触发语包括: + +- "帮我整理我的云盘 / 文档库 / 知识库" +- "帮我盘点这个知识库,给出整理后的目录结构" +- "这个文件夹太乱了,先给我一个整理方案" +- "把知识库里的文档按项目 / 客户 / 时间 / 类型归类" +- "帮我找出未归档、临时、重复、空目录和命名混乱的内容" + +## Non-goals + +默认不生成: + +- 研究报告 +- 对比分析 +- 风险 / 结论 / 行动项 +- 引用来源列表 +- 权限治理报告 + +默认禁止执行: + +- 删除原有文件、文件夹、Wiki 节点或知识空间 +- owner 转移 +- 批量权限申请 +- 批量公开权限修改 +- 批量协作者权限修改 +- 任何资源重命名或标题修改;即使用户要求,也不由本 workflow 执行 + +仅在 rollback cleanup 阶段,允许删除本次 workflow 新建且当前可安全清理的空 Drive 文件夹或空 Wiki 节点,并且必须用户单独确认。不得删除知识空间。 + +如果用户明确要求其他非目标能力,必须转入对应专项流程,并单独确认风险。资源重命名 / 标题修改不属于本 workflow 的可执行能力。 + +## Responsibility Boundary + +| File | Owns | Must Not Own | +|------|------|--------------| +| `lark-drive-workflow-knowledge-organize.md` | Workflow trigger, global contract, state machine, progressive load map, command family allowlist | Stage-specific rules, output templates, execution details | +| `lark-drive-workflow-knowledge-organize-discovery.md` | `PARSE_SCOPE`, `INVENTORY`, target parsing, stop conditions, inventory limits, `ResourceItem` | Classification, plan generation, write execution | +| `lark-drive-workflow-knowledge-organize-analysis.md` | `CONTENT_READ`, `ISSUE_ANALYSIS`, `RULE_GENERATION`, low-confidence reads, issue rules, problem pagination, classification, target tree | Plan execution, write confirmation, verification | +| `lark-drive-workflow-knowledge-organize-planning.md` | `PLAN_GENERATION`, `EXEC_CONFIRM`, `PlanItem`, `DisplayItem`, plan pagination, plan revision, execution scope confirmation | Scope parsing, resource inventory, write execution, verification | +| `lark-drive-workflow-knowledge-organize-execution.md` | `EXECUTE`, `VERIFY`, `PathTokenMap`, write execution, progress reporting, verification, next suggestions, internal recovery hooks | Scope parsing, resource inventory, classification, plan generation, plan revision, rollback execution details | +| `lark-drive-workflow-knowledge-organize-rollback.md` | `ROLLBACK_CONFIRM`, `ROLLBACK`, `ROLLBACK_VERIFY`, `ROLLBACK_CLEANUP_CONFIRM`, `ROLLBACK_CLEANUP`, `ROLLBACK_CLEANUP_VERIFY`, recovery plan generation, recovery execution, cleanup verification | Scope parsing, resource inventory, classification, organization plan generation, normal write execution | + +## Runtime State + +Agent MUST maintain these internal fields during one workflow run: + +| Field | Meaning | +|-------|---------| +| `current_state` | Current state in `Execution State Machine` | +| `target_scope` | Parsed target: Drive folder, Wiki node, Wiki space, personal doc library, single resource, or search scope | +| `environment_profile` | Current environment and CLI profile, such as prod / BOE / PRE and config profile | +| `identity` | `user` by default unless user explicitly asks for app / bot perspective | +| `resource_items` | Complete normalized resource list from discovery | +| `partial` | Whether inventory or content-read limits were hit | +| `low_confidence_items` | Items requiring mandatory partial content read | +| `issue_summary` | Problem types, counts, evidence paths, and suggested handling | +| `classification_rules` | Rules used to map resources to target paths | +| `target_tree` | Proposed target folder / Wiki node tree | +| `source_container_disposition` | Reused / retired source folders or nodes and their intended handling | +| `plan_items` | Complete internal execution plan | +| `plan_version` | Internal version of the current complete plan, such as `v1` / `v2` | +| `active_plan_items` | Latest complete valid plan used for execution confirmation | +| `plan_revision_history` | Internal summaries of user-requested plan revisions | +| `last_user_correction` | Most recent user correction that changed classification, target tree, or plan scope | +| `display_page_state` | Current page, page size, filters, and total count for user-facing pagination | +| `path_token_map` | Mapping from target path to real `folder_token` / `node_token` | +| `execution_scope` | Full plan, current page, filtered subset, or no execution | +| `verification_results` | Per-plan-item verification result after execution | +| `rollback_snapshot` | Internal pre-write snapshot used only for recovery after failure or user-requested restore | +| `execution_journal` | Internal write-operation journal used only for recovery after failure or user-requested restore | +| `rollback_plan` | Internal recovery plan generated only after user asks to restore | +| `rollback_verification_results` | Per-item recovery verification result | +| `rollback_cleanup_plan` | Optional cleanup plan for workflow-created empty folders / nodes after recovery | +| `rollback_cleanup_results` | Cleanup verification result | + +## Execution State Machine + +| State | Entry Condition | Agent MUST Do | User-Facing Output | wait_for_user | Next State | +|-------|-----------------|---------------|--------------------|---------------|------------| +| `PARSE_SCOPE` | Workflow triggered | Load discovery phase; parse target, environment, identity, and target type | Scope confirmation or clarification question | `true` | `INVENTORY` | +| `INVENTORY` | Scope confirmed | Load discovery phase; recursively list resources and build `resource_items` | Inventory progress / summary; continue automatically unless blocked | `false` unless blocked | `CONTENT_READ` | +| `CONTENT_READ` | Inventory complete | Load analysis phase; identify low-confidence items and perform mandatory partial read when needed | Low-confidence read summary | `false` unless auth / permission blocks | `ISSUE_ANALYSIS` | +| `ISSUE_ANALYSIS` | Resource list and partial reads ready | Load analysis phase; detect structure problems, evidence, and organization approach | Inventory result, problems, organization approach, and decision options | `true` | `RULE_GENERATION` | +| `RULE_GENERATION` | User confirms organization approach | Load analysis phase; generate classification rules and `target_tree` | No separate stop; target tree is shown with plan generation | `false` | `PLAN_GENERATION` | +| `PLAN_GENERATION` | Target tree ready | Load planning phase; generate complete internal `plan_items`; show target tree plus plan overview or page | Target tree and plan overview / paginated plan page | `true` | `EXEC_CONFIRM` | +| `EXEC_CONFIRM` | User wants execution | Load planning phase; ask user to choose execution scope | Execution options and write-operation summary | `true` | `EXECUTE` or `DONE` | +| `EXECUTE` | User explicitly confirmed execution scope | Load execution phase; execute only whitelisted write operations for confirmed scope while maintaining internal recovery state | Progress reports for large or long-running execution; if blocked after successful moves, ask whether to try restoring to `整理前的位置` | `false` unless blocked / recovery offered | `VERIFY`, `ROLLBACK_CONFIRM`, or `DONE` | +| `VERIFY` | Execution finished | Load execution phase; rescan target scope and compare actual path/token against plan | Verification table and final summary; if serious mismatches exist, ask whether to try restoring to `整理前的位置` | `false` unless recovery offered | `DONE` or `ROLLBACK_CONFIRM` | +| `ROLLBACK_CONFIRM` | User asks to restore after execution failure / verification mismatch / explicit rollback request | Load rollback phase; generate internal `rollback_plan`; ask whether to execute recovery | Recoverable scope and restore confirmation | `true` | `ROLLBACK` or `DONE` | +| `ROLLBACK` | User explicitly confirms restore execution | Load rollback phase; execute confirmed reverse moves only | Recovery progress / result | `false` | `ROLLBACK_VERIFY` | +| `ROLLBACK_VERIFY` | Recovery execution finished | Load rollback phase; verify restored locations and decide whether cleanup candidates exist | Recovery verification result | `false` | `ROLLBACK_CLEANUP_CONFIRM` or `DONE` | +| `ROLLBACK_CLEANUP_CONFIRM` | Cleanup candidates exist after recovery, or user asks to clean workflow-created empty folders / nodes | Load rollback phase; generate cleanup plan and ask for delete confirmation | Cleanup candidates and delete confirmation | `true` | `ROLLBACK_CLEANUP` or `DONE` | +| `ROLLBACK_CLEANUP` | User explicitly confirms cleanup deletion | Load rollback phase; delete only confirmed workflow-created safe-empty folders / nodes | Cleanup progress / result | `false` | `ROLLBACK_CLEANUP_VERIFY` | +| `ROLLBACK_CLEANUP_VERIFY` | Cleanup deletion finished | Load rollback phase; verify deleted cleanup targets | Cleanup verification result | `false` | `DONE` | +| `DONE` | No more action | Stop | Final answer | `false` | End | + +## Progressive Load Map + +Agent MUST read the phase file for the active state before executing that state. + +| State | Required Phase File | +|-------|---------------------| +| `PARSE_SCOPE` | [`lark-drive-workflow-knowledge-organize-discovery.md`](lark-drive-workflow-knowledge-organize-discovery.md) | +| `INVENTORY` | [`lark-drive-workflow-knowledge-organize-discovery.md`](lark-drive-workflow-knowledge-organize-discovery.md) | +| `CONTENT_READ` | [`lark-drive-workflow-knowledge-organize-analysis.md`](lark-drive-workflow-knowledge-organize-analysis.md) | +| `ISSUE_ANALYSIS` | [`lark-drive-workflow-knowledge-organize-analysis.md`](lark-drive-workflow-knowledge-organize-analysis.md) | +| `RULE_GENERATION` | [`lark-drive-workflow-knowledge-organize-analysis.md`](lark-drive-workflow-knowledge-organize-analysis.md) | +| `PLAN_GENERATION` | [`lark-drive-workflow-knowledge-organize-planning.md`](lark-drive-workflow-knowledge-organize-planning.md) | +| `EXEC_CONFIRM` | [`lark-drive-workflow-knowledge-organize-planning.md`](lark-drive-workflow-knowledge-organize-planning.md) | +| `EXECUTE` | [`lark-drive-workflow-knowledge-organize-execution.md`](lark-drive-workflow-knowledge-organize-execution.md) | +| `VERIFY` | [`lark-drive-workflow-knowledge-organize-execution.md`](lark-drive-workflow-knowledge-organize-execution.md) | +| `ROLLBACK_CONFIRM` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | +| `ROLLBACK` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | +| `ROLLBACK_VERIFY` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | +| `ROLLBACK_CLEANUP_CONFIRM` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | +| `ROLLBACK_CLEANUP` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | +| `ROLLBACK_CLEANUP_VERIFY` | [`lark-drive-workflow-knowledge-organize-rollback.md`](lark-drive-workflow-knowledge-organize-rollback.md) | + +## Command Map + +Use only command families allowed for the current state. Detailed syntax belongs to referenced skills / shortcut docs. + +| State | Allowed Command Families | Purpose | +|-------|--------------------------|---------| +| `PARSE_SCOPE` | `drive +inspect`, `wiki +node-get`, `wiki +space-list`, `wiki spaces get`, `drive +search` | Resolve target scope | +| `INVENTORY` | `wiki +node-list`, `drive files list` (schema path: `drive.files.list`), `drive metas batch_query` | Recursively list and enrich resources | +| `CONTENT_READ` | `docs +fetch`, plus `lark-sheets` / `lark-base` when conditionally required | Partial content read for low-confidence items | +| `ISSUE_ANALYSIS` | No write commands | Analyze `resource_items` only | +| `RULE_GENERATION` | No write commands | Generate classification rules and target tree | +| `PLAN_GENERATION` | No write commands | Generate internal plan and user-facing pages | +| `EXEC_CONFIRM` | No write commands | Ask user to confirm execution scope | +| `EXECUTE` | `drive +create-folder`, `drive +move`, `wiki +node-create`, `wiki +move` existing-node mode only, `drive +task_result`, `drive +apply-permission` only when explicitly confirmed | Execute whitelisted writes | +| `VERIFY` | `wiki +node-list`, `drive files list` (schema path: `drive.files.list`), `drive +task_result` if async result remains pending | Verify actual result | +| `ROLLBACK_CONFIRM` | No write commands | Generate internal recovery plan and ask for restore confirmation | +| `ROLLBACK` | `drive +move`, `wiki +move` existing-node mode only, `drive +task_result` | Execute confirmed reverse moves | +| `ROLLBACK_VERIFY` | `wiki +node-list`, `drive files list` (schema path: `drive.files.list`), `drive +task_result` if async result remains pending | Verify recovery result | +| `ROLLBACK_CLEANUP_CONFIRM` | No write commands | Generate cleanup plan and ask for delete confirmation | +| `ROLLBACK_CLEANUP` | `drive +delete`, `wiki +node-delete`, `drive +task_result` | Delete only confirmed workflow-created safe-empty folders / nodes | +| `ROLLBACK_CLEANUP_VERIFY` | `wiki +node-list`, `drive files list` (schema path: `drive.files.list`), `drive +task_result` if async result remains pending | Verify cleanup deletion result | + +## Wiki Move Mode Constraint + +This workflow MUST NOT use `wiki +move` docs-to-wiki mode. Wiki moves MUST use existing Wiki node mode with `--node-token` only. + +## Permission Request Gate + +`drive +apply-permission` is a write operation and may notify the resource owner. If any state hits resource access denial: + +1. Stop the current state. +2. Show the single target resource, requested permission, reason / remark, and owner-notification implication when known. +3. Ask the user to confirm this single permission request. +4. Only after explicit confirmation, treat the permission request as a confirmed `EXECUTE` operation. +5. After the request is submitted or skipped, return to the blocked state only when the user asks to continue. + +Never request permission automatically, never batch permission requests, and never hide the owner-notification implication. + +## Transition Rules + +1. If `PARSE_SCOPE` cannot determine the target range, ask only for target range clarification and stop. +2. If auth or API scope is missing, follow `lark-shared` permission handling and stop. +3. If resource access permission is missing, follow `Permission Request Gate`. +4. If the user asks to inspect more pages, stay in `PLAN_GENERATION` and update `display_page_state`. +5. If the user declines execution in `EXEC_CONFIRM`, output the saved plan summary and move to `DONE`. +6. If execution fails for an item, record the failure and continue only when the failed item is independent; otherwise stop, report the blocker, and ask whether the user wants to try restoring to `整理前的位置` when any move already succeeded. +7. Do not load the rollback phase merely because a snapshot or journal exists. Load it only after execution failure, serious verification mismatch, or explicit user rollback request, and only after the user chooses to try restore. + +## References + +- [Discovery phase](lark-drive-workflow-knowledge-organize-discovery.md) +- [Analysis phase](lark-drive-workflow-knowledge-organize-analysis.md) +- [Planning phase](lark-drive-workflow-knowledge-organize-planning.md) +- [Execution phase](lark-drive-workflow-knowledge-organize-execution.md) +- [Rollback phase](lark-drive-workflow-knowledge-organize-rollback.md) +- [lark-shared](../../lark-shared/SKILL.md) +- [lark-drive](../SKILL.md) +- [lark-drive-search](lark-drive-search.md) +- [lark-drive-inspect](lark-drive-inspect.md) +- [lark-drive-apply-permission](lark-drive-apply-permission.md) +- [lark-drive-task-result](lark-drive-task-result.md) +- [lark-drive-delete](lark-drive-delete.md) +- [lark-wiki](../../lark-wiki/SKILL.md) +- [lark-wiki-node-delete](../../lark-wiki/references/lark-wiki-node-delete.md) +- [lark-doc](../../lark-doc/SKILL.md) +- [lark-doc-fetch](../../lark-doc/references/lark-doc-fetch.md) +- [lark-sheets](../../lark-sheets/SKILL.md) +- [lark-base](../../lark-base/SKILL.md) diff --git a/skills/lark-wiki/SKILL.md b/skills/lark-wiki/SKILL.md index 3da893306..b5fde05dd 100644 --- a/skills/lark-wiki/SKILL.md +++ b/skills/lark-wiki/SKILL.md @@ -24,6 +24,7 @@ metadata: ## 快速决策 +- 用户要**整理 / 盘点 / 归类 / 重构知识库、个人文档库、文档库目录或 Wiki 节点结构**,或要生成整理方案、目标目录树、移动计划时,不要只使用 Wiki 节点 API。必须先阅读 [`../lark-drive/references/lark-drive-workflow-knowledge-organize.md`](../lark-drive/references/lark-drive-workflow-knowledge-organize.md),该 workflow 负责 Drive / Wiki / 个人文档库的统一入口解析、资源盘点、分类计划、写前确认和结果验证。 - 用户给的是知识库 URL(`.../wiki/`),且后续要查成员/加成员/删成员:先调用 `lark-cli wiki spaces get_node --params '{"token":""}'` 获取 `space_id`,后续成员接口统一使用 `space_id`。 - 用户要**删除**知识空间(`wiki +delete-space`)但只给了名称或 URL:**不能**把名称 / URL 原样传给 `--space-id`,必须先解析出真实 `space_id`。解析方式: - URL(`.../wiki/`):`lark-cli wiki spaces get_node --params '{"token":""}' --format json`,读 `data.node.space_id`。 From 57ba4fae613cfa8ed7932f49dc89ed563a4a957b Mon Sep 17 00:00:00 2001 From: MaxHuang22 Date: Tue, 2 Jun 2026 16:55:02 +0800 Subject: [PATCH 3/4] feat: unconditionally inject --format flag for all shortcuts (#1156) * feat: unconditionally inject --format flag for all shortcuts Removes three HasFormat guards in runner.go so every shortcut gets --format regardless of the Shortcut.HasFormat field value. Shortcuts that already define a custom 'format' flag in Flags[] are skipped to avoid redefinition panics (e.g. mail +triage, +watch). HasFormat is retained in the struct but marked deprecated. Change-Id: I5e8fe07e839d5aed4cefaf7d753dabbaee68fb6e * test: isolate config dir in format-universal test Change-Id: I3a59942aa8a6753cd949ca42f2a19a72f032ff55 * test: revert unnecessary config-dir isolation (mount-only test) Change-Id: I0146e5a2f57f5419863bdeeaa1a662fd8f70bddf --- shortcuts/common/runner.go | 14 +++---- .../common/runner_format_universal_test.go | 39 +++++++++++++++++++ shortcuts/common/types.go | 2 +- 3 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 shortcuts/common/runner_format_universal_test.go diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index ea69cfd31..2a795d550 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -860,9 +860,7 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf } rctx.larkSDK = sdk - if s.HasFormat { - rctx.Format = rctx.Str("format") - } + rctx.Format = rctx.Str("format") rctx.JqExpr, _ = cmd.Flags().GetString("jq") return rctx, nil } @@ -1026,17 +1024,15 @@ func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f } cmd.Flags().Bool("dry-run", false, "print request without executing") - if s.HasFormat { + if cmd.Flags().Lookup("format") == nil { cmd.Flags().String("format", "json", "output format: json (default) | pretty | table | ndjson | csv") + cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp + }) } if s.Risk == "high-risk-write" { cmd.Flags().Bool("yes", false, "confirm high-risk operation") } cmd.Flags().StringP("jq", "q", "", "jq expression to filter JSON output") cmdutil.AddShortcutIdentityFlag(ctx, cmd, f, s.AuthTypes) - if s.HasFormat { - cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp - }) - } } diff --git a/shortcuts/common/runner_format_universal_test.go b/shortcuts/common/runner_format_universal_test.go new file mode 100644 index 000000000..777cd2e47 --- /dev/null +++ b/shortcuts/common/runner_format_universal_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/spf13/cobra" +) + +// TestShortcutMount_FormatFlagAlwaysRegistered verifies that --format is +// injected for every shortcut regardless of the HasFormat field value. +func TestShortcutMount_FormatFlagAlwaysRegistered(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, nil) + parent := &cobra.Command{Use: "root"} + shortcut := Shortcut{ + Service: "im", + Command: "+message-send", + Description: "send message", + HasFormat: false, // explicitly false — format must still be registered + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } + shortcut.Mount(parent, f) + + cmd, _, err := parent.Find([]string{"+message-send"}) + if err != nil { + t.Fatalf("Find() error = %v", err) + } + flag := cmd.Flags().Lookup("format") + if flag == nil { + t.Fatal("--format flag not registered; expected it to be injected even when HasFormat is false") + } + if flag.DefValue != "json" { + t.Errorf("--format default = %q, want %q", flag.DefValue, "json") + } +} diff --git a/shortcuts/common/types.go b/shortcuts/common/types.go index 76626c06c..bf3f0307b 100644 --- a/shortcuts/common/types.go +++ b/shortcuts/common/types.go @@ -49,7 +49,7 @@ type Shortcut struct { // Declarative fields (new framework). AuthTypes []string // supported identities: "user", "bot" (default: ["user"]) Flags []Flag // flag definitions; --dry-run is auto-injected - HasFormat bool // auto-inject --format flag (json|pretty|table|ndjson|csv) + HasFormat bool // Deprecated: --format is now always injected; this field has no effect. Tips []string // optional tips shown in --help output Hidden bool // hide from --help / tab completion (still executable); use when deprecating a command in favor of a replacement From e57d97f341dfce765583f94f7981ac11cf47df18 Mon Sep 17 00:00:00 2001 From: zgz2048 Date: Tue, 2 Jun 2026 17:30:10 +0800 Subject: [PATCH 4/4] docs: optimize base skill references (#1171) --- shortcuts/base/base_advperm_disable.go | 4 + shortcuts/base/base_advperm_enable.go | 3 + shortcuts/base/base_copy.go | 5 + shortcuts/base/base_create.go | 4 + shortcuts/base/base_data_query.go | 7 +- shortcuts/base/base_execute_test.go | 2 +- shortcuts/base/base_form_create.go | 3 + shortcuts/base/base_form_delete.go | 4 + shortcuts/base/base_form_list.go | 2 +- shortcuts/base/base_form_questions_delete.go | 3 + shortcuts/base/base_form_questions_list.go | 3 + shortcuts/base/base_get.go | 5 +- shortcuts/base/base_role_create.go | 7 +- shortcuts/base/base_role_delete.go | 6 + shortcuts/base/base_role_get.go | 4 + shortcuts/base/base_role_list.go | 4 + shortcuts/base/base_role_update.go | 8 +- shortcuts/base/base_shortcut_helpers.go | 2 +- shortcuts/base/base_shortcuts_test.go | 425 ++++++++++++++++- shortcuts/base/dashboard_arrange.go | 3 + shortcuts/base/dashboard_block_create.go | 13 +- shortcuts/base/dashboard_block_delete.go | 4 + shortcuts/base/dashboard_block_get.go | 5 + shortcuts/base/dashboard_block_get_data.go | 1 + shortcuts/base/dashboard_block_list.go | 6 +- shortcuts/base/dashboard_block_update.go | 12 +- shortcuts/base/dashboard_create.go | 5 +- shortcuts/base/dashboard_delete.go | 5 + shortcuts/base/dashboard_get.go | 3 + shortcuts/base/dashboard_list.go | 5 +- shortcuts/base/dashboard_update.go | 2 +- shortcuts/base/field_create.go | 3 +- shortcuts/base/field_delete.go | 6 +- shortcuts/base/field_get.go | 7 +- shortcuts/base/field_list.go | 2 +- shortcuts/base/field_search_options.go | 6 +- shortcuts/base/field_update.go | 5 +- shortcuts/base/helpers_test.go | 8 +- shortcuts/base/high_risk.go | 6 + shortcuts/base/record_batch_create.go | 13 +- shortcuts/base/record_batch_update.go | 13 +- shortcuts/base/record_delete.go | 4 + shortcuts/base/record_history_list.go | 6 +- shortcuts/base/record_ops.go | 7 + shortcuts/base/record_search.go | 8 +- shortcuts/base/record_share_link_create.go | 5 +- shortcuts/base/record_upload_attachment.go | 1 + shortcuts/base/record_upsert.go | 12 +- shortcuts/base/table_create.go | 6 +- shortcuts/base/table_delete.go | 7 +- shortcuts/base/table_get.go | 6 +- shortcuts/base/table_list.go | 2 +- shortcuts/base/view_create.go | 8 +- shortcuts/base/view_delete.go | 6 +- shortcuts/base/view_list.go | 2 +- shortcuts/base/view_set_card.go | 7 +- shortcuts/base/view_set_filter.go | 3 +- shortcuts/base/view_set_group.go | 8 +- shortcuts/base/view_set_sort.go | 8 +- shortcuts/base/view_set_timebar.go | 7 +- shortcuts/base/view_set_visible_fields.go | 7 +- shortcuts/base/workflow_create.go | 10 +- shortcuts/base/workflow_disable.go | 4 + shortcuts/base/workflow_enable.go | 5 + shortcuts/base/workflow_get.go | 8 +- shortcuts/base/workflow_list.go | 6 +- shortcuts/base/workflow_update.go | 11 +- skill-template/domains/base.md | 6 +- skills/lark-base/SKILL.md | 438 +++++------------- .../references/dashboard-block-data-config.md | 4 +- skills/lark-base/references/examples.md | 140 ------ .../references/lark-base-advperm-disable.md | 83 ---- .../references/lark-base-advperm-enable.md | 80 ---- .../references/lark-base-base-copy.md | 74 --- .../references/lark-base-base-create.md | 68 --- .../references/lark-base-base-get.md | 39 -- .../references/lark-base-dashboard-arrange.md | 83 ---- .../lark-base-dashboard-block-create.md | 108 ----- .../lark-base-dashboard-block-delete.md | 46 -- .../lark-base-dashboard-block-get-data.md | 6 +- .../lark-base-dashboard-block-get.md | 57 --- .../lark-base-dashboard-block-list.md | 53 --- .../lark-base-dashboard-block-update.md | 84 ---- .../references/lark-base-dashboard-create.md | 73 --- .../references/lark-base-dashboard-delete.md | 44 -- .../references/lark-base-dashboard-get.md | 59 --- .../references/lark-base-dashboard-list.md | 52 --- .../references/lark-base-dashboard-update.md | 69 --- .../references/lark-base-dashboard.md | 51 +- .../references/lark-base-data-analysis-sop.md | 12 +- .../references/lark-base-data-query-guide.md | 61 +++ .../references/lark-base-data-query.md | 8 +- .../references/lark-base-field-create.md | 3 +- .../references/lark-base-field-delete.md | 51 -- .../references/lark-base-field-get.md | 42 -- ...-properties.md => lark-base-field-json.md} | 4 +- .../references/lark-base-field-list.md | 44 -- .../lark-base-field-search-options.md | 48 -- .../references/lark-base-field-update.md | 5 +- .../lark-base/references/lark-base-field.md | 23 - .../references/lark-base-form-create.md | 87 ---- .../references/lark-base-form-delete.md | 64 --- .../references/lark-base-form-detail.md | 328 ++----------- .../references/lark-base-form-get.md | 68 --- .../references/lark-base-form-list.md | 73 --- .../lark-base-form-questions-delete.md | 68 --- .../lark-base-form-questions-list.md | 84 ---- .../references/lark-base-form-questions.md | 23 - .../references/lark-base-form-submit.md | 5 +- .../references/lark-base-form-update.md | 82 ---- skills/lark-base/references/lark-base-form.md | 25 - .../lark-base/references/lark-base-history.md | 16 - .../lark-base-record-batch-create.md | 1 - .../lark-base-record-batch-update.md | 1 - .../references/lark-base-record-delete.md | 62 --- .../lark-base-record-history-list.md | 79 +--- .../lark-base-record-share-link-create.md | 72 --- .../references/lark-base-record-upsert.md | 1 - .../lark-base/references/lark-base-record.md | 31 -- .../references/lark-base-role-create.md | 89 ---- .../references/lark-base-role-delete.md | 83 ---- .../references/lark-base-role-get.md | 87 ---- .../references/lark-base-role-guide.md | 65 +++ .../references/lark-base-role-list.md | 81 ---- .../references/lark-base-role-update.md | 94 ---- .../references/lark-base-table-create.md | 62 --- .../references/lark-base-table-delete.md | 51 -- .../references/lark-base-table-get.md | 46 -- .../references/lark-base-table-list.md | 43 -- .../references/lark-base-table-update.md | 49 -- .../lark-base/references/lark-base-table.md | 20 - .../references/lark-base-view-create.md | 50 -- .../references/lark-base-view-delete.md | 48 -- .../references/lark-base-view-get-card.md | 38 -- .../references/lark-base-view-get-filter.md | 38 -- .../references/lark-base-view-get-group.md | 38 -- .../references/lark-base-view-get-sort.md | 38 -- .../references/lark-base-view-get-timebar.md | 38 -- .../lark-base-view-get-visible-fields.md | 28 -- .../references/lark-base-view-get.md | 38 -- .../references/lark-base-view-list.md | 44 -- .../references/lark-base-view-rename.md | 44 -- .../references/lark-base-view-set-card.md | 55 --- .../references/lark-base-view-set-filter.md | 4 +- .../references/lark-base-view-set-group.md | 65 --- .../references/lark-base-view-set-sort.md | 63 --- .../references/lark-base-view-set-timebar.md | 51 -- .../lark-base-view-set-visible-fields.md | 46 -- skills/lark-base/references/lark-base-view.md | 44 -- .../references/lark-base-workflow-create.md | 180 ------- .../references/lark-base-workflow-disable.md | 94 ---- .../references/lark-base-workflow-enable.md | 94 ---- .../references/lark-base-workflow-get.md | 147 ------ .../references/lark-base-workflow-guide.md | 10 +- .../references/lark-base-workflow-list.md | 124 ----- .../references/lark-base-workflow-schema.md | 9 +- .../references/lark-base-workflow-update.md | 167 ------- .../references/lark-base-workflow.md | 23 - .../references/lark-base-workspace.md | 18 - skills/lark-base/references/role-config.md | 18 +- 160 files changed, 1081 insertions(+), 5329 deletions(-) create mode 100644 shortcuts/base/high_risk.go delete mode 100644 skills/lark-base/references/examples.md delete mode 100644 skills/lark-base/references/lark-base-advperm-disable.md delete mode 100644 skills/lark-base/references/lark-base-advperm-enable.md delete mode 100644 skills/lark-base/references/lark-base-base-copy.md delete mode 100644 skills/lark-base/references/lark-base-base-create.md delete mode 100644 skills/lark-base/references/lark-base-base-get.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-arrange.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-block-create.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-block-delete.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-block-get.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-block-list.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-block-update.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-create.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-delete.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-get.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-list.md delete mode 100644 skills/lark-base/references/lark-base-dashboard-update.md create mode 100644 skills/lark-base/references/lark-base-data-query-guide.md delete mode 100644 skills/lark-base/references/lark-base-field-delete.md delete mode 100644 skills/lark-base/references/lark-base-field-get.md rename skills/lark-base/references/{lark-base-shortcut-field-properties.md => lark-base-field-json.md} (97%) delete mode 100644 skills/lark-base/references/lark-base-field-list.md delete mode 100644 skills/lark-base/references/lark-base-field-search-options.md delete mode 100644 skills/lark-base/references/lark-base-field.md delete mode 100644 skills/lark-base/references/lark-base-form-create.md delete mode 100644 skills/lark-base/references/lark-base-form-delete.md delete mode 100644 skills/lark-base/references/lark-base-form-get.md delete mode 100644 skills/lark-base/references/lark-base-form-list.md delete mode 100644 skills/lark-base/references/lark-base-form-questions-delete.md delete mode 100644 skills/lark-base/references/lark-base-form-questions-list.md delete mode 100644 skills/lark-base/references/lark-base-form-questions.md delete mode 100644 skills/lark-base/references/lark-base-form-update.md delete mode 100644 skills/lark-base/references/lark-base-form.md delete mode 100644 skills/lark-base/references/lark-base-history.md delete mode 100644 skills/lark-base/references/lark-base-record-delete.md delete mode 100644 skills/lark-base/references/lark-base-record-share-link-create.md delete mode 100644 skills/lark-base/references/lark-base-record.md delete mode 100644 skills/lark-base/references/lark-base-role-create.md delete mode 100644 skills/lark-base/references/lark-base-role-delete.md delete mode 100644 skills/lark-base/references/lark-base-role-get.md create mode 100644 skills/lark-base/references/lark-base-role-guide.md delete mode 100644 skills/lark-base/references/lark-base-role-list.md delete mode 100644 skills/lark-base/references/lark-base-role-update.md delete mode 100644 skills/lark-base/references/lark-base-table-create.md delete mode 100644 skills/lark-base/references/lark-base-table-delete.md delete mode 100644 skills/lark-base/references/lark-base-table-get.md delete mode 100644 skills/lark-base/references/lark-base-table-list.md delete mode 100644 skills/lark-base/references/lark-base-table-update.md delete mode 100644 skills/lark-base/references/lark-base-table.md delete mode 100644 skills/lark-base/references/lark-base-view-create.md delete mode 100644 skills/lark-base/references/lark-base-view-delete.md delete mode 100644 skills/lark-base/references/lark-base-view-get-card.md delete mode 100644 skills/lark-base/references/lark-base-view-get-filter.md delete mode 100644 skills/lark-base/references/lark-base-view-get-group.md delete mode 100644 skills/lark-base/references/lark-base-view-get-sort.md delete mode 100644 skills/lark-base/references/lark-base-view-get-timebar.md delete mode 100644 skills/lark-base/references/lark-base-view-get-visible-fields.md delete mode 100644 skills/lark-base/references/lark-base-view-get.md delete mode 100644 skills/lark-base/references/lark-base-view-list.md delete mode 100644 skills/lark-base/references/lark-base-view-rename.md delete mode 100644 skills/lark-base/references/lark-base-view-set-card.md delete mode 100644 skills/lark-base/references/lark-base-view-set-group.md delete mode 100644 skills/lark-base/references/lark-base-view-set-sort.md delete mode 100644 skills/lark-base/references/lark-base-view-set-timebar.md delete mode 100644 skills/lark-base/references/lark-base-view-set-visible-fields.md delete mode 100644 skills/lark-base/references/lark-base-view.md delete mode 100644 skills/lark-base/references/lark-base-workflow-create.md delete mode 100644 skills/lark-base/references/lark-base-workflow-disable.md delete mode 100644 skills/lark-base/references/lark-base-workflow-enable.md delete mode 100644 skills/lark-base/references/lark-base-workflow-get.md delete mode 100644 skills/lark-base/references/lark-base-workflow-list.md delete mode 100644 skills/lark-base/references/lark-base-workflow-update.md delete mode 100644 skills/lark-base/references/lark-base-workflow.md delete mode 100644 skills/lark-base/references/lark-base-workspace.md diff --git a/shortcuts/base/base_advperm_disable.go b/shortcuts/base/base_advperm_disable.go index 3266d9bca..59266bb19 100644 --- a/shortcuts/base/base_advperm_disable.go +++ b/shortcuts/base/base_advperm_disable.go @@ -25,6 +25,10 @@ var BaseAdvpermDisable = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, }, + Tips: []string{ + baseHighRiskYesTip, + "Disabling advanced permissions invalidates existing custom roles; confirm the target Base before passing --yes.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/base_advperm_enable.go b/shortcuts/base/base_advperm_enable.go index 2f7437ca4..03a248e23 100644 --- a/shortcuts/base/base_advperm_enable.go +++ b/shortcuts/base/base_advperm_enable.go @@ -25,6 +25,9 @@ var BaseAdvpermEnable = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, }, + Tips: []string{ + "Caller must be a Base admin; enable advanced permissions before creating or updating roles.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/base_copy.go b/shortcuts/base/base_copy.go index cb12e0e01..5e2210a85 100644 --- a/shortcuts/base/base_copy.go +++ b/shortcuts/base/base_copy.go @@ -24,6 +24,11 @@ var BaseBaseCopy = common.Shortcut{ {Name: "without-content", Type: "bool", Desc: "copy structure only"}, {Name: "time-zone", Desc: "time zone, e.g. Asia/Shanghai"}, }, + Tips: []string{ + `Example: lark-cli base +base-copy --base-token --name "Copy of Project Tracker"`, + "Use --without-content when the user wants only structure.", + "If copied as bot, output may include permission_grant; report it so the user knows whether they can open the new Base.", + }, DryRun: dryRunBaseCopy, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeBaseCopy(runtime) diff --git a/shortcuts/base/base_create.go b/shortcuts/base/base_create.go index af4286638..47d5e27ed 100644 --- a/shortcuts/base/base_create.go +++ b/shortcuts/base/base_create.go @@ -22,6 +22,10 @@ var BaseBaseCreate = common.Shortcut{ {Name: "folder-token", Desc: "folder token for destination"}, {Name: "time-zone", Desc: "time zone, e.g. Asia/Shanghai"}, }, + Tips: []string{ + `Example: lark-cli base +base-create --name "Project Tracker" --time-zone Asia/Shanghai`, + "If created as bot, output may include permission_grant; report it so the user knows whether they can open the new Base.", + }, DryRun: dryRunBaseCreate, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeBaseCreate(runtime) diff --git a/shortcuts/base/base_data_query.go b/shortcuts/base/base_data_query.go index f3724c1b3..22af5c9fb 100644 --- a/shortcuts/base/base_data_query.go +++ b/shortcuts/base/base_data_query.go @@ -20,7 +20,12 @@ var BaseDataQuery = common.Shortcut{ AuthTypes: authTypes(), Flags: []common.Flag{ baseTokenFlag(true), - {Name: "dsl", Desc: "query JSON DSL (LiteQuery Protocol)", Required: true}, + {Name: "dsl", Desc: "query JSON DSL; read lark-base-data-query-guide.md first, then lark-base-data-query.md for the full DSL SSOT", Required: true}, + }, + Tips: []string{ + "Use +data-query for server-side aggregation, grouping, filtering, sorting, and Top N queries.", + "Read lark-base-data-query-guide.md for common fewshots; use lark-base-data-query.md only when the full DSL reference is needed.", + "`dimensions` and `measures` cannot both be empty.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { var dsl map[string]interface{} diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 97e500e50..3ec741ae9 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -515,7 +515,7 @@ func TestBaseObjectJSONShortcutsRejectArrayInDryRun(t *testing.T) { if !strings.Contains(err.Error(), "--json must be a JSON object") { t.Fatalf("err=%v", err) } - if !strings.Contains(err.Error(), "lark-base skill") { + if !strings.Contains(err.Error(), "match the documented shape") { t.Fatalf("err=%v", err) } if strings.Contains(err.Error(), "array") { diff --git a/shortcuts/base/base_form_create.go b/shortcuts/base/base_form_create.go index 978d24463..238892aa7 100644 --- a/shortcuts/base/base_form_create.go +++ b/shortcuts/base/base_form_create.go @@ -25,6 +25,9 @@ var BaseFormCreate = common.Shortcut{ {Name: "name", Desc: "form name", Required: true}, {Name: "description", Desc: `form description (plain text or markdown link like [text](https://example.com))`}, }, + Tips: []string{ + "Record the returned form_id; form question create/list/update/delete commands need it.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms"). diff --git a/shortcuts/base/base_form_delete.go b/shortcuts/base/base_form_delete.go index f6be4691e..75735ba70 100644 --- a/shortcuts/base/base_form_delete.go +++ b/shortcuts/base/base_form_delete.go @@ -22,6 +22,10 @@ var BaseFormDelete = common.Shortcut{ {Name: "table-id", Desc: "table ID", Required: true}, {Name: "form-id", Desc: "form ID", Required: true}, }, + Tips: []string{ + "Use +form-list or +form-get first when the form target is ambiguous.", + baseHighRiskYesTip, + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id"). diff --git a/shortcuts/base/base_form_list.go b/shortcuts/base/base_form_list.go index 07ead6e5b..34f5bfe0f 100644 --- a/shortcuts/base/base_form_list.go +++ b/shortcuts/base/base_form_list.go @@ -23,7 +23,7 @@ var BaseFormsList = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "Base token (base_token)", Required: true}, {Name: "table-id", Desc: "table ID", Required: true}, - {Name: "page-size", Type: "int", Default: "100", Desc: "page size per request (max 100)"}, + {Name: "page-size", Type: "int", Default: "100", Desc: "page size per request, max 100"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). diff --git a/shortcuts/base/base_form_questions_delete.go b/shortcuts/base/base_form_questions_delete.go index 4a3b03873..8b1472220 100644 --- a/shortcuts/base/base_form_questions_delete.go +++ b/shortcuts/base/base_form_questions_delete.go @@ -25,6 +25,9 @@ var BaseFormQuestionsDelete = common.Shortcut{ {Name: "form-id", Desc: "form ID", Required: true}, {Name: "question-ids", Desc: `JSON array of question IDs to delete, max 10 items, e.g. '["q_001","q_002"]'`, Required: true}, }, + Tips: []string{ + baseHighRiskYesTip, + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions"). diff --git a/shortcuts/base/base_form_questions_list.go b/shortcuts/base/base_form_questions_list.go index 050d3c358..3384fd3d9 100644 --- a/shortcuts/base/base_form_questions_list.go +++ b/shortcuts/base/base_form_questions_list.go @@ -25,6 +25,9 @@ var BaseFormQuestionsList = common.Shortcut{ {Name: "table-id", Desc: "table ID", Required: true}, {Name: "form-id", Desc: "form ID", Required: true}, }, + Tips: []string{ + "Use returned question id values for +form-questions-update and +form-questions-delete.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions"). diff --git a/shortcuts/base/base_get.go b/shortcuts/base/base_get.go index 48e7080ea..6a869ff4a 100644 --- a/shortcuts/base/base_get.go +++ b/shortcuts/base/base_get.go @@ -17,7 +17,10 @@ var BaseBaseGet = common.Shortcut{ Scopes: []string{"base:app:read"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true)}, - DryRun: dryRunBaseGet, + Tips: []string{ + "Use a real Base token; workspace tokens and wiki tokens are not accepted by this command.", + }, + DryRun: dryRunBaseGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeBaseGet(runtime) }, diff --git a/shortcuts/base/base_role_create.go b/shortcuts/base/base_role_create.go index 18b4cb2dc..cf887ece5 100644 --- a/shortcuts/base/base_role_create.go +++ b/shortcuts/base/base_role_create.go @@ -25,7 +25,12 @@ var BaseRoleCreate = common.Shortcut{ AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, - {Name: "json", Desc: `body JSON (AdvPermBaseRoleConfig), e.g. {"role_name":"Reviewer","role_type":"custom_role","table_rule_map":{...}}`, Required: true}, + {Name: "json", Desc: "role config JSON; read lark-base-role-guide.md and role-config.md before constructing permissions", Required: true}, + }, + Tips: []string{ + "Requires advanced permissions to be enabled and the caller to be a Base admin.", + "Use lark-base-role-guide.md as the entry guide and role-config.md as the role permission JSON SSOT.", + "Create supports custom_role only.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/base_role_delete.go b/shortcuts/base/base_role_delete.go index 0c5627fc3..9d7f14c4b 100644 --- a/shortcuts/base/base_role_delete.go +++ b/shortcuts/base/base_role_delete.go @@ -26,6 +26,12 @@ var BaseRoleDelete = common.Shortcut{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true}, }, + Tips: []string{ + baseHighRiskYesTip, + "Requires advanced permissions to be enabled and the caller to be a Base admin.", + "Only custom roles can be deleted; system roles cannot be deleted.", + "Use +role-get first if the role target is ambiguous, then pass --yes to confirm deletion.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/base_role_get.go b/shortcuts/base/base_role_get.go index ada1c9940..626b7f925 100644 --- a/shortcuts/base/base_role_get.go +++ b/shortcuts/base/base_role_get.go @@ -27,6 +27,10 @@ var BaseRoleGet = common.Shortcut{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true}, }, + Tips: []string{ + "Requires advanced permissions to be enabled and the caller to be a Base admin.", + "Use before +role-update to inspect the current full permission config.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/base_role_list.go b/shortcuts/base/base_role_list.go index 08bc29341..110192c12 100644 --- a/shortcuts/base/base_role_list.go +++ b/shortcuts/base/base_role_list.go @@ -26,6 +26,10 @@ var BaseRoleList = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, }, + Tips: []string{ + "Requires advanced permissions to be enabled and the caller to be a Base admin.", + "Returns role summaries; use +role-get for the full permission config.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/base_role_update.go b/shortcuts/base/base_role_update.go index 8c05c4d89..30e1e4ac3 100644 --- a/shortcuts/base/base_role_update.go +++ b/shortcuts/base/base_role_update.go @@ -26,7 +26,13 @@ var BaseRoleUpdate = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true}, - {Name: "json", Desc: `body JSON (delta AdvPermBaseRoleConfig), e.g. {"role_name":"New Name","role_type":"custom_role","table_rule_map":{...}}`, Required: true}, + {Name: "json", Desc: "delta role config JSON; read lark-base-role-guide.md and role-config.md before changing permissions", Required: true}, + }, + Tips: []string{ + baseHighRiskYesTip, + "Requires advanced permissions to be enabled and the caller to be a Base admin.", + "Update is a delta merge: only changed fields are updated, others remain unchanged.", + "Use lark-base-role-guide.md as the entry guide and role-config.md as the role permission JSON SSOT.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/base_shortcut_helpers.go b/shortcuts/base/base_shortcut_helpers.go index 67ea64c6e..8d42fe4da 100644 --- a/shortcuts/base/base_shortcut_helpers.go +++ b/shortcuts/base/base_shortcut_helpers.go @@ -63,7 +63,7 @@ func loadJSONInput(pc *parseCtx, raw string, flagName string) (string, error) { } func jsonInputTip(flagName string) string { - return fmt.Sprintf("tip: pass a valid JSON directly, or use --%s @file.json; use the lark-base skill or this command's reference to find the expected body", flagName) + return fmt.Sprintf("tip: pass a valid JSON directly, or use --%s @file.json; for complex JSON/DSL, read the lark-base reference and match the documented shape", flagName) } func formatJSONError(flagName string, target string, err error) error { diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 3e3926566..d6f455730 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -198,6 +198,25 @@ func TestBaseDeleteShortcutsRisk(t *testing.T) { } } +func TestBaseHighRiskShortcutsTipsGuideAgents(t *testing.T) { + for _, shortcut := range Shortcuts() { + if shortcut.Risk != "high-risk-write" { + continue + } + parent := &cobra.Command{Use: "base"} + shortcut.Mount(parent, &cmdutil.Factory{}) + cmd := parent.Commands()[0] + flag := cmd.Flags().Lookup("yes") + if flag == nil { + t.Fatalf("%s missing --yes flag", shortcut.Command) + } + tips := strings.Join(cmdutil.GetTips(cmd), "\n") + if !strings.Contains(tips, "pass --yes without asking again") { + t.Fatalf("%s tips missing agent guidance:\n%s", shortcut.Command, tips) + } + } +} + func TestBaseFieldCreateHelpHidesReadGuideFlag(t *testing.T) { parent := &cobra.Command{Use: "base"} BaseFieldCreate.Mount(parent, &cmdutil.Factory{}) @@ -251,20 +270,19 @@ func TestBaseRecordReadHelpGuidesAgents(t *testing.T) { name: "record search", shortcut: BaseRecordSearch, wantHelp: []string{ - "requires keyword/search_fields", - "optional select_fields/view_id/offset/limit", + `record search JSON object, e.g. {"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"limit":50}`, + "for keyword search only", "output format: markdown (default) | json", }, wantTips: []string{ - `lark-cli base +record-search --base-token --table-id --json`, - `"select_fields":["Name","Status"]`, - `JSON shape: {"keyword":"","search_fields":[""]`, + "Happy path fields: keyword (string), search_fields", "search_fields length 1-20", "limit range 1-200 defaults to 10", "view_id scopes search to records in that view", "Default output is markdown", "only for keyword search", "lark-base record read SOP", + "inventing search JSON", }, }, { @@ -311,6 +329,401 @@ func TestBaseRecordReadHelpGuidesAgents(t *testing.T) { } } +func TestBaseDashboardHelpGuidesAgents(t *testing.T) { + tests := []struct { + name string + shortcut common.Shortcut + wantTips []string + }{ + { + name: "dashboard list", + shortcut: BaseDashboardList, + wantTips: []string{ + "Use returned dashboard_id values", + }, + }, + { + name: "dashboard get", + shortcut: BaseDashboardGet, + wantTips: []string{ + "block-level details", + }, + }, + { + name: "dashboard create", + shortcut: BaseDashboardCreate, + wantTips: []string{ + "Record the returned dashboard_id", + }, + }, + { + name: "dashboard update", + shortcut: BaseDashboardUpdate, + wantTips: []string{}, + }, + { + name: "dashboard delete", + shortcut: BaseDashboardDelete, + wantTips: []string{ + "lark-cli base +dashboard-delete --base-token --dashboard-id --yes", + "also deletes its blocks", + "pass --yes", + }, + }, + { + name: "dashboard arrange", + shortcut: BaseDashboardArrange, + wantTips: []string{ + "not deterministic or position-specific", + }, + }, + { + name: "dashboard block list", + shortcut: BaseDashboardBlockList, + wantTips: []string{ + "lark-cli base +dashboard-block-list --base-token --dashboard-id ", + "Use returned block_id and type values", + }, + }, + { + name: "dashboard block get", + shortcut: BaseDashboardBlockGet, + wantTips: []string{ + "lark-cli base +dashboard-block-get --base-token --dashboard-id --block-id ", + "metadata such as name, type, layout, and data_config", + "computed chart result", + }, + }, + { + name: "dashboard block get data", + shortcut: BaseDashboardBlockGetData, + wantTips: []string{ + "lark-cli base +dashboard-block-get-data --base-token --block-id ", + "does not need --dashboard-id", + "computed chart protocol JSON", + }, + }, + { + name: "dashboard block create", + shortcut: BaseDashboardBlockCreate, + wantTips: []string{ + `lark-cli base +dashboard-block-create --base-token --dashboard-id --name "Order Count" --type statistics --data-config '{"table_name":"Orders","count_all":true}'`, + `--type text --data-config '{"text":"# Sales Dashboard"}'`, + "+table-list and +field-list", + "not table_id or field_id", + "dashboard-block-data-config.md as the SSOT", + "do not invent data_config from natural language", + "sequentially", + }, + }, + { + name: "dashboard block update", + shortcut: BaseDashboardBlockUpdate, + wantTips: []string{ + `lark-cli base +dashboard-block-update --base-token --dashboard-id --block-id --name "Total Sales"`, + `--data-config '{"series":[{"field_name":"Amount","rollup":"SUM"}]}'`, + "dashboard-block-data-config.md as the SSOT", + "do not invent data_config from natural language", + "Block type cannot be changed", + "top-level keys", + }, + }, + { + name: "dashboard block delete", + shortcut: BaseDashboardBlockDelete, + wantTips: []string{ + "lark-cli base +dashboard-block-delete --base-token --dashboard-id --block-id --yes", + "pass --yes", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &cobra.Command{Use: "base"} + tt.shortcut.Mount(parent, &cmdutil.Factory{}) + cmd := parent.Commands()[0] + + tips := strings.Join(cmdutil.GetTips(cmd), "\n") + for _, want := range tt.wantTips { + if !strings.Contains(tips, want) { + t.Fatalf("tips missing %q:\n%s", want, tips) + } + } + }) + } +} + +func TestBaseWorkflowHelpGuidesAgents(t *testing.T) { + tests := []struct { + name string + shortcut common.Shortcut + wantTips []string + }{ + { + name: "workflow list", + shortcut: BaseWorkflowList, + wantTips: []string{ + "workflow_id values with wkf prefix", + "auto-paginates", + }, + }, + { + name: "workflow get", + shortcut: BaseWorkflowGet, + wantTips: []string{ + "workflow-id must start with wkf", + "steps may be an empty array", + "Use +workflow-get before +workflow-update", + "lark-base-workflow-schema.md", + }, + }, + { + name: "workflow create", + shortcut: BaseWorkflowCreate, + wantTips: []string{ + "lark-cli base +workflow-create --base-token --json @workflow.json", + "client_token is required", + "New workflows are created disabled", + "+table-list and +field-list", + "Step ids must be unique", + "lark-base-workflow-guide.md as the entry guide", + "lark-base-workflow-schema.md as the steps JSON SSOT", + "do not invent steps[].type/data/next/children from natural language", + }, + }, + { + name: "workflow update", + shortcut: BaseWorkflowUpdate, + wantTips: []string{ + "lark-cli base +workflow-update --base-token --workflow-id --json @workflow.json", + "PUT uses full replacement semantics", + "Use +workflow-get first", + "keep title/status/steps fields", + "workflow-id must start with wkf", + "Updating does not enable or disable", + "do not invent steps[].type/data/next/children from natural language", + }, + }, + { + name: "workflow enable", + shortcut: BaseWorkflowEnable, + wantTips: []string{ + "workflow-id must start with wkf", + "does not modify steps", + "New workflows are created disabled", + }, + }, + { + name: "workflow disable", + shortcut: BaseWorkflowDisable, + wantTips: []string{ + "workflow-id must start with wkf", + "does not delete the workflow or its steps", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &cobra.Command{Use: "base"} + tt.shortcut.Mount(parent, &cmdutil.Factory{}) + cmd := parent.Commands()[0] + + tips := strings.Join(cmdutil.GetTips(cmd), "\n") + for _, want := range tt.wantTips { + if !strings.Contains(tips, want) { + t.Fatalf("tips missing %q:\n%s", want, tips) + } + } + }) + } +} + +func TestBaseJSONExamplesLiveInFlagDescriptions(t *testing.T) { + tests := []struct { + name string + shortcut common.Shortcut + wantHelp []string + }{ + { + name: "table create fields", + shortcut: BaseTableCreate, + wantHelp: []string{ + `field JSON array for create, e.g. [{"name":"Title","type":"text"}`, + }, + }, + { + name: "view set filter", + shortcut: BaseViewSetFilter, + wantHelp: []string{ + `filter JSON object, e.g. {"logic":"and","conditions":[["Status","==","Todo"]]}`, + }, + }, + { + name: "view set sort", + shortcut: BaseViewSetSort, + wantHelp: []string{ + `sort_config JSON object, e.g. {"sort_config":[{"field":"Priority","desc":true}]}`, + `use {"sort_config":[]} to clear`, + }, + }, + { + name: "view set group", + shortcut: BaseViewSetGroup, + wantHelp: []string{ + `group JSON object with group_config array, e.g. {"group_config":[{"field":"Status","desc":false}]}`, + }, + }, + { + name: "view set card", + shortcut: BaseViewSetCard, + wantHelp: []string{ + `card JSON object, e.g. {"cover_field":"Cover"} or {"cover_field":null} to clear`, + }, + }, + { + name: "view set timebar", + shortcut: BaseViewSetTimebar, + wantHelp: []string{ + `timebar JSON object with start_time, end_time, title, e.g. {"start_time":"Start Date","end_time":"End Date","title":"Name"}`, + }, + }, + { + name: "view set visible fields", + shortcut: BaseViewSetVisibleFields, + wantHelp: []string{ + `visible fields JSON object, e.g. {"visible_fields":["Name","Status"]}`, + }, + }, + { + name: "form question delete", + shortcut: BaseFormQuestionsDelete, + wantHelp: []string{ + `JSON array of question IDs to delete, max 10 items, e.g. '["q_001","q_002"]'`, + }, + }, + { + name: "record search json", + shortcut: BaseRecordSearch, + wantHelp: []string{ + `record search JSON object, e.g. {"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"limit":50}`, + }, + }, + { + name: "record upsert json", + shortcut: BaseRecordUpsert, + wantHelp: []string{ + `record field map JSON object, e.g. {"Name":"Alice","Status":"Todo"}; do not wrap in fields`, + }, + }, + { + name: "record batch create json", + shortcut: BaseRecordBatchCreate, + wantHelp: []string{ + `batch create JSON object, e.g. {"fields":["Name","Status"],"rows":[["Task A","Todo"],["Task B",null]]}; rows follow fields order`, + }, + }, + { + name: "record batch update json", + shortcut: BaseRecordBatchUpdate, + wantHelp: []string{ + `batch update JSON object, e.g. {"record_id_list":["rec_xxx"],"patch":{"Status":"Done"}}; same patch applies to all records`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &cobra.Command{Use: "base"} + tt.shortcut.Mount(parent, &cmdutil.Factory{}) + cmd := parent.Commands()[0] + + help := cmd.Flags().FlagUsages() + for _, want := range tt.wantHelp { + if !strings.Contains(help, want) { + t.Fatalf("flag help missing %q:\n%s", want, help) + } + } + }) + } +} + +func TestBaseRecordWriteHelpGuidesAgents(t *testing.T) { + tests := []struct { + name string + shortcut common.Shortcut + wantTips []string + }{ + { + name: "record upsert", + shortcut: BaseRecordUpsert, + wantTips: []string{ + "Happy path JSON is a top-level field map", + "Without --record-id this creates a record", + "does not auto-upsert by business key", + "use +field-list to confirm real writable fields", + "do not write system fields, formula, lookup, or attachment fields", + "CellValue happy path: text/phone/url", + "select -> \"Todo\"", + "multi-select -> [\"Tag A\",\"Tag B\"]", + "datetime -> \"2026-03-24 10:00:00\"", + "checkbox -> true/false", + `ID-based CellValue: user/group/link fields use arrays like [{"id":"ou_xxx"}]`, + `location uses {"lng":116.397428,"lat":39.90923}`, + "Do not guess user/chat/linked-record IDs or location coordinates", + "lark-base-cell-value.md", + "do not invent values for fields not covered by the happy path", + }, + }, + { + name: "record batch create", + shortcut: BaseRecordBatchCreate, + wantTips: []string{ + "Happy path fields: fields is the column order", + "rows is an array of row arrays", + "may use null for empty cells", + "use +field-list to confirm real writable fields", + "Batch create supports max 200 rows per call", + "CellValue happy path: text/phone/url", + `ID-based CellValue: user/group/link fields use arrays like [{"id":"ou_xxx"}]`, + "lark-base-cell-value.md", + "do not invent values for fields not covered by the happy path", + }, + }, + { + name: "record batch update", + shortcut: BaseRecordBatchUpdate, + wantTips: []string{ + "Happy path fields: record_id_list is the target record IDs", + "patch is a field map applied unchanged to every target record", + "Do not use +record-batch-update for per-row different values", + "use +field-list to confirm real writable fields", + "Batch update supports max 200 records per call", + "CellValue happy path: text/phone/url", + `ID-based CellValue: user/group/link fields use arrays like [{"id":"ou_xxx"}]`, + "lark-base-cell-value.md", + "do not invent values for fields not covered by the happy path", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent := &cobra.Command{Use: "base"} + tt.shortcut.Mount(parent, &cmdutil.Factory{}) + cmd := parent.Commands()[0] + + tips := strings.Join(cmdutil.GetTips(cmd), "\n") + for _, want := range tt.wantTips { + if !strings.Contains(tips, want) { + t.Fatalf("tips missing %q:\n%s", want, tips) + } + } + }) + } +} + func TestBaseFieldUpdateHelpGuidesAgents(t *testing.T) { parent := &cobra.Command{Use: "base"} BaseFieldUpdate.Mount(parent, &cmdutil.Factory{}) @@ -328,7 +741,7 @@ func TestBaseFieldUpdateHelpGuidesAgents(t *testing.T) { tips := strings.Join(cmdutil.GetTips(cmd), "\n") wantTips := []string{ - `lark-cli base +field-update --base-token --table-id --field-id --json '{"name":"Status","type":"text"}'`, + `lark-cli base +field-update --base-token --table-id --field-id "Status" --json '{"name":"Status","type":"text"}' --yes`, `"type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]`, "full field-definition PUT semantics", "Read the current field first with +field-get", diff --git a/shortcuts/base/dashboard_arrange.go b/shortcuts/base/dashboard_arrange.go index 320b704a3..ab031add0 100644 --- a/shortcuts/base/dashboard_arrange.go +++ b/shortcuts/base/dashboard_arrange.go @@ -22,6 +22,9 @@ var BaseDashboardArrange = common.Shortcut{ dashboardIDFlag(true), {Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"}, }, + Tips: []string{ + "Server-side smart layout is not deterministic or position-specific; use only when the user asks to arrange or beautify a dashboard.", + }, DryRun: dryRunDashboardArrange, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeDashboardArrange(runtime) diff --git a/shortcuts/base/dashboard_block_create.go b/shortcuts/base/dashboard_block_create.go index e74671d9d..ec8af8d3b 100644 --- a/shortcuts/base/dashboard_block_create.go +++ b/shortcuts/base/dashboard_block_create.go @@ -25,10 +25,19 @@ var BaseDashboardBlockCreate = common.Shortcut{ dashboardIDFlag(true), {Name: "name", Desc: "block name", Required: true}, {Name: "type", Desc: "block type: column(柱状图)|bar(条形图)|line(折线图)|pie(饼图)|ring(环形图)|area(面积图)|combo(组合图)|scatter(散点图)|funnel(漏斗图)|wordCloud(词云)|radar(雷达图)|statistics(指标卡)|text(文本). Read dashboard-block-data-config.md before creating.", Required: true}, - {Name: "data-config", Desc: "data config JSON object (table_name, series, count_all, group_by, filter, etc.)"}, - {Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"}, + {Name: "data-config", Desc: "data_config JSON object; read dashboard-block-data-config.md for the SSOT"}, + {Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"}, {Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"}, }, + Tips: []string{ + `lark-cli base +dashboard-block-create --base-token --dashboard-id --name "Order Count" --type statistics --data-config '{"table_name":"Orders","count_all":true}'`, + `lark-cli base +dashboard-block-create --base-token --dashboard-id --name "Dashboard Note" --type text --data-config '{"text":"# Sales Dashboard"}'`, + "Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.", + "data_config uses table and field names, not table_id or field_id.", + "Read dashboard-block-data-config.md as the SSOT for chart templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.", + "Record the returned block_id; block update/delete/get-data commands need it.", + "Create dashboard blocks sequentially; do not parallelize multiple block creates for the same dashboard.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { pc := newParseCtx(runtime) if runtime.Bool("no-validate") { diff --git a/shortcuts/base/dashboard_block_delete.go b/shortcuts/base/dashboard_block_delete.go index 97d667a98..2e78a5995 100644 --- a/shortcuts/base/dashboard_block_delete.go +++ b/shortcuts/base/dashboard_block_delete.go @@ -22,6 +22,10 @@ var BaseDashboardBlockDelete = common.Shortcut{ dashboardIDFlag(true), blockIDFlag(true), }, + Tips: []string{ + "lark-cli base +dashboard-block-delete --base-token --dashboard-id --block-id --yes", + baseHighRiskYesTip, + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id"). diff --git a/shortcuts/base/dashboard_block_get.go b/shortcuts/base/dashboard_block_get.go index b6c605e3c..5f6d62631 100644 --- a/shortcuts/base/dashboard_block_get.go +++ b/shortcuts/base/dashboard_block_get.go @@ -24,6 +24,11 @@ var BaseDashboardBlockGet = common.Shortcut{ blockIDFlag(true), {Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"}, }, + Tips: []string{ + "lark-cli base +dashboard-block-get --base-token --dashboard-id --block-id ", + "Use this command for block metadata such as name, type, layout, and data_config.", + "Use +dashboard-block-get-data when you need the computed chart result instead of metadata.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { params := map[string]interface{}{} if uid := strings.TrimSpace(runtime.Str("user-id-type")); uid != "" { diff --git a/shortcuts/base/dashboard_block_get_data.go b/shortcuts/base/dashboard_block_get_data.go index 184e6353f..3ee3b5a66 100644 --- a/shortcuts/base/dashboard_block_get_data.go +++ b/shortcuts/base/dashboard_block_get_data.go @@ -23,6 +23,7 @@ var BaseDashboardBlockGetData = common.Shortcut{ }, Tips: []string{ "lark-cli base +dashboard-block-get-data --base-token --block-id ", + "This command does not need --dashboard-id.", "Use +dashboard-block-get first when you need block metadata like name, type, or data_config.", "This command returns computed chart protocol JSON directly, not wrapped block metadata.", "Text blocks do not have computed chart data; this shortcut is for chart/statistics blocks.", diff --git a/shortcuts/base/dashboard_block_list.go b/shortcuts/base/dashboard_block_list.go index 852dcc18b..2f4d7461e 100644 --- a/shortcuts/base/dashboard_block_list.go +++ b/shortcuts/base/dashboard_block_list.go @@ -21,9 +21,13 @@ var BaseDashboardBlockList = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), dashboardIDFlag(true), - {Name: "page-size", Desc: "page size (max 100)"}, + {Name: "page-size", Desc: "page size, default 20, max 100"}, {Name: "page-token", Desc: "pagination token"}, }, + Tips: []string{ + "lark-cli base +dashboard-block-list --base-token --dashboard-id ", + "Use returned block_id and type values for +dashboard-block-get/update/delete/get-data.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { params := map[string]interface{}{} if ps := strings.TrimSpace(runtime.Str("page-size")); ps != "" { diff --git a/shortcuts/base/dashboard_block_update.go b/shortcuts/base/dashboard_block_update.go index 6f8a37dc9..718d7e856 100644 --- a/shortcuts/base/dashboard_block_update.go +++ b/shortcuts/base/dashboard_block_update.go @@ -24,10 +24,18 @@ var BaseDashboardBlockUpdate = common.Shortcut{ dashboardIDFlag(true), blockIDFlag(true), {Name: "name", Desc: "new block name"}, - {Name: "data-config", Desc: "data config JSON. For chart types: table_name, series|count_all, group_by, filter. For text type: text (markdown supported). See dashboard-block-data-config.md for details."}, - {Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"}, + {Name: "data-config", Desc: "data_config JSON object; read dashboard-block-data-config.md for the SSOT"}, + {Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"}, {Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"}, }, + Tips: []string{ + `lark-cli base +dashboard-block-update --base-token --dashboard-id --block-id --name "Total Sales"`, + `lark-cli base +dashboard-block-update --base-token --dashboard-id --block-id --data-config '{"series":[{"field_name":"Amount","rollup":"SUM"}]}'`, + "Read dashboard-block-data-config.md as the SSOT for data_config templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.", + "Use +dashboard-block-get first to inspect the current data_config before replacing nested values.", + "Block type cannot be changed; delete and recreate the block to change chart type.", + "data_config update merges top-level keys, but each provided key is replaced as a whole.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { pc := newParseCtx(runtime) if runtime.Bool("no-validate") { diff --git a/shortcuts/base/dashboard_create.go b/shortcuts/base/dashboard_create.go index 214d8d175..d0ac6ab29 100644 --- a/shortcuts/base/dashboard_create.go +++ b/shortcuts/base/dashboard_create.go @@ -20,7 +20,10 @@ var BaseDashboardCreate = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), {Name: "name", Desc: "dashboard name", Required: true}, - {Name: "theme-style", Desc: "theme style"}, + {Name: "theme-style", Desc: "theme style, defaults to platform default when omitted"}, + }, + Tips: []string{ + "Record the returned dashboard_id; dashboard block create/get/update/delete/arrange commands need it.", }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { body := map[string]interface{}{} diff --git a/shortcuts/base/dashboard_delete.go b/shortcuts/base/dashboard_delete.go index 5d6df0064..587d5f8a4 100644 --- a/shortcuts/base/dashboard_delete.go +++ b/shortcuts/base/dashboard_delete.go @@ -21,6 +21,11 @@ var BaseDashboardDelete = common.Shortcut{ baseTokenFlag(true), dashboardIDFlag(true), }, + Tips: []string{ + "lark-cli base +dashboard-delete --base-token --dashboard-id --yes", + "Deleting a dashboard also deletes its blocks and cannot be recovered.", + baseHighRiskYesTip, + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id"). diff --git a/shortcuts/base/dashboard_get.go b/shortcuts/base/dashboard_get.go index 90f21f6cd..a42642790 100644 --- a/shortcuts/base/dashboard_get.go +++ b/shortcuts/base/dashboard_get.go @@ -21,6 +21,9 @@ var BaseDashboardGet = common.Shortcut{ baseTokenFlag(true), dashboardIDFlag(true), }, + Tips: []string{ + "Use +dashboard-block-list or +dashboard-block-get when you need block-level details.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id"). diff --git a/shortcuts/base/dashboard_list.go b/shortcuts/base/dashboard_list.go index 8d270daa0..c1eca4902 100644 --- a/shortcuts/base/dashboard_list.go +++ b/shortcuts/base/dashboard_list.go @@ -20,9 +20,12 @@ var BaseDashboardList = common.Shortcut{ HasFormat: true, Flags: []common.Flag{ baseTokenFlag(true), - {Name: "page-size", Desc: "page size (max 100)"}, + {Name: "page-size", Desc: "page size, max 100"}, {Name: "page-token", Desc: "pagination token"}, }, + Tips: []string{ + "Use returned dashboard_id values for +dashboard-get, +dashboard-block-list, and +dashboard-block-create.", + }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { params := map[string]interface{}{} if ps := strings.TrimSpace(runtime.Str("page-size")); ps != "" { diff --git a/shortcuts/base/dashboard_update.go b/shortcuts/base/dashboard_update.go index 5832e9021..f9dda81a4 100644 --- a/shortcuts/base/dashboard_update.go +++ b/shortcuts/base/dashboard_update.go @@ -21,7 +21,7 @@ var BaseDashboardUpdate = common.Shortcut{ baseTokenFlag(true), dashboardIDFlag(true), {Name: "name", Desc: "new dashboard name"}, - {Name: "theme-style", Desc: "theme style"}, + {Name: "theme-style", Desc: "theme style, leave empty to keep current theme"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { body := map[string]interface{}{} diff --git a/shortcuts/base/field_create.go b/shortcuts/base/field_create.go index f2f342c1c..121117627 100644 --- a/shortcuts/base/field_create.go +++ b/shortcuts/base/field_create.go @@ -23,7 +23,8 @@ var BaseFieldCreate = common.Shortcut{ {Name: "i-have-read-guide", Type: "bool", Desc: "set only after you have read the formula/lookup guide for those field types", Hidden: true}, }, Tips: []string{ - `Example: --json '{"name":"Status","type":"text"}'`, + `Example text: lark-cli base +field-create --base-token --table-id --json '{"name":"Status","type":"text"}'`, + `Example select: lark-cli base +field-create --base-token --table-id --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}'`, "Agent hint: use the lark-base skill's field-create guide for usage and limits.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/field_delete.go b/shortcuts/base/field_delete.go index d242763d1..8b6a0a793 100644 --- a/shortcuts/base/field_delete.go +++ b/shortcuts/base/field_delete.go @@ -17,7 +17,11 @@ var BaseFieldDelete = common.Shortcut{ Scopes: []string{"base:field:delete"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)}, - DryRun: dryRunFieldDelete, + Tips: []string{ + baseHighRiskYesTip, + `Example: lark-cli base +field-delete --base-token --table-id --field-id "Status" --yes`, + }, + DryRun: dryRunFieldDelete, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeFieldDelete(runtime) }, diff --git a/shortcuts/base/field_get.go b/shortcuts/base/field_get.go index 63d66a697..a272b54c5 100644 --- a/shortcuts/base/field_get.go +++ b/shortcuts/base/field_get.go @@ -17,7 +17,12 @@ var BaseFieldGet = common.Shortcut{ Scopes: []string{"base:field:read"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)}, - DryRun: dryRunFieldGet, + Tips: []string{ + `Example: lark-cli base +field-get --base-token --table-id --field-id "Status"`, + "field-id accepts a field ID (fld...) or the field name from the current table.", + "Returns full field configuration; use it as the baseline before +field-update.", + }, + DryRun: dryRunFieldGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeFieldGet(runtime) }, diff --git a/shortcuts/base/field_list.go b/shortcuts/base/field_list.go index da487827b..7851a6f91 100644 --- a/shortcuts/base/field_list.go +++ b/shortcuts/base/field_list.go @@ -20,7 +20,7 @@ var BaseFieldList = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"}, - {Name: "limit", Type: "int", Default: "100", Desc: "pagination size"}, + {Name: "limit", Type: "int", Default: "100", Desc: "pagination size, range 1-200"}, }, DryRun: dryRunFieldList, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/field_search_options.go b/shortcuts/base/field_search_options.go index 674cfe6ba..1783d3681 100644 --- a/shortcuts/base/field_search_options.go +++ b/shortcuts/base/field_search_options.go @@ -22,7 +22,11 @@ var BaseFieldSearchOptions = common.Shortcut{ fieldRefFlag(true), {Name: "keyword", Desc: "keyword for option query"}, {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"}, - {Name: "limit", Type: "int", Default: "30", Desc: "pagination size"}, + {Name: "limit", Type: "int", Default: "30", Desc: "pagination size, default 30"}, + }, + Tips: []string{ + `Example: lark-cli base +field-search-options --base-token --table-id --field-id "Status" --keyword "Do"`, + "Use only for fields with options, such as select or multi-select fields.", }, DryRun: dryRunFieldSearchOptions, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/field_update.go b/shortcuts/base/field_update.go index f8e8a47d0..177ad7e9a 100644 --- a/shortcuts/base/field_update.go +++ b/shortcuts/base/field_update.go @@ -24,8 +24,9 @@ var BaseFieldUpdate = common.Shortcut{ {Name: "i-have-read-guide", Type: "bool", Desc: "acknowledge reading formula/lookup guide before creating or updating those field types", Hidden: true}, }, Tips: []string{ - `Example: lark-cli base +field-update --base-token --table-id --field-id --json '{"name":"Status","type":"text"}'`, - `Example: lark-cli base +field-update --base-token --table-id --field-id --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}'`, + baseHighRiskYesTip, + `Example text: lark-cli base +field-update --base-token --table-id --field-id "Status" --json '{"name":"Status","type":"text"}' --yes`, + `Example select: lark-cli base +field-update --base-token --table-id --field-id "Status" --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}' --yes`, "Update uses full field-definition PUT semantics. Read the current field first with +field-get, then send the target state.", "Type conversion is allowlist-based: only use CLI for safe conversions; otherwise migrate through a new field, or ask the user to finish high-risk conversions in the web UI.", "Formula and lookup updates require reading the corresponding guide first.", diff --git a/shortcuts/base/helpers_test.go b/shortcuts/base/helpers_test.go index 1038c33e9..a49f0efc4 100644 --- a/shortcuts/base/helpers_test.go +++ b/shortcuts/base/helpers_test.go @@ -38,7 +38,7 @@ func TestParseHelpers(t *testing.T) { if err != nil || obj["name"] != "demo" { t.Fatalf("obj=%v err=%v", obj, err) } - if _, err := parseJSONObject(testPC, `[1]`, "json"); err == nil || !strings.Contains(err.Error(), "--json must be a JSON object") || !strings.Contains(err.Error(), "lark-base skill") || strings.Contains(err.Error(), "array") { + if _, err := parseJSONObject(testPC, `[1]`, "json"); err == nil || !strings.Contains(err.Error(), "--json must be a JSON object") || !strings.Contains(err.Error(), "match the documented shape") || strings.Contains(err.Error(), "array") { t.Fatalf("err=%v", err) } if _, err := parseJSONObject(testPC, `null`, "json"); err == nil || !strings.Contains(err.Error(), "--json must be a JSON object") { @@ -66,7 +66,7 @@ func TestParseHelpers(t *testing.T) { if _, err := parseStringListFlexible(testPC, `[1]`, "fields"); err == nil || !strings.Contains(err.Error(), "invalid JSON string array") { t.Fatalf("err=%v", err) } - if _, err := parseJSONValue(testPC, "{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a valid JSON directly") || !strings.Contains(err.Error(), "@file.json") || !strings.Contains(err.Error(), "lark-base skill") { + if _, err := parseJSONValue(testPC, "{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a valid JSON directly") || !strings.Contains(err.Error(), "@file.json") || !strings.Contains(err.Error(), "complex JSON/DSL") { t.Fatalf("err=%v", err) } if !reflect.DeepEqual(parseStringList("m,n"), []string{"m", "n"}) { @@ -334,11 +334,11 @@ func TestJSONInputHelpers(t *testing.T) { t.Fatalf("err=%v", err) } syntaxErr := formatJSONError("json", "object", &json.SyntaxError{Offset: 7}) - if !strings.Contains(syntaxErr.Error(), "near byte 7") || !strings.Contains(syntaxErr.Error(), "tip: pass a valid JSON directly") || !strings.Contains(syntaxErr.Error(), "@file.json") || !strings.Contains(syntaxErr.Error(), "lark-base skill") { + if !strings.Contains(syntaxErr.Error(), "near byte 7") || !strings.Contains(syntaxErr.Error(), "tip: pass a valid JSON directly") || !strings.Contains(syntaxErr.Error(), "@file.json") || !strings.Contains(syntaxErr.Error(), "complex JSON/DSL") { t.Fatalf("syntaxErr=%v", syntaxErr) } typeErr := formatJSONError("json", "object", &json.UnmarshalTypeError{Field: "filter_info"}) - if !strings.Contains(typeErr.Error(), `field "filter_info"`) || !strings.Contains(typeErr.Error(), "tip: pass a valid JSON directly") || !strings.Contains(typeErr.Error(), "@file.json") || !strings.Contains(typeErr.Error(), "lark-base skill") { + if !strings.Contains(typeErr.Error(), `field "filter_info"`) || !strings.Contains(typeErr.Error(), "tip: pass a valid JSON directly") || !strings.Contains(typeErr.Error(), "@file.json") || !strings.Contains(typeErr.Error(), "complex JSON/DSL") { t.Fatalf("typeErr=%v", typeErr) } } diff --git a/shortcuts/base/high_risk.go b/shortcuts/base/high_risk.go new file mode 100644 index 000000000..dd34bf034 --- /dev/null +++ b/shortcuts/base/high_risk.go @@ -0,0 +1,6 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +const baseHighRiskYesTip = "This is a high-risk write command. If the user explicitly requested it and the target is unambiguous, pass --yes without asking again." diff --git a/shortcuts/base/record_batch_create.go b/shortcuts/base/record_batch_create.go index 5e0d85c36..50cde35fa 100644 --- a/shortcuts/base/record_batch_create.go +++ b/shortcuts/base/record_batch_create.go @@ -19,13 +19,14 @@ var BaseRecordBatchCreate = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "json", Desc: "batch create JSON object", Required: true}, - }, - Tips: []string{ - `Example: --json '{"fields":["Title","Status"],"rows":[["Task A","Open"],["Task B","Done"]]}'`, - "Agent hint: use the lark-base skill's record-batch-create guide for usage and limits.", - "Agent hint: use lark-base-cell-value.md as the source of truth for each CellValue.", + {Name: "json", Desc: `batch create JSON object, e.g. {"fields":["Name","Status"],"rows":[["Task A","Todo"],["Task B",null]]}; rows follow fields order`, Required: true}, }, + Tips: append([]string{ + "Happy path fields: fields is the column order; rows is an array of row arrays; each row must match fields order and may use null for empty cells.", + "Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.", + "Batch create supports max 200 rows per call.", + "Use the record-batch-create guide for command limits and edge cases.", + }, recordCellValueHappyPathTips...), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateRecordJSON(runtime) }, diff --git a/shortcuts/base/record_batch_update.go b/shortcuts/base/record_batch_update.go index 4b8d026ae..74a52cea8 100644 --- a/shortcuts/base/record_batch_update.go +++ b/shortcuts/base/record_batch_update.go @@ -19,13 +19,14 @@ var BaseRecordBatchUpdate = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "json", Desc: "batch update JSON object", Required: true}, - }, - Tips: []string{ - `Example: --json '{"record_id_list":["recXXX"],"patch":{"Status":"Done"}}'`, - "Agent hint: use the lark-base skill's record-batch-update guide for usage and limits.", - "Agent hint: use lark-base-cell-value.md as the source of truth for each patch CellValue.", + {Name: "json", Desc: `batch update JSON object, e.g. {"record_id_list":["rec_xxx"],"patch":{"Status":"Done"}}; same patch applies to all records`, Required: true}, }, + Tips: append([]string{ + "Happy path fields: record_id_list is the target record IDs; patch is a field map applied unchanged to every target record.", + "Do not use +record-batch-update for per-row different values; call +record-upsert per record or use another supported flow.", + "Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.", + "Batch update supports max 200 records per call; use the record-batch-update guide for command limits and edge cases.", + }, recordCellValueHappyPathTips...), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateRecordJSON(runtime) }, diff --git a/shortcuts/base/record_delete.go b/shortcuts/base/record_delete.go index 158ddcb50..281376d5e 100644 --- a/shortcuts/base/record_delete.go +++ b/shortcuts/base/record_delete.go @@ -22,6 +22,10 @@ var BaseRecordDelete = common.Shortcut{ {Name: "record-id", Type: "string_array", Desc: "record ID (repeatable)"}, {Name: "json", Desc: `JSON object with record_id_list, e.g. {"record_id_list":["rec_xxx"]}`}, }, + Tips: []string{ + baseHighRiskYesTip, + `Example: lark-cli base +record-delete --base-token --table-id --record-id --record-id --yes`, + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateRecordSelection(runtime) }, diff --git a/shortcuts/base/record_history_list.go b/shortcuts/base/record_history_list.go index 3a2f08e6e..896de0d42 100644 --- a/shortcuts/base/record_history_list.go +++ b/shortcuts/base/record_history_list.go @@ -21,7 +21,11 @@ var BaseRecordHistoryList = common.Shortcut{ tableRefFlag(true), recordRefFlag(true), {Name: "max-version", Type: "int", Desc: "max version for next page"}, - {Name: "page-size", Type: "int", Default: "30", Desc: "pagination size"}, + {Name: "page-size", Type: "int", Default: "30", Desc: "pagination size, max 50"}, + }, + Tips: []string{ + `Example: lark-cli base +record-history-list --base-token --table-id --record-id `, + "This reads one record's history only; it is not a table-wide audit scan.", }, DryRun: dryRunRecordHistoryList, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index f1873c23f..71cf01f0d 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -15,6 +15,13 @@ import ( const maxRecordSelectionCount = 200 const maxBatchGetSelectFieldCount = 100 +var recordCellValueHappyPathTips = []string{ + `CellValue happy path: text/phone/url -> "text"; number/currency/percent/rating -> 12.5; select -> "Todo"; multi-select -> ["Tag A","Tag B"]; datetime -> "2026-03-24 10:00:00"; checkbox -> true/false.`, + `ID-based CellValue: user/group/link fields use arrays like [{"id":"ou_xxx"}], [{"id":"oc_xxx"}], [{"id":"rec_xxx"}]; location uses {"lng":116.397428,"lat":39.90923}; null clears a cell when allowed.`, + "Do not guess user/chat/linked-record IDs or location coordinates; resolve them first with the relevant contact/im/record lookup flow.", + "Use lark-base-cell-value.md for complex CellValue shapes and special field types; do not invent values for fields not covered by the happy path.", +} + type recordSelection struct { recordIDs []string selectFields []string diff --git a/shortcuts/base/record_search.go b/shortcuts/base/record_search.go index 1b4ef35b5..ff85818a6 100644 --- a/shortcuts/base/record_search.go +++ b/shortcuts/base/record_search.go @@ -20,17 +20,15 @@ var BaseRecordSearch = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "json", Desc: `record search JSON object; requires keyword/search_fields, optional select_fields/view_id/offset/limit`, Required: true}, + {Name: "json", Desc: `record search JSON object, e.g. {"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"limit":50}; for keyword search only`, Required: true}, recordReadFormatFlag(), }, Tips: []string{ - `Example: lark-cli base +record-search --base-token --table-id --json '{"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"limit":50}'`, - `JSON shape: {"keyword":"","search_fields":[""],"select_fields":[""],"view_id":"","offset":0,"limit":10}.`, + `Happy path fields: keyword (string), search_fields (1-20 field names/ids), select_fields (optional projection, <=50), view_id (optional), offset (default 0), limit (default 10, range 1-200).`, "JSON constraints: keyword length >=1; search_fields length 1-20; select_fields length <=50; offset >=0 defaults to 0; limit range 1-200 defaults to 10.", "view_id scopes search to records in that view; when select_fields is omitted, returned fields follow that view's visible fields.", "Default output is markdown; pass --format json to get the raw JSON envelope.", - "Use +record-search only for keyword search; use a filtered view plus +record-list for structured conditions.", - "Agent hint: follow the lark-base record read SOP for record read routing and limits.", + "Use +record-search only for keyword search; for structured conditions, sorting, Top/Bottom N, or global conclusions, follow the lark-base record read SOP instead of inventing search JSON.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := validateRecordReadFormat(runtime); err != nil { diff --git a/shortcuts/base/record_share_link_create.go b/shortcuts/base/record_share_link_create.go index 8baf3589a..522369fcb 100644 --- a/shortcuts/base/record_share_link_create.go +++ b/shortcuts/base/record_share_link_create.go @@ -22,8 +22,9 @@ var BaseRecordShareLinkCreate = common.Shortcut{ {Name: "record-ids", Type: "string_slice", Desc: "record IDs to generate share links for (comma-separated or repeatable, max 100)", Required: true}, }, Tips: []string{ - `Single record: --base-token xxx --table-id tblxxx --record-ids recxxx`, - `Multiple records: --base-token xxx --table-id tblxxx --record-ids rec001,rec002,rec003`, + `Example: lark-cli base +record-share-link-create --base-token --table-id --record-ids `, + "Max 100 record IDs per call; duplicate IDs are ignored.", + "Output record_share_links maps record_id to URL; records without permission or missing records may be absent.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateRecordShareBatch(runtime) diff --git a/shortcuts/base/record_upload_attachment.go b/shortcuts/base/record_upload_attachment.go index 74ded4877..3bc564162 100644 --- a/shortcuts/base/record_upload_attachment.go +++ b/shortcuts/base/record_upload_attachment.go @@ -117,6 +117,7 @@ var BaseRecordRemoveAttachment = common.Shortcut{ {Name: "file-token", Type: "string_array", Desc: "attachment file_token to remove from the target cell; repeat to remove multiple attachments; max 50 tokens", Required: true}, }, Tips: []string{ + baseHighRiskYesTip, `Example: lark-cli base +record-remove-attachment --base-token --table-id --record-id --field-id --file-token --yes`, `Repeat --file-token to remove multiple attachments from the same cell in one call.`, `This is a high-risk write command and requires --yes.`, diff --git a/shortcuts/base/record_upsert.go b/shortcuts/base/record_upsert.go index f83f8b8eb..abcea7e9f 100644 --- a/shortcuts/base/record_upsert.go +++ b/shortcuts/base/record_upsert.go @@ -20,12 +20,14 @@ var BaseRecordUpsert = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), recordRefFlag(false), - {Name: "json", Desc: "record JSON object: Map", Required: true}, - }, - Tips: []string{ - `Example: --json '{"Name":"Alice"}'`, - "Agent hint: use the lark-base skill's record-upsert guide for usage and limits.", + {Name: "json", Desc: `record field map JSON object, e.g. {"Name":"Alice","Status":"Todo"}; do not wrap in fields`, Required: true}, }, + Tips: append([]string{ + "Happy path JSON is a top-level field map: each key is a real field name or field ID, each value is that field's CellValue.", + "Without --record-id this creates a record; with --record-id this updates that record. It does not auto-upsert by business key.", + "Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.", + "Use the record-upsert guide for command limits and edge cases.", + }, recordCellValueHappyPathTips...), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateRecordJSON(runtime) }, diff --git a/shortcuts/base/table_create.go b/shortcuts/base/table_create.go index 3bb65a8a1..b49c0a4c0 100644 --- a/shortcuts/base/table_create.go +++ b/shortcuts/base/table_create.go @@ -20,7 +20,11 @@ var BaseTableCreate = common.Shortcut{ baseTokenFlag(true), {Name: "name", Desc: "table name", Required: true}, {Name: "view", Desc: "view JSON object/array for create"}, - {Name: "fields", Desc: "field JSON array for create"}, + {Name: "fields", Desc: `field JSON array for create, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]`}, + }, + Tips: []string{ + "Before using --fields, read lark-base-field-json.md or rely on the same field JSON shape used by +field-create; do not invent field properties.", + "The first --fields item replaces the default field.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateTableCreate(runtime) diff --git a/shortcuts/base/table_delete.go b/shortcuts/base/table_delete.go index 58ad50100..0426d5e3f 100644 --- a/shortcuts/base/table_delete.go +++ b/shortcuts/base/table_delete.go @@ -17,7 +17,12 @@ var BaseTableDelete = common.Shortcut{ Scopes: []string{"base:table:delete"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true)}, - DryRun: dryRunTableDelete, + Tips: []string{ + `Example: lark-cli base +table-delete --base-token --table-id "Old Tasks" --yes`, + "table-id accepts a table ID (tbl...) or the table name in the current Base.", + baseHighRiskYesTip, + }, + DryRun: dryRunTableDelete, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeTableDelete(runtime) }, diff --git a/shortcuts/base/table_get.go b/shortcuts/base/table_get.go index c427b4504..ac29a8ee3 100644 --- a/shortcuts/base/table_get.go +++ b/shortcuts/base/table_get.go @@ -17,7 +17,11 @@ var BaseTableGet = common.Shortcut{ Scopes: []string{"base:table:read", "base:field:read", "base:view:read"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true)}, - DryRun: dryRunTableGet, + Tips: []string{ + `Example: lark-cli base +table-get --base-token --table-id "Tasks"`, + "table-id accepts a table ID (tbl...) or the table name in the current Base.", + }, + DryRun: dryRunTableGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeTableGet(runtime) }, diff --git a/shortcuts/base/table_list.go b/shortcuts/base/table_list.go index 01de1572d..6c5e58520 100644 --- a/shortcuts/base/table_list.go +++ b/shortcuts/base/table_list.go @@ -19,7 +19,7 @@ var BaseTableList = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"}, - {Name: "limit", Type: "int", Default: "50", Desc: "pagination limit"}, + {Name: "limit", Type: "int", Default: "50", Desc: "pagination size, range 1-100"}, }, DryRun: dryRunTableList, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/view_create.go b/shortcuts/base/view_create.go index 6e38b0453..9f803a361 100644 --- a/shortcuts/base/view_create.go +++ b/shortcuts/base/view_create.go @@ -19,11 +19,13 @@ var BaseViewCreate = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "json", Desc: "view JSON object/array", Required: true}, + {Name: "json", Desc: "view JSON object/array; type defaults to grid; type range: grid, kanban, gallery, calendar, gantt", Required: true}, }, Tips: []string{ - `Example: --json '{"name":"Main","type":"grid"}'`, - "Agent hint: use the lark-base skill's view-create guide for usage and limits.", + `Example: lark-cli base +view-create --base-token --table-id --json '{"name":"Main","type":"grid"}'`, + `Minimal: --json '{"name":"Main"}' creates a grid view.`, + "Do not pass form as a view type; form views are managed through form commands.", + `Use +view-set-visible-fields after creation when the user needs a specific field order or visibility.`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewCreate(runtime) diff --git a/shortcuts/base/view_delete.go b/shortcuts/base/view_delete.go index 5db39902b..493e726be 100644 --- a/shortcuts/base/view_delete.go +++ b/shortcuts/base/view_delete.go @@ -17,7 +17,11 @@ var BaseViewDelete = common.Shortcut{ Scopes: []string{"base:view:write_only"}, AuthTypes: authTypes(), Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)}, - DryRun: dryRunViewDelete, + Tips: []string{ + baseHighRiskYesTip, + `Example: lark-cli base +view-delete --base-token --table-id --view-id "Old View" --yes`, + }, + DryRun: dryRunViewDelete, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { return executeViewDelete(runtime) }, diff --git a/shortcuts/base/view_list.go b/shortcuts/base/view_list.go index 30fba37be..141b26152 100644 --- a/shortcuts/base/view_list.go +++ b/shortcuts/base/view_list.go @@ -20,7 +20,7 @@ var BaseViewList = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"}, - {Name: "limit", Type: "int", Default: "100", Desc: "pagination size"}, + {Name: "limit", Type: "int", Default: "100", Desc: "pagination size, range 1-200"}, }, DryRun: dryRunViewList, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/view_set_card.go b/shortcuts/base/view_set_card.go index 410a9b46e..3a0a46d8a 100644 --- a/shortcuts/base/view_set_card.go +++ b/shortcuts/base/view_set_card.go @@ -20,11 +20,12 @@ var BaseViewSetCard = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: "card JSON object", Required: true}, + {Name: "json", Desc: `card JSON object, e.g. {"cover_field":"Cover"} or {"cover_field":null} to clear`, Required: true}, }, Tips: []string{ - `Example: --json '{"cover_field":"fldCover"}'`, - "Agent hint: use the lark-base skill's view-set-card guide for usage and limits.", + "Supported view types: gallery, kanban.", + "cover_field should be an attachment field id/name, or null to clear.", + "Use +view-get-card first when updating an existing card view configuration.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewJSONObject(runtime) diff --git a/shortcuts/base/view_set_filter.go b/shortcuts/base/view_set_filter.go index 721129e06..c3a97762b 100644 --- a/shortcuts/base/view_set_filter.go +++ b/shortcuts/base/view_set_filter.go @@ -20,10 +20,9 @@ var BaseViewSetFilter = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: "filter JSON object", Required: true}, + {Name: "json", Desc: `filter JSON object, e.g. {"logic":"and","conditions":[["Status","==","Todo"]]}`, Required: true}, }, Tips: []string{ - `Example: --json '{"logic":"and","conditions":[["fldStatus","==","Todo"]]}'`, "Agent hint: use the lark-base skill's view-set-filter guide for usage and limits.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/view_set_group.go b/shortcuts/base/view_set_group.go index b96df3942..5247ca947 100644 --- a/shortcuts/base/view_set_group.go +++ b/shortcuts/base/view_set_group.go @@ -20,11 +20,13 @@ var BaseViewSetGroup = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: "group JSON object", Required: true}, + {Name: "json", Desc: `group JSON object with group_config array, e.g. {"group_config":[{"field":"Status","desc":false}]}; use {"group_config":[]} to clear`, Required: true}, }, Tips: []string{ - `Example: --json '{"group_config":[{"field":"fldStatus","desc":false}]}'`, - "Agent hint: use the lark-base skill's view-set-group guide for usage and limits.", + "Supported view types: grid, kanban, gantt.", + "Use a JSON object, not a bare array; grouping fields must be supported by the current view.", + "group_config supports max 3 group items.", + "Use +view-get-group first when modifying an existing grouping configuration.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewJSONObject(runtime) diff --git a/shortcuts/base/view_set_sort.go b/shortcuts/base/view_set_sort.go index 5ce24a562..743e722c3 100644 --- a/shortcuts/base/view_set_sort.go +++ b/shortcuts/base/view_set_sort.go @@ -20,11 +20,13 @@ var BaseViewSetSort = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: "sort_config JSON object", Required: true}, + {Name: "json", Desc: `sort_config JSON object, e.g. {"sort_config":[{"field":"Priority","desc":true}]}; use {"sort_config":[]} to clear; max 10 items`, Required: true}, }, Tips: []string{ - `Example: --json '{"sort_config":[{"field":"fldPriority","desc":true}]}'`, - "Agent hint: use the lark-base skill's view-set-sort guide for usage and limits.", + "Supported view types: grid, kanban, gallery, gantt.", + "Use a JSON object, not a bare array; sorting fields must be supported by the current view.", + "sort_config supports max 10 sort items.", + "Use +view-get-sort first when modifying an existing sort configuration.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewJSONObject(runtime) diff --git a/shortcuts/base/view_set_timebar.go b/shortcuts/base/view_set_timebar.go index 88f23a936..f037aacc8 100644 --- a/shortcuts/base/view_set_timebar.go +++ b/shortcuts/base/view_set_timebar.go @@ -20,11 +20,12 @@ var BaseViewSetTimebar = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: "timebar JSON object", Required: true}, + {Name: "json", Desc: `timebar JSON object with start_time, end_time, title, e.g. {"start_time":"Start Date","end_time":"End Date","title":"Name"}`, Required: true}, }, Tips: []string{ - `Example: --json '{"start_time":"fldStart","end_time":"fldEnd","title":"fldTitle"}'`, - "Agent hint: use the lark-base skill's view-set-timebar guide for usage and limits.", + "Supported view types: calendar, gantt.", + "start_time, end_time, and title are required; use date/time fields for start_time and end_time.", + "Use +view-get-timebar first when modifying an existing timebar configuration.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewJSONObject(runtime) diff --git a/shortcuts/base/view_set_visible_fields.go b/shortcuts/base/view_set_visible_fields.go index 48c7b0fb9..e1b54b121 100644 --- a/shortcuts/base/view_set_visible_fields.go +++ b/shortcuts/base/view_set_visible_fields.go @@ -20,11 +20,12 @@ var BaseViewSetVisibleFields = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true), - {Name: "json", Desc: `visible fields JSON object with "visible_fields"`, Required: true}, + {Name: "json", Desc: `visible fields JSON object, e.g. {"visible_fields":["Name","Status"]}`, Required: true}, }, Tips: []string{ - `Example: --json '{"visible_fields":["fldXXX"]}'`, - "Agent hint: use the lark-base skill's view-set-visible-fields guide for usage and limits.", + "Supported view types: grid, kanban, gallery, calendar, gantt.", + "Use a JSON object, not a bare array; primary field may be forced to the first position by the API.", + "visible_fields controls both visibility and order; include every field that should remain visible.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateViewJSONObject(runtime) diff --git a/shortcuts/base/workflow_create.go b/shortcuts/base/workflow_create.go index c2c3c8c9d..30c7beb09 100644 --- a/shortcuts/base/workflow_create.go +++ b/shortcuts/base/workflow_create.go @@ -19,7 +19,15 @@ var BaseWorkflowCreate = common.Shortcut{ AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, - {Name: "json", Desc: `workflow body JSON, e.g. {"title":"My Workflow","steps":[...]}`, Required: true}, + {Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before constructing steps", Required: true}, + }, + Tips: []string{ + "lark-cli base +workflow-create --base-token --json @workflow.json", + "client_token is required and should be unique per create request.", + "New workflows are created disabled; call +workflow-enable after creation when the user wants it active.", + "Before constructing steps, use +table-list and +field-list to confirm real table and field names.", + "Step ids must be unique, and every next/children link must reference an existing step id.", + "Use lark-base-workflow-guide.md as the entry guide and lark-base-workflow-schema.md as the steps JSON SSOT; do not invent steps[].type/data/next/children from natural language.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/workflow_disable.go b/shortcuts/base/workflow_disable.go index 7b3e2c3cd..582c6f88b 100644 --- a/shortcuts/base/workflow_disable.go +++ b/shortcuts/base/workflow_disable.go @@ -21,6 +21,10 @@ var BaseWorkflowDisable = common.Shortcut{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, }, + Tips: []string{ + "workflow-id must start with wkf; do not pass a tbl table ID from the same URL.", + "Disable only changes workflow state; it does not delete the workflow or its steps.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/workflow_enable.go b/shortcuts/base/workflow_enable.go index 3bf9e96d9..98cba38c0 100644 --- a/shortcuts/base/workflow_enable.go +++ b/shortcuts/base/workflow_enable.go @@ -21,6 +21,11 @@ var BaseWorkflowEnable = common.Shortcut{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, }, + Tips: []string{ + "workflow-id must start with wkf; do not pass a tbl table ID from the same URL.", + "Enable only changes workflow state; it does not modify steps.", + "New workflows are created disabled; enable after creation only when the user wants it active.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") diff --git a/shortcuts/base/workflow_get.go b/shortcuts/base/workflow_get.go index 2a7517be2..f0d591f4a 100644 --- a/shortcuts/base/workflow_get.go +++ b/shortcuts/base/workflow_get.go @@ -20,7 +20,13 @@ var BaseWorkflowGet = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, - {Name: "user-id-type", Desc: "user ID type for creator/updater fields", Enum: []string{"open_id", "union_id", "user_id"}}, + {Name: "user-id-type", Desc: "user ID type for creator/updater fields, default open_id", Enum: []string{"open_id", "union_id", "user_id"}}, + }, + Tips: []string{ + "workflow-id must start with wkf; use +workflow-list if the ID is unknown.", + "steps may be an empty array; that is valid for an unconfigured workflow.", + "Use +workflow-get before +workflow-update, then edit the returned definition and keep fields you do not intend to change.", + "Read lark-base-workflow-schema.md when interpreting or reusing returned steps.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/workflow_list.go b/shortcuts/base/workflow_list.go index 92a405400..2ee6a17b7 100644 --- a/shortcuts/base/workflow_list.go +++ b/shortcuts/base/workflow_list.go @@ -20,7 +20,11 @@ var BaseWorkflowList = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "status", Desc: "filter by status", Enum: []string{"enabled", "disabled"}}, - {Name: "page-size", Type: "int", Default: "100", Desc: "page size per request (max 100)"}, + {Name: "page-size", Type: "int", Default: "100", Desc: "page size per request, max 100"}, + }, + Tips: []string{ + "Returns workflow_id values with wkf prefix; pass those IDs to +workflow-get/enable/disable/update.", + "This shortcut auto-paginates and returns all matched workflows.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index ff56a7be4..e9ffa4de8 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -20,7 +20,16 @@ var BaseWorkflowUpdate = common.Shortcut{ Flags: []common.Flag{ {Name: "base-token", Desc: "base token", Required: true}, {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, - {Name: "json", Desc: `workflow body JSON, e.g. {"title":"New Title","steps":[...]}`, Required: true}, + {Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before replacing steps", Required: true}, + }, + Tips: []string{ + "lark-cli base +workflow-update --base-token --workflow-id --json @workflow.json", + "PUT uses full replacement semantics; omitting steps clears the existing workflow steps.", + "Use +workflow-get first, then edit the returned definition and keep title/status/steps fields you do not intend to change.", + "workflow-id must start with wkf; do not pass a tbl table ID.", + "Step ids must be unique, and every next/children link must reference an existing step id.", + "Updating does not enable or disable a workflow; call +workflow-enable or +workflow-disable separately.", + "Use lark-base-workflow-guide.md as the entry guide and lark-base-workflow-schema.md as the steps JSON SSOT; do not invent steps[].type/data/next/children from natural language.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str("base-token")) == "" { diff --git a/skill-template/domains/base.md b/skill-template/domains/base.md index b914a74a2..a5a8c7d87 100644 --- a/skill-template/domains/base.md +++ b/skill-template/domains/base.md @@ -8,7 +8,7 @@ - `lark-cli base records create` ❌ 2. **优先使用 Shortcut** — 有 Shortcut 的操作不要手拼原生 API 3. **写记录前** — 先调用 `table.fields list` 获取字段 `type/ui_type`,再读 [lark-base-cell-value.md](../../skills/lark-base/references/lark-base-cell-value.md);该文档是 CellValue 的 source of truth -4. **写字段前** — 先读 [lark-base-shortcut-field-properties.md](../../skills/lark-base/references/lark-base-shortcut-field-properties.md) 确认字段类型 JSON 结构 +4. **写字段前** — 先读 [lark-base-field-json.md](../../skills/lark-base/references/lark-base-field-json.md) 确认字段类型 JSON 结构 5. **筛选查询前** — 先读 [lark-base-view-set-filter.md](../../skills/lark-base/references/lark-base-view-set-filter.md),当前 `base/v3` 通过 `view.filter update + table.records list` 组合完成筛选读取 6. **批量上限 200 条/次** — 同一表建议串行写入,并在批次间延迟 0.5–1 秒 7. **改名和删除按明确意图执行** — 视图重命名这类低风险改名操作,目标和新名称明确时可直接执行;删除记录 / 字段 / 表时,只要用户已经明确要求删除且目标明确,也可直接执行,不需要再补一次确认 @@ -113,7 +113,7 @@ lark-cli wiki spaces.get_node --params '{"token":"Pgrrwvr***********UnRb"}' ## 参考文档 -- [lark-base-shortcut-field-properties.md](../../skills/lark-base/references/lark-base-shortcut-field-properties.md) — 字段类型 JSON 配置 +- [lark-base-field-json.md](../../skills/lark-base/references/lark-base-field-json.md) — 字段类型 JSON 配置 - [lark-base-cell-value.md](../../skills/lark-base/references/lark-base-cell-value.md) — CellValue source of truth - [lark-base-view-set-filter.md](../../skills/lark-base/references/lark-base-view-set-filter.md) — 查询筛选指南(filter / operator / sort / 分页) -- [examples.md](../../skills/lark-base/references/examples.md) — 完整操作示例(建表、筛选、更新) +- 具体命令示例由命令 --help 内置 tips 承接;复杂 JSON 只读上方保留 reference diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index e398f2cf1..b3efa34df 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -1,7 +1,7 @@ --- name: lark-base version: 1.2.2 -description: "当需要用 lark-cli 操作飞书多维表格(Base)时调用:搜索 Base、建表、字段管理、记录读写、记录分享链接、视图配置、历史查询,以及角色/表单/仪表盘管理/工作流;也适用于把旧的 +table / +field / +record 写法改成当前命令写法。涉及字段设计、公式字段、查找引用、跨表计算、行级派生指标、数据分析需求时也必须使用本 skill。" +description: "飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、workflow、角色权限;遇到 Base/多维表格/bitable 或 /base/ 链接时使用。文件导入转 lark-drive,认证/授权转 lark-shared。" metadata: requires: bins: ["lark-cli"] @@ -10,353 +10,151 @@ metadata: # base -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md)。 -> **执行前必做:** 执行任何 `base` 命令前,必须先阅读对应命令的 reference 文档,再调用命令。 -> **查询类任务必做:** 涉及筛选、排序、Top/Bottom N、聚合、多表关联、查询后写入或判断全局结论时,必须先阅读 [`references/lark-base-data-analysis-sop.md`](references/lark-base-data-analysis-sop.md),再选择 `record / view / data-query` 路径。 -> **命名约定:** Base 业务命令仅使用 `lark-cli base +...` 形式;解析 Wiki 链接使用 `lark-cli wiki +node-get`。 -> **分流规则:** 如果用户要“把本地文件导入成 Base / 多维表格 / bitable”,第一步不是 `base`,而是 `lark-cli drive +import --type bitable`;导入完成后再回到 `lark-cli base +...` 做表内操作。 +## 何时使用 -## 1. 何时使用本 Skill +使用本 skill: -### 1.1 触发条件 +- 用户明确提到 Base / 多维表格 / bitable,或给出 `/base/` 链接。 +- 用户要在 Base 内建表、改表、管理字段、写记录、查记录、配视图。 +- 用户要在 Base 内做公式字段、lookup 字段、跨表计算、派生指标、筛选聚合、TopN、统计分析。 +- 用户要管理 Base 表单、仪表盘、workflow、高级权限或角色。 +- 用户要把旧 Base 聚合式命令或旧写法迁移到当前 `lark-cli base +...` shortcut。 -以下场景应使用本 skill: +不要使用本 skill: -- 用户明确要操作飞书多维表格 / Base。 -- 用户要建表、改表、查表、删表,或管理字段、记录、视图。 -- 用户要做公式字段、lookup 字段、派生指标、跨表计算。 -- 用户要做临时统计、聚合分析、比较排序、求最值。 -- 用户要管理 workflow、dashboard、表单、角色权限。 -- 用户给出 `/base/{token}` 链接。 -- 用户给出 `/wiki/{token}` 链接,且最终解析为 `bitable`。 -- 用户要把旧的 Base 聚合式写法改成当前原子命令写法,例如把旧 `+table / +field / +record / +view / +history / +workspace` 改写成当前命令。 +- 只是认证、初始化配置、切换身份、处理 scope 或权限授权恢复,转 `lark-shared`。 +- 把本地 Excel / CSV / `.base` 导入成 Base,转 `lark-drive +import --type bitable`。 +- 泛化数据分析、字段设计、公式讨论,但没有 Base/多维表格上下文。 -以下场景不应使用本 skill: +## 使用边界 -- 用户只是做认证、初始化配置、切换 `--as user/bot`、处理 scope。此时先读 `../lark-shared/SKILL.md`。 -- 用户只是泛化地讨论“数据分析 / 字段设计”,但并不在 Base 场景中。不要因为提到“统计 / 公式 / lookup”就误触发。 +- Base 业务操作只使用 `lark-cli base +...` shortcut,不使用旧聚合式 `+table / +field / +record / +view / +history / +workspace`。 +- 本轮 Base 不依赖 `lark-cli schema`。SKILL 只保留路由、风险和复杂 JSON/DSL;简单命令由命令自身的参数、tips 和错误恢复承接。 +- 用户要把 Excel / CSV / `.base` 导入成 Base 时,先转 `lark-cli drive +import --type bitable`,导入完成后再回到 Base 命令。 +- 用户只给 Base 名称或关键词时,先用 `lark-cli drive +search --query --doc-types bitable` 定位资源。 +- Base 命令必须先有 `base_token` 或可解析出的 Base URL。没有 token 时:用户要新建就用 `+base-create`;用户给标题/关键词就搜 `lark-cli drive +search --query "" --doc-types bitable --only-title --as user`;仍无法定位时,反问用户具体是哪一个 Base。 +- 认证、初始化、scope、身份切换、权限不足恢复属于 `lark-shared`;Base 文档只保留会影响 Base 路径选择的权限规则。 -### 1.2 前置约束 +## 快速路由 -1. 先阅读 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md)。 -2. Base 业务命令仅使用 `lark-cli base +...` 形式的 shortcut 命令。 -3. 如果输入是 Wiki 链接或 Wiki token,并且用户想读取/操作其中的 Base,先执行 `lark-cli wiki +node-get --node-token `;当返回 `data.obj_type=bitable` 时,把 `data.obj_token` 当作 `--base-token`。不要把 URL 里的 `/wiki/{token}` 当成 Base token。(旧的 `--token` flag 仍可用,但已 deprecated,会在 stderr 打印迁移提示。) -4. 定位到命令后,先读该命令对应的 reference,再执行命令。 -5. 如果用户要把本地 Excel / CSV / `.base` 快照导入成 Base / 多维表格 / bitable,第一步不是 `base`,而是 `lark-cli drive +import --type bitable`;导入完成后再回到 `lark-cli base +...` 做表内操作。 -6. 不要在 Base 场景改走 `lark-cli api /open-apis/bitable/v1/...`。 -7. 如果用户只给 Base 名称、关键词,或说“帮我找一个多维表格”,先通过 `lark-cli drive +search --query --doc-types bitable` 搜索 Base / 多维表格资源;拿到 Base URL 后再使用本 skill 的 `base +...` 命令。复杂搜索再读 [`../lark-drive/references/lark-drive-search.md`](../lark-drive/references/lark-drive-search.md):标题精确匹配、限定 owner(`--mine` / `--creator-ids`,owner 语义非"最初创建人")/群/文件夹/时间范围、只搜标题/评论、分页/全量搜索。 +| 用户目标 | 优先命令 | 何时读 reference | +|---|---|---| +| 查 Base 本体 | `+base-get` | 用返回确认 Base 名称、owner、权限和可继续操作的 token | +| 创建/复制 Base | `+base-create` / `+base-copy` | 写入后报告新 Base 标识;注意返回中的 `permission_grant` | +| 管理表 | `+table-list/get/create/update/delete` | `+table-create --fields` 复杂时读 `lark-base-field-json.md` | +| 列/查/删字段 | `+field-list/get/delete/search-options` | 写入前用 list/get 确认字段类型、选项、ID;删除前确认目标字段 | +| 创建/更新字段 | `+field-create` / `+field-update` | 必读 `lark-base-field-json.md`;公式读 `formula-field-guide.md`;lookup 读 `lookup-field-guide.md`;命令细节读 `lark-base-field-create.md` / `lark-base-field-update.md` | +| 读记录明细 | `+record-get` / `+record-list` / `+record-search` | 涉及筛选、排序、Top/Bottom N、聚合、多表关联、全局结论时读 `lark-base-data-analysis-sop.md` | +| 写记录 | `+record-upsert` / `+record-batch-create` / `+record-batch-update` | 必读对应 record reference 和 `lark-base-cell-value.md` | +| 附件字段 | `+record-upload-attachment` / `+record-download-attachment` / `+record-remove-attachment` | 附件不要伪造成普通 CellValue;上传走本地文件,下载/删除按 file token 或字段定位 | +| 删除记录 / 分享记录链接 / 历史 | `+record-delete` / `+record-share-link-create` / `+record-history-list` | 删除前确认 record;分享链接最多 100 条;历史读 `lark-base-record-history-list.md`,只查单条记录,不做整表审计 | +| 管理视图 | `+view-*` | `+view-set-filter` 读 `lark-base-view-set-filter.md`;其余配置先 get 现状,再按返回结构更新 | +| 一次性聚合统计 | `+data-query` | 必读 `lark-base-data-analysis-sop.md` 和入口 `lark-base-data-query-guide.md`;完整 DSL 再读 `lark-base-data-query.md` | +| 公式字段 | `+field-create/update --json '{"type":"formula",...}'` | 必读 `formula-field-guide.md`,读后再加隐藏确认 flag `--i-have-read-guide` | +| Lookup 字段 | `+field-create/update --json '{"type":"lookup",...}'` | 必读 `lookup-field-guide.md`,读后再加隐藏确认 flag `--i-have-read-guide` | +| 表单提交 | `+form-submit` | 先读 `lark-base-form-detail.md` 获取题目、filter 和附件所需 `base_token`;提交 JSON 读 `lark-base-form-submit.md` | +| 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | 读对应 form-questions reference | +| 其他表单管理 | `+form-list/get/detail/create/update/delete` / `+form-questions-list/delete` | `+form-detail` 读 `lark-base-form-detail.md`;删除前确认目标表单 | +| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 `lark-base-dashboard.md`;组件 `data_config` 读 `dashboard-block-data-config.md`;读取图表计算结果用 `+dashboard-block-get-data` | +| Workflow | `+workflow-*` | 创建/更新或理解 steps 时读入口 `lark-base-workflow-guide.md` 和 steps JSON SSOT `lark-base-workflow-schema.md`;list/get/enable/disable 只处理 workflow ID 与启停状态 | +| 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 `lark-base-role-guide.md`;角色 create/update 或解读完整配置再读权限 JSON SSOT `role-config.md`;系统角色不可删除;关闭高级权限会影响自定义角色 | -## 2. 模块与命令导航 +## Base 心智模型 -本章按“先选模块,再选命令”的方式组织。先判断用户目标属于哪个大模块,再进入对应子模块,按要求阅读 reference 后执行命令。 +- Base 曾用名 Bitable;返回字段、错误或旧文档里的 `bitable` 多为历史兼容,不代表应改走裸 API 或另一套命令。 +- 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。 +- 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。 +- 一次性统计、筛选、TopN 优先用 `+data-query` 或临时视图;需要长期显示在表中时,才新增 `formula` / `lookup` 字段。 +- `formula` 适合常规计算、条件判断、文本/日期处理和长期派生指标;`lookup` 适合明确的跨表查找、筛选后取值或聚合引用。 +- 写入、分析、公式、lookup、workflow、dashboard 前,先读取真实结构:表、字段、视图、关联表和 dashboard block 名称都以命令返回为准。 +- 跨表场景必须读取目标表结构;link 单元格中的关联 `record_id` 只是连接键,最终回答要回查并展示用户可读字段。 -### 2.1 模块地图 +## 身份与权限降级 -| 大模块 | 处理什么问题 | 包含的小模块 / 能力 | -|------|-------------|-------------------| -| Base 模块 | 管理 Base 本体,或从链接进入 Base 场景 | `base-create / base-get / base-copy`,Base / Wiki 链接解析 | -| 表与数据模块 | 管理 Base 内部结构与日常数据操作 | `table / field / record / view` | -| 公式 / Lookup 模块 | 处理派生字段、条件判断、跨表计算、固定查找引用 | `formula / lookup` 字段创建与更新 | -| 数据分析模块 | 做一次性筛选、分组、聚合分析 | `data-query` | -| Workflow 模块 | 管理自动化流程 | `workflow-list / get / create / update / enable / disable` | -| Dashboard 模块 | 管理仪表盘和图表组件 | `dashboard-* / dashboard-block-*` | -| 表单模块 | 管理表单和表单题目 | `form-* / form-questions-*` | -| 权限与角色模块 | 管理高级权限和自定义角色 | `advperm-* / role-*` | +- 默认显式使用 `--as user` 操作用户资源;只有用户明确要求应用身份时,才直接用 `--as bot`。 +- user 身份报 scope/授权不足,或错误中包含 `permission_violations` / `hint`,先转 `lark-shared` 做用户授权恢复,不要直接降级 bot。 +- user 身份报资源级无访问且无授权恢复提示时,才可用 `--as bot` 重试一次;bot 仍失败就停止重试并按权限错误处理。 +- `91403` 或明确不可访问错误不要循环换身份重试。 +- `+base-create` / `+base-copy` 若用 bot 身份执行,关注返回中的 `permission_grant`,并把用户是否可打开新 Base 告知用户。 -### 2.2 Base 模块 +## 查询与统计规则 -用于管理 Base 本体,或从用户给出的链接进入后续 Base 操作。 -模块索引:[`references/lark-base-workspace.md`](references/lark-base-workspace.md) +涉及查询、统计或判断结论时,先阅读 `references/lark-base-data-analysis-sop.md`,并遵守: -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `lark-cli drive +search --query --doc-types bitable` | 按名称、关键词查找 Base / 多维表格 / bitable | 复杂搜索再读 [`../lark-drive/references/lark-drive-search.md`](../lark-drive/references/lark-drive-search.md) | 先定位资源,再回到 `base +...` 操作表内数据 | -| `+base-create` | 创建新的 Base | [`lark-base-base-create.md`](references/lark-base-base-create.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 写入操作;执行前先读 reference;`--folder-token`、`--time-zone` 都是可选项 | -| `+base-get` | 获取 Base 信息 | [`lark-base-base-get.md`](references/lark-base-base-get.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 适合确认 Base 本体信息,不替代表/字段结构读取 | -| `+base-copy` | 复制已有 Base | [`lark-base-base-copy.md`](references/lark-base-base-copy.md)、[`lark-base-workspace.md`](references/lark-base-workspace.md) | 写入操作;执行前先读 reference;复制成功后应主动返回新 Base 标识信息 | - -### 2.3 表与数据模块 - -这是最常用的大模块,包含 `table / field / record / view` 四类子模块。 -补充示例:[`references/examples.md`](references/examples.md),适合需要串联 table / record / view 完整操作链路时再读。 - -#### 2.3.1 Table 子模块 - -子模块索引:[`references/lark-base-table.md`](references/lark-base-table.md) - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+table-list / +table-get` | 列出数据表,或获取单个表详情 | [`lark-base-table-list.md`](references/lark-base-table-list.md)、[`lark-base-table-get.md`](references/lark-base-table-get.md) | `+table-list` 只能串行执行;`+table-get` 适合删除/修改前确认目标 | -| `+table-create / +table-update / +table-delete` | 创建、更新或删除数据表 | [`lark-base-table-create.md`](references/lark-base-table-create.md)、[`lark-base-table-update.md`](references/lark-base-table-update.md)、[`lark-base-table-delete.md`](references/lark-base-table-delete.md) | 创建适合一次性建表;更新前先确认目标表;删除时用户已明确目标可直接执行并带 `--yes` | - -#### 2.3.2 Field 子模块 - -普通字段管理走这里;如果字段类型是 `formula` 或 `lookup`,转到下方“公式 / Lookup 模块”。 -子模块索引:[`references/lark-base-field.md`](references/lark-base-field.md) - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+field-list / +field-get` | 列出字段结构,或获取单个字段详情 | [`lark-base-field-list.md`](references/lark-base-field-list.md)、[`lark-base-field-get.md`](references/lark-base-field-get.md) | 写记录、写字段、做分析前常先读 `+field-list`;`+field-list` 只能串行执行;`+field-get` 适合删除/更新前确认目标 | -| `+field-create / +field-update / +field-delete` | 创建、更新或删除普通字段 | [`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-field-delete.md`](references/lark-base-field-delete.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 写字段前先看字段属性规范;如果涉及类型转换,直接按 `+field-update` 中的字段类型变更规则执行,只在安全白名单内考虑原地转换;如果类型是 `formula / lookup`,先转去读对应 guide;更新或删除时用户已明确目标可直接执行并带 `--yes` | -| `+field-search-options` | 查询字段可选项 | [`lark-base-field-search-options.md`](references/lark-base-field-search-options.md) | 适合单选/多选等选项型字段 | - -#### 2.3.3 Record 子模块 - -子模块索引:[`references/lark-base-record.md`](references/lark-base-record.md)、[`references/lark-base-history.md`](references/lark-base-history.md) - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+record-search / +record-list / +record-get` | 按关键词检索记录、读取记录明细 / 分页导出,或按 ID 获取一条或多条记录 | [`lark-base-data-analysis-sop.md`](references/lark-base-data-analysis-sop.md) | 记录读取统一先读 data analysis SOP:已知 `record_id` 用 `+record-get`;明确关键词用 `+record-search`;普通明细用 `+record-list`;明确筛选 / 排序 / Top N 用临时视图投影后 `+record-list --view-id`;统计聚合才分流到 `+data-query`;`+record-get` 支持重复 `--record-id` 或 `--json` 读取多条记录 | -| `+record-upsert / +record-batch-create / +record-batch-update` | 创建、更新或批量写入记录 | [`lark-base-record-upsert.md`](references/lark-base-record-upsert.md)、[`lark-base-record-batch-create.md`](references/lark-base-record-batch-create.md)、[`lark-base-record-batch-update.md`](references/lark-base-record-batch-update.md)、[`lark-base-cell-value.md`](references/lark-base-cell-value.md) | 写前先 `+field-list`;只写存储字段;`+record-batch-update` 为同值更新(同一 patch 应用到多条记录);批量单次不超过 `200` 条;附件不要走这里 | -| `+record-upload-attachment` | 给已有记录上传一个或多个附件 | 看 `lark-cli base +record-upload-attachment --help` | 附件上传专用链路,不要用 `+record-upsert` / `+record-batch-*` 伪造附件值;不支持 `--name` | -| `+record-download-attachment` | 下载一个或多个 Base 附件到本地 | 看 `lark-cli base +record-download-attachment --help` | Base 附件必须用这个命令下载;用其他下载入口可能失败 | -| `+record-remove-attachment` | 删除附件字段里的一个或多个附件 | 看 `lark-cli base +record-remove-attachment --help` | 删除操作;确认目标后带 `--yes` | -| `+record-delete` | 删除一条或多条记录 | [`lark-base-record-delete.md`](references/lark-base-record-delete.md) | 删除多条时重复传 `--record-id` 指定多个记录;用户已明确目标可直接执行并带 `--yes` | -| `+record-history-list` | 查询指定记录的变更历史 | [`lark-base-record-history-list.md`](references/lark-base-record-history-list.md) | 按 `table-id + record-id` 查询,不支持整表扫描;`+record-history-list` 只能串行执行 | -| `+record-share-link-create` | 为一条或多条记录生成分享链接 | [`lark-base-record-share-link-create.md`](references/lark-base-record-share-link-create.md) | 单次最多 100 条;重复 record_id 会自动去重;适合分享单条记录或批量分享场景 | - -#### 2.3.4 View 子模块 - -子模块索引:[`references/lark-base-view.md`](references/lark-base-view.md) - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+view-list / +view-get` | 列出视图,或获取视图详情 | [`lark-base-view-list.md`](references/lark-base-view-list.md)、[`lark-base-view-get.md`](references/lark-base-view-get.md) | `+view-list` 只能串行执行;`+view-get` 适合查看已有视图配置 | -| `+view-create / +view-delete / +view-rename` | 创建、删除或重命名视图 | [`lark-base-view-create.md`](references/lark-base-view-create.md)、[`lark-base-view-delete.md`](references/lark-base-view-delete.md)、[`lark-base-view-rename.md`](references/lark-base-view-rename.md) | 创建前先确认表和视图类型;删除前先确认目标;用户已明确新名字时可直接重命名 | -| `+view-get-filter / +view-set-filter` | 读取或配置筛选条件 | [`lark-base-view-get-filter.md`](references/lark-base-view-get-filter.md)、[`lark-base-view-set-filter.md`](references/lark-base-view-set-filter.md)、[`lark-base-data-analysis-sop.md`](references/lark-base-data-analysis-sop.md) | 常与 `+record-list` 组合,用于按视图筛选读取 | -| `+view-get-sort / +view-set-sort` | 读取或配置排序 | [`lark-base-view-get-sort.md`](references/lark-base-view-get-sort.md)、[`lark-base-view-set-sort.md`](references/lark-base-view-set-sort.md) | 字段名必须来自真实结构 | -| `+view-get-group / +view-set-group` | 读取或配置分组 | [`lark-base-view-get-group.md`](references/lark-base-view-get-group.md)、[`lark-base-view-set-group.md`](references/lark-base-view-set-group.md) | 字段名必须来自真实结构 | -| `+view-get-visible-fields / +view-set-visible-fields` | 读取或配置视图可见字段 | [`lark-base-view-get-visible-fields.md`](references/lark-base-view-get-visible-fields.md)、[`lark-base-view-set-visible-fields.md`](references/lark-base-view-set-visible-fields.md) | 用于控制视图中的字段顺序与可见性;字段名必须来自真实结构 | -| `+view-get-card / +view-set-card` | 读取或配置卡片视图 | [`lark-base-view-get-card.md`](references/lark-base-view-get-card.md)、[`lark-base-view-set-card.md`](references/lark-base-view-set-card.md) | 适合卡片展示场景 | -| `+view-get-timebar / +view-set-timebar` | 读取或配置时间轴视图 | [`lark-base-view-get-timebar.md`](references/lark-base-view-get-timebar.md)、[`lark-base-view-set-timebar.md`](references/lark-base-view-set-timebar.md) | 适合时间线展示场景 | - -### 2.4 公式 / Lookup 模块 - -只要用户诉求涉及派生指标、条件判断、文本处理、日期差、跨表计算、跨表筛选后取值,都要先判断是否进入本模块。 - -默认优先考虑 `formula`:适合常规计算、条件判断、文本处理、日期差、跨表聚合,以及需要长期显示在表里的派生结果。 -只有当用户明确要求 `lookup`,或场景天然符合 `from / select / where / aggregate` 这种固定查找建模时,再使用 `lookup`。 - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+field-create`(`type=formula`) | 创建公式字段 | [`formula-field-guide.md`](references/formula-field-guide.md)、[`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 没读 guide 前不要直接创建 | -| `+field-update`(`type=formula`) | 更新公式字段 | [`formula-field-guide.md`](references/formula-field-guide.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 先拿当前表结构 | -| `+field-create`(`type=lookup`) | 创建 lookup 字段 | [`lookup-field-guide.md`](references/lookup-field-guide.md)、[`lark-base-field-create.md`](references/lark-base-field-create.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 没读 guide 前不要直接创建 | -| `+field-update`(`type=lookup`) | 更新 lookup 字段 | [`lookup-field-guide.md`](references/lookup-field-guide.md)、[`lark-base-field-update.md`](references/lark-base-field-update.md)、[`lark-base-shortcut-field-properties.md`](references/lark-base-shortcut-field-properties.md) | 跨表时还要拿目标表结构 | - -### 2.5 数据分析模块 - -用于一次性分析和临时聚合查询。用户要的是“这次算出来的结果”,而不是把结果沉淀成字段时,优先进入本模块。 - -进入本模块前先确认几件事: - -- `+data-query` 只做聚合查询(分组、过滤、排序、聚合计算),不用于列出原始记录或逐条明细。 -- 调用者必须是目标多维表格的管理员,拥有目标多维表格的 FA(Full Access / 完全访问权限),否则会返回权限错误。 -- `+data-query` 只支持白名单字段类型;`formula`、`lookup`、附件、系统字段、关联等字段不能用于 `dimensions / measures / filters / sort`。 - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+data-query` | 做分组统计、SUM / AVG / COUNT / MAX / MIN、条件筛选后的聚合分析 | [`lark-base-data-query.md`](references/lark-base-data-query.md) | 字段名必须精确匹配真实字段名;不要用 `+record-list` / `+record-search` 拉全量再手算;`+data-query` 不返回原始记录;使用前先确认权限和字段类型是否受支持 | - -### 2.6 Workflow 模块 - -这是高约束模块。执行任何 workflow 命令前,都必须先读对应命令文档和 schema。 -模块索引:[`references/lark-base-workflow.md`](references/lark-base-workflow.md) - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+workflow-list / +workflow-get` | 列出 workflow,或获取完整 workflow 结构 | [`lark-base-workflow-list.md`](references/lark-base-workflow-list.md)、[`lark-base-workflow-get.md`](references/lark-base-workflow-get.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | `+workflow-list` 只返回摘要且只能串行执行;需要完整结构时用 `+workflow-get` | -| `+workflow-create / +workflow-update` | 创建或更新 workflow | [`lark-base-workflow-create.md`](references/lark-base-workflow-create.md)、[`lark-base-workflow-update.md`](references/lark-base-workflow-update.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | 先读 schema;禁止凭自然语言猜 `type`;先确认真实表名和字段名 | -| `+workflow-enable / +workflow-disable` | 启用或停用 workflow | [`lark-base-workflow-enable.md`](references/lark-base-workflow-enable.md)、[`lark-base-workflow-disable.md`](references/lark-base-workflow-disable.md)、[`lark-base-workflow-schema.md`](references/lark-base-workflow-schema.md) | 启用或停用前先确认目标 workflow;`workflow_id` 与 `table_id` 需按前缀区分 | - -### 2.7 Dashboard 模块 - -当用户提到“仪表盘、dashboard、数据看板、图表、可视化、block、组件、添加组件、创建图表”等关键词时,进入本模块,并先阅读 [`lark-base-dashboard.md`](references/lark-base-dashboard.md)。 - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+dashboard-list / +dashboard-get` | 列出仪表盘,或获取仪表盘详情 | [`lark-base-dashboard-list.md`](references/lark-base-dashboard-list.md)、[`lark-base-dashboard-get.md`](references/lark-base-dashboard-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 进入仪表盘语义后先读 guide;`+dashboard-list` 只能串行执行 | -| `+dashboard-create / +dashboard-update / +dashboard-delete` | 创建、更新或删除仪表盘 | [`lark-base-dashboard-create.md`](references/lark-base-dashboard-create.md)、[`lark-base-dashboard-update.md`](references/lark-base-dashboard-update.md)、[`lark-base-dashboard-delete.md`](references/lark-base-dashboard-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md) | 创建前先明确看板目标和展示场景;更新前先读取当前配置;删除前先确认目标 | -| `+dashboard-block-list / +dashboard-block-get` | 列出图表组件,或获取单个 block 详情 | [`lark-base-dashboard-block-list.md`](references/lark-base-dashboard-block-list.md)、[`lark-base-dashboard-block-get.md`](references/lark-base-dashboard-block-get.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | `+dashboard-block-list` 只能串行执行;查看配置细节时读 block config 文档 | -| `+dashboard-block-get-data` | 获取图表组件的计算结果 | [`lark-base-dashboard-block-get-data.md`](references/lark-base-dashboard-block-get-data.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | 适合读取图表组件的最终计算结果;此命令不返回 block 元数据,只返回计算结果 | -| `+dashboard-block-create / +dashboard-block-update / +dashboard-block-delete` | 创建、更新或删除图表组件 | [`lark-base-dashboard-block-create.md`](references/lark-base-dashboard-block-create.md)、[`lark-base-dashboard-block-update.md`](references/lark-base-dashboard-block-update.md)、[`lark-base-dashboard-block-delete.md`](references/lark-base-dashboard-block-delete.md)、[`lark-base-dashboard.md`](references/lark-base-dashboard.md)、[`dashboard-block-data-config.md`](references/dashboard-block-data-config.md) | 涉及 `data_config`、图表类型、filter 时要读 block config 文档;删除前先确认目标 | - -### 2.8 表单模块 - -用于管理表单本体和表单题目。 -模块索引:[`references/lark-base-form.md`](references/lark-base-form.md)、[`references/lark-base-form-questions.md`](references/lark-base-form-questions.md) -表单问题相关操作依赖 `form-id`;具体获取方式见 `form-list` 和 `form-create` 的 reference。 - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+form-list / +form-get` | 列出表单,或获取单个表单 | [`lark-base-form-list.md`](references/lark-base-form-list.md)、[`lark-base-form-get.md`](references/lark-base-form-get.md) | `+form-list` 可用来获取 `form-id`;`+form-get` 适合查看已有表单配置 | -| `+form-detail` | 通过表单分享链接获取表单详情(含题目列表、字段类型、校验规则) | [`lark-base-form-detail.md`](references/lark-base-form-detail.md) | 只读;仅需 `--share-token`(从分享链接提取),不需要 base-token/table-id/form-id;返回的 `questions` 可直接用于 `+form-submit` 构造参数 | -| `+form-submit` | 通过表单分享链接填写并提交表单(支持普通字段 + 附件上传) | [`lark-base-form-submit.md`](references/lark-base-form-submit.md) | 写入操作;仅支持 share_token 模式;**当 `--json` 包含 attachments 时必须额外提供 `--base-token`**(附件上传到 Base Drive Media 需要);附件通过 `--json.attachments` 传入本地路径,CLI 自动并行上传 | -| `+form-create / +form-update / +form-delete` | 创建、更新或删除表单 | [`lark-base-form-create.md`](references/lark-base-form-create.md)、[`lark-base-form-update.md`](references/lark-base-form-update.md)、[`lark-base-form-delete.md`](references/lark-base-form-delete.md) | 创建后可继续进入表单问题相关操作;更新或删除前先确认目标表单 | -| `+form-questions-list` | 列出表单题目 | [`lark-base-form-questions-list.md`](references/lark-base-form-questions-list.md) | 适合查看已有题目结构 | -| `+form-questions-create / +form-questions-update / +form-questions-delete` | 创建、更新或删除题目 | [`lark-base-form-questions-create.md`](references/lark-base-form-questions-create.md)、[`lark-base-form-questions-update.md`](references/lark-base-form-questions-update.md)、[`lark-base-form-questions-delete.md`](references/lark-base-form-questions-delete.md) | 先确认 `form-id`;更新或删除前先确认题目目标 | - -### 2.9 权限与角色模块 - -用于启用高级权限,以及管理 Base 自定义角色。 -涉及 `+advperm-enable / +advperm-disable / +role-*` 时,操作用户必须为 Base 管理员,否则会返回权限错误。 - -| 命令 | 用途 / 何时使用 | 必读 reference | 路由提醒 | -|------|------------------|----------------|----------| -| `+advperm-enable / +advperm-disable` | 启用或停用高级权限 | [`lark-base-advperm-enable.md`](references/lark-base-advperm-enable.md)、[`lark-base-advperm-disable.md`](references/lark-base-advperm-disable.md) | 管理角色前必须先启用;停用是高风险操作,会使已有自定义角色失效 | -| `+role-list / +role-get` | 列出角色,或获取角色详情 | [`lark-base-role-list.md`](references/lark-base-role-list.md)、[`lark-base-role-get.md`](references/lark-base-role-get.md)、[`role-config.md`](references/role-config.md) | `+role-list` 只能串行执行;`+role-get` 适合查看完整权限配置 | -| `+role-create / +role-update / +role-delete` | 创建、更新或删除角色 | [`lark-base-role-create.md`](references/lark-base-role-create.md)、[`lark-base-role-update.md`](references/lark-base-role-update.md)、[`lark-base-role-delete.md`](references/lark-base-role-delete.md)、[`role-config.md`](references/role-config.md) | `+role-create` 仅支持 `custom_role`;`+role-update` 采用 Delta Merge,`role_name` 和 `role_type` 即使不改也必须传当前值;`+role-delete` 不可逆 | - -## 3. 多维表格通用知识 - -飞书多维表格英文名是 `Base`,曾用名 `Bitable`;因此旧文档、返回字段、参数名或错误信息里出现 `bitable` 多属历史兼容,不代表应改用另一套命令体系。 - -### 3.1 字段分类与可写性 - -| 字段类型 | 含义 | 能否直接作为 `+record-upsert / +record-batch-create / +record-batch-update` 写入目标 | 说明 | -|----------|------|-----------------------------------------------------------|------| -| 存储字段 | 真实存用户输入的数据 | 可以 | 常见如文本、数字、日期、单选、多选、人员、关联 | -| 附件字段 | 存储文件附件 | 不应直接按普通字段写 | 上传附件走 `+record-upload-attachment`;下载附件走 `+record-download-attachment`;删除附件走 `+record-remove-attachment` | -| 地理位置字段 | 存储坐标并由平台解析地址 | 可以 | 写入必须使用 `{lng,lat}`;读取、筛选和转文本等场景使用 `full_address` 字符串;只有公式能访问坐标 | -| 系统字段 | 平台自动维护 | 不可以 | 常见如创建时间、更新时间、创建人、修改人、自动编号 | -| `formula` 字段 | 通过表达式计算 | 不可以 | 只读字段 | -| `lookup` 字段 | 通过跨表规则查找引用 | 不可以 | 只读字段 | - -### 3.2 任务选路心智模型 - -| 用户诉求 | 优先方案 | 不要误走 | -|---------|----------|----------| -| 一次性分析 / 临时统计 | `+data-query` | 不要用 `+record-list` / `+record-search` 拉全量后手算 | -| 要把结果长期显示在表里 | `formula` 字段 | 不要只给一次性手工分析结果 | -| 用户明确要求 lookup,或天然是固定查找配置 | `lookup` 字段 | 不要默认先上 lookup;先判断 formula 是否更合适 | -| 读取原始记录明细 / 关键词检索 / 导出 | `+record-search / +record-list / +record-get` | 不要拿 `+data-query` 当取数命令 | -| 上传附件到记录 | `+record-upload-attachment` | 不要用 `+record-upsert` / `+record-batch-*` 伪造附件值 | -| 下载记录里的附件文件 | `+record-download-attachment --record-id --output `,可加 `--file-token ` 只下指定附件 | Base 附件必须用这个命令下载;用其他下载入口可能失败 | -| 写入地理位置 | `+record-upsert` / `+record-batch-*` 传 `{lng,lat}` | 不要把纯地址文本当成 CellValue | -| 基于视图做筛选读取 | `+view-set-filter` + `+record-list` | 不要跳过视图筛选直接猜条件 | -| 本地 Excel / CSV / `.base` 导入为 Base | `lark-cli drive +import --type bitable` | 不要误走 `+base-create`、`+table-create` 或 `+record-upsert` | - -### 3.3 查询执行契约 - -涉及查询、统计或判断结论时,先阅读 [`references/lark-base-data-analysis-sop.md`](references/lark-base-data-analysis-sop.md),并遵守以下高优先级规则: - -1. `+record-list` 默认页、固定 `--limit` 和本地 `jq` 只能证明已读取范围内的事实,不能直接支撑全局最值、全量计数、Top/Bottom N、异常识别或分组结论。 -2. 能由 Base 表达的筛选、排序、投影、聚合、分组和限制,应在 Base 云端查询服务中执行;不要先拉明细到本地上下文再手工筛选排序。 +1. `+record-list` 的默认页、固定 `--limit` 和本地 `jq` 只能证明已读取范围内的事实,不能直接支撑全局最值、全量计数、Top/Bottom N、异常识别或分组结论。 +2. 能由 Base 表达的筛选、排序、投影、聚合、分组和限制,应在 Base 云端查询能力中执行;不要先拉明细到本地上下文再手工筛选排序。 3. `has_more=true` 或等价分页信号表示当前结果不是全量;除非用户只要样例/前 N 条,不能基于该页回答全局问题。 4. 多表查询必须先确认关系字段和连接键;link 单元格里的 `record_id` 是关系键,不是用户可读答案。 5. 最终答案必须能追溯到真实表、真实字段、查询范围、筛选/排序/聚合条件和必要的连接键。 +6. 一次性分析优先用 `+data-query` 或临时视图;要把结果长期显示在表里,才考虑新增 `formula` / `lookup` 字段。 +7. `+data-query` 返回聚合结果,不返回原始记录明细;需要输出实体字段时,用聚合结果中的业务 key 或 record_id 再走 record 路径回查。 -### 3.4 表名、字段名与表达式引用 +## 写入前置规则 -1. 表名、字段名必须精确匹配真实返回,来源应是 `+table-list / +table-get / +field-list`。 -2. 不要凭自然语言猜名称,不要自行改写用户口述中的表名、字段名。 -3. `formula / lookup / data-query / workflow` 中出现的名称同样必须精确匹配;表达式引用、where 条件、DSL 字段名、workflow 配置都遵守同一规则。 -4. 跨表场景必须额外读取目标表结构,不能只看当前表。 +- 写记录前先读字段结构;只写存储字段。系统字段、附件字段、`formula`、`lookup` 不作为普通记录写入目标。 +- 附件上传、下载、删除走专用 `+record-*-attachment` 命令。 +- 写字段前先读 `lark-base-field-json.md`;涉及 `formula` / `lookup` 时必须读对应 guide。 +- 表名、字段名、视图名、workflow 配置中的名称必须来自真实返回;跨表场景还要读取目标表结构。 +- 删除、角色更新、字段更新等高风险操作遵循 CLI 的 confirmation gate;目标不明确时先用 get/list 消歧。 +- 批量写入单批最多 200 条;连续写同一表时串行执行,遇到 `1254291` 按短暂等待后重试处理。 +- `+record-batch-update` 是“同值批量更新”:同一份 patch 应用到全部 `record_id_list`,不要拿它做逐行不同值映射。 +- select/multiselect 写入未知选项可能触发平台新增选项;不是要新增时,先用 `+field-list` 或 `+field-search-options` 确认可选值。 -### 3.5 Token 与链接 +## 表单与视图细节 -这是高优先级章节。只要用户输入里出现链接、token,或报错涉及 `baseToken` / `wiki_token` / `obj_token`,都应优先回到这里检查。 +- `+form-submit` 前必须先跑 `+form-detail`,读取 `questions[].type`、`required`、`filter` 和附件场景需要的 `base_token`;不要填写被 filter 隐藏的问题。 +- 表单附件不要写进 `fields`,放在 `--json.attachments`;提交附件时必须同时传表单所属 Base 的 `--base-token`。 +- `+view-set-filter` 是唯一保留的 view reference;sort/group/card/timebar/visible-fields 这类配置先用对应 get 命令读现状,保留未修改字段,只替换用户要求变更的配置。 +- 临时视图适合一次性筛选/排序后读取;如果筛选结果对用户后续查看有价值,应保留为持久视图并说明名称和用途。 -| 输入类型 | 正确处理方式 | 说明 | -|---------|--------------|------| -| 直接 Base 链接 `/base/{token}` | 直接提取 token 作为 `--base-token` | 不要把完整 URL 直接作为 `--base-token` | -| Wiki 链接 `/wiki/{token}` | 先用下方 fast path 解析 `data.obj_token` | 不要把 `wiki_token` 直接当 `--base-token`;如果这一步失败,再看 [`lark-wiki-node-get.md`](../lark-wiki/references/lark-wiki-node-get.md) | -| URL 中的 `?table={id}` | 先按前缀判断对象类型 | `tbl` 开头表示数据表 `table-id`,可作为 `--table-id`;`blk` 开头表示仪表盘 `dashboard-ID`;`wkf` 开头表示 `workflow-ID`;`ldx` 开头表示内嵌文档,不要一律当成 `--table-id` | -| URL 中的 `?view={id}` | 提取为 `--view-id` | 适合直接定位视图 | +## Token 与链接 -Wiki Base fast path: +| 输入类型 | 含义 / 正确处理方式 | +|---|---| +| `/base/{token}` | 普通 Base 链接;提取 `/base/` 后的 token 作为 `--base-token` | +| `/wiki/{token}` | Wiki 节点链接;先 `wiki +node-get`,当 `data.obj_type=bitable` 时使用 `data.obj_token` 作为 `--base-token` | +| `/base/{token}?table={id}` | `table` 参数用于定位 Base 内对象:`tbl` 开头是数据表 `--table-id`;`blk` 开头是 dashboard ID;`wkf` 开头是 workflow ID | +| `/base/{token}?view={id}` | `view` 参数用于定位表视图,提取为 `--view-id`;通常还需要确认 `table` 参数或先查表结构 | +| `/share/base/form/{shareToken}` | 表单分享链接;这是表单 share token,走 `+form-detail` / `+form-submit --share-token ` | +| `/share/base/view/{shareToken}` | 视图分享链接;具有分享权限语义,暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开 | +| `/share/base/dashboard/{shareToken}` | 仪表盘分享链接;具有分享权限语义,暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开 | +| `/record/{shareToken}` | 记录分享链接;暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开。若用户想生成现有记录的分享链接,用 `+record-share-link-create --base-token --table-id --record-ids ` | +| `/base/workspace/{token}` | BaseApp / workspace 链接;暂不支持用 CLI 直接访问 | -```bash -BASE_TOKEN="$(lark-cli wiki +node-get --as user --node-token "" --jq '.data | select(.obj_type == "bitable") | .obj_token')" -``` +`wiki +node-get` 返回非 `bitable` 时,不继续使用 Base 命令:`docx` 转文档,`sheet` 转表格,其他云空间对象转对应 skill 或 drive。 -| `lark-cli wiki +node-get` 返回的 `data.obj_type` | 后续路线 | 说明 | -|-----------------------------------------------|----------|------| -| `bitable` | 优先走 `lark-cli base +...` | 如果 shortcut 不覆盖,再用 `lark-cli base `;不要改走 `lark-cli api /open-apis/bitable/v1/...` | -| `docx` | 转到文档 / Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | -| `sheet` | 转到 Sheets 相关 skill | 不继续使用本 skill 的 Base 命令 | -| `slides` | 转到 Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | -| `mindnote` | 转到 Drive 相关 skill | 不继续使用本 skill 的 Base 命令 | +## Dashboard / Workflow / Role -### 3.6 身份选择与权限降级策略 +- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 `dashboard-block-data-config.md`,组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。 +- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 `lark-base-workflow-guide.md` 和 steps JSON SSOT `lark-base-workflow-schema.md`;enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。 +- Role 的复杂点是权限 JSON。角色操作先读入口 `lark-base-role-guide.md`;`+role-create` 只支持自定义角色;`+role-update` 是 delta merge;角色 create/update 或解读完整配置时读权限 JSON SSOT `role-config.md`。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。 -多维表格通常属于用户的个人或团队资源。**默认应优先使用 `--as user`(用户身份)执行所有 Base 操作**,始终显式指定身份。 +## 常见恢复 -- **`--as user`(推荐)**:以当前登录用户身份操作其有权访问的 Base。执行前先完成用户授权: +| 错误 / 现象 | 恢复动作 | +|---|---| +| `param baseToken is invalid` / `base_token invalid` | 检查是否把 wiki token、workspace token 或完整 URL 当成了 `--base-token`;按 `Token 与链接` 重新定位真实 Base token | +| `not found` 且输入来自 Wiki 链接 | 优先检查是否把 wiki token 当成 base token,不要立刻改走裸 API | +| `1254045` 字段名不存在 | 重新 `+field-list`,使用真实字段名或字段 ID;注意空格、大小写和跨表字段 | +| `1254015` 字段值类型不匹配 | 先 `+field-list`,再按 `lark-base-cell-value.md` 构造 CellValue | +| 日期 / 人员 / 超链接字段报格式错误 | 日期用 `YYYY-MM-DD HH:mm:ss`;人员用 `[{ "id": "ou_xxx" }]`;超链接用 URL 或 markdown link 字符串 | +| formula / lookup 创建失败 | 先读 `formula-field-guide.md` / `lookup-field-guide.md`,再按 guide 重建请求 | +| `ignored_fields` / `READONLY` | 移除只读字段,只写存储字段 | +| `1254104` | 批量超过 200,分批调用 | +| `1254291` | 并发写冲突,串行写入并在批次间短暂等待 | +| `91403` | 无权限访问该 Base,按 `lark-shared` 权限流程处理,不要盲目重试 | -```bash -lark-cli auth login --domain base -``` +## 保留 Reference -- **`--as bot`(降级)**:仅当 user 身份权限不足、且 bot 身份确实拥有目标 Base 的访问权限时,才降级使用。bot 看不到用户私有资源,行为以应用身份执行。 - -**执行规则**: - -1. 所有操作默认先用 `--as user`。 -2. 若 user 身份返回权限错误,先判断是否为**不可重试错误码**(如 `91403`)。若是,**立即停止**,不做任何重试或降级,直接按 `lark-shared` 权限不足处理流程引导用户解决。 -3. 非不可重试错误码时,检查错误响应中是否包含 `permission_violations` / `hint` 等提权引导信息: - - **有提权引导**:按 `lark-shared` 权限不足处理流程,先引导用户完成 user 身份提权(`auth login --scope`);确认提权成功后,以 `--as user` 重试。 - - **无提权引导**(如资源级无访问权限、非 scope 不足):切换到 `--as bot` 重试**一次**。 -4. 若 bot 身份仍然返回权限错误,**立即停止重试**,根据错误响应按 `lark-shared` 流程引导用户解决(引导去开发者后台开通 scope 或确认资源访问权限)。 -5. 只有在用户明确要求"用应用身份 / bot 身份操作",才跳过 user 直接使用 `--as bot`。 - -## 4. 执行规则 - -### 4.1 标准执行顺序 - -1. 先判断任务属于哪个模块,选对命令族。 -2. 如果用户给了链接,先解析 token,不要把 wiki token、完整 URL 或其他对象 ID 误当成 `base_token`。 -3. 如果是查询类任务,先判断问题范围,阅读 data analysis SOP,再决定使用 `record / view / data-query`。 -4. 先拿结构,再写命令,避免猜表名、字段名、表达式引用。 -5. 定位到命令后,先读对应 reference,再执行命令。 -6. 执行命令,并按返回结果判断下一步。 -7. 回复时返回关键结果和后续可继续操作的信息,方便 agent 链式执行下一步。 - -### 4.2 不可违反规则 - -1. 先拿结构,再写命令;至少先拿当前表结构,跨表时还要拿目标表结构。 -2. 不要猜表名、字段名、表达式引用,一律以真实返回为准。 -3. 只使用原子命令;不要回退到旧的聚合式 `+table / +field / +record / +view / +history / +workspace`。 -4. 写记录前先读字段结构;先 `+field-list`,再按 [`lark-base-cell-value.md`](references/lark-base-cell-value.md) 构造 CellValue。 -5. 写字段前先看字段属性规范;先读 `lark-base-shortcut-field-properties.md`,再构造 `+field-create / +field-update` 的 JSON。 -6. 只写可写字段;系统字段、附件字段、`formula`、`lookup` 默认不作为普通记录写入目标。 -7. 聚合分析与取数分流;统计走 `+data-query`,关键词检索走 `+record-search`,明细走 `+record-list / +record-get`。 -8. 筛选查询按视图能力执行;先用 `+view-set-filter` 配置筛选,再结合 `+record-list` 读取。 -9. 全局查询不得基于默认分页、小 `--limit` 或未证明全量的本地 `jq` 结果下结论。 -10. Base 场景不要改走裸 API,不要切去 `lark-cli api /open-apis/bitable/v1/...`。 -11. 统一使用 `--base-token`。 -12. workflow 场景先读 schema,不要凭自然语言猜 `type`。 -13. dashboard 场景先读 guide;提到图表、看板、block 就先进入 dashboard 模块。 -14. formula / lookup 场景先读 guide;没读 guide 前不要直接创建或更新。 - -### 4.3 并发、分页与批量限制 - -- `+table-list / +field-list / +record-list / +view-list / +record-history-list / +role-list / +dashboard-list / +dashboard-block-list / +workflow-list` 禁止并发调用,只能串行执行。 -- `+record-list` 分页时,`--limit` 最大 `200`;先拉首批并检查 `has_more`,只有用户明确需要更多数据时再继续翻页。 -- 批量写入时,单批不超过 `200` 条。 -- 连续写入同一表时,必须串行写入,批次间延迟 `0.5–1` 秒。 - -### 4.4 确认与回复规则 - -- 视图重命名时,用户已明确“把哪个视图改成什么名字”时,`+view-rename` 直接执行即可。 -- 更新字段或删除记录 / 字段 / 表时,如果用户已经明确目标,`+field-update / +record-delete / +field-delete / +table-delete` 可直接执行,并带 `--yes`。 -- 删除目标仍有歧义时,先用 `+record-get / +field-get / +table-get` 或相应 list 命令确认。 -- `+base-create / +base-copy` 成功后,回复中必须主动返回新 Base 的标识信息;若结果带可访问链接,也应一并返回。 -- 若 Base 由 bot 身份创建或复制,shortcut 会自动尝试为当前 CLI 用户补授 `full_access`,并在输出中返回 `permission_grant`;agent 不需要再手动编排单独授权。owner 转移必须单独确认,禁止擅自执行。 - -## 5. 常见错误与恢复 - -| 错误 / 现象 | 含义 | 恢复动作 | -|-------------|------|----------| -| `1254064` | 日期格式错误 | 传 `YYYY-MM-DD HH:mm:ss` 字符串,不要写相对时间 | -| `1254068` | 超链接格式错误 | `"https://example.com"` 或 `"[文本](https://example.com)"` | -| `1254066` | 人员字段错误 | `[{ "id": "ou_xxx" }]` | -| `1254045` | 字段名不存在 | 检查字段名(含空格、大小写) | -| `1254015` | 字段值类型不匹配 | 先 `+field-list`,再按类型构造 | -| `param baseToken is invalid` / `base_token invalid` | 把 wiki token、workspace token 或其他 token 当成了 `base_token` | 如果输入来自 `/wiki/...`,先用 `lark-cli wiki +node-get --node-token ` 取真实 `data.obj_token`;当 `data.obj_type=bitable` 时,用 `data.obj_token` 作为 `--base-token` 重试,不要改走 `bitable/v1` | -| `not found` 且用户给的是 wiki 链接 | 常见于把 wiki token 当成 base token | 优先回退检查 wiki 解析,而不是改走 `bitable/v1` | -| formula / lookup 创建失败 | 指南未读或结构不合法 | 先读 `formula-field-guide.md` / `lookup-field-guide.md`,再按 guide 重建请求 | -| `ignored_fields` / `READONLY` | 只读字段被当成可写字段,常见于系统字段、formula、lookup | 移除只读字段,只写存储字段;计算结果交给 formula / lookup / 系统字段自动产出 | -| `1254104` | 批量超 200 条 | 分批调用 | -| `1254291` | 并发写冲突 | 串行写入 + 批次间延迟 | -| `91403` | 无权限访问该 Base | **不要重试**。按 `lark-shared` 权限不足处理流程引导用户解决权限问题 | +- `lark-base-data-analysis-sop.md`:查询/统计/全局结论的选路 SOP +- `lark-base-data-query-guide.md` / `lark-base-data-query.md`:聚合查询入口 fewshot 与 DSL SSOT +- `lark-base-cell-value.md`:记录 CellValue 构造 +- `lark-base-field-json.md`:字段 JSON 构造 +- `formula-field-guide.md` / `lookup-field-guide.md`:公式与 lookup 字段 +- `lark-base-field-create.md` / `lark-base-field-update.md`:字段创建/更新命令级补充 +- `lark-base-record-upsert.md` / `lark-base-record-batch-create.md` / `lark-base-record-batch-update.md` / `lark-base-record-history-list.md`:记录写入 JSON 与历史返回解释 +- `lark-base-view-set-filter.md`:视图筛选 JSON +- `lark-base-form-detail.md` / `lark-base-form-submit.md` / `lark-base-form-questions-create.md` / `lark-base-form-questions-update.md`:表单详情、提交和复杂 JSON +- `lark-base-dashboard.md` / `dashboard-block-data-config.md` / `lark-base-dashboard-block-get-data.md`:仪表盘、组件配置与图表结果协议 +- `lark-base-workflow-guide.md` / `lark-base-workflow-schema.md`:workflow 入口与 steps JSON SSOT +- `lark-base-role-guide.md` / `role-config.md`:角色入口与权限 JSON SSOT diff --git a/skills/lark-base/references/dashboard-block-data-config.md b/skills/lark-base/references/dashboard-block-data-config.md index e5f400fe3..92e27478a 100644 --- a/skills/lark-base/references/dashboard-block-data-config.md +++ b/skills/lark-base/references/dashboard-block-data-config.md @@ -1,6 +1,6 @@ -# dashboard block data_config 参考 +# dashboard block data_config SSOT -Block 的 `data_config` 字段因 `type` 不同而变化。本文档描述所有共享结构。 +Block 的 `data_config` 字段因 `type` 不同而变化。本文档是 dashboard block `data_config` 的单一事实来源(SSOT),包含组件类型、字段结构、筛选格式、约束和可复制模板。 ## 支持的组件类型(`type` 枚举) diff --git a/skills/lark-base/references/examples.md b/skills/lark-base/references/examples.md deleted file mode 100644 index f98e956e8..000000000 --- a/skills/lark-base/references/examples.md +++ /dev/null @@ -1,140 +0,0 @@ -# 飞书多维表格使用场景完整示例(base) - -本文档提供基于 `lark-cli base +...` shortcut 的完整示例。 - -> **返回**: [SKILL.md](../SKILL.md) | **参考**: [shortcut 字段 JSON 规范](lark-base-shortcut-field-properties.md) · [CellValue 规范](lark-base-cell-value.md) - ---- - -## 场景 1:用 unified Shortcut 快速建表 - -适合已经明确字段结构、希望一次性完成建表的场景。 - -```bash -lark-cli base +table-create \ - --base-token bascnXXXXXXXX \ - --name "客户管理表" \ - --fields '[ - {"name":"客户名称","type":"text","description":"主标题字段"}, - {"name":"负责人","type":"user","multiple":false,"description":"用于标记客户跟进的直接负责人"}, - {"name":"签约日期","type":"datetime"}, - {"name":"状态","type":"select","multiple":false,"options":[{"name":"进行中"},{"name":"已完成"}]} - ]' -``` - ---- - -## 场景 2:创建数据表并查看字段 - -适合需要先建表、再确认字段结构的场景。 - -### 步骤 1:在已有 Base 中创建数据表 - -```bash -lark-cli base +table-create \ - --base-token bascnXXXXXXXX \ - --name "客户管理表" -``` - -### 步骤 2:列出字段 - -```bash -lark-cli base +field-list \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --limit 100 -``` - -> 提示:Base token 统一通过 `--base-token` 传入;表 ID 统一通过 `--table-id` 传入。 - ---- - -## 场景 3:创建、读取、更新单条记录 - -### 新增记录 - -```bash -lark-cli base +record-upsert \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --json '{ - "客户名称":"字节跳动", - "负责人":[{"id":"ou_xxx"}], - "状态":"进行中" - }' -``` - -### 列出记录 - -```bash -lark-cli base +record-list \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --limit 100 -``` - -### 更新记录 - -```bash -lark-cli base +record-upsert \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --record-id recXXXXXXXX \ - --json '{ - "状态":"已完成" - }' -``` - -### 删除记录 - -```bash -lark-cli base +record-delete \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --record-id recXXXXXXXX \ - --yes -``` - ---- - -## 场景 4:配置视图筛选后按视图读取记录 - -需要筛选查询时,推荐先写视图筛选,再通过 `view_id` 读取记录。 - -### 更新视图筛选条件 - -```bash -lark-cli base +view-set-filter \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --view-id vewXXXXXXXX \ - --json '{ - "logic":"and", - "conditions":[ - { - "field_name":"状态", - "operator":"is", - "value":["进行中"] - } - ] - }' -``` - -### 按视图读取记录 - -```bash -lark-cli base +record-list \ - --base-token bascnXXXXXXXX \ - --table-id tblXXXXXXXX \ - --view-id vewXXXXXXXX \ - --limit 100 -``` - ---- - -## 场景 5:什么时候优先用 Shortcut - -- 需要一次性建表并附带字段、视图时,优先 `lark-cli base +table-create` -- 需要按业务字段名做 upsert 时,优先 `lark-cli base +record-upsert` -- 需要配置筛选视图时,优先 `lark-cli base +view-set-filter` -- 需要记录历史时,优先 `lark-cli base +record-history-list` diff --git a/skills/lark-base/references/lark-base-advperm-disable.md b/skills/lark-base/references/lark-base-advperm-disable.md deleted file mode 100644 index 3781dd824..000000000 --- a/skills/lark-base/references/lark-base-advperm-disable.md +++ /dev/null @@ -1,83 +0,0 @@ -# base +advperm-disable - -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -停用指定 Base 的高级权限。停用后自定义角色等高级权限功能将不可用。 - -## 推荐命令 - -```bash -# 停用高级权限 -lark-cli base +advperm-disable \ - --base-token VwGhbYCXQaYGMzsWlEZcBbfMnod -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token,27 位字母数字字符串 | - -## API 入参详情 - -**HTTP 方法和路径:** - -``` -PUT /open-apis/base/v3/bases/:base_token/advperm/enable?enable=false -``` - -**Path 参数:** - -| 参数 | 必填 | 说明 | -|------|------|------| -| `base_token` | 是 | Base 的唯一标识,27 位字母数字字符串 | - -**Query 参数:** - -| 参数 | 必填 | 类型 | 说明 | -|------|------|------|------| -| `enable` | 是 | bool | 固定为 `false`,表示停用高级权限 | - -## API 出参详情 - -**Response:** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `code` | int32 | 错误码,0 表示成功 | -| `message` | string | 错误信息 | -| `data` | string | 成功时为空 | - -## 返回值 - -命令成功后输出 JSON: - -```json -{ - "ok": true, - "data": { - "success": true - } -} -``` - -## 工作流 - -> [!CAUTION] -> 这是**高风险写入操作** — 停用高级权限会影响所有已配置的自定义角色,执行前必须向用户确认。 - -1. 向用户确认 `--base-token`,并提醒停用会影响已有角色配置 -2. 执行命令 -3. 确认返回 `code: 0` 表示停用成功 - -## 坑点 - -- ⚠️ **操作用户必须为 Base 管理员**:非管理员调用会返回权限错误 -- ⚠️ **停用影响已有角色**:停用高级权限后,已创建的自定义角色将失效 -- ⚠️ **API 路径版本**:本接口使用 `base/v3`,路径必须从原始文档提取,不要用 WebSearch 补全 -- ⚠️ **data 字段是 JSON 字符串**:响应中 `data` 是 string 类型(非 object),需要双重解析 - -## 参考 - -- [lark-base](../SKILL.md) — 多维表格全部命令 -- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-base/references/lark-base-advperm-enable.md b/skills/lark-base/references/lark-base-advperm-enable.md deleted file mode 100644 index 3dd2f4d61..000000000 --- a/skills/lark-base/references/lark-base-advperm-enable.md +++ /dev/null @@ -1,80 +0,0 @@ -# base +advperm-enable - -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -启用指定 Base 的高级权限。启用后可使用自定义角色等高级权限功能。 - -## 推荐命令 - -```bash -# 启用高级权限 -lark-cli base +advperm-enable \ - --base-token VwGh**************Mnod -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token,27 位字母数字字符串 | - -## API 入参详情 - -**HTTP 方法和路径:** - -``` -PUT /open-apis/base/v3/bases/:base_token/advperm/enable?enable=true -``` - -**Path 参数:** - -| 参数 | 必填 | 说明 | -|------|------|------| -| `base_token` | 是 | Base 的唯一标识,27 位字母数字字符串 | - -**Query 参数:** - -| 参数 | 必填 | 类型 | 说明 | -|------|------|------|------| -| `enable` | 是 | bool | 固定为 `true`,表示启用高级权限 | - -## API 出参详情 - -**Response:** - -| 字段 | 类型 | 说明 | -|------|------|------| -| `code` | int32 | 错误码,0 表示成功 | -| `message` | string | 错误信息 | -| `data` | string | 成功时为空 | - -## 返回值 - -命令成功后输出 JSON: - -```json -{ - "ok": true, - "data": { - "success": true - } -} -``` - -## 工作流 - -1. 向用户确认 `--base-token` -2. 执行命令 -3. 确认返回 `code: 0` 表示启用成功 - -## 坑点 - -- ⚠️ **操作用户必须为 Base 管理员**:非管理员调用会返回权限错误 -- ⚠️ **API 路径版本**:本接口使用 `base/v3`,路径必须从原始文档提取,不要用 WebSearch 补全 -- ⚠️ **data 字段是 JSON 字符串**:响应中 `data` 是 string 类型(非 object),需要双重解析 -- ⚠️ **启用后才能管理角色**:`+role-create / +role-update / +role-delete` 等角色操作需要先启用高级权限 - -## 参考 - -- [lark-base](../SKILL.md) — 多维表格全部命令 -- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-base/references/lark-base-base-copy.md b/skills/lark-base/references/lark-base-base-copy.md deleted file mode 100644 index 33f637509..000000000 --- a/skills/lark-base/references/lark-base-base-copy.md +++ /dev/null @@ -1,74 +0,0 @@ -# base +base-copy - -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -复制一个已有 Base;可选只复制结构,不复制内容。 - -## 推荐命令 - -```bash -lark-cli base +base-copy \ - --base-token app_xxx \ - --name "Copied Base" - -lark-cli base +base-copy \ - --base-token app_xxx \ - --name "Copied Base" \ - --folder-token fld_xxx \ - --time-zone Asia/Shanghai \ - --without-content -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | 源 Base Token | -| `--name ` | 否 | 新 Base 名称 | -| `--folder-token ` | 否 | 目标文件夹 token | -| `--time-zone ` | 否 | 时区,如 `Asia/Shanghai` | -| `--without-content` | 否 | 只复制结构,不复制内容 | - -## API 入参详情 - -**HTTP 方法和路径:** - -``` -POST /open-apis/base/v3/bases/:base_token/copy -``` - -## 返回重点 - -- 返回 `base`。 -- CLI 会额外标记 `copied: true`。 -- 回复结果时,必须主动返回新 Base 的可访问链接: - - 优先使用返回结果中的 `base.url` - - 同时返回新 Base 的 token - - 如果本次返回没有 `url`,至少返回新 Base 的名称和 token - -> [!IMPORTANT] -> 如果 Base 是**以应用身份(bot)复制**出来的,shortcut 会在复制成功后自动尝试为当前 CLI 用户添加该 Base 的 `full_access`(管理员)权限,并在输出中附带 `permission_grant` 字段。 -> -> `permission_grant.status` 语义如下: -> - `granted`:当前 CLI 用户已获得该 Base 的管理员权限 -> - `skipped`:Base 已复制成功,但没有可授权的当前 CLI 用户,或复制结果缺少可授权 token -> - `failed`:Base 已复制成功,但自动授权失败;结果中会包含失败原因,用户可稍后重试授权,或继续使用应用身份(bot)处理该 Base -> -> 回复复制结果时,除 `base token` 和可访问链接外,还必须明确告知用户 `permission_grant` 的结果。 -> -> **仍然不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 - -## 工作流 - -> [!CAUTION] -> 这是**写入操作** — 执行前必须向用户确认。 - -1. 先确认源 Base Token。 -2. `--name`、`--folder-token`、`--time-zone` 都是可选项;用户没要求时不要为这些可选参数额外追问。 -3. 只要结构时,显式传 `--without-content`。 -4. 复制成功后,整理并返回:新 Base 名称、token,以及响应中已有的可访问链接。 - -## 参考 - -- [lark-base-workspace.md](lark-base-workspace.md) — base / workspace 索引页 -- [lark-base-base-create.md](lark-base-base-create.md) — 创建全新 Base diff --git a/skills/lark-base/references/lark-base-base-create.md b/skills/lark-base/references/lark-base-base-create.md deleted file mode 100644 index 2544b037c..000000000 --- a/skills/lark-base/references/lark-base-base-create.md +++ /dev/null @@ -1,68 +0,0 @@ -# base +base-create - -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -创建一个新的 Base;可选指定父文件夹和时区。 - -## 推荐命令 - -```bash -lark-cli base +base-create \ - --name "New Base" - -lark-cli base +base-create \ - --name "项目管理" \ - --folder-token fld_xxx \ - --time-zone Asia/Shanghai -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--name ` | 是 | 新 Base 名称 | -| `--folder-token ` | 否 | 目标文件夹 token | -| `--time-zone ` | 否 | 时区,如 `Asia/Shanghai` | - -## API 入参详情 - -**HTTP 方法和路径:** - -``` -POST /open-apis/base/v3/bases -``` - -## 返回重点 - -- 返回 `base`。 -- CLI 会额外标记 `created: true`。 -- 回复结果时,必须主动返回新 Base 的可访问链接: - - 优先使用返回结果中的 `base.url` - - 同时返回新 Base 的 token - - 如果本次返回没有 `url`,至少返回新 Base 的名称和 token - -> [!IMPORTANT] -> 如果 Base 是**以应用身份(bot)创建**的,shortcut 会在创建成功后自动尝试为当前 CLI 用户添加该 Base 的 `full_access`(管理员)权限,并在输出中附带 `permission_grant` 字段。 -> -> `permission_grant.status` 语义如下: -> - `granted`:当前 CLI 用户已获得该 Base 的管理员权限 -> - `skipped`:Base 已创建成功,但没有可授权的当前 CLI 用户,或创建结果缺少可授权 token -> - `failed`:Base 已创建成功,但自动授权失败;结果中会包含失败原因,用户可稍后重试授权,或继续使用应用身份(bot)处理该 Base -> -> 回复创建结果时,除 `base token` 和可访问链接外,还必须明确告知用户 `permission_grant` 的结果。 -> -> **仍然不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。 - -## 工作流 - -> [!CAUTION] -> 这是**写入操作** — 执行前必须向用户确认。 - -1. 先确认 Base 名称。 -2. `--folder-token`、`--time-zone` 都是可选项;用户没要求时不要为此额外追问。 -3. 创建成功后,整理并返回:Base 名称、token,以及响应中已有的可访问链接。 - -## 参考 - -- [lark-base-workspace.md](lark-base-workspace.md) — base / workspace 索引页 -- [lark-base-base-copy.md](lark-base-base-copy.md) — 复制 Base diff --git a/skills/lark-base/references/lark-base-base-get.md b/skills/lark-base/references/lark-base-base-get.md deleted file mode 100644 index 57791f332..000000000 --- a/skills/lark-base/references/lark-base-base-get.md +++ /dev/null @@ -1,39 +0,0 @@ -# base +base-get - -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -读取一个 Base 的详情。 - -## 推荐命令 - -```bash -lark-cli base +base-get \ - --base-token app_xxx -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | - -## API 入参详情 - -**HTTP 方法和路径:** - -``` -GET /open-apis/base/v3/bases/:base_token -``` - -## 返回重点 - -- 返回 `base`,通常包含 `base_token / name / url` 等信息。 - -## 坑点 - -- ⚠️ 先确认传入的是 `base_token`,不是 `workspace_token`。 -- ⚠️ 如果最初输入来自 `/wiki/...`,不要直接把 `wiki_token` 当 `--base-token`;若报 `param baseToken is invalid` / `base_token invalid`,先用 `lark-cli wiki spaces get_node` 取 `node.obj_token`,再重试 `+base-get`。 - -## 参考 - -- [lark-base-workspace.md](lark-base-workspace.md) — base 索引页 diff --git a/skills/lark-base/references/lark-base-dashboard-arrange.md b/skills/lark-base/references/lark-base-dashboard-arrange.md deleted file mode 100644 index 7a793137d..000000000 --- a/skills/lark-base/references/lark-base-dashboard-arrange.md +++ /dev/null @@ -1,83 +0,0 @@ -# base +dashboard-arrange - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 - -自动重新排列仪表盘组件布局。服务端根据组件数量和类型进行智能布局优化。 - -## 使用场景 - -| 场景 | 说明 | -|------|------| -| **从 0 到 1 搭建后** | 使用 `+dashboard-create` 和 `+dashboard-block-create` 创建仪表盘后,默认布局可能不够工整美观,推荐使用本命令做一次整体重排 | -| **用户明确要求** | 用户主动要求对已有仪表盘进行布局重排或美化时 | - -> [!CAUTION] -> - **不建议**在已有仪表盘上自动调用此命令,除非用户明确要求 -> - 排列结果是**服务端智能推荐**,不一定完全符合用户预期 -> - 无法指定具体位置(如"第一排放 A,第二排放 B"),排列逻辑是**自适应**的 - -## 推荐命令 - -```bash -# 基础用法 -lark-cli base +dashboard-arrange \ - --base-token xxx \ - --dashboard-id blk_xxx -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID | -| `--user-id-type ` | 否 | 用户 ID 类型:open_id / union_id / user_id | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -## 返回示例 - -```json -{ - "dashboard_id": "blk_xxx", - "name": "数据分析", - "blocks": [ - { - "block_id": "chtbxxxx", - "block_name": "总销售额", - "block_type": "statistics", - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 6 - } - }, - { - "block_id": "chtbcrxxxx", - "block_name": "月度趋势", - "block_type": "column", - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 6 - } - } - ], - "arranged": true -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|------| -| `blocks[].layout` | 重排后的布局信息,包含 x/y/w/h | -| `arranged` | 是否重排成功 | - -> [!CAUTION] -> 这是**写入操作** — 执行前必须向用户确认。 - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/skills/lark-base/references/lark-base-dashboard-block-create.md b/skills/lark-base/references/lark-base-dashboard-block-create.md deleted file mode 100644 index 0e8aa1d2a..000000000 --- a/skills/lark-base/references/lark-base-dashboard-block-create.md +++ /dev/null @@ -1,108 +0,0 @@ -# base +dashboard-block-create - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 -> **关键:** 创建前必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解组件类型和 data_config 结构 - -在仪表盘中创建一个组件(Block)。 - -## 关键约束 - -- **`type` 创建后不可修改**,创建时务必选对 -- **`data_config` 结构随 `type` 变化**,不同组件类型字段不同,**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造** -- **组件创建必须串行执行**,不能并发 - -## 推荐命令 - -```bash -# 简单示例:创建一个指标卡(统计记录数) -lark-cli base +dashboard-block-create \ - --base-token xxx \ - --dashboard-id blk_xxx \ - --name "总记录数" \ - --type statistics \ - --data-config '{"table_name":"订单表","count_all":true}' - -# 文本组件示例(Markdown 富文本) -lark-cli base +dashboard-block-create \ - --base-token xxx \ - --dashboard-id blk_xxx \ - --name "说明文字" \ - --type text \ - --data-config '{"text":"# 标题\n## 副标题\n**加粗** *斜体* ~~删除~~\n1. 列表1\n2. 列表2"}' - -# 复杂配置用文件传入 -lark-cli base +dashboard-block-create \ - --base-token xxx \ - --dashboard-id blk_xxx \ - --name "销售额趋势" \ - --type line \ - --data-config @config.json -``` - -完整流程参考 [lark-base-dashboard.md](lark-base-dashboard.md) 的「场景 1:从 0 到 1 创建仪表盘」 - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID(从 `+dashboard-list/get` 获取) | -| `--name ` | **是** | 组件名称(允许重名) | -| `--type ` | **是** | 组件类型,见下方枚举值。**不同 type 对应不同的 data_config 结构**,常用:`column`(柱状图)、`line`(折线图)、`pie`(饼图)、`statistics`(指标卡)、`text`(文本) | -| `--data-config ` | 否 | 数据配置 JSON,**结构随 type 变化**。**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造**。创建时会做本地校验,更新时由后端校验 | -| `--user-id-type ` | 否 | 用户 ID 类型,filter 涉及人员字段时使用 | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -### type 枚举值 - -| 值 | 说明 | -|----|------| -| `column` | 柱状图 | -| `bar` | 条形图 | -| `line` | 折线图 | -| `pie` | 饼图 | -| `ring` | 环形图 | -| `area` | 面积图 | -| `combo` | 组合图 | -| `scatter` | 散点图 | -| `funnel` | 漏斗图 | -| `wordCloud` | 词云 | -| `radar` | 雷达图 | -| `statistics` | 指标卡 | -| `text` | 文本(支持 Markdown) | - -## 返回示例 - -```json -{ - "block": { - "block_id": "chtxxxxxxxx", - "name": "总记录数", - "type": "statistics", - "data_config": { - "table_name": "电商交易明细", - "count_all": true - } - }, - "created": true -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|------| -| `block.block_id` | 组件 ID,后续编辑/删除需要用到,务必记录 | -| `block.name` | 组件名称 | -| `block.type` | 组件类型 | -| `block.data_config` | 实际创建的数据配置(可能包含后端自动添加的默认值)| -| `created` | 是否创建成功 | - -> [!CAUTION] -> 这是**写入操作** — 执行前必须向用户确认。 - - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 -- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构、图表类型、filter 规则 diff --git a/skills/lark-base/references/lark-base-dashboard-block-delete.md b/skills/lark-base/references/lark-base-dashboard-block-delete.md deleted file mode 100644 index d98939539..000000000 --- a/skills/lark-base/references/lark-base-dashboard-block-delete.md +++ /dev/null @@ -1,46 +0,0 @@ -# base +dashboard-block-delete - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 - -删除仪表盘中的一个组件(Block),不可恢复。 - -## 推荐命令 - -```bash -lark-cli base +dashboard-block-delete \ - --base-token bascn***************CtadY \ - --dashboard-id blkxxx \ - --block-id chtxxxxxxxx -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID | -| `--block-id ` | 是 | Block ID | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -## 返回示例 - -```json -{ - "block_id": "chtxxxxxxxx", - "deleted": true -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|------| -| `block_id` | 被删除的组件 ID | -| `deleted` | 是否删除成功 | - -> [!CAUTION] -> 这是**写入操作**且**不可逆** — 执行前必须向用户确认。 - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/skills/lark-base/references/lark-base-dashboard-block-get-data.md b/skills/lark-base/references/lark-base-dashboard-block-get-data.md index 757b5dd79..8470323f8 100644 --- a/skills/lark-base/references/lark-base-dashboard-block-get-data.md +++ b/skills/lark-base/references/lark-base-dashboard-block-get-data.md @@ -13,7 +13,7 @@ > [!IMPORTANT] > - 本命令返回的是**图表结果协议**,不是 block 元数据; -> - 如果你需要 `name`、`type`、`layout`、`data_config` 等配置,请先用 [`+dashboard-block-get`](lark-base-dashboard-block-get.md); +> - 如果你需要 `name`、`type`、`layout`、`data_config` 等配置,请先用 `+dashboard-block-get`; > - 文本组件(`text`)不涉及计算,不适用本命令; ## 一句话理解 @@ -668,7 +668,7 @@ lark-cli base +dashboard-block-get-data \ - `data_config` - 所属 dashboard 信息 -这些都应该通过 [`+dashboard-block-get`](lark-base-dashboard-block-get.md) 获取。 +这些都应该通过 `+dashboard-block-get` 获取。 ### 误区 2:以为它返回的是原始记录 @@ -713,5 +713,5 @@ GET /open-apis/base/v3/bases/bascn_example_token/dashboards/blocks/chtxxxxxxxx/d ## 参考 - [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块总指引 -- [lark-base-dashboard-block-get.md](lark-base-dashboard-block-get.md) — 获取 block 元数据 +- `+dashboard-block-get` — 获取 block 元数据 - [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构和组件类型说明 diff --git a/skills/lark-base/references/lark-base-dashboard-block-get.md b/skills/lark-base/references/lark-base-dashboard-block-get.md deleted file mode 100644 index 0994f6059..000000000 --- a/skills/lark-base/references/lark-base-dashboard-block-get.md +++ /dev/null @@ -1,57 +0,0 @@ -# base +dashboard-block-get - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 - -获取仪表盘中单个组件的详情(包含 data_config 完整配置)。常用于:1) 查看组件的完整配置;2) 编辑组件前了解当前配置。 - -## 推荐命令 - -```bash -lark-cli base +dashboard-block-get \ - --base-token bascn***************CtadY \ - --dashboard-id blkxxx \ - --block-id chtxxxxxxxx -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID | -| `--block-id ` | 是 | Block ID | -| `--user-id-type ` | 否 | 用户 ID 类型:open_id / union_id / user_id | -| `--format ` | 否 | 输出格式 | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -## 返回示例 - -```json -{ - "block": { - "block_id": "chtxxxxxxxx", - "name": "柱状图", - "type": "column", - "data_config": { - "table_name": "电商交易明细", - "series": [{"field_name": "营销费用", "rollup": "SUM"}], - "group_by": [{"field_name": "品类", "mode": "integrated"}] - }, - "layout": {"x": 0, "y": 0, "w": 6, "h": 4} - } -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|-------------------------------| -| `block.block_id` | 组件 ID | -| `block.name` | 组件名称 | -| `block.type` | 组件类型(如 `column`/`line`/`pie`) | -| `block.data_config` | 数据配置(新建/编辑组件时可基于此字段修改) | -| `block.layout` | 布局信息(只读,x/y/w/h 坐标和尺寸) | - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/skills/lark-base/references/lark-base-dashboard-block-list.md b/skills/lark-base/references/lark-base-dashboard-block-list.md deleted file mode 100644 index ff8a0155a..000000000 --- a/skills/lark-base/references/lark-base-dashboard-block-list.md +++ /dev/null @@ -1,53 +0,0 @@ -# base +dashboard-block-list - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 - -分页列出仪表盘中的所有组件(Block)。常用于:1) 查看仪表盘有哪些组件;2) 获取组件 ID 和类型用于后续编辑/删除。 - -## 推荐命令 - -```bash -lark-cli base +dashboard-block-list \ - --base-token bascn***************CtadY \ - --dashboard-id blkxxx -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID | -| `--page-size ` | 否 | 每页数量,默认 20,最大 100 | -| `--page-token ` | 否 | 分页标记 | -| `--format ` | 否 | 输出格式:json / pretty / table / csv / ndjson | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -## 返回示例 - -```json -{ - "items": [ - {"block_id": "chtxxxxxxxx", "name": "图表", "type": "column"}, - {"block_id": "chtxxxxxxxx", "name": "总利润", "type": "statistics"} - ], - "total": 4, - "has_more": false -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|------| -| `items` | 组件列表,每项包含 `block_id`(ID)、`name`(名称)、`type`(类型)| -| `total` | 组件总数 | -| `has_more` | 是否有更多组件(为 `true` 时需用 `page_token` 继续获取)| - -## 坑点 - -- `+dashboard-block-list` 禁止并发调用;批量执行时只能串行。 - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 diff --git a/skills/lark-base/references/lark-base-dashboard-block-update.md b/skills/lark-base/references/lark-base-dashboard-block-update.md deleted file mode 100644 index 38cc39eda..000000000 --- a/skills/lark-base/references/lark-base-dashboard-block-update.md +++ /dev/null @@ -1,84 +0,0 @@ -# base +dashboard-block-update - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流 -> **关键:** 更新前必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解 data_config 结构和更新规则 - -更新仪表盘中组件的名称或数据配置。 - -## 关键约束 - -- **不可修改 `type` 和 `layout`** — 只能更新 `name` 和 `data_config`。 -- **`data_config` 顶层按 key merge** — 只需传入要修改的顶层字段,未传的字段保留原值;但每个字段内部是全量替换(如传新 `filter` 会完整覆盖旧 `filter`)。 -- **`series` 与 `count_all` 二选一** — 且至少提供其一。 -- **表名用 name,不是 ID** — `table_name` 对应的是表名称(如「订单表」),不是 `table_id`。 -- **`user_id_type`** 仅在 filter 涉及人员字段时有意义。 - -> [!TIP] -> CLI 默认会对 `data_config` 做轻量校验与规范化;如需兼容特殊场景,可加 `--no-validate` 跳过。 - -## 推荐命令 - -```bash -# 示例 1:更新组件名称 -lark-cli base +dashboard-block-update \ - --base-token xxx \ - --dashboard-id blk_xxx \ - --block-id chtxxxxxxxx \ - --name "新名称" - -# 示例 2:更新数据配置(只传要改的字段,未传字段保留原值) -lark-cli base +dashboard-block-update \ - --base-token xxx \ - --dashboard-id blk_xxx \ - --block-id chtxxxxxxxx \ - --data-config '{"filter":{"conjunction":"and","conditions":[{"field_name":"状态","operator":"is","value":"已完成"}]}}' -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--dashboard-id ` | 是 | 仪表盘 ID | -| `--block-id ` | 是 | Block ID | -| `--name ` | 否 | 新名称 | -| `--data-config ` | 否 | 数据配置 JSON。**结构随 block 的 `type` 变化**。**⚠️ 必须阅读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解如何构造** | -| `--user-id-type ` | 否 | 用户 ID 类型,filter 涉及人员字段时使用 | -| `--no-validate` | 否 | 跳过 data_config 本地校验(用于兼容特殊场景) | -| `--dry-run` | 否 | 预览 API 调用,不执行 | - -## 返回示例 - -```json -{ - "block": { - "block_id": "chtxxxxxxxx", - "name": "新名称", - "type": "column", - "data_config": { - "table_name": "订单表", - "series": [{"field_name": "金额", "rollup": "SUM"}], - "group_by": [{"field_name": "类别", "mode": "integrated"}] - } - }, - "updated": true -} -``` - -## 返回重点 - -| 字段 | 说明 | -|------|------| -| `block.block_id` | 组件 ID | -| `block.name` | 更新后的名称 | -| `block.type` | 组件类型(不可修改)| -| `block.data_config` | 更新后的数据配置 | -| `updated` | 是否更新成功 | - -> [!CAUTION] -> 这是**写入操作** — 执行前必须向用户确认。 - -## 参考 - -- [lark-base-dashboard.md](lark-base-dashboard.md) — dashboard 模块指引 -- [dashboard-block-data-config.md](dashboard-block-data-config.md) — data_config 结构、图表类型、filter 规则 diff --git a/skills/lark-base/references/lark-base-dashboard-create.md b/skills/lark-base/references/lark-base-dashboard-create.md deleted file mode 100644 index 6215b4aac..000000000 --- a/skills/lark-base/references/lark-base-dashboard-create.md +++ /dev/null @@ -1,73 +0,0 @@ -# base +dashboard-create - -> **前置条件:** 先阅读 [lark-base-dashboard.md](lark-base-dashboard.md) 了解整体工作流。 - -创建空白仪表盘。创建成功后务必记录返回的 `dashboard_id`,后续添加组件和管理仪表盘都需要用到。 - -## 关键约束 - -- **dashboard_id** 在 create 返回中取得,后续 get/update/delete 使用。 - -## 推荐命令 - -```bash -# 创建仪表盘 -lark-cli base +dashboard-create \ - --base-token VwGhb**************fMnod \ - --name "销售报表" - -# 创建仪表盘(指定主题) -lark-cli base +dashboard-create \ - --base-token VwGhb**************fMnod \ - --name "销售报表" \ - --theme-style default -``` - -## 参数 - -| 参数 | 必填 | 说明 | -|------|------|------| -| `--base-token ` | 是 | Base Token | -| `--name ` | 是 | 仪表盘名称 | -| `--theme-style