mirror of
https://github.com/larksuite/cli.git
synced 2026-07-09 18:34:03 +08:00
Compare commits
1 Commits
main
...
sun/docs-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba497de3ba |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -2,29 +2,6 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [v1.0.67] - 2026-07-08
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- **mail**: add message modify and trash shortcuts (#1567)
|
|
||||||
- support whiteboard file inputs in docs XML (#1784)
|
|
||||||
- **vc**: refine meeting-events output and reaction forwarding (#1674)
|
|
||||||
- **affordance**: usage guidance for shortcuts and per-command skills (#1793)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- accept opaque wiki node tokens (#1789)
|
|
||||||
- **apps**: make db --environment optional, auto-select branch server-side (#1735)
|
|
||||||
- preserve original filename in multipart file upload (#1767)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- restore one-time authorization guidance in lark-apps skill (#1794)
|
|
||||||
|
|
||||||
### Misc
|
|
||||||
|
|
||||||
- e2e: harden CLI E2E retry, cleanup, and domain selection (#1709)
|
|
||||||
|
|
||||||
## [v1.0.66] - 2026-07-07
|
## [v1.0.66] - 2026-07-07
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -1421,7 +1398,6 @@ Bundled AI agent skills for intelligent assistance:
|
|||||||
- Bilingual documentation (English & Chinese).
|
- Bilingual documentation (English & Chinese).
|
||||||
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
|
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
|
||||||
|
|
||||||
[v1.0.67]: https://github.com/larksuite/cli/releases/tag/v1.0.67
|
|
||||||
[v1.0.66]: https://github.com/larksuite/cli/releases/tag/v1.0.66
|
[v1.0.66]: https://github.com/larksuite/cli/releases/tag/v1.0.66
|
||||||
[v1.0.65]: https://github.com/larksuite/cli/releases/tag/v1.0.65
|
[v1.0.65]: https://github.com/larksuite/cli/releases/tag/v1.0.65
|
||||||
[v1.0.64]: https://github.com/larksuite/cli/releases/tag/v1.0.64
|
[v1.0.64]: https://github.com/larksuite/cli/releases/tag/v1.0.64
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ func TestBuildAPIError_ExitCodeMatrix(t *testing.T) {
|
|||||||
{"230027 user_not_authorized", 230027, errs.CategoryAuthorization, errs.SubtypeUserUnauthorized, 3, "PermissionError"},
|
{"230027 user_not_authorized", 230027, errs.CategoryAuthorization, errs.SubtypeUserUnauthorized, 3, "PermissionError"},
|
||||||
{"1470403 task_permission_denied", 1470403, errs.CategoryAuthorization, errs.SubtypePermissionDenied, 3, "PermissionError"},
|
{"1470403 task_permission_denied", 1470403, errs.CategoryAuthorization, errs.SubtypePermissionDenied, 3, "PermissionError"},
|
||||||
{"1470400 task_invalid_params", 1470400, errs.CategoryAPI, errs.SubtypeInvalidParameters, 1, "APIError"},
|
{"1470400 task_invalid_params", 1470400, errs.CategoryAPI, errs.SubtypeInvalidParameters, 1, "APIError"},
|
||||||
{"1062507 drive_parent_sibling_limit", 1062507, errs.CategoryAPI, errs.SubtypeQuotaExceeded, 1, "APIError"},
|
|
||||||
{"99991400 rate_limit", 99991400, errs.CategoryAPI, errs.SubtypeRateLimit, 1, "APIError"},
|
{"99991400 rate_limit", 99991400, errs.CategoryAPI, errs.SubtypeRateLimit, 1, "APIError"},
|
||||||
{"99991661 token_missing", 99991661, errs.CategoryAuthentication, errs.SubtypeTokenMissing, 3, "AuthenticationError"},
|
{"99991661 token_missing", 99991661, errs.CategoryAuthentication, errs.SubtypeTokenMissing, 3, "AuthenticationError"},
|
||||||
{"21000 challenge_required", 21000, errs.CategoryPolicy, errs.Subtype("challenge_required"), 6, "SecurityPolicyError"},
|
{"21000 challenge_required", 21000, errs.CategoryPolicy, errs.Subtype("challenge_required"), 6, "SecurityPolicyError"},
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ var driveCodeMeta = map[int]CodeMeta{
|
|||||||
1061043: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file size beyond limit
|
1061043: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file size beyond limit
|
||||||
1061044: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // parent folder does not exist (upload)
|
1061044: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // parent folder does not exist (upload)
|
||||||
1061101: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file quota exceeded
|
1061101: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file quota exceeded
|
||||||
1062507: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // parent folder child count limit exceeded
|
|
||||||
1062009: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // actual size inconsistent with declared size
|
1062009: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // actual size inconsistent with declared size
|
||||||
1063001: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // secure label invalid parameter
|
1063001: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // secure label invalid parameter
|
||||||
1063002: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypePermissionDenied}, // secure label permission denied
|
1063002: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypePermissionDenied}, // secure label permission denied
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@larksuite/cli",
|
"name": "@larksuite/cli",
|
||||||
"version": "1.0.67",
|
"version": "1.0.66",
|
||||||
"description": "The official CLI for Lark/Feishu open platform",
|
"description": "The official CLI for Lark/Feishu open platform",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lark-cli": "scripts/run.js"
|
"lark-cli": "scripts/run.js"
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/larksuite/cli/internal/cmdutil"
|
|
||||||
"github.com/larksuite/cli/shortcuts/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mountBaseShortcutFlags(t *testing.T, s common.Shortcut, name string) *cobra.Command {
|
|
||||||
t.Helper()
|
|
||||||
parent := &cobra.Command{Use: "test"}
|
|
||||||
s.Mount(parent, &cmdutil.Factory{})
|
|
||||||
cmd, _, err := parent.Find([]string{name})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Find(%s) error = %v", name, err)
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// record-list 获得 --json 简写
|
|
||||||
func TestRecordListRegistersJSONShorthand(t *testing.T) {
|
|
||||||
cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list")
|
|
||||||
fl := cmd.Flags().Lookup("json")
|
|
||||||
if fl == nil {
|
|
||||||
t.Fatal("+record-list missing --json shorthand")
|
|
||||||
}
|
|
||||||
if fl.Usage != "shorthand for --format json" {
|
|
||||||
t.Errorf("usage = %q, want shorthand", fl.Usage)
|
|
||||||
}
|
|
||||||
if def := cmd.Flags().Lookup("format").DefValue; def != "markdown" {
|
|
||||||
t.Errorf("format default = %q, want markdown (unchanged)", def)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// record-search / record-get 的 --json 保持请求体语义,不被覆盖(回归锚点)
|
|
||||||
func TestRecordSearchGetKeepRequestBodyJSON(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
shortcut common.Shortcut
|
|
||||||
cmdName string
|
|
||||||
}{
|
|
||||||
{"record-search", BaseRecordSearch, "+record-search"},
|
|
||||||
{"record-get", BaseRecordGet, "+record-get"},
|
|
||||||
} {
|
|
||||||
cmd := mountBaseShortcutFlags(t, tc.shortcut, tc.cmdName)
|
|
||||||
fl := cmd.Flags().Lookup("json")
|
|
||||||
if fl == nil {
|
|
||||||
t.Fatalf("%s: --json (request body) missing", tc.name)
|
|
||||||
}
|
|
||||||
if strings.Contains(fl.Usage, "shorthand") {
|
|
||||||
t.Fatalf("%s: request-body --json overwritten by shorthand: %q", tc.name, fl.Usage)
|
|
||||||
}
|
|
||||||
if fl.Value.Type() != "string" {
|
|
||||||
t.Fatalf("%s: --json type = %q, want string", tc.name, fl.Value.Type())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum 已接入:help 描述携带枚举后缀(框架对带 Enum 的 flag 自动追加 " (markdown|json)")
|
|
||||||
func TestRecordReadFormatFlagCarriesEnum(t *testing.T) {
|
|
||||||
cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list")
|
|
||||||
usage := cmd.Flags().Lookup("format").Usage
|
|
||||||
if !strings.Contains(usage, "(markdown|json)") {
|
|
||||||
t.Fatalf("format usage missing enum suffix: %q", usage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,6 @@ func recordReadFormatFlag() common.Flag {
|
|||||||
return common.Flag{
|
return common.Flag{
|
||||||
Name: "format",
|
Name: "format",
|
||||||
Default: "markdown",
|
Default: "markdown",
|
||||||
Enum: []string{"markdown", "json"},
|
|
||||||
Desc: "output format: markdown (default) | json",
|
Desc: "output format: markdown (default) | json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1027,7 +1027,6 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf
|
|||||||
}
|
}
|
||||||
rctx.larkSDK = sdk
|
rctx.larkSDK = sdk
|
||||||
|
|
||||||
applyJSONShorthand(cmd, s)
|
|
||||||
rctx.Format = rctx.Str("format")
|
rctx.Format = rctx.Str("format")
|
||||||
rctx.JqExpr, _ = cmd.Flags().GetString("jq")
|
rctx.JqExpr, _ = cmd.Flags().GetString("jq")
|
||||||
return rctx, nil
|
return rctx, nil
|
||||||
@@ -1173,75 +1172,6 @@ func registerShortcutFlags(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut)
|
|||||||
registerShortcutFlagsWithContext(context.Background(), cmd, f, s)
|
registerShortcutFlagsWithContext(context.Background(), cmd, f, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcutDeclaresJSONFlag reports whether the shortcut itself declares a flag
|
|
||||||
// named "json" in its Flags list (custom semantics, e.g. event +subscribe's
|
|
||||||
// pretty-print switch or base +record-search's request-body payload).
|
|
||||||
// Framework-injected flags never appear in s.Flags, so this cleanly separates
|
|
||||||
// "self-declared json" from "injected shorthand".
|
|
||||||
func shortcutDeclaresJSONFlag(s *Shortcut) bool {
|
|
||||||
for _, fl := range s.Flags {
|
|
||||||
if fl.Name == "json" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// shortcutFormatSupportsJSON reports whether the command's format flag accepts
|
|
||||||
// "json": a self-declared format supports it only when its Enum lists "json";
|
|
||||||
// a framework-injected default format (no format entry in s.Flags) always does.
|
|
||||||
func shortcutFormatSupportsJSON(s *Shortcut) bool {
|
|
||||||
for _, fl := range s.Flags {
|
|
||||||
if fl.Name == "format" {
|
|
||||||
return slices.Contains(fl.Enum, "json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true // framework-injected: json (default) | pretty | table | ndjson | csv
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureJSONShorthand registers --json as a shorthand for --format json when:
|
|
||||||
// 1. the command has a format flag (self-declared or framework-injected), AND
|
|
||||||
// 2. that format supports "json" (see shortcutFormatSupportsJSON), AND
|
|
||||||
// 3. no flag named "json" is registered yet — pflag panics on duplicate
|
|
||||||
// registration, and commands that declare their own --json (event
|
|
||||||
// +subscribe, base +record-search/-get) keep their custom semantics.
|
|
||||||
func ensureJSONShorthand(cmd *cobra.Command, s *Shortcut) {
|
|
||||||
// A shortcut that declares its own "json" flag defines custom semantics
|
|
||||||
// (e.g. pretty-print switch, request-body payload) — never a shorthand.
|
|
||||||
if shortcutDeclaresJSONFlag(s) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cmd.Flags().Lookup("format") == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !shortcutFormatSupportsJSON(s) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Safety net: pflag panics on duplicate registration.
|
|
||||||
if cmd.Flags().Lookup("json") != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd.Flags().Bool("json", false, "shorthand for --format json")
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyJSONShorthand folds the injected --json shorthand into the format flag
|
|
||||||
// itself, before rctx.Format caches it — so both the cached value (OutFormat,
|
|
||||||
// ValidateJqFlags, dry-run) and later runtime.Str("format") reads observe
|
|
||||||
// "json". An explicitly passed --format always wins over the shorthand (the
|
|
||||||
// shorthand only fills in when the user did not choose a format). Shortcuts
|
|
||||||
// that declare their own "json" flag keep its custom semantics untouched.
|
|
||||||
func applyJSONShorthand(cmd *cobra.Command, s *Shortcut) {
|
|
||||||
if shortcutDeclaresJSONFlag(s) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if cmd.Flags().Lookup("json") == nil || cmd.Flags().Changed("format") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if set, _ := cmd.Flags().GetBool("json"); set {
|
|
||||||
_ = cmd.Flags().Set("format", "json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) {
|
func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) {
|
||||||
for _, fl := range s.Flags {
|
for _, fl := range s.Flags {
|
||||||
desc := fl.Desc
|
desc := fl.Desc
|
||||||
@@ -1305,8 +1235,10 @@ func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f
|
|||||||
cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
})
|
||||||
|
if cmd.Flags().Lookup("json") == nil {
|
||||||
|
cmd.Flags().Bool("json", false, "shorthand for --format json")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ensureJSONShorthand(cmd, s)
|
|
||||||
if s.Risk == "high-risk-write" {
|
if s.Risk == "high-risk-write" {
|
||||||
cmd.Flags().Bool("yes", false, "confirm high-risk operation")
|
cmd.Flags().Bool("yes", false, "confirm high-risk operation")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/larksuite/cli/internal/cmdutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const jsonShorthandUsage = "shorthand for --format json"
|
|
||||||
|
|
||||||
func mountTestShortcut(t *testing.T, s Shortcut) *cobra.Command {
|
|
||||||
t.Helper()
|
|
||||||
f, _, _, _ := cmdutil.TestFactory(t, nil)
|
|
||||||
parent := &cobra.Command{Use: "root"}
|
|
||||||
s.Mount(parent, f)
|
|
||||||
cmd, _, err := parent.Find([]string{s.Command})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Find() error = %v", err)
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义 format 且 Enum 含 json → 注册简写(本次修复的核心行为)
|
|
||||||
func TestJSONShorthand_CustomFormatWithJSONEnum_Registered(t *testing.T) {
|
|
||||||
cmd := mountTestShortcut(t, Shortcut{
|
|
||||||
Service: "mail", Command: "+fake-triage", Description: "x",
|
|
||||||
Flags: []Flag{{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "fmt"}},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
})
|
|
||||||
fl := cmd.Flags().Lookup("json")
|
|
||||||
if fl == nil {
|
|
||||||
t.Fatal("--json not registered for custom-format shortcut whose Enum contains json")
|
|
||||||
}
|
|
||||||
if fl.Usage != jsonShorthandUsage {
|
|
||||||
t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage)
|
|
||||||
}
|
|
||||||
// 默认输出格式不被改变
|
|
||||||
if def := cmd.Flags().Lookup("format").DefValue; def != "table" {
|
|
||||||
t.Errorf("format default = %q, want table", def)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义 format 但 Enum 不含 json → 不注册
|
|
||||||
func TestJSONShorthand_CustomFormatWithoutJSONEnum_NotRegistered(t *testing.T) {
|
|
||||||
cmd := mountTestShortcut(t, Shortcut{
|
|
||||||
Service: "x", Command: "+no-json", Description: "x",
|
|
||||||
Flags: []Flag{{Name: "format", Default: "csv", Enum: []string{"csv", "table"}, Desc: "fmt"}},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
})
|
|
||||||
if cmd.Flags().Lookup("json") != nil {
|
|
||||||
t.Fatal("--json must NOT be registered when format Enum lacks json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义 format 但无 Enum(现状 triage 形态)→ 不注册(Enum 是判定依据)
|
|
||||||
func TestJSONShorthand_CustomFormatNoEnum_NotRegistered(t *testing.T) {
|
|
||||||
cmd := mountTestShortcut(t, Shortcut{
|
|
||||||
Service: "x", Command: "+legacy", Description: "x",
|
|
||||||
Flags: []Flag{{Name: "format", Default: "table", Desc: "fmt"}},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
})
|
|
||||||
if cmd.Flags().Lookup("json") != nil {
|
|
||||||
t.Fatal("--json must NOT be registered when format has no Enum metadata")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自声明 json flag(subscribe 的 pretty / record-search 的请求体)→ 不覆盖、不 panic、语义保留
|
|
||||||
func TestJSONShorthand_SelfDeclaredJSON_Preserved(t *testing.T) {
|
|
||||||
cmd := mountTestShortcut(t, Shortcut{
|
|
||||||
Service: "event", Command: "+fake-subscribe", Description: "x",
|
|
||||||
Flags: []Flag{
|
|
||||||
{Name: "json", Type: "bool", Desc: "pretty-print JSON instead of NDJSON"},
|
|
||||||
},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
})
|
|
||||||
fl := cmd.Flags().Lookup("json")
|
|
||||||
if fl == nil {
|
|
||||||
t.Fatal("self-declared --json missing")
|
|
||||||
}
|
|
||||||
if fl.Usage != "pretty-print JSON instead of NDJSON" {
|
|
||||||
t.Errorf("self-declared --json usage overwritten: %q", fl.Usage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseMounted mounts the shortcut and parses args against the command's FlagSet
|
|
||||||
// (registration side effects included), without executing RunE.
|
|
||||||
func parseMounted(t *testing.T, s Shortcut, args []string) *cobra.Command {
|
|
||||||
t.Helper()
|
|
||||||
cmd := mountTestShortcut(t, s)
|
|
||||||
if err := cmd.ParseFlags(args); err != nil {
|
|
||||||
t.Fatalf("ParseFlags(%v) error = %v", args, err)
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func customFormatShortcut() Shortcut {
|
|
||||||
return Shortcut{
|
|
||||||
Service: "mail", Command: "+fake-triage", Description: "x",
|
|
||||||
Flags: []Flag{{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "fmt"}},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --json 单独使用 → format 归一化为 json
|
|
||||||
func TestApplyJSONShorthand_JSONAlone_SetsFormatJSON(t *testing.T) {
|
|
||||||
s := customFormatShortcut()
|
|
||||||
cmd := parseMounted(t, s, []string{"--json"})
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
if got := cmd.Flags().Lookup("format").Value.String(); got != "json" {
|
|
||||||
t.Fatalf("format = %q, want json", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显式 --format 优先于 --json 简写:--format table --json → table
|
|
||||||
func TestApplyJSONShorthand_ExplicitFormatWins(t *testing.T) {
|
|
||||||
s := customFormatShortcut()
|
|
||||||
cmd := parseMounted(t, s, []string{"--format", "table", "--json"})
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
if got := cmd.Flags().Lookup("format").Value.String(); got != "table" {
|
|
||||||
t.Fatalf("format = %q, want table (explicit --format must win)", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --format json --json → json(一致,无冲突)
|
|
||||||
func TestApplyJSONShorthand_ExplicitJSONFormatConsistent(t *testing.T) {
|
|
||||||
s := customFormatShortcut()
|
|
||||||
cmd := parseMounted(t, s, []string{"--format", "json", "--json"})
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
if got := cmd.Flags().Lookup("format").Value.String(); got != "json" {
|
|
||||||
t.Fatalf("format = %q, want json", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 均不传 → 默认值不变
|
|
||||||
func TestApplyJSONShorthand_NoFlags_DefaultUntouched(t *testing.T) {
|
|
||||||
s := customFormatShortcut()
|
|
||||||
cmd := parseMounted(t, s, nil)
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
if got := cmd.Flags().Lookup("format").Value.String(); got != "table" {
|
|
||||||
t.Fatalf("format = %q, want table (default untouched)", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自声明 string 型 --json(record-search 形态:format+json 双声明)→ 归一化跳过
|
|
||||||
func TestApplyJSONShorthand_SelfDeclaredStringJSON_Skipped(t *testing.T) {
|
|
||||||
s := Shortcut{
|
|
||||||
Service: "base", Command: "+fake-record-search", Description: "x",
|
|
||||||
Flags: []Flag{
|
|
||||||
{Name: "format", Default: "markdown", Enum: []string{"markdown", "json"}, Desc: "fmt"},
|
|
||||||
{Name: "json", Desc: "request body JSON object"},
|
|
||||||
},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
}
|
|
||||||
cmd := parseMounted(t, s, []string{"--json", `{"keyword":"Alice"}`})
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
if got := cmd.Flags().Lookup("format").Value.String(); got != "markdown" {
|
|
||||||
t.Fatalf("format = %q, want markdown (self-declared json must not normalize)", got)
|
|
||||||
}
|
|
||||||
if got := cmd.Flags().Lookup("json").Value.String(); got != `{"keyword":"Alice"}` {
|
|
||||||
t.Fatalf("request-body --json corrupted: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自声明 bool 型 --json(subscribe 形态:无自定义 format,框架注入 format)→ 归一化跳过
|
|
||||||
func TestApplyJSONShorthand_SelfDeclaredBoolJSON_Skipped(t *testing.T) {
|
|
||||||
s := Shortcut{
|
|
||||||
Service: "event", Command: "+fake-subscribe", Description: "x",
|
|
||||||
Flags: []Flag{
|
|
||||||
{Name: "json", Type: "bool", Desc: "pretty-print JSON instead of NDJSON"},
|
|
||||||
},
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
}
|
|
||||||
cmd := parseMounted(t, s, []string{"--json"})
|
|
||||||
applyJSONShorthand(cmd, &s)
|
|
||||||
// 注入的 format 默认即 json;这里断言的是 Changed 状态未被归一化污染
|
|
||||||
if cmd.Flags().Changed("format") {
|
|
||||||
t.Fatal("normalization must not touch format for shortcuts declaring their own --json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无自定义 format(普通命令)→ 注入默认 format + 简写(现状回归)
|
|
||||||
func TestJSONShorthand_DefaultInjectedFormat_StillRegistered(t *testing.T) {
|
|
||||||
cmd := mountTestShortcut(t, Shortcut{
|
|
||||||
Service: "im", Command: "+plain", Description: "x",
|
|
||||||
Execute: func(context.Context, *RuntimeContext) error { return nil },
|
|
||||||
})
|
|
||||||
fl := cmd.Flags().Lookup("json")
|
|
||||||
if fl == nil {
|
|
||||||
t.Fatal("--json missing on default-format shortcut (regression)")
|
|
||||||
}
|
|
||||||
if fl.Usage != jsonShorthandUsage {
|
|
||||||
t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -623,10 +623,6 @@ func driveClassifyBatchFailure(err error) driveBatchFailureDecision {
|
|||||||
case problem.Subtype == errs.SubtypeRateLimit || problem.Code == 99991400:
|
case problem.Subtype == errs.SubtypeRateLimit || problem.Code == 99991400:
|
||||||
decision.Class = "rate_limited"
|
decision.Class = "rate_limited"
|
||||||
decision.Terminal = true
|
decision.Terminal = true
|
||||||
case problem.Code == 1062507:
|
|
||||||
decision.Class = "parent_sibling_limit"
|
|
||||||
decision.Terminal = true
|
|
||||||
decision.Hint = "The destination parent folder has reached its child-count limit. Clean up that folder, choose another --folder-token, or split the upload across subfolders before retrying."
|
|
||||||
case problem.Subtype == errs.SubtypeQuotaExceeded || problem.Code == 1061043:
|
case problem.Subtype == errs.SubtypeQuotaExceeded || problem.Code == 1061043:
|
||||||
decision.Class = "file_size_limit"
|
decision.Class = "file_size_limit"
|
||||||
case problem.Code == 1062009:
|
case problem.Code == 1062009:
|
||||||
|
|||||||
@@ -1334,75 +1334,6 @@ func TestDrivePushAbortsAfterCreateFolderMissingScope(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDrivePushAbortsAfterCreateFolderParentSiblingLimit(t *testing.T) {
|
|
||||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
withDriveWorkingDir(t, tmpDir)
|
|
||||||
if err := os.MkdirAll(filepath.Join("local", "a"), 0o755); err != nil {
|
|
||||||
t.Fatalf("MkdirAll a: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Join("local", "b"), 0o755); err != nil {
|
|
||||||
t.Fatalf("MkdirAll b: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reg.Register(&httpmock.Stub{
|
|
||||||
Method: "GET",
|
|
||||||
URL: "folder_token=folder_root",
|
|
||||||
Body: map[string]interface{}{
|
|
||||||
"code": 0, "msg": "ok",
|
|
||||||
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
reg.Register(&httpmock.Stub{
|
|
||||||
Method: "POST",
|
|
||||||
URL: "/open-apis/drive/v1/files/create_folder",
|
|
||||||
Body: map[string]interface{}{
|
|
||||||
"code": 1062507,
|
|
||||||
"msg": "parent node out of sibling num.",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
err := mountAndRunDrive(t, DrivePush, []string{
|
|
||||||
"+push",
|
|
||||||
"--local-dir", "local",
|
|
||||||
"--folder-token", "folder_root",
|
|
||||||
"--as", "bot",
|
|
||||||
}, f, stdout)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected partial failure, got nil\nstdout: %s", stdout.String())
|
|
||||||
}
|
|
||||||
var pfErr *output.PartialFailureError
|
|
||||||
if !errors.As(err, &pfErr) {
|
|
||||||
t.Fatalf("expected *output.PartialFailureError, got %T: %v", err, err)
|
|
||||||
}
|
|
||||||
summary, items := splitDrivePushStdout(t, stdout.Bytes())
|
|
||||||
if got := summary["failed"]; got != float64(1) {
|
|
||||||
t.Fatalf("summary.failed = %v, want 1", got)
|
|
||||||
}
|
|
||||||
if got := summary["aborted"]; got != true {
|
|
||||||
t.Fatalf("summary.aborted = %v, want true", got)
|
|
||||||
}
|
|
||||||
if len(items) != 1 {
|
|
||||||
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
|
|
||||||
}
|
|
||||||
item := items[0]
|
|
||||||
if item["rel_path"] != "a" || item["phase"] != "create_folder" || item["error_class"] != "parent_sibling_limit" {
|
|
||||||
t.Fatalf("unexpected failed item: %#v", item)
|
|
||||||
}
|
|
||||||
if item["code"] != float64(1062507) || item["subtype"] != "quota_exceeded" || item["retryable"] != false {
|
|
||||||
t.Fatalf("unexpected failure metadata: %#v", item)
|
|
||||||
}
|
|
||||||
if got, _ := item["hint"].(string); !strings.Contains(got, "--folder-token") || !strings.Contains(got, "child-count limit") {
|
|
||||||
t.Fatalf("hint should explain the destination folder child-count limit, got item=%#v", item)
|
|
||||||
}
|
|
||||||
for _, item := range items {
|
|
||||||
if item["rel_path"] == "b" {
|
|
||||||
t.Fatalf("parent sibling limit must abort before b, got items=%#v", items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrivePushDetectsLocalFileChangedBeforeUpload(t *testing.T) {
|
func TestDrivePushDetectsLocalFileChangedBeforeUpload(t *testing.T) {
|
||||||
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/larksuite/cli/errs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// help 必须列出 --json 简写
|
|
||||||
func TestMailTriageHelpListsJSONShorthand(t *testing.T) {
|
|
||||||
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
||||||
if err := runMountedMailShortcutWithCobraOutput(t, MailTriage, []string{"+triage", "-h"}, f, stdout); err != nil {
|
|
||||||
t.Fatalf("help returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout.String(), "shorthand for --format json") {
|
|
||||||
t.Fatalf("triage help missing --json shorthand\n%s", stdout.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMailWatchHelpListsJSONShorthand(t *testing.T) {
|
|
||||||
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
||||||
if err := runMountedMailShortcutWithCobraOutput(t, MailWatch, []string{"+watch", "-h"}, f, stdout); err != nil {
|
|
||||||
t.Fatalf("help returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout.String(), "shorthand for --format json") {
|
|
||||||
t.Fatalf("watch help missing --json shorthand\n%s", stdout.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 行为验证:--json 走 JSON 输出路径,不输出 table read hint
|
|
||||||
func TestMailTriageJSONShorthandDoesNotEmitReadHint(t *testing.T) {
|
|
||||||
f, stdout, stderr, reg := mailShortcutTestFactory(t)
|
|
||||||
registerTriageReadHintStubs(reg)
|
|
||||||
|
|
||||||
err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--json", "--max", "1"}, f, stdout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("triage --json returned error: %v", err)
|
|
||||||
}
|
|
||||||
reg.Verify(t)
|
|
||||||
if strings.Contains(stderr.String(), "tip: read full content:") {
|
|
||||||
t.Fatalf("--json must follow the JSON path, got table hint\nstderr=%s", stderr.String())
|
|
||||||
}
|
|
||||||
if !strings.Contains(stdout.String(), `"messages"`) {
|
|
||||||
t.Fatalf("--json stdout missing JSON payload\n%s", stdout.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等价性验证:--json 与 --format json 的 dry-run 输出一致
|
|
||||||
func TestMailTriageJSONShorthandDryRunEquivalence(t *testing.T) {
|
|
||||||
f1, stdout1, _, _ := mailShortcutTestFactory(t)
|
|
||||||
if err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--json", "--max", "1", "--dry-run"}, f1, stdout1); err != nil {
|
|
||||||
t.Fatalf("--json --dry-run error: %v", err)
|
|
||||||
}
|
|
||||||
f2, stdout2, _, _ := mailShortcutTestFactory(t)
|
|
||||||
if err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "json", "--max", "1", "--dry-run"}, f2, stdout2); err != nil {
|
|
||||||
t.Fatalf("--format json --dry-run error: %v", err)
|
|
||||||
}
|
|
||||||
if stdout1.String() != stdout2.String() {
|
|
||||||
t.Fatalf("dry-run outputs differ:\n--json:\n%s\n--format json:\n%s", stdout1.String(), stdout2.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先级验证:显式 --format table 优先,--json 让位 → 仍走 table 路径
|
|
||||||
func TestMailTriageExplicitTableWinsOverJSONShorthand(t *testing.T) {
|
|
||||||
f, stdout, stderr, reg := mailShortcutTestFactory(t)
|
|
||||||
registerTriageReadHintStubs(reg)
|
|
||||||
|
|
||||||
err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "table", "--json", "--max", "1"}, f, stdout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("triage returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(stderr.String(), "tip: read full content:") {
|
|
||||||
t.Fatalf("explicit --format table must win over --json (expected table hint)\nstderr=%s", stderr.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误验证:Enum 硬校验
|
|
||||||
func TestMailTriageEnumRejectsUnknownFormat(t *testing.T) {
|
|
||||||
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
||||||
err := runMountedMailShortcut(t, MailTriage, []string{"+triage", "--format", "bogus", "--max", "1", "--dry-run"}, f, stdout)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected validation error for --format bogus")
|
|
||||||
}
|
|
||||||
problem, ok := errs.ProblemOf(err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("error = %T, want typed errs problem carrier", err)
|
|
||||||
}
|
|
||||||
if problem.Category != errs.CategoryValidation {
|
|
||||||
t.Fatalf("category = %q, want %q", problem.Category, errs.CategoryValidation)
|
|
||||||
}
|
|
||||||
if problem.Subtype != errs.SubtypeInvalidArgument {
|
|
||||||
t.Fatalf("subtype = %q, want %q", problem.Subtype, errs.SubtypeInvalidArgument)
|
|
||||||
}
|
|
||||||
var ve *errs.ValidationError
|
|
||||||
if !errors.As(err, &ve) {
|
|
||||||
t.Fatalf("error = %T, want *errs.ValidationError", err)
|
|
||||||
}
|
|
||||||
if ve.Param != "--format" {
|
|
||||||
t.Fatalf("param = %q, want --format", ve.Param)
|
|
||||||
}
|
|
||||||
if !strings.Contains(problem.Message, `invalid value "bogus" for --format`) {
|
|
||||||
t.Fatalf("message = %q, want enum validation message", problem.Message)
|
|
||||||
}
|
|
||||||
if !strings.Contains(problem.Message, "table, json, data") {
|
|
||||||
t.Fatalf("message = %q, want allowed values list", problem.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ var MailTriage = common.Shortcut{
|
|||||||
Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"},
|
Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"},
|
||||||
AuthTypes: []string{"user", "bot"},
|
AuthTypes: []string{"user", "bot"},
|
||||||
Flags: []common.Flag{
|
Flags: []common.Flag{
|
||||||
{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "output format: table | json | data (json/data output object with pagination fields)"},
|
{Name: "format", Default: "table", Desc: "output format: table | json | data (json/data output object with pagination fields)"},
|
||||||
{Name: "max", Type: "int", Default: "20", Desc: "maximum number of messages to fetch (1-400; auto-paginates internally)"},
|
{Name: "max", Type: "int", Default: "20", Desc: "maximum number of messages to fetch (1-400; auto-paginates internally)"},
|
||||||
{Name: "page-size", Type: "int", Desc: "alias for --max"},
|
{Name: "page-size", Type: "int", Desc: "alias for --max"},
|
||||||
{Name: "page-token", Desc: "pagination token from a previous response to fetch the next page"},
|
{Name: "page-token", Desc: "pagination token from a previous response to fetch the next page"},
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ var MailWatch = common.Shortcut{
|
|||||||
Scopes: []string{"mail:event", "mail:user_mailbox.event.mail_address:read", "mail:user_mailbox:readonly", "mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"},
|
Scopes: []string{"mail:event", "mail:user_mailbox.event.mail_address:read", "mail:user_mailbox:readonly", "mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"},
|
||||||
AuthTypes: []string{"user"},
|
AuthTypes: []string{"user"},
|
||||||
Flags: []common.Flag{
|
Flags: []common.Flag{
|
||||||
{Name: "format", Default: "data", Enum: []string{"json", "data"}, Desc: "json: NDJSON stream with ok/data envelope; data: bare NDJSON stream"},
|
{Name: "format", Default: "data", Desc: "json: NDJSON stream with ok/data envelope; data: bare NDJSON stream"},
|
||||||
{Name: "msg-format", Default: "metadata", Desc: "message payload mode: metadata(headers + meta, for triage/notification) | minimal(IDs and state only, no headers, for tracking read/folder changes) | plain_text_full(all metadata fields + full plain-text body) | event(raw WebSocket event, no API call, for debug) | full(full message including HTML body and attachments)"},
|
{Name: "msg-format", Default: "metadata", Desc: "message payload mode: metadata(headers + meta, for triage/notification) | minimal(IDs and state only, no headers, for tracking read/folder changes) | plain_text_full(all metadata fields + full plain-text body) | event(raw WebSocket event, no API call, for debug) | full(full message including HTML body and attachments)"},
|
||||||
{Name: "output-dir", Desc: "Write each message as a JSON file (always full payload, regardless of --msg-format)"},
|
{Name: "output-dir", Desc: "Write each message as a JSON file (always full payload, regardless of --msg-format)"},
|
||||||
{Name: "mailbox", Default: "me", Desc: "email address (default: me)"},
|
{Name: "mailbox", Default: "me", Desc: "email address (default: me)"},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: lark-doc
|
name: lark-doc
|
||||||
version: 2.0.0
|
|
||||||
description: "飞书云文档(Docx / Wiki 文档):读取和编辑飞书文档内容。当用户给出文档 URL 或 token,或需要查看、创建、编辑文档、插入或下载文档图片附件时使用。文档中嵌入的电子表格、多维表格、画板,先用本 skill 提取 token 再切到对应 skill。当用户给出 doubao.com 的 /docx/ 或 /wiki/ URL/token 时,也应直接使用本 skill;路由依据是 URL 路径模式和 token,而不是域名。不负责文档评论管理,也不负责表格或 Base 的数据操作。当用户明确要操作飞书思维笔记时,也使用本 skill。"
|
description: "飞书云文档(Docx / Wiki 文档):读取和编辑飞书文档内容。当用户给出文档 URL 或 token,或需要查看、创建、编辑文档、插入或下载文档图片附件时使用。文档中嵌入的电子表格、多维表格、画板,先用本 skill 提取 token 再切到对应 skill。当用户给出 doubao.com 的 /docx/ 或 /wiki/ URL/token 时,也应直接使用本 skill;路由依据是 URL 路径模式和 token,而不是域名。不负责文档评论管理,也不负责表格或 Base 的数据操作。当用户明确要操作飞书思维笔记时,也使用本 skill。"
|
||||||
metadata:
|
metadata:
|
||||||
requires:
|
requires:
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ lark-cli docs +update --doc "<doc_id>" --command block_replace \
|
|||||||
--content '<p>替换后的段落内容</p>'
|
--content '<p>替换后的段落内容</p>'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
投票 block 也使用 `block_replace` 做整块替换:replacement 中的新 `<poll>` 会创建新的投票 block,不继承旧投票的 block id、option id、票数、投票人、发布时间或当前用户选择;如需把投票改成普通内容,直接把 `--content` 写成目标 XML。
|
||||||
|
|
||||||
### block_delete — 删除指定 block
|
### block_delete — 删除指定 block
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
|
|||||||
|-|-|-|
|
|-|-|-|
|
||||||
| `<title>` | 文档标题(每篇唯一)| `align` |
|
| `<title>` | 文档标题(每篇唯一)| `align` |
|
||||||
| `<checkbox>` | 待办项| `done="true"\|"false"` |
|
| `<checkbox>` | 待办项| `done="true"\|"false"` |
|
||||||
|
| `<poll>` | 投票块,支持创建草稿、可选创建后发布 | `poll-type`, `is-anonymous`, `enable-due-time`, `due-time`, `publish-on-create` |
|
||||||
|
|
||||||
## 容器标签
|
## 容器标签
|
||||||
|标签|说明|关键属性|
|
|标签|说明|关键属性|
|
||||||
@@ -48,6 +49,40 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
|
|||||||
- `<sub-page-list>` — `<sub-page-list></sub-page-list>` 子页面列表块;仅 wiki 文档可插入
|
- `<sub-page-list>` — `<sub-page-list></sub-page-list>` 子页面列表块;仅 wiki 文档可插入
|
||||||
- bitable、base_ref、synced_reference、synced_source、okr — 不可创建,仅支持移动
|
- bitable、base_ref、synced_reference、synced_source、okr — 不可创建,仅支持移动
|
||||||
|
|
||||||
|
## 投票 block
|
||||||
|
|
||||||
|
投票使用结构化 XML 表达。默认创建未发布草稿:空标题、两个空选项、单选、实名、无截止时间、结果策略固定为投票后可见。
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<poll></poll>
|
||||||
|
```
|
||||||
|
|
||||||
|
带内容创建:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<poll poll-type="single" is-anonymous="false">
|
||||||
|
<poll-title>午饭吃什么?</poll-title>
|
||||||
|
<poll-option>米饭</poll-option>
|
||||||
|
<poll-option>面条</poll-option>
|
||||||
|
</poll>
|
||||||
|
```
|
||||||
|
|
||||||
|
创建后尝试发布:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<poll publish-on-create="true">
|
||||||
|
<poll-title>午饭吃什么?</poll-title>
|
||||||
|
<poll-option>米饭</poll-option>
|
||||||
|
<poll-option>面条</poll-option>
|
||||||
|
</poll>
|
||||||
|
```
|
||||||
|
|
||||||
|
公开可写属性只有:`poll-type="single|multiple"`、`is-anonymous="true|false"`、`enable-due-time="true|false"`、`due-time="毫秒时间戳"`、`publish-on-create="true|false"`。不要写入 `when-result-visible`、`option-id`、票数、投票人或当前用户投票状态。
|
||||||
|
|
||||||
|
读取已发布投票时,XML 可能带只读结果字段,例如 `is-published`、`result-visible`、`user-count`、`poll-option count/percent/selected/voters-ref`。这些字段只用于展示,重新导入或 `block_replace` 时会被忽略;匿名投票不会通过 `reference_map` 暴露真实投票人。`voters-ref` 是读取详情的 opaque handle,不是投票操作入口。
|
||||||
|
|
||||||
|
修改投票配置、替换已发布投票、把投票替换成普通内容,都使用 `block_replace`,语义是删除旧 block 并插入 replacement。新 `<poll>` 会创建新的投票 block,不继承旧 block id、option id、票数、投票人、发布时间或当前用户选择。
|
||||||
|
|
||||||
# 四、块级复制与移动
|
# 四、块级复制与移动
|
||||||
|
|
||||||
## 移动(block_move_after)
|
## 移动(block_move_after)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: lark-drive
|
name: lark-drive
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: "飞书云空间(云盘/云存储):管理 Drive 文件和文件夹,包含上传/下载、创建文件夹、复制/移动/删除、查看元数据、评论/权限/订阅、标题、版本和本地文件导入。用户需要整理云盘目录、处理云空间资源 URL/token、判断链接类型/真实 token/标题,或导入 Word/Markdown/Excel/CSV/PPTX/.base 为 docx/sheet/bitable/slides 时使用;doubao.com 云空间 URL/token 也按资源路径和 token 路由,不回退 WebFetch。不负责:文档内容编辑(走 lark-doc)、表格/Base 表内数据操作(走 lark-sheets/lark-base)、知识空间节点/成员管理(走 lark-wiki)、原生 Markdown 文件读写/patch/diff(走 lark-markdown)。"
|
description: "飞书云空间(云盘/云存储):管理 Drive 文件和文件夹,包含上传/下载、创建文件夹、复制/移动/删除、查看元数据、评论/权限/订阅、标题、版本和本地文件导入。用户需要整理云盘目录、处理云空间资源 URL/token,或导入 Word/Markdown/Excel/CSV/PPTX/.base 为 docx/sheet/bitable/slides 时使用;doubao.com 云空间 URL/token 也按资源路径和 token 路由,不回退 WebFetch。不负责:文档内容编辑(走 lark-doc)、表格/Base 表内数据操作(走 lark-sheets/lark-base)、知识空间节点/成员管理(走 lark-wiki)、原生 Markdown 文件读写/patch/diff(走 lark-markdown)。"
|
||||||
metadata:
|
metadata:
|
||||||
requires:
|
requires:
|
||||||
bins: ["lark-cli"]
|
bins: ["lark-cli"]
|
||||||
@@ -21,10 +21,8 @@ metadata:
|
|||||||
## 快速决策
|
## 快速决策
|
||||||
|
|
||||||
- 用户要**复制文档 / 创建副本 / 另存为副本**时,使用 `lark-cli drive files copy`。先用 `lark-cli schema drive.files.copy --format json` 确认参数;如果来源是 wiki URL/token,先用 `lark-cli drive +inspect` 获取底层 `token` 和 `type`,不要把 wiki token 直接当 `file_token`。`params.file_token` 传源文档 token,`data.folder_token` 传目标文件夹 token,`data.name` 传副本名称,`data.type` 传源文件类型(如 `docx` / `sheet` / `bitable` / `slides`)。示例:`lark-cli drive files copy --params '{"file_token":"<DOC_TOKEN>"}' --data '{"folder_token":"<FOLDER_TOKEN>","name":"<COPY_NAME>","type":"docx"}'`。如返回 `confirmation_required`,按 `lark-shared` 高风险审批协议向用户确认后,在原命令末尾追加 `--yes` 重试。
|
- 用户要**复制文档 / 创建副本 / 另存为副本**时,使用 `lark-cli drive files copy`。先用 `lark-cli schema drive.files.copy --format json` 确认参数;如果来源是 wiki URL/token,先用 `lark-cli drive +inspect` 获取底层 `token` 和 `type`,不要把 wiki token 直接当 `file_token`。`params.file_token` 传源文档 token,`data.folder_token` 传目标文件夹 token,`data.name` 传副本名称,`data.type` 传源文件类型(如 `docx` / `sheet` / `bitable` / `slides`)。示例:`lark-cli drive files copy --params '{"file_token":"<DOC_TOKEN>"}' --data '{"folder_token":"<FOLDER_TOKEN>","name":"<COPY_NAME>","type":"docx"}'`。如返回 `confirmation_required`,按 `lark-shared` 高风险审批协议向用户确认后,在原命令末尾追加 `--yes` 重试。
|
||||||
- 用户要**识别飞书 / doubao 云空间 URL 的类型和 token**时,可以先按 URL 路径形态做轻量判断;当路径已明确指向 docx / sheet / bitable / slides / file / folder 等资源时,可直接提取对应 token/type。传入 wiki URL、需要识别标题或 canonical URL、URL/token 有歧义,或后续操作依赖底层真实资源时,再使用 `lark-cli drive +inspect --url '<url>'` 进行识别;具体用法、失败处理和边界见 [`references/lark-drive-inspect.md`](references/lark-drive-inspect.md)。
|
|
||||||
- 高风险写操作(删除、公开权限修改、owner 转移、版本删除/回滚、批量移动/覆盖/同步)必须同时满足三个条件才执行:目标已解析为该操作可直接使用的执行对象,执行细节已明确到可直接调用命令(例如删除的 file-token/type、公开权限修改的共享范围、owner 转移的目标 owner、版本删除/回滚的 version id、移动/覆盖/同步的目标位置和冲突策略),且用户在本轮明确确认执行这些具体目标和执行细节。用户只说“删除没用的文件”“开放/共享给大家”“改成开放”“覆盖/移动这些”只表示目标状态;先只读发现并列出候选、权限档位或执行方案,停止等待用户确认。
|
|
||||||
- 用户要**检查 / 治理文档权限、公开范围、链接分享、外部访问、复制下载权限、密级标签、owner 转移**,或要“权限风险报告、收紧权限、申请查看 / 编辑权限、转移 / 批量转移 owner”,必须先阅读 [`references/lark-drive-workflow.md`](references/lark-drive-workflow.md),再按其中 `Workflow Registry` 进入 [`permission_governance`](references/lark-drive-workflow-permission-governance.md) workflow。
|
- 用户要**检查 / 治理文档权限、公开范围、链接分享、外部访问、复制下载权限、密级标签、owner 转移**,或要“权限风险报告、收紧权限、申请查看 / 编辑权限、转移 / 批量转移 owner”,必须先阅读 [`references/lark-drive-workflow.md`](references/lark-drive-workflow.md),再按其中 `Workflow Registry` 进入 [`permission_governance`](references/lark-drive-workflow-permission-governance.md) workflow。
|
||||||
- 用户要**整理云盘 / 文件夹 / 文档库 / 知识库 / 个人文档库**,或要“盘点目录结构、找出未归档/临时/重复/空目录、生成整理方案”,必须先阅读 [`references/lark-drive-workflow.md`](references/lark-drive-workflow.md),再按其中 `Workflow Registry` 进入 [`knowledge_organize`](references/lark-drive-workflow-knowledge-organize.md) workflow。默认只生成方案;创建目录、移动资源、申请权限都必须单独确认。
|
- 用户要**整理云盘 / 文件夹 / 文档库 / 知识库 / 个人文档库**,或要“盘点目录结构、找出未归档/临时/重复/空目录、生成整理方案”,必须先阅读 [`references/lark-drive-workflow-knowledge-organize.md`](references/lark-drive-workflow-knowledge-organize.md)。默认只生成方案;创建目录、移动资源、申请权限都必须单独确认。
|
||||||
- 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间(云盘/云存储)对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"(→ `--created-by-me`,原始创建者语义)、"我负责/owner 的"(→ `--mine`,owner 语义)、"最近一周我打开过的 xxx"、"某人 owner 的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。
|
- 用户要**搜文档 / Wiki / 电子表格 / 多维表格 / 云空间(云盘/云存储)对象**,优先使用 `lark-cli drive +search`。自然语言里"最近我编辑过的"、"我创建的"(→ `--created-by-me`,原始创建者语义)、"我负责/owner 的"(→ `--mine`,owner 语义)、"最近一周我打开过的 xxx"、"某人 owner 的 docx" 等直接映射到扁平 flag,避免手写嵌套 JSON。
|
||||||
- 用户要**根据文档评论定位正文位置**,例如 根据评论 review 文档、根据评论内容回看文档、区分多处相同引用文本时,对于 docx 类型(`file_type=docx`)的文档支持通过 `need_relation=true` 返回评论位置,其他类型暂不支持,具体用法需要先阅读 [`references/lark-drive-comment-location.md`](references/lark-drive-comment-location.md) 了解。
|
- 用户要**根据文档评论定位正文位置**,例如 根据评论 review 文档、根据评论内容回看文档、区分多处相同引用文本时,对于 docx 类型(`file_type=docx`)的文档支持通过 `need_relation=true` 返回评论位置,其他类型暂不支持,具体用法需要先阅读 [`references/lark-drive-comment-location.md`](references/lark-drive-comment-location.md) 了解。
|
||||||
- 用户给出 doubao.com 的云空间资源 URL/token,或明确提到豆包里的 file/folder/docx/sheet/bitable/wiki 资源时,仍按资源类型、URL 路径和 token 路由到本 skill;不要因为域名不是飞书而回退到 WebFetch。
|
- 用户给出 doubao.com 的云空间资源 URL/token,或明确提到豆包里的 file/folder/docx/sheet/bitable/wiki 资源时,仍按资源类型、URL 路径和 token 路由到本 skill;不要因为域名不是飞书而回退到 WebFetch。
|
||||||
@@ -164,7 +162,7 @@ lark-cli drive <resource> <method> [flags] # 调用 API
|
|||||||
|
|
||||||
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
|
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
|
||||||
>
|
>
|
||||||
> **高频原生命令:** 读取 Drive 文件夹清单时使用 `drive files list`,使用前先读 [`references/lark-drive-files-list.md`](references/lark-drive-files-list.md),按模板通过 `--params` 传参并手动处理分页;不要把 `--page-all` 输出直接交给 JSON 解析脚本。
|
> **高频原生命令:** 读取 Drive 文件夹清单时使用 `drive files list`,必须按 [`references/lark-drive-files-list.md`](references/lark-drive-files-list.md) 的模板通过 `--params` 传 `folder_token` / `page_token`,并手动处理分页;不要把 `--page-all` 输出直接交给 JSON 解析脚本。
|
||||||
|
|
||||||
### files
|
### files
|
||||||
|
|
||||||
@@ -206,12 +204,10 @@ lark-cli drive <resource> <method> [flags] # 调用 API
|
|||||||
### file.statistics
|
### file.statistics
|
||||||
|
|
||||||
- `get` — 获取文件统计信息
|
- `get` — 获取文件统计信息
|
||||||
- 获取 docx / 文件统计信息时,建议优先使用 typed flags:`lark-cli drive file.statistics get --file-token <token> --file-type <type> --format json`;`--params` JSON 也支持,适合批量拼装或 raw 参数场景。
|
|
||||||
|
|
||||||
### file.view_records
|
### file.view_records
|
||||||
|
|
||||||
- `list` — 获取文档的访问者记录
|
- `list` — 获取文档的访问者记录
|
||||||
- 查看 docx 最近访问记录、返回 open_id、最多 N 条时,建议优先使用 typed flags:`lark-cli drive file.view_records list --file-token <docx_token> --file-type docx --page-size <N> --viewer-id-type open_id --format json`;`--params` JSON 也支持,适合批量拼装、分页续跑或 raw 参数场景。
|
|
||||||
|
|
||||||
### file.comment.reply.reactions
|
### file.comment.reply.reactions
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,6 @@
|
|||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> 这是**高风险写操作**。CLI 层要求显式传 `--yes`;如果用户已经明确要求删除且目标明确,直接执行并带上 `--yes`。
|
> 这是**高风险写操作**。CLI 层要求显式传 `--yes`;如果用户已经明确要求删除且目标明确,直接执行并带上 `--yes`。
|
||||||
> “目标明确”表示用户给出了可解析为 `file-token` + `type` 的具体 URL/token,或对你刚列出的可解析资源列表逐项/整批确认删除。按“没用的”“临时的”“疑似重复的”“全部旧文件”等描述搜索出来的候选属于待确认目标;这类请求先列候选、说明筛选依据和影响范围,然后停止等待确认。
|
|
||||||
|
|
||||||
## 删除前门槛
|
|
||||||
|
|
||||||
执行 `drive +delete --yes` 前同时满足:
|
|
||||||
|
|
||||||
| 条件 | 可执行信号 |
|
|
||||||
|------|------------|
|
|
||||||
| 具体目标 | 单个可解析为 `file-token` + `type` 的 URL/token,或用户确认过且可解析的资源列表 |
|
|
||||||
| 执行确认 | 用户在本轮明确说确认删除这些具体目标 |
|
|
||||||
|
|
||||||
若缺少任一条件,使用 `drive +search`、`drive +inspect` 或只读 API 收集候选并回复待确认清单;启发式规则(打开时间、标题模式、owner、文件类型等)只能作为候选筛选依据,不能升级为删除确认。执行 `drive +delete` 时必须使用解析后的 `--file-token` 和 `--type`。
|
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
|
|||||||
@@ -41,37 +41,12 @@ lark-cli drive files list \
|
|||||||
|
|
||||||
也可以省略 `folder_token` 字段来请求根目录,但在 Agent 编排中建议显式传空字符串,避免把“忘记传参数”和“确认请求根目录”混在一起。
|
也可以省略 `folder_token` 字段来请求根目录,但在 Agent 编排中建议显式传空字符串,避免把“忘记传参数”和“确认请求根目录”混在一起。
|
||||||
|
|
||||||
## 按时间排序
|
|
||||||
|
|
||||||
默认不要传 `order_by` / `direction`;服务端会按默认顺序返回。只有用户明确要求按创建时间或编辑时间排序时,才使用服务端排序参数。
|
|
||||||
|
|
||||||
按创建时间升序列出当前文件夹直接子项:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
lark-cli drive files list \
|
|
||||||
--params '{"folder_token":"<folder_token>","order_by":"CreatedTime","direction":"ASC","page_size":200}' \
|
|
||||||
--format json
|
|
||||||
```
|
|
||||||
|
|
||||||
按编辑时间降序列出当前文件夹直接子项:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
lark-cli drive files list \
|
|
||||||
--params '{"folder_token":"<folder_token>","order_by":"EditedTime","direction":"DESC","page_size":200}' \
|
|
||||||
--format json
|
|
||||||
```
|
|
||||||
|
|
||||||
以上示例返回排序后的当前页;如果返回 `has_more=true`,保持相同 `folder_token` / `order_by` / `direction` / `page_size`,把 `next_page_token` 放入 `page_token` 继续翻页。
|
|
||||||
|
|
||||||
## 参数规则
|
## 参数规则
|
||||||
|
|
||||||
1. `folder_token` 必须放在 `--params` JSON 里;不要使用不存在的 `--folder-token` flag。
|
1. `folder_token` 必须放在 `--params` JSON 里;不要使用不存在的 `--folder-token` flag。
|
||||||
2. `page_token` 必须放在 `--params` JSON 里;不要依赖 shell 变量拼接不完整的 JSON。
|
2. `page_token` 必须放在 `--params` JSON 里;不要依赖 shell 变量拼接不完整的 JSON。
|
||||||
3. 默认不要传 `order_by` / `direction`;只有用户明确要求按创建时间 / 编辑时间排序时才使用服务端排序参数。
|
3. `page_size` 建议显式设置为 `200`。如果服务端或环境返回参数错误,再降级到服务端允许的值,并记录降级原因。
|
||||||
4. 排序参数映射:创建时间 -> `order_by:"CreatedTime"`;编辑时间 / 修改时间 -> `order_by:"EditedTime"`;升序 -> `direction:"ASC"`;降序 -> `direction:"DESC"`。不要省略排序参数后再用 Python / shell 客户端排序替代。
|
4. 调用前如果不确定字段结构,先运行 `lark-cli schema drive.files.list` 查看 `--params` 结构。
|
||||||
5. 排序查询建议带 `page_size:200` 减少翻页;只有用户要求完整分页、递归盘点、大目录全量导出,或当前页返回 `has_more=true` 后继续翻页时,才加入 `page_token`。
|
|
||||||
6. `page_size` 在分页、递归盘点或全量导出时建议显式设置为 `200`。如果服务端或环境返回参数错误,再降级到服务端允许的值,并记录降级原因。
|
|
||||||
7. 调用前如果不确定字段结构,先运行 `lark-cli schema drive.files.list` 查看 `--params` 结构。
|
|
||||||
|
|
||||||
## 返回结构与解析
|
## 返回结构与解析
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,4 @@ JSON 输出包含以下字段:
|
|||||||
- `--url` 为必填参数
|
- `--url` 为必填参数
|
||||||
- 当 `--url` 是 bare token(非完整 URL)时,`--type` 也是必填的
|
- 当 `--url` 是 bare token(非完整 URL)时,`--type` 也是必填的
|
||||||
- wiki URL 会自动调用 `get_node` API 解包,输出中 `type` 和 `token` 是底层文档的类型和 token
|
- wiki URL 会自动调用 `get_node` API 解包,输出中 `type` 和 `token` 是底层文档的类型和 token
|
||||||
- `+inspect` 只用于识别/消歧;如果任务已能通过 URL 路径形态完成路由判断,不必把它作为所有 Drive 操作的通用前置步骤
|
|
||||||
- `+inspect` 失败后不要自动切到写接口继续尝试,先按错误提示处理权限、scope 或链接问题
|
|
||||||
- 支持 `--dry-run` 查看将调用的 API 步骤
|
- 支持 `--dry-run` 查看将调用的 API 步骤
|
||||||
|
|||||||
@@ -10,18 +10,6 @@
|
|||||||
|
|
||||||
如果用户只是想向文档 owner 申请访问权限,优先使用 [`lark-drive-apply-permission.md`](lark-drive-apply-permission.md)。
|
如果用户只是想向文档 owner 申请访问权限,优先使用 [`lark-drive-apply-permission.md`](lark-drive-apply-permission.md)。
|
||||||
|
|
||||||
## 公开权限修改前门槛
|
|
||||||
|
|
||||||
公开权限修改是高风险写操作。执行 `drive permission.public patch --yes` 前同时确认:
|
|
||||||
|
|
||||||
| 条件 | 可执行信号 |
|
|
||||||
|------|------------|
|
|
||||||
| 具体目标 | 单个 URL/token,或用户确认过的资源列表 |
|
|
||||||
| 公开范围 | 用户明确选择组织内/互联网、可读/可编辑等具体 `link_share_entity` 档位 |
|
|
||||||
| 执行确认 | 用户在本轮确认按该目标和范围执行 |
|
|
||||||
|
|
||||||
“开放一下”“共享给大家”“让大家能看”只表达目标状态,不包含具体公开范围。先列出可选范围并停止等待用户选择;公开档位必须来自用户选择,CLI 的 `--yes` 只表示已获得用户对该档位的执行确认。
|
|
||||||
|
|
||||||
## 公开权限错误码
|
## 公开权限错误码
|
||||||
|
|
||||||
调用 `lark-cli drive permission.public patch` 更新文档公开权限失败时,如果返回以下错误码,按表格给用户明确下一步。不要把这些错误简单归类为缺少 scope;它们通常表示租户、对外分享或文档密级策略拦截。
|
调用 `lark-cli drive permission.public patch` 更新文档公开权限失败时,如果返回以下错误码,按表格给用户明确下一步。不要把这些错误简单归类为缺少 scope;它们通常表示租户、对外分享或文档密级策略拦截。
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
|
|||||||
| `permission_denied` | `1061004` / HTTP 403 | 当前身份无权操作目标资源 | 停止重试,检查目标文件夹权限、身份类型(user / bot)和资源可见性 |
|
| `permission_denied` | `1061004` / HTTP 403 | 当前身份无权操作目标资源 | 停止重试,检查目标文件夹权限、身份类型(user / bot)和资源可见性 |
|
||||||
| `invalid_api_parameters` | `1061002` | API 参数被服务端拒绝 | 停止重试,检查 `--folder-token`、覆盖模式、`file_token`、文件名和上传参数;不要对同一参数组合批量重试 |
|
| `invalid_api_parameters` | `1061002` | API 参数被服务端拒绝 | 停止重试,检查 `--folder-token`、覆盖模式、`file_token`、文件名和上传参数;不要对同一参数组合批量重试 |
|
||||||
| `parent_node_missing` | `1061044` | 上传 / 建目录使用的父文件夹不存在或当前身份不可见 | 停止重试,检查 `--folder-token` 是否仍存在、是否有权限、父目录是否在 push 过程中被删除;不要继续上传同一目录树 |
|
| `parent_node_missing` | `1061044` | 上传 / 建目录使用的父文件夹不存在或当前身份不可见 | 停止重试,检查 `--folder-token` 是否仍存在、是否有权限、父目录是否在 push 过程中被删除;不要继续上传同一目录树 |
|
||||||
| `parent_sibling_limit` | `1062507` | 目标父文件夹单层子节点数量超过上限 | 停止重试,清理目标目录、换一个 `--folder-token`,或把上传内容拆到多个子目录 |
|
|
||||||
| `rate_limited` | `99991400` | 触发频控 | 停止当前批次,退避后再重试 |
|
| `rate_limited` | `99991400` | 触发频控 | 停止当前批次,退避后再重试 |
|
||||||
| `server_error` | `1061001` / `2200` | Drive 服务端异常 | 停止当前批次,稍后重试;保留 `log_id` 便于排查 |
|
| `server_error` | `1061001` / `2200` | Drive 服务端异常 | 停止当前批次,稍后重试;保留 `log_id` 便于排查 |
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
# 知识整理工作流
|
# 知识整理工作流
|
||||||
|
|
||||||
Workflow id: `knowledge_organize`
|
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.
|
||||||
|
|
||||||
Risk / Structure: `R2-R3` / `S3`
|
|
||||||
|
|
||||||
This file implements the registered knowledge organization workflow. Before execution, the agent MUST read [`lark-drive-workflow.md`](lark-drive-workflow.md) and [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md), and follow the shared execution protocol, Artifact Contract, Workflow Loading rules, authentication rules, and write confirmation rules.
|
|
||||||
|
|
||||||
It defines the workflow-specific 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.
|
Phase files are references for this workflow, not independent skills. Do not route user requests directly to a phase file.
|
||||||
|
|
||||||
@@ -86,7 +80,7 @@ When this workflow is triggered, the agent MUST:
|
|||||||
|
|
||||||
## Runtime State
|
## Runtime State
|
||||||
|
|
||||||
This workflow extends the shared Artifact Contract. Agent MUST maintain these internal fields during one workflow run:
|
Agent MUST maintain these internal fields during one workflow run:
|
||||||
|
|
||||||
| Field | Meaning |
|
| Field | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
@@ -120,24 +114,24 @@ This workflow extends the shared Artifact Contract. Agent MUST maintain these in
|
|||||||
|
|
||||||
## Execution State Machine
|
## Execution State Machine
|
||||||
|
|
||||||
| State | Protocol Step | Entry Condition | Agent MUST Do | User-Facing Output | wait_for_user | Next State |
|
| State | Entry Condition | Agent MUST Do | User-Facing Output | wait_for_user | Next State |
|
||||||
|-------|---------------|-----------------|---------------|--------------------|---------------|------------|
|
|-------|-----------------|---------------|--------------------|---------------|------------|
|
||||||
| `PARSE_SCOPE` | `route` / `scope` | Workflow triggered | Load discovery phase; parse target, environment, identity, and target type | Scope confirmation or clarification question | `true` | `INVENTORY` |
|
| `PARSE_SCOPE` | Workflow triggered | Load discovery phase; parse target, environment, identity, and target type | Scope confirmation or clarification question | `true` | `INVENTORY` |
|
||||||
| `INVENTORY` | `read` | Scope confirmed | Load discovery phase; recursively list resources and build `resource_items` | Inventory progress / summary; continue automatically unless blocked | `false` unless blocked | `CONTENT_READ` |
|
| `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` | `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` |
|
| `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` | `assess` / `plan` | 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` |
|
| `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` | `assess` / `plan` | 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` |
|
| `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` | `assess` / `plan` | 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` |
|
| `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` | `confirm` | User wants execution | Load planning phase; ask user to choose execution scope | Execution options and write-operation summary | `true` | `EXECUTE` or `DONE` |
|
| `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` | `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` |
|
| `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` | `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` |
|
| `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` | `recovery 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_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` | `recovery execute` | User explicitly confirms restore execution | Load rollback phase; execute confirmed reverse moves only | Recovery progress / result | `false` | `ROLLBACK_VERIFY` |
|
| `ROLLBACK` | User explicitly confirms restore execution | Load rollback phase; execute confirmed reverse moves only | Recovery progress / result | `false` | `ROLLBACK_VERIFY` |
|
||||||
| `ROLLBACK_VERIFY` | `recovery 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_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 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_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` | `cleanup execute` | 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` | 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 verify` | Cleanup deletion finished | Load rollback phase; verify deleted cleanup targets | Cleanup verification result | `false` | `DONE` |
|
| `ROLLBACK_CLEANUP_VERIFY` | Cleanup deletion finished | Load rollback phase; verify deleted cleanup targets | Cleanup verification result | `false` | `DONE` |
|
||||||
| `DONE` | `done` | No more action | Stop | Final answer | `false` | End |
|
| `DONE` | No more action | Stop | Final answer | `false` | End |
|
||||||
|
|
||||||
## Progressive Load Map
|
## Progressive Load Map
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Structure Level:
|
|||||||
2. Entry file 超过约 300 行时,优先拆 `commands`、`outputs` 或 `artifacts` reference。
|
2. Entry file 超过约 300 行时,优先拆 `commands`、`outputs` 或 `artifacts` reference。
|
||||||
3. 只有执行、验证、恢复或 rollback 状态链复杂到影响可读性时,才升级到 `S3` phase files。
|
3. 只有执行、验证、恢复或 rollback 状态链复杂到影响可读性时,才升级到 `S3` phase files。
|
||||||
4. 垂直业务包优先作为已有 workflow 的 recipe / policy / template,不默认新增独立 workflow。
|
4. 垂直业务包优先作为已有 workflow 的 recipe / policy / template,不默认新增独立 workflow。
|
||||||
5. 已有样板:`permission_governance` 是 `R2/S2`;`knowledge_organize` 是 `R2-R3/S3`。
|
5. 已有样板:`permission_governance` 是 `R2/S2`;已发布的独立 `knowledge_organize` 是 `R2-R3/S3`,当前不作为本总框架 registry entry。
|
||||||
|
|
||||||
## 加载与拆分边界
|
## 加载与拆分边界
|
||||||
|
|
||||||
@@ -111,7 +111,6 @@ Structure Level:
|
|||||||
| Workflow | Status | Risk | Structure | Entry File | Trigger |
|
| Workflow | Status | Risk | Structure | Entry File | Trigger |
|
||||||
|----------|--------|------|-----------|------------|---------|
|
|----------|--------|------|-----------|------------|---------|
|
||||||
| `permission_governance` | Registered | `R2` | `S2` | [`lark-drive-workflow-permission-governance.md`](lark-drive-workflow-permission-governance.md) | 权限审计、公开链接/外部访问、复制/下载/评论/分享设置、权限申请、owner 转移 / 批量 owner 转移、密级标签调整 |
|
| `permission_governance` | Registered | `R2` | `S2` | [`lark-drive-workflow-permission-governance.md`](lark-drive-workflow-permission-governance.md) | 权限审计、公开链接/外部访问、复制/下载/评论/分享设置、权限申请、owner 转移 / 批量 owner 转移、密级标签调整 |
|
||||||
| `knowledge_organize` | Registered | `R2-R3` | `S3` | [`lark-drive-workflow-knowledge-organize.md`](lark-drive-workflow-knowledge-organize.md) | 整理云盘 / 文件夹 / 文档库 / 知识库、盘点目录结构、归类资源、生成整理方案,并在用户确认后创建目录或移动资源 |
|
|
||||||
|
|
||||||
## Workflow Loading
|
## Workflow Loading
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ lark-cli mail +watch --print-output-schema
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `--mailbox <id>` | `me` | 订阅目标邮箱 |
|
| `--mailbox <id>` | `me` | 订阅目标邮箱 |
|
||||||
| `--msg-format <mode>` | `metadata` | 输出模式:`metadata` / `minimal` / `plain_text_full` / `full` / `event` |
|
| `--msg-format <mode>` | `metadata` | 输出模式:`metadata` / `minimal` / `plain_text_full` / `full` / `event` |
|
||||||
| `--format <mode>` | `data` | 输出样式:`json`(带 ok/data 信封的 NDJSON 流)/ `data`(裸 NDJSON 流) |
|
| `--format <mode>` | `table` | 输出样式:`table` / `json` / `data` |
|
||||||
| `--folder-ids <json-array>` | — | 文件夹 ID 过滤,如 `["INBOX","SENT"]` |
|
| `--folder-ids <json-array>` | — | 文件夹 ID 过滤,如 `["INBOX","SENT"]` |
|
||||||
| `--folders <json-array>` | — | 文件夹名称过滤(与 `--folder-ids` 取并集) |
|
| `--folders <json-array>` | — | 文件夹名称过滤(与 `--folder-ids` 取并集) |
|
||||||
| `--label-ids <json-array>` | — | 标签 ID 过滤,如 `["FLAGGED","IMPORTANT"]` |
|
| `--label-ids <json-array>` | — | 标签 ID 过滤,如 `["FLAGGED","IMPORTANT"]` |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
## Core Rules
|
## Core Rules
|
||||||
|
|
||||||
- `asset_need` is metadata only. It can guide page design, but it must not require web search, local download, media upload, or external tools.
|
- `asset_need` is metadata only. It can guide page design, but it must not require web search, local download, media upload, or external tools.
|
||||||
- Every planned asset must include a fallback visual plan. The fallback can use native charts, tables, whiteboard diagrams, placeholder regions, or XML shapes, text, and arrows as appropriate.
|
- Every planned asset must include a fallback visual plan so the slide can be generated with XML shapes, text, arrows, tables, simple charts, whiteboard diagrams, or placeholder regions.
|
||||||
- Asset needs must serve the page's `key_message` and `visual_focus`. Do not add decorative assets that do not clarify the page.
|
- Asset needs must serve the page's `key_message` and `visual_focus`. Do not add decorative assets that do not clarify the page.
|
||||||
- Prefer a few high-value asset plans over one asset on every page. For a 6-page technical or business deck, plan assets on at least 3 pages when the content allows.
|
- Prefer a few high-value asset plans over one asset on every page. For a 6-page technical or business deck, plan assets on at least 3 pages when the content allows.
|
||||||
- If a real local asset already exists or the user provides one, it can be used through the normal media-upload workflow. Still keep `fallback_if_missing` in the plan.
|
- If a real local asset already exists or the user provides one, it can be used through the normal media-upload workflow. Still keep `fallback_if_missing` in the plan.
|
||||||
@@ -43,7 +43,7 @@ For a page without a meaningful asset need, use:
|
|||||||
- `architecture_diagram`: system components, data flow, dependency map, or model structure.
|
- `architecture_diagram`: system components, data flow, dependency map, or model structure.
|
||||||
- `icon`: small semantic symbol for a concept, step, role, or status.
|
- `icon`: small semantic symbol for a concept, step, role, or status.
|
||||||
- `logo`: brand, product, team, or customer mark.
|
- `logo`: brand, product, team, or customer mark.
|
||||||
- `chart`: column, bar, line, area, radar, pie, doughnut/ring, or combo data visual. Note: `<chart>` does not support funnel or scatter — map those to `<whiteboard>` SVG at generation time.
|
- `chart`: line, bar, pie, radar, area, or combo data visual. Note: `<chart>` does not support funnel or scatter — map those to `<whiteboard>` SVG at generation time.
|
||||||
- `infographic`: composed visual explanation, usually combining labels, numbers, and simple shapes.
|
- `infographic`: composed visual explanation, usually combining labels, numbers, and simple shapes.
|
||||||
- `screenshot`: product UI, terminal output, workflow state, or page capture.
|
- `screenshot`: product UI, terminal output, workflow state, or page capture.
|
||||||
- `flow_diagram`: process, sequence, decision tree, or mechanism diagram.
|
- `flow_diagram`: process, sequence, decision tree, or mechanism diagram.
|
||||||
@@ -64,23 +64,11 @@ Match asset type to slide role:
|
|||||||
|
|
||||||
`suggested_query` is only a future lookup hint. Write it as a short phrase a human or later workflow could search, but do not execute the search unless the user separately requests real assets.
|
`suggested_query` is only a future lookup hint. Write it as a short phrase a human or later workflow could search, but do not execute the search unless the user separately requests real assets.
|
||||||
|
|
||||||
For `asset_type: "chart"`:
|
|
||||||
|
|
||||||
- If the visual is a supported standard data chart — column, bar, line, area, radar, pie, doughnut/ring, or combo — `fallback_if_missing` must still render as a native `<chart>`.
|
|
||||||
- Do not imitate supported standard data visuals with manual drawing primitives or `<whiteboard>`.
|
|
||||||
- Choose the data source explicitly:
|
|
||||||
- `user_provided`: when the user provides concrete values, tables, CSV, or metric lists, use those values and do not replace them with mock data.
|
|
||||||
- `mock_placeholder`: when the user asks for a placeholder, template, example, or chart position to replace later, use mock data in a native `<chart>`.
|
|
||||||
- `mock_required_by_intent`: when the user does not provide concrete values but asks for data expression, charts, trends, comparisons, or distributions, use mock data in a native `<chart>`.
|
|
||||||
- Mock data must be labeled as `模拟数据,仅占位,待替换真实数据` or equivalent. Do not present mock values as facts.
|
|
||||||
- Manual drawing fallbacks are allowed only for unsupported chart types such as scatter, funnel, waterfall-like custom visuals, or decorative non-data visuals.
|
|
||||||
|
|
||||||
`fallback_if_missing` must be concrete enough to turn into XML, for example:
|
`fallback_if_missing` must be concrete enough to turn into XML, for example:
|
||||||
|
|
||||||
- "Draw a simplified attention matrix with 5 token labels, semi-transparent cells, and arrows to output token."
|
- "Draw a simplified attention matrix with 5 token labels, semi-transparent cells, and arrows to output token."
|
||||||
- "Use three grouped boxes with arrows from client to gateway to service; add small protocol labels."
|
- "Use three grouped boxes with arrows from client to gateway to service; add small protocol labels."
|
||||||
- "Render a native `<chart>` using the user-provided series."
|
- "Render a mini bar chart with 4 bars using shapes and value labels."
|
||||||
- "Render a native `<chart>` with mock placeholder values and label it as `模拟数据,仅占位,待替换真实数据`."
|
|
||||||
- "Use a bordered placeholder panel with product area labels, not an empty image."
|
- "Use a bordered placeholder panel with product area labels, not an empty image."
|
||||||
|
|
||||||
Weak fallbacks to avoid:
|
Weak fallbacks to avoid:
|
||||||
@@ -130,7 +118,7 @@ Business comparison page:
|
|||||||
When generating XML:
|
When generating XML:
|
||||||
|
|
||||||
1. If an asset exists and the workflow supports it, place it in the planned visual region.
|
1. If an asset exists and the workflow supports it, place it in the planned visual region.
|
||||||
2. If no asset exists, immediately render `fallback_if_missing` with the planned XML-native element type. Supported standard data visuals still use native `<chart>`; other fallbacks may use shapes, text, lines, arrows, tables, whiteboard diagrams, or placeholder panels.
|
2. If no asset exists, immediately render `fallback_if_missing` with XML-native shapes, text, lines, arrows, tables, whiteboard diagrams, or chart-like elements.
|
||||||
3. Size the fallback to satisfy `visual_focus`; it should be a real page element, not a tiny decoration.
|
3. Size the fallback to satisfy `visual_focus`; it should be a real page element, not a tiny decoration.
|
||||||
4. Keep text-density limits. Do not compensate for missing assets by adding long bullet text.
|
4. Keep text-density limits. Do not compensate for missing assets by adding long bullet text.
|
||||||
5. After creation, fetch the presentation and verify asset pages are not blank and that each planned fallback is visible when no real asset was used.
|
5. After creation, fetch the presentation and verify asset pages are not blank and that each planned fallback is visible when no real asset was used.
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
`<whiteboard>` 放在 `<data>` 内,内部可放 **SVG** 或 **Mermaid**,用于绘制流程图、时序图、架构图、散点图、漏斗图、自定义图标、装饰图案等 `<chart>` 和 `<shape>` 难以覆盖的视觉内容。
|
`<whiteboard>` 放在 `<data>` 内,内部可放 **SVG** 或 **Mermaid**,用于绘制流程图、时序图、架构图、散点图、漏斗图、自定义图标、装饰图案等 `<chart>` 和 `<shape>` 难以覆盖的视觉内容。
|
||||||
|
|
||||||
普通柱状图、条形图、折线图、面积图、雷达图、饼图 / 环图和组合图应优先使用原生 `<chart>`。除非用户明确要求像素级自定义,或图表类型确实不受 `<chart>` 支持,否则不要用 `<whiteboard>` + SVG / Mermaid 重画这些标准图表。
|
|
||||||
|
|
||||||
> 前置条件:使用本文档前先阅读 [lark-slides SKILL.md](../SKILL.md)。
|
> 前置条件:使用本文档前先阅读 [lark-slides SKILL.md](../SKILL.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -14,13 +12,13 @@
|
|||||||
|
|
||||||
| 场景 | 推荐元素 |
|
| 场景 | 推荐元素 |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| 有结构化数据序列的柱/条/折线/面积/雷达/饼/环/组合图 | `<chart>` — 原生渲染,支持 legend / tooltip / 系列配色 |
|
| 有结构化数据序列的柱/条/折线/面积/雷达/饼/组合图 | `<chart>` — 原生渲染,支持 legend / tooltip / 系列配色 |
|
||||||
| 散点图、漏斗图(`<chart>` 不支持)或其他非原生数据视觉 | `<whiteboard>` SVG |
|
| 散点图、漏斗图(`<chart>` 不支持) | `<whiteboard>` SVG |
|
||||||
| 流程图、时序图、架构图、类图、ER 图等拓扑图 | `<whiteboard>` Mermaid 或 SVG |
|
| 流程图、时序图、架构图、类图、ER 图等拓扑图 | `<whiteboard>` Mermaid 或 SVG |
|
||||||
| 自定义图标、徽标、示意性图形(需要 path/polygon 精确控制) | `<whiteboard>` SVG |
|
| 自定义图标、徽标、示意性图形(需要 path/polygon 精确控制) | `<whiteboard>` SVG |
|
||||||
| 进度条、波浪背景、装饰图案、像素级自定义可视化 | `<whiteboard>` SVG |
|
| 进度条、波浪背景、装饰图案、像素级自定义可视化 | `<whiteboard>` SVG |
|
||||||
|
|
||||||
> 适合 `<chart>` 的内容就用 `<chart>`,不要用 SVG / Mermaid 手绘——原生渲染更省力、结构更稳定,也更容易被回读和后续编辑。
|
> 适合 `<chart>` 的内容就用 `<chart>`,不要用 SVG 手绘——原生渲染更省力且质量更高。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,13 +39,9 @@ SVG 内的坐标相对于 whiteboard 自身左上角(0,0),与 slide 坐标
|
|||||||
|
|
||||||
## SVG 还是 Mermaid?
|
## SVG 还是 Mermaid?
|
||||||
|
|
||||||
选择分三步:**先排除原生 `<chart>`,再判断 whiteboard 类型,最后看当前模型身份**。
|
选择分两步:**先看图表类型,再看当前模型身份**。
|
||||||
|
|
||||||
### 第一步:先确认是否应该使用 `<chart>`
|
### 第一步:图表类型优先判断
|
||||||
|
|
||||||
如果内容是柱状图、条形图、折线图、面积图、雷达图、饼图 / 环图或组合图,返回使用原生 `<chart>`,不要继续套用本文档的 SVG / Mermaid 路径。
|
|
||||||
|
|
||||||
### 第二步:whiteboard 类型优先判断
|
|
||||||
|
|
||||||
以下类型**推荐 Mermaid**,自动布局、代码简洁;如需精确匹配品牌配色或自定义节点样式,可改用 SVG:
|
以下类型**推荐 Mermaid**,自动布局、代码简洁;如需精确匹配品牌配色或自定义节点样式,可改用 SVG:
|
||||||
|
|
||||||
@@ -56,19 +50,20 @@ SVG 内的坐标相对于 whiteboard 自身左上角(0,0),与 slide 坐标
|
|||||||
| 流程图、决策树、架构图 | `flowchart TD` / `flowchart LR` |
|
| 流程图、决策树、架构图 | `flowchart TD` / `flowchart LR` |
|
||||||
| 时序图 | `sequenceDiagram` |
|
| 时序图 | `sequenceDiagram` |
|
||||||
| 类图 | `classDiagram` |
|
| 类图 | `classDiagram` |
|
||||||
|
| 饼图 | `pie` |
|
||||||
| 甘特图 | `gantt` |
|
| 甘特图 | `gantt` |
|
||||||
| 状态图 | `stateDiagram-v2` |
|
| 状态图 | `stateDiagram-v2` |
|
||||||
| 思维导图 | `mindmap` |
|
| 思维导图 | `mindmap` |
|
||||||
| ER 图 | `erDiagram` |
|
| ER 图 | `erDiagram` |
|
||||||
|
|
||||||
### 第三步:非原生图表与装饰元素按模型身份选路径
|
### 第二步:数据图表与装饰元素按模型身份选路径
|
||||||
|
|
||||||
上表以外的场景(散点图、漏斗图、进度条、时间线、波浪背景、星点纹理等)需要精确控制坐标和配色,SVG 表达力更强,但各模型生成 SVG 的能力有差异:
|
上表以外的场景(散点图、漏斗图、进度条、时间线、波浪背景、星点纹理等)需要精确控制坐标和配色,SVG 表达力更强,但各模型生成 SVG 的能力有差异:
|
||||||
|
|
||||||
| 模型身份 | 路径 |
|
| 模型身份 | 路径 |
|
||||||
|----------|------|
|
|----------|------|
|
||||||
| Claude / Gemini / GPT / GLM | **SVG** — 精确控制坐标、颜色、透明度 |
|
| Claude / Gemini / GPT / GLM | **SVG** — 精确控制坐标、颜色、透明度 |
|
||||||
| Doubao / Seed / Other | **Mermaid** — 用 `gantt`、`flowchart` 等近似表达;确实无法用 Mermaid 表达时才回退到简单 SVG 矩形/线条 |
|
| Doubao / Seed / Other | **Mermaid** — 用 `pie`、`gantt` 等近似表达;确实无法用 Mermaid 表达时才回退到简单 SVG 矩形/线条 |
|
||||||
|
|
||||||
> **先自报身份再选路径**:在决定使用 SVG 之前,确认当前模型属于哪一类。不要跳过这一步。
|
> **先自报身份再选路径**:在决定使用 SVG 之前,确认当前模型属于哪一类。不要跳过这一步。
|
||||||
|
|
||||||
@@ -78,13 +73,13 @@ SVG 内的坐标相对于 whiteboard 自身左上角(0,0),与 slide 坐标
|
|||||||
|
|
||||||
### ⚠️ 设计品质要求
|
### ⚠️ 设计品质要求
|
||||||
|
|
||||||
在 slide 里嵌入 `<whiteboard>` 的目的是**表达原生 `<chart>` 或基础 `<shape>` 难以覆盖的视觉关系**,不是把标准数据图表手绘一遍。
|
在 slide 里嵌入 `<whiteboard>` 的目的是**提升视觉质量**,不是把数字堆进去。
|
||||||
|
|
||||||
- **不要只用矩形加文字应付**:通篇纯白底色 + 方块 + 黑字等于白做,这是不及格输出
|
- **不要只用矩形加文字应付**:通篇纯白底色 + 方块 + 黑字等于白做,这是不及格输出
|
||||||
- **非原生数据视觉必须有坐标系**:散点、漏斗等仍要有必要的坐标轴、刻度、数值标注或分段说明,不要只画点或色块
|
- **数据图表必须有坐标系**:坐标轴、网格线、数值标注缺一不可,不要只画柱子或点
|
||||||
- **字号必须有层级**:标题 ≠ 标签 ≠ 数值,混用同一字号会消灭视觉焦点
|
- **字号必须有层级**:标题 ≠ 标签 ≠ 数值,混用同一字号会消灭视觉焦点
|
||||||
- **配色要与 slide 主题呼应**:深色 slide 背景下图表用透明底或深色卡片;浅色背景下避免再加纯白底块
|
- **配色要与 slide 主题呼应**:深色 slide 背景下图表用透明底或深色卡片;浅色背景下避免再加纯白底块
|
||||||
- **每个 whiteboard 都是设计机会**:主动用圆角、半透明填充、清晰分组、节点状态等细节拉开与默认模板的差距
|
- **每个 whiteboard 都是设计机会**:主动用圆角、半透明填充、折线面积、点装饰等细节拉开与默认模板的差距
|
||||||
- **写 SVG 前先判断背景亮度**:背景亮度 < 30% 时,装饰元素"对比不足"比"过强"危害更大,宁重勿轻;
|
- **写 SVG 前先判断背景亮度**:背景亮度 < 30% 时,装饰元素"对比不足"比"过强"危害更大,宁重勿轻;
|
||||||
- **装饰层次用亮度跳跃,不用线性叠透明度**:`α=0.04→0.08→0.12` 的等差递增在深色底上几乎看不出差异(相邻层亮度差 ≈20);正确做法是非线性跳跃如 `0.10→0.40→0.70→1.0`,相邻层亮度差 ≥60。
|
- **装饰层次用亮度跳跃,不用线性叠透明度**:`α=0.04→0.08→0.12` 的等差递增在深色底上几乎看不出差异(相邻层亮度差 ≈20);正确做法是非线性跳跃如 `0.10→0.40→0.70→1.0`,相邻层亮度差 ≥60。
|
||||||
|
|
||||||
@@ -111,11 +106,11 @@ whiteboard 渲染时以**所有子元素的几何包围盒合并结果**为内
|
|||||||
|
|
||||||
| 元素 | 说明 | 典型用途 |
|
| 元素 | 说明 | 典型用途 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| `<rect>` | 矩形,支持 `rx` 圆角 | 卡片、进度条、分段色块 |
|
| `<rect>` | 矩形,支持 `rx` 圆角 | 柱图、卡片、进度条 |
|
||||||
| `<circle>` | 圆 | 节点、装饰点、环形图 |
|
| `<circle>` | 圆 | 节点、装饰点、环形图 |
|
||||||
| `<ellipse>` | 椭圆 | 自定义轮廓图形 |
|
| `<ellipse>` | 椭圆 | 自定义轮廓图形 |
|
||||||
| `<line>` | 直线 | 轴线、分隔线、连接线 |
|
| `<line>` | 直线 | 坐标轴、分隔线 |
|
||||||
| `<path>` | 任意路径(支持 Q/C 曲线) | 波浪、曲线、弧形 |
|
| `<path>` | 任意路径(支持 Q/C 曲线) | 波浪、折线、弧形 |
|
||||||
| `<text>` | 文本,支持中文 | 标签、数值 |
|
| `<text>` | 文本,支持中文 | 标签、数值 |
|
||||||
| `<polygon>` | 多边形 | 箭头、星形、面积填充 |
|
| `<polygon>` | 多边形 | 箭头、星形、面积填充 |
|
||||||
| `<g>` | 分组 | 批量变换、语义分组 |
|
| `<g>` | 分组 | 批量变换、语义分组 |
|
||||||
@@ -128,25 +123,27 @@ whiteboard 渲染时以**所有子元素的几何包围盒合并结果**为内
|
|||||||
---
|
---
|
||||||
### 元素计算
|
### 元素计算
|
||||||
|
|
||||||
SVG 中只要涉及批量定位、等间距排布或数据映射,**建议额外运行一个 Python 脚本把坐标算出来再填入 SVG**,而不是手动估值。适用范围包括散点、漏斗、装饰性点阵、等间距圆、重复图案等;普通柱状图、折线图、饼图仍应回到原生 `<chart>`。
|
SVG 中只要涉及批量定位、等间距排布或数据映射,**建议额外运行一个 Python 脚本把坐标算出来再填入 SVG**,而不是手动估值。适用范围不限于数据图表——装饰性点阵、等间距圆、重复图案同样适用。
|
||||||
|
|
||||||
> **主动去算**:写 SVG 之前先运行脚本,把输出当注释贴在 `<svg>` 开头,再照着填坐标。估值几乎每次都需要反复调整,跳过这步反而更慢。
|
> **主动去算**:写 SVG 之前先运行脚本,把输出当注释贴在 `<svg>` 开头,再照着填坐标。估值几乎每次都需要反复调整,跳过这步反而更慢。
|
||||||
|
|
||||||
**散点图 / 装饰性点阵范式**
|
**数据图表(柱状图范式)**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
W, H = 360, 260
|
W, H = 360, 260
|
||||||
origin_x, origin_y = 50, 216 # 左下角,SVG Y 轴向下
|
origin_x, origin_y = 50, 216 # 左下角,SVG Y 轴向下
|
||||||
cw, ch = 290, 184
|
cw, ch = 290, 184
|
||||||
|
|
||||||
points = [(12, 40), (28, 80), (45, 65)]
|
data, y_max = [120, 160, 90], 200
|
||||||
x_min, x_max, y_min, y_max = 0, 50, 0, 100
|
bar_w = int(cw / len(data) * 0.62)
|
||||||
for i, (xv, yv) in enumerate(points):
|
for i, v in enumerate(data):
|
||||||
x = round(origin_x + (xv - x_min) / (x_max - x_min) * cw)
|
cx = round(origin_x + (i + 0.5) * cw / len(data))
|
||||||
y = round(origin_y - (yv - y_min) / (y_max - y_min) * ch)
|
y = round(origin_y - v / y_max * ch)
|
||||||
print(f"point-{i}: cx={x} cy={y}")
|
print(f"bar-{i}: x={cx - bar_w//2} y={y} w={bar_w} h={round(origin_y - y)}")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
折线图:`x = origin_x + i/(n-1)*cw`,`y = origin_y - (v-y_min)/(y_max-y_min)*ch`。
|
||||||
|
|
||||||
**装饰性元素(等间距范式)**
|
**装饰性元素(等间距范式)**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -163,9 +160,9 @@ for i in range(n):
|
|||||||
```python
|
```python
|
||||||
# 每个元素登记 (x, y, w, h),含 stroke 外扩
|
# 每个元素登记 (x, y, w, h),含 stroke 外扩
|
||||||
elements = [
|
elements = [
|
||||||
(10, 20, 80, 160), # item-0
|
(10, 20, 80, 160), # bar-0
|
||||||
(107, 10, 80, 170), # item-1
|
(107, 10, 80, 170), # bar-1
|
||||||
(204, 40, 80, 140), # item-2
|
(204, 40, 80, 140), # bar-2
|
||||||
(0, 0, 300, 1), # x-axis
|
(0, 0, 300, 1), # x-axis
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -264,6 +261,7 @@ print(f"whiteboard width={wb_w} height={wb_h}")
|
|||||||
| 流程图 | `flowchart TD` / `flowchart LR` | 业务流程、决策树、工作流 |
|
| 流程图 | `flowchart TD` / `flowchart LR` | 业务流程、决策树、工作流 |
|
||||||
| 时序图 | `sequenceDiagram` | 系统交互、API 调用链 |
|
| 时序图 | `sequenceDiagram` | 系统交互、API 调用链 |
|
||||||
| 甘特图 | `gantt` | 项目计划、里程碑 |
|
| 甘特图 | `gantt` | 项目计划、里程碑 |
|
||||||
|
| 饼图 | `pie` | 占比数据 |
|
||||||
| 类图 | `classDiagram` | 对象关系、架构设计 |
|
| 类图 | `classDiagram` | 对象关系、架构设计 |
|
||||||
| ER 图 | `erDiagram` | 数据库结构 |
|
| ER 图 | `erDiagram` | 数据库结构 |
|
||||||
| 状态图 | `stateDiagram-v2` | 状态机、生命周期 |
|
| 状态图 | `stateDiagram-v2` | 状态机、生命周期 |
|
||||||
@@ -281,6 +279,7 @@ Mermaid 图表会自动撑满 whiteboard 区域。建议:
|
|||||||
|---------|-----------|------------|
|
|---------|-----------|------------|
|
||||||
| 流程图(5-8 节点) | 720-816 | 300-400 |
|
| 流程图(5-8 节点) | 720-816 | 300-400 |
|
||||||
| 时序图(3-5 参与者) | 720-816 | 320-420 |
|
| 时序图(3-5 参与者) | 720-816 | 320-420 |
|
||||||
|
| 饼图 | 500-600 | 300-360 |
|
||||||
| 甘特图 | 816 | 280-360 |
|
| 甘特图 | 816 | 280-360 |
|
||||||
| 思维导图 | 816 | 380-480 |
|
| 思维导图 | 816 | 380-480 |
|
||||||
|
|
||||||
@@ -308,7 +307,7 @@ Mermaid 语法包含 `[`、`>`、`-->`,不用 CDATA 直接写会破坏 XML 解
|
|||||||
- [ ] 文字 `y` 坐标为 baseline 位置,最小值 ≥ font-size(避免被裁切)
|
- [ ] 文字 `y` 坐标为 baseline 位置,最小值 ≥ font-size(避免被裁切)
|
||||||
|
|
||||||
**SVG 模式——视觉品质检查:**
|
**SVG 模式——视觉品质检查:**
|
||||||
- [ ] 非原生数据视觉有必要的坐标轴、网格线、数值标注或分段说明,没有"裸点"或无解释色块
|
- [ ] 坐标轴、网格线、数值标注齐全,没有"裸柱子"或"裸折线"
|
||||||
- [ ] 字号有层级:标题 > 数值 > 轴标签,非全部相同
|
- [ ] 字号有层级:标题 > 数值 > 轴标签,非全部相同
|
||||||
- [ ] 单一数据系列用同一颜色,多系列用不同颜色且对比充足
|
- [ ] 单一数据系列用同一颜色,多系列用不同颜色且对比充足
|
||||||
- [ ] 轴标签与图表元素互不遮挡,留有足够空间
|
- [ ] 轴标签与图表元素互不遮挡,留有足够空间
|
||||||
|
|||||||
@@ -119,34 +119,6 @@ Each slide must include:
|
|||||||
- `text_density`: `low`, `medium`, or `high`.
|
- `text_density`: `low`, `medium`, or `high`.
|
||||||
- `speaker_intent`: why the speaker needs this page and how it advances the story.
|
- `speaker_intent`: why the speaker needs this page and how it advances the story.
|
||||||
|
|
||||||
Optional slide fields:
|
|
||||||
|
|
||||||
- `chart_contract`: required when the page plan includes a standard data chart that `<chart>` supports. Use this shape:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"chart_contract": {
|
|
||||||
"required": true,
|
|
||||||
"render_as": "native_chart",
|
|
||||||
"chart_type": "line",
|
|
||||||
"data_source": "mock_placeholder",
|
|
||||||
"data_series_required": true,
|
|
||||||
"placeholder_label_required": true,
|
|
||||||
"manual_shape_fallback_allowed": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When `chart_contract.required == true`, XML generation must produce a `<chart>` element on that slide. A shape, line, polyline, or whiteboard approximation does not satisfy the plan.
|
|
||||||
|
|
||||||
`data_source` must be one of:
|
|
||||||
|
|
||||||
- `user_provided`: the user supplied concrete values, tables, CSV, or metric lists; use them and do not replace them with mock data.
|
|
||||||
- `mock_placeholder`: the user asked for a placeholder, template, example, or later-replaceable chart position; use mock data in native `<chart>`.
|
|
||||||
- `mock_required_by_intent`: the user did not provide concrete values but asked for data expression, charts, trends, comparisons, or distributions; use mock data in native `<chart>`.
|
|
||||||
|
|
||||||
`data_series_required` means the generated XML must include `<chartData>`. It does not require user-provided real-world values. When real values are unavailable but chart expression is part of the user's intent, write mock or placeholder values into native `<chart>` and label them clearly instead of switching to manual drawing primitives or metric blocks.
|
|
||||||
|
|
||||||
## Layout Vocabulary
|
## Layout Vocabulary
|
||||||
|
|
||||||
Use one of these `layout_type` values unless the user explicitly needs a custom structure:
|
Use one of these `layout_type` values unless the user explicitly needs a custom structure:
|
||||||
@@ -211,7 +183,6 @@ Use an object for one planned asset, an array for multiple real needs, or `asset
|
|||||||
- `purpose`: why this asset helps the page's key message.
|
- `purpose`: why this asset helps the page's key message.
|
||||||
- `suggested_query`: short future lookup hint only; do not execute it unless separately requested.
|
- `suggested_query`: short future lookup hint only; do not execute it unless separately requested.
|
||||||
- `fallback_if_missing`: concrete XML-native visual plan using shapes, labels, tables, whiteboard diagrams, or placeholder panels.
|
- `fallback_if_missing`: concrete XML-native visual plan using shapes, labels, tables, whiteboard diagrams, or placeholder panels.
|
||||||
- `chart_contract`: when `asset_type` is `chart` and the visual is a supported standard data chart, set this optional slide-level field so generation is locked to native `<chart>`.
|
|
||||||
|
|
||||||
For detailed rules and examples, read `asset-planning.md`.
|
For detailed rules and examples, read `asset-planning.md`.
|
||||||
|
|
||||||
@@ -219,7 +190,7 @@ Good examples:
|
|||||||
|
|
||||||
- `{"asset_type":"architecture_diagram","purpose":"Explain component relationships.","suggested_query":"service architecture diagram","fallback_if_missing":"Draw a component diagram with grouped boxes, connector arrows, and short labels."}`
|
- `{"asset_type":"architecture_diagram","purpose":"Explain component relationships.","suggested_query":"service architecture diagram","fallback_if_missing":"Draw a component diagram with grouped boxes, connector arrows, and short labels."}`
|
||||||
- `{"asset_type":"logo","purpose":"Identify the customer context.","suggested_query":"customer logo","fallback_if_missing":"Use a text label in a small badge."}`
|
- `{"asset_type":"logo","purpose":"Identify the customer context.","suggested_query":"customer logo","fallback_if_missing":"Use a text label in a small badge."}`
|
||||||
- `{"asset_type":"chart","purpose":"Show adoption trend.","suggested_query":"monthly adoption trend chart","fallback_if_missing":"Render a native `<chart>` using the provided series when available; otherwise render a native `<chart>` with mock placeholder values and label it as 模拟数据,仅占位,待替换真实数据."}`
|
- `{"asset_type":"chart","purpose":"Show adoption trend.","suggested_query":"monthly adoption trend chart","fallback_if_missing":"Draw a simple trend line chart with axis labels and data points."}`
|
||||||
|
|
||||||
## XML Generation Contract
|
## XML Generation Contract
|
||||||
|
|
||||||
@@ -230,7 +201,6 @@ Before writing each slide XML, map the plan fields to concrete decisions:
|
|||||||
- `visual_focus` determines the largest visual region or emphasized object.
|
- `visual_focus` determines the largest visual region or emphasized object.
|
||||||
- `text_density` caps visible text volume.
|
- `text_density` caps visible text volume.
|
||||||
- `asset_need` informs placeholder diagrams, icons, charts, screenshots, or shape-based fallback visuals only. Missing real assets must use `fallback_if_missing`, not blank regions.
|
- `asset_need` informs placeholder diagrams, icons, charts, screenshots, or shape-based fallback visuals only. Missing real assets must use `fallback_if_missing`, not blank regions.
|
||||||
- `chart_contract` locks supported standard data charts to native `<chart>` output. Manual approximations are allowed only when the planned chart type is unsupported by `<chart>` or when the visual is explicitly non-data/decorative.
|
|
||||||
|
|
||||||
After creating the PPT, fetch the presentation and verify:
|
After creating the PPT, fetch the presentation and verify:
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -3018,7 +3018,7 @@
|
|||||||
|
|
||||||
子元素(mermaid 与 svg 二选一):
|
子元素(mermaid 与 svg 二选一):
|
||||||
- mermaid: Mermaid 源码文本, 可使用 CDATA 包裹
|
- mermaid: Mermaid 源码文本, 可使用 CDATA 包裹
|
||||||
适用场景: 流程图、时序图、思维导图、类图、甘特图、ER 图、用户旅程等结构图
|
适用场景: 流程图、时序图、思维导图、类图、甘特图、饼图等结构化图表
|
||||||
特点: 用简短的文本声明描述图表逻辑, 由渲染引擎自动布局, 无需手动计算坐标
|
特点: 用简短的文本声明描述图表逻辑, 由渲染引擎自动布局, 无需手动计算坐标
|
||||||
示例: <mermaid><![CDATA[flowchart TD\n A[开始] --> B[结束]]]></mermaid>
|
示例: <mermaid><![CDATA[flowchart TD\n A[开始] --> B[结束]]]></mermaid>
|
||||||
- svg: SVG 内容
|
- svg: SVG 内容
|
||||||
|
|||||||
@@ -263,56 +263,7 @@
|
|||||||
- `<chartLegend>`
|
- `<chartLegend>`
|
||||||
- `<chartTooltip>`
|
- `<chartTooltip>`
|
||||||
|
|
||||||
完整图表类型覆盖示例见 [slides_chart_demo.xml](slides_chart_demo.xml),其中包含柱状、条形、折线、面积、饼 / 环、雷达等原生 `<chart>` 示例,以及散点、气泡、漏斗、帕累托、瀑布等 `<whiteboard>` SVG 图表示例。
|
如果要写图表 XML,建议直接以 XSD 为准,不要自行发明更简化的 chart DSL。
|
||||||
|
|
||||||
组合图示例(来自 [slides_chart_demo.xml](slides_chart_demo.xml)):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<chart width="556" height="350" topLeftX="42" topLeftY="132">
|
|
||||||
<chartPlotArea>
|
|
||||||
<chartPlot type="combo">
|
|
||||||
<chartExtra/>
|
|
||||||
<chartSeriesList>
|
|
||||||
<chartSeries index="1" comboType="column"/>
|
|
||||||
<chartSeries index="2" comboType="line" yAxisPosition="right">
|
|
||||||
<chartTooltip format="0%"/>
|
|
||||||
</chartSeries>
|
|
||||||
</chartSeriesList>
|
|
||||||
</chartPlot>
|
|
||||||
<chartAxes>
|
|
||||||
<chartAxis type="x">
|
|
||||||
<chartLabel fontSize="10"/>
|
|
||||||
</chartAxis>
|
|
||||||
<chartAxis type="y" position="left">
|
|
||||||
<chartGridLine color="rgb(226, 232, 240)"/>
|
|
||||||
<chartLabel fontSize="10"/>
|
|
||||||
</chartAxis>
|
|
||||||
<chartAxis type="y" position="right">
|
|
||||||
<chartLabel fontSize="10" format="0%"/>
|
|
||||||
</chartAxis>
|
|
||||||
</chartAxes>
|
|
||||||
</chartPlotArea>
|
|
||||||
<chartLegend position="bottom" fontSize="11"/>
|
|
||||||
<chartData>
|
|
||||||
<dim1>
|
|
||||||
<chartField name="季度">24Q1,24Q2,24Q3,24Q4,25Q1,25Q2,25Q3,25Q4</chartField>
|
|
||||||
</dim1>
|
|
||||||
<dim2>
|
|
||||||
<chartField name="营收">180,195,210,245,220,238,258,296</chartField>
|
|
||||||
<chartField name="增速">0.08,0.12,0.15,0.18,0.22,0.22,0.23,0.21</chartField>
|
|
||||||
</dim2>
|
|
||||||
</chartData>
|
|
||||||
<chartTitle fontSize="12" color="rgba(15, 30, 58, 1)" bold="true">营收(亿美元, 左轴) · 同比增速(%, 右轴)</chartTitle>
|
|
||||||
<chartStyle>
|
|
||||||
<chartBackground color="rgba(0, 0, 0, 0)"/>
|
|
||||||
<chartBorder color="rgb(222, 224, 227)" width="0"/>
|
|
||||||
<chartColorTheme>
|
|
||||||
<color value="rgb(28, 71, 120)"/>
|
|
||||||
<color value="rgb(240, 129, 54)"/>
|
|
||||||
</chartColorTheme>
|
|
||||||
</chartStyle>
|
|
||||||
</chart>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 样式元素
|
## 样式元素
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
|
|||||||
### whiteboard
|
### whiteboard
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<!-- SVG 模式:<chart> 不支持的图表或自定义视觉、装饰元素 -->
|
<!-- SVG 模式:数据图表、装饰元素 -->
|
||||||
<whiteboard topLeftX="580" topLeftY="120" width="340" height="280">
|
<whiteboard topLeftX="580" topLeftY="120" width="340" height="280">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="60" y="80" width="40" height="140" rx="3" fill="rgba(59,130,246,0.85)"/>
|
<rect x="60" y="80" width="40" height="140" rx="3" fill="rgba(59,130,246,0.85)"/>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ metadata:
|
|||||||
|
|
||||||
## 快速决策
|
## 快速决策
|
||||||
|
|
||||||
- 用户要**整理 / 盘点 / 归类 / 重构知识库、个人文档库、文档库目录或 Wiki 节点结构**,或要生成整理方案、目标目录树、移动计划时,不要只使用 Wiki 节点 API。必须先阅读 [`../lark-drive/references/lark-drive-workflow.md`](../lark-drive/references/lark-drive-workflow.md),再按其中 `Workflow Registry` 进入 [`knowledge_organize`](../lark-drive/references/lark-drive-workflow-knowledge-organize.md) workflow;该 workflow 负责 Drive / Wiki / 个人文档库的统一入口解析、资源盘点、分类计划、写前确认和结果验证。
|
- 用户要**整理 / 盘点 / 归类 / 重构知识库、个人文档库、文档库目录或 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/<token>`),且后续要查成员/加成员/删成员:先调用 `lark-cli wiki spaces get_node --params '{"token":"<wiki_token>"}'` 获取 `space_id`,后续成员接口统一使用 `space_id`。
|
- 用户给的是知识库 URL(`.../wiki/<token>`),且后续要查成员/加成员/删成员:先调用 `lark-cli wiki spaces get_node --params '{"token":"<wiki_token>"}'` 获取 `space_id`,后续成员接口统一使用 `space_id`。
|
||||||
- 用户要**删除**知识空间(`wiki +delete-space`)但只给了名称或 URL:**不能**把名称 / URL 原样传给 `--space-id`,必须先解析出真实 `space_id`。解析方式:
|
- 用户要**删除**知识空间(`wiki +delete-space`)但只给了名称或 URL:**不能**把名称 / URL 原样传给 `--space-id`,必须先解析出真实 `space_id`。解析方式:
|
||||||
- URL(`.../wiki/<token>`):`lark-cli wiki spaces get_node --params '{"token":"<wiki_token>"}' --format json`,读 `data.node.space_id`。
|
- URL(`.../wiki/<token>`):`lark-cli wiki spaces get_node --params '{"token":"<wiki_token>"}' --format json`,读 `data.node.space_id`。
|
||||||
|
|||||||
Reference in New Issue
Block a user