Compare commits

..

1 Commits

Author SHA1 Message Date
zhanghuanxu
461c11943d feat:edit slides 2026-07-01 17:46:00 +08:00
129 changed files with 24704 additions and 6712 deletions

View File

@@ -2,40 +2,6 @@
All notable changes to this project will be documented in this file.
## [v1.0.62] - 2026-07-01
### Features
- **vc**: Add meeting message send shortcut (#1643)
- **doc**: Add document word statistics helper (#1697)
- **cli**: Interactive upgrade prompt for bare `lark-cli` invocation (#1498)
- **install**: Fail closed when `checksums.txt` is missing during install (#1503)
### Bug Fixes
- **drive**: Improve batch failure handling for push/pull/sync (#1703)
- **base**: Support JSON array input for field create (#1661)
- **task**: Expose completion state in `my tasks` output (#1641)
- **cli**: Reduce public content credential false positives (#1700)
## [v1.0.61] - 2026-06-30
### Features
- **apps**: Add `db`, `file`, `openapi-key` and observability shortcuts (#1596)
- **identity**: Add `whoami` command showing effective identity (#1666)
- **docs**: Add reference map flags (#1547)
### Bug Fixes
- **identity**: Correct identity diagnosis under external credential providers (#1693)
- **cli**: Harden git credential error handling (#1676)
### Documentation
- **doc**: Guide document copy skill usage (#1673)
- **doc**: Fix lark-doc media token examples (#1662)
## [v1.0.60] - 2026-06-29
### Features
@@ -1333,8 +1299,6 @@ Bundled AI agent skills for intelligent assistance:
- Bilingual documentation (English & Chinese).
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
[v1.0.62]: https://github.com/larksuite/cli/releases/tag/v1.0.62
[v1.0.61]: https://github.com/larksuite/cli/releases/tag/v1.0.61
[v1.0.60]: https://github.com/larksuite/cli/releases/tag/v1.0.60
[v1.0.59]: https://github.com/larksuite/cli/releases/tag/v1.0.59
[v1.0.58]: https://github.com/larksuite/cli/releases/tag/v1.0.58

View File

@@ -214,9 +214,6 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
groupRootCommands(rootCmd)
installUnknownSubcommandGuard(rootCmd)
// Bare `lark-cli` in an interactive terminal offers an interactive upgrade
// before printing help; non-bare invocations and non-TTY are unaffected.
installRootUpgradePrompt(f, rootCmd)
if mode := f.ResolveStrictMode(ctx); mode.IsActive() && !cfg.skipStrictMode {
pruneForStrictMode(rootCmd, mode)

View File

@@ -129,10 +129,7 @@ func doctorRun(opts *DoctorOptions) error {
if diagnostics.Bot.Available || diagnostics.User.Available {
checks = append(checks, pass("identity_ready", "at least one identity is available"))
} else {
// No hint: this only summarizes the two checks above, which already carry
// the source-appropriate remediation. A command here would be redundant,
// or wrong (`auth status` is blocked under an external provider).
checks = append(checks, fail("identity_ready", "no usable bot or user identity is available", ""))
checks = append(checks, fail("identity_ready", "no usable bot or user identity is available", "run: lark-cli auth status --verify"))
}
// ── 4 & 5. Endpoint reachability ──

View File

@@ -4,19 +4,14 @@
package doctor
import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"github.com/spf13/cobra"
extcred "github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
)
func TestNewCmdDoctor_FlagParsing(t *testing.T) {
@@ -145,84 +140,14 @@ func TestDoctorRun_SplitsBotAndMissingUserIdentity(t *testing.T) {
}
func assertCheck(t *testing.T, checks []checkResult, name, status string) {
t.Helper()
if got := findCheck(t, checks, name); got.Status != status {
t.Fatalf("%s status = %q, want %q", name, got.Status, status)
}
}
func findCheck(t *testing.T, checks []checkResult, name string) checkResult {
t.Helper()
for _, check := range checks {
if check.Name == name {
return check
if check.Status != status {
t.Fatalf("%s status = %q, want %q", name, check.Status, status)
}
return
}
}
t.Fatalf("check %q not found in %#v", name, checks)
return checkResult{}
}
type fakeExtProvider struct {
name string
account *extcred.Account
}
func (p *fakeExtProvider) Name() string { return p.name }
func (p *fakeExtProvider) ResolveAccount(context.Context) (*extcred.Account, error) {
return p.account, nil
}
func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*extcred.Token, error) {
return nil, nil
}
// Under an external credential provider with no usable identity, the
// identity_ready hint must not point at `auth status` (blocked there); the
// per-identity checks already carry the source-appropriate escalation.
func TestDoctor_ExternalProvider_IdentityReadyHintNotBlockedCommand(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
CurrentApp: "default",
Apps: []core.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu}},
}); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
// Provider serves neither identity: bot unsupported, user supported but not
// signed in → both unavailable → identity_ready fails.
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser)}
cred := credential.NewCredentialProvider(
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}},
nil, nil,
func() (*http.Client, error) { return nil, nil },
)
out := &bytes.Buffer{}
f := &cmdutil.Factory{
Config: func() (*core.CliConfig, error) { return cfg, nil },
Credential: cred,
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
}
if err := doctorRun(&DoctorOptions{Factory: f, Ctx: context.Background(), Offline: true}); err == nil {
t.Fatalf("doctorRun() = nil, want failure when no identity is available")
}
var got struct {
Checks []checkResult `json:"checks"`
}
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal() error = %v\n%s", err, out.String())
}
ready := findCheck(t, got.Checks, "identity_ready")
if ready.Status != "fail" {
t.Fatalf("identity_ready status = %q, want fail", ready.Status)
}
// The summary defers to the per-identity checks; it carries no hint of its
// own (a command here would be wrong under an external provider).
if ready.Hint != "" {
t.Fatalf("identity_ready should carry no hint, got %q", ready.Hint)
}
user := findCheck(t, got.Checks, "user_identity")
if !strings.Contains(user.Hint, "external") || strings.Contains(user.Hint, "auth login") {
t.Fatalf("user_identity hint not external-appropriate: %q", user.Hint)
}
}

View File

@@ -4,13 +4,11 @@
package cmd
import (
"context"
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/output"
"github.com/spf13/cobra"
)
@@ -82,40 +80,6 @@ func TestFlagDidYouMean_UnknownFlagSuggestsAndListsValid(t *testing.T) {
}
}
func TestFlagDidYouMean_WikiNodeGetSuggestsNodeToken(t *testing.T) {
t.Setenv("LARKSUITE_CLI_REMOTE_META", "off")
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
root := Build(context.Background(), cmdutil.InvocationContext{}, WithoutPlugins())
root.SetArgs([]string{
"wiki", "+node-get",
"--node", "https://feishu.cn/wiki/wikcnABC",
"--as", "user",
})
err := root.Execute()
var verr *errs.ValidationError
if !errors.As(err, &verr) {
t.Fatalf("expected *errs.ValidationError, got %T (%v)", err, err)
}
if len(verr.Params) != 1 || verr.Params[0].Name != "--node" {
t.Fatalf("Params = %v, want one entry named --node", verr.Params)
}
found := false
for _, s := range verr.Params[0].Suggestions {
if s == "--node-token" {
found = true
break
}
}
if !found {
t.Fatalf("Params[0].Suggestions = %v, want --node-token", verr.Params[0].Suggestions)
}
if !strings.Contains(verr.Hint, "--node-token") {
t.Fatalf("hint = %q, want --node-token", verr.Hint)
}
}
func TestFlagDidYouMean_OtherErrorStaysGeneric(t *testing.T) {
c := &cobra.Command{Use: "demo"}
err := flagDidYouMean(c, errors.New("flag needs an argument: --find"))

View File

@@ -1,90 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package cmd
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/update"
"github.com/spf13/cobra"
)
// runRootUpgrade locates the registered `update` subcommand and runs it, so the
// interactive root-command upgrade reuses exactly `lark-cli update` behavior
// (install-method detection, output, error handling). Package-level var so
// tests can stub it and avoid real network / self-update.
var runRootUpgrade = func(cmd *cobra.Command) {
for _, c := range cmd.Root().Commands() {
if c.Name() == "update" && c.RunE != nil {
_ = c.RunE(c, nil) // update prints its own output/errors; swallow here
return
}
}
}
// isBareRootInvocation reports whether this is a bare `lark-cli` (no subcommand,
// no flags) — the only invocation that triggers the interactive upgrade prompt.
// Mirrors unknownSubcommandRunE's "bare group prints help" branch: args empty
// AND no flag tokens in the raw invocation.
func isBareRootInvocation(args []string) bool {
return len(args) == 0 && len(flagTokensInArgs(rawInvocationArgs)) == 0
}
// readYes reads one line and reports whether it is an affirmative y/yes.
// EOF / empty / anything else → false (default No, matching the [y/N] prompt).
func readYes(r io.Reader) bool {
line, _ := bufio.NewReader(r).ReadString('\n')
switch strings.ToLower(strings.TrimSpace(line)) {
case "y", "yes":
return true
default:
return false
}
}
// offerRootUpgrade prompts for an interactive upgrade when running bare
// `lark-cli` in an interactive terminal with a cached newer version. Every
// failure is swallowed — it must never affect help output or the exit code.
func offerRootUpgrade(f *cmdutil.Factory, cmd *cobra.Command) {
ios := f.IOStreams
// Gates 1/2/3: need to read stdin AND show the prompt on stderr, and require
// stdout TTY too so this only fires in a pure foreground terminal session.
if !ios.IsTerminal || !ios.OutIsTerminal || !ios.StderrIsTerminal {
return
}
// Gate 4: cached newer version. CheckCached applies opt-out (shouldSkip)
// and the IsNewer/semver validation chain; it reads the on-disk cache that
// the 24h-throttled RefreshCache maintains (CheckCached itself has no TTL).
info := update.CheckCached(build.Version)
if info == nil {
return
}
fmt.Fprintf(ios.ErrOut, "lark-cli %s available (current %s). Upgrade now? [y/N]: ", info.Latest, info.Current)
if !readYes(ios.In) {
return
}
runRootUpgrade(cmd)
}
// installRootUpgradePrompt wraps the root command's RunE (set to
// unknownSubcommandRunE by installUnknownSubcommandGuard) so a bare `lark-cli`
// invocation offers an interactive upgrade before printing help. Non-bare
// invocations are passed straight through, unchanged.
func installRootUpgradePrompt(f *cmdutil.Factory, root *cobra.Command) {
inner := root.RunE
if inner == nil {
return
}
root.RunE = func(cmd *cobra.Command, args []string) error {
if isBareRootInvocation(args) {
offerRootUpgrade(f, cmd)
}
return inner(cmd, args)
}
}

View File

@@ -1,191 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package cmd
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/cobra"
)
func writeUpdateState(t *testing.T, dir, latest string) {
t.Helper()
data := fmt.Sprintf(`{"latest_version":%q,"checked_at":%d}`, latest, time.Now().Unix())
if err := os.WriteFile(filepath.Join(dir, "update-state.json"), []byte(data), 0o644); err != nil {
t.Fatal(err)
}
}
func TestReadYes(t *testing.T) {
cases := map[string]bool{
"y\n": true, "Y\n": true, "yes\n": true, "YES\n": true, " y \n": true,
"n\n": false, "\n": false, "": false, "nope\n": false, "yeah\n": false,
}
for in, want := range cases {
if got := readYes(strings.NewReader(in)); got != want {
t.Errorf("readYes(%q) = %v, want %v", in, got, want)
}
}
}
func TestIsBareRootInvocation(t *testing.T) {
orig := rawInvocationArgs
t.Cleanup(func() { rawInvocationArgs = orig })
rawInvocationArgs = nil
if !isBareRootInvocation([]string{}) {
t.Error("empty args + no raw flag tokens should be bare")
}
rawInvocationArgs = []string{"--profile", "x"}
if isBareRootInvocation([]string{}) {
t.Error("flag token present → not bare")
}
rawInvocationArgs = nil
if isBareRootInvocation([]string{"im"}) {
t.Error("positional arg → not bare")
}
}
func TestOfferRootUpgrade(t *testing.T) {
origV := build.Version
build.Version = "1.0.0" // release version so shouldSkip()==false
t.Cleanup(func() { build.Version = origV })
origRun := runRootUpgrade
t.Cleanup(func() { runRootUpgrade = origRun })
// This test builds a Factory literal (no NewDefault), so it never runs
// workspace detection; pin the process-global workspace to Local so
// statePath() resolves under LARKSUITE_CLI_CONFIG_DIR rather than a stale
// subdir inherited from a prior test in the package.
origWS := core.CurrentWorkspace()
t.Cleanup(func() { core.SetCurrentWorkspace(origWS) })
core.SetCurrentWorkspace(core.WorkspaceLocal)
cases := []struct {
name string
in, out, err bool
input string
latest string // "" → no state file (CheckCached nil)
optOut bool
wantPrompt, wantRun bool
}{
{"all-tty+y", true, true, true, "y\n", "2.0.0", false, true, true},
{"all-tty+yes", true, true, true, "yes\n", "2.0.0", false, true, true},
{"all-tty+n", true, true, true, "n\n", "2.0.0", false, true, false},
{"all-tty+empty", true, true, true, "\n", "2.0.0", false, true, false},
{"all-tty+eof", true, true, true, "", "2.0.0", false, true, false},
{"stdin-not-tty", false, true, true, "y\n", "2.0.0", false, false, false},
{"stdout-not-tty", true, false, true, "y\n", "2.0.0", false, false, false},
{"stderr-not-tty", true, true, false, "y\n", "2.0.0", false, false, false},
{"no-newer-version", true, true, true, "y\n", "", false, false, false},
{"already-latest", true, true, true, "y\n", "1.0.0", false, false, false}, // post-upgrade: current == cached latest → no prompt
{"cache-older-than-current", true, true, true, "y\n", "0.9.0", false, false, false},
{"opt-out", true, true, true, "y\n", "2.0.0", true, false, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
// Clear env that update.shouldSkip treats as "suppress" so the
// test is deterministic regardless of host (GitHub Actions sets
// CI=true, which would otherwise suppress the prompt).
t.Setenv("CI", "")
t.Setenv("BUILD_NUMBER", "")
t.Setenv("RUN_ID", "")
t.Setenv("LARKSUITE_CLI_NO_UPDATE_NOTIFIER", "")
if tc.latest != "" {
writeUpdateState(t, dir, tc.latest)
}
if tc.optOut {
t.Setenv("LARKSUITE_CLI_NO_UPDATE_NOTIFIER", "1")
}
called := false
runRootUpgrade = func(*cobra.Command) { called = true }
var errBuf bytes.Buffer
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
In: strings.NewReader(tc.input),
Out: &bytes.Buffer{},
ErrOut: &errBuf,
IsTerminal: tc.in,
OutIsTerminal: tc.out,
StderrIsTerminal: tc.err,
}}
offerRootUpgrade(f, &cobra.Command{})
gotPrompt := strings.Contains(errBuf.String(), "available")
if gotPrompt != tc.wantPrompt {
t.Errorf("prompt: got %v want %v (stderr=%q)", gotPrompt, tc.wantPrompt, errBuf.String())
}
if called != tc.wantRun {
t.Errorf("runRootUpgrade called: got %v want %v", called, tc.wantRun)
}
})
}
}
func TestInstallRootUpgradePromptPreservesInner(t *testing.T) {
orig := rawInvocationArgs
t.Cleanup(func() { rawInvocationArgs = orig })
rawInvocationArgs = nil
innerCalls := 0
root := &cobra.Command{Use: "lark-cli"}
root.RunE = func(cmd *cobra.Command, args []string) error { innerCalls++; return nil }
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
In: strings.NewReader(""), Out: &bytes.Buffer{}, ErrOut: &bytes.Buffer{},
}}
installRootUpgradePrompt(f, root)
if err := root.RunE(root, []string{}); err != nil {
t.Fatalf("bare RunE err = %v", err)
}
if err := root.RunE(root, []string{"im"}); err != nil {
t.Fatalf("non-bare RunE err = %v", err)
}
if innerCalls != 2 {
t.Errorf("inner RunE should run for both bare and non-bare, got %d", innerCalls)
}
}
// TestRunRootUpgradeDispatchesToUpdate covers the real runRootUpgrade dispatch
// path (not the stub used elsewhere): from any command it must locate the
// registered "update" subcommand via cmd.Root() and invoke its RunE.
func TestRunRootUpgradeDispatchesToUpdate(t *testing.T) {
root := &cobra.Command{Use: "lark-cli"}
ran := 0
root.AddCommand(&cobra.Command{Use: "update", RunE: func(*cobra.Command, []string) error { ran++; return nil }})
child := &cobra.Command{Use: "im"}
root.AddCommand(child)
runRootUpgrade(child) // child.Root() resolves to root, which has "update"
if ran != 1 {
t.Errorf("runRootUpgrade should locate and run update's RunE once, got %d", ran)
}
}
// TestInstallRootUpgradePromptNilInnerNoop covers the inner == nil guard:
// when root has no RunE, installRootUpgradePrompt must not wrap it.
func TestInstallRootUpgradePromptNilInnerNoop(t *testing.T) {
root := &cobra.Command{Use: "lark-cli"} // RunE is nil
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
In: strings.NewReader(""), Out: &bytes.Buffer{}, ErrOut: &bytes.Buffer{},
}}
installRootUpgradePrompt(f, root)
if root.RunE != nil {
t.Error("installRootUpgradePrompt must not wrap a nil RunE (inner==nil guard)")
}
}

View File

@@ -5,6 +5,8 @@ package whoami
import (
"context"
"fmt"
"io"
"github.com/spf13/cobra"
@@ -15,13 +17,6 @@ import (
)
// whoamiResult is the structured output of `lark-cli whoami`.
//
// The self-vs-delegated distinction is carried by `identity`: a bot identity is
// the app acting as itself; a user identity is the app acting *on behalf of* a
// person (calls are attributed to that user, who is not necessarily present).
// onBehalfOf only *names* that person and so appears only once a user is
// resolved — a user identity that is not signed in still has identity "user"
// but no onBehalfOf yet. Do not read "no onBehalfOf" as "self"; read `identity`.
type whoamiResult struct {
Profile string `json:"profile"`
AppID string `json:"appId"`
@@ -31,44 +26,34 @@ type whoamiResult struct {
IdentitySource string `json:"identitySource"`
Available bool `json:"available"`
TokenStatus string `json:"tokenStatus"`
OnBehalfOf *delegatedUser `json:"onBehalfOf,omitempty"`
OpenID string `json:"openId,omitempty"`
UserName string `json:"userName,omitempty"`
Hint string `json:"hint,omitempty"`
}
// delegatedUser is the user a user-identity acts on behalf of.
type delegatedUser struct {
UserName string `json:"userName,omitempty"`
OpenID string `json:"openId,omitempty"`
}
// Options holds inputs for the whoami command.
type Options struct {
Factory *cmdutil.Factory
As string
JSON bool
}
// NewCmdWhoami creates the top-level whoami command. It reports the identity
// that the next API call would actually use (resolved via Factory.ResolveAs),
// together with the active profile, app, and token status. Output is always
// JSON — whoami is consumed by agents. With the built-in credential path it is
// local-only; when an external credential provider manages tokens, resolving
// the identity may contact that provider.
// together with the active profile, app, and token status. It is local-only:
// no network calls are made.
func NewCmdWhoami(f *cmdutil.Factory) *cobra.Command {
opts := &Options{Factory: f}
cmd := &cobra.Command{
Use: "whoami",
Short: "Show the current effective identity, app, profile, and token status (JSON)",
Short: "Show the current effective identity, app, profile, and token status",
RunE: func(cmd *cobra.Command, args []string) error {
return whoamiRun(cmd, opts)
},
}
cmdutil.DisableAuthCheck(cmd)
cmdutil.AddAPIIdentityFlag(context.Background(), cmd, f, &opts.As)
// Output is always JSON. Accept (and ignore) --json so existing
// `whoami --json` callers don't break; hide it to avoid implying a non-JSON
// mode exists.
cmd.Flags().Bool("json", true, "deprecated: output is always JSON")
_ = cmd.Flags().MarkHidden("json")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "structured JSON output")
cmdutil.SetRisk(cmd, "read")
return cmd
}
@@ -82,11 +67,10 @@ func whoamiRun(cmd *cobra.Command, opts *Options) error {
ctx := cmd.Context()
flagAs := core.Identity(opts.As)
as := f.ResolveAs(ctx, cmd, flagAs)
// Validate as a real API call does (strict mode, then identity) so whoami
// can't preview an identity the next call would refuse.
if err := f.CheckStrictMode(ctx, as); err != nil {
return err
}
// Reject an explicit --as that does not resolve to a usable identity, so a
// typo like `--as admin` fails clearly instead of echoing back a bogus
// identity. Keeps the §5.1 invariant (identity is always user or bot) and
// matches how api/service/shortcut commands validate the resolved identity.
if err := f.CheckIdentity(as, []string{"user", "bot"}); err != nil {
return err
}
@@ -98,7 +82,11 @@ func whoamiRun(cmd *cobra.Command, opts *Options) error {
)
diag := identitydiag.Diagnose(ctx, f, cfg, false)
res := buildResult(cfg, as, source, diag)
output.PrintJson(f.IOStreams.Out, res)
if opts.JSON {
output.PrintJson(f.IOStreams.Out, res)
return nil
}
formatPretty(f.IOStreams.Out, res)
return nil
}
@@ -106,18 +94,17 @@ func whoamiRun(cmd *cobra.Command, opts *Options) error {
// Mirrors Factory.ResolveAs precedence: explicit flag wins; otherwise an
// auto-detected result means auto-detect; otherwise a strict-mode forced
// identity means strict-mode; otherwise it came from configured default-as.
// Values are snake_case to match the other enum fields (e.g. tokenStatus).
func resolveSource(changedAs bool, flagAs core.Identity, autoDetected bool, strictForced core.Identity) string {
if changedAs && (flagAs == core.AsUser || flagAs == core.AsBot) {
return "flag"
}
if autoDetected {
return "auto_detect"
return "auto-detect"
}
if strictForced != "" {
return "strict_mode"
return "strict-mode"
}
return "default_as"
return "default-as"
}
// buildResult maps the resolved identity and local diagnostics into the output.
@@ -135,29 +122,46 @@ func buildResult(cfg *core.CliConfig, as core.Identity, source string, diag iden
Identity: string(as),
IdentitySource: source,
}
// Use the diagnosed hint as-is: it is tailored to the credential source, so
// it never says "auth login" when that is blocked under an external provider.
switch as {
case core.AsBot:
res.Available = diag.Bot.Available
res.TokenStatus = diag.Bot.Status
if !diag.Bot.Available {
res.Hint = diag.Bot.Hint
res.Hint = "Bot identity not configured. Set app secret or bot token (see `lark-cli config --help`)."
}
default: // user
res.Available = diag.User.Available
// Use Status (not the raw TokenStatus) so the vocab matches the bot
// branch: "ready" means usable for both. available stays the canonical
// usable signal; tokenStatus is the readable state behind it.
res.TokenStatus = diag.User.Status
// Set onBehalfOf only when a user is actually resolved; an unresolved
// user identity (not signed in) has no one to act on behalf of yet.
if diag.User.UserName != "" || diag.User.OpenID != "" {
res.OnBehalfOf = &delegatedUser{UserName: diag.User.UserName, OpenID: diag.User.OpenID}
res.OpenID = diag.User.OpenID
res.UserName = diag.User.UserName
res.TokenStatus = diag.User.TokenStatus
if res.TokenStatus == "" {
res.TokenStatus = "missing"
}
if !diag.User.Available {
res.Hint = diag.User.Hint
res.Hint = "No usable user token. Run `lark-cli auth login`."
}
}
return res
}
// formatPretty writes the human-readable one-glance summary.
func formatPretty(w io.Writer, r *whoamiResult) {
fmt.Fprintf(w, "Profile: %s (%s, %s)\n", r.Profile, r.AppID, r.Brand)
fmt.Fprintf(w, "Identity: %s (%s)\n", r.Identity, r.IdentitySource)
if r.Identity == string(core.AsUser) && r.UserName != "" {
if r.OpenID != "" {
fmt.Fprintf(w, "User: %s (%s)\n", r.UserName, r.OpenID)
} else {
fmt.Fprintf(w, "User: %s\n", r.UserName)
}
}
token := r.TokenStatus
if !r.Available && r.Hint != "" {
token = r.TokenStatus + " — " + r.Hint
}
// Write the label and value as separate %s args rather than one combined
// literal. A single label-colon-value literal trips the public-content
// credential scanner as a false-positive credential assignment; splitting
// the args avoids it while producing identical output.
fmt.Fprintf(w, "%s%s\n", "Token: ", token)
}

View File

@@ -5,19 +5,15 @@ package whoami
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/larksuite/cli/errs"
extcred "github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/identitydiag"
)
@@ -32,10 +28,10 @@ func TestResolveSource(t *testing.T) {
}{
{"explicit flag user", true, core.AsUser, false, "", "flag"},
{"explicit flag bot", true, core.AsBot, false, "", "flag"},
{"flag auto falls through to auto-detect", true, core.AsAuto, true, "", "auto_detect"},
{"auto detected", false, "", true, "", "auto_detect"},
{"strict mode", false, "", false, core.AsBot, "strict_mode"},
{"default_as", false, "", false, "", "default_as"},
{"flag auto falls through to auto-detect", true, core.AsAuto, true, "", "auto-detect"},
{"auto detected", false, "", true, "", "auto-detect"},
{"strict mode", false, "", false, core.AsBot, "strict-mode"},
{"default-as", false, "", false, "", "default-as"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -50,19 +46,18 @@ func TestResolveSource(t *testing.T) {
func TestBuildResult_UserValid(t *testing.T) {
cfg := &core.CliConfig{ProfileName: "my-app", AppID: "cli_x", Brand: core.BrandLark, DefaultAs: core.AsAuto}
diag := identitydiag.Result{
User: identitydiag.Identity{Available: true, Status: "ready", TokenStatus: "valid", OpenID: "ou_x", UserName: "Alice"},
User: identitydiag.Identity{Available: true, TokenStatus: "valid", OpenID: "ou_x", UserName: "Alice"},
}
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
r := buildResult(cfg, core.AsUser, "auto-detect", diag)
if r.Identity != "user" || r.IdentitySource != "auto_detect" {
if r.Identity != "user" || r.IdentitySource != "auto-detect" {
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
}
// tokenStatus mirrors the unified Status vocab ("ready"), not the raw "valid".
if !r.Available || r.TokenStatus != "ready" {
if !r.Available || r.TokenStatus != "valid" {
t.Fatalf("available=%v status=%q", r.Available, r.TokenStatus)
}
if r.OnBehalfOf == nil || r.OnBehalfOf.OpenID != "ou_x" || r.OnBehalfOf.UserName != "Alice" {
t.Fatalf("onBehalfOf = %#v, want Alice/ou_x", r.OnBehalfOf)
if r.OpenID != "ou_x" || r.UserName != "Alice" {
t.Fatalf("openId/userName = %q/%q", r.OpenID, r.UserName)
}
if r.Hint != "" {
t.Fatalf("hint = %q, want empty", r.Hint)
@@ -75,9 +70,9 @@ func TestBuildResult_UserValid(t *testing.T) {
func TestBuildResult_UserMissingToken(t *testing.T) {
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandLark}
diag := identitydiag.Result{
User: identitydiag.Identity{Available: false, Status: "missing", Hint: "run: lark-cli auth login --help"}, // never logged in
User: identitydiag.Identity{Available: false, TokenStatus: ""}, // never logged in
}
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
r := buildResult(cfg, core.AsUser, "auto-detect", diag)
if r.Available {
t.Fatalf("available = true, want false")
@@ -85,10 +80,8 @@ func TestBuildResult_UserMissingToken(t *testing.T) {
if r.TokenStatus != "missing" {
t.Fatalf("tokenStatus = %q, want missing", r.TokenStatus)
}
// whoami renders the diagnosed hint verbatim (single source of truth) so it
// stays correct for the external-provider path without whoami knowing about it.
if r.Hint != diag.User.Hint {
t.Fatalf("hint = %q, want propagated %q", r.Hint, diag.User.Hint)
if r.Hint == "" {
t.Fatalf("hint empty, want guidance")
}
if r.DefaultAs != "auto" {
t.Fatalf("defaultAs = %q, want auto (empty normalized)", r.DefaultAs)
@@ -100,16 +93,16 @@ func TestBuildResult_BotReady(t *testing.T) {
diag := identitydiag.Result{
Bot: identitydiag.Identity{Available: true, Status: "ready"},
}
r := buildResult(cfg, core.AsBot, "default_as", diag)
r := buildResult(cfg, core.AsBot, "default-as", diag)
if r.Identity != "bot" || r.IdentitySource != "default_as" {
if r.Identity != "bot" || r.IdentitySource != "default-as" {
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
}
if !r.Available || r.TokenStatus != "ready" {
t.Fatalf("available=%v status=%q", r.Available, r.TokenStatus)
}
if r.OnBehalfOf != nil {
t.Fatalf("bot must not carry onBehalfOf: %#v", r.OnBehalfOf)
if r.OpenID != "" || r.UserName != "" {
t.Fatalf("bot must not carry openId/userName: %#v", r)
}
if r.Hint != "" {
t.Fatalf("hint = %q, want empty", r.Hint)
@@ -119,9 +112,9 @@ func TestBuildResult_BotReady(t *testing.T) {
func TestBuildResult_BotNotConfigured(t *testing.T) {
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu}
diag := identitydiag.Result{
Bot: identitydiag.Identity{Available: false, Status: "not_configured", Hint: "run: lark-cli config --help"},
Bot: identitydiag.Identity{Available: false, Status: "not_configured"},
}
r := buildResult(cfg, core.AsBot, "auto_detect", diag)
r := buildResult(cfg, core.AsBot, "auto-detect", diag)
if r.Available {
t.Fatalf("available = true, want false")
@@ -129,8 +122,58 @@ func TestBuildResult_BotNotConfigured(t *testing.T) {
if r.TokenStatus != "not_configured" {
t.Fatalf("tokenStatus = %q, want not_configured", r.TokenStatus)
}
if r.Hint != diag.Bot.Hint {
t.Fatalf("hint = %q, want propagated %q", r.Hint, diag.Bot.Hint)
if r.Hint == "" {
t.Fatalf("hint empty, want guidance")
}
}
func TestFormatPretty_User(t *testing.T) {
var buf bytes.Buffer
formatPretty(&buf, &whoamiResult{
Profile: "my-app", AppID: "cli_x", Brand: core.BrandLark,
Identity: "user", IdentitySource: "auto-detect",
Available: true, TokenStatus: "valid", OpenID: "ou_x", UserName: "Alice",
})
out := buf.String()
for _, want := range []string{
"Profile: my-app (cli_x, lark)",
"Identity: user (auto-detect)",
"User: Alice (ou_x)",
"Token: valid",
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q\n--- got ---\n%s", want, out)
}
}
}
func TestFormatPretty_BotNoUserLine(t *testing.T) {
var buf bytes.Buffer
formatPretty(&buf, &whoamiResult{
Profile: "p", AppID: "cli_x", Brand: core.BrandFeishu,
Identity: "bot", IdentitySource: "default-as",
Available: true, TokenStatus: "ready",
})
out := buf.String()
if strings.Contains(out, "User:") {
t.Errorf("bot output must not contain User: line\n%s", out)
}
if !strings.Contains(out, "Identity: bot (default-as)") || !strings.Contains(out, "Token: ready") {
t.Errorf("unexpected bot output:\n%s", out)
}
}
func TestFormatPretty_UnavailableShowsHint(t *testing.T) {
var buf bytes.Buffer
formatPretty(&buf, &whoamiResult{
Profile: "p", AppID: "cli_x", Brand: core.BrandLark,
Identity: "user", IdentitySource: "auto-detect",
Available: false, TokenStatus: "missing",
Hint: "No usable user token. Run `lark-cli auth login`.",
})
out := buf.String()
if !strings.Contains(out, "Token: missing — No usable user token.") {
t.Errorf("expected token line with hint, got:\n%s", out)
}
}
@@ -140,7 +183,7 @@ func TestWhoami_BotJSON(t *testing.T) {
})
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{}) // bare whoami: output is always JSON, no flag needed
cmd.SetArgs([]string{"--json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}
@@ -161,8 +204,8 @@ func TestWhoami_BotJSON(t *testing.T) {
if got.IdentitySource == "" {
t.Fatalf("identitySource empty")
}
if got.OnBehalfOf != nil {
t.Fatalf("bot (self) must not carry onBehalfOf: %#v", got.OnBehalfOf)
if got.OpenID != "" {
t.Fatalf("bot must not carry openId: %q", got.OpenID)
}
}
@@ -213,108 +256,3 @@ func TestWhoami_ConfigErrorPropagates(t *testing.T) {
t.Fatalf("Execute() error = %v, want it to wrap %v", err, wantErr)
}
}
func TestWhoami_StrictModeRejectsCrossIdentity(t *testing.T) {
// Bot-only account → strict mode bot. A real `--as user` call would be
// rejected by CheckStrictMode; whoami must reject it identically rather than
// previewing a user identity the next call would refuse.
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
SupportedIdentities: 2, // bot only
})
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{"--as", "user", "--json"})
err := cmd.Execute()
if err == nil {
t.Fatalf("Execute() with --as user under strict bot = nil, want strict-mode rejection")
}
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("error type = %T, want *errs.ValidationError: %v", err, err)
}
}
type fakeExtProvider struct {
name string
account *extcred.Account
}
func (p *fakeExtProvider) Name() string { return p.name }
func (p *fakeExtProvider) ResolveAccount(context.Context) (*extcred.Account, error) {
return p.account, nil
}
func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*extcred.Token, error) {
return nil, nil // no UAT served locally; whoami runs with verify=false
}
func externalWhoamiFactory(cfg *core.CliConfig) (*cmdutil.Factory, *bytes.Buffer) {
cred := credential.NewCredentialProvider(
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: cfg.AppID}}},
nil, nil,
func() (*http.Client, error) { return nil, nil },
)
out := &bytes.Buffer{}
f := &cmdutil.Factory{
Config: func() (*core.CliConfig, error) { return cfg, nil },
Credential: cred,
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
}
return f, out
}
// Regression for the external-provider blind spot: with credentials managed by
// an extension provider, a signed-in user must read as available, and an
// unavailable identity must not be told to "auth login" (which is blocked).
func TestWhoami_ExternalProvider_UserReady(t *testing.T) {
cfg := &core.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
SupportedIdentities: uint8(extcred.SupportsAll), UserOpenId: "ou_x", UserName: "Alice",
}
f, out := externalWhoamiFactory(cfg)
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{"--as", "user", "--json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}
var got whoamiResult
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("Unmarshal: %v\n%s", err, out.String())
}
if got.Identity != "user" || !got.Available || got.TokenStatus != "ready" {
t.Fatalf("got %#v, want user/available/ready", got)
}
if got.OnBehalfOf == nil || got.OnBehalfOf.UserName != "Alice" || got.OnBehalfOf.OpenID != "ou_x" {
t.Fatalf("onBehalfOf = %#v, want Alice/ou_x (delegated)", got.OnBehalfOf)
}
if got.Hint != "" {
t.Fatalf("hint = %q, want empty when available", got.Hint)
}
}
func TestWhoami_ExternalProvider_UserHintNotKeychain(t *testing.T) {
cfg := &core.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
SupportedIdentities: uint8(extcred.SupportsUser), // user supported but not signed in
}
f, out := externalWhoamiFactory(cfg)
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{"--as", "user", "--json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v", err)
}
var got whoamiResult
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("Unmarshal: %v\n%s", err, out.String())
}
if got.Identity != "user" || got.Available {
t.Fatalf("got identity=%q available=%v, want user/false", got.Identity, got.Available)
}
if strings.Contains(got.Hint, "auth login") {
t.Fatalf("hint must not point at auth login under external provider: %q", got.Hint)
}
if !strings.Contains(got.Hint, "external") {
t.Fatalf("hint should explain external management: %q", got.Hint)
}
}

View File

@@ -18,9 +18,6 @@ type IOStreams struct {
Out io.Writer
ErrOut io.Writer
IsTerminal bool
// OutIsTerminal reports whether Out is an interactive terminal. Mirrors
// IsTerminal; computed once in NewIOStreams and assignable directly in tests.
OutIsTerminal bool
// StderrIsTerminal reports whether ErrOut is an interactive terminal.
// Advisory warnings written to stderr (e.g. the proxy notice) gate on this
// so they stay out of non-interactive output (pipes, CI, agent runs).
@@ -30,24 +27,19 @@ type IOStreams struct {
}
// NewIOStreams builds an IOStreams from arbitrary readers/writers.
// IsTerminal / OutIsTerminal / StderrIsTerminal are each derived from the
// underlying *os.File of in / out / errOut respectively; non-file
// readers/writers (bytes.Buffer, strings.Reader, …) yield false.
// IsTerminal / StderrIsTerminal are derived from in's / errOut's underlying
// *os.File, if any; non-file streams (bytes.Buffer, strings.Reader, …) yield
// false.
func NewIOStreams(in io.Reader, out, errOut io.Writer) *IOStreams {
fileIsTerminal := func(v any) bool {
if f, ok := v.(*os.File); ok {
return term.IsTerminal(int(f.Fd()))
}
return false
isTerminal := false
if f, ok := in.(*os.File); ok {
isTerminal = term.IsTerminal(int(f.Fd()))
}
return &IOStreams{
In: in,
Out: out,
ErrOut: errOut,
IsTerminal: fileIsTerminal(in),
OutIsTerminal: fileIsTerminal(out),
StderrIsTerminal: fileIsTerminal(errOut),
stderrIsTerminal := false
if f, ok := errOut.(*os.File); ok {
stderrIsTerminal = term.IsTerminal(int(f.Fd()))
}
return &IOStreams{In: in, Out: out, ErrOut: errOut, IsTerminal: isTerminal, StderrIsTerminal: stderrIsTerminal}
}
// SystemIO creates an IOStreams wired to the process's standard file descriptors.

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package cmdutil
import (
"bytes"
"os"
"testing"
)
func TestNewIOStreamsTerminalFlagsNonFile(t *testing.T) {
s := NewIOStreams(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})
if s.IsTerminal || s.OutIsTerminal || s.StderrIsTerminal {
t.Errorf("non-file streams must not be terminals: in=%v out=%v err=%v",
s.IsTerminal, s.OutIsTerminal, s.StderrIsTerminal)
}
}
func TestNewIOStreamsTerminalFlagsPipe(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer w.Close()
s := NewIOStreams(r, w, w)
if s.OutIsTerminal || s.StderrIsTerminal {
t.Errorf("os.Pipe must not be a terminal: out=%v err=%v", s.OutIsTerminal, s.StderrIsTerminal)
}
}

View File

@@ -10,20 +10,8 @@ import "github.com/larksuite/cli/errs"
// ambiguous codes fall back to CategoryAPI via BuildAPIError.
// BuildAPIError consumes this map via mergeCodeMeta + LookupCodeMeta.
var driveCodeMeta = map[int]CodeMeta{
1061001: {Category: errs.CategoryAPI, Subtype: errs.SubtypeServerError, Retryable: true}, // Drive "unknown error"
1061002: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // params error
1061004: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypePermissionDenied}, // forbidden
1061007: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // file has been deleted
1061043: {Category: errs.CategoryAPI, Subtype: errs.SubtypeQuotaExceeded}, // file size beyond limit
1061044: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // parent folder does not exist (upload)
1062009: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // actual size inconsistent with declared size
1063001: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // secure label invalid parameter
1063002: {Category: errs.CategoryAuthorization, Subtype: errs.SubtypePermissionDenied}, // secure label permission denied
1063013: {Category: errs.CategoryValidation, Subtype: errs.SubtypeFailedPrecondition}, // secure label downgrade requires approval
1069302: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // comment endpoint "Invalid or missing parameters"
99992402: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // platform field validation failed
9499: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // invalid parameter type in JSON field
2200: {Category: errs.CategoryAPI, Subtype: errs.SubtypeServerError, Retryable: true}, // Drive tenant/internal errors
1061044: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // parent folder does not exist (upload)
1069302: {Category: errs.CategoryAPI, Subtype: errs.SubtypeInvalidParameters}, // comment endpoint "Invalid or missing parameters"
}
func init() { mergeCodeMeta(driveCodeMeta, "drive") }

View File

@@ -27,13 +27,6 @@ func TestLookupCodeMeta_DriveCodes(t *testing.T) {
// 1069302: comment endpoint's opaque "Invalid or missing parameters"
// (shortcuts/drive/drive_add_comment.go) → API-side parameter rejection.
{1069302, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
// Secure label endpoint codes observed from drive +secure-label-update
// failure telemetry.
{1063001, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
{1063002, errs.CategoryAuthorization, errs.SubtypePermissionDenied, false},
{1063013, errs.CategoryValidation, errs.SubtypeFailedPrecondition, false},
{99992402, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
{9499, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%d", tc.code), func(t *testing.T) {

View File

@@ -102,35 +102,6 @@ func TestLookupCodeMeta_RetryableRateLimit(t *testing.T) {
}
}
func TestLookupCodeMeta_DrivePushCodes(t *testing.T) {
cases := []struct {
code int
wantCat errs.Category
wantSubtype errs.Subtype
wantRetry bool
}{
{1061001, errs.CategoryAPI, errs.SubtypeServerError, true},
{1061002, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
{1061004, errs.CategoryAuthorization, errs.SubtypePermissionDenied, false},
{1061007, errs.CategoryAPI, errs.SubtypeNotFound, false},
{1061043, errs.CategoryAPI, errs.SubtypeQuotaExceeded, false},
{1062009, errs.CategoryAPI, errs.SubtypeInvalidParameters, false},
{2200, errs.CategoryAPI, errs.SubtypeServerError, true},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%d", tc.code), func(t *testing.T) {
got, ok := LookupCodeMeta(tc.code)
if !ok {
t.Fatalf("LookupCodeMeta(%d) ok=false, want true", tc.code)
}
if got.Category != tc.wantCat || got.Subtype != tc.wantSubtype || got.Retryable != tc.wantRetry {
t.Fatalf("LookupCodeMeta(%d) = %+v, want Category=%v Subtype=%v Retryable=%v",
tc.code, got, tc.wantCat, tc.wantSubtype, tc.wantRetry)
}
})
}
}
func TestLookupCodeMeta_Unknown(t *testing.T) {
_, ok := LookupCodeMeta(999999)
if ok {

View File

@@ -13,7 +13,6 @@ import (
"strings"
"time"
extcred "github.com/larksuite/cli/extension/credential"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
@@ -62,131 +61,12 @@ func Diagnose(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, veri
if ctx == nil {
ctx = context.Background()
}
// An external provider mints tokens on demand and blocks interactive auth,
// so the built-in keychain heuristics and "auth login" hints don't apply.
if provider := activeExternalProvider(ctx, f); provider != "" {
return diagnoseExternal(ctx, f, cfg, provider, verify)
}
return Result{
Bot: diagnoseBot(ctx, f, cfg, verify),
User: diagnoseUser(ctx, f, cfg, verify),
}
}
// activeExternalProvider returns the active extension provider name, or "".
// An error degrades to the built-in path: an unreachable provider would already
// have failed the f.Config() that produced cfg.
func activeExternalProvider(ctx context.Context, f *cmdutil.Factory) string {
if f == nil || f.Credential == nil {
return ""
}
name, err := f.Credential.ActiveExtensionProviderName(ctx)
if err != nil {
return ""
}
return name
}
func diagnoseExternal(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, provider string, verify bool) Result {
if cfg == nil || cfg.AppID == "" {
notConfigured := Identity{
Status: StatusNotConfigured,
Message: "not configured (missing app config)",
Hint: externalCredentialHint(provider),
}
return Result{Bot: notConfigured, User: notConfigured}
}
// SupportedIdentities == 0 is "unspecified" — treat as both, per CanBot.
ids := extcred.IdentitySupport(cfg.SupportedIdentities)
supportsBot := cfg.SupportedIdentities == 0 || ids.Has(extcred.SupportsBot)
supportsUser := cfg.SupportedIdentities == 0 || ids.Has(extcred.SupportsUser)
return Result{
Bot: diagnoseExternalBot(ctx, f, cfg, provider, supportsBot, verify),
User: diagnoseExternalUser(ctx, f, cfg, provider, supportsUser, verify),
}
}
func diagnoseExternalBot(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, provider string, supported, verify bool) Identity {
if !supported {
return notProvidedExternally("Bot", provider)
}
id := Identity{Status: StatusReady, Available: true, Message: "Bot identity: ready (provided by " + provider + ")"}
if !verify {
return id
}
token, err := resolveBotToken(ctx, f, cfg)
if err != nil {
return externalVerifyFailed(id, "Bot", provider, err)
}
info, err := fetchBotInfo(ctx, f, cfg, token)
if err != nil {
return externalVerifyFailed(id, "Bot", provider, err)
}
id.Verified = boolPtr(true)
id.OpenID = info.OpenID
id.AppName = info.AppName
return id
}
func diagnoseExternalUser(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, provider string, supported, verify bool) Identity {
if !supported {
return notProvidedExternally("User", provider)
}
// enrichUserInfo populates UserOpenId only after the provider returns and
// verifies a UAT (and clears it on failure), so a resolved open id is the
// external analogue of a keychain token being present.
if cfg.UserOpenId == "" {
return Identity{
Status: StatusMissing,
Message: "User identity: not signed in via credential source " + provider,
Hint: externalCredentialHint(provider),
}
}
id := Identity{
Status: StatusReady,
Available: true,
TokenStatus: StatusReady,
UserName: cfg.UserName,
OpenID: cfg.UserOpenId,
Message: "User identity: ready (provided by " + provider + ")",
}
if !verify {
return id
}
if _, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(core.AsUser, cfg.AppID)); err != nil {
return externalVerifyFailed(id, "User", provider, err)
}
id.Verified = boolPtr(true)
return id
}
func notProvidedExternally(label, provider string) Identity {
return Identity{
Status: StatusNotConfigured,
Message: label + " identity: not provided by credential source " + provider,
Hint: externalCredentialHint(provider),
}
}
// externalVerifyFailed flips id to verify-failed, keeping any identity fields
// (open id, user name) already resolved before the probe.
func externalVerifyFailed(id Identity, label, provider string, err error) Identity {
id.Available = false
id.Verified = boolPtr(false)
id.Status = StatusVerifyFailed
id.TokenStatus = ""
id.Message = label + " identity: verify failed: " + err.Error()
id.Hint = externalCredentialHint(provider)
return id
}
// externalCredentialHint reports the constraint, not a remediation: the
// identity is the provider's to manage, not lark-cli's to fix. What to do about
// it is the caller's call — there may be no user to ask.
func externalCredentialHint(provider string) string {
return fmt.Sprintf("managed by the external credential provider %q and cannot be configured via lark-cli", provider)
}
func diagnoseBot(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, verify bool) Identity {
if cfg == nil || cfg.AppID == "" {
return Identity{

View File

@@ -10,11 +10,9 @@ import (
"testing"
"time"
extcred "github.com/larksuite/cli/extension/credential"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/httpmock"
"github.com/zalando/go-keyring"
)
@@ -350,136 +348,3 @@ func TestDiagnose_UserIdentityNeedsRefresh(t *testing.T) {
t.Fatalf("token status = %q, want needs_refresh", got.User.TokenStatus)
}
}
// fakeExtProvider is a minimal credential.extcred.Provider for exercising the
// external-credential diagnosis path. account makes the provider "active";
// token (when set) satisfies ResolveToken during verify.
type fakeExtProvider struct {
name string
account *extcred.Account
token *extcred.Token
}
func (p *fakeExtProvider) Name() string { return p.name }
func (p *fakeExtProvider) ResolveAccount(context.Context) (*extcred.Account, error) {
return p.account, nil
}
func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*extcred.Token, error) {
return p.token, nil
}
func externalFactory(prov *fakeExtProvider, cfg *core.CliConfig) *cmdutil.Factory {
cred := credential.NewCredentialProvider(
[]extcred.Provider{prov}, nil, nil,
func() (*http.Client, error) { return nil, nil },
)
return &cmdutil.Factory{
Config: func() (*core.CliConfig, error) { return cfg, nil },
Credential: cred,
IOStreams: &cmdutil.IOStreams{},
}
}
// assertExternalHint locks the contract that an external-provider hint never
// points at interactive commands blocked under an external provider.
func assertExternalHint(t *testing.T, hint string) {
t.Helper()
if hint == "" {
t.Fatalf("hint empty, want external guidance")
}
for _, blocked := range []string{"auth login", "config --help"} {
if strings.Contains(hint, blocked) {
t.Fatalf("hint %q must not point at %q (blocked under external provider)", hint, blocked)
}
}
if !strings.Contains(hint, "external") {
t.Fatalf("hint %q should explain credentials are external", hint)
}
}
func TestDiagnose_External_UserReady(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsAll), UserOpenId: "ou_x", UserName: "Alice"}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}, cfg)
got := Diagnose(context.Background(), f, cfg, false)
// The bug this guards: the built-in path read the keychain (empty under an
// external provider) and reported the user as missing. Now availability
// follows the resolved account, so a signed-in user reads as ready.
if !got.User.Available || got.User.Status != StatusReady || got.User.TokenStatus != StatusReady {
t.Fatalf("user = %#v, want ready/available", got.User)
}
if got.User.OpenID != "ou_x" || got.User.UserName != "Alice" {
t.Fatalf("user identity = %#v", got.User)
}
if got.User.Hint != "" {
t.Fatalf("hint = %q, want empty when available", got.User.Hint)
}
if !got.Bot.Available || got.Bot.Status != StatusReady {
t.Fatalf("bot = %#v, want ready/available", got.Bot)
}
}
func TestDiagnose_External_UserNotSignedIn(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsAll)}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}, cfg)
got := Diagnose(context.Background(), f, cfg, false)
if got.User.Available || got.User.Status != StatusMissing {
t.Fatalf("user = %#v, want missing/unavailable", got.User)
}
assertExternalHint(t, got.User.Hint)
}
func TestDiagnose_External_BotOnly(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsBot), UserOpenId: "ou_x"}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}, cfg)
got := Diagnose(context.Background(), f, cfg, false)
if !got.Bot.Available || got.Bot.Status != StatusReady {
t.Fatalf("bot = %#v, want ready/available", got.Bot)
}
// Provider declares bot-only: user is unavailable even though an open id is
// present, and the hint is external (not "auth login").
if got.User.Available || got.User.Status != StatusNotConfigured {
t.Fatalf("user = %#v, want not_configured/unavailable", got.User)
}
assertExternalHint(t, got.User.Hint)
}
func TestDiagnose_External_UserOnly(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandLark, SupportedIdentities: uint8(extcred.SupportsUser), UserOpenId: "ou_x", UserName: "Bob"}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}, cfg)
got := Diagnose(context.Background(), f, cfg, false)
if !got.User.Available || got.User.Status != StatusReady {
t.Fatalf("user = %#v, want ready/available", got.User)
}
if got.Bot.Available || got.Bot.Status != StatusNotConfigured {
t.Fatalf("bot = %#v, want not_configured/unavailable", got.Bot)
}
assertExternalHint(t, got.Bot.Hint)
}
func TestDiagnose_External_VerifyUserResolvesToken(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser), UserOpenId: "ou_x", UserName: "Alice"}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}, token: &extcred.Token{Value: "ext-uat"}}, cfg)
got := Diagnose(context.Background(), f, cfg, true)
if !got.User.Available || got.User.Verified == nil || !*got.User.Verified {
t.Fatalf("user = %#v, want available and verified", got.User)
}
}
func TestDiagnose_External_VerifyUserTokenUnavailable(t *testing.T) {
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser), UserOpenId: "ou_x"}
f := externalFactory(&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}, cfg)
got := Diagnose(context.Background(), f, cfg, true)
if got.User.Available || got.User.Status != StatusVerifyFailed {
t.Fatalf("user = %#v, want verify_failed/unavailable", got.User)
}
if got.User.Verified == nil || *got.User.Verified {
t.Fatalf("verified = %v, want false", got.User.Verified)
}
assertExternalHint(t, got.User.Hint)
}

View File

@@ -52,9 +52,6 @@ func isPlaceholderValue(value string) bool {
normalized := strings.ToLower(trimmed)
if normalized == "" ||
normalized == "=" ||
printfPlaceholderValue(normalized) ||
htmlEntityAnglePlaceholder(normalized) ||
starMaskedPlaceholder(normalized) ||
percentWrappedPlaceholder(normalized) ||
angleWrappedPlaceholder(normalized) ||
urlWithAnglePlaceholder(normalized) ||
@@ -64,28 +61,9 @@ func isPlaceholderValue(value string) bool {
return namedPlaceholderValue(normalized)
}
func htmlEntityAnglePlaceholder(value string) bool {
if !strings.HasPrefix(value, "<") || !strings.HasSuffix(value, ">") {
return false
}
return anglePlaceholderIdentifier(strings.TrimSuffix(strings.TrimPrefix(value, "<"), ">"))
}
func starMaskedPlaceholder(value string) bool {
var stars int
for _, r := range value {
if r == '*' {
stars++
continue
}
return false
}
return stars >= 3
}
func namedPlaceholderValue(value string) bool {
switch value {
case "...", "***", "****", "placeholder", "redacted", "<redacted>", "xxxx", "test-secret", "test-token", "dry-run", "dry_run":
case "...", "placeholder", "redacted", "<redacted>", "xxxx", "test-secret":
return true
}
return strings.Contains(value, "cli_example") ||
@@ -93,15 +71,6 @@ func namedPlaceholderValue(value string) bool {
conventionalNamedPlaceholderValue(value)
}
func printfPlaceholderValue(value string) bool {
switch value {
case "%d", "%s", "%q", "%v", "%w", "%x", "%T":
return true
default:
return false
}
}
func allXPlaceholder(value string) bool {
if len(value) < 4 {
return false

View File

@@ -54,9 +54,8 @@ func scanText(file, source, text string, detectorFile bool) []Finding {
keyName, _ := normalizedCredentialAssignmentKey(match[0])
if value == "" ||
isNonSecretLiteralValue(value) ||
isBenignCodeCredentialExpression(file, line, match[0], value) ||
isBenignCodeCredentialExpression(file, value) ||
isPlaceholderValue(value) ||
isPermissionScopeIdentifierAssignment(keyName, value) ||
isResourceTokenPlaceholderAssignment(keyName, value) {
continue
}
@@ -267,7 +266,7 @@ func isResourceTokenPlaceholderAssignment(key, value string) bool {
case key == "retry_without_token" && numericStringPlaceholderValue(value):
return true
case tokenLikePlaceholderKey(key):
return tokenLikePlaceholderValue(key, value)
return tokenLikePlaceholderValue(value)
default:
return false
}
@@ -279,13 +278,12 @@ func tokenLikePlaceholderKey(key string) bool {
strings.HasSuffix(key, "-token")
}
func tokenLikePlaceholderValue(key, value string) bool {
func tokenLikePlaceholderValue(value string) bool {
normalized := strings.ToLower(strings.Trim(value, `"'`))
if normalized == "" || credentialShapedIdentifier(normalized) {
return false
}
return resourceTokenPlaceholderValue(value) ||
maskedTokenFixturePlaceholderValue(key, normalized) ||
isPlaceholderValue(value) ||
normalized == "token" ||
strings.Contains(normalized, "...") ||
@@ -295,51 +293,6 @@ func tokenLikePlaceholderValue(key, value string) bool {
strings.HasPrefix(normalized, ".")
}
func maskedTokenFixturePlaceholderValue(key, value string) bool {
if authCredentialTokenKey(key) {
return false
}
var stars, alnum int
for _, r := range value {
switch {
case r == '*':
stars++
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
alnum++
default:
return false
}
}
return stars >= 6 && alnum > 0
}
func authCredentialTokenKey(key string) bool {
switch strings.ReplaceAll(strings.ToLower(key), "-", "_") {
case "access_token",
"refresh_token",
"session_token",
"bearer_token",
"auth_token",
"authorization_token",
"id_token":
return true
default:
return false
}
}
func isPermissionScopeIdentifierAssignment(key, value string) bool {
if !strings.HasSuffix(key, "_token") {
return false
}
switch strings.ToLower(strings.Trim(value, `"',;`)) {
case "read", "write", "modify", "readonly", "get_as_user":
return true
default:
return false
}
}
func idempotencyTokenPlaceholderValue(value string) bool {
return numericStringPlaceholderValue(value) || uuidStringPlaceholderValue(value)
}
@@ -380,87 +333,20 @@ func numericStringPlaceholderValue(value string) bool {
return true
}
func isBenignCodeCredentialExpression(file, line, match, value string) bool {
func isBenignCodeCredentialExpression(file, value string) bool {
normalized := strings.TrimSpace(value)
if strings.HasPrefix(normalized, "regexp.MustCompile(") {
return true
}
if !sourceCodeFile(file) || credentialShapedValue(value) {
if !sourceCodeFile(file) || quotedLiteral(value) || credentialShapedValue(value) {
return false
}
if rhs, ok := sourceCodeTypedCredentialRHS(line, match); ok {
return isBenignTypedCredentialRHS(rhs)
}
rawValueQuoted := credentialAssignmentRawValueQuoted(match)
if sourceCodeLiteralLooksNonSecret(normalized, !rawValueQuoted) {
return true
}
if sourceCodeFormatStringLiteral(normalized) && sourceCodeFormatArgumentContext(line, match) {
return true
}
if strings.Contains(match, "+") {
return true
}
if rawValueQuoted {
return false
}
if quotedLiteral(value) {
return sourceCodeLiteralLooksNonSecret(value, false)
}
return codeReferenceExpression(normalized)
}
func sourceCodeTypedCredentialRHS(line, match string) (string, bool) {
idx := strings.Index(line, match)
if idx < 0 {
return "", false
}
key, ok := credentialAssignmentKey(match)
if !ok {
return "", false
}
rest := strings.TrimSpace(line[idx+len(key):])
if !strings.HasPrefix(rest, ":") {
return "", false
}
typeAndRHS := strings.TrimSpace(strings.TrimPrefix(rest, ":"))
assignmentIdx := strings.Index(typeAndRHS, "=")
if assignmentIdx < 0 {
return "", false
}
return strings.TrimSpace(typeAndRHS[assignmentIdx+1:]), true
}
func isBenignTypedCredentialRHS(value string) bool {
value = strings.TrimRight(strings.TrimSpace(value), ",;")
if value == "" || isNonSecretLiteralValue(value) || isPlaceholderValue(value) {
return true
}
if credentialShapedValue(value) {
return false
}
if sourceCodeLiteralLooksNonSecret(value, !quotedLiteral(value)) {
return true
}
if quotedLiteral(value) {
return false
}
return codeReferenceExpression(value)
}
func credentialAssignmentRawValueQuoted(match string) bool {
key, ok := credentialAssignmentKey(match)
if !ok {
return false
}
rest := strings.TrimSpace(strings.TrimPrefix(match[len(key):], ":"))
rest = strings.TrimSpace(strings.TrimPrefix(rest, "="))
return strings.HasPrefix(rest, `"`) || strings.HasPrefix(rest, `'`)
}
func sourceCodeFile(file string) bool {
switch filepath.Ext(file) {
case ".go", ".js", ".jsx", ".py", ".ts", ".tsx":
case ".go", ".py":
return true
default:
return false
@@ -474,147 +360,7 @@ func quotedLiteral(value string) bool {
(strings.HasPrefix(normalized, `'`) && strings.HasSuffix(normalized, `'`)))
}
func sourceCodeLiteralLooksNonSecret(value string, allowNumeric bool) bool {
literal := strings.Trim(strings.TrimSpace(value), `"'`)
if strings.HasPrefix(literal, "/") {
return true
}
return (allowNumeric && numericStringPlaceholderValue(literal)) ||
sourceCodeEnvVarNameLiteral(literal) ||
sourceCodeAttributeNameLiteral(literal) ||
sourceCodeFakeOrPlaceholderLiteral(literal) ||
sourceCodeCredentialTermLiteral(literal) ||
sourceCodeCredentialPrefixLiteral(literal) ||
sourceCodeVocabularyLiteral(literal) ||
sourceCodeSchemaTypeLiteral(literal) ||
benignCredentialStatusLiteral(literal)
}
func sourceCodeFormatArgumentContext(line, match string) bool {
idx := strings.Index(line, match)
if idx < 0 {
return false
}
prefix := line[:idx]
if semicolon := strings.LastIndex(prefix, ";"); semicolon >= 0 {
prefix = prefix[semicolon+1:]
}
return strings.Contains(prefix, "fmt.") ||
strings.Contains(prefix, "log.") ||
strings.Contains(prefix, "printf(") ||
strings.Contains(prefix, "Printf(") ||
strings.Contains(prefix, "Errorf(") ||
strings.Contains(prefix, "Fprintf(")
}
func sourceCodeFormatStringLiteral(value string) bool {
for i := 0; i < len(value)-1; i++ {
if value[i] != '%' {
continue
}
if value[i+1] == '%' {
i++
continue
}
j := i + 1
for j < len(value) && strings.ContainsRune("#+- 0.0123456789", rune(value[j])) {
j++
}
if j < len(value) && strings.ContainsRune("vTtbcdoOqxXUeEfFgGspw", rune(value[j])) {
return true
}
}
return false
}
func sourceCodeEnvVarNameLiteral(value string) bool {
if value == "" || !strings.Contains(value, "_") {
return false
}
var hasCredentialMarker bool
for _, r := range value {
switch {
case r >= 'A' && r <= 'Z':
case r >= '0' && r <= '9':
case r == '_':
default:
return false
}
}
for _, marker := range []string{"TOKEN", "SECRET", "KEY", "PASSWORD", "PASSWD"} {
if strings.Contains(value, marker) {
hasCredentialMarker = true
break
}
}
return hasCredentialMarker
}
func sourceCodeAttributeNameLiteral(value string) bool {
normalized := strings.ToLower(value)
return strings.HasPrefix(normalized, "data-") && delimitedPlaceholderIdentifier(normalized)
}
func sourceCodeFakeOrPlaceholderLiteral(value string) bool {
normalized := strings.ToLower(value)
return strings.HasPrefix(normalized, "fake_") ||
strings.HasPrefix(normalized, "fake-") ||
strings.Contains(normalized, "placeholder") ||
(strings.Contains(normalized, "<") && strings.Contains(normalized, ">"))
}
func sourceCodeCredentialTermLiteral(value string) bool {
normalized := strings.ToLower(strings.ReplaceAll(value, "-", "_"))
return conventionalCredentialPlaceholderName(normalized)
}
func sourceCodeCredentialPrefixLiteral(value string) bool {
switch strings.ToLower(value) {
case "appsecret:":
return true
default:
return false
}
}
func sourceCodeVocabularyLiteral(value string) bool {
switch strings.ToLower(value) {
case "bot", "tenant", "user":
return true
default:
return false
}
}
func sourceCodeSchemaTypeLiteral(value string) bool {
normalized := strings.ToLower(value)
return normalized == "string" || strings.HasPrefix(normalized, "string(")
}
func benignCredentialStatusLiteral(value string) bool {
normalized := strings.ToLower(strings.ReplaceAll(value, "-", "_"))
if !delimitedPlaceholderIdentifier(normalized) {
return false
}
for _, marker := range []string{
"bad_fmt",
"expired",
"format",
"invalid",
"missing",
"permission",
"status",
"type",
} {
if strings.Contains(normalized, marker) {
return true
}
}
return false
}
func codeReferenceExpression(value string) bool {
value = strings.TrimRight(strings.TrimSpace(value), ";")
if value == "" {
return false
}
@@ -623,10 +369,7 @@ func codeReferenceExpression(value string) bool {
return true
}
}
if !codeIdentifier(value) {
return false
}
return codeIdentifier(value)
return codeIdentifier(value) && !credentialNameFragment(value)
}
func codeIdentifier(value string) bool {
@@ -643,6 +386,16 @@ func codeIdentifier(value string) bool {
return true
}
func credentialNameFragment(value string) bool {
normalized := strings.ToLower(value)
for _, marker := range []string{"secret", "token", "password", "passwd", "key"} {
if strings.Contains(normalized, marker) {
return true
}
}
return false
}
func isNonSecretLiteralValue(value string) bool {
switch strings.ToLower(strings.TrimSpace(strings.Trim(value, `"'`))) {
case "true", "false", "null", "nil", "{", "[":

View File

@@ -770,172 +770,6 @@ func TestScanFileAllowsPythonArgumentTokens(t *testing.T) {
}
}
func TestScanFileAllowsPythonCredentialTypeAnnotations(t *testing.T) {
got := ScanFile("fixtures/doc_word_stat.py", []byte(strings.Join([]string{
"class Counter:",
" def __init__(self) -> None:",
" self._token_kind: TokenKind | None = None",
" self.access_token: AccessToken | None = None",
}, "\n")+"\n"))
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
t.Fatalf("python credential-shaped type annotations should not be credential findings: %#v", got)
}
}
}
func TestScanFileAllowsSourceCodeCredentialNonSecretLiterals(t *testing.T) {
got := ScanFile("fixtures/auth_paths.go", []byte(strings.Join([]string{
`const PathOAuthTokenV2 = "/open-apis/authen/v2/oauth/token"`,
`return fmt.Errorf("failed to remove token: %v", err)`,
`const LarkErrTokenMissing = "token_missing"`,
`const LarkErrTokenExpired = 99991677`,
`const CliAppSecret = "LARKSUITE_CLI_APP_SECRET"`,
`const LargeAttachmentTokenAttr = "data-mail-token"`,
`const fakeOfficeTokenPrefix = "fake_office_"`,
`fmt.Fprintf(w, " - token=%s filename=%s\n", att.Token, att.FileName)`,
`tokenTypeHint := "access_token"`,
`const TokenTenant Token = "tenant"`,
`const secretKeyPrefix = "appsecret:"`,
`output.PrintJson(out, map[string]interface{}{"appSecret": "****"})`,
`return &credential.TokenResult{Token: "test-token"}, nil`,
`fmt.Fprintf(w, "password=%s\n", pat)`,
`text += "(img_token:" + imgToken + ")"`,
`map[string]interface{}{"token": "string(optional, from inspect)"}`,
`this.token = token;`,
`// AppSecret: "appsecret:<appId>"`,
}, "\n")+"\n"))
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
t.Fatalf("source code non-secret literals should not be credential findings: %#v", got)
}
}
}
func TestScanFileAllowsCredentialLikePublicPlaceholders(t *testing.T) {
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
`app_secret=***`,
`{"token":"&lt;wiki_token&gt;"}`,
`{"token":"Pgrrwvr***********UnRb"}`,
`"scope_name": "auth:user_access_token:read"`,
}, "\n")+"\n"))
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
t.Fatalf("public placeholders and scope identifiers should not be credential findings: %#v", got)
}
}
}
func TestScanFileDetectsPartiallyMaskedCredentialValues(t *testing.T) {
got := ScanFile("fixtures/config.md", []byte(strings.Join([]string{
"client_secret=realprefix***realsuffix",
"client_secret=ab********cd",
"access_token=ab********cd",
"refresh_token=realprefix********realsuffix",
}, "\n")+"\n"))
var count int
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
count++
}
}
if count != 4 {
t.Fatalf("partially masked credential findings = %d, want 4: %#v", count, got)
}
}
func TestScanFileAllowsDryRunCredentialPlaceholders(t *testing.T) {
got := ScanFile("fixtures/ci.yml", []byte(strings.Join([]string{
"LARKSUITE_CLI_APP_SECRET=dry-run",
"client_secret: dry_run",
}, "\n")+"\n"))
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
t.Fatalf("dry-run credential placeholders should not be credential findings: %#v", got)
}
}
}
func TestScanFileDetectsTypedCredentialAssignmentsWithSecretRHS(t *testing.T) {
cases := []struct {
name string
file string
text string
}{
{
name: "typescript simple secret",
file: "fixtures/source_secret.ts",
text: `const clientSecret: string = "real-client-secret-value"`,
},
{
name: "typescript numeric password",
file: "fixtures/source_secret.ts",
text: `const password: string = "12345678901234567890"`,
},
{
name: "typescript union secret",
file: "fixtures/source_secret.ts",
text: `const clientSecret: string | undefined = "real-client-secret-value"`,
},
{
name: "python simple secret",
file: "fixtures/source_secret.py",
text: `self.client_secret: str = "real-client-secret-value"`,
},
{
name: "python union secret",
file: "fixtures/source_secret.py",
text: `self.client_secret: str | None = "real-client-secret-value"`,
},
{
name: "python optional secret",
file: "fixtures/source_secret.py",
text: `self.client_secret: Optional[str] = "real-client-secret-value"`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := ScanFile(tc.file, []byte(tc.text+"\n"))
if !findingRules(got)["public_content_generic_credential"] {
t.Fatalf("typed credential assignment should be reported: %#v", got)
}
})
}
}
func TestScanFileDetectsCredentialShapedSourceCodeLiterals(t *testing.T) {
githubToken := "ghp_" + "1234567890abcdef1234567890abcdef1234"
got := ScanFile("fixtures/source_secret.go", []byte(strings.Join([]string{
`const ClientSecret = "real-client-secret-value"`,
`const GithubToken = "` + githubToken + `"`,
`const Password = "12345678901234567890"`,
`const ClientSecretNumber = "12345678901234567890"`,
`const ClientSecretFormat = "abc%sdefreal"`,
`fmt.Println("done"); const ClientSecret = "abc%sdefreal"`,
}, "\n")+"\n"))
var count int
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
count++
}
}
if count != 6 {
t.Fatalf("source code credential-shaped literal findings = %d, want 6: %#v", count, got)
}
}
func TestScanFileAllowsPrintfCredentialPlaceholders(t *testing.T) {
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
"client_secret=%s",
"access_token=%v",
}, "\n")+"\n"))
for _, item := range got {
if item.Rule == "public_content_generic_credential" {
t.Fatalf("printf placeholders should not be credential findings: %#v", got)
}
}
}
func TestScanFileAllowsEllipsisCredentialPlaceholders(t *testing.T) {
got := ScanFile("fixtures/lark-doc-fetch.md", []byte(strings.Join([]string{
`<img token="..." url="https://..." width="..." height="..."/>`,

View File

@@ -1,6 +1,6 @@
{
"name": "@larksuite/cli",
"version": "1.0.63",
"version": "1.0.60",
"description": "The official CLI for Lark/Feishu open platform",
"bin": {
"lark-cli": "scripts/run.js"

View File

@@ -89,18 +89,6 @@ func TestDryRunFieldOps(t *testing.T) {
)
assertDryRunContains(t, dryRunFieldGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_1")
assertDryRunContains(t, dryRunFieldCreate(ctx, rt), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/fields")
arrayRT := newBaseTestRuntime(
map[string]string{
"base-token": "app_x",
"table-id": "tbl_1",
"json": `[{"name":"A","type":"text"},{"name":"B","type":"text"}]`,
},
nil,
nil,
)
assertDryRunContains(t, dryRunFieldCreate(ctx, arrayRT), `"name":"A"`, `"name":"B"`)
assertDryRunContains(t, dryRunFieldUpdate(ctx, rt), "PUT /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_1")
assertDryRunContains(t, dryRunFieldDelete(ctx, rt), "DELETE /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_1")
assertDryRunContains(t, dryRunFieldSearchOptions(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_1/options", "offset=3", "limit=30", "query=open")

View File

@@ -830,6 +830,11 @@ func TestBaseObjectJSONShortcutsRejectArrayInDryRun(t *testing.T) {
shortcut common.Shortcut
args []string
}{
{
name: "field create",
shortcut: BaseFieldCreate,
args: []string{"+field-create", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `[]`, "--dry-run"},
},
{
name: "field update",
shortcut: BaseFieldUpdate,
@@ -1097,54 +1102,6 @@ func TestBaseFieldExecuteCRUD(t *testing.T) {
}
})
t.Run("create array sequentially", func(t *testing.T) {
oldDelay := fieldCreateBatchDelay
fieldCreateBatchDelay = 0
t.Cleanup(func() { fieldCreateBatchDelay = oldDelay })
factory, stdout, reg := newExecuteFactory(t)
firstStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields",
BodyFilter: func(body []byte) bool {
return strings.Contains(string(body), `"name":"A"`)
},
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{"id": "fld_a", "name": "A", "type": "text"},
},
}
secondStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields",
BodyFilter: func(body []byte) bool {
return strings.Contains(string(body), `"name":"B"`)
},
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{"id": "fld_b", "name": "B", "type": "text"},
},
}
reg.Register(firstStub)
reg.Register(secondStub)
err := runShortcut(t, BaseFieldCreate, []string{"+field-create", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `[{"name":"A","type":"text"},{"name":"B","type":"text"}]`}, factory, stdout)
if err != nil {
t.Fatalf("err=%v", err)
}
data := decodeBaseEnvelope(t, stdout)
if data["created"] != true || data["total"] != float64(2) {
t.Fatalf("unexpected output: %#v", data)
}
fields, _ := data["fields"].([]interface{})
if len(fields) != 2 {
t.Fatalf("fields len=%d output=%#v", len(fields), data)
}
if !strings.Contains(string(firstStub.CapturedBody), `"name":"A"`) || !strings.Contains(string(secondStub.CapturedBody), `"name":"B"`) {
t.Fatalf("unexpected request bodies: %s / %s", firstStub.CapturedBody, secondStub.CapturedBody)
}
})
t.Run("delete", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{

View File

@@ -1060,15 +1060,6 @@ func TestBaseFieldValidate(t *testing.T) {
if err := BaseFieldCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "json": "{"}, nil, nil)); err == nil || !strings.Contains(err.Error(), "--json invalid JSON object") {
t.Fatalf("err=%v", err)
}
if err := BaseFieldCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "json": `[{"name":"a","type":"text"},{"name":"b","type":"text"}]`}, nil, nil)); err != nil {
t.Fatalf("array create validate err=%v", err)
}
if err := BaseFieldCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "json": `[{"name":"a","type":"text"},1]`}, nil, nil)); err == nil || !strings.Contains(err.Error(), "--json item 2 must be an object") {
t.Fatalf("err=%v", err)
}
if err := BaseFieldCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "json": `[{"name":"a","type":"formula"}]`}, nil, nil)); err == nil || !strings.Contains(err.Error(), "--i-have-read-guide is required") {
t.Fatalf("err=%v", err)
}
if err := BaseFieldCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "t", "json": `{"name":"f1","type":"formula"}`}, nil, nil)); err == nil || !strings.Contains(err.Error(), "--i-have-read-guide is required") {
t.Fatalf("err=%v", err)
}

View File

@@ -6,13 +6,10 @@ package base
import (
"context"
"strings"
"time"
"github.com/larksuite/cli/shortcuts/common"
)
var fieldCreateBatchDelay = time.Second
func dryRunFieldList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
offset := runtime.Int("offset")
if offset < 0 {
@@ -36,14 +33,12 @@ func dryRunFieldGet(_ context.Context, runtime *common.RuntimeContext) *common.D
func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
bodies, _ := parseFieldCreateBodies(pc, runtime.Str("json"))
dr := common.NewDryRunAPI().
body, _ := parseJSONObject(pc, runtime.Str("json"), "json")
return common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields").
Body(body).
Set("base_token", runtime.Str("base-token")).
Set("table_id", baseTableID(runtime))
for _, body := range bodies {
dr.POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields").Body(body)
}
return dr
}
func dryRunFieldUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
@@ -100,16 +95,11 @@ func validateFormulaLookupGuideAck(runtime *common.RuntimeContext, command strin
}
func validateFieldCreate(runtime *common.RuntimeContext) error {
bodies, err := parseFieldCreateBodies(newParseCtx(runtime), runtime.Str("json"))
body, err := validateFieldJSON(runtime)
if err != nil {
return err
}
for _, body := range bodies {
if err := validateFormulaLookupGuideAck(runtime, "+field-create", body); err != nil {
return err
}
}
return nil
return validateFormulaLookupGuideAck(runtime, "+field-create", body)
}
func validateFieldUpdate(runtime *common.RuntimeContext) error {
@@ -150,38 +140,17 @@ func executeFieldGet(runtime *common.RuntimeContext) error {
}
func executeFieldCreate(runtime *common.RuntimeContext) error {
bodies, err := parseFieldCreateBodies(newParseCtx(runtime), runtime.Str("json"))
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, runtime.Str("json"), "json")
if err != nil {
return err
}
fields := make([]interface{}, 0, len(bodies))
for idx, body := range bodies {
if idx > 0 && fieldCreateBatchDelay > 0 {
time.Sleep(fieldCreateBatchDelay)
}
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields"), nil, body)
if err != nil {
return err
}
fields = append(fields, data)
}
if len(fields) == 1 {
runtime.Out(map[string]interface{}{"field": fields[0], "created": true}, nil)
return nil
}
runtime.Out(map[string]interface{}{"fields": fields, "created": true, "total": len(fields)}, nil)
return nil
}
func parseFieldCreateBodies(pc *parseCtx, raw string) ([]map[string]interface{}, error) {
bodies, err := parseObjectList(pc, raw, "json")
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields"), nil, body)
if err != nil {
return nil, err
return err
}
if len(bodies) == 0 {
return nil, baseFlagErrorf("--json must contain at least one field JSON object")
}
return bodies, nil
runtime.Out(map[string]interface{}{"field": data, "created": true}, nil)
return nil
}
func executeFieldUpdate(runtime *common.RuntimeContext) error {

View File

@@ -18,7 +18,6 @@ func v2CreateFlags() []common.Flag {
return []common.Flag{
{Name: "title", Desc: "document title; when provided, the CLI prepends it to --content as <title>...</title> so the title wins over later content titles"},
{Name: "content", Desc: "document body; XML by default or Markdown when --doc-format markdown. " + docsContentSkillHelp + "; use --help for the latest command flags", Input: []string{common.File, common.Stdin}},
{Name: "reference-map", Desc: docsReferenceMapFlagDesc, Input: []string{common.File, common.Stdin}},
{Name: "doc-format", Desc: "content format; xml is default and supports richer DocxXML blocks, markdown imports plain Markdown", Default: "xml", Enum: []string{"xml", "markdown"}},
{Name: "parent-token", Desc: "parent folder token or wiki node token; mutually exclusive with --parent-position"},
{Name: "parent-position", Desc: "parent position such as my_library; mutually exclusive with --parent-token"},
@@ -33,8 +32,8 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
if runtime.Changed("title") && title == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--title must not be empty").WithParam("--title")
}
if err := validateDocsV2ReferenceMapFlags(runtime); err != nil {
return err
if runtime.Str("content") == "" && title == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required unless --title is provided").WithParam("--content")
}
if runtime.Str("parent-token") != "" && runtime.Str("parent-position") != "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parent-token and --parent-position are mutually exclusive").WithParams(
@@ -42,21 +41,11 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
errs.InvalidParam{Name: "--parent-position", Reason: "mutually exclusive with --parent-token"},
)
}
if runtime.Str("content") == "" && title == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required unless --title is provided").WithParam("--content")
}
if runtime.Str("content") != "" {
_, err := resolveDocsV2ContentReferenceMap(runtime)
return err
}
return nil
}
func dryRunCreateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, err := buildCreateBodyWithHTML5ReferenceMap(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
body := buildCreateBody(runtime)
desc := "OpenAPI: create document"
if runtime.IsBot() {
desc += ". After document creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new document."
@@ -68,10 +57,7 @@ func dryRunCreateV2(_ context.Context, runtime *common.RuntimeContext) *common.D
}
func executeCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
body, err := buildCreateBodyWithHTML5ReferenceMap(runtime)
if err != nil {
return err
}
body := buildCreateBody(runtime)
data, err := doDocAPI(runtime, "POST", "/open-apis/docs_ai/v1/documents", body)
if err != nil {
@@ -100,10 +86,7 @@ func buildCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
}
func buildCreateContent(runtime *common.RuntimeContext) string {
return buildCreateContentWithBody(runtime, runtime.Str("content"))
}
func buildCreateContentWithBody(runtime *common.RuntimeContext, content string) string {
content := runtime.Str("content")
title := strings.TrimSpace(runtime.Str("title"))
if title == "" {
return content

View File

@@ -14,7 +14,7 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)
const docsFetchExtraParam = `{"enable_user_cite_reference_map":true,"return_html5_block_data":true}`
const docsFetchExtraParam = `{"enable_user_cite_reference_map":true}`
// v2FetchFlags returns the flag definitions for the v2 (OpenAPI) fetch path.
func v2FetchFlags() []common.Flag {
@@ -71,9 +71,6 @@ func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error {
if err != nil {
return err
}
if err := processHTML5BlockReferenceMapForFetch(runtime, effectiveFetchFormat(runtime), ref.Token, data); err != nil {
return err
}
if warning := addFetchDetailDowngradeWarning(runtime, data); warning != "" && runtime.Format == "pretty" {
fmt.Fprintf(runtime.IO().ErrOut, "warning: %s\n", warning)
}

View File

@@ -505,14 +505,14 @@ func TestBuildFetchBodyIncludesFetchExtraParamByDefault(t *testing.T) {
if got["enable_user_cite_reference_map"] != true {
t.Fatalf("enable_user_cite_reference_map = %#v, want true in %#v", got["enable_user_cite_reference_map"], got)
}
if got["return_html5_block_data"] != true {
t.Fatalf("return_html5_block_data = %#v, want true in %#v", got["return_html5_block_data"], got)
if _, ok := got["return_html5_block_data"]; ok {
t.Fatalf("extra_param should not request html5 block data: %#v", got)
}
if _, ok := got["reference_map_mode"]; ok {
t.Fatalf("extra_param should not use legacy reference_map_mode: %#v", got)
}
if len(got) != 2 {
t.Fatalf("extra_param should only contain fetch reference_map and html5 data toggles: %#v", got)
if len(got) != 1 {
t.Fatalf("extra_param should only contain fetch reference_map toggle: %#v", got)
}
}
@@ -579,46 +579,6 @@ func TestDocsFetchIMMarkdownRequestsMarkdownFromAPI(t *testing.T) {
}
}
func TestDocsFetchIMMarkdownIgnoresHTML5BlockInsideCodeFence(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-im-markdown-code-fence"))
registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents/doxcnFetchIMMarkdownFence/fetch", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcnFetchIMMarkdownFence",
"revision_id": float64(1),
"content": "```xml\n<html5-block data-ref=\"html5_1\"></html5-block>\n```\n",
},
})
err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--doc", "doxcnFetchIMMarkdownFence",
"--doc-format", "im-markdown",
"--format", "json",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var envelope map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("decode output: %v\nraw=%s", err, stdout.String())
}
if errField, ok := envelope["error"]; ok {
t.Fatalf("fetch output should not contain error: %#v", errField)
}
data, _ := envelope["data"].(map[string]interface{})
doc, _ := data["document"].(map[string]interface{})
content, _ := doc["content"].(string)
if !strings.Contains(content, "```xml\n<html5-block data-ref=\"html5_1\"></html5-block>\n```") {
t.Fatalf("fenced html5-block should stay in content, got:\n%s", content)
}
if _, ok := doc["reference_map"]; ok {
t.Fatalf("fenced html5-block should not create reference_map side effects: %#v", doc["reference_map"])
}
}
func TestDocsFetchMarkdownDetailDowngradesToSimple(t *testing.T) {
t.Parallel()

View File

@@ -5,7 +5,6 @@ package doc
import (
"context"
"errors"
"os"
"strings"
"testing"
@@ -64,39 +63,6 @@ func TestDocsUpdateDryRunIgnoresAPIVersionCompatFlag(t *testing.T) {
}
}
func TestBuildUpdateBodyWithHTML5ReferenceMapReportsPathError(t *testing.T) {
t.Parallel()
runtime := newUpdateShortcutTestRuntime(t, "", map[string]string{
"content": `<html5-block path="@missing.html"></html5-block>`,
})
_, err := buildUpdateBodyWithHTML5ReferenceMap(runtime)
if err == nil {
t.Fatal("buildUpdateBodyWithHTML5ReferenceMap() succeeded, want error")
}
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("ProblemOf() ok = false for %T (%v)", err, err)
}
if p.Category != errs.CategoryValidation {
t.Fatalf("category = %q, want %q", p.Category, errs.CategoryValidation)
}
if p.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("subtype = %q, want %q", p.Subtype, errs.SubtypeInvalidArgument)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("error type = %T, want *errs.ValidationError", err)
}
if validationErr.Param != "path" {
t.Fatalf("param = %q, want path", validationErr.Param)
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("error should preserve os.ErrNotExist cause, got: %v", err)
}
}
func TestDocsUpdateV2ReferenceMapFlagIsPublicFileInput(t *testing.T) {
t.Parallel()

View File

@@ -24,9 +24,7 @@ var validCommandsV2 = map[string]bool{
"append": true,
}
const docsReferenceMapFlagDesc = "结构化 `reference_map` JSON object必须与 `--content` 一起使用。普通写入优先把结构写在正文里;`--reference-map` 主要用于保留或回放已有 `document.reference_map`。支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。"
const docsUpdateReferenceMapFlagDesc = docsReferenceMapFlagDesc
const docsUpdateReferenceMapFlagDesc = "结构化 `reference_map` JSON object `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map`。"
// v2UpdateFlags returns the flag definitions for the v2 (OpenAPI) update path.
func v2UpdateFlags() []common.Flag {
@@ -117,20 +115,13 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--command append requires --content").WithParam("--content")
}
}
if content != "" {
_, err := resolveDocsV2ContentReferenceMap(runtime)
return err
}
return nil
}
func dryRunUpdateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
// Validate has already accepted --doc; parseDocumentRef cannot fail here.
ref, _ := parseDocumentRef(runtime.Str("doc"))
body, err := buildUpdateBodyWithHTML5ReferenceMap(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
body, _ := buildUpdateBodyWithReferenceMap(runtime)
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token)
return common.NewDryRunAPI().
PUT(apiPath).
@@ -143,7 +134,7 @@ func executeUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
ref, _ := parseDocumentRef(runtime.Str("doc"))
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token)
body, err := buildUpdateBodyWithHTML5ReferenceMap(runtime)
body, err := buildUpdateBodyWithReferenceMap(runtime)
if err != nil {
return err
}

View File

@@ -1,696 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package doc
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/shortcuts/common"
)
const (
html5BlockTag = "html5-block"
html5BlockPathAttr = "path"
html5BlockDataRefAttr = "data-ref"
html5BlockDataAttr = "data"
html5BlockReferenceRoot = "doc-fetch-resources"
html5BlockReferenceMaxRaw = 1024
)
var (
html5BlockStartTagPattern = regexp.MustCompile(`(?is)<html5-block\b[^>]*>`)
html5BlockElementPattern = regexp.MustCompile(`(?is)<html5-block\b[^>]*>(.*?)</html5-block>`)
html5BlockSafeNamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
)
type html5BlockReferenceEntry struct {
Data string `json:"data,omitempty"`
Path string `json:"path,omitempty"`
UserID string `json:"user_id,omitempty"`
}
type html5BlockReferenceMap map[string]map[string]html5BlockReferenceEntry
type docsV2WriteInput struct {
Content string
ReferenceMap map[string]interface{}
}
type html5BlockAttr struct {
Name string
Value string
}
type html5BlockStartTag struct {
Attrs []html5BlockAttr
SelfClosing bool
}
func buildCreateBodyWithHTML5ReferenceMap(runtime *common.RuntimeContext) (map[string]interface{}, error) {
body := buildCreateBody(runtime)
if runtime.Str("content") == "" && !runtime.Changed("reference-map") {
return body, nil
}
input, err := resolveDocsV2ContentReferenceMap(runtime)
if err != nil {
return nil, err
}
body["content"] = buildCreateContentWithBody(runtime, input.Content)
if len(input.ReferenceMap) > 0 {
body["reference_map"] = input.ReferenceMap
}
return body, nil
}
func buildUpdateBodyWithHTML5ReferenceMap(runtime *common.RuntimeContext) (map[string]interface{}, error) {
body := buildUpdateBody(runtime)
input, err := resolveDocsV2ContentReferenceMap(runtime)
if err != nil {
return nil, err
}
if input.Content != "" {
body["content"] = input.Content
}
if len(input.ReferenceMap) > 0 {
body["reference_map"] = input.ReferenceMap
}
return body, nil
}
func validateDocsV2ReferenceMapFlags(runtime *common.RuntimeContext) error {
if runtime.Changed("reference-map") && runtime.Str("content") == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map requires --content").WithParam("--reference-map")
}
return nil
}
func resolveDocsV2ContentReferenceMap(runtime *common.RuntimeContext) (docsV2WriteInput, error) {
input := docsV2WriteInput{Content: runtime.Str("content")}
if raw := runtime.Str("reference-map"); strings.TrimSpace(raw) != "" {
refMap, err := parseReferenceMapObject(raw, "--reference-map")
if err != nil {
return docsV2WriteInput{}, err
}
input.ReferenceMap = refMap
}
return prepareDocsV2WriteInput(runtime, input)
}
func prepareDocsV2WriteInput(runtime *common.RuntimeContext, input docsV2WriteInput) (docsV2WriteInput, error) {
refMap := cloneReferenceMapObject(input.ReferenceMap)
html5RefMap, err := html5ReferenceMapFromObject(refMap)
if err != nil {
return docsV2WriteInput{}, err
}
content, html5RefMap, err := prepareHTML5BlockWriteContent(runtime, runtime.Str("doc-format"), input.Content, html5RefMap)
if err != nil {
return docsV2WriteInput{}, err
}
if err := resolveReferenceMapPaths(runtime, html5RefMap); err != nil {
return docsV2WriteInput{}, err
}
refMap = mergeHTML5ReferenceMap(refMap, html5RefMap)
return docsV2WriteInput{
Content: content,
ReferenceMap: refMap,
}, nil
}
func parseReferenceMapObject(raw string, label string) (map[string]interface{}, error) {
if len(bytes.TrimSpace([]byte(raw))) == 0 || string(bytes.TrimSpace([]byte(raw))) == "null" {
return nil, nil
}
var refMap map[string]interface{}
if err := json.Unmarshal([]byte(raw), &refMap); err != nil {
return nil, common.ValidationErrorf("%s is not valid reference_map JSON: %v", label, err).WithParam(label).WithCause(err)
}
return refMap, nil
}
func parseHTML5BlockReferenceMapBytes(raw []byte, label string) (html5BlockReferenceMap, error) {
if len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" {
return nil, nil
}
var refMap html5BlockReferenceMap
if err := json.Unmarshal(raw, &refMap); err != nil {
return nil, common.ValidationErrorf("%s is not valid reference_map JSON: %v", label, err).WithParam(label).WithCause(err)
}
return compactReferenceMap(refMap), nil
}
func prepareHTML5BlockWriteContent(runtime *common.RuntimeContext, format string, content string, refMap html5BlockReferenceMap) (string, html5BlockReferenceMap, error) {
if !strings.Contains(content, "<html5-block") {
return content, compactReferenceMap(refMap), nil
}
if err := validateHTML5BlockWriteElementBodies(format, content); err != nil {
return "", nil, err
}
refMap = cloneReferenceMap(refMap)
if refMap == nil {
refMap = html5BlockReferenceMap{}
}
ensureReferenceGroup(refMap, html5BlockTag)
nextRef := nextHTML5BlockRef(refMap)
rewrite := func(segment string) (string, error) {
return rewriteHTML5BlockStartTags(segment, func(raw string) (string, error) {
tag, err := parseHTML5BlockStartTag(raw)
if err != nil {
return "", common.ValidationErrorf("invalid html5-block tag: %v", err).WithParam("html5-block")
}
if tag.hasAttr(html5BlockDataAttr) {
return "", common.ValidationErrorf("html5-block data is reserved for SDK internals; use data-ref with reference_map or path=\"@relative.html\"").WithParam("html5-block")
}
pathValue, hasPath := tag.attr(html5BlockPathAttr)
dataRef, hasDataRef := tag.attr(html5BlockDataRefAttr)
if hasPath && hasDataRef {
return "", common.ValidationErrorf("html5-block cannot contain both path and data-ref").WithParam("html5-block")
}
if hasDataRef {
ref := strings.TrimSpace(dataRef)
if ref == "" {
return "", common.ValidationErrorf("html5-block data-ref cannot be empty").WithParam("data-ref")
}
if _, ok := refMap[html5BlockTag][ref]; !ok {
return "", common.ValidationErrorf("reference_map.%s.%s is required for html5-block data-ref", html5BlockTag, ref).WithParam("reference_map")
}
return tag.render(false), nil
}
if !hasPath {
return "", common.ValidationErrorf("html5-block requires path=\"@relative.html\" or data-ref with reference_map").WithParam("html5-block")
}
data, err := readHTML5BlockPath(runtime, pathValue, "html5-block path")
if err != nil {
return "", err
}
ref := nextRef()
refMap[html5BlockTag][ref] = html5BlockReferenceEntry{Data: data}
tag.removeAttrs(html5BlockPathAttr, html5BlockDataRefAttr, html5BlockDataAttr)
tag.Attrs = append(tag.Attrs, html5BlockAttr{Name: html5BlockDataRefAttr, Value: ref})
return tag.render(false), nil
})
}
var (
out string
err error
)
if strings.TrimSpace(format) == "markdown" {
out = applyOutsideCodeFences(content, func(segment string) string {
if err != nil {
return segment
}
outSegment, rewriteErr := rewrite(segment)
if rewriteErr != nil {
err = rewriteErr
return segment
}
return outSegment
})
} else {
out, err = rewrite(content)
}
if err != nil {
return "", nil, err
}
return out, compactReferenceMap(refMap), nil
}
func validateHTML5BlockWriteElementBodies(format string, content string) error {
validateSegment := func(segment string) error {
matches := html5BlockElementPattern.FindAllStringSubmatchIndex(segment, -1)
for _, match := range matches {
if len(match) < 4 || match[2] < 0 || match[3] < 0 {
continue
}
if strings.TrimSpace(segment[match[2]:match[3]]) != "" {
return common.ValidationErrorf("html5-block content must be loaded from path=\"@relative.html\" or reference_map; remove content between <html5-block> and </html5-block>").WithParam("html5-block")
}
}
return nil
}
if strings.TrimSpace(format) != "markdown" {
return validateSegment(content)
}
var validateErr error
_ = applyOutsideCodeFences(content, func(segment string) string {
if validateErr != nil {
return segment
}
validateErr = validateSegment(segment)
return segment
})
return validateErr
}
func processHTML5BlockReferenceMapForFetch(runtime *common.RuntimeContext, format string, docToken string, data map[string]interface{}) error {
doc, _ := data["document"].(map[string]interface{})
if doc == nil {
return nil
}
content, _ := doc["content"].(string)
if !hasProcessableHTML5Block(format, content) {
return nil
}
refMap, err := referenceMapFromDocument(doc)
if err != nil {
return err
}
group := refMap[html5BlockTag]
if group == nil {
return common.ValidationErrorf("document.reference_map.%s is required for fetched html5-block content", html5BlockTag).WithParam("reference_map")
}
if err := validateFetchedHTML5BlockRefs(format, content, refMap); err != nil {
return err
}
changed := false
for ref, entry := range group {
if entry.Data == "" || len([]byte(entry.Data)) <= html5BlockReferenceMaxRaw {
continue
}
relPath, err := writeHTML5BlockReferenceFile(runtime, docToken, ref, entry.Data)
if err != nil {
return err
}
entry.Data = ""
entry.Path = "@" + filepath.ToSlash(relPath)
group[ref] = entry
changed = true
}
if changed {
doc["reference_map"] = refMap
}
return nil
}
func referenceMapFromDocument(doc map[string]interface{}) (html5BlockReferenceMap, error) {
raw, ok := doc["reference_map"]
if !ok || raw == nil {
return nil, common.ValidationErrorf("document.reference_map is required for fetched html5-block content").WithParam("reference_map")
}
refMap, err := referenceMapFromValue(raw, "document.reference_map")
if err != nil {
return nil, err
}
if len(refMap) == 0 {
return nil, common.ValidationErrorf("document.reference_map is required for fetched html5-block content").WithParam("reference_map")
}
return refMap, nil
}
func referenceMapFromValue(value interface{}, label string) (html5BlockReferenceMap, error) {
if typed, ok := value.(html5BlockReferenceMap); ok {
return compactReferenceMap(typed), nil
}
raw, err := json.Marshal(value)
if err != nil {
return nil, common.ValidationErrorf("%s is not valid reference_map JSON: %v", label, err).WithParam("reference_map").WithCause(err)
}
return parseHTML5BlockReferenceMapBytes(raw, label)
}
func validateFetchedHTML5BlockRefs(format string, content string, refMap html5BlockReferenceMap) error {
validateSegment := func(segment string) error {
_, err := rewriteHTML5BlockStartTags(segment, func(raw string) (string, error) {
tag, parseErr := parseHTML5BlockStartTag(raw)
if parseErr != nil {
return raw, common.ValidationErrorf("invalid html5-block tag in fetched content: %v", parseErr).WithParam("html5-block")
}
ref, ok := tag.attr(html5BlockDataRefAttr)
if !ok || strings.TrimSpace(ref) == "" {
return raw, common.ValidationErrorf("fetched html5-block is missing data-ref; cannot resolve HTML reference").WithParam("html5-block")
}
ref = strings.TrimSpace(ref)
if _, ok := refMap[html5BlockTag][ref]; !ok {
return raw, common.ValidationErrorf("document.reference_map.%s.%s is missing; cannot resolve html5-block. Re-run fetch or check that the upstream document.reference_map field includes this ref.", html5BlockTag, ref).WithParam("reference_map")
}
return raw, nil
})
return err
}
if strings.TrimSpace(format) != "markdown" {
return validateSegment(content)
}
var validateErr error
_ = applyOutsideCodeFences(content, func(segment string) string {
if validateErr != nil {
return segment
}
validateErr = validateSegment(segment)
return segment
})
return validateErr
}
func resolveReferenceMapPaths(runtime *common.RuntimeContext, refMap html5BlockReferenceMap) error {
for typ, group := range refMap {
for ref, entry := range group {
if strings.TrimSpace(entry.Path) == "" {
continue
}
if entry.Data != "" {
return common.ValidationErrorf("reference_map.%s.%s must use either data or path, not both", typ, ref).WithParam("reference_map")
}
data, err := readHTML5BlockPath(runtime, entry.Path, fmt.Sprintf("reference_map.%s.%s.path", typ, ref))
if err != nil {
return err
}
entry.Data = data
entry.Path = ""
group[ref] = entry
}
}
return nil
}
func readHTML5BlockPath(runtime *common.RuntimeContext, pathValue string, label string) (string, error) {
pathRaw := strings.TrimSpace(pathValue)
if !strings.HasPrefix(pathRaw, "@") {
return "", common.ValidationErrorf("%s %q must start with @, for example @widget.html", label, pathValue).WithParam("path")
}
relPath := strings.TrimSpace(strings.TrimPrefix(pathRaw, "@"))
if relPath == "" {
return "", common.ValidationErrorf("%s cannot be empty after @", label).WithParam("path")
}
clean := filepath.Clean(relPath)
if filepath.IsAbs(clean) || clean == "." || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
return "", common.ValidationErrorf("%s %q must be a relative path within the current working directory", label, pathValue).WithParam("path")
}
if strings.ToLower(filepath.Ext(clean)) != ".html" {
return "", common.ValidationErrorf("%s %q must point to a .html file", label, pathValue).WithParam("path")
}
data, err := cmdutil.ReadInputFile(runtime.FileIO(), clean)
if err != nil {
return "", common.ValidationErrorf("%s %q cannot be read from the current working directory; check that the file exists relative to where lark-cli is running: %v", label, clean, err).WithParam("path").WithCause(err)
}
return string(data), nil
}
func hasProcessableHTML5Block(format string, content string) bool {
if !strings.Contains(content, "<html5-block") {
return false
}
if strings.TrimSpace(format) != "markdown" {
return true
}
found := false
_ = applyOutsideCodeFences(content, func(segment string) string {
if strings.Contains(segment, "<html5-block") {
found = true
}
return segment
})
return found
}
func applyOutsideCodeFences(content string, fn func(segment string) string) string {
var out strings.Builder
var segment strings.Builder
inFence := false
flush := func() {
if segment.Len() == 0 {
return
}
out.WriteString(fn(segment.String()))
segment.Reset()
}
for _, line := range strings.SplitAfter(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
if !inFence {
flush()
inFence = true
} else {
inFence = false
}
out.WriteString(line)
continue
}
if inFence {
out.WriteString(line)
} else {
segment.WriteString(line)
}
}
flush()
return out.String()
}
func cloneReferenceMap(refMap html5BlockReferenceMap) html5BlockReferenceMap {
if len(refMap) == 0 {
return nil
}
out := make(html5BlockReferenceMap, len(refMap))
for typ, group := range refMap {
if len(group) == 0 {
continue
}
outGroup := make(map[string]html5BlockReferenceEntry, len(group))
for ref, entry := range group {
outGroup[ref] = entry
}
out[typ] = outGroup
}
return out
}
func cloneReferenceMapObject(refMap map[string]interface{}) map[string]interface{} {
if len(refMap) == 0 {
return nil
}
out := make(map[string]interface{}, len(refMap))
for key, value := range refMap {
out[key] = value
}
return out
}
func html5ReferenceMapFromObject(refMap map[string]interface{}) (html5BlockReferenceMap, error) {
if len(refMap) == 0 {
return nil, nil
}
group, ok := refMap[html5BlockTag]
if !ok || group == nil {
return nil, nil
}
return referenceMapFromValue(map[string]interface{}{html5BlockTag: group}, "reference_map."+html5BlockTag)
}
func mergeHTML5ReferenceMap(refMap map[string]interface{}, html5RefMap html5BlockReferenceMap) map[string]interface{} {
group := html5RefMap[html5BlockTag]
if len(group) == 0 {
return refMap
}
if refMap == nil {
refMap = map[string]interface{}{}
}
refMap[html5BlockTag] = group
return refMap
}
func compactReferenceMap(refMap html5BlockReferenceMap) html5BlockReferenceMap {
if len(refMap) == 0 {
return nil
}
out := make(html5BlockReferenceMap, len(refMap))
for typ, group := range refMap {
if len(group) == 0 {
continue
}
out[typ] = group
}
if len(out) == 0 {
return nil
}
return out
}
func ensureReferenceGroup(refMap html5BlockReferenceMap, typ string) {
if refMap[typ] == nil {
refMap[typ] = map[string]html5BlockReferenceEntry{}
}
}
func nextHTML5BlockRef(refMap html5BlockReferenceMap) func() string {
next := 1
return func() string {
for {
ref := fmt.Sprintf("html5_%d", next)
next++
if _, exists := refMap[html5BlockTag][ref]; !exists {
return ref
}
}
}
}
func writeHTML5BlockReferenceFile(runtime *common.RuntimeContext, docToken string, ref string, html string) (string, error) {
if !isSafeHTML5BlockResourceName(docToken) {
return "", common.ValidationErrorf("document_id %q cannot be used as a resource directory name", docToken).WithParam("document_id")
}
if !isSafeHTML5BlockResourceName(ref) {
return "", common.ValidationErrorf("html5-block data-ref %q cannot be used as a file name", ref).WithParam("data-ref")
}
relPath := filepath.Join(html5BlockReferenceRoot, docToken, ref+".html")
data := []byte(html)
_, err := runtime.FileIO().Save(relPath, fileio.SaveOptions{
ContentType: "text/html; charset=utf-8",
ContentLength: int64(len(data)),
}, bytes.NewReader(data))
if err != nil {
if errors.Is(err, fileio.ErrPathValidation) {
return "", common.ValidationErrorf("cannot write html5-block reference file %q: %v", relPath, err).WithParam("reference_map").WithCause(err)
}
return "", errs.NewInternalError(errs.SubtypeFileIO, "cannot write html5-block reference file %q: %v", relPath, err).WithCause(err)
}
return relPath, nil
}
func isSafeHTML5BlockResourceName(name string) bool {
return name != "." && name != ".." && html5BlockSafeNamePattern.MatchString(name)
}
func rewriteHTML5BlockStartTags(content string, fn func(raw string) (string, error)) (string, error) {
var rewriteErr error
out := html5BlockStartTagPattern.ReplaceAllStringFunc(content, func(raw string) string {
if rewriteErr != nil {
return raw
}
rewritten, err := fn(raw)
if err != nil {
rewriteErr = err
return raw
}
return rewritten
})
if rewriteErr != nil {
return "", rewriteErr
}
return out, nil
}
func parseHTML5BlockStartTag(raw string) (html5BlockStartTag, error) {
trimmed := strings.TrimSpace(raw)
selfClosing := strings.HasSuffix(trimmed, "/>")
decoder := xml.NewDecoder(strings.NewReader(raw))
for {
tok, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return html5BlockStartTag{}, err
}
start, ok := tok.(xml.StartElement)
if !ok {
continue
}
if start.Name.Local != html5BlockTag {
return html5BlockStartTag{}, fmt.Errorf("expected <%s>, got <%s>", html5BlockTag, start.Name.Local) //nolint:forbidigo // intermediate parse helper; callers wrap with typed validation errors.
}
attrs := make([]html5BlockAttr, 0, len(start.Attr))
for _, attr := range start.Attr {
attrs = append(attrs, html5BlockAttr{Name: attr.Name.Local, Value: attr.Value})
}
return html5BlockStartTag{Attrs: attrs, SelfClosing: selfClosing}, nil
}
return html5BlockStartTag{}, fmt.Errorf("missing start element") //nolint:forbidigo // intermediate parse helper; callers wrap with typed validation errors.
}
func (t html5BlockStartTag) attr(name string) (string, bool) {
for _, attr := range t.Attrs {
if attr.Name == name {
return attr.Value, true
}
}
return "", false
}
func (t html5BlockStartTag) hasAttr(name string) bool {
_, ok := t.attr(name)
return ok
}
func (t *html5BlockStartTag) removeAttrs(names ...string) {
remove := make(map[string]struct{}, len(names))
for _, name := range names {
remove[name] = struct{}{}
}
attrs := t.Attrs[:0]
for _, attr := range t.Attrs {
if _, ok := remove[attr.Name]; ok {
continue
}
attrs = append(attrs, attr)
}
t.Attrs = attrs
}
func (t html5BlockStartTag) render(selfClosing bool) string {
var b strings.Builder
b.WriteByte('<')
b.WriteString(html5BlockTag)
for _, attr := range t.Attrs {
b.WriteByte(' ')
b.WriteString(attr.Name)
b.WriteString(`="`)
b.WriteString(escapeXMLAttr(attr.Value))
b.WriteByte('"')
}
if selfClosing {
b.WriteString("/>")
} else {
b.WriteByte('>')
}
if t.SelfClosing && !selfClosing {
b.WriteString("</")
b.WriteString(html5BlockTag)
b.WriteByte('>')
}
return b.String()
}
func escapeXMLAttr(value string) string {
var b strings.Builder
for _, r := range value {
switch r {
case '&':
b.WriteString("&amp;")
case '<':
b.WriteString("&lt;")
case '>':
b.WriteString("&gt;")
case '"':
b.WriteString("&quot;")
case '\'':
b.WriteString("&apos;")
default:
b.WriteRune(r)
}
}
return b.String()
}

View File

@@ -1,563 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package doc
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/shortcuts/common"
)
func TestDocsV2ReferenceMapFlagIsPublicFileInput(t *testing.T) {
for name, flags := range map[string][]common.Flag{
"create": v2CreateFlags(),
"update": v2UpdateFlags(),
} {
t.Run(name, func(t *testing.T) {
flag := findDocsTestFlag(flags, "reference-map")
if flag.Name == "" {
t.Fatal("reference-map flag not found")
}
if flag.Hidden {
t.Fatal("reference-map flag should be public")
}
if !hasDocsTestInput(flag, common.File) || !hasDocsTestInput(flag, common.Stdin) {
t.Fatalf("reference-map Input = %#v, want file and stdin", flag.Input)
}
if !strings.Contains(flag.Desc, "@reference-map.json") {
t.Fatalf("reference-map help should mention @file support, got %q", flag.Desc)
}
})
}
}
func TestDocsV2InputFlagIsNotAvailable(t *testing.T) {
for name, flags := range map[string][]common.Flag{
"create": v2CreateFlags(),
"update": v2UpdateFlags(),
} {
t.Run(name, func(t *testing.T) {
for _, flag := range flags {
if flag.Name == "input" {
t.Fatalf("%s should not expose input flag", name)
}
}
})
}
}
func TestDocsUpdateV2ReferenceMapPreservesGenericGroups(t *testing.T) {
t.Parallel()
runtime := newUpdateShortcutTestRuntime(t, "", map[string]string{
"command": "append",
"content": `<p><widget data-ref="r1"></widget></p>`,
"reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`,
})
body, err := buildUpdateBodyWithHTML5ReferenceMap(runtime)
if err != nil {
t.Fatalf("buildUpdateBodyWithHTML5ReferenceMap: %v", err)
}
refMap, ok := body["reference_map"].(map[string]interface{})
if !ok {
t.Fatalf("reference_map = %#v, want object", body["reference_map"])
}
widget, _ := refMap["widget"].(map[string]interface{})
r1, _ := widget["r1"].(map[string]interface{})
if got := r1["label"]; got != "widget-ref-value" {
t.Fatalf("reference_map.widget.r1.label = %#v, want widget-ref-value; body=%#v", got, body)
}
}
func TestDocsCreateV2HTML5BlockReferenceMapFromPath(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("widget.html", []byte("<html><body>hello</body></html>"), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
f, stdout, _, reg := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
stub := registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_new_doc",
"revision_id": float64(1),
},
})
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<title>demo</title><html5-block path="@widget.html"></html5-block>`,
"--as", "user",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeRequestBody(t, stub.CapturedBody)
if got := body["content"].(string); !strings.Contains(got, `<html5-block data-ref="html5_1"></html5-block>`) {
t.Fatalf("content was not rewritten with data-ref: %s", got)
}
refMap := decodeHTML5ReferenceMap(t, body["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<html><body>hello</body></html>" {
t.Fatalf("reference_map html data = %q", got)
}
if _, ok := body["resources"]; ok {
t.Fatalf("request body must not use resources: %#v", body)
}
}
func findDocsTestFlag(flags []common.Flag, name string) common.Flag {
for _, flag := range flags {
if flag.Name == name {
return flag
}
}
return common.Flag{}
}
func hasDocsTestInput(flag common.Flag, input string) bool {
for _, item := range flag.Input {
if item == input {
return true
}
}
return false
}
func TestDocsUpdateV2HTML5BlockReferenceMapFromPath(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("widget.html", []byte("<section>updated</section>"), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-html5-update"))
stub := registerDocsAIStub(reg, "PUT", "/open-apis/docs_ai/v1/documents/doxcn_doc", map[string]interface{}{
"document": map[string]interface{}{
"revision_id": float64(2),
"new_blocks": []interface{}{
map[string]interface{}{
"block_type": "html5-block",
"block_id": "blk_html5",
"block_token": "boardXXXX",
},
},
},
"result": "success",
})
err := mountAndRunDocs(t, DocsUpdate, []string{
"+update",
"--api-version", "v2",
"--doc", "doxcn_doc",
"--command", "append",
"--content", `<html5-block path="@widget.html"></html5-block>`,
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeRequestBody(t, stub.CapturedBody)
if got := body["content"].(string); got != `<html5-block data-ref="html5_1"></html5-block>` {
t.Fatalf("content = %q", got)
}
refMap := decodeHTML5ReferenceMap(t, body["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<section>updated</section>" {
t.Fatalf("reference_map html data = %q", got)
}
var envelope map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("decode stdout: %v\n%s", err, stdout.String())
}
data, _ := envelope["data"].(map[string]interface{})
doc, _ := data["document"].(map[string]interface{})
if blocks, _ := doc["new_blocks"].([]interface{}); len(blocks) != 1 {
t.Fatalf("new_blocks not preserved in stdout: %#v", doc)
}
}
func TestDocsFetchV2HTML5BlockKeepsSmallReferenceMapInline(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-html5-fetch"))
registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents/doxcn_fetch/fetch", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_fetch",
"revision_id": float64(3),
"content": `<docx><html5-block data-ref="html5_1"></html5-block></docx>`,
"reference_map": map[string]interface{}{
"html5-block": map[string]interface{}{
"html5_1": map[string]interface{}{"data": "<html><main>fetched</main></html>"},
},
},
},
"tips": "must_read_html_code",
})
err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--api-version", "v2",
"--doc", "doxcn_fetch",
"--format", "json",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
written := filepath.Join(dir, html5BlockReferenceRoot, "doxcn_fetch", "html5_1.html")
if _, err := os.Stat(written); err == nil {
t.Fatalf("small html should stay inline, got file %s", written)
}
var envelope map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("decode stdout: %v\n%s", err, stdout.String())
}
data, _ := envelope["data"].(map[string]interface{})
doc, _ := data["document"].(map[string]interface{})
if got := doc["content"].(string); !strings.Contains(got, `<html5-block data-ref="html5_1"></html5-block>`) {
t.Fatalf("content should keep data-ref: %s", got)
}
refMap := decodeHTML5ReferenceMap(t, doc["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<html><main>fetched</main></html>" {
t.Fatalf("reference_map html data = %q", got)
}
if _, ok := doc["resources"]; ok {
t.Fatalf("fetch output must not use resources: %#v", doc)
}
if _, ok := data["suggestions"]; ok {
t.Fatalf("CLI must not add suggestions; service tips is enough: %#v", data["suggestions"])
}
if got := data["tips"]; got != "must_read_html_code" {
t.Fatalf("tips should be preserved from service response, got %#v", got)
}
}
func TestDocsFetchV2HTML5BlockLargeReferenceMapUsesPath(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
largeHTML := "<html><main>" + strings.Repeat("x", html5BlockReferenceMaxRaw+1) + "</main></html>"
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-html5-fetch-large"))
registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents/doxcn_fetch/fetch", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_fetch",
"revision_id": float64(3),
"content": `<docx><html5-block data-ref="html5_1"></html5-block></docx>`,
"reference_map": map[string]interface{}{
"html5-block": map[string]interface{}{
"html5_1": map[string]interface{}{"data": largeHTML},
},
},
},
})
err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--api-version", "v2",
"--doc", "doxcn_fetch",
"--format", "json",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
written := filepath.Join(dir, html5BlockReferenceRoot, "doxcn_fetch", "html5_1.html")
raw, err := os.ReadFile(written)
if err != nil {
t.Fatalf("ReadFile(%s) error: %v", written, err)
}
if string(raw) != largeHTML {
t.Fatalf("materialized html = %q", raw)
}
var envelope map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("decode stdout: %v\n%s", err, stdout.String())
}
data, _ := envelope["data"].(map[string]interface{})
doc, _ := data["document"].(map[string]interface{})
if got := doc["content"].(string); strings.Contains(got, `path="@`) || !strings.Contains(got, `data-ref="html5_1"`) {
t.Fatalf("content should keep data-ref and not path: %s", got)
}
refMap := decodeHTML5ReferenceMap(t, doc["reference_map"])
entry := refMap[html5BlockTag]["html5_1"]
if entry.Data != "" || entry.Path != "@doc-fetch-resources/doxcn_fetch/html5_1.html" {
t.Fatalf("large html should be represented as path, got %#v", entry)
}
}
func TestDocsCreateV2HTML5BlockReferenceMapAdvancedInput(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
stub := registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_new_doc",
"revision_id": float64(1),
},
})
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block data-ref="html5_1"></html5-block>`,
"--reference-map", `{"html5-block":{"html5_1":{"data":"<html></html>"}}}`,
"--as", "user",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeRequestBody(t, stub.CapturedBody)
if got := body["content"].(string); got != `<html5-block data-ref="html5_1"></html5-block>` {
t.Fatalf("content = %q", got)
}
refMap := decodeHTML5ReferenceMap(t, body["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<html></html>" {
t.Fatalf("reference_map html data = %q", got)
}
}
func TestDocsCreateV2HTML5BlockReferenceMapFromFile(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("reference-map.json", []byte(`{"html5-block":{"html5_1":{"data":"<html>from file</html>"}}}`), 0o600); err != nil {
t.Fatalf("WriteFile(reference-map.json) error: %v", err)
}
f, stdout, _, reg := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
stub := registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_new_doc",
"revision_id": float64(1),
},
})
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block data-ref="html5_1"></html5-block>`,
"--reference-map", "@reference-map.json",
"--as", "user",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeRequestBody(t, stub.CapturedBody)
refMap := decodeHTML5ReferenceMap(t, body["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<html>from file</html>" {
t.Fatalf("reference_map html data = %q", got)
}
}
func TestDocsCreateV2HTML5BlockRejectsMissingReferenceMap(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block data-ref="html5_1"></html5-block>`,
"--as", "user",
})
if err == nil || !strings.Contains(err.Error(), `reference_map.html5-block.html5_1 is required`) {
t.Fatalf("expected missing reference_map error, got: %v", err)
}
}
func TestDocsCreateV2HTML5BlockRejectsInternalDataAttr(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block data="PGh0bWw+PC9odG1sPg=="></html5-block>`,
"--as", "user",
})
if err == nil || !strings.Contains(err.Error(), `html5-block data is reserved for SDK internals`) {
t.Fatalf("expected internal data attr error, got: %v", err)
}
}
func TestDocsCreateV2HTML5BlockPathReadFailure(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block path="@missing.html"></html5-block>`,
"--as", "user",
})
if err == nil || !strings.Contains(err.Error(), `html5-block path "missing.html" cannot be read from the current working directory`) {
t.Fatalf("expected path read error, got: %v", err)
}
}
func TestDocsCreateV2HTML5BlockRejectsInlineContent(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("widget.html", []byte("<section>from file</section>"), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--content", `<html5-block path="@widget.html"><section>inline</section></html5-block>`,
"--as", "user",
})
if err == nil || !strings.Contains(err.Error(), `html5-block content must be loaded from path="@relative.html"`) {
t.Fatalf("expected inline content error, got: %v", err)
}
}
func TestDocsFetchV2MissingHTML5BlockReferenceFails(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-html5-fetch-missing"))
registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents/doxcn_fetch/fetch", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_fetch",
"revision_id": float64(3),
"content": `<docx><html5-block data-ref="html5_missing"></html5-block></docx>`,
"reference_map": map[string]interface{}{
"html5-block": map[string]interface{}{
"html5_1": map[string]interface{}{"data": "<html></html>"},
},
},
},
})
err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--api-version", "v2",
"--doc", "doxcn_fetch",
"--format", "json",
"--as", "user",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "Re-run fetch or check that the upstream document.reference_map field includes this ref") {
t.Fatalf("expected missing reference_map error, got: %v", err)
}
}
func TestHTML5BlockMarkdownCodeFenceIsIgnored(t *testing.T) {
for _, fence := range []string{"```", "~~~"} {
t.Run(fence, func(t *testing.T) {
content := fence + "xml\n<html5-block data-ref=\"html5_1\"></html5-block>\n" + fence + "\n"
if hasProcessableHTML5Block("markdown", content) {
t.Fatalf("html5-block inside markdown code fence should be ignored")
}
})
}
}
func TestWriteHTML5BlockReferenceFileRejectsDotNames(t *testing.T) {
runtime := newFetchShortcutTestRuntime(t, "", nil)
tests := []struct {
name string
docToken string
ref string
want string
}{
{name: "dot doc token", docToken: ".", ref: "html5_1", want: "document_id"},
{name: "dotdot doc token", docToken: "..", ref: "html5_1", want: "document_id"},
{name: "dot ref", docToken: "doxcn_fetch", ref: ".", want: "data-ref"},
{name: "dotdot ref", docToken: "doxcn_fetch", ref: "..", want: "data-ref"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := writeHTML5BlockReferenceFile(runtime, tt.docToken, tt.ref, "<html></html>")
if err == nil || !strings.Contains(err.Error(), tt.want) {
t.Fatalf("writeHTML5BlockReferenceFile() error = %v, want %q", err, tt.want)
}
})
}
}
func TestPrepareHTML5BlockWriteContentMarkdownRaw(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
if err := os.WriteFile("widget.html", []byte("<html><body>markdown</body></html>"), 0o600); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
f, stdout, _, reg := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
stub := registerDocsAIStub(reg, "POST", "/open-apis/docs_ai/v1/documents", map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcn_new_doc",
"revision_id": float64(1),
},
})
err := runDocsCreateShortcut(t, f, stdout, []string{
"+create",
"--api-version", "v2",
"--doc-format", "markdown",
"--content", "before\n<html5-block path=\"@widget.html\"></html5-block>\nafter",
"--as", "user",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeRequestBody(t, stub.CapturedBody)
if got := body["content"].(string); !strings.Contains(got, `<html5-block data-ref="html5_1"></html5-block>`) {
t.Fatalf("content was not rewritten: %s", got)
}
refMap := decodeHTML5ReferenceMap(t, body["reference_map"])
if got := refMap[html5BlockTag]["html5_1"].Data; got != "<html><body>markdown</body></html>" {
t.Fatalf("reference_map html data = %q", got)
}
}
func registerDocsAIStub(reg *httpmock.Registry, method string, url string, data map[string]interface{}) *httpmock.Stub {
stub := &httpmock.Stub{
Method: method,
URL: url,
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": data,
},
}
reg.Register(stub)
return stub
}
func decodeRequestBody(t *testing.T, raw []byte) map[string]interface{} {
t.Helper()
var body map[string]interface{}
if err := json.Unmarshal(bytes.TrimSpace(raw), &body); err != nil {
t.Fatalf("decode request body: %v\n%s", err, raw)
}
return body
}
func decodeHTML5ReferenceMap(t *testing.T, raw interface{}) html5BlockReferenceMap {
t.Helper()
data, err := json.Marshal(raw)
if err != nil {
t.Fatalf("marshal reference_map: %v\n%#v", err, raw)
}
var refMap html5BlockReferenceMap
if err := json.Unmarshal(data, &refMap); err != nil {
t.Fatalf("decode reference_map: %v\n%s", err, data)
}
return refMap
}

View File

@@ -37,16 +37,11 @@ const (
)
type drivePullItem struct {
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
SourceID string `json:"source_id,omitempty"`
Action string `json:"action"`
Error string `json:"error,omitempty"`
Phase string `json:"phase,omitempty"`
ErrorClass string `json:"error_class,omitempty"`
Code int `json:"code,omitempty"`
Subtype string `json:"subtype,omitempty"`
Retryable *bool `json:"retryable,omitempty"`
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
SourceID string `json:"source_id,omitempty"`
Action string `json:"action"`
Error string `json:"error,omitempty"`
}
type drivePullTarget struct {
@@ -194,9 +189,6 @@ var DrivePull = common.Shortcut{
sort.Strings(downloadablePaths)
for _, rel := range downloadablePaths {
if drivePullHasTerminalFailure(items) {
break
}
targetFile := remoteFiles[rel]
downloadToken := targetFile.DownloadToken
itemFileToken := targetFile.ItemFileToken
@@ -212,9 +204,13 @@ var DrivePull = common.Shortcut{
// pre-existing file under --if-exists=skip silently
// hides the conflict. Surface as a failure.
if info.IsDir() {
conflictErr := errs.NewValidationError(errs.SubtypeFailedPrecondition, "local path is a directory, remote is a regular file: %s", target)
item, _ := drivePullFailedItem(rel, itemFileToken, itemSourceID, "failed", "local", conflictErr)
items = append(items, item)
items = append(items, drivePullItem{
RelPath: rel,
FileToken: itemFileToken,
SourceID: itemSourceID,
Action: "failed",
Error: fmt.Sprintf("local path is a directory, remote is a regular file: %s", target),
})
failed++
downloadFailed++
continue
@@ -227,14 +223,9 @@ var DrivePull = common.Shortcut{
}
if err := drivePullDownload(ctx, runtime, downloadToken, target, targetFile.ModifiedTime); err != nil {
item, terminal := drivePullFailedItem(rel, itemFileToken, itemSourceID, "failed", "download", err)
items = append(items, item)
items = append(items, drivePullItem{RelPath: rel, FileToken: itemFileToken, SourceID: itemSourceID, Action: "failed", Error: err.Error()})
failed++
downloadFailed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +pull after terminal %s failure: %v\n", item.Phase, err)
break
}
continue
}
items = append(items, drivePullItem{RelPath: rel, FileToken: itemFileToken, SourceID: itemSourceID, Action: "downloaded"})
@@ -260,8 +251,7 @@ var DrivePull = common.Shortcut{
for _, absPath := range localAbsPaths {
rel, relErr := filepath.Rel(safeRoot, absPath)
if relErr != nil {
item, _ := drivePullFailedItem(absPath, "", "", "delete_failed", "delete_local", relErr)
items = append(items, item)
items = append(items, drivePullItem{RelPath: absPath, Action: "delete_failed", Error: relErr.Error()})
failed++
continue
}
@@ -281,9 +271,7 @@ var DrivePull = common.Shortcut{
// acceptable here. Shortcuts cannot import internal/vfs
// directly (depguard rule shortcuts-no-vfs).
if err := os.Remove(absPath); err != nil { //nolint:forbidigo // see comment above
deleteErr := errs.NewInternalError(errs.SubtypeFileIO, "delete local %q: %s", rel, err).WithCause(err)
item, _ := drivePullFailedItem(rel, "", "", "delete_failed", "delete_local", deleteErr)
items = append(items, item)
items = append(items, drivePullItem{RelPath: rel, Action: "delete_failed", Error: err.Error()})
failed++
continue
}
@@ -298,7 +286,6 @@ var DrivePull = common.Shortcut{
"skipped": skipped,
"failed": failed,
"deleted_local": deletedLocal,
"aborted": drivePullHasTerminalFailure(items),
},
"items": items,
}
@@ -330,32 +317,6 @@ var DrivePull = common.Shortcut{
},
}
func drivePullFailedItem(relPath, fileToken, sourceID, action, phase string, err error) (drivePullItem, bool) {
decision := driveClassifyBatchFailure(err)
item := drivePullItem{
RelPath: relPath,
FileToken: fileToken,
SourceID: sourceID,
Action: action,
Error: err.Error(),
Phase: phase,
ErrorClass: decision.Class,
Code: decision.Code,
Subtype: decision.Subtype,
Retryable: driveBoolPtr(decision.Retryable),
}
return item, decision.Terminal
}
func drivePullHasTerminalFailure(items []drivePullItem) bool {
for _, item := range items {
if driveTerminalBatchErrorClass(item.ErrorClass) {
return true
}
}
return false
}
// drivePullDownload streams one Drive file into the local mirror target and
// then best-effort aligns the local mtime to Drive's modified_time.
func drivePullDownload(ctx context.Context, runtime *common.RuntimeContext, fileToken, target, remoteModifiedTime string) error {

View File

@@ -1032,66 +1032,6 @@ func TestDrivePullDownloadFailureSkipsDeleteLocalAndExitsNonZero(t *testing.T) {
}
}
func TestDrivePullAbortsAfterDownloadForbidden(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %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{}{
map[string]interface{}{"token": "tok_a", "name": "a.txt", "type": "file"},
map[string]interface{}{"token": "tok_b", "name": "b.txt", "type": "file"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_a/download",
Status: http.StatusForbidden,
RawBody: []byte("forbidden"),
})
err := mountAndRunDrive(t, DrivePull, []string{
"+pull",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
assertDrivePullPartialFailure(t, err)
summary, items := splitDrivePullStdout(t, stdout.Bytes())
if got := summary["aborted"]; got != true {
t.Fatalf("summary.aborted = %v, want true", got)
}
if got := summary["failed"]; got != float64(1) {
t.Fatalf("summary.failed = %v, want 1", 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.txt" || item["phase"] != "download" || item["error_class"] != "permission_denied" {
t.Fatalf("unexpected failed item: %#v", item)
}
if item["code"] != float64(http.StatusForbidden) || item["retryable"] != false {
t.Fatalf("unexpected failure classification: %#v", item)
}
if _, statErr := os.Stat(filepath.Join("local", "b.txt")); !os.IsNotExist(statErr) {
t.Fatalf("b.txt should not be downloaded after terminal permission failure; stat err=%v", statErr)
}
}
// TestDrivePullDeleteLocalDoesNotEscapeViaSymlinkParentRef is the
// regression for the "link/.." escape applied to --delete-local — the
// most dangerous variant, since the bug would otherwise let the kernel

View File

@@ -29,25 +29,12 @@ const (
)
type drivePushItem struct {
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
Action string `json:"action"`
Version string `json:"version,omitempty"`
SizeBytes int64 `json:"size_bytes,omitempty"`
Error string `json:"error,omitempty"`
Phase string `json:"phase,omitempty"`
ErrorClass string `json:"error_class,omitempty"`
Code int `json:"code,omitempty"`
Subtype string `json:"subtype,omitempty"`
Retryable *bool `json:"retryable,omitempty"`
}
type driveBatchFailureDecision struct {
Class string
Code int
Subtype string
Retryable bool
Terminal bool
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
Action string `json:"action"`
Version string `json:"version,omitempty"`
SizeBytes int64 `json:"size_bytes,omitempty"`
Error string `json:"error,omitempty"`
}
// DrivePush is a one-way, file-level mirror from a local directory onto a
@@ -261,14 +248,9 @@ var DrivePush = common.Shortcut{
continue
}
if _, ensureErr := drivePushEnsureFolder(ctx, runtime, folderToken, relDir, folderCache); ensureErr != nil {
item, terminal := drivePushFailedItem(relDir, "", "failed", "create_folder", 0, ensureErr)
items = append(items, item)
items = append(items, drivePushItem{RelPath: relDir, Action: "failed", Error: ensureErr.Error()})
failed++
uploadFailed = true
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, ensureErr)
break
}
continue
}
items = append(items, drivePushItem{RelPath: relDir, FileToken: folderCache[relDir], Action: "folder_created"})
@@ -284,9 +266,6 @@ var DrivePush = common.Shortcut{
for _, rel := range localPaths {
localFile := localFiles[rel]
if uploadFailed && drivePushHasTerminalFailure(items) {
break
}
if entry, ok := remoteFiles[rel]; ok {
if drivePushShouldSkipExisting(localFile, entry, ifExists) {
@@ -296,14 +275,9 @@ var DrivePush = common.Shortcut{
}
parentToken, parentErr := drivePushEnsureParentToken(ctx, runtime, folderToken, rel, folderCache)
if parentErr != nil {
item, terminal := drivePushFailedItem(rel, entry.FileToken, "failed", "create_folder", localFile.Size, parentErr)
items = append(items, item)
items = append(items, drivePushItem{RelPath: rel, FileToken: entry.FileToken, Action: "failed", SizeBytes: localFile.Size, Error: parentErr.Error()})
failed++
uploadFailed = true
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, parentErr)
break
}
continue
}
token, version, upErr := drivePushUploadFile(ctx, runtime, localFile, entry.FileToken, parentToken)
@@ -327,14 +301,9 @@ var DrivePush = common.Shortcut{
if failedToken == "" {
failedToken = entry.FileToken
}
item, terminal := drivePushFailedItem(rel, failedToken, "failed", "upload", localFile.Size, upErr)
items = append(items, item)
items = append(items, drivePushItem{RelPath: rel, FileToken: failedToken, Action: "failed", SizeBytes: localFile.Size, Error: upErr.Error()})
failed++
uploadFailed = true
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, upErr)
break
}
continue
}
items = append(items, drivePushItem{RelPath: rel, FileToken: token, Action: "overwritten", Version: version, SizeBytes: localFile.Size})
@@ -345,26 +314,16 @@ var DrivePush = common.Shortcut{
parentRel := drivePushParentRel(rel)
parentToken, ensureErr := drivePushEnsureFolder(ctx, runtime, folderToken, parentRel, folderCache)
if ensureErr != nil {
item, terminal := drivePushFailedItem(rel, "", "failed", "create_folder", localFile.Size, ensureErr)
items = append(items, item)
items = append(items, drivePushItem{RelPath: rel, Action: "failed", SizeBytes: localFile.Size, Error: ensureErr.Error()})
failed++
uploadFailed = true
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, ensureErr)
break
}
continue
}
token, _, upErr := drivePushUploadFile(ctx, runtime, localFile, "", parentToken)
if upErr != nil {
item, terminal := drivePushFailedItem(rel, "", "failed", "upload", localFile.Size, upErr)
items = append(items, item)
items = append(items, drivePushItem{RelPath: rel, Action: "failed", SizeBytes: localFile.Size, Error: upErr.Error()})
failed++
uploadFailed = true
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, upErr)
break
}
continue
}
items = append(items, drivePushItem{RelPath: rel, FileToken: token, Action: "uploaded", SizeBytes: localFile.Size})
@@ -391,11 +350,7 @@ var DrivePush = common.Shortcut{
}
sort.Strings(remoteRelPaths)
abortDelete := false
for _, rel := range remoteRelPaths {
if abortDelete {
break
}
keepToken := ""
if _, ok := localFiles[rel]; ok {
if chosen, ok := remoteFiles[rel]; ok {
@@ -407,14 +362,8 @@ var DrivePush = common.Shortcut{
continue
}
if err := drivePushDeleteFile(ctx, runtime, entry.FileToken); err != nil {
item, terminal := drivePushFailedItem(rel, entry.FileToken, "delete_failed", "delete", 0, err)
items = append(items, item)
items = append(items, drivePushItem{RelPath: rel, FileToken: entry.FileToken, Action: "delete_failed", Error: err.Error()})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +push after terminal %s failure: %v\n", item.Phase, err)
abortDelete = true
break
}
continue
}
items = append(items, drivePushItem{RelPath: rel, FileToken: entry.FileToken, Action: "deleted_remote"})
@@ -429,7 +378,6 @@ var DrivePush = common.Shortcut{
"skipped": skipped,
"failed": failed,
"deleted_remote": deletedRemote,
"aborted": drivePushHasTerminalFailure(items),
},
"items": items,
}
@@ -559,91 +507,6 @@ func drivePushShouldSkipSmart(localFile drivePushLocalFile, remoteFile driveRemo
return cmp >= 0
}
func drivePushFailedItem(relPath, fileToken, action, phase string, sizeBytes int64, err error) (drivePushItem, bool) {
decision := driveClassifyBatchFailure(err)
item := drivePushItem{
RelPath: relPath,
FileToken: fileToken,
Action: action,
SizeBytes: sizeBytes,
Error: err.Error(),
Phase: phase,
ErrorClass: decision.Class,
Code: decision.Code,
Subtype: decision.Subtype,
Retryable: driveBoolPtr(decision.Retryable),
}
return item, decision.Terminal
}
func driveBoolPtr(v bool) *bool {
return &v
}
func driveClassifyBatchFailure(err error) driveBatchFailureDecision {
decision := driveBatchFailureDecision{Class: "unknown", Retryable: errs.IsRetryable(err)}
problem, ok := errs.ProblemOf(err)
if !ok {
return decision
}
decision.Code = problem.Code
decision.Subtype = string(problem.Subtype)
decision.Retryable = problem.Retryable
switch {
case problem.Category == errs.CategoryAuthorization && problem.Code == 99991672:
decision.Class = "app_scope_missing"
decision.Terminal = true
case problem.Category == errs.CategoryAuthorization && problem.Code == 99991679:
decision.Class = "user_scope_missing"
decision.Terminal = true
case problem.Category == errs.CategoryAuthorization && problem.Subtype == errs.SubtypePermissionDenied:
decision.Class = "permission_denied"
decision.Terminal = true
case problem.Category == errs.CategoryNetwork && problem.Code == http.StatusForbidden:
decision.Class = "permission_denied"
decision.Terminal = true
case problem.Subtype == errs.SubtypeInvalidParameters || problem.Code == 1061002:
decision.Class = "invalid_api_parameters"
decision.Terminal = true
case problem.Subtype == errs.SubtypeRateLimit || problem.Code == 99991400:
decision.Class = "rate_limited"
decision.Terminal = true
case problem.Subtype == errs.SubtypeQuotaExceeded || problem.Code == 1061043:
decision.Class = "file_size_limit"
case problem.Code == 1062009:
decision.Class = "upload_size_mismatch"
case problem.Subtype == errs.SubtypeNotFound || problem.Code == 1061007:
decision.Class = "remote_not_found"
case problem.Subtype == errs.SubtypeServerError || problem.Code == 1061001 || problem.Code == 2200:
decision.Class = "server_error"
decision.Terminal = true
case problem.Subtype == errs.SubtypeFailedPrecondition:
decision.Class = "local_file_changed"
default:
decision.Class = string(problem.Subtype)
}
return decision
}
func drivePushHasTerminalFailure(items []drivePushItem) bool {
for _, item := range items {
if driveTerminalBatchErrorClass(item.ErrorClass) {
return true
}
}
return false
}
func driveTerminalBatchErrorClass(errorClass string) bool {
switch errorClass {
case "app_scope_missing", "user_scope_missing", "permission_denied", "invalid_api_parameters", "rate_limited", "server_error":
return true
default:
return false
}
}
func drivePushRemoteViews(entries []driveRemoteEntry, duplicateRemote string) (map[string]driveRemoteEntry, map[string]driveRemoteEntry, map[string][]driveRemoteEntry, error) {
remoteFiles := make(map[string]driveRemoteEntry, len(entries))
remoteFolders := make(map[string]driveRemoteEntry, len(entries))
@@ -737,12 +600,6 @@ func drivePushEnsureParentToken(ctx context.Context, runtime *common.RuntimeCont
// the three-step prepare/part/finish flow, which mirrors drive +upload's
// existing multipart logic.
func drivePushUploadFile(ctx context.Context, runtime *common.RuntimeContext, file drivePushLocalFile, existingToken, parentToken string) (string, string, error) {
if err := drivePushValidateUploadRequest(file, existingToken, parentToken); err != nil {
return "", "", err
}
if err := drivePushVerifyLocalSnapshot(runtime, file); err != nil {
return "", "", err
}
if file.Size > common.MaxDriveMediaUploadSinglePartSize {
token, err := drivePushUploadMultipart(ctx, runtime, file, existingToken, parentToken)
// Multipart finish does not return version on the existing
@@ -755,44 +612,6 @@ func drivePushUploadFile(ctx context.Context, runtime *common.RuntimeContext, fi
return drivePushUploadAll(ctx, runtime, file, existingToken, parentToken)
}
func drivePushValidateUploadRequest(file drivePushLocalFile, existingToken, parentToken string) error {
if strings.TrimSpace(file.FileName) == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot upload %q: file name is empty", file.RelPath)
}
if file.Size < 0 {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot upload %q: file size is negative", file.RelPath)
}
if strings.TrimSpace(parentToken) == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot upload %q: parent folder token is empty", file.RelPath)
}
if err := validate.ResourceName(parentToken, "parent_node"); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot upload %q: %s", file.RelPath, err)
}
if existingToken != "" {
if err := validate.ResourceName(existingToken, "file_token"); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot overwrite %q: %s", file.RelPath, err)
}
}
return nil
}
func drivePushVerifyLocalSnapshot(runtime *common.RuntimeContext, file drivePushLocalFile) error {
info, err := runtime.FileIO().Stat(file.OpenPath)
if err != nil {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "local file changed during push: %s is no longer readable: %v", file.RelPath, err).WithCause(err)
}
if !info.Mode().IsRegular() {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "local file changed during push: %s is no longer a regular file", file.RelPath)
}
if info.Size() != file.Size {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "local file changed during push: %s snapshot size no longer matches", file.RelPath)
}
if modTimer, ok := info.(interface{ ModTime() time.Time }); ok && !modTimer.ModTime().Equal(file.ModTime) {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "local file changed during push: %s snapshot modtime no longer matches", file.RelPath)
}
return nil
}
func drivePushUploadAll(_ context.Context, runtime *common.RuntimeContext, file drivePushLocalFile, existingToken, parentToken string) (string, string, error) {
f, err := runtime.FileIO().Open(file.OpenPath)
if err != nil {

View File

@@ -5,10 +5,8 @@ package drive
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
@@ -16,14 +14,12 @@ import (
"testing"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
// countingOpenProvider wraps a fileio.Provider and counts FileIO.Open
@@ -656,82 +652,6 @@ func TestDrivePushDeleteRemoteSkipsOnlineDocs(t *testing.T) {
}
}
func TestDrivePushDeleteRemoteAbortsAfterTerminalFailure(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %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{}{
map[string]interface{}{"token": "tok_a", "name": "a.txt", "type": "file"},
map[string]interface{}{"token": "tok_b", "name": "b.txt", "type": "file"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/drive/v1/files/tok_a",
Body: map[string]interface{}{
"code": 1061004,
"msg": "forbidden",
},
})
// No DELETE stub for tok_b: terminal delete failure must stop before it.
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--delete-remote",
"--yes",
"--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) || pfErr.Code != output.ExitAPI {
t.Fatalf("expected ExitAPI *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 got := summary["deleted_remote"]; got != float64(0) {
t.Fatalf("summary.deleted_remote = %v, want 0", got)
}
if len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
}
item := items[0]
if item["action"] != "delete_failed" || item["phase"] != "delete" || item["error_class"] != "permission_denied" {
t.Fatalf("unexpected failed item: %#v", item)
}
if item["code"] != float64(1061004) || item["retryable"] != false {
t.Fatalf("unexpected failure metadata: %#v", item)
}
for _, item := range items {
if item["file_token"] == "tok_b" {
t.Fatalf("terminal delete failure must abort before tok_b, got items=%#v", items)
}
}
}
func TestDrivePushNewestOverwritesChosenDuplicateAndDeletesSibling(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
@@ -966,22 +886,21 @@ func TestDrivePushOverwriteWithoutVersionFails(t *testing.T) {
t.Errorf("expected ExitAPI (%d), got code=%d", output.ExitAPI, pfErr.Code)
}
summary, items := splitDrivePushStdout(t, stdout.Bytes())
if got := summary["failed"]; got != float64(1) {
t.Errorf("summary.failed = %v, want 1", got)
out := stdout.String()
// summary.failed should reflect the missing version; summary.uploaded
// should not pretend the overwrite succeeded.
if !strings.Contains(out, `"failed": 1`) {
t.Errorf("expected failed=1, got: %s", out)
}
if len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
}
if got, _ := items[0]["error"].(string); !strings.Contains(got, "no version") {
t.Errorf("items[0].error = %q, want missing-version message", got)
if !strings.Contains(out, "no version") {
t.Errorf("expected error about missing version in items[].error, got: %s", out)
}
// Pin the token-stability contract: the failed item must surface the
// token returned by upload_all (tok_keep_new), NOT the fallback
// entry.FileToken (tok_keep). Without this, a regression that always
// uses entry.FileToken on failure would slip through.
if got := items[0]["file_token"]; got != "tok_keep_new" {
t.Errorf("items[0].file_token = %v, want tok_keep_new", got)
if !strings.Contains(out, `"file_token": "tok_keep_new"`) {
t.Errorf("expected failed item to surface upload_all's returned file_token (tok_keep_new), got: %s", out)
}
}
@@ -1043,313 +962,24 @@ func TestDrivePushOverwritePartialSuccessSurfacesReturnedToken(t *testing.T) {
t.Fatalf("expected ExitAPI from *output.PartialFailureError, got %T %v", err, err)
}
out := stdout.String()
// Partial failure reports an ok:false result envelope on stdout (not a
// misleading ok:true) while still carrying BOTH the succeeded and failed
// items — consistent with the pre-change payload. The failed side is
// asserted via "failed": 1 and the succeeded side via tok_keep_partial.
envelope := decodeDrivePushStdout(t, stdout.Bytes())
if envelope.OK {
t.Fatalf("partial failure must emit ok=false; stdout=%s", stdout.String())
if !strings.Contains(out, `"ok": false`) {
t.Errorf("partial failure must emit an ok:false result envelope, got: %s", out)
}
summary, items := splitDrivePushStdout(t, stdout.Bytes())
if got := summary["failed"]; got != float64(1) {
t.Errorf("summary.failed = %v, want 1", got)
}
if len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
if !strings.Contains(out, `"failed": 1`) {
t.Errorf("expected failed=1, got: %s", out)
}
// The freshly returned token must be the one in items[].file_token,
// not the stale entry.FileToken (tok_keep_old).
if got := items[0]["file_token"]; got != "tok_keep_partial" {
t.Errorf("items[0].file_token = %v, want tok_keep_partial", got)
if !strings.Contains(out, `"file_token": "tok_keep_partial"`) {
t.Errorf("expected items[].file_token to surface upload_all's returned token (tok_keep_partial), got: %s", out)
}
if got := items[0]["file_token"]; got == "tok_keep_old" {
t.Errorf("must NOT fall back to entry.FileToken when upload_all returned a token; item=%#v", items[0])
}
}
func TestDrivePushAbortsAfterUploadParamsError(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "a.txt"), []byte("A"), 0o644); err != nil {
t.Fatalf("WriteFile a: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "b.txt"), []byte("B"), 0o644); err != nil {
t.Fatalf("WriteFile 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/upload_all",
Body: map[string]interface{}{
"code": 1061002,
"msg": "params error.",
},
})
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.txt" || item["phase"] != "upload" || item["error_class"] != "invalid_api_parameters" {
t.Fatalf("unexpected failed item: %#v", item)
}
if item["code"] != float64(1061002) || item["subtype"] != "invalid_parameters" || item["retryable"] != false {
t.Fatalf("unexpected failure metadata: %#v", item)
}
for _, item := range items {
if item["rel_path"] == "b.txt" {
t.Fatalf("terminal upload params error must abort before b.txt, got items=%#v", items)
}
}
}
func TestDrivePushAbortsAfterCreateFolderMissingScope(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": 99991672,
"msg": "Access denied. One of the following scopes is required: [drive:drive, space:folder:create].",
},
})
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"] != "app_scope_missing" {
t.Fatalf("unexpected failed item: %#v", item)
}
if item["code"] != float64(99991672) || item["retryable"] != false {
t.Fatalf("unexpected failure metadata: %#v", item)
}
for _, item := range items {
if item["rel_path"] == "b" {
t.Fatalf("missing folder-create scope must abort before b, got items=%#v", items)
}
}
}
func TestDrivePushDetectsLocalFileChangedBeforeUpload(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
localPath := filepath.Join("local", "changing.txt")
if err := os.WriteFile(localPath, []byte("before"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
OnMatch: func(req *http.Request) {
if err := os.WriteFile(localPath, []byte("after-change"), 0o644); err != nil {
t.Fatalf("mutate local file: %v", err)
}
},
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
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 != false {
t.Fatalf("summary.aborted = %v, want false", got)
}
if len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
}
item := items[0]
if item["error_class"] != "local_file_changed" || item["subtype"] != "failed_precondition" || item["retryable"] != false {
t.Fatalf("unexpected failure metadata: %#v", item)
}
if got, _ := item["error"].(string); !strings.Contains(got, "local file changed during push") {
t.Fatalf("items[0].error = %q, want local-change message", got)
}
if strings.Contains(stdout.String(), "httpmock: no stub") {
t.Fatalf("upload_all was called after local snapshot changed:\n%s", stdout.String())
}
problemErr := drivePushVerifyLocalSnapshot(common.TestNewRuntimeContext(&cobra.Command{Use: "drive +push"}, &core.CliConfig{}), drivePushLocalFile{
RelPath: "missing.txt",
OpenPath: filepath.Join("local", "missing.txt"),
FileName: "missing.txt",
Size: 1,
ModTime: time.Now(),
})
problem, ok := errs.ProblemOf(problemErr)
if !ok {
t.Fatalf("ProblemOf(snapshot error) ok=false, err=%T %v", problemErr, problemErr)
}
if problem.Subtype != errs.SubtypeFailedPrecondition {
t.Fatalf("snapshot error subtype = %q, want %q", problem.Subtype, errs.SubtypeFailedPrecondition)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("snapshot error category = %q, want %q", problem.Category, errs.CategoryValidation)
}
if errors.Unwrap(problemErr) == nil {
t.Fatalf("snapshot error cause was not preserved")
}
}
func TestDrivePushDetectsSameSizeLocalFileChangedBeforeUpload(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
localPath := filepath.Join("local", "changing.txt")
if err := os.WriteFile(localPath, []byte("before"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
originalModTime := time.Unix(100, 0)
changedModTime := time.Unix(200, 0)
if err := os.Chtimes(localPath, originalModTime, originalModTime); err != nil {
t.Fatalf("Chtimes original: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
OnMatch: func(req *http.Request) {
if err := os.WriteFile(localPath, []byte("AFTER!"), 0o644); err != nil {
t.Fatalf("mutate local file: %v", err)
}
if err := os.Chtimes(localPath, changedModTime, changedModTime); err != nil {
t.Fatalf("Chtimes changed: %v", err)
}
},
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
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 len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
}
item := items[0]
if item["rel_path"] != "changing.txt" || item["error_class"] != "local_file_changed" || item["subtype"] != "failed_precondition" {
t.Fatalf("unexpected failure metadata: %#v", item)
}
if got, _ := item["error"].(string); !strings.Contains(got, "snapshot modtime no longer matches") {
t.Fatalf("items[0].error = %q, want modtime mismatch", got)
}
if strings.Contains(stdout.String(), "httpmock: no stub") {
t.Fatalf("upload_all was called after local snapshot changed:\n%s", stdout.String())
if strings.Contains(out, `"file_token": "tok_keep_old"`) {
t.Errorf("must NOT fall back to entry.FileToken when upload_all returned a token; got: %s", out)
}
}
@@ -1483,32 +1113,6 @@ func TestDrivePushExitsZeroOnCleanRun(t *testing.T) {
}
}
type drivePushStdoutEnvelope struct {
OK bool `json:"ok"`
Data struct {
Summary map[string]interface{} `json:"summary"`
Items []map[string]interface{} `json:"items"`
} `json:"data"`
}
func decodeDrivePushStdout(t *testing.T, stdout []byte) drivePushStdoutEnvelope {
t.Helper()
var envelope drivePushStdoutEnvelope
if err := json.Unmarshal(stdout, &envelope); err != nil {
t.Fatalf("unmarshal stdout: %v\nraw=%s", err, string(stdout))
}
return envelope
}
func splitDrivePushStdout(t *testing.T, stdout []byte) (map[string]interface{}, []map[string]interface{}) {
t.Helper()
envelope := decodeDrivePushStdout(t, stdout)
if envelope.Data.Summary == nil {
t.Fatalf("stdout missing data.summary; raw=%s", string(stdout))
}
return envelope.Data.Summary, envelope.Data.Items
}
// TestDrivePushUploadsSiblingWhenRemoteSameNameIsNativeDoc pins the
// behavior when a local regular file shares its rel_path with a Lark
// native cloud document on Drive (sheet/docx/bitable/...).

View File

@@ -72,7 +72,7 @@ var DriveSearch = common.Shortcut{
Description: "Search Lark docs, Wiki, and spreadsheet files with flat filters (Search v2: doc_wiki/search)",
Risk: "read",
Scopes: []string{"search:docs:read"},
AuthTypes: []string{"user", "bot"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "query", Desc: "search keyword (may be empty to browse by filter only)"},

View File

@@ -6,7 +6,6 @@ package drive
import (
"context"
"fmt"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/validate"
@@ -18,13 +17,6 @@ const (
secureLabelUpdateScope = "docs:secure_label:write_only"
)
type secureLabelOperation string
const (
secureLabelOperationList secureLabelOperation = "list"
secureLabelOperationUpdate secureLabelOperation = "update"
)
var secureLabelTypes = permApplyTypes
// DriveSecureLabelList lists secure labels available to the current user.
@@ -36,9 +28,6 @@ var DriveSecureLabelList = common.Shortcut{
Scopes: []string{secureLabelReadScope},
AuthTypes: []string{"user"},
HasFormat: true,
Tips: []string{
"Use the `id` field from this command as --label-id for +secure-label-update; do not use the display name.",
},
Flags: []common.Flag{
{Name: "page-size", Type: "int", Default: "10", Desc: "page size, 1-10"},
{Name: "page-token", Desc: "pagination token from previous response"},
@@ -64,7 +53,7 @@ var DriveSecureLabelList = common.Shortcut{
nil,
)
if err != nil {
return decorateSecureLabelError(err, secureLabelOperationList)
return err
}
runtime.OutFormat(data, nil, nil)
return nil
@@ -79,21 +68,13 @@ var DriveSecureLabelUpdate = common.Shortcut{
Risk: "write",
Scopes: []string{secureLabelUpdateScope},
AuthTypes: []string{"user"},
Tips: []string{
"Pass the numeric label id returned by +secure-label-list; display names like Public(D) are rejected.",
"Downgrading a secure label may require approval; retrying the same request will not bypass approval.",
"When updating many files, serialize requests and back off on rate_limit errors.",
},
Flags: []common.Flag{
{Name: "token", Desc: "target file token or document URL (docx/sheets/base/file/wiki/doc/mindnote/slides)", Required: true},
{Name: "type", Desc: "target type; auto-inferred from URL when omitted", Enum: secureLabelTypes},
{Name: "label-id", Desc: "secure label ID to set", Required: true},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, _, err := resolveSecureLabelTarget(runtime.Str("token"), runtime.Str("type")); err != nil {
return err
}
_, err := normalizeSecureLabelID(runtime.Str("label-id"))
_, _, err := resolveSecureLabelTarget(runtime.Str("token"), runtime.Str("type"))
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
@@ -101,15 +82,11 @@ var DriveSecureLabelUpdate = common.Shortcut{
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
labelID, err := normalizeSecureLabelID(runtime.Str("label-id"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
return common.NewDryRunAPI().
Desc("Update Drive secure label").
PATCH("/open-apis/drive/v2/files/:file_token/secure_label").
Params(map[string]interface{}{"type": docType}).
Body(map[string]interface{}{"id": labelID}).
Body(map[string]interface{}{"id": runtime.Str("label-id")}).
Set("file_token", token)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
@@ -117,18 +94,14 @@ var DriveSecureLabelUpdate = common.Shortcut{
if err != nil {
return err
}
labelID, err := normalizeSecureLabelID(runtime.Str("label-id"))
if err != nil {
return err
}
body := map[string]interface{}{"id": labelID}
body := map[string]interface{}{"id": runtime.Str("label-id")}
data, err := runtime.CallAPITyped("PATCH",
fmt.Sprintf("/open-apis/drive/v2/files/%s/secure_label", validate.EncodePathSegment(token)),
map[string]interface{}{"type": docType},
body,
)
if err != nil {
return decorateSecureLabelError(err, secureLabelOperationUpdate)
return err
}
runtime.Out(data, nil)
return nil
@@ -149,70 +122,3 @@ func buildSecureLabelListParams(runtime *common.RuntimeContext) map[string]inter
func resolveSecureLabelTarget(raw, explicitType string) (token, docType string, err error) {
return resolvePermApplyTarget(raw, explicitType)
}
// normalizeSecureLabelID trims a label id and rejects display names before the
// request reaches Drive, where they otherwise surface as opaque JSON errors.
func normalizeSecureLabelID(raw string) (string, error) {
labelID := strings.TrimSpace(raw)
if labelID == "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--label-id is required").
WithParam("--label-id")
}
for _, r := range labelID {
if r < '0' || r > '9' {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--label-id must be a numeric secure label ID, not a display name: %q", raw).
WithParam("--label-id").
WithHint("run `lark-cli drive +secure-label-list` and pass the numeric `id` value; do not pass label names like `Public(D)`")
}
}
return labelID, nil
}
// decorateSecureLabelError appends command-aware recovery guidance while
// preserving upstream/classifier hints already attached to the typed error.
func decorateSecureLabelError(err error, operation secureLabelOperation) error {
if err == nil {
return nil
}
p, ok := errs.ProblemOf(err)
if !ok {
return err
}
guidance := secureLabelErrorGuidance(p.Code, operation)
if guidance == "" {
return err
}
if p.Hint == "" {
p.Hint = guidance
} else if !strings.Contains(p.Hint, guidance) {
p.Hint = p.Hint + "; " + guidance
}
return err
}
// secureLabelErrorGuidance returns recovery guidance for secure-label API
// failures whose generic code-level classification needs command context.
func secureLabelErrorGuidance(code int, operation secureLabelOperation) string {
switch code {
case 99991400:
if operation == secureLabelOperationUpdate {
return "secure label updates are rate limited; retry later with exponential backoff and serialize bulk updates"
}
return "secure label listing is rate limited; retry later with exponential backoff"
case 1063013:
if operation == secureLabelOperationUpdate {
return "secure label downgrade requires approval; request approval or choose a non-downgrade label before retrying"
}
case 1063002:
if operation == secureLabelOperationUpdate {
return "the current user lacks permission to update this file's secure label; use a user with file and security-label permission"
}
return "the current user lacks permission to list secure labels; use a user with security-label read permission"
case 1063001, 99992402, 9499:
if operation == secureLabelOperationUpdate {
return "check --token/--type and pass a secure label ID from `lark-cli drive +secure-label-list`, not the display name"
}
return "check secure label list parameters such as --page-size, --page-token, and --lang"
}
return ""
}

View File

@@ -5,11 +5,9 @@ package drive
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
)
@@ -92,54 +90,13 @@ func TestDriveSecureLabelList_ExecuteSuccess(t *testing.T) {
}
}
func TestDriveSecureLabelList_RateLimitPreservesUpstreamHint(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v2/my_secure_labels?page_size=10",
Status: 429,
Body: map[string]interface{}{
"code": 99991400,
"msg": "rate limit exceeded",
"error": map[string]interface{}{
"details": []interface{}{
map[string]interface{}{"value": "server says slow down"},
},
},
},
})
err := mountAndRunDrive(t, DriveSecureLabelList, []string{
"+secure-label-list",
"--as", "user",
}, f, nil)
if err == nil {
t.Fatal("expected rate limit error")
}
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected APIError, got %T: %v", err, err)
}
if apiErr.Subtype != errs.SubtypeRateLimit || apiErr.Code != 99991400 || !apiErr.Retryable {
t.Fatalf("problem = %+v, want code=99991400 subtype=rate_limit retryable=true", apiErr.Problem)
}
for _, want := range []string{"server says slow down", "secure label listing is rate limited"} {
if !strings.Contains(apiErr.Hint, want) {
t.Fatalf("hint missing %q: %q", want, apiErr.Hint)
}
}
if strings.Contains(apiErr.Hint, "updates are rate limited") {
t.Fatalf("list hint should not use update-specific wording: %q", apiErr.Hint)
}
}
func TestDriveSecureLabelUpdate_DryRunInfersTypeFromURL(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{
"+secure-label-update",
"--token", "https://example.feishu.cn/docx/doxTok123?from=share",
"--label-id", " 7217780879644737539 ",
"--label-id", "7217780879644737539",
"--dry-run", "--as", "user",
}, f, stdout)
if err != nil {
@@ -175,7 +132,7 @@ func TestDriveSecureLabelUpdate_ExecuteSuccess(t *testing.T) {
"+secure-label-update",
"--token", "doxTok123",
"--type", "docx",
"--label-id", " 7217780879644737539 ",
"--label-id", "7217780879644737539",
"--as", "user",
}, f, stdout)
if err != nil {
@@ -191,32 +148,7 @@ func TestDriveSecureLabelUpdate_ExecuteSuccess(t *testing.T) {
}
}
func TestDriveSecureLabelUpdate_RejectsDisplayNameAsLabelID(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{
"+secure-label-update",
"--token", "doxTok123",
"--type", "docx",
"--label-id", "Public(D)",
"--as", "user",
}, f, stdout)
if err == nil {
t.Fatal("expected label id validation error")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected ValidationError, got %T: %v", err, err)
}
if validationErr.Param != "--label-id" {
t.Fatalf("Param = %q, want --label-id", validationErr.Param)
}
if !strings.Contains(validationErr.Hint, "+secure-label-list") {
t.Fatalf("hint missing list guidance: %q", validationErr.Hint)
}
}
func TestDriveSecureLabelUpdate_DowngradeApprovalReturnsFailedPrecondition(t *testing.T) {
func TestDriveSecureLabelUpdate_DowngradeApprovalReturnsAPIError(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
reg.Register(&httpmock.Stub{
Method: "PATCH",
@@ -237,78 +169,7 @@ func TestDriveSecureLabelUpdate_DowngradeApprovalReturnsFailedPrecondition(t *te
if err == nil {
t.Fatal("expected 1063013 error")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected ValidationError, got %T: %v", err, err)
}
if validationErr.Subtype != errs.SubtypeFailedPrecondition || validationErr.Code != 1063013 {
t.Fatalf("problem = %+v, want code=1063013 subtype=failed_precondition", validationErr.Problem)
}
if !strings.Contains(validationErr.Hint, "approval") {
t.Fatalf("hint missing approval guidance: %q", validationErr.Hint)
}
}
func TestDriveSecureLabelUpdate_InvalidJSONTypeGetsLabelHint(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/drive/v2/files/doxTok123/secure_label",
Status: 400,
Body: map[string]interface{}{
"code": 9499, "msg": "Invalid parameter type in json: id",
},
})
err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{
"+secure-label-update",
"--token", "https://example.feishu.cn/docx/doxTok123",
"--label-id", "7217780879644737539",
"--as", "user",
}, f, nil)
if err == nil {
t.Fatal("expected 9499 error")
}
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected APIError, got %T: %v", err, err)
}
if apiErr.Subtype != errs.SubtypeInvalidParameters || apiErr.Code != 9499 {
t.Fatalf("problem = %+v, want code=9499 subtype=invalid_parameters", apiErr.Problem)
}
if !strings.Contains(apiErr.Hint, "+secure-label-list") {
t.Fatalf("hint missing secure label list guidance: %q", apiErr.Hint)
}
}
func TestDriveSecureLabelUpdate_RateLimitIsRetryableWithBackoffHint(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/drive/v2/files/doxTok123/secure_label",
Status: 429,
Body: map[string]interface{}{
"code": 99991400, "msg": "rate limit exceeded",
},
})
err := mountAndRunDrive(t, DriveSecureLabelUpdate, []string{
"+secure-label-update",
"--token", "https://example.feishu.cn/docx/doxTok123",
"--label-id", "7217780879644737539",
"--as", "user",
}, f, nil)
if err == nil {
t.Fatal("expected rate limit error")
}
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected APIError, got %T: %v", err, err)
}
if apiErr.Subtype != errs.SubtypeRateLimit || apiErr.Code != 99991400 || !apiErr.Retryable {
t.Fatalf("problem = %+v, want code=99991400 subtype=rate_limit retryable=true", apiErr.Problem)
}
if !strings.Contains(apiErr.Hint, "backoff") {
t.Fatalf("hint missing backoff guidance: %q", apiErr.Hint)
if !strings.Contains(err.Error(), "Security label downgrade requires approval") {
t.Fatalf("expected raw API error message, got: %v", err)
}
}

View File

@@ -25,21 +25,12 @@ const (
driveSyncOnConflictAsk = "ask"
)
func driveSyncActionScopes() []string {
return []string{"drive:file:download", "drive:file:upload", "space:folder:create"}
}
type driveSyncItem struct {
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
Action string `json:"action"`
Direction string `json:"direction,omitempty"` // "pull" or "push"
Error string `json:"error,omitempty"`
Phase string `json:"phase,omitempty"`
ErrorClass string `json:"error_class,omitempty"`
Code int `json:"code,omitempty"`
Subtype string `json:"subtype,omitempty"`
Retryable *bool `json:"retryable,omitempty"`
RelPath string `json:"rel_path"`
FileToken string `json:"file_token,omitempty"`
Action string `json:"action"`
Direction string `json:"direction,omitempty"` // "pull" or "push"
Error string `json:"error,omitempty"`
}
// DriveSync performs a two-way sync between a local directory and a Drive
@@ -75,7 +66,6 @@ var DriveSync = common.Shortcut{
"Default --on-conflict=remote-wins pulls the remote version when both sides changed a file. Use local-wins to push instead, keep-both to rename and keep both copies, or ask for interactive resolution.",
"Pass --quick for faster best-effort diff detection using modified_time instead of SHA-256 hash (no remote file downloads needed during diffing).",
"Because +sync acts on the diff, --quick can still pull, overwrite, or rename files when timestamps differ even if file contents are actually unchanged.",
"Actual sync execution pre-flights download, upload, and folder-create scopes before listing or walking, so missing grants fail before any partial sync can start.",
"Only entries with type=file are synced; online docs (docx, sheet, bitable, mindnote, slides) and shortcuts are skipped.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
@@ -120,8 +110,10 @@ var DriveSync = common.Shortcut{
duplicateRemote = driveDuplicateRemoteFail
}
quick := runtime.Bool("quick")
if err := runtime.EnsureScopes(driveSyncActionScopes()); err != nil {
return err
if !quick {
if err := runtime.EnsureScopes([]string{"drive:file:download"}); err != nil {
return err
}
}
safeRoot, err := validate.SafeInputPath(localDir)
@@ -270,6 +262,18 @@ var DriveSync = common.Shortcut{
var pulled, pushed, skipped, failed int
items := make([]driveSyncItem, 0)
if quick && driveSyncNeedsDownloadScope(newRemote, modified, conflictResolutions) {
if err := runtime.EnsureScopes([]string{"drive:file:download"}); err != nil {
return err
}
}
plannedUploads := driveSyncPlannedUploadPaths(newLocal, modified, conflictResolutions)
if len(plannedUploads) > 0 {
if err := runtime.EnsureScopes([]string{"drive:file:upload"}); err != nil {
return err
}
}
// Build push infrastructure: local walk for push + remote views + folder cache.
folderCache := map[string]string{"": folderToken}
for relDir, entry := range remoteFolders {
@@ -283,18 +287,20 @@ var DriveSync = common.Shortcut{
return err
}
if driveSyncNeedsCreateScope(plannedUploads, localDirs, folderCache) {
if err := runtime.EnsureScopes([]string{"space:folder:create"}); err != nil {
return err
}
}
// Mirror local directory structure first (same as +push), so
// empty local directories are not silently dropped.
for _, relDir := range localDirs {
if driveSyncHasTerminalFailure(items) {
break
}
if _, alreadyRemote := folderCache[relDir]; alreadyRemote {
continue
}
if _, ensureErr := drivePushEnsureFolder(ctx, runtime, folderToken, relDir, folderCache); ensureErr != nil {
item, _ := driveSyncFailedItem(relDir, "", "failed", "push", "create_folder", ensureErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: relDir, Action: "failed", Direction: "push", Error: ensureErr.Error()})
failed++
continue
}
@@ -304,9 +310,6 @@ var DriveSync = common.Shortcut{
// 2a. Pull new_remote files.
for _, entry := range newRemote {
if driveSyncHasTerminalFailure(items) {
break
}
targetFile, ok := pullRemoteFiles[entry.RelPath]
if !ok {
// Non-file type (doc, shortcut, etc.) — skip.
@@ -314,13 +317,8 @@ var DriveSync = common.Shortcut{
}
target := filepath.Join(rootRelToCwd, entry.RelPath)
if err := drivePullDownload(ctx, runtime, targetFile.DownloadToken, target, targetFile.ModifiedTime); err != nil {
item, terminal := driveSyncFailedItem(entry.RelPath, entry.FileToken, "failed", "pull", "download", err)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: entry.FileToken, Action: "failed", Direction: "pull", Error: err.Error()})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +sync after terminal %s failure: %v\n", item.Phase, err)
break
}
continue
}
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: entry.FileToken, Action: "downloaded", Direction: "pull"})
@@ -329,9 +327,6 @@ var DriveSync = common.Shortcut{
// 2b. Push new_local files.
for _, entry := range newLocal {
if driveSyncHasTerminalFailure(items) {
break
}
localFile, ok := pushLocalFiles[entry.RelPath]
if !ok {
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "skipped", Direction: "push", Error: "local file disappeared during sync"})
@@ -341,20 +336,14 @@ var DriveSync = common.Shortcut{
parentRel := drivePushParentRel(entry.RelPath)
parentToken, ensureErr := drivePushEnsureFolder(ctx, runtime, folderToken, parentRel, folderCache)
if ensureErr != nil {
item, _ := driveSyncFailedItem(entry.RelPath, "", "failed", "push", "create_folder", ensureErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "failed", Direction: "push", Error: ensureErr.Error()})
failed++
continue
}
token, _, upErr := drivePushUploadFile(ctx, runtime, localFile, "", parentToken)
if upErr != nil {
item, terminal := driveSyncFailedItem(entry.RelPath, token, "failed", "push", "upload", upErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "failed", Direction: "push", Error: upErr.Error()})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +sync after terminal %s failure: %v\n", item.Phase, upErr)
break
}
continue
}
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: token, Action: "uploaded", Direction: "push"})
@@ -363,9 +352,6 @@ var DriveSync = common.Shortcut{
// 2c. Resolve modified files by --on-conflict strategy.
for _, entry := range modified {
if driveSyncHasTerminalFailure(items) {
break
}
remoteFile := remoteFiles[entry.RelPath]
localFile, hasLocal := pushLocalFiles[entry.RelPath]
if !hasLocal {
@@ -393,13 +379,8 @@ var DriveSync = common.Shortcut{
}
target := filepath.Join(rootRelToCwd, entry.RelPath)
if err := drivePullDownload(ctx, runtime, targetFile.DownloadToken, target, targetFile.ModifiedTime); err != nil {
item, terminal := driveSyncFailedItem(entry.RelPath, entry.FileToken, "failed", "pull", "download", err)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: entry.FileToken, Action: "failed", Direction: "pull", Error: err.Error()})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +sync after terminal %s failure: %v\n", item.Phase, err)
break
}
continue
}
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: entry.FileToken, Action: "downloaded", Direction: "pull"})
@@ -415,8 +396,7 @@ var DriveSync = common.Shortcut{
}
parentToken, parentErr := drivePushEnsureFolder(ctx, runtime, folderToken, drivePushParentRel(entry.RelPath), folderCache)
if parentErr != nil {
item, _ := driveSyncFailedItem(entry.RelPath, existingToken, "failed", "push", "create_folder", parentErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: existingToken, Action: "failed", Direction: "push", Error: parentErr.Error()})
failed++
continue
}
@@ -431,13 +411,8 @@ var DriveSync = common.Shortcut{
if failedToken == "" {
failedToken = existingToken
}
item, terminal := driveSyncFailedItem(entry.RelPath, failedToken, "failed", "push", "upload", upErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: failedToken, Action: "failed", Direction: "push", Error: upErr.Error()})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +sync after terminal %s failure: %v\n", item.Phase, upErr)
break
}
continue
}
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: token, Action: "overwritten", Direction: "push"})
@@ -458,8 +433,7 @@ var DriveSync = common.Shortcut{
}
suffixedRel, err := relPathWithUniqueFileTokenSuffix(entry.RelPath, remoteFile.FileToken, occupied)
if err != nil {
item, _ := driveSyncFailedItem(entry.RelPath, "", "failed", "conflict", "conflict", err)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "failed", Direction: "conflict", Error: err.Error()})
failed++
continue
}
@@ -467,9 +441,7 @@ var DriveSync = common.Shortcut{
oldAbsPath := filepath.Join(safeRoot, filepath.FromSlash(entry.RelPath))
newAbsPath := filepath.Join(safeRoot, filepath.FromSlash(suffixedRel))
if err := os.Rename(oldAbsPath, newAbsPath); err != nil { //nolint:forbidigo // shortcuts cannot import internal/vfs (depguard rule shortcuts-no-vfs); safeRoot is validated.
renameErr := errs.NewInternalError(errs.SubtypeFileIO, "rename local: %s", err).WithCause(err)
item, _ := driveSyncFailedItem(entry.RelPath, "", "failed", "conflict", "conflict", renameErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "failed", Direction: "conflict", Error: fmt.Sprintf("rename local: %s", err)})
failed++
continue
}
@@ -482,30 +454,19 @@ var DriveSync = common.Shortcut{
if rollbackErr != nil {
errMsg += "; rollback failed: " + rollbackErr.Error()
}
notFoundErr := errs.NewAPIError(errs.SubtypeNotFound, "%s", errMsg)
item, _ := driveSyncFailedItem(entry.RelPath, "", "failed", "pull", "download", notFoundErr)
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "failed", Direction: "pull", Error: errMsg})
failed++
continue
}
target := filepath.Join(rootRelToCwd, entry.RelPath)
if err := drivePullDownload(ctx, runtime, targetFile.DownloadToken, target, targetFile.ModifiedTime); err != nil {
downloadErr := err
rollbackErr := driveSyncRollbackRenamedLocal(oldAbsPath, newAbsPath)
errMsg := err.Error()
if rollbackErr != nil {
errMsg += "; rollback failed: " + rollbackErr.Error()
}
item, terminal := driveSyncFailedItem(entry.RelPath, entry.FileToken, "failed", "pull", "download", downloadErr)
if rollbackErr != nil {
item.Error = errMsg
}
items = append(items, item)
items = append(items, driveSyncItem{RelPath: entry.RelPath, FileToken: entry.FileToken, Action: "failed", Direction: "pull", Error: errMsg})
failed++
if terminal {
fmt.Fprintf(runtime.IO().ErrOut, "Aborting +sync after terminal %s failure: %v\n", item.Phase, downloadErr)
break
}
continue
}
items = append(items, driveSyncItem{RelPath: entry.RelPath, Action: "renamed_local", Direction: "conflict"})
@@ -531,7 +492,6 @@ var DriveSync = common.Shortcut{
"pushed": pushed,
"skipped": skipped,
"failed": failed,
"aborted": driveSyncHasTerminalFailure(items),
},
"items": items,
}
@@ -560,32 +520,6 @@ func driveSyncStatusRemoteFiles(pullRemoteFiles map[string]drivePullTarget) map[
return remoteFiles
}
func driveSyncFailedItem(relPath, fileToken, action, direction, phase string, err error) (driveSyncItem, bool) {
decision := driveClassifyBatchFailure(err)
item := driveSyncItem{
RelPath: relPath,
FileToken: fileToken,
Action: action,
Direction: direction,
Error: err.Error(),
Phase: phase,
ErrorClass: decision.Class,
Code: decision.Code,
Subtype: decision.Subtype,
Retryable: driveBoolPtr(decision.Retryable),
}
return item, decision.Terminal
}
func driveSyncHasTerminalFailure(items []driveSyncItem) bool {
for _, item := range items {
if driveTerminalBatchErrorClass(item.ErrorClass) {
return true
}
}
return false
}
// driveSyncAskConflict prompts the user for a conflict resolution strategy
// for a single file. Returns the strategy string, or empty string if the
// user chose to skip.
@@ -624,6 +558,51 @@ func driveSyncAskConflict(relPath string, runtime *common.RuntimeContext) (strin
}
}
func driveSyncNeedsDownloadScope(newRemote, modified []driveStatusEntry, conflictResolutions map[string]string) bool {
if len(newRemote) > 0 {
return true
}
for _, entry := range modified {
switch conflictResolutions[entry.RelPath] {
case driveSyncOnConflictRemoteWins, driveSyncOnConflictKeepBoth:
return true
}
}
return false
}
func driveSyncPlannedUploadPaths(newLocal, modified []driveStatusEntry, conflictResolutions map[string]string) []string {
planned := make([]string, 0, len(newLocal)+len(modified))
for _, entry := range newLocal {
planned = append(planned, entry.RelPath)
}
for _, entry := range modified {
if conflictResolutions[entry.RelPath] == driveSyncOnConflictLocalWins {
planned = append(planned, entry.RelPath)
}
}
return planned
}
func driveSyncNeedsCreateScope(uploadPaths []string, localDirs []string, folderCache map[string]string) bool {
for _, relPath := range uploadPaths {
parentRel := drivePushParentRel(relPath)
if parentRel == "" {
continue
}
if _, ok := folderCache[parentRel]; !ok {
return true
}
}
// Empty local directories also need create_folder if not already on Drive.
for _, relDir := range localDirs {
if _, ok := folderCache[relDir]; !ok {
return true
}
}
return false
}
func driveSyncRollbackRenamedLocal(oldAbsPath, newAbsPath string) error {
if info, err := os.Stat(oldAbsPath); err == nil { //nolint:forbidigo // shortcuts cannot import internal/vfs (depguard rule shortcuts-no-vfs); safeRoot is validated.
if info.IsDir() {

View File

@@ -311,71 +311,6 @@ func TestDriveSyncRemoteWinsPullsNewRemoteAndPushesNewLocal(t *testing.T) {
}
}
func TestDriveSyncAbortsAfterNewRemoteDownloadForbidden(t *testing.T) {
syncTestConfig := &core.CliConfig{
AppID: "drive-sync-forbidden", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
f, stdout, _, reg := cmdutil.TestFactory(t, syncTestConfig)
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %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{}{
map[string]interface{}{"token": "tok_a", "name": "a.txt", "type": "file", "modified_time": "100"},
map[string]interface{}{"token": "tok_b", "name": "b.txt", "type": "file", "modified_time": "100"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_a/download",
Status: http.StatusForbidden,
RawBody: []byte("forbidden"),
})
err := mountAndRunDrive(t, DriveSync, []string{
"+sync",
"--local-dir", "local",
"--folder-token", "folder_root",
"--quick",
"--as", "bot",
}, f, stdout)
assertDriveSyncPartialFailure(t, err)
summary := driveSyncStdoutSummary(t, stdout.Bytes())
if got := summary["aborted"]; got != true {
t.Fatalf("summary.aborted = %v, want true", got)
}
if got := summary["failed"]; got != float64(1) {
t.Fatalf("summary.failed = %v, want 1", got)
}
items := driveSyncStdoutItems(t, stdout.Bytes())
if len(items) != 1 {
t.Fatalf("items len = %d, want 1; items=%#v", len(items), items)
}
item := items[0]
if item.RelPath != "a.txt" || item.Direction != "pull" || item.Phase != "download" || item.ErrorClass != "permission_denied" {
t.Fatalf("unexpected failed item: %#v", item)
}
if item.Code != http.StatusForbidden || item.Retryable == nil || *item.Retryable {
t.Fatalf("unexpected failure classification: %#v", item)
}
if _, statErr := os.Stat(filepath.Join("local", "b.txt")); !os.IsNotExist(statErr) {
t.Fatalf("b.txt should not be downloaded after terminal permission failure; stat err=%v", statErr)
}
}
// TestDriveSyncLocalWinsPushesOverRemote verifies that --on-conflict=local-wins
// pushes the local version over the remote file.
func TestDriveSyncLocalWinsPushesOverRemote(t *testing.T) {
@@ -1617,11 +1552,11 @@ func TestDriveSyncDryRunQuickAcceptsMetadataOnlyScope(t *testing.T) {
}
}
func TestDriveSyncPreflightsActionScopesBeforeListing(t *testing.T) {
func TestDriveSyncExactRemoteWinsAcceptsDownloadOnlyScope(t *testing.T) {
syncTestConfig := &core.CliConfig{
AppID: "drive-sync-download-scope-only", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
f, stdout, _, _ := cmdutil.TestFactory(t, syncTestConfig)
f, stdout, _, reg := cmdutil.TestFactory(t, syncTestConfig)
f.Credential = credential.NewCredentialProvider(nil, nil, &driveStatusScopedTokenResolver{scopes: "drive:drive.metadata:readonly drive:file:download"}, nil)
tmpDir := t.TempDir()
@@ -1633,6 +1568,34 @@ func TestDriveSyncPreflightsActionScopesBeforeListing(t *testing.T) {
t.Fatalf("WriteFile a.txt: %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{}{
map[string]interface{}{"token": "tok_a", "name": "a.txt", "type": "file"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_a/download",
Status: 200,
Body: []byte("remote-a"),
Headers: http.Header{"Content-Type": []string{"application/octet-stream"}},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_a/download",
Status: 200,
Body: []byte("remote-a"),
Headers: http.Header{"Content-Type": []string{"application/octet-stream"}},
})
err := mountAndRunDrive(t, DriveSync, []string{
"+sync",
"--local-dir", "local",
@@ -1640,30 +1603,11 @@ func TestDriveSyncPreflightsActionScopesBeforeListing(t *testing.T) {
"--on-conflict", "remote-wins",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatalf("expected action-scope preflight to reject download-only scope\nstdout: %s", stdout.String())
if err != nil {
t.Fatalf("expected exact remote-wins to succeed with download-only scope, got: %v\nstdout: %s", err, stdout.String())
}
var permErr *errs.PermissionError
if !errors.As(err, &permErr) {
t.Fatalf("expected *errs.PermissionError, got %T: %v", err, err)
}
if permErr.Subtype != errs.SubtypeMissingScope {
t.Fatalf("Subtype = %q, want %q", permErr.Subtype, errs.SubtypeMissingScope)
}
for _, scope := range []string{"drive:file:upload", "space:folder:create"} {
found := false
for _, missing := range permErr.MissingScopes {
if missing == scope {
found = true
break
}
}
if !found {
t.Fatalf("MissingScopes = %v, want %s", permErr.MissingScopes, scope)
}
}
if strings.Contains(stdout.String(), "folder_root") {
t.Fatalf("preflight should fail before remote listing, got stdout: %s", stdout.String())
if strings.Contains(strings.ToLower(stdout.String()), "missing_scope") {
t.Fatalf("should not surface missing_scope, got: %s", stdout.String())
}
}
@@ -2608,6 +2552,30 @@ func TestDriveSyncAskConflictRemoteShortForms(t *testing.T) {
}
}
// TestDriveSyncNeedsDownloadScopeReturnsFalseForLocalWinsOnly verifies
// that driveSyncNeedsDownloadScope returns false when there are no
// new_remote entries and all modified entries resolve to local-wins.
func TestDriveSyncNeedsDownloadScopeReturnsFalseForLocalWinsOnly(t *testing.T) {
modified := []driveStatusEntry{{RelPath: "a.txt"}, {RelPath: "b.txt"}}
resolutions := map[string]string{"a.txt": driveSyncOnConflictLocalWins, "b.txt": driveSyncOnConflictLocalWins}
if driveSyncNeedsDownloadScope(nil, modified, resolutions) {
t.Fatal("expected false when no new_remote and all conflicts are local-wins")
}
}
// TestDriveSyncNeedsDownloadScopeReturnsTrueForKeepBoth verifies that
// driveSyncNeedsDownloadScope returns true when a modified entry resolves
// to keep-both (which requires pulling the remote version).
func TestDriveSyncNeedsDownloadScopeReturnsTrueForKeepBoth(t *testing.T) {
modified := []driveStatusEntry{{RelPath: "a.txt"}}
resolutions := map[string]string{"a.txt": driveSyncOnConflictKeepBoth}
if !driveSyncNeedsDownloadScope(nil, modified, resolutions) {
t.Fatal("expected true when a conflict resolves to keep-both")
}
}
// TestDriveSyncRemoteWinsReportsMissingPullView verifies that when a
// modified file's rel_path is not in pullRemoteFiles during the
// remote-wins branch, a failed item is reported instead of a panic.
@@ -3115,19 +3083,3 @@ func driveSyncStdoutItems(t *testing.T, stdout []byte) []driveSyncItem {
}
return envelope.Data.Items
}
func driveSyncStdoutSummary(t *testing.T, stdout []byte) map[string]interface{} {
t.Helper()
var envelope struct {
Data struct {
Summary map[string]interface{} `json:"summary"`
} `json:"data"`
}
if err := json.Unmarshal(stdout, &envelope); err != nil {
t.Fatalf("unmarshal stdout: %v\nraw=%s", err, string(stdout))
}
if envelope.Data.Summary == nil {
t.Fatalf("stdout missing data.summary; raw=%s", string(stdout))
}
return envelope.Data.Summary
}

View File

@@ -3,10 +3,7 @@
package drive
import (
"reflect"
"testing"
)
import "testing"
// TestShortcutsIncludesExpectedCommands verifies the drive shortcut registry contains the expected commands.
func TestShortcutsIncludesExpectedCommands(t *testing.T) {
@@ -61,12 +58,3 @@ func TestShortcutsIncludesExpectedCommands(t *testing.T) {
}
}
}
func TestDriveSearchSupportsUserAndBotIdentity(t *testing.T) {
t.Parallel()
want := []string{"user", "bot"}
if !reflect.DeepEqual(DriveSearch.AuthTypes, want) {
t.Fatalf("DriveSearch.AuthTypes = %v, want %v", DriveSearch.AuthTypes, want)
}
}

View File

@@ -200,21 +200,16 @@ var GetMyTasks = common.Shortcut{
for _, item := range filteredItems {
urlVal, _ := item["url"].(string)
urlVal = truncateTaskURL(urlVal)
completed, completedAt := taskCompletionState(item)
outputItem := map[string]interface{}{
"guid": item["guid"],
"summary": item["summary"],
"url": urlVal,
"completed": completed,
"guid": item["guid"],
"summary": item["summary"],
"url": urlVal,
}
if createdAtStr, ok := item["created_at"].(string); ok {
if ts, err := strconv.ParseInt(createdAtStr, 10, 64); err == nil {
outputItem["created_at"] = time.UnixMilli(ts).Local().Format(time.RFC3339)
}
}
if !completedAt.IsZero() {
outputItem["completed_at"] = completedAt.Local().Format(time.RFC3339)
}
if dueObj, ok := item["due"].(map[string]interface{}); ok {
if tsStr, ok := dueObj["timestamp"].(string); ok {
if ts, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
@@ -242,7 +237,6 @@ var GetMyTasks = common.Shortcut{
summary, _ := item["summary"].(string)
urlVal, _ := item["url"].(string)
urlVal = truncateTaskURL(urlVal)
completed, completedAt := taskCompletionState(item)
var dueTimeStr string
if dueObj, ok := item["due"].(map[string]interface{}); ok {
@@ -265,10 +259,6 @@ var GetMyTasks = common.Shortcut{
if urlVal != "" {
fmt.Fprintf(w, " URL: %s\n", urlVal)
}
fmt.Fprintf(w, " Completed: %t\n", completed)
if !completedAt.IsZero() {
fmt.Fprintf(w, " Completed At: %s\n", completedAt.Local().Format("2006-01-02 15:04"))
}
if dueTimeStr != "" {
fmt.Fprintf(w, " Due: %s\n", dueTimeStr)
}
@@ -288,15 +278,3 @@ var GetMyTasks = common.Shortcut{
return nil
},
}
func taskCompletionState(item map[string]interface{}) (bool, time.Time) {
completedAtStr, _ := item["completed_at"].(string)
if completedAtStr == "" || completedAtStr == "0" {
return false, time.Time{}
}
ts, err := strconv.ParseInt(completedAtStr, 10, 64)
if err != nil {
return false, time.Time{}
}
return true, time.UnixMilli(ts)
}

View File

@@ -110,118 +110,6 @@ func TestGetMyTasks_LocalTimeFormatting(t *testing.T) {
}
}
func TestGetMyTasks_IncludesCompletionStateInJSON(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedCompletedAt := time.UnixMilli(tsMs).Local().Format(time.RFC3339)
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-open",
"summary": "Open Task",
"completed_at": "0",
"url": "https://example.com/task-open",
},
map[string]interface{}{
"guid": "task-done",
"summary": "Done Task",
"completed_at": tsStr,
"url": "https://example.com/task-done",
},
},
"has_more": false,
"page_token": "",
},
},
})
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
err := runMountedTaskShortcut(t, s, []string{"+get-my-tasks", "--format", "json", "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
outNorm := strings.ReplaceAll(stdout.String(), `":"`, `": "`)
for _, expected := range []string{
`"guid": "task-open"`,
`"completed": false`,
`"guid": "task-done"`,
`"completed": true`,
`"completed_at": "` + expectedCompletedAt + `"`,
} {
if !strings.Contains(outNorm, expected) {
t.Fatalf("output missing expected string (%s), got: %s", expected, stdout.String())
}
}
}
func TestGetMyTasks_IncludesCompletionStateInPretty(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedCompletedAt := time.UnixMilli(tsMs).Local().Format("2006-01-02 15:04")
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-open",
"summary": "Open Task",
"completed_at": "0",
"url": "https://example.com/task-open",
},
map[string]interface{}{
"guid": "task-done",
"summary": "Done Task",
"completed_at": tsStr,
"url": "https://example.com/task-done",
},
},
"has_more": false,
"page_token": "",
},
},
})
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
err := runMountedTaskShortcut(t, s, []string{"+get-my-tasks", "--format", "pretty", "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
out := stdout.String()
for _, expected := range []string{
"[1] Open Task\n ID: task-open\n URL: https://example.com/task-open\n Completed: false\n",
"[2] Done Task\n ID: task-done\n URL: https://example.com/task-done\n Completed: true\n Completed At: " + expectedCompletedAt + "\n",
} {
if !strings.Contains(out, expected) {
t.Fatalf("output missing expected string (%s), got: %s", expected, out)
}
}
if count := strings.Count(out, "Completed At:"); count != 1 {
t.Fatalf("Completed At count = %d, want 1; output: %s", count, out)
}
}
// TestGetMyTasks_InvalidTimeFlags locks the three time-flag validation arms in
// Execute (--created_at / --due-start / --due-end). The parse runs before any
// API call, so a malformed value deterministically surfaces a typed

View File

@@ -16,6 +16,5 @@ func Shortcuts() []common.Shortcut {
VCMeetingLeave,
VCMeetingListActive,
VCMeetingEvents,
VCMeetingMessageSend,
}
}

View File

@@ -838,7 +838,7 @@ func TestVCShortcuts_RegistersMeetingAgentCommands(t *testing.T) {
for _, shortcut := range got {
commands = append(commands, shortcut.Command)
}
want := []string{"+search", "+notes", "+recording", "+detail", "+meeting-join", "+meeting-leave", "+meeting-list-active", "+meeting-events", "+meeting-message-send"}
want := []string{"+search", "+notes", "+recording", "+detail", "+meeting-join", "+meeting-leave", "+meeting-list-active", "+meeting-events"}
if !reflect.DeepEqual(commands, want) {
t.Fatalf("shortcut commands = %#v, want %#v", commands, want)
}

View File

@@ -1,161 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
const (
meetingMessageTypeText = "text"
meetingMessageTypeReaction = "reaction"
// Keep the client-side cap below the server-side content limit.
meetingMessageMaxTextBytes = 48 * 1024
meetingMessageMaxUUIDBytes = 128
)
// VCMeetingMessageSend sends an in-meeting text message or reaction emoji.
var VCMeetingMessageSend = common.Shortcut{
Service: "vc",
Command: "+meeting-message-send",
Description: "Send an in-meeting text message or reaction emoji",
Risk: "write",
Scopes: []string{"vc:meeting.message:write"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "meeting-id", Required: true, Desc: "meeting ID to send into"},
{Name: "msg-type", Desc: "message type: text or reaction"},
{Name: "text", Desc: "text content when --msg-type text"},
{Name: "emoji-type", Desc: "emoji key when --msg-type reaction, for example LOVE, THUMBSUP, VC_NoSound"},
{Name: "uuid", Desc: "optional idempotency key"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := validateMeetingEventsMeetingID(runtime.Str("meeting-id")); err != nil {
return err
}
_, err := validateMeetingMessagePayload(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, err := buildMeetingMessageSendBody(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
return common.NewDryRunAPI().
POST(buildMeetingMessageSendPath()).
Body(body)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
body, err := buildMeetingMessageSendBody(runtime)
if err != nil {
return err
}
data, err := runtime.CallAPITyped(http.MethodPost, buildMeetingMessageSendPath(), nil, body)
if err != nil {
return err
}
if data == nil {
data = map[string]interface{}{}
}
runtime.OutFormat(data, nil, func(w io.Writer) {
fmt.Fprintln(w, "Meeting message sent.")
if msgType := common.GetString(data, "msg_type"); msgType != "" {
fmt.Fprintf(w, " Type: %s\n", msgType)
} else if msgType, _ := body["msg_type"].(string); msgType != "" {
fmt.Fprintf(w, " Type: %s\n", msgType)
}
if uuid := common.GetString(data, "uuid"); uuid != "" {
fmt.Fprintf(w, " UUID: %s\n", uuid)
}
})
return nil
},
}
func buildMeetingMessageSendPath() string {
return "/open-apis/vc/v1/bots/message"
}
func buildMeetingMessageSendBody(runtime *common.RuntimeContext) (map[string]interface{}, error) {
msgType, err := validateMeetingMessagePayload(runtime)
if err != nil {
return nil, err
}
body := map[string]interface{}{
"meeting_id": strings.TrimSpace(runtime.Str("meeting-id")),
"msg_type": msgType,
}
switch msgType {
case meetingMessageTypeText:
body["content"] = strings.TrimSpace(runtime.Str("text"))
case meetingMessageTypeReaction:
body["content"] = strings.TrimSpace(runtime.Str("emoji-type"))
}
if uuid := strings.TrimSpace(runtime.Str("uuid")); uuid != "" {
body["uuid"] = uuid
}
return body, nil
}
func validateMeetingMessagePayload(runtime *common.RuntimeContext) (string, error) {
msgType, err := resolveMeetingMessageType(runtime)
if err != nil {
return "", err
}
if msgType == meetingMessageTypeText {
text := strings.TrimSpace(runtime.Str("text"))
if len(text) > meetingMessageMaxTextBytes {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, fmt.Sprintf("--text is too long; max %d bytes", meetingMessageMaxTextBytes)).WithParam("--text")
}
}
if uuid := strings.TrimSpace(runtime.Str("uuid")); len(uuid) > meetingMessageMaxUUIDBytes {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, fmt.Sprintf("--uuid is too long; max %d bytes", meetingMessageMaxUUIDBytes)).WithParam("--uuid")
}
return msgType, nil
}
func resolveMeetingMessageType(runtime *common.RuntimeContext) (string, error) {
msgType := strings.ToLower(strings.TrimSpace(runtime.Str("msg-type")))
text := strings.TrimSpace(runtime.Str("text"))
emojiType := strings.TrimSpace(runtime.Str("emoji-type"))
if msgType == "" {
switch {
case text != "" && emojiType == "":
msgType = meetingMessageTypeText
case text == "" && emojiType != "":
msgType = meetingMessageTypeReaction
default:
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--msg-type is required when both --text and --emoji-type are empty or both are set").WithParam("--msg-type")
}
}
switch msgType {
case meetingMessageTypeText:
if text == "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--text is required when --msg-type text").WithParam("--text")
}
if emojiType != "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--emoji-type cannot be used when --msg-type text").WithParam("--emoji-type")
}
case meetingMessageTypeReaction:
if emojiType == "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--emoji-type is required when --msg-type reaction").WithParam("--emoji-type")
}
if text != "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--text cannot be used when --msg-type reaction").WithParam("--text")
}
default:
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--msg-type must be text or reaction").WithParam("--msg-type")
}
return msgType, nil
}

View File

@@ -1,312 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/shortcuts/common"
)
func newMeetingMessageSendRuntime() *common.RuntimeContext {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("meeting-id", "", "")
cmd.Flags().String("msg-type", "", "")
cmd.Flags().String("text", "", "")
cmd.Flags().String("emoji-type", "", "")
cmd.Flags().String("uuid", "", "")
return common.TestNewRuntimeContext(cmd, defaultConfig())
}
func mustSetMeetingMessageSendFlag(t *testing.T, runtime *common.RuntimeContext, name, value string) {
t.Helper()
if err := runtime.Cmd.Flags().Set(name, value); err != nil {
t.Fatalf("Flags().Set(%q, %q) error = %v", name, value, err)
}
}
func assertMeetingMessageSendValidationError(t *testing.T, err error, wantParam string) {
t.Helper()
if err == nil {
t.Fatal("expected validation error")
}
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed problem, got %T: %v", err, err)
}
if p.Category != errs.CategoryValidation {
t.Errorf("Category = %q, want %q", p.Category, errs.CategoryValidation)
}
if p.Subtype != errs.SubtypeInvalidArgument {
t.Errorf("Subtype = %q, want %q", p.Subtype, errs.SubtypeInvalidArgument)
}
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err)
}
if ve.Param != wantParam {
t.Errorf("Param = %q, want %q", ve.Param, wantParam)
}
}
func TestMeetingMessageSendBuildBody_Text(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "text", " hello ")
mustSetMeetingMessageSendFlag(t, runtime, "uuid", " cid-1 ")
body, err := buildMeetingMessageSendBody(runtime)
if err != nil {
t.Fatalf("buildMeetingMessageSendBody() error = %v", err)
}
if body["msg_type"] != meetingMessageTypeText {
t.Fatalf("msg_type = %v, want text", body["msg_type"])
}
if body["content"] != "hello" {
t.Fatalf("content = %v, want hello", body["content"])
}
if body["uuid"] != "cid-1" {
t.Fatalf("uuid = %v, want cid-1", body["uuid"])
}
}
func TestMeetingMessageSendBuildBody_Reaction(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction")
mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "LOVE")
body, err := buildMeetingMessageSendBody(runtime)
if err != nil {
t.Fatalf("buildMeetingMessageSendBody() error = %v", err)
}
if body["msg_type"] != meetingMessageTypeReaction {
t.Fatalf("msg_type = %v, want reaction", body["msg_type"])
}
if body["content"] != "LOVE" {
t.Fatalf("content = %v, want LOVE", body["content"])
}
if _, ok := body["text"]; ok {
t.Fatalf("text should be omitted for reaction, got %#v", body["text"])
}
if _, ok := body["emoji_type"]; ok {
t.Fatalf("emoji_type should be omitted for reaction, got %#v", body["emoji_type"])
}
}
func TestMeetingMessageSendBuildBody_ReactionVCFeedbackKey(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction")
mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "VC_NoSound")
body, err := buildMeetingMessageSendBody(runtime)
if err != nil {
t.Fatalf("buildMeetingMessageSendBody() error = %v", err)
}
if body["content"] != "VC_NoSound" {
t.Fatalf("content = %v, want VC_NoSound", body["content"])
}
}
func TestMeetingMessageSendValidateRejectsMeetingNumber(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "123456789")
mustSetMeetingMessageSendFlag(t, runtime, "text", "hello")
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--meeting-id")
if !strings.Contains(err.Error(), "9-digit meeting number") {
t.Fatalf("error = %v, want 9-digit meeting number hint", err)
}
}
func TestMeetingMessageSendValidateRejectsMissingEmojiType(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction")
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--emoji-type")
if !strings.Contains(err.Error(), "--emoji-type is required") {
t.Fatalf("error = %v, want --emoji-type required", err)
}
}
func TestMeetingMessageSendValidateRejectsTextMessageWithEmojiType(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "text")
mustSetMeetingMessageSendFlag(t, runtime, "text", "hello")
mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "LOVE")
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--emoji-type")
if !strings.Contains(err.Error(), "--emoji-type cannot be used") {
t.Fatalf("error = %v, want --emoji-type conflict", err)
}
}
func TestMeetingMessageSendValidateRejectsReactionMessageWithText(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction")
mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "LOVE")
mustSetMeetingMessageSendFlag(t, runtime, "text", "hello")
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--text")
if !strings.Contains(err.Error(), "--text cannot be used") {
t.Fatalf("error = %v, want --text conflict", err)
}
}
func TestMeetingMessageSendValidateRejectsLongText(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
mustSetMeetingMessageSendFlag(t, runtime, "text", strings.Repeat("a", meetingMessageMaxTextBytes+1))
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--text")
if !strings.Contains(err.Error(), "--text is too long") {
t.Fatalf("error = %v, want --text too long", err)
}
}
func TestMeetingMessageSendValidateRejectsLongUUID(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
mustSetMeetingMessageSendFlag(t, runtime, "text", "hello")
mustSetMeetingMessageSendFlag(t, runtime, "uuid", strings.Repeat("u", meetingMessageMaxUUIDBytes+1))
err := VCMeetingMessageSend.Validate(context.Background(), runtime)
assertMeetingMessageSendValidationError(t, err, "--uuid")
if !strings.Contains(err.Error(), "--uuid is too long") {
t.Fatalf("error = %v, want --uuid too long", err)
}
}
func TestMeetingMessageSendDryRun_Text(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
err := mountAndRun(t, VCMeetingMessageSend, []string{
"+meeting-message-send", "--dry-run", "--as", "user",
"--meeting-id", "7651377260537433044",
"--text", "hello",
"--uuid", "cid-1",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
for _, want := range []string{
"/open-apis/vc/v1/bots/message",
"\"meeting_id\": \"7651377260537433044\"",
"\"msg_type\": \"text\"",
"\"content\": \"hello\"",
"\"uuid\": \"cid-1\"",
} {
if !strings.Contains(out, want) {
t.Fatalf("dry-run output missing %q: %s", want, out)
}
}
}
func TestMeetingMessageSendDryRun_ValidationErrorEnvelope(t *testing.T) {
runtime := newMeetingMessageSendRuntime()
mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044")
dryRun := VCMeetingMessageSend.DryRun(context.Background(), runtime)
if got := dryRun.Format(); !strings.Contains(got, "--msg-type is required") {
t.Fatalf("dry-run error = %v, want --msg-type required", got)
}
}
func TestMeetingMessageSendExecute_Text(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
stub := &httpmock.Stub{
Method: "POST",
URL: buildMeetingMessageSendPath(),
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"msg_type": "text",
"uuid": "cid-1",
},
},
}
reg.Register(stub)
err := mountAndRun(t, VCMeetingMessageSend, []string{
"+meeting-message-send", "--as", "user",
"--format", "pretty",
"--meeting-id", "7651377260537433044",
"--text", "hello",
"--uuid", "cid-1",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
reg.Verify(t)
var req map[string]interface{}
if err := json.Unmarshal(stub.CapturedBody, &req); err != nil {
t.Fatalf("failed to parse request body: %v", err)
}
for key, want := range map[string]string{
"meeting_id": "7651377260537433044",
"msg_type": "text",
"content": "hello",
"uuid": "cid-1",
} {
if req[key] != want {
t.Errorf("%s = %v, want %s", key, req[key], want)
}
}
out := stdout.String()
for _, want := range []string{
"Meeting message sent.",
"Type: text",
"UUID: cid-1",
} {
if !strings.Contains(out, want) {
t.Fatalf("output missing %q: %s", want, out)
}
}
}
func TestMeetingMessageSendExecute_ReactionFallsBackToRequestType(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: buildMeetingMessageSendPath(),
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{},
},
})
err := mountAndRun(t, VCMeetingMessageSend, []string{
"+meeting-message-send", "--as", "user",
"--format", "pretty",
"--meeting-id", "7651377260537433044",
"--msg-type", "reaction",
"--emoji-type", "LOVE",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
reg.Verify(t)
if out := stdout.String(); !strings.Contains(out, "Type: reaction") {
t.Fatalf("output missing fallback type: %s", out)
}
}

View File

@@ -69,9 +69,7 @@ var WikiNodeGet = common.Shortcut{
{Name: "space-id", Desc: "optional: assert the resolved node lives in this space"},
},
Tips: []string{
"Example: lark-cli wiki +node-get --node-token https://feishu.cn/wiki/<token> --as user --format json",
"--node-token accepts a raw token (wikcnXXX, docxXXX, ...) or a Lark URL like https://feishu.cn/wiki/<token> or https://feishu.cn/docx/<token>.",
"Use --node-token in new commands; --token is a deprecated compatibility alias for old scripts.",
"For raw obj_tokens (not starting with wik), pass --obj-type so the API knows how to resolve them; URL inputs infer it from the path.",
"Pair with +move / +node-copy / +delete-space to confirm space_id, obj_type, and parent before mutating.",
"--token is the deprecated original name and still works for backward compatibility; new scripts should use --node-token.",

View File

@@ -243,7 +243,6 @@
默认值 / 约束:
- `style.format` 默认 `yyyy/MM/dd` 可用格式:`yyyy/MM/dd``yyyy/MM/dd HH:mm``yyyy/MM/dd HH:mm Z``yyyy-MM-dd``yyyy-MM-dd HH:mm``yyyy-MM-dd HH:mm Z``MM-dd``MM/dd/yyyy``dd/MM/yyyy`
- `style.format` 只控制前端显示格式;当前可配置格式最多显示到分钟,底层时间值仍可保留秒级精度。
常用写法:

View File

@@ -46,7 +46,6 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '<p>
- `resource-*` 目前仅支持 Docx 封面资源;其他图片、附件或素材请走 `+media-*`
- 如果目标是画板/whiteboard/画板缩略图 → 只能用 `lark-cli docs +media-download --type whiteboard`(不要用 `+media-preview`
- 拿到 spreadsheet URL/token 后 → 切到 `lark-sheets` 做对象内部操作
- 用户需要统计文档的**总字数 / 总字符数**word count / character count先读取 [`lark-doc-word-stat.md`](references/lark-doc-word-stat.md),并按其中流程调用 [`scripts/doc_word_stat.py`](scripts/doc_word_stat.py);统计口径以该脚本为准,不要改用其他方式自行计算。
- 用户说"给文档加评论""查看评论""回复评论""给评论加/删除表情 reaction" → 切到 `lark-drive` 处理
- 文档内容中出现嵌入的 `<sheet>``<bitable>``<cite file-type="sheets|bitable">` 标签时 → **必须主动提取 token 并切到对应技能下钻读取内部数据**,不能只呈现标签本身

View File

@@ -60,7 +60,6 @@ lark-cli docs +create --doc-format markdown --title "项目计划" --content $'#
| ------------------- | -- |---------------------------------------------|
| `--title` | 否 | 文档标题Markdown 导入时使用XML 创建推荐在 `--content` 开头写 `<title>...</title>`;多个标题仅保留第一个并在 `warnings` / `degrade_details` 提示 |
| `--content` | 视情况 | 文档内容XML 或 Markdown 格式);不传 `--content` 时必须传 `--title` |
| `--reference-map` | 否 | 结构化 `reference_map` JSON object必须与 `--content` 一起使用。普通写入优先把结构写在正文里;该参数主要用于保留或回放已有 `document.reference_map`。支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。 |
| `--doc-format` | 否 | 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) |
| `--parent-token` | 否 | 父文件夹或知识库节点 token`--parent-position` 互斥) |
| `--parent-position` | 否 | 父节点位置,如 `my_library`(与 `--parent-token` 互斥) |

View File

@@ -24,7 +24,7 @@
| `--command` | 是 | 操作指令(见下方指令速查表) |
| `--doc-format` | 否 | 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) |
| `--content` | 视指令 | 写入内容(`str_replace` 传空字符串可实现删除) |
| `--reference-map` | 否 | 结构化 `reference_map` JSON object必须与 `--content` 一起使用。普通写入优先把结构写在正文里;该参数主要用于保留或回放已有 `document.reference_map`支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。 |
| `--reference-map` | 否 | 结构化 `reference_map` JSON object `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map` |
| `--pattern` | 视指令 | 匹配文本str_replace |
| `--block-id` | 视指令 | 目标 block IDblock_* 操作),逗号分隔可批量删除,-1 表示末尾 |
| `--src-block-ids` | 视指令 | 源 block ID逗号分隔用于 block_copy_insert_after / block_move_after |

View File

@@ -1,77 +0,0 @@
# 文档统计:总字数 / 总字符数
当用户需要统计 Docx / Wiki 文档的总字数或总字符数时,使用本 skill 附带脚本 `scripts/doc_word_stat.py`。统计口径以该脚本为准,不要改用其他方式自行计算,也不要只读取 simple 摘要后统计。
## 调用方式
在线文档使用 XML full 内容,并让脚本读取 `docs +fetch --format json` 的 envelope
```bash
lark-cli docs +fetch --doc "$URL" --doc-format xml --detail full --format json \
| python3 skills/lark-doc/scripts/doc_word_stat.py --protocol xml --lark-json --pretty
```
`$URL` 可以是用户给出的 docx/wiki URL也可以是可被 `docs +fetch` 解析的 token。
如需在自动化或回归验证中发现未覆盖块类型,追加严格参数:
```bash
lark-cli docs +fetch --doc "$URL" --doc-format xml --detail full --format json \
| python3 skills/lark-doc/scripts/doc_word_stat.py --protocol xml --lark-json --pretty --fail-on-unsupported --fail-on-unknown
```
## 如何读取结果
脚本输出 JSON。对用户汇报时默认只读两个核心字段
- `word_count`:总字数。按语义单位统计汉字、英文单词/URL/code path、数字、中文标点普通贴着英文的英文标点不计入但独立 ASCII 符号、中文之间的 `/` 等以脚本结果为准。
- `char_count`:总字符数。统计汉字、英文字母、数字、中英文标点和脚本识别的可见符号;空格不计入。
其余字段用于排查或解释:
- `breakdown`:拆分统计来源,例如 `han_chars``english_words``digits``chinese_punctuations`
- `unknown_blocks`:脚本遇到未知 XML/Markdown 块类型;通常表示需要扩展解析规则。
- `unsupported_blocks`:脚本识别到块类型,但当前无法可靠提取可见文本。
- `diagnostics.has_unknown` / `diagnostics.has_unsupported`:快速判断统计是否存在覆盖风险。
如果 `unknown_blocks``unsupported_blocks` 非空,回复用户时要说明“已统计可提取文本,但存在未覆盖块,结果可能偏低”,并列出对应块类型。为空时可直接给出结果。
## 输出示例
输入正文等价于:`标题` + `一个苹果是 an apple。` 时,输出形态如下:
```json
{
"word_count": 10,
"char_count": 15,
"breakdown": {
"han_chars": 7,
"english_words": 2,
"number_words": 0,
"chinese_punctuations": 1,
"english_letters": 7,
"digits": 0,
"english_punctuations": 0,
"symbol_words": 0,
"symbol_chars": 0
},
"protocol": "xml",
"unknown_blocks": [],
"unsupported_blocks": [],
"diagnostics": {
"has_unknown": false,
"has_unsupported": false,
"types": {},
"unknown_types": {},
"unsupported_types": {},
"actions": {}
}
}
```
面向用户的回复可简化为:
```text
总字数10
总字符数15
```

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
基于 Search v2 接口 `POST /open-apis/search/v2/doc_wiki/search`支持以**用户身份或应用身份**统一搜索云空间(云盘/云存储)对象。
基于 Search v2 接口 `POST /open-apis/search/v2/doc_wiki/search`,以**用户身份**统一搜索云空间(云盘/云存储)对象。
核心特性:
@@ -14,8 +14,6 @@
> **资源发现入口统一**`drive +search` 同样返回 `SHEET` / `Base` / `FOLDER` 等全部云空间(云盘/云存储)对象,不只是文档 / Wiki。用户说"找一个表格"、"找报表"、"最近打开的表格"时,也从这里开始;定位后再切到对应业务 skill如 `lark-sheets`)做对象内部操作。
> **身份边界**普通关键词、类型、文件夹、Wiki 空间、owner/open_id 等显式过滤支持 `--as user` 或 `--as bot`。`--mine` / `--created-by-me` 依赖当前登录用户 open_id 自动填充过滤条件;应用身份下如果没有配置用户 open_id请改用显式 `--creator-ids` / `--original-creator-ids`。
## 命令
> **关键约束:搜索关键词必须通过 `--query` 传递。**

View File

@@ -33,7 +33,7 @@ lark-cli config init --new
| 按业务域授权 | `lark-cli auth login --domain docs --domain drive --no-wait --json``--domain` 可重复,也可用逗号分隔 |
| 指定单个 scope 授权 | `lark-cli auth login --scope "<scope>" --no-wait --json` |
| 检查当前登录态、是谁登录、token 是否有效 | `lark-cli auth status --json --verify`;回答时引用 `identity``verified``identities.user.status``identities.user.userName``identities.user.openId`(用户 open id`identities.user.tokenStatus``identities.user.scope` |
| 快速看当前身份状态 | `lark-cli whoami`;实际生效的那一个身份 |
| 快速看当前生效身份(人类可读) | `lark-cli whoami`聚焦实际生效的那一个身份(走 `--as`/`default-as`/strict-mode 解析,可能与 `auth status` 的「第一个可用身份」不同),含当前 profile。脚本/agent 取结构化结果加 `--json`(字段 `identity``identitySource``available``tokenStatus`)。深度诊断 / 服务器校验仍用 `auth status --json --verify` |
| 退出当前机器的用户登录态 | `lark-cli auth logout --json``loggedOut:true` 表示注销成功 |
| bot 缺少权限 | 不要执行 `auth login`;引导用户在开发者后台开通 bot scope优先复用错误里的 `console_url` |
| 取消用户对应用的全部服务端授权 | `auth logout` 只清本机登录态;服务端授权需用户在飞书授权管理页取消 |

View File

@@ -4,7 +4,7 @@ version: 1.0.0
description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。不负责云文档内容编辑走 lark-doc、云文档里的独立画板对象走 lark-whiteboard注意 slide 内嵌的流程图/架构图仍属本 skill、上传或下载普通文件走 lark-drive。"
metadata:
requires:
bins: ["lark-cli"]
bins: [ "lark-cli" ]
cliHelp: "lark-cli slides --help"
---
@@ -12,217 +12,55 @@ metadata:
## Quick Reference
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| 已有 PPT 大幅改写 | 多页整页重建用 `+replace-pages`,单页局部编辑用 `+replace-slide` | `xml_presentations.get``lark-slides-replace-pages.md``lark-slides-edit-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token回读全文或单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `xml_presentations.get``xml_presentation.slide.get` |
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面 | `slides +screenshot``lark-slides-screenshot.md` |
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides``@./path` 占位符 |
| 在 slide 中绘制柱/条/折线/面积/雷达/饼等有数据序列的图表 | 使用原生 `<chart>` 元素 | `xml-schema-quick-ref.md` |
| 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | 必须先用 Read 工具读取参考文档,再生成 `<whiteboard>` 元素 | [`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md) |
| 使用语义图标 | 先检索 IconPark再写 `<icon iconType="...">` | `iconpark_tool.py search → resolve``iconpark.md` |
| 用户提到模板、主题、版式 | 先检索模板,再摘要,必要时裁切骨架 | `template_tool.py search → summarize → extract` |
| 创建失败、空白页、3350001、布局异常 | 先回读状态,再按排障清单修复,不假设原操作原子成功 | `troubleshooting.md``validation-checklist.md` |
本地 `skills/lark-slides/scripts/` 下的 Python 工具要求 Python 3.10+;如果 `python3 --version` 低于 3.10,先切换到 3.10+ 解释器再运行脚本。
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),认证、权限和全局参数均以 lark-shared 为准。**
| 用户需求 | 指引 |
|----------|------|
| 读取 / 分析本地 PPTX 内容 | 文本用 `python -m markitdown presentation.pptx`;视觉总览用 `python3 scripts/thumbnail.py presentation.pptx`;原始 OOXML 用 `python3 scripts/office/unpack.py presentation.pptx unpacked/` |
| 从模板创建或编辑已有本地 PPTX | 先读 `lark-slides-pptx-template-workflows.md` |
| 从零新建飞书在线 PPT | 先读 `lark-slides-create-workflows.md` |
| 获取在线 slides 内容、读取 / 分析已有在线 PPT | XML 内容优先用 `slides +xml-get` 保存到文件;页面视觉内容用 `slides +screenshot`,详见 `lark-slides-screenshot.md` |
**CRITICAL — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
## 读取 / 分析内容
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**
**CRITICAL — 新建演示文稿或大幅改写页面时,生成 XML 前 MUST 读取 [visual-planning.md](references/visual-planning.md),确保 `layout_type`、`visual_focus`、`text_density` 实际改变页面几何、主视觉和文本量。**
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
**CRITICAL — 创建或大幅改写后MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素、检查空白/破损页、明显溢出、布局风险XML 语法和文本重叠静态检查优先使用 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py)。**
**CRITICAL — 创建前自检或失败排障时MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
**CRITICAL — 如果用户提到“模板”“套用模板”“参考某种主题/风格/版式”或用户需求明显落在已有场景模板内如工作汇报、产品介绍、商业计划书、培训、晋升汇报等MUST 先用 [`scripts/template_tool.py`](scripts/template_tool.py) 的 `search` 做模板检索;默认给出 2-3 个最匹配模板候选供用户选择。锁定模板后用 `summarize` 获取主题和布局摘要;只有需要布局骨架时才用 `extract` 裁切目标页型 XML。不要直接读取完整模板 XML。**
> [!NOTE]
> `scripts/template_tool.py` 需要 Python 3。`references/template-index.json` 是脚本缓存/轻量路由索引,不是默认给 agent 阅读的文档;`assets/templates/*.xml` 是机器资源,只应通过脚本摘要或裁切,不要全文读取。
**CRITICAL — 使用模板生成或改写页面时MUST 先 `summarize` 目标页型;只有需要具体布局骨架时才 `extract`。**
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](references/lark-slides-replace-slide.md)(块级替换/插入,不动页序);已有 Slides 的多页大改优先用 [`+replace-pages`](references/lark-slides-replace-pages.md) 在原 presentation 内批量重建页面,避免 `slides +create` 生成新链接。选择 action 和完整读-改-写流程见 [`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)。
## 身份选择
飞书幻灯片通常是用户自己的内容资源。**默认应优先显式使用 `--as user`(用户身份)执行 slides 相关操作**,始终显式指定身份。
- **`--as user`(推荐)**:以当前登录用户身份创建、读取、管理演示文稿。执行前先完成用户授权:
### 本地 PPTX
```bash
lark-cli auth login --domain slides
# 提取文本
python -m markitdown presentation.pptx
# 生成视觉总览图
python3 scripts/thumbnail.py presentation.pptx
# 解包查看原始 OOXML
python3 scripts/office/unpack.py presentation.pptx unpacked/
```
- **`--as bot`**:仅在用户明确要求以应用身份操作,或需要让 bot 持有/创建资源时使用。使用 bot 身份时,要额外确认 bot 是否真的有目标演示文稿的访问权限。
**执行规则**
1. 创建、读取、增删 slide、按用户给出的链接继续编辑已有 PPT默认都先用 `--as user`
2. 如果出现权限不足,先检查当前是否误用了 bot 身份;不要默认回退到 bot。
3. 只有在用户明确要求"用应用身份 / bot 身份操作",或当前工作流就是 bot 创建资源后再做协作授权时,才切换到 `--as bot`
## 执行前必做
> **重要**`references/slides_xml_schema_definition.xml` 是此 skill 唯一正确的 XML 协议来源;其他 md 仅是对它和 CLI schema 的摘要。
高频只读:
- [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md)
- [planning-layer.md](references/planning-layer.md)(新建 / 大幅改写)
- [visual-planning.md](references/visual-planning.md)(新建 / 大幅改写)
- [asset-planning.md](references/asset-planning.md)(新建 / 大幅改写)
- [validation-checklist.md](references/validation-checklist.md)(创建 / 大幅改写后)
按需再读:
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)、[`lark-slides-replace-pages.md`](references/lark-slides-replace-pages.md)
- 截图:[`lark-slides-screenshot.md`](references/lark-slides-screenshot.md)
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
- 流程图 / 时序图 / 架构图 / 装饰图案:[`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md)
- 图标:[`iconpark.md`](references/iconpark.md)、[`scripts/iconpark_tool.py`](scripts/iconpark_tool.py)
- 模板:[`template-catalog.md`](references/template-catalog.md)、[`scripts/template_tool.py`](scripts/template_tool.py)
- 排障:[`troubleshooting.md`](references/troubleshooting.md)
- 完整协议:[`slides_xml_schema_definition.xml`](references/slides_xml_schema_definition.xml)
## Workflow
> **这是演示文稿,不是文档。** 每页 slide 是独立的视觉画面,信息密度要低,排版要留白。
### Design Ideas
不要生成无设计感的幻灯片。纯白背景 + 标题 + bullets 只能作为极简临时稿,不能作为正式交付。
开始写 XML 前,先在 `slide_plan.json` 里确定 deck 级视觉策略:
- **主题化配色**:配色必须服务本次主题、行业和受众,不要默认蓝色商务风。如果把同一套颜色换到另一个完全不同主题仍然成立,说明配色不够具体。
- **主次比例**:选择 1 个主色承担约 60-70% 视觉权重1-2 个辅助色承担结构和分区1 个强调色只用于关键数字、结论或行动点。不要让所有颜色权重相同。
- **背景一致性**:先确定全 deck 的背景策略,默认保持同一明暗基调和底色体系;只有分节、转场或强调页才有意改变背景,并必须通过相同主色、纹理、边栏或 motif 让变化看起来属于同一套设计。无论深浅,都要保证正文、图标和线条对比充足。
- **统一 motif**:选择一个可复用视觉母题贯穿全文,例如粗侧边栏、圆形图标底、半出血图片区、编号节点、卡片左上角色块或大号数字。不要每页换一套装饰语言。
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构、大号数字、示意图或由 shape 组成的抽象视觉。文本框本身不算主视觉。
可优先考虑这些页面形态:
- **双栏结构**:左文右图或左图右文,视觉区域占 35-45% 宽度。
- **图标行**:图标在色块或圆形底中,右侧是短标题和一句解释。
- **2x2 / 2x3 网格**:适合能力、模块、风险、行动项,每格内容保持同等层级。
- **半出血视觉**:图片或抽象形状占据左/右半屏,文字覆盖或贴边排布。
- **大数字卡片**:关键指标用 60-72pt 数字,下面配 10-14pt 标签。
- **对比列**before/after、方案 A/B、问题/解法用左右并列,标题和基线严格对齐。
- **时间线/流程图**:步骤用节点和箭头表达,流程方向必须一眼可见。
字体和间距建议:
- 标题 36-44pt关键结论可更大正文 14-18pt注释 10-12pt。
- 正文默认左对齐;只在封面、结尾或大号数字场景中使用居中。
- 页面边距至少 40px内容块之间保持 24-40px 间距,并在同一 deck 内保持一致。
- 卡片内边距要真实留出空间,不要让文字贴边;对齐 shape 和文字时要考虑文本框 padding。
常见错误必须避免:
- 不要所有页面复用同一种标题 + 三 bullets 版式。
- 不要用低对比文字或低对比图标,例如浅灰字压在浅色背景上。
- 不要让装饰线穿过文字,或让页脚、来源、编号挤压主体内容。
- 不要把素材缺失表现为空白图片框;必须按 `fallback_if_missing` 生成 XML-native 视觉。
- 不要留下模板占位文案、示例公司名、示例日期或与用户主题无关的原模板内容。
### 创建方式选择
| 场景 | 推荐方式 |
|------|----------|
| 简单 XML1-3 页、结构简单、几乎无复杂中文和特殊字符) | `slides +create --slides '[...]'` 一步创建 |
| 复杂 XML多页、含中文、大段文本、复杂布局、嵌套引号、特殊字符较多 | **两步创建**:先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide create` 逐页添加 |
| 已有 PPT 继续追加或插入页面 | 使用 `xml_presentation.slide create`,必要时配合 `before_slide_id` |
> [!WARNING]
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议使用两步创建法。
> [!IMPORTANT]
> `slides +create --slides` 底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
### 模板与脚本优先流程
模板细则见 [template-catalog.md](references/template-catalog.md)。主流程只记住:先 `search`,锁定后 `summarize`,需要骨架时才 `extract`;不要直接读取完整模板 XML 或照搬占位文案。
### 在线 Slides
```bash
python3 skills/lark-slides/scripts/template_tool.py search --query "<用户需求原文>" --limit 3
python3 skills/lark-slides/scripts/template_tool.py summarize --template <template-id> --label <封面|目录|分节|内容|结尾>
python3 skills/lark-slides/scripts/template_tool.py extract --template <template-id> --label <页型> --out /tmp/template-slice.xml
# 读取完整 XML 内容,优先保存到文件再分析
lark-cli slides +xml-get --as user --presentation <slides_url_or_token> --output presentation.xml --json
# 获取页面截图;必须指定 --slide-number 或 --slide-id多个页面可重复传 --slide-number
lark-cli slides +screenshot --as user --presentation <slides_url_or_token> --slide-number 1 --output-dir screenshots --json
```
```text
Step 1: 需求澄清 & 读取知识
- 澄清主题、受众、页数、风格;模板需求按“模板与脚本优先流程”处理
- 读取 xml-schema-quick-ref.md新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
在线 Slides 的截图参数和页码语义详见 [`lark-slides-screenshot.md`](references/lark-slides-screenshot.md);需要继续编辑在线 Slides 时,按 `lark-slides-create-workflows.md` / `lark-slides-replace-workflows.md` 选择创建或替换流程。
Step 2: 生成大纲 → 用户确认 → 写入 slide_plan.json
- 生成结构化大纲供用户确认;如使用模板,标明基于哪个模板改写
- 新建 / 大幅改写必须先创建目录并写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`
- plan 字段、路径命名、模板边界和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
## 编辑 PPTX 工作流
Step 3: 按 slide_plan.json 生成 XML → 创建
- 逐页消费 plankey_message 定主结论layout_type 定几何visual_focus 定主视觉text_density 定文本量
- 缺少真实素材时必须用 `fallback_if_missing` 生成 XML-native 兜底视觉;不要留空
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
**完整流程先读 [`lark-slides-pptx-template-workflows.md`](references/lark-slides-pptx-template-workflows.md)。**
Step 4: 审查 & 交付
- 创建完成后,必须用 xml_presentations.get 读取全文 XML并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
- 没问题 → 交付:告知用户演示文稿 ID 和访问方式
```
1.`thumbnail.py``markitdown` 分析模板。
2. 解包 -> 调整页面结构 -> 编辑内容 -> 清理 -> 打包。
3. 交付前完成必需 QA。
### jq 命令模板(编辑已有 PPT 时使用)
## 从零创建
新建 PPT 推荐用 `+create --slides`。以下 jq 模板适用于向已有演示文稿追加页面的场景,可以避免手动转义双引号:
**完整流程先读 [`lark-slides-create-workflows.md`](references/lark-slides-create-workflows.md)。**
```bash
# 追加到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="BACKGROUND_COLOR"/></fill></style>
<data>
在这里放置 shape、line、table、chart、whiteboard 等元素
</data>
</slide>' '{slide:{content:$content}}')"
# 插到指定页之前before_slide_id 必须在 --data body 里,与 slide 同级
# ⚠️ 不要把 before_slide_id 写进 --params —— CLI 会当未知 query 参数静默下发,服务端忽略,新页跑到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide ...>...</slide>' --arg before 'TARGET_SLIDE_ID' \
'{slide:{content:$content}, before_slide_id:$before}')"
```
> 渐变色必须使用 `rgba()` 格式并带百分比停靠点,如 `linear-gradient(135deg,rgba(15,23,42,1) 0%,rgba(56,97,140,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端回退为白色。
### 大纲模板
生成大纲时使用以下格式,交给用户确认:
```text
[PPT 标题] — [定位描述],面向 [目标受众]
模板:[未使用模板 / <category>/<template>.xml推荐原因]
页面结构N 页):
1. 封面页:[标题文案]
2. [页面主题][要点1]、[要点2]、[要点3]
3. [页面主题][要点描述]
...
N. 结尾页:[结尾文案]
风格:[配色方案][排版风格]
```
当没有本地 PPTX 模板 / 参考演示文稿,或目标是新建飞书 / Lark 在线 Slides 而不是本地 `.pptx` 文件时,使用该流程。
## 核心概念
@@ -259,35 +97,126 @@ Slides (演示文稿)
└── slide_id (页面唯一标识)
```
## Shortcuts 与 API
## 身份选择
Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`)。有 Shortcut 的操作优先使用
飞书幻灯片通常是用户自己的内容资源。**默认应优先显式使用 `--as user`(用户身份)执行 slides 相关操作**,始终显式指定身份
| Shortcut | 说明 |
|----------|------|
| [`+create`](references/lark-slides-create.md) | 创建 PPT可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
| [`+replace-pages`](references/lark-slides-replace-pages.md) | 在原演示文稿内批量重建多个页面:先创建新页到旧页前,再删除旧页;适合已有 Slides 的多页大改,不新建链接 |
没有 Shortcut 覆盖时使用原生 API。高频资源`xml_presentations.get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
- **`--as user`(推荐)**:以当前登录用户身份创建、读取、管理演示文稿。执行前先完成用户授权:
```bash
lark-cli schema slides.<resource>.<method> # 调用 API 前必须先查看参数结构
lark-cli slides <resource> <method> [flags] # 调用 API
lark-cli auth login --domain slides
```
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式
- **`--as bot`**:仅在用户明确要求以应用身份操作,或需要让 bot 持有/创建资源时使用。使用 bot 身份时,要额外确认 bot 是否真的有目标演示文稿的访问权限
## 核心规则
**执行规则**
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide.create` 逐页添加
3. **`<slide>` 直接子元素只有 `<style>``<data>``<note>`**:文本和图形必须放在 `<data>`
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内
5. **保存关键 ID**:后续操作需要 `xml_presentation_id``slide_id``revision_id`
6. **删除谨慎**:删除操作不可逆,且至少保留一页幻灯片
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide``block_replace` / `block_insert`),不要整页重建;已有 Slides 的多页整页重建用 `+replace-pages`,不要用 `slides +create` 新建整份 PPT只有没有 shortcut 覆盖的特殊单页整页操作才手动 `slide.create` + `slide.delete`
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传或 `+create --slides``@./path` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**slides upload API 不支持分片上传)。
1. 创建、读取、增删 slide、按用户给出的链接继续编辑已有 PPT默认都先用 `--as user`
2. 如果出现权限不足,先检查当前是否误用了 bot 身份;不要默认回退到 bot。
3. 只有在用户明确要求"用应用身份 / bot 身份操作",或当前工作流就是 bot 创建资源后再做协作授权时,才切换到 `--as bot`
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
## 设计思路
不要交付只有白底、标题和项目符号的幻灯片。正式页面至少要在视觉元素、信息结构、配色、字体或留白上体现明确设计意图。
### 开始前
- **配色贴合主题**:配色要回应本次主题、行业和受众,不要套用通用蓝色商务风;如果换到另一个完全无关主题也成立,说明选择还不够具体。
- **建立视觉主次**:主色承担约 60-70% 视觉权重1-2 个辅助色负责分区和层级,强调色只用于关键数字、结论或行动点。
- **规划明暗节奏**:可采用深色封面、浅色内容、深色结尾的结构,也可以全篇深色;无论哪种策略,都要保证正文、图标和线条有足够对比度。
- **固定一个视觉母题**:选择一个可复用元素贯穿全文,例如圆角图片框、彩色圆形图标底、粗侧边栏、编号节点、半出血图片区或大号数字,避免每页换一套装饰语言。
### 配色参考
根据内容选择颜色,不要把蓝色当作默认答案。下表只提供方向感,实际使用时可按页面明暗、透明度和对比需求微调。
| 风格 | 主色 | 辅助色 | 强调色 |
|------|------|--------|--------|
| **深夜高管风** | `1E2761`(深海军蓝) | `CADCFC`(冰蓝) | `FFFFFF`(白) |
| **森林苔藓风** | `2C5F2D`(森林绿) | `97BC62`(苔绿色) | `F5F5F5`(浅米白) |
| **珊瑚活力风** | `F96167`(珊瑚红) | `F9E795`(金黄) | `2F3C7E`(藏青) |
| **暖陶土风** | `B85042`(陶土红) | `E7E8D1`(沙色) | `A7BEAE`(鼠尾草绿) |
| **海洋渐变风** | `065A82`(深海蓝) | `1C7293`(蓝绿色) | `21295C`(午夜蓝) |
| **炭灰极简风** | `36454F`(炭灰) | `F2F2F2`(灰白) | `212121`(近黑) |
| **青绿信任风** | `028090`(青蓝) | `00A896`(海沫绿) | `02C39A`(薄荷绿) |
| **莓果奶油风** | `6D2E46`(莓紫) | `A26769`(玫瑰灰) | `ECE2D0`(奶油色) |
| **鼠尾草冷静风** | `84B59F`(鼠尾草绿) | `69A297`(桉叶绿) | `50808E`(石板蓝) |
| **樱桃强对比风** | `990011`(樱桃红) | `FCF6F5`(暖白) | `2F3C7E`(藏青) |
### 单页设计
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构、大号数字、示意图或由 shape 组成的抽象视觉。文本框本身不算主视觉。
布局可选:
- 双栏结构:左文右图或左图右文,视觉区域占 35-45% 宽度。
- 图标文本行:图标放在色块或圆形底中,旁边配短标题和一句解释。
- 2x2 / 2x3 网格:适合能力、模块、风险、行动项,每格内容保持同等层级。
- 半出血视觉:图片或抽象形状占据左/右半屏,文字覆盖或贴边排布。
数据展示可选:
- 大数字卡片:关键指标用 60-72pt 数字,下面配 10-14pt 标签。
- 对比列before/after、方案 A/B、问题/解法用左右并列,标题和基线严格对齐。
- 时间线或流程图:步骤用编号节点和箭头表达,流程方向必须一眼可见。
视觉细节可选:
- section header 旁可以放小号彩色圆形图标。
- 关键数字、tagline 或结论短句可用斜体或强调色,但不要把整段正文都做成强调样式。
### 字体排版
标题字体可以更有性格,正文字体必须清晰耐读;不要整份 deck 都默认 Arial。生成 XML 时,`fontFamily` 应使用以下支持字体的精确名称;同一份 deck 内优先选择 1-2 个字体家族,避免每页混用太多字体。
| 标题字体 | 正文字体 |
|----------|----------|
| Arial Black | Arial |
| Georgia | Calibri |
| Trebuchet MS | Calibri |
| Playfair Display | Lato |
| Montserrat | Open Sans |
| 思源宋体 | 思源黑体 |
| 元素 | 字号 |
|------|------|
| 页面标题 | 36-44pt加粗 |
| 分区标题 | 20-24pt加粗 |
| 正文 | 14-16pt |
| 注释/来源 | 10-12pt弱化处理 |
#### 常用中文字体
思源宋体、寒蝉德黑体、标小智无界黑、寒蝉锦书宋、站酷小薇体、寒蝉团圆体 圆体、寒蝉团圆体 黑体、荆南缘默体、寒蝉端黑宋、资源圆体、钟齐流江毛草、寒蝉端黑体、站酷庆科黄油体、寒蝉云墨黑、有字库龙藏体、寒蝉全圆体、思源黑体、钟齐志莽行书、抖音美好体、马善政毛笔楷体、霞鹜 975 圆体
#### 常用拉丁字体
Francois One、Heebo、Lobster、Roboto Slab、Varela Round、PT Serif、Signika、Vollkorn、Mulish、Rokkitt、Inconsolata、PT Sans Caption、EB Garamond、Dancing Script、Rajdhani、Poppins、Merriweather、PT Sans Narrow、Libre Baskerville、Slabo 27px、Inter、Noto Serif、Yanone Kaffeesatz、Merriweather Sans、Lato、Source Code Pro、Mukta、Teko、Hind Siliguri、Catamaran、Arvo、Alegreya Sans、Titillium Web、Roboto Mono、Play、Indie Flower、Ubuntu Condensed、Libre Franklin、Barlow、PT Sans、Acme、Cuprum、Josefin Sans、DM Sans、Playfair Display、Rubik、Questrial、Anton、Oswald、Cabin、Ubuntu、Abel、Exo 2、Bree Serif、Roboto Condensed、Amatic SC、Abril Fatface、Comfortaa、IBM Plex Sans、Work Sans、Kanit、Noto Sans、Alegreya、Shadows Into Light、Barlow Condensed、Nunito Sans、Quicksand、Overpass、Bebas Neue、Raleway、Exo、Archivo Narrow、Hind、Open Sans、Poiret One、Asap、Roboto、Nunito、Bitter、Dosis、Oxygen、Prompt、Karla、Fjalla One、Fira Sans、Crimson Text、Pacifico、Arimo、Maven Pro、Cairo、Montserrat、Righteous、Lora
#### 其他语言字体
角ゴシック、본고딕、Nanum Gothic
#### 系统字体
Arial、Arial Black、Calibri、Comic Sans Ms、Sans Serif、Serif、Times New Roman、Tahoma、Trebuchet MS、Verdana、Georgia、Garamond、黑体、宋体、楷体、Hiragino Mincho
### 留白与间距
- 页面边距至少 0.5"。
- 内容块之间保持 0.3-0.5" 间距,并在同一 deck 内保持一致。
- 留出呼吸感,不要填满每一寸空间。
- 卡片内边距要真实留出空间;对齐 shape 和文字时要考虑文本框 padding必要时给文本框设置 `margin: 0`
### 避免事项
- 不要所有页面复用同一种标题 + 三 bullets 版式。
- 不要正文居中;段落和列表默认左对齐,只在封面、结尾或大号数字场景中居中。
- 不要缺少字号层级;标题需要 36pt+,明显区别于 14-16pt 正文。
- 不要默认蓝色;配色要反映具体主题。
- 不要随机混用间距;选择 0.3" 或 0.5" 间距后全 deck 统一。
- 不要只设计一页,其余页面保持 plain要么全篇贯彻设计语言要么整体保持克制。
- 不要创建纯文本页;必须加入图片、图标、图表或其他视觉元素,避免 plain title + bullets。
- 不要忘记文本框 padding线条或 shape 与文字边缘对齐时,设置 `margin: 0` 或为 padding 做偏移。
- 不要使用低对比元素;图标和文字都必须和背景有强对比。
- 不要在标题下方画一条装饰强调线;这类做法很容易显得模板化。优先用留白、背景色块或结构分区建立层级。

View File

@@ -0,0 +1,202 @@
---
name: lark-slides
version: 1.0.0
description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。不负责云文档内容编辑走 lark-doc、云文档里的独立画板对象走 lark-whiteboard注意 slide 内嵌的流程图/架构图仍属本 skill、上传或下载普通文件走 lark-drive。"
metadata:
requires:
bins: ["lark-cli"]
cliHelp: "lark-cli slides --help"
---
# slides (v1)
## Quick Reference
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| 已有 PPT 大幅改写 | 多页整页重建用 `+replace-pages`,单页局部编辑用 `+replace-slide` | `xml_presentations.get``lark-slides-replace-pages.md``lark-slides-replace-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token回读全文或单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `xml_presentations.get``xml_presentation.slide.get` |
| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面 | `slides +screenshot``lark-slides-screenshot.md` |
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides``@./path` 占位符 |
| 在 slide 中绘制柱/条/折线/面积/雷达/饼等有数据序列的图表 | 使用原生 `<chart>` 元素 | `xml-schema-quick-ref.md` |
| 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | 必须先用 Read 工具读取参考文档,再生成 `<whiteboard>` 元素 | [`lark-slides-whiteboard.md`](lark-slides-whiteboard.md) |
| 使用语义图标 | 先检索 IconPark再写 `<icon iconType="...">` | `iconpark_tool.py search → resolve``iconpark.md` |
| 用户提到模板、主题、版式 | 先检索模板,再摘要,必要时裁切骨架 | `template_tool.py search → summarize → extract` |
| 创建失败、空白页、3350001、布局异常 | 先回读状态,再按排障清单修复,不假设原操作原子成功 | `troubleshooting.md``validation-checklist.md` |
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md),认证、权限和全局参数均以 lark-shared 为准。**
**CRITICAL — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录规划层规则和中间产物生命周期见 [planning-layer.md](planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**
**CRITICAL — 新建演示文稿或大幅改写页面时,生成 XML 前 MUST 读取 [visual-planning.md](visual-planning.md),确保 `layout_type`、`visual_focus`、`text_density` 实际改变页面几何、主视觉和文本量。**
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
**CRITICAL — 创建或大幅改写后MUST 按 [validation-checklist.md](validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素、检查空白/破损页、明显溢出、布局风险XML 语法和文本重叠静态检查优先使用 [`../scripts/xml_text_overlap_lint.py`](../scripts/xml_text_overlap_lint.py)。**
**CRITICAL — 创建前自检或失败排障时MUST 按 [troubleshooting.md](troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
**CRITICAL — 如果用户提到“模板”“套用模板”“参考某种主题/风格/版式”或用户需求明显落在已有场景模板内如工作汇报、产品介绍、商业计划书、培训、晋升汇报等MUST 先用 [`../scripts/template_tool.py`](../scripts/template_tool.py) 的 `search` 做模板检索;默认给出 2-3 个最匹配模板候选供用户选择。锁定模板后用 `summarize` 获取主题和布局摘要;只有需要布局骨架时才用 `extract` 裁切目标页型 XML。不要直接读取完整模板 XML。**
> [!NOTE]
> `../scripts/template_tool.py` 需要 Python 3。`template-index.json` 是脚本缓存/轻量路由索引,不是默认给 agent 阅读的文档;`../assets/templates/*.xml` 是机器资源,只应通过脚本摘要或裁切,不要全文读取。
**CRITICAL — 使用模板生成或改写页面时MUST 先 `summarize` 目标页型;只有需要具体布局骨架时才 `extract`。**
**编辑已有幻灯片页面**:单个标题、文本块、图片或局部元素优先用 [`+replace-slide`](lark-slides-replace-slide.md)(块级替换/插入,不动页序);已有 Slides 的多页大改优先用 [`+replace-pages`](lark-slides-replace-pages.md) 在原 presentation 内批量重建页面,避免 `slides +create` 生成新链接。选择 action 和完整读-改-写流程见 [`lark-slides-replace-workflows.md`](lark-slides-replace-workflows.md)。
## 执行前必做
> **重要**`slides_xml_schema_definition.xml` 是此 skill 唯一正确的 XML 协议来源;其他 md 仅是对它和 CLI schema 的摘要。
高频只读:
- [xml-schema-quick-ref.md](xml-schema-quick-ref.md)
- [planning-layer.md](planning-layer.md)(新建 / 大幅改写)
- [visual-planning.md](visual-planning.md)(新建 / 大幅改写)
- [asset-planning.md](asset-planning.md)(新建 / 大幅改写)
- [validation-checklist.md](validation-checklist.md)(创建 / 大幅改写后)
按需再读:
- 创建:[`lark-slides-create.md`](lark-slides-create.md)
- 编辑:[`lark-slides-replace-workflows.md`](lark-slides-replace-workflows.md)、[`lark-slides-replace-slide.md`](lark-slides-replace-slide.md)、[`lark-slides-replace-pages.md`](lark-slides-replace-pages.md)
- 截图:[`lark-slides-screenshot.md`](lark-slides-screenshot.md)
- 图片:[`lark-slides-media-upload.md`](lark-slides-media-upload.md)
- 流程图 / 时序图 / 架构图 / 装饰图案:[`lark-slides-whiteboard.md`](lark-slides-whiteboard.md)
- 图标:[`iconpark.md`](iconpark.md)、[`../scripts/iconpark_tool.py`](../scripts/iconpark_tool.py)
- 模板:[`template-catalog.md`](template-catalog.md)、[`../scripts/template_tool.py`](../scripts/template_tool.py)
- 排障:[`troubleshooting.md`](troubleshooting.md)
- 完整协议:[`slides_xml_schema_definition.xml`](slides_xml_schema_definition.xml)
## Workflow
> **这是演示文稿,不是文档。** 每页 slide 是独立的视觉画面,信息密度要低,排版要留白。
### 创建方式选择
| 场景 | 推荐方式 |
|------|----------|
| 简单 XML1-3 页、结构简单、几乎无复杂中文和特殊字符) | `slides +create --slides '[...]'` 一步创建 |
| 复杂 XML多页、含中文、大段文本、复杂布局、嵌套引号、特殊字符较多 | **两步创建**:先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide create` 逐页添加 |
| 已有 PPT 继续追加或插入页面 | 使用 `xml_presentation.slide create`,必要时配合 `before_slide_id` |
> [!WARNING]
> `--slides '[...]'` 的风险点主要在 shell 参数传递,而不是单纯页数。即使只有 1 页,只要 XML 足够复杂,也建议使用两步创建法。
> [!IMPORTANT]
> `slides +create --slides` 底层会逐页创建,不是原子操作。中途失败时先记录 `xml_presentation_id`,回读确认当前状态,再继续修复或追加。
### 模板与脚本优先流程
模板细则见 [template-catalog.md](template-catalog.md)。主流程只记住:先 `search`,锁定后 `summarize`,需要骨架时才 `extract`;不要直接读取完整模板 XML 或照搬占位文案。
```bash
python3 skills/lark-slides/scripts/template_tool.py search --query "<用户需求原文>" --limit 3
python3 skills/lark-slides/scripts/template_tool.py summarize --template <template-id> --label <封面|目录|分节|内容|结尾>
python3 skills/lark-slides/scripts/template_tool.py extract --template <template-id> --label <页型> --out /tmp/template-slice.xml
```
```text
Step 1: 需求澄清 & 读取知识
- 澄清主题、受众、页数、风格;模板需求按“模板与脚本优先流程”处理
- 读取 xml-schema-quick-ref.md新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
Step 2: 生成大纲 → 用户确认 → 写入 slide_plan.json
- 生成结构化大纲供用户确认;如使用模板,标明基于哪个模板改写
- 新建 / 大幅改写必须先创建目录并写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`
- plan 字段、路径命名、模板边界和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
Step 3: 按 slide_plan.json 生成 XML → 创建
- 逐页消费 plankey_message 定主结论layout_type 定几何visual_focus 定主视觉text_density 定文本量
- 缺少真实素材时必须用 `fallback_if_missing` 生成 XML-native 兜底视觉;不要留空
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
Step 4: 审查 & 交付
- 创建完成后,必须用 xml_presentations.get 读取全文 XML并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
- 失败或部分成功按 troubleshooting.md 处理;局部问题优先用 `+replace-slide` 修正
- 没问题 → 交付:告知用户演示文稿 ID 和访问方式
```
### jq 命令模板(编辑已有 PPT 时使用)
新建 PPT 推荐用 `+create --slides`。以下 jq 模板适用于向已有演示文稿追加页面的场景,可以避免手动转义双引号:
```bash
# 追加到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide xmlns="http://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="BACKGROUND_COLOR"/></fill></style>
<data>
在这里放置 shape、line、table、chart、whiteboard 等元素
</data>
</slide>' '{slide:{content:$content}}')"
# 插到指定页之前before_slide_id 必须在 --data body 里,与 slide 同级
# ⚠️ 不要把 before_slide_id 写进 --params —— CLI 会当未知 query 参数静默下发,服务端忽略,新页跑到末尾
lark-cli slides xml_presentation.slide create \
--as user \
--params '{"xml_presentation_id":"YOUR_ID"}' \
--data "$(jq -n --arg content '<slide ...>...</slide>' --arg before 'TARGET_SLIDE_ID' \
'{slide:{content:$content}, before_slide_id:$before}')"
```
> 渐变色必须使用 `rgba()` 格式并带百分比停靠点,如 `linear-gradient(135deg,rgba(15,23,42,1) 0%,rgba(56,97,140,1) 100%)`。使用 `rgb()` 或省略停靠点会导致服务端回退为白色。
### 大纲模板
生成大纲时使用以下格式,交给用户确认:
```text
[PPT 标题] — [定位描述],面向 [目标受众]
模板:[未使用模板 / <category>/<template>.xml推荐原因]
页面结构N 页):
1. 封面页:[标题文案]
2. [页面主题][要点1]、[要点2]、[要点3]
3. [页面主题][要点描述]
...
N. 结尾页:[结尾文案]
风格:[配色方案][排版风格]
```
## Shortcuts 与 API
Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`)。有 Shortcut 的操作优先使用。
| Shortcut | 说明 |
|----------|------|
| [`+create`](lark-slides-create.md) | 创建 PPT可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
| [`+media-upload`](lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
| [`+replace-slide`](lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
| [`+replace-pages`](lark-slides-replace-pages.md) | 在原演示文稿内批量重建多个页面:先创建新页到旧页前,再删除旧页;适合已有 Slides 的多页大改,不新建链接 |
没有 Shortcut 覆盖时使用原生 API。高频资源`xml_presentations.get` 读取全文;`xml_presentation.slide.create/delete/get/replace` 管理单页。
```bash
lark-cli schema slides.<resource>.<method> # 调用 API 前必须先查看参数结构
lark-cli slides <resource> <method> [flags] # 调用 API
```
> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
## 核心规则
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide.create` 逐页添加
3. **`<slide>` 直接子元素只有 `<style>``<data>``<note>`**:文本和图形必须放在 `<data>`
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内
5. **保存关键 ID**:后续操作需要 `xml_presentation_id``slide_id``revision_id`
6. **删除谨慎**:删除操作不可逆,且至少保留一页幻灯片
7. **编辑已有页面优先原链接更新**:修改单个 shape/img 用 `+replace-slide``block_replace` / `block_insert`),不要整页重建;已有 Slides 的多页整页重建用 `+replace-pages`,不要用 `slides +create` 新建整份 PPT只有没有 shortcut 覆盖的特殊单页整页操作才手动 `slide.create` + `slide.delete`
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传或 `+create --slides``@./path` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**slides upload API 不支持分片上传)。
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。

View File

@@ -0,0 +1,432 @@
# PPTX 模板编辑工作流
本文用于用户给定 `.pptx` 模板或已有 `.pptx`,并要求基于它编辑、改写、续写、美化或生成新的 PPTX。流程对齐 Anthropic `skills/pptx/editing.md` 的实现思路:先分析模板,再规划页面映射,先完成结构调整,最后逐页编辑内容;除非用户明确不要导入为飞书 Slides否则最终默认导入并交付在线 Slides。
> **边界**:本流程的编辑阶段只使用 `skills/lark-slides/scripts/` 下的 PPTX / OOXML 脚本和本地 OOXML 编辑,不使用 `lark-cli slides`、`xml_presentations.*`、`slides_xml_schema_definition.xml`、`template_tool.py`、`iconpark_tool.py`、`xml_text_overlap_lint.py`、`+replace-slide`、`+replace-pages`、`+media-upload` 或任何飞书在线 Slides 组件。交付阶段按后文 [导入为飞书 Slides (Required)](#导入为飞书-slides-required) 执行。
> **Python 版本**:本流程中的 Python 脚本要求 Python 3.10+。执行前先确认 `python3 --version`;如果环境仍是 Python 3.9.x不要继续运行这些脚本先切换到 Python 3.10+。
## Template-Based Workflow
### 1. 分析模板
先生成模板缩略图,再提取可读文本。缩略图用于看版式,文本输出用于识别占位内容。
```bash
python3 skills/lark-slides/scripts/thumbnail.py \
"work/<task-id>/template.pptx" \
"work/<task-id>/template-thumbnails" \
--cols 4
python -m markitdown "work/<task-id>/template.pptx" \
> "work/<task-id>/template.md"
```
如果当前环境没有 `markitdown`,不要安装依赖;改为解包后直接阅读 `ppt/slides/slide*.xml` 中的文本占位符。
分析时记录:
- 每页的 `slideN.xml` 文件名、页序、页面用途和可复用程度。
- 封面、目录、章节页、内容页、图文页、数据页、引用页、结尾页等页型。
- 每页的占位文本、图片槽、图表槽、图标槽、页脚和备注。
- 哪些页面适合复制,哪些页面应删除。
### 2. 规划页面映射
为每个内容章节选择一个模板页面。不要默认使用标题 + bullets 的基础页。
优先主动寻找并复用不同版式:
- 2 栏 / 3 栏布局
- 图片 + 文本组合
- 全出血图片 + 文字覆盖
- 引用 / callout 页
- 章节分隔页
- 关键数字 / 指标页
- 图标网格或图标 + 文本行
- 表格、流程、时间线或图表页
避免每一页都重复同一种文本密集版式。内容类型要匹配页面风格:关键点可以用列表页,团队或模块信息适合多栏页,证言或观点适合引用页,指标适合大数字页。
### 3. 解包
```bash
python3 skills/lark-slides/scripts/office/unpack.py \
"work/<task-id>/template.pptx" \
"work/<task-id>/unpacked"
```
`office/unpack.py` 会解包 PPTX、格式化 XML / `.rels`,并处理智能引号转义。后续编辑都在 `unpacked/` 内完成。
如果中途使用 PowerPoint、LibreOffice、`python-pptx` 或其他外部工具直接修改了 packed `.pptx` 文件,旧的 `unpacked/` 目录就不再代表最新内容。后续若要回到本脚本工作流继续编辑,必须先把最新 `.pptx` 重新 unpack 到新的目录,或清空旧目录后重新 unpack不要继续基于过期的 `unpacked/` 打包。
### 4. 构建演示文稿结构
结构调整必须在内容编辑之前完成,并由主 agent 自己做,不要交给子 agent 并行处理。
- 删除不用的页面:从 `ppt/presentation.xml``<p:sldIdLst>` 中移除对应 `<p:sldId>`,然后运行 `clean.py` 清理孤立文件。
- 复制要复用的页面:用 `add_slide.py`,不要手动复制 slide 文件。
- 从版式创建页面:用 `add_slide.py unpacked/ slideLayoutN.xml`
- 调整页序:重排 `ppt/presentation.xml``<p:sldIdLst>``<p:sldId>` 顺序。
- 完成所有新增、删除、复制、重排后,再进入内容编辑阶段。
```bash
# 复制已有页面,例如基于 slide2.xml 创建新页
python3 skills/lark-slides/scripts/add_slide.py \
"work/<task-id>/unpacked" \
slide2.xml
# 或基于 slide layout 创建空白新页
python3 skills/lark-slides/scripts/add_slide.py \
"work/<task-id>/unpacked" \
slideLayout2.xml
```
`add_slide.py` 会补充 slide 文件、content type 和 presentation relationship并打印需要加入 `ppt/presentation.xml``<p:sldId .../>`。把这行插入到目标页序位置。
### 5. 编辑内容
结构稳定后,再逐页编辑 `ppt/slides/slideN.xml`。每页是独立 XML 文件,可以在这个阶段并行处理;如果使用子 agent提示必须包含
- 要编辑的 slide 文件路径。
- 使用 Edit 工具做所有改动。
- 本文的格式规则和常见坑。
每页处理顺序:
1. 读取该页 XML。
2. 找出所有占位内容文本、图片、图表、图标、caption、来源说明。
3. 将每个占位内容替换为最终内容。
4. 删除多余元素,而不是只清空文字。
5. 保留模板原有段落、run、对齐、字体、字号、颜色和间距结构。
> **重要**:内容替换优先使用 Edit 工具,不要用 `sed` 或临时 Python 脚本批量替换。Edit 工具会迫使修改点具体化,可靠性更高。
### 6. 清理
```bash
python3 skills/lark-slides/scripts/clean.py \
"work/<task-id>/unpacked"
```
`clean.py` 会移除不在 `<p:sldIdLst>` 中的页面、未引用媒体、孤立 `.rels`、未引用图表 / diagram / drawing / theme / notes并更新 `[Content_Types].xml`
### 7. 打包
```bash
python3 skills/lark-slides/scripts/office/pack.py \
"work/<task-id>/unpacked" \
"work/<task-id>/output.pptx" \
--original "work/<task-id>/template.pptx"
```
`office/pack.py` 会先做 PPTX 校验,再压缩 XML 并打包。若只需要重写 ZIP 压缩,不改变内容,可用:
```bash
python3 skills/lark-slides/scripts/rezip.py \
"work/<task-id>/output.pptx" \
"work/<task-id>/output.rezipped.pptx"
```
### 8. 视觉 QA
必须按后文 [QA (Required)](#qa-required) 完成内容 QA、图片渲染、视觉检查和修复复验。`thumbnail.py` 可用于快速页序检查;最终视觉 QA 优先使用 [Converting to Images](#converting-to-images) 生成的全分辨率页面图。
```bash
python3 skills/lark-slides/scripts/thumbnail.py \
"work/<task-id>/output.pptx" \
"work/<task-id>/preview/thumbnails" \
--cols 4
```
## QA (Required)
默认假设第一版一定有问题。QA 的目标不是确认成功,而是主动找出缺陷并修到没有新问题。
### Content QA
用文本抽取检查内容是否缺失、顺序是否错误、是否还有模板占位文案。
```bash
python -m markitdown "work/<task-id>/output.pptx" \
> "work/<task-id>/qa/content.md"
```
模板编辑必须检查残留占位词。命中任何结果都要先修复,再进入最终交付。
```bash
python -m markitdown "work/<task-id>/output.pptx" \
| grep -iE "xxxx|lorem|ipsum|this.*(page|slide).*layout|placeholder|占位|示例|请替换"
```
检查重点:
- 内容是否完整,没有漏掉用户材料里的章节、数字、结论或来源。
- 页序和章节顺序是否正确。
- 标题、图表标签、脚注、页脚、引用和备注是否仍匹配当前内容。
- 是否残留模板说明文字、占位头像、空卡片、默认图表数据或示例 logo。
### Visual QA
把 PPTX 转成逐页图片后检查。即使只有 2-3 页,也建议让另一个 agent / reviewer 用新视角看图;自己盯着 XML 太久,很容易只看到预期结果。
检查时使用类似提示:
```text
请视觉检查这些幻灯片。默认其中存在问题,请主动找出它们。
重点检查:
- 元素重叠:文字穿过形状、线条穿过文字、多个元素堆叠。
- 文本溢出或在文本框 / 页面边缘被裁切。
- 装饰线、分割线或背景块只适配单行标题,但标题实际换成两行。
- 来源、脚注或页码与正文内容碰撞。
- 元素距离太近,卡片、栏目或段落之间小于 0.3 英寸。
- 外边距不足,内容距离页面边缘小于 0.5 英寸。
- 同类元素未对齐,栏宽、卡片高度、图标位置不一致。
- 低对比度文字或图标。
- 文本框过窄导致异常换行。
- 残留占位内容、空头像框、空图标底座、默认图表数据。
逐页列出所有问题或可疑区域,即使只是轻微问题。
```
给 reviewer / subagent 的输入应包含每张图的路径和预期内容:
```text
Read and analyze these images:
1. /abs/path/slide-01.jpg (Expected: 封面,包含标题、副标题、主视觉)
2. /abs/path/slide-02.jpg (Expected: 三栏方案对比,包含 A/B/C 三个模块)
Report all issues found, including minor ones.
```
### Verification Loop
1. 生成 PPTX。
2. 转成图片。
3. 检查并列出问题;如果第一轮没有发现任何问题,要更严格地重新看一遍。
4. 修复问题。
5. 重新打包并只复验受影响页面。
6. 重复直到一整轮检查没有新增问题。
不要在至少完成一次“发现问题 → 修复 → 复验”的闭环前宣称完成。
## Converting to Images
将 PPTX 转为逐页图片,用于视觉检查。
```bash
mkdir -p "work/<task-id>/preview"
python3 skills/lark-slides/scripts/office/soffice.py \
--headless \
--convert-to pdf \
--outdir "work/<task-id>/preview" \
"work/<task-id>/output.pptx"
pdftoppm \
-jpeg \
-r 150 \
"work/<task-id>/preview/output.pdf" \
"work/<task-id>/preview/slide"
```
这会生成 `slide-1.jpg``slide-2.jpg` 等文件。给 reviewer 时可按实际文件名列出;若需要稳定的两位编号,可在本地重命名为 `slide-01.jpg``slide-02.jpg`
修复后只重渲染某几页:
```bash
pdftoppm \
-jpeg \
-r 150 \
-f N \
-l N \
"work/<task-id>/preview/output.pdf" \
"work/<task-id>/preview/slide-fixed"
```
也可以用 `thumbnail.py` 生成总览图,辅助检查页序、隐藏页和整体节奏:
```bash
python3 skills/lark-slides/scripts/thumbnail.py \
"work/<task-id>/output.pptx" \
"work/<task-id>/preview/thumbnails" \
--cols 4
```
如果 LibreOffice / `soffice` 在本机崩溃或无法生成 PDF可用系统预览能力做临时降级检查但最终交付前仍应优先使用成功渲染出的逐页图片、在线截图或人工打开 PPTX 复验。
```bash
qlmanage -t -s 1200 -o "work/<task-id>/preview" "work/<task-id>/output.pptx"
```
## 导入为飞书 Slides (Required)
本流程的编辑阶段只处理本地 PPTX。除非用户明确说明“不导入为飞书 Slides”或“只要本地 PPTX”否则先完成本地 QA再切到 `lark-drive` 导入,并把在线 Slides 作为默认交付物。
```bash
lark-cli drive +import --as user \
--file "work/<task-id>/output.pptx" \
--type slides \
--name "<title>" \
--json
```
导入成功后保存返回的 `url` / `token`,最终回复中必须交付 Slides 链接。需要在线视觉复验时,用 `slides +screenshot` 指定页码;多页截图优先在一次命令里重复传 `--slide-number`,避免紧密循环触发频控。
```bash
lark-cli slides +screenshot --as user \
--presentation "<slides_url_or_token>" \
--slide-number 1 \
--slide-number 2 \
--output-dir "work/<task-id>/online-screenshots" \
--json
```
## Dependencies
本流程依赖以下工具;缺失时先说明不能完成对应验证,不要静默跳过。
- Python 3.10+`skills/lark-slides/scripts/` 下的脚本使用 Python 3.10+ 语法,不支持 Python 3.9.x。
- `markitdown[pptx]`:文本抽取和占位内容检查。
- `Pillow``thumbnail.py` 生成缩略图网格。
- LibreOffice (`soffice`)PPTX 转 PDF本仓通过 `skills/lark-slides/scripts/office/soffice.py` 包装调用环境。
- Poppler (`pdftoppm`)PDF 转逐页图片。
- `defusedxml`OOXML 脚本安全解析 XML。
不把 `pptxgenjs` 作为本模板编辑流程的必需依赖;它属于从零创建 PPTX 的路线,不是本文件的默认工作流。
## Scripts
| Script | Purpose |
|--------|---------|
| `office/unpack.py` | 解包并 pretty-print PPTX XML / `.rels` |
| `add_slide.py` | 复制 slide 或从 slideLayout 创建 slide |
| `clean.py` | 删除孤立页面、媒体、关系和 content type |
| `office/pack.py` | 校验并重新打包 PPTX |
| `office/validate.py` | 单独校验解包目录或 PPTX 文件 |
| `thumbnail.py` | 生成带 slide 文件名标签的缩略图网格 |
| `rezip.py` | 仅重写 ZIP 压缩,不改内容 |
### office/unpack.py
```bash
python3 skills/lark-slides/scripts/office/unpack.py input.pptx unpacked/
```
解包 PPTX格式化 XML转义智能引号。
### add_slide.py
```bash
python3 skills/lark-slides/scripts/add_slide.py unpacked/ slide2.xml
python3 skills/lark-slides/scripts/add_slide.py unpacked/ slideLayout2.xml
```
第一种复制已有页面,第二种从 layout 创建页面。脚本会打印需要插入 `ppt/presentation.xml``<p:sldId>`
### clean.py
```bash
python3 skills/lark-slides/scripts/clean.py unpacked/
```
清理不再被 presentation 或 slide relationships 引用的文件。
### office/pack.py
```bash
python3 skills/lark-slides/scripts/office/pack.py \
unpacked/ output.pptx --original input.pptx
```
校验、修复可自动修复的问题、压缩 XML并重新打包 PPTX。
### thumbnail.py
```bash
python3 skills/lark-slides/scripts/thumbnail.py input.pptx [output_prefix] [--cols N]
```
生成缩略图网格,图片下方标注 `slideN.xml`。默认 3 列,`--cols` 最大值由脚本限制。
## Slide Operations
页面顺序由 `ppt/presentation.xml` 里的 `<p:sldIdLst>` 决定。
- **Reorder**:重排 `<p:sldId>` 元素。
- **Delete**:删除 `<p:sldId>`,再运行 `clean.py`
- **Add**:使用 `add_slide.py`。不要手动复制 slide 文件;脚本会处理备注关系、`[Content_Types].xml` 和 relationship ID。
## Formatting Rules
- **标题、分组标题和行内标签要加粗**:在对应 run 的 `<a:rPr>` 上使用 `b="1"`。例如页面标题、section header以及 `Status:``Description:` 这类行首标签。
- **不要使用 Unicode bullet `•`**:使用 PPTX 原生列表格式,例如 `<a:buChar>``<a:buAutoNum>`
- **列表样式保持一致**:尽量继承模板 layout 的 bullet 设置;只有模板缺失或需要显式修正时才新增 bullet 属性。
- **多条内容不要拼进一个字符串**:编号列表、多步骤、多段说明应拆成多个 `<a:p>`;复制原段落的 `<a:pPr>` 以保留行距和缩进。
- **长文本要适配模板槽位**:更长的替换文本可能溢出或异常换行;优先压缩文案、拆页或换用更合适的模板页。
- **保留空白时加 `xml:space="preserve"`**:对有前后空格的 `<a:t>` 使用 `xml:space="preserve"`
- **XML 解析用 `defusedxml.minidom`**:不要用 `xml.etree.ElementTree` 重写 OOXML它容易破坏命名空间和格式。
## Common Pitfalls
### Template Adaptation
当源内容项少于模板槽位时删除多余的整组元素包括图片、shape、文本框和图标不要只清空文字。清空文字但保留视觉元素会留下无意义的头像框、图标底座或空卡片。
模板槽位不等于源内容项。例如模板有 4 个成员卡片而源内容只有 3 人,应删除第 4 个成员的整组元素,而不是只删姓名。
### Multi-Item Content
多步骤、多条结论、多段说明应拆成多个段落。
错误做法:所有项目都塞进同一个 `<a:t>`
```xml
<a:t>Step 1: Do the first thing. Step 2: Do the second thing.</a:t>
```
正确做法:每个项目使用独立 `<a:p>`,并对 header run 加粗。
```xml
<a:p>
<a:r><a:rPr b="1"/><a:t>Step 1</a:t></a:r>
<a:r><a:t> Do the first thing.</a:t></a:r>
</a:p>
<a:p>
<a:r><a:rPr b="1"/><a:t>Step 2</a:t></a:r>
<a:r><a:t> Do the second thing.</a:t></a:r>
</a:p>
```
### Smart Quotes
`office/unpack.py` / `office/pack.py` 会处理智能引号。手动新增带引号的文本时,优先写 XML entity。
| Character | XML entity |
|-----------|------------|
| `“` | `&#x201C;` |
| `”` | `&#x201D;` |
| `` | `&#x2018;` |
| `` | `&#x2019;` |
### Visual QA
修改内容后必须看渲染结果。尤其检查:
- 模板页型是否单调重复。
- 长文本是否溢出、遮挡或异常换行。
- 图片和图标是否缺失、变形或裁剪错误。
- 删除内容后是否留下孤立形状。
- 图表标签、图例、数据系列和来源说明是否仍然匹配。
## 交付格式
最终回复用户时说明:
- 输出 PPTX 的绝对路径。
- 导入后的飞书 Slides 链接;如果用户明确不要导入,说明本次按用户要求未导入。
- 源文件是否保持未修改。
- 复用了哪些模板页型。
- 做过哪些关键结构调整和内容替换。
- 是否完成缩略图或全分辨率视觉 QA如果未做说明具体原因。

View File

@@ -237,4 +237,4 @@ lark-cli slides +replace-slide --as user \
- [xml_presentation.slide get](lark-slides-xml-presentation-slide-get.md) — 读原页拿 `block_id` / `revision_id`
- [xml_presentation.slide replace](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考
- [+media-upload](lark-slides-media-upload.md) — 上传图片拿 `file_token`
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环 + 决策树
- [lark-slides-edit-workflows.md](lark-slides-replace-workflows) — 读-改-写闭环 + 决策树

View File

@@ -107,4 +107,4 @@ lark-cli slides xml_presentation.slide get --as user --params '{
- [slides +replace-slide](lark-slides-replace-slide.md) — 块级替换 shortcut推荐
- [xml_presentation.slide replace](lark-slides-xml-presentation-slide-replace.md) — 底层 replace API 参考
- [xml_presentations get](lark-slides-xml-presentations-get.md) — 读整个 PPT
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环
- [lark-slides-edit-workflows.md](lark-slides-replace-workflows) — 读-改-写闭环

View File

@@ -184,4 +184,4 @@ lark-cli slides xml_presentation.slide replace --as user --params '{
- [slides +replace-slide](lark-slides-replace-slide.md) — 块级替换 shortcut推荐自动注入 id
- [xml_presentation.slide get](lark-slides-xml-presentation-slide-get.md) — 读原页拿 block short ID
- [slides +media-upload](lark-slides-media-upload.md) — 上传图片拿 file_token
- [lark-slides-edit-workflows.md](lark-slides-edit-workflows.md) — 读-改-写闭环 + 决策树
- [lark-slides-edit-workflows.md](lark-slides-replace-workflows) — 读-改-写闭环 + 决策树

View File

@@ -0,0 +1,195 @@
"""Add a new slide to an unpacked PPTX directory.
Usage: python add_slide.py <unpacked_dir> <source>
The source can be:
- A slide file (e.g., slide2.xml) - duplicates the slide
- A layout file (e.g., slideLayout2.xml) - creates from layout
Examples:
python add_slide.py unpacked/ slide2.xml
# Duplicates slide2, creates slide5.xml
python add_slide.py unpacked/ slideLayout2.xml
# Creates slide5.xml from slideLayout2.xml
To see available layouts: ls unpacked/ppt/slideLayouts/
Prints the <p:sldId> element to add to presentation.xml.
"""
import re
import shutil
import sys
from pathlib import Path
def get_next_slide_number(slides_dir: Path) -> int:
existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml")
if (m := re.match(r"slide(\d+)\.xml", f.name))]
return max(existing) + 1 if existing else 1
def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None:
slides_dir = unpacked_dir / "ppt" / "slides"
rels_dir = slides_dir / "_rels"
layouts_dir = unpacked_dir / "ppt" / "slideLayouts"
layout_path = layouts_dir / layout_file
if not layout_path.exists():
print(f"Error: {layout_path} not found", file=sys.stderr)
sys.exit(1)
next_num = get_next_slide_number(slides_dir)
dest = f"slide{next_num}.xml"
dest_slide = slides_dir / dest
dest_rels = rels_dir / f"{dest}.rels"
slide_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="0" cy="0"/>
</a:xfrm>
</p:grpSpPr>
</p:spTree>
</p:cSld>
<p:clrMapOvr>
<a:masterClrMapping/>
</p:clrMapOvr>
</p:sld>'''
dest_slide.write_text(slide_xml, encoding="utf-8")
rels_dir.mkdir(exist_ok=True)
rels_xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/{layout_file}"/>
</Relationships>'''
dest_rels.write_text(rels_xml, encoding="utf-8")
_add_to_content_types(unpacked_dir, dest)
rid = _add_to_presentation_rels(unpacked_dir, dest)
next_slide_id = _get_next_slide_id(unpacked_dir)
print(f"Created {dest} from {layout_file}")
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
def duplicate_slide(unpacked_dir: Path, source: str) -> None:
slides_dir = unpacked_dir / "ppt" / "slides"
rels_dir = slides_dir / "_rels"
source_slide = slides_dir / source
if not source_slide.exists():
print(f"Error: {source_slide} not found", file=sys.stderr)
sys.exit(1)
next_num = get_next_slide_number(slides_dir)
dest = f"slide{next_num}.xml"
dest_slide = slides_dir / dest
source_rels = rels_dir / f"{source}.rels"
dest_rels = rels_dir / f"{dest}.rels"
shutil.copy2(source_slide, dest_slide)
if source_rels.exists():
shutil.copy2(source_rels, dest_rels)
rels_content = dest_rels.read_text(encoding="utf-8")
rels_content = re.sub(
r'\s*<Relationship[^>]*Type="[^"]*notesSlide"[^>]*/>\s*',
"\n",
rels_content,
)
dest_rels.write_text(rels_content, encoding="utf-8")
_add_to_content_types(unpacked_dir, dest)
rid = _add_to_presentation_rels(unpacked_dir, dest)
next_slide_id = _get_next_slide_id(unpacked_dir)
print(f"Created {dest} from {source}")
print(f'Add to presentation.xml <p:sldIdLst>: <p:sldId id="{next_slide_id}" r:id="{rid}"/>')
def _add_to_content_types(unpacked_dir: Path, dest: str) -> None:
content_types_path = unpacked_dir / "[Content_Types].xml"
content_types = content_types_path.read_text(encoding="utf-8")
new_override = f'<Override PartName="/ppt/slides/{dest}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
if f"/ppt/slides/{dest}" not in content_types:
content_types = content_types.replace("</Types>", f" {new_override}\n</Types>")
content_types_path.write_text(content_types, encoding="utf-8")
def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str:
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
pres_rels = pres_rels_path.read_text(encoding="utf-8")
rids = [int(m) for m in re.findall(r'Id="rId(\d+)"', pres_rels)]
next_rid = max(rids) + 1 if rids else 1
rid = f"rId{next_rid}"
new_rel = f'<Relationship Id="{rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/{dest}"/>'
if f"slides/{dest}" not in pres_rels:
pres_rels = pres_rels.replace("</Relationships>", f" {new_rel}\n</Relationships>")
pres_rels_path.write_text(pres_rels, encoding="utf-8")
return rid
def _get_next_slide_id(unpacked_dir: Path) -> int:
pres_path = unpacked_dir / "ppt" / "presentation.xml"
pres_content = pres_path.read_text(encoding="utf-8")
slide_ids = [int(m) for m in re.findall(r'<p:sldId[^>]*id="(\d+)"', pres_content)]
return max(slide_ids) + 1 if slide_ids else 256
def parse_source(source: str) -> tuple[str, str | None]:
if source.startswith("slideLayout") and source.endswith(".xml"):
return ("layout", source)
return ("slide", None)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python add_slide.py <unpacked_dir> <source>", file=sys.stderr)
print("", file=sys.stderr)
print("Source can be:", file=sys.stderr)
print(" slide2.xml - duplicate an existing slide", file=sys.stderr)
print(" slideLayout2.xml - create from a layout template", file=sys.stderr)
print("", file=sys.stderr)
print("To see available layouts: ls <unpacked_dir>/ppt/slideLayouts/", file=sys.stderr)
sys.exit(1)
unpacked_dir = Path(sys.argv[1])
source = sys.argv[2]
if not unpacked_dir.exists():
print(f"Error: {unpacked_dir} not found", file=sys.stderr)
sys.exit(1)
source_type, layout_file = parse_source(source)
if source_type == "layout" and layout_file is not None:
create_slide_from_layout(unpacked_dir, layout_file)
else:
duplicate_slide(unpacked_dir, source)

View File

@@ -0,0 +1,323 @@
"""Remove unreferenced files from an unpacked PPTX directory.
Usage: python clean.py <unpacked_dir>
Example:
python clean.py unpacked/
This script removes:
- Orphaned slides (not in sldIdLst) and their relationships
- [trash] directory (unreferenced files)
- Orphaned .rels files for deleted resources
- Unreferenced media, embeddings, charts, diagrams, drawings, ink files
- Unreferenced theme files
- Unreferenced notes slides
- Content-Type overrides for deleted files
"""
import sys
from pathlib import Path
import defusedxml.minidom
PRESENTATIONML_NAMESPACE = "http://schemas.openxmlformats.org/presentationml/2006/main"
OFFICE_RELATIONSHIPS_NAMESPACE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
)
def _iter_elements_by_local_name(dom, local_name: str):
return [
node
for node in dom.getElementsByTagName("*")
if node.localName == local_name or node.tagName.split(":")[-1] == local_name
]
def _get_relationship_id(element) -> str:
rid = element.getAttributeNS(OFFICE_RELATIONSHIPS_NAMESPACE, "id")
if rid:
return rid
return element.getAttribute("r:id")
def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]:
pres_path = unpacked_dir / "ppt" / "presentation.xml"
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
if not pres_path.exists() or not pres_rels_path.exists():
return set()
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
rid_to_slide = {}
for rel in _iter_elements_by_local_name(rels_dom, "Relationship"):
rid = rel.getAttribute("Id")
target = rel.getAttribute("Target")
rel_type = rel.getAttribute("Type")
if "slide" in rel_type and target.startswith("slides/"):
rid_to_slide[rid] = target.replace("slides/", "")
pres_dom = defusedxml.minidom.parse(str(pres_path))
slide_nodes = [
node
for node in _iter_elements_by_local_name(pres_dom, "sldId")
if not node.namespaceURI or node.namespaceURI == PRESENTATIONML_NAMESPACE
]
referenced_rids = {_get_relationship_id(node) for node in slide_nodes}
referenced_rids.discard("")
referenced_slides = {
rid_to_slide[rid] for rid in referenced_rids if rid in rid_to_slide
}
if slide_nodes and not referenced_slides:
raise ValueError(
"No referenced slides could be resolved from ppt/presentation.xml; "
"refusing to clean slides to avoid deleting valid slide files"
)
return referenced_slides
def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
slides_dir = unpacked_dir / "ppt" / "slides"
slides_rels_dir = slides_dir / "_rels"
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
if not slides_dir.exists():
return []
referenced_slides = get_slides_in_sldidlst(unpacked_dir)
removed = []
for slide_file in slides_dir.glob("slide*.xml"):
if slide_file.name not in referenced_slides:
rel_path = slide_file.relative_to(unpacked_dir)
slide_file.unlink()
removed.append(str(rel_path))
rels_file = slides_rels_dir / f"{slide_file.name}.rels"
if rels_file.exists():
rels_file.unlink()
removed.append(str(rels_file.relative_to(unpacked_dir)))
if removed and pres_rels_path.exists():
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
changed = False
for rel in list(_iter_elements_by_local_name(rels_dom, "Relationship")):
target = rel.getAttribute("Target")
if target.startswith("slides/"):
slide_name = target.replace("slides/", "")
if slide_name not in referenced_slides:
if rel.parentNode:
rel.parentNode.removeChild(rel)
changed = True
if changed:
with open(pres_rels_path, "wb") as f:
f.write(rels_dom.toxml(encoding="utf-8"))
return removed
def remove_trash_directory(unpacked_dir: Path) -> list[str]:
trash_dir = unpacked_dir / "[trash]"
removed = []
if trash_dir.exists() and trash_dir.is_dir():
for file_path in trash_dir.iterdir():
if file_path.is_file():
rel_path = file_path.relative_to(unpacked_dir)
removed.append(str(rel_path))
file_path.unlink()
trash_dir.rmdir()
return removed
def get_slide_referenced_files(unpacked_dir: Path) -> set:
referenced = set()
slides_rels_dir = unpacked_dir / "ppt" / "slides" / "_rels"
if not slides_rels_dir.exists():
return referenced
for rels_file in slides_rels_dir.glob("*.rels"):
dom = defusedxml.minidom.parse(str(rels_file))
for rel in _iter_elements_by_local_name(dom, "Relationship"):
target = rel.getAttribute("Target")
if not target:
continue
target_path = (rels_file.parent.parent / target).resolve()
try:
referenced.add(target_path.relative_to(unpacked_dir.resolve()))
except ValueError:
pass
return referenced
def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]:
resource_dirs = ["charts", "diagrams", "drawings"]
removed = []
slide_referenced = get_slide_referenced_files(unpacked_dir)
for dir_name in resource_dirs:
rels_dir = unpacked_dir / "ppt" / dir_name / "_rels"
if not rels_dir.exists():
continue
for rels_file in rels_dir.glob("*.rels"):
resource_file = rels_dir.parent / rels_file.name.replace(".rels", "")
try:
resource_rel_path = resource_file.resolve().relative_to(unpacked_dir.resolve())
except ValueError:
continue
if not resource_file.exists() or resource_rel_path not in slide_referenced:
rels_file.unlink()
rel_path = rels_file.relative_to(unpacked_dir)
removed.append(str(rel_path))
return removed
def get_referenced_files(unpacked_dir: Path) -> set:
referenced = set()
for rels_file in unpacked_dir.rglob("*.rels"):
dom = defusedxml.minidom.parse(str(rels_file))
for rel in _iter_elements_by_local_name(dom, "Relationship"):
target = rel.getAttribute("Target")
if not target:
continue
target_path = (rels_file.parent.parent / target).resolve()
try:
referenced.add(target_path.relative_to(unpacked_dir.resolve()))
except ValueError:
pass
return referenced
def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"]
removed = []
for dir_name in resource_dirs:
dir_path = unpacked_dir / "ppt" / dir_name
if not dir_path.exists():
continue
for file_path in dir_path.glob("*"):
if not file_path.is_file():
continue
rel_path = file_path.relative_to(unpacked_dir)
if rel_path not in referenced:
file_path.unlink()
removed.append(str(rel_path))
theme_dir = unpacked_dir / "ppt" / "theme"
if theme_dir.exists():
for file_path in theme_dir.glob("theme*.xml"):
rel_path = file_path.relative_to(unpacked_dir)
if rel_path not in referenced:
file_path.unlink()
removed.append(str(rel_path))
theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels"
if theme_rels.exists():
theme_rels.unlink()
removed.append(str(theme_rels.relative_to(unpacked_dir)))
notes_dir = unpacked_dir / "ppt" / "notesSlides"
if notes_dir.exists():
for file_path in notes_dir.glob("*.xml"):
if not file_path.is_file():
continue
rel_path = file_path.relative_to(unpacked_dir)
if rel_path not in referenced:
file_path.unlink()
removed.append(str(rel_path))
notes_rels_dir = notes_dir / "_rels"
if notes_rels_dir.exists():
for file_path in notes_rels_dir.glob("*.rels"):
notes_file = notes_dir / file_path.name.replace(".rels", "")
if not notes_file.exists():
file_path.unlink()
removed.append(str(file_path.relative_to(unpacked_dir)))
return removed
def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None:
ct_path = unpacked_dir / "[Content_Types].xml"
if not ct_path.exists():
return
dom = defusedxml.minidom.parse(str(ct_path))
changed = False
for override in list(_iter_elements_by_local_name(dom, "Override")):
part_name = override.getAttribute("PartName").lstrip("/")
if part_name in removed_files:
if override.parentNode:
override.parentNode.removeChild(override)
changed = True
if changed:
with open(ct_path, "wb") as f:
f.write(dom.toxml(encoding="utf-8"))
def clean_unused_files(unpacked_dir: Path) -> list[str]:
all_removed = []
slides_removed = remove_orphaned_slides(unpacked_dir)
all_removed.extend(slides_removed)
trash_removed = remove_trash_directory(unpacked_dir)
all_removed.extend(trash_removed)
while True:
removed_rels = remove_orphaned_rels_files(unpacked_dir)
referenced = get_referenced_files(unpacked_dir)
removed_files = remove_orphaned_files(unpacked_dir, referenced)
total_removed = removed_rels + removed_files
if not total_removed:
break
all_removed.extend(total_removed)
if all_removed:
update_content_types(unpacked_dir, all_removed)
return all_removed
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python clean.py <unpacked_dir>", file=sys.stderr)
print("Example: python clean.py unpacked/", file=sys.stderr)
sys.exit(1)
unpacked_dir = Path(sys.argv[1])
if not unpacked_dir.exists():
print(f"Error: {unpacked_dir} not found", file=sys.stderr)
sys.exit(1)
try:
removed = clean_unused_files(unpacked_dir)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if removed:
print(f"Removed {len(removed)} unreferenced files:")
for f in removed:
print(f" {f}")
else:
print("No unreferenced files found")

View File

@@ -0,0 +1,91 @@
import tempfile
import unittest
from pathlib import Path
from clean import clean_unused_files
PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"
REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
class CleanTest(unittest.TestCase):
def test_clean_preserves_referenced_slide_with_rewritten_prefixes(self):
with tempfile.TemporaryDirectory() as temp_dir:
unpacked = Path(temp_dir)
self._write_minimal_pptx_unpacked(unpacked, include_slide_rel=True)
removed = clean_unused_files(unpacked)
self.assertTrue((unpacked / "ppt/slides/slide1.xml").exists())
self.assertFalse((unpacked / "ppt/slides/slide2.xml").exists())
self.assertIn("ppt/slides/slide2.xml", removed)
def test_clean_refuses_to_delete_when_slide_reference_cannot_be_resolved(self):
with tempfile.TemporaryDirectory() as temp_dir:
unpacked = Path(temp_dir)
self._write_minimal_pptx_unpacked(unpacked, include_slide_rel=False)
with self.assertRaises(ValueError):
clean_unused_files(unpacked)
self.assertTrue((unpacked / "ppt/slides/slide1.xml").exists())
self.assertTrue((unpacked / "ppt/slides/slide2.xml").exists())
def _write_minimal_pptx_unpacked(
self, unpacked: Path, include_slide_rel: bool
) -> None:
(unpacked / "ppt/_rels").mkdir(parents=True)
(unpacked / "ppt/slides").mkdir(parents=True)
(unpacked / "ppt/presentation.xml").write_text(
f"""<?xml version="1.0" encoding="UTF-8"?>
<ns0:presentation xmlns:ns0="{PRESENTATION_NS}" xmlns:ns1="{REL_NS}">
<ns0:sldIdLst>
<ns0:sldId id="256" ns1:id="rId1"/>
</ns0:sldIdLst>
</ns0:presentation>
""",
encoding="utf-8",
)
rel_entries = []
if include_slide_rel:
rel_entries.append(
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" '
'Target="slides/slide1.xml"/>'
)
rel_entries.append(
'<Relationship Id="rId2" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" '
'Target="slides/slide2.xml"/>'
)
(unpacked / "ppt/_rels/presentation.xml.rels").write_text(
f"""<?xml version="1.0" encoding="UTF-8"?>
<ns2:Relationships xmlns:ns2="{PACKAGE_REL_NS}">
{' '.join(entry.replace("<Relationship", "<ns2:Relationship") for entry in rel_entries)}
</ns2:Relationships>
""",
encoding="utf-8",
)
for slide_name in ["slide1.xml", "slide2.xml"]:
(unpacked / "ppt/slides" / slide_name).write_text(
"<p:sld/>", encoding="utf-8"
)
(unpacked / "[Content_Types].xml").write_text(
"""<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
<Override PartName="/ppt/slides/slide2.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>
""",
encoding="utf-8",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,8 @@
OOXML_FAMILY = {
".docx": "docx",
".dotx": "docx",
".pptx": "pptx",
".potx": "pptx",
".xlsx": "xlsx",
".xltx": "xlsx",
}

View File

@@ -0,0 +1,199 @@
"""Merge adjacent runs with identical formatting in DOCX.
Merges adjacent <w:r> elements that have identical <w:rPr> properties.
Works on runs in paragraphs and inside tracked changes (<w:ins>, <w:del>).
Also:
- Removes rsid attributes from runs (revision metadata that doesn't affect rendering)
- Removes proofErr elements (spell/grammar markers that block merging)
"""
from pathlib import Path
import defusedxml.minidom
def merge_runs(input_dir: str) -> tuple[int, str]:
doc_xml = Path(input_dir) / "word" / "document.xml"
if not doc_xml.exists():
return 0, f"Error: {doc_xml} not found"
try:
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
root = dom.documentElement
_remove_elements(root, "proofErr")
_strip_run_rsid_attrs(root)
containers = {run.parentNode for run in _find_elements(root, "r")}
merge_count = 0
for container in containers:
merge_count += _merge_runs_in(container)
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
return merge_count, f"Merged {merge_count} runs"
except Exception as e:
return 0, f"Error: {e}"
def _find_elements(root, tag: str) -> list:
results = []
def traverse(node):
if node.nodeType == node.ELEMENT_NODE:
name = node.localName or node.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(node)
for child in node.childNodes:
traverse(child)
traverse(root)
return results
def _get_child(parent, tag: str):
for child in parent.childNodes:
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name == tag or name.endswith(f":{tag}"):
return child
return None
def _get_children(parent, tag: str) -> list:
results = []
for child in parent.childNodes:
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(child)
return results
def _is_adjacent(elem1, elem2) -> bool:
node = elem1.nextSibling
while node:
if node == elem2:
return True
if node.nodeType == node.ELEMENT_NODE:
return False
if node.nodeType == node.TEXT_NODE and node.data.strip():
return False
node = node.nextSibling
return False
def _remove_elements(root, tag: str):
for elem in _find_elements(root, tag):
if elem.parentNode:
elem.parentNode.removeChild(elem)
def _strip_run_rsid_attrs(root):
for run in _find_elements(root, "r"):
for attr in list(run.attributes.values()):
if "rsid" in attr.name.lower():
run.removeAttribute(attr.name)
def _merge_runs_in(container) -> int:
merge_count = 0
run = _first_child_run(container)
while run:
while True:
next_elem = _next_element_sibling(run)
if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
_merge_run_content(run, next_elem)
container.removeChild(next_elem)
merge_count += 1
else:
break
_consolidate_text(run)
run = _next_sibling_run(run)
return merge_count
def _first_child_run(container):
for child in container.childNodes:
if child.nodeType == child.ELEMENT_NODE and _is_run(child):
return child
return None
def _next_element_sibling(node):
sibling = node.nextSibling
while sibling:
if sibling.nodeType == sibling.ELEMENT_NODE:
return sibling
sibling = sibling.nextSibling
return None
def _next_sibling_run(node):
sibling = node.nextSibling
while sibling:
if sibling.nodeType == sibling.ELEMENT_NODE:
if _is_run(sibling):
return sibling
sibling = sibling.nextSibling
return None
def _is_run(node) -> bool:
name = node.localName or node.tagName
return name == "r" or name.endswith(":r")
def _can_merge(run1, run2) -> bool:
rpr1 = _get_child(run1, "rPr")
rpr2 = _get_child(run2, "rPr")
if (rpr1 is None) != (rpr2 is None):
return False
if rpr1 is None:
return True
return rpr1.toxml() == rpr2.toxml()
def _merge_run_content(target, source):
for child in list(source.childNodes):
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name != "rPr" and not name.endswith(":rPr"):
target.appendChild(child)
def _consolidate_text(run):
t_elements = _get_children(run, "t")
for i in range(len(t_elements) - 1, 0, -1):
curr, prev = t_elements[i], t_elements[i - 1]
if _is_adjacent(prev, curr):
prev_text = prev.firstChild.data if prev.firstChild else ""
curr_text = curr.firstChild.data if curr.firstChild else ""
merged = prev_text + curr_text
if prev.firstChild:
prev.firstChild.data = merged
else:
prev.appendChild(run.ownerDocument.createTextNode(merged))
if merged.startswith(" ") or merged.endswith(" "):
prev.setAttribute("xml:space", "preserve")
elif prev.hasAttribute("xml:space"):
prev.removeAttribute("xml:space")
run.removeChild(curr)

View File

@@ -0,0 +1,197 @@
"""Simplify tracked changes by merging adjacent w:ins or w:del elements.
Merges adjacent <w:ins> elements from the same author into a single element.
Same for <w:del> elements. This makes heavily-redlined documents easier to
work with by reducing the number of tracked change wrappers.
Rules:
- Only merges w:ins with w:ins, w:del with w:del (same element type)
- Only merges if same author (ignores timestamp differences)
- Only merges if truly adjacent (only whitespace between them)
"""
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
import defusedxml.minidom
WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
def simplify_redlines(input_dir: str) -> tuple[int, str]:
doc_xml = Path(input_dir) / "word" / "document.xml"
if not doc_xml.exists():
return 0, f"Error: {doc_xml} not found"
try:
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
root = dom.documentElement
merge_count = 0
containers = _find_elements(root, "p") + _find_elements(root, "tc")
for container in containers:
merge_count += _merge_tracked_changes_in(container, "ins")
merge_count += _merge_tracked_changes_in(container, "del")
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
return merge_count, f"Simplified {merge_count} tracked changes"
except Exception as e:
return 0, f"Error: {e}"
def _merge_tracked_changes_in(container, tag: str) -> int:
merge_count = 0
tracked = [
child
for child in container.childNodes
if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
]
if len(tracked) < 2:
return 0
i = 0
while i < len(tracked) - 1:
curr = tracked[i]
next_elem = tracked[i + 1]
if _can_merge_tracked(curr, next_elem):
_merge_tracked_content(curr, next_elem)
container.removeChild(next_elem)
tracked.pop(i + 1)
merge_count += 1
else:
i += 1
return merge_count
def _is_element(node, tag: str) -> bool:
name = node.localName or node.tagName
return name == tag or name.endswith(f":{tag}")
def _get_author(elem) -> str:
author = elem.getAttribute("w:author")
if not author:
for attr in elem.attributes.values():
if attr.localName == "author" or attr.name.endswith(":author"):
return attr.value
return author
def _can_merge_tracked(elem1, elem2) -> bool:
if _get_author(elem1) != _get_author(elem2):
return False
node = elem1.nextSibling
while node and node != elem2:
if node.nodeType == node.ELEMENT_NODE:
return False
if node.nodeType == node.TEXT_NODE and node.data.strip():
return False
node = node.nextSibling
return True
def _merge_tracked_content(target, source):
while source.firstChild:
child = source.firstChild
source.removeChild(child)
target.appendChild(child)
def _find_elements(root, tag: str) -> list:
results = []
def traverse(node):
if node.nodeType == node.ELEMENT_NODE:
name = node.localName or node.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(node)
for child in node.childNodes:
traverse(child)
traverse(root)
return results
def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]:
if not doc_xml_path.exists():
return {}
try:
tree = ET.parse(doc_xml_path)
root = tree.getroot()
except ET.ParseError:
return {}
namespaces = {"w": WORD_NS}
author_attr = f"{{{WORD_NS}}}author"
authors: dict[str, int] = {}
for tag in ["ins", "del"]:
for elem in root.findall(f".//w:{tag}", namespaces):
author = elem.get(author_attr)
if author:
authors[author] = authors.get(author, 0) + 1
return authors
def _get_authors_from_docx(docx_path: Path) -> dict[str, int]:
try:
with zipfile.ZipFile(docx_path, "r") as zf:
if "word/document.xml" not in zf.namelist():
return {}
with zf.open("word/document.xml") as f:
tree = ET.parse(f)
root = tree.getroot()
namespaces = {"w": WORD_NS}
author_attr = f"{{{WORD_NS}}}author"
authors: dict[str, int] = {}
for tag in ["ins", "del"]:
for elem in root.findall(f".//w:{tag}", namespaces):
author = elem.get(author_attr)
if author:
authors[author] = authors.get(author, 0) + 1
return authors
except (zipfile.BadZipFile, ET.ParseError):
return {}
def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str:
modified_xml = modified_dir / "word" / "document.xml"
modified_authors = get_tracked_change_authors(modified_xml)
if not modified_authors:
return default
original_authors = _get_authors_from_docx(original_docx)
new_changes: dict[str, int] = {}
for author, count in modified_authors.items():
original_count = original_authors.get(author, 0)
diff = count - original_count
if diff > 0:
new_changes[author] = diff
if not new_changes:
return default
if len(new_changes) == 1:
return next(iter(new_changes))
raise ValueError(
f"Multiple authors added new changes: {new_changes}. "
"Cannot infer which author to validate."
)

View File

@@ -0,0 +1,160 @@
"""Pack a directory into a DOCX, PPTX, or XLSX file (or .dotx/.potx/.xltx template).
Validates with auto-repair, condenses XML formatting, and creates the Office file.
Usage:
python pack.py <input_directory> <output_file> [--original <file>] [--validate true|false]
Examples:
python pack.py unpacked/ output.docx --original input.docx
python pack.py unpacked/ output.pptx --validate false
"""
import argparse
import sys
import shutil
import tempfile
import zipfile
from pathlib import Path
import defusedxml.minidom
from helpers import OOXML_FAMILY
from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
def pack(
input_directory: str,
output_file: str,
original_file: str | None = None,
validate: bool = True,
infer_author_func=None,
) -> tuple[None, str]:
input_dir = Path(input_directory)
output_path = Path(output_file)
family = OOXML_FAMILY.get(output_path.suffix.lower())
if not input_dir.is_dir():
return None, f"Error: {input_dir} is not a directory"
if family is None:
return None, f"Error: {output_file} must be one of: {', '.join(sorted(OOXML_FAMILY))}"
if validate and original_file:
original_path = Path(original_file)
if original_path.exists():
success, output = _run_validation(
input_dir, original_path, family, infer_author_func
)
if output:
print(output)
if not success:
return None, f"Error: Validation failed for {input_dir}"
with tempfile.TemporaryDirectory() as temp_dir:
temp_content_dir = Path(temp_dir) / "content"
shutil.copytree(input_dir, temp_content_dir)
for pattern in ["*.xml", "*.rels"]:
for xml_file in temp_content_dir.rglob(pattern):
_condense_xml(xml_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
for f in temp_content_dir.rglob("*"):
if f.is_file():
zf.write(f, f.relative_to(temp_content_dir))
return None, f"Successfully packed {input_dir} to {output_file}"
def _run_validation(
unpacked_dir: Path,
original_file: Path,
family: str,
infer_author_func=None,
) -> tuple[bool, str | None]:
output_lines = []
validators = []
if family == "docx":
author = "Claude"
if infer_author_func:
try:
author = infer_author_func(unpacked_dir, original_file)
except ValueError as e:
print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr)
validators = [
DOCXSchemaValidator(unpacked_dir, original_file),
RedliningValidator(unpacked_dir, original_file, author=author),
]
elif family == "pptx":
validators = [PPTXSchemaValidator(unpacked_dir, original_file)]
if not validators:
return True, None
total_repairs = sum(v.repair() for v in validators)
if total_repairs:
output_lines.append(f"Auto-repaired {total_repairs} issue(s)")
success = all(v.validate() for v in validators)
if success:
output_lines.append("All validations PASSED!")
return success, "\n".join(output_lines) if output_lines else None
def _condense_xml(xml_file: Path) -> None:
try:
with open(xml_file, encoding="utf-8") as f:
dom = defusedxml.minidom.parse(f)
for element in dom.getElementsByTagName("*"):
if element.tagName.endswith(":t"):
continue
for child in list(element.childNodes):
if (
child.nodeType == child.TEXT_NODE
and child.nodeValue
and child.nodeValue.strip() == ""
) or child.nodeType == child.COMMENT_NODE:
element.removeChild(child)
xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
except Exception as e:
print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr)
raise
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Pack a directory into a DOCX, PPTX, or XLSX file (or .dotx/.potx/.xltx template)"
)
parser.add_argument("input_directory", help="Unpacked Office document directory")
parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx)")
parser.add_argument(
"--original",
help="Original file for validation comparison",
)
parser.add_argument(
"--validate",
type=lambda x: x.lower() == "true",
default=True,
metavar="true|false",
help="Run validation with auto-repair (default: true)",
)
args = parser.parse_args()
_, message = pack(
args.input_directory,
args.output_file,
original_file=args.original,
validate=args.validate,
)
print(message)
if "Error" in message:
sys.exit(1)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:complexType name="CT_ShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Shape">
<xsd:sequence>
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_ConnectorNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Connector">
<xsd:sequence>
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence>
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrameNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrame">
<xsd:sequence>
<xsd:element name="nvGraphicFramePr" type="CT_GraphicFrameNonVisual" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GroupShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupShape">
<xsd:sequence>
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_ObjectChoices">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:simpleType name="ST_MarkerCoordinate">
<xsd:restriction base="xsd:double">
<xsd:minInclusive value="0.0"/>
<xsd:maxInclusive value="1.0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Marker">
<xsd:sequence>
<xsd:element name="x" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
<xsd:element name="y" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RelSizeAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:group ref="EG_ObjectChoices"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AbsSizeAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_Anchor">
<xsd:choice>
<xsd:element name="relSizeAnchor" type="CT_RelSizeAnchor"/>
<xsd:element name="absSizeAnchor" type="CT_AbsSizeAnchor"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Drawing">
<xsd:sequence>
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:element name="lockedCanvas" type="a:CT_GvmlGroupShape"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/picture">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:schema>

View File

@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:import schemaLocation="shared-relationshipReference.xsd"
namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:complexType name="CT_AnchorClientData">
<xsd:attribute name="fLocksWithSheet" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPrintsWithSheet" type="xsd:boolean" use="optional" default="true"/>
</xsd:complexType>
<xsd:complexType name="CT_ShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Shape">
<xsd:sequence>
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_ConnectorNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Connector">
<xsd:sequence>
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence>
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicalObjectFrameNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GraphicalObjectFrame">
<xsd:sequence>
<xsd:element name="nvGraphicFramePr" type="CT_GraphicalObjectFrameNonVisual" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GroupShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupShape">
<xsd:sequence>
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_ObjectChoices">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
<xsd:element name="contentPart" type="CT_Rel"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:complexType name="CT_Rel">
<xsd:attribute ref="r:id" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_ColID">
<xsd:restriction base="xsd:int">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RowID">
<xsd:restriction base="xsd:int">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Marker">
<xsd:sequence>
<xsd:element name="col" type="ST_ColID"/>
<xsd:element name="colOff" type="a:ST_Coordinate"/>
<xsd:element name="row" type="ST_RowID"/>
<xsd:element name="rowOff" type="a:ST_Coordinate"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ST_EditAs">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="twoCell"/>
<xsd:enumeration value="oneCell"/>
<xsd:enumeration value="absolute"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_TwoCellAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="editAs" type="ST_EditAs" use="optional" default="twoCell"/>
</xsd:complexType>
<xsd:complexType name="CT_OneCellAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AbsoluteAnchor">
<xsd:sequence>
<xsd:element name="pos" type="a:CT_Point2D"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_Anchor">
<xsd:choice>
<xsd:element name="twoCellAnchor" type="CT_TwoCellAnchor"/>
<xsd:element name="oneCellAnchor" type="CT_OneCellAnchor"/>
<xsd:element name="absoluteAnchor" type="CT_AbsoluteAnchor"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Drawing">
<xsd:sequence>
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="wsDr" type="CT_Drawing"/>
</xsd:schema>

View File

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:dpct="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:import schemaLocation="wml.xsd"
namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/picture"
schemaLocation="dml-picture.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:complexType name="CT_EffectExtent">
<xsd:attribute name="l" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="t" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="r" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="b" type="a:ST_Coordinate" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_WrapDistance">
<xsd:restriction base="xsd:unsignedInt"/>
</xsd:simpleType>
<xsd:complexType name="CT_Inline">
<xsd:sequence>
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="0" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_WrapText">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="bothSides"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="largest"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_WrapPath">
<xsd:sequence>
<xsd:element name="start" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/>
<xsd:element name="lineTo" type="a:CT_Point2D" minOccurs="2" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="edited" type="xsd:boolean" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapNone"/>
<xsd:complexType name="CT_WrapSquare">
<xsd:sequence>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapTight">
<xsd:sequence>
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapThrough">
<xsd:sequence>
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapTopBottom">
<xsd:sequence>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_WrapType">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="wrapNone" type="CT_WrapNone" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapSquare" type="CT_WrapSquare" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapTight" type="CT_WrapTight" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapThrough" type="CT_WrapThrough" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapTopAndBottom" type="CT_WrapTopBottom" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:simpleType name="ST_PositionOffset">
<xsd:restriction base="xsd:int"/>
</xsd:simpleType>
<xsd:simpleType name="ST_AlignH">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RelFromH">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="column"/>
<xsd:enumeration value="character"/>
<xsd:enumeration value="leftMargin"/>
<xsd:enumeration value="rightMargin"/>
<xsd:enumeration value="insideMargin"/>
<xsd:enumeration value="outsideMargin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_PosH">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="align" type="ST_AlignH" minOccurs="1" maxOccurs="1"/>
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="relativeFrom" type="ST_RelFromH" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_AlignV">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="top"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RelFromV">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="paragraph"/>
<xsd:enumeration value="line"/>
<xsd:enumeration value="topMargin"/>
<xsd:enumeration value="bottomMargin"/>
<xsd:enumeration value="insideMargin"/>
<xsd:enumeration value="outsideMargin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_PosV">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="align" type="ST_AlignV" minOccurs="1" maxOccurs="1"/>
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="relativeFrom" type="ST_RelFromV" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Anchor">
<xsd:sequence>
<xsd:element name="simplePos" type="a:CT_Point2D"/>
<xsd:element name="positionH" type="CT_PosH"/>
<xsd:element name="positionV" type="CT_PosV"/>
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
<xsd:group ref="EG_WrapType"/>
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="0" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="simplePos" type="xsd:boolean"/>
<xsd:attribute name="relativeHeight" type="xsd:unsignedInt" use="required"/>
<xsd:attribute name="behindDoc" type="xsd:boolean" use="required"/>
<xsd:attribute name="locked" type="xsd:boolean" use="required"/>
<xsd:attribute name="layoutInCell" type="xsd:boolean" use="required"/>
<xsd:attribute name="hidden" type="xsd:boolean" use="optional"/>
<xsd:attribute name="allowOverlap" type="xsd:boolean" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_TxbxContent">
<xsd:group ref="w:EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/>
</xsd:complexType>
<xsd:complexType name="CT_TextboxInfo">
<xsd:sequence>
<xsd:element name="txbxContent" type="CT_TxbxContent" minOccurs="1" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:unsignedShort" use="optional" default="0"/>
</xsd:complexType>
<xsd:complexType name="CT_LinkedTextboxInformation">
<xsd:sequence>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:unsignedShort" use="required"/>
<xsd:attribute name="seq" type="xsd:unsignedShort" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingShape">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="cNvCnPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:choice>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="txbx" type="CT_TextboxInfo" minOccurs="1" maxOccurs="1"/>
<xsd:element name="linkedTxbx" type="CT_LinkedTextboxInformation" minOccurs="1"
maxOccurs="1"/>
</xsd:choice>
<xsd:element name="bodyPr" type="a:CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="normalEastAsianFlow" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrame">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvFrPr" type="a:CT_NonVisualGraphicFrameProperties" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingContentPartNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:element name="cNvContentPartPr" type="a:CT_NonVisualContentPartProperties" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingContentPart">
<xsd:sequence>
<xsd:element name="nvContentPartPr" type="CT_WordprocessingContentPartNonVisual" minOccurs="0" maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="0" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional"/>
<xsd:attribute ref="r:id" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingGroup">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="wsp"/>
<xsd:element name="grpSp" type="CT_WordprocessingGroup"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element ref="dpct:pic"/>
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
</xsd:choice>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingCanvas">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="bg" type="a:CT_BackgroundFormatting" minOccurs="0" maxOccurs="1"/>
<xsd:element name="whole" type="a:CT_WholeE2oFormatting" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="wsp"/>
<xsd:element ref="dpct:pic"/>
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
<xsd:element ref="wgp"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
</xsd:choice>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="wpc" type="CT_WordprocessingCanvas"/>
<xsd:element name="wgp" type="CT_WordprocessingGroup"/>
<xsd:element name="wsp" type="CT_WordprocessingShape"/>
<xsd:element name="inline" type="CT_Inline"/>
<xsd:element name="anchor" type="CT_Anchor"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
elementFormDefault="qualified">
<xsd:complexType name="CT_AdditionalCharacteristics">
<xsd:sequence>
<xsd:element name="characteristic" type="CT_Characteristic" minOccurs="0"
maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Characteristic">
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="relation" type="ST_Relation" use="required"/>
<xsd:attribute name="val" type="xsd:string" use="required"/>
<xsd:attribute name="vocabulary" type="xsd:anyURI" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_Relation">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ge"/>
<xsd:enumeration value="le"/>
<xsd:enumeration value="gt"/>
<xsd:enumeration value="lt"/>
<xsd:enumeration value="eq"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="additionalCharacteristics" type="CT_AdditionalCharacteristics"/>
</xsd:schema>

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:simpleType name="ST_SourceType">
<xsd:restriction base="s:ST_String">
<xsd:enumeration value="ArticleInAPeriodical"/>
<xsd:enumeration value="Book"/>
<xsd:enumeration value="BookSection"/>
<xsd:enumeration value="JournalArticle"/>
<xsd:enumeration value="ConferenceProceedings"/>
<xsd:enumeration value="Report"/>
<xsd:enumeration value="SoundRecording"/>
<xsd:enumeration value="Performance"/>
<xsd:enumeration value="Art"/>
<xsd:enumeration value="DocumentFromInternetSite"/>
<xsd:enumeration value="InternetSite"/>
<xsd:enumeration value="Film"/>
<xsd:enumeration value="Interview"/>
<xsd:enumeration value="Patent"/>
<xsd:enumeration value="ElectronicSource"/>
<xsd:enumeration value="Case"/>
<xsd:enumeration value="Misc"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_NameListType">
<xsd:sequence>
<xsd:element name="Person" type="CT_PersonType" minOccurs="1" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_PersonType">
<xsd:sequence>
<xsd:element name="Last" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="First" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="Middle" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NameType">
<xsd:sequence>
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NameOrCorporateType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
<xsd:element name="Corporate" minOccurs="1" maxOccurs="1" type="s:ST_String"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AuthorType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Artist" type="CT_NameType"/>
<xsd:element name="Author" type="CT_NameOrCorporateType"/>
<xsd:element name="BookAuthor" type="CT_NameType"/>
<xsd:element name="Compiler" type="CT_NameType"/>
<xsd:element name="Composer" type="CT_NameType"/>
<xsd:element name="Conductor" type="CT_NameType"/>
<xsd:element name="Counsel" type="CT_NameType"/>
<xsd:element name="Director" type="CT_NameType"/>
<xsd:element name="Editor" type="CT_NameType"/>
<xsd:element name="Interviewee" type="CT_NameType"/>
<xsd:element name="Interviewer" type="CT_NameType"/>
<xsd:element name="Inventor" type="CT_NameType"/>
<xsd:element name="Performer" type="CT_NameOrCorporateType"/>
<xsd:element name="ProducerName" type="CT_NameType"/>
<xsd:element name="Translator" type="CT_NameType"/>
<xsd:element name="Writer" type="CT_NameType"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SourceType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="AbbreviatedCaseNumber" type="s:ST_String"/>
<xsd:element name="AlbumTitle" type="s:ST_String"/>
<xsd:element name="Author" type="CT_AuthorType"/>
<xsd:element name="BookTitle" type="s:ST_String"/>
<xsd:element name="Broadcaster" type="s:ST_String"/>
<xsd:element name="BroadcastTitle" type="s:ST_String"/>
<xsd:element name="CaseNumber" type="s:ST_String"/>
<xsd:element name="ChapterNumber" type="s:ST_String"/>
<xsd:element name="City" type="s:ST_String"/>
<xsd:element name="Comments" type="s:ST_String"/>
<xsd:element name="ConferenceName" type="s:ST_String"/>
<xsd:element name="CountryRegion" type="s:ST_String"/>
<xsd:element name="Court" type="s:ST_String"/>
<xsd:element name="Day" type="s:ST_String"/>
<xsd:element name="DayAccessed" type="s:ST_String"/>
<xsd:element name="Department" type="s:ST_String"/>
<xsd:element name="Distributor" type="s:ST_String"/>
<xsd:element name="Edition" type="s:ST_String"/>
<xsd:element name="Guid" type="s:ST_String"/>
<xsd:element name="Institution" type="s:ST_String"/>
<xsd:element name="InternetSiteTitle" type="s:ST_String"/>
<xsd:element name="Issue" type="s:ST_String"/>
<xsd:element name="JournalName" type="s:ST_String"/>
<xsd:element name="LCID" type="s:ST_Lang"/>
<xsd:element name="Medium" type="s:ST_String"/>
<xsd:element name="Month" type="s:ST_String"/>
<xsd:element name="MonthAccessed" type="s:ST_String"/>
<xsd:element name="NumberVolumes" type="s:ST_String"/>
<xsd:element name="Pages" type="s:ST_String"/>
<xsd:element name="PatentNumber" type="s:ST_String"/>
<xsd:element name="PeriodicalTitle" type="s:ST_String"/>
<xsd:element name="ProductionCompany" type="s:ST_String"/>
<xsd:element name="PublicationTitle" type="s:ST_String"/>
<xsd:element name="Publisher" type="s:ST_String"/>
<xsd:element name="RecordingNumber" type="s:ST_String"/>
<xsd:element name="RefOrder" type="s:ST_String"/>
<xsd:element name="Reporter" type="s:ST_String"/>
<xsd:element name="SourceType" type="ST_SourceType"/>
<xsd:element name="ShortTitle" type="s:ST_String"/>
<xsd:element name="StandardNumber" type="s:ST_String"/>
<xsd:element name="StateProvince" type="s:ST_String"/>
<xsd:element name="Station" type="s:ST_String"/>
<xsd:element name="Tag" type="s:ST_String"/>
<xsd:element name="Theater" type="s:ST_String"/>
<xsd:element name="ThesisType" type="s:ST_String"/>
<xsd:element name="Title" type="s:ST_String"/>
<xsd:element name="Type" type="s:ST_String"/>
<xsd:element name="URL" type="s:ST_String"/>
<xsd:element name="Version" type="s:ST_String"/>
<xsd:element name="Volume" type="s:ST_String"/>
<xsd:element name="Year" type="s:ST_String"/>
<xsd:element name="YearAccessed" type="s:ST_String"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="Sources" type="CT_Sources"/>
<xsd:complexType name="CT_Sources">
<xsd:sequence>
<xsd:element name="Source" type="CT_SourceType" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="SelectedStyle" type="s:ST_String"/>
<xsd:attribute name="StyleName" type="s:ST_String"/>
<xsd:attribute name="URI" type="s:ST_String"/>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
elementFormDefault="qualified">
<xsd:simpleType name="ST_Lang">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_HexColorRGB">
<xsd:restriction base="xsd:hexBinary">
<xsd:length value="3" fixed="true"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Panose">
<xsd:restriction base="xsd:hexBinary">
<xsd:length value="10"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CalendarType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="gregorian"/>
<xsd:enumeration value="gregorianUs"/>
<xsd:enumeration value="gregorianMeFrench"/>
<xsd:enumeration value="gregorianArabic"/>
<xsd:enumeration value="hijri"/>
<xsd:enumeration value="hebrew"/>
<xsd:enumeration value="taiwan"/>
<xsd:enumeration value="japan"/>
<xsd:enumeration value="thai"/>
<xsd:enumeration value="korea"/>
<xsd:enumeration value="saka"/>
<xsd:enumeration value="gregorianXlitEnglish"/>
<xsd:enumeration value="gregorianXlitFrench"/>
<xsd:enumeration value="none"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_AlgClass">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="hash"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CryptProv">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="rsaAES"/>
<xsd:enumeration value="rsaFull"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_AlgType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="typeAny"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ColorType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_Guid">
<xsd:restriction base="xsd:token">
<xsd:pattern value="\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OnOff">
<xsd:union memberTypes="xsd:boolean ST_OnOff1"/>
</xsd:simpleType>
<xsd:simpleType name="ST_OnOff1">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="on"/>
<xsd:enumeration value="off"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_String">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_XmlName">
<xsd:restriction base="xsd:NCName">
<xsd:minLength value="1"/>
<xsd:maxLength value="255"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TrueFalse">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="false"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TrueFalseBlank">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="false"/>
<xsd:enumeration value=""/>
<xsd:enumeration value="True"/>
<xsd:enumeration value="False"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_UnsignedDecimalNumber">
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TwipsMeasure">
<xsd:union memberTypes="ST_UnsignedDecimalNumber ST_PositiveUniversalMeasure"/>
</xsd:simpleType>
<xsd:simpleType name="ST_VerticalAlignRun">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="baseline"/>
<xsd:enumeration value="superscript"/>
<xsd:enumeration value="subscript"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Xstring">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_XAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_YAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="inline"/>
<xsd:enumeration value="top"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConformanceClass">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="strict"/>
<xsd:enumeration value="transitional"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_UniversalMeasure">
<xsd:restriction base="xsd:string">
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositiveUniversalMeasure">
<xsd:restriction base="ST_UniversalMeasure">
<xsd:pattern value="[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Percentage">
<xsd:restriction base="xsd:string">
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FixedPercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="-?((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositivePercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="[0-9]+(\.[0-9]+)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositiveFixedPercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:complexType name="CT_DatastoreSchemaRef">
<xsd:attribute name="uri" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_DatastoreSchemaRefs">
<xsd:sequence>
<xsd:element name="schemaRef" type="CT_DatastoreSchemaRef" minOccurs="0" maxOccurs="unbounded"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DatastoreItem">
<xsd:sequence>
<xsd:element name="schemaRefs" type="CT_DatastoreSchemaRefs" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="itemID" type="s:ST_Guid" use="required"/>
</xsd:complexType>
<xsd:element name="datastoreItem" type="CT_DatastoreItem"/>
</xsd:schema>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
targetNamespace="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
attributeFormDefault="qualified" elementFormDefault="qualified">
<xsd:complexType name="CT_Schema">
<xsd:attribute name="uri" type="xsd:string" default=""/>
<xsd:attribute name="manifestLocation" type="xsd:string"/>
<xsd:attribute name="schemaLocation" type="xsd:string"/>
<xsd:attribute name="schemaLanguage" type="xsd:token"/>
</xsd:complexType>
<xsd:complexType name="CT_SchemaLibrary">
<xsd:sequence>
<xsd:element name="schema" type="CT_Schema" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="schemaLibrary" type="CT_SchemaLibrary"/>
</xsd:schema>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
blockDefault="#all" elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:element name="Properties" type="CT_Properties"/>
<xsd:complexType name="CT_Properties">
<xsd:sequence>
<xsd:element name="property" minOccurs="0" maxOccurs="unbounded" type="CT_Property"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Property">
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
<xsd:element ref="vt:array"/>
<xsd:element ref="vt:blob"/>
<xsd:element ref="vt:oblob"/>
<xsd:element ref="vt:empty"/>
<xsd:element ref="vt:null"/>
<xsd:element ref="vt:i1"/>
<xsd:element ref="vt:i2"/>
<xsd:element ref="vt:i4"/>
<xsd:element ref="vt:i8"/>
<xsd:element ref="vt:int"/>
<xsd:element ref="vt:ui1"/>
<xsd:element ref="vt:ui2"/>
<xsd:element ref="vt:ui4"/>
<xsd:element ref="vt:ui8"/>
<xsd:element ref="vt:uint"/>
<xsd:element ref="vt:r4"/>
<xsd:element ref="vt:r8"/>
<xsd:element ref="vt:decimal"/>
<xsd:element ref="vt:lpstr"/>
<xsd:element ref="vt:lpwstr"/>
<xsd:element ref="vt:bstr"/>
<xsd:element ref="vt:date"/>
<xsd:element ref="vt:filetime"/>
<xsd:element ref="vt:bool"/>
<xsd:element ref="vt:cy"/>
<xsd:element ref="vt:error"/>
<xsd:element ref="vt:stream"/>
<xsd:element ref="vt:ostream"/>
<xsd:element ref="vt:storage"/>
<xsd:element ref="vt:ostorage"/>
<xsd:element ref="vt:vstream"/>
<xsd:element ref="vt:clsid"/>
</xsd:choice>
<xsd:attribute name="fmtid" use="required" type="s:ST_Guid"/>
<xsd:attribute name="pid" use="required" type="xsd:int"/>
<xsd:attribute name="name" use="optional" type="xsd:string"/>
<xsd:attribute name="linkTarget" use="optional" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
elementFormDefault="qualified" blockDefault="#all">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
<xsd:element name="Properties" type="CT_Properties"/>
<xsd:complexType name="CT_Properties">
<xsd:all>
<xsd:element name="Template" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Manager" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Company" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Pages" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Words" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Characters" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="PresentationFormat" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Lines" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Paragraphs" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Slides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Notes" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="TotalTime" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="HiddenSlides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="MMClips" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="ScaleCrop" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="HeadingPairs" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
<xsd:element name="TitlesOfParts" minOccurs="0" maxOccurs="1" type="CT_VectorLpstr"/>
<xsd:element name="LinksUpToDate" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="CharactersWithSpaces" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="SharedDoc" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="HyperlinkBase" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="HLinks" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
<xsd:element name="HyperlinksChanged" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="DigSig" minOccurs="0" maxOccurs="1" type="CT_DigSigBlob"/>
<xsd:element name="Application" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="AppVersion" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="DocSecurity" minOccurs="0" maxOccurs="1" type="xsd:int"/>
</xsd:all>
</xsd:complexType>
<xsd:complexType name="CT_VectorVariant">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_VectorLpstr">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DigSigBlob">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:blob"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
blockDefault="#all" elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:simpleType name="ST_VectorBaseType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="variant"/>
<xsd:enumeration value="i1"/>
<xsd:enumeration value="i2"/>
<xsd:enumeration value="i4"/>
<xsd:enumeration value="i8"/>
<xsd:enumeration value="ui1"/>
<xsd:enumeration value="ui2"/>
<xsd:enumeration value="ui4"/>
<xsd:enumeration value="ui8"/>
<xsd:enumeration value="r4"/>
<xsd:enumeration value="r8"/>
<xsd:enumeration value="lpstr"/>
<xsd:enumeration value="lpwstr"/>
<xsd:enumeration value="bstr"/>
<xsd:enumeration value="date"/>
<xsd:enumeration value="filetime"/>
<xsd:enumeration value="bool"/>
<xsd:enumeration value="cy"/>
<xsd:enumeration value="error"/>
<xsd:enumeration value="clsid"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ArrayBaseType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="variant"/>
<xsd:enumeration value="i1"/>
<xsd:enumeration value="i2"/>
<xsd:enumeration value="i4"/>
<xsd:enumeration value="int"/>
<xsd:enumeration value="ui1"/>
<xsd:enumeration value="ui2"/>
<xsd:enumeration value="ui4"/>
<xsd:enumeration value="uint"/>
<xsd:enumeration value="r4"/>
<xsd:enumeration value="r8"/>
<xsd:enumeration value="decimal"/>
<xsd:enumeration value="bstr"/>
<xsd:enumeration value="date"/>
<xsd:enumeration value="bool"/>
<xsd:enumeration value="cy"/>
<xsd:enumeration value="error"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Cy">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\s*[0-9]*\.[0-9]{4}\s*"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Error">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\s*0x[0-9A-Za-z]{8}\s*"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Empty"/>
<xsd:complexType name="CT_Null"/>
<xsd:complexType name="CT_Vector">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="variant"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="i8"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="ui8"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="lpstr"/>
<xsd:element ref="lpwstr"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="filetime"/>
<xsd:element ref="bool"/>
<xsd:element ref="cy"/>
<xsd:element ref="error"/>
<xsd:element ref="clsid"/>
</xsd:choice>
<xsd:attribute name="baseType" type="ST_VectorBaseType" use="required"/>
<xsd:attribute name="size" type="xsd:unsignedInt" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Array">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="variant"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="int"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="uint"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="decimal"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="bool"/>
<xsd:element ref="error"/>
<xsd:element ref="cy"/>
</xsd:choice>
<xsd:attribute name="lBounds" type="xsd:int" use="required"/>
<xsd:attribute name="uBounds" type="xsd:int" use="required"/>
<xsd:attribute name="baseType" type="ST_ArrayBaseType" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Variant">
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element ref="variant"/>
<xsd:element ref="vector"/>
<xsd:element ref="array"/>
<xsd:element ref="blob"/>
<xsd:element ref="oblob"/>
<xsd:element ref="empty"/>
<xsd:element ref="null"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="i8"/>
<xsd:element ref="int"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="ui8"/>
<xsd:element ref="uint"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="decimal"/>
<xsd:element ref="lpstr"/>
<xsd:element ref="lpwstr"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="filetime"/>
<xsd:element ref="bool"/>
<xsd:element ref="cy"/>
<xsd:element ref="error"/>
<xsd:element ref="stream"/>
<xsd:element ref="ostream"/>
<xsd:element ref="storage"/>
<xsd:element ref="ostorage"/>
<xsd:element ref="vstream"/>
<xsd:element ref="clsid"/>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="CT_Vstream">
<xsd:simpleContent>
<xsd:extension base="xsd:base64Binary">
<xsd:attribute name="version" type="s:ST_Guid"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:element name="variant" type="CT_Variant"/>
<xsd:element name="vector" type="CT_Vector"/>
<xsd:element name="array" type="CT_Array"/>
<xsd:element name="blob" type="xsd:base64Binary"/>
<xsd:element name="oblob" type="xsd:base64Binary"/>
<xsd:element name="empty" type="CT_Empty"/>
<xsd:element name="null" type="CT_Null"/>
<xsd:element name="i1" type="xsd:byte"/>
<xsd:element name="i2" type="xsd:short"/>
<xsd:element name="i4" type="xsd:int"/>
<xsd:element name="i8" type="xsd:long"/>
<xsd:element name="int" type="xsd:int"/>
<xsd:element name="ui1" type="xsd:unsignedByte"/>
<xsd:element name="ui2" type="xsd:unsignedShort"/>
<xsd:element name="ui4" type="xsd:unsignedInt"/>
<xsd:element name="ui8" type="xsd:unsignedLong"/>
<xsd:element name="uint" type="xsd:unsignedInt"/>
<xsd:element name="r4" type="xsd:float"/>
<xsd:element name="r8" type="xsd:double"/>
<xsd:element name="decimal" type="xsd:decimal"/>
<xsd:element name="lpstr" type="xsd:string"/>
<xsd:element name="lpwstr" type="xsd:string"/>
<xsd:element name="bstr" type="xsd:string"/>
<xsd:element name="date" type="xsd:dateTime"/>
<xsd:element name="filetime" type="xsd:dateTime"/>
<xsd:element name="bool" type="xsd:boolean"/>
<xsd:element name="cy" type="ST_Cy"/>
<xsd:element name="error" type="ST_Error"/>
<xsd:element name="stream" type="xsd:base64Binary"/>
<xsd:element name="ostream" type="xsd:base64Binary"/>
<xsd:element name="storage" type="xsd:base64Binary"/>
<xsd:element name="ostorage" type="xsd:base64Binary"/>
<xsd:element name="vstream" type="CT_Vstream"/>
<xsd:element name="clsid" type="s:ST_Guid"/>
</xsd:schema>

View File

@@ -0,0 +1,582 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/math">
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
schemaLocation="wml.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd"/>
<xsd:simpleType name="ST_Integer255">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Integer255">
<xsd:attribute name="val" type="ST_Integer255" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Integer2">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="-2"/>
<xsd:maxInclusive value="2"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Integer2">
<xsd:attribute name="val" type="ST_Integer2" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_SpacingRule">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="4"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_SpacingRule">
<xsd:attribute name="val" type="ST_SpacingRule" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_UnSignedInteger">
<xsd:restriction base="xsd:unsignedInt"/>
</xsd:simpleType>
<xsd:complexType name="CT_UnSignedInteger">
<xsd:attribute name="val" type="ST_UnSignedInteger" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Char">
<xsd:restriction base="xsd:string">
<xsd:maxLength value="1"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Char">
<xsd:attribute name="val" type="ST_Char" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_OnOff">
<xsd:attribute name="val" type="s:ST_OnOff"/>
</xsd:complexType>
<xsd:complexType name="CT_String">
<xsd:attribute name="val" type="s:ST_String"/>
</xsd:complexType>
<xsd:complexType name="CT_XAlign">
<xsd:attribute name="val" type="s:ST_XAlign" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_YAlign">
<xsd:attribute name="val" type="s:ST_YAlign" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Shp">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="centered"/>
<xsd:enumeration value="match"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Shp">
<xsd:attribute name="val" type="ST_Shp" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_FType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="bar"/>
<xsd:enumeration value="skw"/>
<xsd:enumeration value="lin"/>
<xsd:enumeration value="noBar"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_FType">
<xsd:attribute name="val" type="ST_FType" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_LimLoc">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="undOvr"/>
<xsd:enumeration value="subSup"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_LimLoc">
<xsd:attribute name="val" type="ST_LimLoc" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_TopBot">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="bot"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_TopBot">
<xsd:attribute name="val" type="ST_TopBot" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Script">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="roman"/>
<xsd:enumeration value="script"/>
<xsd:enumeration value="fraktur"/>
<xsd:enumeration value="double-struck"/>
<xsd:enumeration value="sans-serif"/>
<xsd:enumeration value="monospace"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Script">
<xsd:attribute name="val" type="ST_Script"/>
</xsd:complexType>
<xsd:simpleType name="ST_Style">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="p"/>
<xsd:enumeration value="b"/>
<xsd:enumeration value="i"/>
<xsd:enumeration value="bi"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Style">
<xsd:attribute name="val" type="ST_Style"/>
</xsd:complexType>
<xsd:complexType name="CT_ManualBreak">
<xsd:attribute name="alnAt" type="ST_Integer255"/>
</xsd:complexType>
<xsd:group name="EG_ScriptStyle">
<xsd:sequence>
<xsd:element name="scr" minOccurs="0" type="CT_Script"/>
<xsd:element name="sty" minOccurs="0" type="CT_Style"/>
</xsd:sequence>
</xsd:group>
<xsd:complexType name="CT_RPR">
<xsd:sequence>
<xsd:element name="lit" minOccurs="0" type="CT_OnOff"/>
<xsd:choice>
<xsd:element name="nor" minOccurs="0" type="CT_OnOff"/>
<xsd:sequence>
<xsd:group ref="EG_ScriptStyle"/>
</xsd:sequence>
</xsd:choice>
<xsd:element name="brk" minOccurs="0" type="CT_ManualBreak"/>
<xsd:element name="aln" minOccurs="0" type="CT_OnOff"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Text">
<xsd:simpleContent>
<xsd:extension base="s:ST_String">
<xsd:attribute ref="xml:space" use="optional"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="CT_R">
<xsd:sequence>
<xsd:element name="rPr" type="CT_RPR" minOccurs="0"/>
<xsd:group ref="w:EG_RPr" minOccurs="0"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:group ref="w:EG_RunInnerContent"/>
<xsd:element name="t" type="CT_Text" minOccurs="0"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_CtrlPr">
<xsd:sequence>
<xsd:group ref="w:EG_RPrMath" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AccPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Acc">
<xsd:sequence>
<xsd:element name="accPr" type="CT_AccPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BarPr">
<xsd:sequence>
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Bar">
<xsd:sequence>
<xsd:element name="barPr" type="CT_BarPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BoxPr">
<xsd:sequence>
<xsd:element name="opEmu" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="noBreak" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="diff" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="brk" type="CT_ManualBreak" minOccurs="0"/>
<xsd:element name="aln" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Box">
<xsd:sequence>
<xsd:element name="boxPr" type="CT_BoxPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BorderBoxPr">
<xsd:sequence>
<xsd:element name="hideTop" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideBot" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideLeft" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideRight" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeH" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeV" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeBLTR" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeTLBR" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BorderBox">
<xsd:sequence>
<xsd:element name="borderBoxPr" type="CT_BorderBoxPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DPr">
<xsd:sequence>
<xsd:element name="begChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="sepChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="endChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="shp" type="CT_Shp" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_D">
<xsd:sequence>
<xsd:element name="dPr" type="CT_DPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_EqArrPr">
<xsd:sequence>
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
<xsd:element name="maxDist" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="objDist" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_EqArr">
<xsd:sequence>
<xsd:element name="eqArrPr" type="CT_EqArrPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_FPr">
<xsd:sequence>
<xsd:element name="type" type="CT_FType" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_F">
<xsd:sequence>
<xsd:element name="fPr" type="CT_FPr" minOccurs="0"/>
<xsd:element name="num" type="CT_OMathArg"/>
<xsd:element name="den" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_FuncPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Func">
<xsd:sequence>
<xsd:element name="funcPr" type="CT_FuncPr" minOccurs="0"/>
<xsd:element name="fName" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupChrPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="vertJc" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupChr">
<xsd:sequence>
<xsd:element name="groupChrPr" type="CT_GroupChrPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimLowPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimLow">
<xsd:sequence>
<xsd:element name="limLowPr" type="CT_LimLowPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="lim" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimUppPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimUpp">
<xsd:sequence>
<xsd:element name="limUppPr" type="CT_LimUppPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="lim" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MCPr">
<xsd:sequence>
<xsd:element name="count" type="CT_Integer255" minOccurs="0"/>
<xsd:element name="mcJc" type="CT_XAlign" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MC">
<xsd:sequence>
<xsd:element name="mcPr" type="CT_MCPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MCS">
<xsd:sequence>
<xsd:element name="mc" type="CT_MC" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MPr">
<xsd:sequence>
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
<xsd:element name="plcHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="cGpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="cSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="cGp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="mcs" type="CT_MCS" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MR">
<xsd:sequence>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_M">
<xsd:sequence>
<xsd:element name="mPr" type="CT_MPr" minOccurs="0"/>
<xsd:element name="mr" type="CT_MR" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NaryPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="limLoc" type="CT_LimLoc" minOccurs="0"/>
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="subHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="supHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Nary">
<xsd:sequence>
<xsd:element name="naryPr" type="CT_NaryPr" minOccurs="0"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_PhantPr">
<xsd:sequence>
<xsd:element name="show" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroWid" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroAsc" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroDesc" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="transp" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Phant">
<xsd:sequence>
<xsd:element name="phantPr" type="CT_PhantPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RadPr">
<xsd:sequence>
<xsd:element name="degHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Rad">
<xsd:sequence>
<xsd:element name="radPr" type="CT_RadPr" minOccurs="0"/>
<xsd:element name="deg" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SPrePr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SPre">
<xsd:sequence>
<xsd:element name="sPrePr" type="CT_SPrePr" minOccurs="0"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSub">
<xsd:sequence>
<xsd:element name="sSubPr" type="CT_SSubPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sub" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubSupPr">
<xsd:sequence>
<xsd:element name="alnScr" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubSup">
<xsd:sequence>
<xsd:element name="sSubSupPr" type="CT_SSubSupPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSupPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSup">
<xsd:sequence>
<xsd:element name="sSupPr" type="CT_SSupPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_OMathMathElements">
<xsd:choice>
<xsd:element name="acc" type="CT_Acc"/>
<xsd:element name="bar" type="CT_Bar"/>
<xsd:element name="box" type="CT_Box"/>
<xsd:element name="borderBox" type="CT_BorderBox"/>
<xsd:element name="d" type="CT_D"/>
<xsd:element name="eqArr" type="CT_EqArr"/>
<xsd:element name="f" type="CT_F"/>
<xsd:element name="func" type="CT_Func"/>
<xsd:element name="groupChr" type="CT_GroupChr"/>
<xsd:element name="limLow" type="CT_LimLow"/>
<xsd:element name="limUpp" type="CT_LimUpp"/>
<xsd:element name="m" type="CT_M"/>
<xsd:element name="nary" type="CT_Nary"/>
<xsd:element name="phant" type="CT_Phant"/>
<xsd:element name="rad" type="CT_Rad"/>
<xsd:element name="sPre" type="CT_SPre"/>
<xsd:element name="sSub" type="CT_SSub"/>
<xsd:element name="sSubSup" type="CT_SSubSup"/>
<xsd:element name="sSup" type="CT_SSup"/>
<xsd:element name="r" type="CT_R"/>
</xsd:choice>
</xsd:group>
<xsd:group name="EG_OMathElements">
<xsd:choice>
<xsd:group ref="EG_OMathMathElements"/>
<xsd:group ref="w:EG_PContentMath"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_OMathArgPr">
<xsd:sequence>
<xsd:element name="argSz" type="CT_Integer2" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_OMathArg">
<xsd:sequence>
<xsd:element name="argPr" type="CT_OMathArgPr" minOccurs="0"/>
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ST_Jc">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="centerGroup"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_OMathJc">
<xsd:attribute name="val" type="ST_Jc"/>
</xsd:complexType>
<xsd:complexType name="CT_OMathParaPr">
<xsd:sequence>
<xsd:element name="jc" type="CT_OMathJc" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_TwipsMeasure">
<xsd:attribute name="val" type="s:ST_TwipsMeasure" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_BreakBin">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="before"/>
<xsd:enumeration value="after"/>
<xsd:enumeration value="repeat"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_BreakBin">
<xsd:attribute name="val" type="ST_BreakBin"/>
</xsd:complexType>
<xsd:simpleType name="ST_BreakBinSub">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="--"/>
<xsd:enumeration value="-+"/>
<xsd:enumeration value="+-"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_BreakBinSub">
<xsd:attribute name="val" type="ST_BreakBinSub"/>
</xsd:complexType>
<xsd:complexType name="CT_MathPr">
<xsd:sequence>
<xsd:element name="mathFont" type="CT_String" minOccurs="0"/>
<xsd:element name="brkBin" type="CT_BreakBin" minOccurs="0"/>
<xsd:element name="brkBinSub" type="CT_BreakBinSub" minOccurs="0"/>
<xsd:element name="smallFrac" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="dispDef" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="lMargin" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="rMargin" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="defJc" type="CT_OMathJc" minOccurs="0"/>
<xsd:element name="preSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="postSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="interSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="intraSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:choice minOccurs="0">
<xsd:element name="wrapIndent" type="CT_TwipsMeasure"/>
<xsd:element name="wrapRight" type="CT_OnOff"/>
</xsd:choice>
<xsd:element name="intLim" type="CT_LimLoc" minOccurs="0"/>
<xsd:element name="naryLim" type="CT_LimLoc" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="mathPr" type="CT_MathPr"/>
<xsd:complexType name="CT_OMathPara">
<xsd:sequence>
<xsd:element name="oMathParaPr" type="CT_OMathParaPr" minOccurs="0"/>
<xsd:element name="oMath" type="CT_OMath" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_OMath">
<xsd:sequence>
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="oMathPara" type="CT_OMathPara"/>
<xsd:element name="oMath" type="CT_OMath"/>
</xsd:schema>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
blockDefault="#all">
<xsd:simpleType name="ST_RelationshipId">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:attribute name="id" type="ST_RelationshipId"/>
<xsd:attribute name="embed" type="ST_RelationshipId"/>
<xsd:attribute name="link" type="ST_RelationshipId"/>
<xsd:attribute name="dm" type="ST_RelationshipId" default=""/>
<xsd:attribute name="lo" type="ST_RelationshipId" default=""/>
<xsd:attribute name="qs" type="ST_RelationshipId" default=""/>
<xsd:attribute name="cs" type="ST_RelationshipId" default=""/>
<xsd:attribute name="blip" type="ST_RelationshipId" default=""/>
<xsd:attribute name="pict" type="ST_RelationshipId"/>
<xsd:attribute name="href" type="ST_RelationshipId"/>
<xsd:attribute name="topLeft" type="ST_RelationshipId"/>
<xsd:attribute name="topRight" type="ST_RelationshipId"/>
<xsd:attribute name="bottomLeft" type="ST_RelationshipId"/>
<xsd:attribute name="bottomRight" type="ST_RelationshipId"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,570 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-microsoft-com:vml"
xmlns:pvml="urn:schemas-microsoft-com:office:powerpoint"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:vml" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="urn:schemas-microsoft-com:office:office"
schemaLocation="vml-officeDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
schemaLocation="wml.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:word"
schemaLocation="vml-wordprocessingDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:excel"
schemaLocation="vml-spreadsheetDrawing.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:powerpoint"
schemaLocation="vml-presentationDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:attributeGroup name="AG_Id">
<xsd:attribute name="id" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Style">
<xsd:attribute name="style" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Type">
<xsd:attribute name="type" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Adj">
<xsd:attribute name="adj" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Path">
<xsd:attribute name="path" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Fill">
<xsd:attribute name="filled" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Chromakey">
<xsd:attribute name="chromakey" type="s:ST_ColorType" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Ext">
<xsd:attribute name="ext" form="qualified" type="ST_Ext"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_CoreAttributes">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="href" type="xsd:string" use="optional"/>
<xsd:attribute name="target" type="xsd:string" use="optional"/>
<xsd:attribute name="class" type="xsd:string" use="optional"/>
<xsd:attribute name="title" type="xsd:string" use="optional"/>
<xsd:attribute name="alt" type="xsd:string" use="optional"/>
<xsd:attribute name="coordsize" type="xsd:string" use="optional"/>
<xsd:attribute name="coordorigin" type="xsd:string" use="optional"/>
<xsd:attribute name="wrapcoords" type="xsd:string" use="optional"/>
<xsd:attribute name="print" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_ShapeAttributes">
<xsd:attributeGroup ref="AG_Chromakey"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="stroked" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="strokeweight" type="xsd:string" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_OfficeCoreAttributes">
<xsd:attribute ref="o:spid"/>
<xsd:attribute ref="o:oned"/>
<xsd:attribute ref="o:regroupid"/>
<xsd:attribute ref="o:doubleclicknotify"/>
<xsd:attribute ref="o:button"/>
<xsd:attribute ref="o:userhidden"/>
<xsd:attribute ref="o:bullet"/>
<xsd:attribute ref="o:hr"/>
<xsd:attribute ref="o:hrstd"/>
<xsd:attribute ref="o:hrnoshade"/>
<xsd:attribute ref="o:hrpct"/>
<xsd:attribute ref="o:hralign"/>
<xsd:attribute ref="o:allowincell"/>
<xsd:attribute ref="o:allowoverlap"/>
<xsd:attribute ref="o:userdrawn"/>
<xsd:attribute ref="o:bordertopcolor"/>
<xsd:attribute ref="o:borderleftcolor"/>
<xsd:attribute ref="o:borderbottomcolor"/>
<xsd:attribute ref="o:borderrightcolor"/>
<xsd:attribute ref="o:dgmlayout"/>
<xsd:attribute ref="o:dgmnodekind"/>
<xsd:attribute ref="o:dgmlayoutmru"/>
<xsd:attribute ref="o:insetmode"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_OfficeShapeAttributes">
<xsd:attribute ref="o:spt"/>
<xsd:attribute ref="o:connectortype"/>
<xsd:attribute ref="o:bwmode"/>
<xsd:attribute ref="o:bwpure"/>
<xsd:attribute ref="o:bwnormal"/>
<xsd:attribute ref="o:forcedash"/>
<xsd:attribute ref="o:oleicon"/>
<xsd:attribute ref="o:ole"/>
<xsd:attribute ref="o:preferrelative"/>
<xsd:attribute ref="o:cliptowrap"/>
<xsd:attribute ref="o:clip"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_AllCoreAttributes">
<xsd:attributeGroup ref="AG_CoreAttributes"/>
<xsd:attributeGroup ref="AG_OfficeCoreAttributes"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_AllShapeAttributes">
<xsd:attributeGroup ref="AG_ShapeAttributes"/>
<xsd:attributeGroup ref="AG_OfficeShapeAttributes"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_ImageAttributes">
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="cropleft" type="xsd:string" use="optional"/>
<xsd:attribute name="croptop" type="xsd:string" use="optional"/>
<xsd:attribute name="cropright" type="xsd:string" use="optional"/>
<xsd:attribute name="cropbottom" type="xsd:string" use="optional"/>
<xsd:attribute name="gain" type="xsd:string" use="optional"/>
<xsd:attribute name="blacklevel" type="xsd:string" use="optional"/>
<xsd:attribute name="gamma" type="xsd:string" use="optional"/>
<xsd:attribute name="grayscale" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="bilevel" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_StrokeAttributes">
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="linestyle" type="ST_StrokeLineStyle" use="optional"/>
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
<xsd:attribute name="joinstyle" type="ST_StrokeJoinStyle" use="optional"/>
<xsd:attribute name="endcap" type="ST_StrokeEndCap" use="optional"/>
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
<xsd:attribute name="filltype" type="ST_FillType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="imageaspect" type="ST_ImageAspect" use="optional"/>
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="startarrow" type="ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="startarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="startarrowlength" type="ST_StrokeArrowLength" use="optional"/>
<xsd:attribute name="endarrow" type="ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="endarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="endarrowlength" type="ST_StrokeArrowLength" use="optional"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:forcedash"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="o:relid"/>
</xsd:attributeGroup>
<xsd:group name="EG_ShapeElements">
<xsd:choice>
<xsd:element ref="path"/>
<xsd:element ref="formulas"/>
<xsd:element ref="handles"/>
<xsd:element ref="fill"/>
<xsd:element ref="stroke"/>
<xsd:element ref="shadow"/>
<xsd:element ref="textbox"/>
<xsd:element ref="textpath"/>
<xsd:element ref="imagedata"/>
<xsd:element ref="o:skew"/>
<xsd:element ref="o:extrusion"/>
<xsd:element ref="o:callout"/>
<xsd:element ref="o:lock"/>
<xsd:element ref="o:clippath"/>
<xsd:element ref="o:signatureline"/>
<xsd:element ref="w10:wrap"/>
<xsd:element ref="w10:anchorlock"/>
<xsd:element ref="w10:bordertop"/>
<xsd:element ref="w10:borderbottom"/>
<xsd:element ref="w10:borderleft"/>
<xsd:element ref="w10:borderright"/>
<xsd:element ref="x:ClientData" minOccurs="0"/>
<xsd:element ref="pvml:textdata" minOccurs="0"/>
</xsd:choice>
</xsd:group>
<xsd:element name="shape" type="CT_Shape"/>
<xsd:element name="shapetype" type="CT_Shapetype"/>
<xsd:element name="group" type="CT_Group"/>
<xsd:element name="background" type="CT_Background"/>
<xsd:complexType name="CT_Shape">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="o:ink"/>
<xsd:element ref="pvml:iscomment"/>
<xsd:element ref="o:equationxml"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_Type"/>
<xsd:attributeGroup ref="AG_Adj"/>
<xsd:attributeGroup ref="AG_Path"/>
<xsd:attribute ref="o:gfxdata"/>
<xsd:attribute name="equationxml" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Shapetype">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="o:complex" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_Adj"/>
<xsd:attributeGroup ref="AG_Path"/>
<xsd:attribute ref="o:master"/>
</xsd:complexType>
<xsd:complexType name="CT_Group">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="group"/>
<xsd:element ref="shape"/>
<xsd:element ref="shapetype"/>
<xsd:element ref="arc"/>
<xsd:element ref="curve"/>
<xsd:element ref="image"/>
<xsd:element ref="line"/>
<xsd:element ref="oval"/>
<xsd:element ref="polyline"/>
<xsd:element ref="rect"/>
<xsd:element ref="roundrect"/>
<xsd:element ref="o:diagram"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute name="editas" type="ST_EditAs" use="optional"/>
<xsd:attribute ref="o:tableproperties"/>
<xsd:attribute ref="o:tablelimits"/>
</xsd:complexType>
<xsd:complexType name="CT_Background">
<xsd:sequence>
<xsd:element ref="fill" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute ref="o:bwmode"/>
<xsd:attribute ref="o:bwpure"/>
<xsd:attribute ref="o:bwnormal"/>
<xsd:attribute ref="o:targetscreensize"/>
</xsd:complexType>
<xsd:element name="fill" type="CT_Fill"/>
<xsd:element name="formulas" type="CT_Formulas"/>
<xsd:element name="handles" type="CT_Handles"/>
<xsd:element name="imagedata" type="CT_ImageData"/>
<xsd:element name="path" type="CT_Path"/>
<xsd:element name="textbox" type="CT_Textbox"/>
<xsd:element name="shadow" type="CT_Shadow"/>
<xsd:element name="stroke" type="CT_Stroke"/>
<xsd:element name="textpath" type="CT_TextPath"/>
<xsd:complexType name="CT_Fill">
<xsd:sequence>
<xsd:element ref="o:fill" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="type" type="ST_FillType" use="optional"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute name="size" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="position" type="xsd:string" use="optional"/>
<xsd:attribute name="aspect" type="ST_ImageAspect" use="optional"/>
<xsd:attribute name="colors" type="xsd:string" use="optional"/>
<xsd:attribute name="angle" type="xsd:decimal" use="optional"/>
<xsd:attribute name="alignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="focus" type="xsd:string" use="optional"/>
<xsd:attribute name="focussize" type="xsd:string" use="optional"/>
<xsd:attribute name="focusposition" type="xsd:string" use="optional"/>
<xsd:attribute name="method" type="ST_FillMethod" use="optional"/>
<xsd:attribute ref="o:detectmouseclick"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:opacity2"/>
<xsd:attribute name="recolor" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotate" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute ref="o:relid" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Formulas">
<xsd:sequence>
<xsd:element name="f" type="CT_F" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_F">
<xsd:attribute name="eqn" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_Handles">
<xsd:sequence>
<xsd:element name="h" type="CT_H" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_H">
<xsd:attribute name="position" type="xsd:string"/>
<xsd:attribute name="polar" type="xsd:string"/>
<xsd:attribute name="map" type="xsd:string"/>
<xsd:attribute name="invx" type="s:ST_TrueFalse"/>
<xsd:attribute name="invy" type="s:ST_TrueFalse"/>
<xsd:attribute name="switch" type="s:ST_TrueFalseBlank"/>
<xsd:attribute name="xrange" type="xsd:string"/>
<xsd:attribute name="yrange" type="xsd:string"/>
<xsd:attribute name="radiusrange" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ImageData">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_ImageAttributes"/>
<xsd:attributeGroup ref="AG_Chromakey"/>
<xsd:attribute name="embosscolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="recolortarget" type="s:ST_ColorType"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:oleid"/>
<xsd:attribute ref="o:detectmouseclick"/>
<xsd:attribute ref="o:movie"/>
<xsd:attribute ref="o:relid"/>
<xsd:attribute ref="r:id"/>
<xsd:attribute ref="r:pict"/>
<xsd:attribute ref="r:href"/>
</xsd:complexType>
<xsd:complexType name="CT_Path">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="v" type="xsd:string" use="optional"/>
<xsd:attribute name="limo" type="xsd:string" use="optional"/>
<xsd:attribute name="textboxrect" type="xsd:string" use="optional"/>
<xsd:attribute name="fillok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokeok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="shadowok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="arrowok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="gradientshapeok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="textpathok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="insetpenok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="o:connecttype"/>
<xsd:attribute ref="o:connectlocs"/>
<xsd:attribute ref="o:connectangles"/>
<xsd:attribute ref="o:extrusionok"/>
</xsd:complexType>
<xsd:complexType name="CT_Shadow">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="ST_ShadowType" use="optional"/>
<xsd:attribute name="obscured" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="offset2" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Stroke">
<xsd:sequence>
<xsd:element ref="o:left" minOccurs="0"/>
<xsd:element ref="o:top" minOccurs="0"/>
<xsd:element ref="o:right" minOccurs="0"/>
<xsd:element ref="o:bottom" minOccurs="0"/>
<xsd:element ref="o:column" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_StrokeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_Textbox">
<xsd:choice>
<xsd:element ref="w:txbxContent" minOccurs="0"/>
<xsd:any namespace="##local" processContents="skip"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="inset" type="xsd:string" use="optional"/>
<xsd:attribute ref="o:singleclick"/>
<xsd:attribute ref="o:insetmode"/>
</xsd:complexType>
<xsd:complexType name="CT_TextPath">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fitshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fitpath" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="trim" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="xscale" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="string" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:element name="arc" type="CT_Arc"/>
<xsd:element name="curve" type="CT_Curve"/>
<xsd:element name="image" type="CT_Image"/>
<xsd:element name="line" type="CT_Line"/>
<xsd:element name="oval" type="CT_Oval"/>
<xsd:element name="polyline" type="CT_PolyLine"/>
<xsd:element name="rect" type="CT_Rect"/>
<xsd:element name="roundrect" type="CT_RoundRect"/>
<xsd:complexType name="CT_Arc">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="startAngle" type="xsd:decimal" use="optional"/>
<xsd:attribute name="endAngle" type="xsd:decimal" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Curve">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="from" type="xsd:string" use="optional"/>
<xsd:attribute name="control1" type="xsd:string" use="optional"/>
<xsd:attribute name="control2" type="xsd:string" use="optional"/>
<xsd:attribute name="to" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Image">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_ImageAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_Line">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="from" type="xsd:string" use="optional"/>
<xsd:attribute name="to" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Oval">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_PolyLine">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="o:ink"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="points" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Rect">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_RoundRect">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="arcsize" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_Ext">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="view"/>
<xsd:enumeration value="edit"/>
<xsd:enumeration value="backwardCompatible"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="solid"/>
<xsd:enumeration value="gradient"/>
<xsd:enumeration value="gradientRadial"/>
<xsd:enumeration value="tile"/>
<xsd:enumeration value="pattern"/>
<xsd:enumeration value="frame"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillMethod">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="linear"/>
<xsd:enumeration value="sigma"/>
<xsd:enumeration value="any"/>
<xsd:enumeration value="linear sigma"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ShadowType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="single"/>
<xsd:enumeration value="double"/>
<xsd:enumeration value="emboss"/>
<xsd:enumeration value="perspective"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeLineStyle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="single"/>
<xsd:enumeration value="thinThin"/>
<xsd:enumeration value="thinThick"/>
<xsd:enumeration value="thickThin"/>
<xsd:enumeration value="thickBetweenThin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeJoinStyle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="round"/>
<xsd:enumeration value="bevel"/>
<xsd:enumeration value="miter"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeEndCap">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="flat"/>
<xsd:enumeration value="square"/>
<xsd:enumeration value="round"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowLength">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="short"/>
<xsd:enumeration value="medium"/>
<xsd:enumeration value="long"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowWidth">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="narrow"/>
<xsd:enumeration value="medium"/>
<xsd:enumeration value="wide"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="block"/>
<xsd:enumeration value="classic"/>
<xsd:enumeration value="oval"/>
<xsd:enumeration value="diamond"/>
<xsd:enumeration value="open"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ImageAspect">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ignore"/>
<xsd:enumeration value="atMost"/>
<xsd:enumeration value="atLeast"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_EditAs">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="canvas"/>
<xsd:enumeration value="orgchart"/>
<xsd:enumeration value="radial"/>
<xsd:enumeration value="cycle"/>
<xsd:enumeration value="stacked"/>
<xsd:enumeration value="venn"/>
<xsd:enumeration value="bullseye"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -0,0 +1,509 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:office:office" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="urn:schemas-microsoft-com:vml" schemaLocation="vml-main.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:attribute name="bwmode" type="ST_BWMode"/>
<xsd:attribute name="bwpure" type="ST_BWMode"/>
<xsd:attribute name="bwnormal" type="ST_BWMode"/>
<xsd:attribute name="targetscreensize" type="ST_ScreenSize"/>
<xsd:attribute name="insetmode" type="ST_InsetMode" default="custom"/>
<xsd:attribute name="spt" type="xsd:float"/>
<xsd:attribute name="wrapcoords" type="xsd:string"/>
<xsd:attribute name="oned" type="s:ST_TrueFalse"/>
<xsd:attribute name="regroupid" type="xsd:integer"/>
<xsd:attribute name="doubleclicknotify" type="s:ST_TrueFalse"/>
<xsd:attribute name="connectortype" type="ST_ConnectorType" default="straight"/>
<xsd:attribute name="button" type="s:ST_TrueFalse"/>
<xsd:attribute name="userhidden" type="s:ST_TrueFalse"/>
<xsd:attribute name="forcedash" type="s:ST_TrueFalse"/>
<xsd:attribute name="oleicon" type="s:ST_TrueFalse"/>
<xsd:attribute name="ole" type="s:ST_TrueFalseBlank"/>
<xsd:attribute name="preferrelative" type="s:ST_TrueFalse"/>
<xsd:attribute name="cliptowrap" type="s:ST_TrueFalse"/>
<xsd:attribute name="clip" type="s:ST_TrueFalse"/>
<xsd:attribute name="bullet" type="s:ST_TrueFalse"/>
<xsd:attribute name="hr" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrstd" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrnoshade" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrpct" type="xsd:float"/>
<xsd:attribute name="hralign" type="ST_HrAlign" default="left"/>
<xsd:attribute name="allowincell" type="s:ST_TrueFalse"/>
<xsd:attribute name="allowoverlap" type="s:ST_TrueFalse"/>
<xsd:attribute name="userdrawn" type="s:ST_TrueFalse"/>
<xsd:attribute name="bordertopcolor" type="xsd:string"/>
<xsd:attribute name="borderleftcolor" type="xsd:string"/>
<xsd:attribute name="borderbottomcolor" type="xsd:string"/>
<xsd:attribute name="borderrightcolor" type="xsd:string"/>
<xsd:attribute name="connecttype" type="ST_ConnectType"/>
<xsd:attribute name="connectlocs" type="xsd:string"/>
<xsd:attribute name="connectangles" type="xsd:string"/>
<xsd:attribute name="master" type="xsd:string"/>
<xsd:attribute name="extrusionok" type="s:ST_TrueFalse"/>
<xsd:attribute name="href" type="xsd:string"/>
<xsd:attribute name="althref" type="xsd:string"/>
<xsd:attribute name="title" type="xsd:string"/>
<xsd:attribute name="singleclick" type="s:ST_TrueFalse"/>
<xsd:attribute name="oleid" type="xsd:float"/>
<xsd:attribute name="detectmouseclick" type="s:ST_TrueFalse"/>
<xsd:attribute name="movie" type="xsd:float"/>
<xsd:attribute name="spid" type="xsd:string"/>
<xsd:attribute name="opacity2" type="xsd:string"/>
<xsd:attribute name="relid" type="r:ST_RelationshipId"/>
<xsd:attribute name="dgmlayout" type="ST_DiagramLayout"/>
<xsd:attribute name="dgmnodekind" type="xsd:integer"/>
<xsd:attribute name="dgmlayoutmru" type="ST_DiagramLayout"/>
<xsd:attribute name="gfxdata" type="xsd:base64Binary"/>
<xsd:attribute name="tableproperties" type="xsd:string"/>
<xsd:attribute name="tablelimits" type="xsd:string"/>
<xsd:element name="shapedefaults" type="CT_ShapeDefaults"/>
<xsd:element name="shapelayout" type="CT_ShapeLayout"/>
<xsd:element name="signatureline" type="CT_SignatureLine"/>
<xsd:element name="ink" type="CT_Ink"/>
<xsd:element name="diagram" type="CT_Diagram"/>
<xsd:element name="equationxml" type="CT_EquationXml"/>
<xsd:complexType name="CT_ShapeDefaults">
<xsd:all minOccurs="0">
<xsd:element ref="v:fill" minOccurs="0"/>
<xsd:element ref="v:stroke" minOccurs="0"/>
<xsd:element ref="v:textbox" minOccurs="0"/>
<xsd:element ref="v:shadow" minOccurs="0"/>
<xsd:element ref="skew" minOccurs="0"/>
<xsd:element ref="extrusion" minOccurs="0"/>
<xsd:element ref="callout" minOccurs="0"/>
<xsd:element ref="lock" minOccurs="0"/>
<xsd:element name="colormru" minOccurs="0" type="CT_ColorMru"/>
<xsd:element name="colormenu" minOccurs="0" type="CT_ColorMenu"/>
</xsd:all>
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="spidmax" type="xsd:integer" use="optional"/>
<xsd:attribute name="style" type="xsd:string" use="optional"/>
<xsd:attribute name="fill" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="stroke" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
<xsd:attribute name="allowincell" form="qualified" type="s:ST_TrueFalse"/>
</xsd:complexType>
<xsd:complexType name="CT_Ink">
<xsd:sequence/>
<xsd:attribute name="i" type="xsd:string"/>
<xsd:attribute name="annotation" type="s:ST_TrueFalse"/>
<xsd:attribute name="contentType" type="ST_ContentType" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_SignatureLine">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="issignatureline" type="s:ST_TrueFalse"/>
<xsd:attribute name="id" type="s:ST_Guid"/>
<xsd:attribute name="provid" type="s:ST_Guid"/>
<xsd:attribute name="signinginstructionsset" type="s:ST_TrueFalse"/>
<xsd:attribute name="allowcomments" type="s:ST_TrueFalse"/>
<xsd:attribute name="showsigndate" type="s:ST_TrueFalse"/>
<xsd:attribute name="suggestedsigner" type="xsd:string" form="qualified"/>
<xsd:attribute name="suggestedsigner2" type="xsd:string" form="qualified"/>
<xsd:attribute name="suggestedsigneremail" type="xsd:string" form="qualified"/>
<xsd:attribute name="signinginstructions" type="xsd:string"/>
<xsd:attribute name="addlxml" type="xsd:string"/>
<xsd:attribute name="sigprovurl" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ShapeLayout">
<xsd:all>
<xsd:element name="idmap" type="CT_IdMap" minOccurs="0"/>
<xsd:element name="regrouptable" type="CT_RegroupTable" minOccurs="0"/>
<xsd:element name="rules" type="CT_Rules" minOccurs="0"/>
</xsd:all>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_IdMap">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="data" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_RegroupTable">
<xsd:sequence>
<xsd:element name="entry" type="CT_Entry" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_Entry">
<xsd:attribute name="new" type="xsd:int" use="optional"/>
<xsd:attribute name="old" type="xsd:int" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Rules">
<xsd:sequence>
<xsd:element name="r" type="CT_R" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_R">
<xsd:sequence>
<xsd:element name="proxy" type="CT_Proxy" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string" use="required"/>
<xsd:attribute name="type" type="ST_RType" use="optional"/>
<xsd:attribute name="how" type="ST_How" use="optional"/>
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Proxy">
<xsd:attribute name="start" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
<xsd:attribute name="end" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
<xsd:attribute name="connectloc" type="xsd:int" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Diagram">
<xsd:sequence>
<xsd:element name="relationtable" type="CT_RelationTable" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="dgmstyle" type="xsd:integer" use="optional"/>
<xsd:attribute name="autoformat" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="reverse" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="autolayout" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="dgmscalex" type="xsd:integer" use="optional"/>
<xsd:attribute name="dgmscaley" type="xsd:integer" use="optional"/>
<xsd:attribute name="dgmfontsize" type="xsd:integer" use="optional"/>
<xsd:attribute name="constrainbounds" type="xsd:string" use="optional"/>
<xsd:attribute name="dgmbasetextscale" type="xsd:integer" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_EquationXml">
<xsd:sequence>
<xsd:any namespace="##any"/>
</xsd:sequence>
<xsd:attribute name="contentType" type="ST_AlternateMathContentType" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_AlternateMathContentType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:complexType name="CT_RelationTable">
<xsd:sequence>
<xsd:element name="rel" type="CT_Relation" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_Relation">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="idsrc" type="xsd:string" use="optional"/>
<xsd:attribute name="iddest" type="xsd:string" use="optional"/>
<xsd:attribute name="idcntr" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_ColorMru">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="colors" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ColorMenu">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType"/>
<xsd:attribute name="shadowcolor" type="s:ST_ColorType"/>
<xsd:attribute name="extrusioncolor" type="s:ST_ColorType"/>
</xsd:complexType>
<xsd:element name="skew" type="CT_Skew"/>
<xsd:element name="extrusion" type="CT_Extrusion"/>
<xsd:element name="callout" type="CT_Callout"/>
<xsd:element name="lock" type="CT_Lock"/>
<xsd:element name="OLEObject" type="CT_OLEObject"/>
<xsd:element name="complex" type="CT_Complex"/>
<xsd:element name="left" type="CT_StrokeChild"/>
<xsd:element name="top" type="CT_StrokeChild"/>
<xsd:element name="right" type="CT_StrokeChild"/>
<xsd:element name="bottom" type="CT_StrokeChild"/>
<xsd:element name="column" type="CT_StrokeChild"/>
<xsd:element name="clippath" type="CT_ClipPath"/>
<xsd:element name="fill" type="CT_Fill"/>
<xsd:complexType name="CT_Skew">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="id" type="xsd:string" use="optional"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Extrusion">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="ST_ExtrusionType" default="parallel" use="optional"/>
<xsd:attribute name="render" type="ST_ExtrusionRender" default="solid" use="optional"/>
<xsd:attribute name="viewpointorigin" type="xsd:string" use="optional"/>
<xsd:attribute name="viewpoint" type="xsd:string" use="optional"/>
<xsd:attribute name="plane" type="ST_ExtrusionPlane" default="XY" use="optional"/>
<xsd:attribute name="skewangle" type="xsd:float" use="optional"/>
<xsd:attribute name="skewamt" type="xsd:string" use="optional"/>
<xsd:attribute name="foredepth" type="xsd:string" use="optional"/>
<xsd:attribute name="backdepth" type="xsd:string" use="optional"/>
<xsd:attribute name="orientation" type="xsd:string" use="optional"/>
<xsd:attribute name="orientationangle" type="xsd:float" use="optional"/>
<xsd:attribute name="lockrotationcenter" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="autorotationcenter" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotationcenter" type="xsd:string" use="optional"/>
<xsd:attribute name="rotationangle" type="xsd:string" use="optional"/>
<xsd:attribute name="colormode" type="ST_ColorMode" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="shininess" type="xsd:float" use="optional"/>
<xsd:attribute name="specularity" type="xsd:string" use="optional"/>
<xsd:attribute name="diffusity" type="xsd:string" use="optional"/>
<xsd:attribute name="metal" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="edge" type="xsd:string" use="optional"/>
<xsd:attribute name="facet" type="xsd:string" use="optional"/>
<xsd:attribute name="lightface" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="brightness" type="xsd:string" use="optional"/>
<xsd:attribute name="lightposition" type="xsd:string" use="optional"/>
<xsd:attribute name="lightlevel" type="xsd:string" use="optional"/>
<xsd:attribute name="lightharsh" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="lightposition2" type="xsd:string" use="optional"/>
<xsd:attribute name="lightlevel2" type="xsd:string" use="optional"/>
<xsd:attribute name="lightharsh2" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Callout">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="xsd:string" use="optional"/>
<xsd:attribute name="gap" type="xsd:string" use="optional"/>
<xsd:attribute name="angle" type="ST_Angle" use="optional"/>
<xsd:attribute name="dropauto" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="drop" type="ST_CalloutDrop" use="optional"/>
<xsd:attribute name="distance" type="xsd:string" use="optional"/>
<xsd:attribute name="lengthspecified" type="s:ST_TrueFalse" default="f" use="optional"/>
<xsd:attribute name="length" type="xsd:string" use="optional"/>
<xsd:attribute name="accentbar" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="textborder" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="minusx" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="minusy" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Lock">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="position" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="selection" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="grouping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="ungrouping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotation" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="cropping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="verticies" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="adjusthandles" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="text" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="aspectratio" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="shapetype" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_OLEObject">
<xsd:sequence>
<xsd:element name="LinkType" type="ST_OLELinkType" minOccurs="0"/>
<xsd:element name="LockedField" type="s:ST_TrueFalseBlank" minOccurs="0"/>
<xsd:element name="FieldCodes" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="Type" type="ST_OLEType" use="optional"/>
<xsd:attribute name="ProgID" type="xsd:string" use="optional"/>
<xsd:attribute name="ShapeID" type="xsd:string" use="optional"/>
<xsd:attribute name="DrawAspect" type="ST_OLEDrawAspect" use="optional"/>
<xsd:attribute name="ObjectID" type="xsd:string" use="optional"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute name="UpdateMode" type="ST_OLEUpdateMode" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Complex">
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_StrokeChild">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="linestyle" type="v:ST_StrokeLineStyle" use="optional"/>
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
<xsd:attribute name="joinstyle" type="v:ST_StrokeJoinStyle" use="optional"/>
<xsd:attribute name="endcap" type="v:ST_StrokeEndCap" use="optional"/>
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="filltype" type="v:ST_FillType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="imageaspect" type="v:ST_ImageAspect" use="optional"/>
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="startarrow" type="v:ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="startarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="startarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
<xsd:attribute name="endarrow" type="v:ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="endarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="endarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
<xsd:attribute ref="href"/>
<xsd:attribute ref="althref"/>
<xsd:attribute ref="title"/>
<xsd:attribute ref="forcedash"/>
</xsd:complexType>
<xsd:complexType name="CT_ClipPath">
<xsd:attribute name="v" type="xsd:string" use="required" form="qualified"/>
</xsd:complexType>
<xsd:complexType name="CT_Fill">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="type" type="ST_FillType"/>
</xsd:complexType>
<xsd:simpleType name="ST_RType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="arc"/>
<xsd:enumeration value="callout"/>
<xsd:enumeration value="connector"/>
<xsd:enumeration value="align"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_How">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="middle"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="right"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_BWMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="color"/>
<xsd:enumeration value="auto"/>
<xsd:enumeration value="grayScale"/>
<xsd:enumeration value="lightGrayscale"/>
<xsd:enumeration value="inverseGray"/>
<xsd:enumeration value="grayOutline"/>
<xsd:enumeration value="highContrast"/>
<xsd:enumeration value="black"/>
<xsd:enumeration value="white"/>
<xsd:enumeration value="hide"/>
<xsd:enumeration value="undrawn"/>
<xsd:enumeration value="blackTextAndLines"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ScreenSize">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="544,376"/>
<xsd:enumeration value="640,480"/>
<xsd:enumeration value="720,512"/>
<xsd:enumeration value="800,600"/>
<xsd:enumeration value="1024,768"/>
<xsd:enumeration value="1152,862"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_InsetMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="auto"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ColorMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="auto"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ContentType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_DiagramLayout">
<xsd:restriction base="xsd:integer">
<xsd:enumeration value="0"/>
<xsd:enumeration value="1"/>
<xsd:enumeration value="2"/>
<xsd:enumeration value="3"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="perspective"/>
<xsd:enumeration value="parallel"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionRender">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="solid"/>
<xsd:enumeration value="wireFrame"/>
<xsd:enumeration value="boundingCube"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionPlane">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="XY"/>
<xsd:enumeration value="ZX"/>
<xsd:enumeration value="YZ"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Angle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="any"/>
<xsd:enumeration value="30"/>
<xsd:enumeration value="45"/>
<xsd:enumeration value="60"/>
<xsd:enumeration value="90"/>
<xsd:enumeration value="auto"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CalloutDrop">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_CalloutPlacement">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="user"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConnectorType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="straight"/>
<xsd:enumeration value="elbow"/>
<xsd:enumeration value="curved"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_HrAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConnectType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="rect"/>
<xsd:enumeration value="segments"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLELinkType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Embed"/>
<xsd:enumeration value="Link"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEDrawAspect">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Content"/>
<xsd:enumeration value="Icon"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEUpdateMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Always"/>
<xsd:enumeration value="OnCall"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="gradientCenter"/>
<xsd:enumeration value="solid"/>
<xsd:enumeration value="pattern"/>
<xsd:enumeration value="tile"/>
<xsd:enumeration value="frame"/>
<xsd:enumeration value="gradientUnscaled"/>
<xsd:enumeration value="gradientRadial"/>
<xsd:enumeration value="gradient"/>
<xsd:enumeration value="background"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:powerpoint"
targetNamespace="urn:schemas-microsoft-com:office:powerpoint" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="iscomment" type="CT_Empty"/>
<xsd:element name="textdata" type="CT_Rel"/>
<xsd:complexType name="CT_Empty"/>
<xsd:complexType name="CT_Rel">
<xsd:attribute name="id" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:excel"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:office:excel" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:element name="ClientData" type="CT_ClientData"/>
<xsd:complexType name="CT_ClientData">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="MoveWithCells" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SizeWithCells" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Anchor" type="xsd:string"/>
<xsd:element name="Locked" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DefaultSize" type="s:ST_TrueFalseBlank"/>
<xsd:element name="PrintObject" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Disabled" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoFill" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoLine" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoPict" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaMacro" type="xsd:string"/>
<xsd:element name="TextHAlign" type="xsd:string"/>
<xsd:element name="TextVAlign" type="xsd:string"/>
<xsd:element name="LockText" type="s:ST_TrueFalseBlank"/>
<xsd:element name="JustLastX" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SecretEdit" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Default" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Help" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Cancel" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Dismiss" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Accel" type="xsd:integer"/>
<xsd:element name="Accel2" type="xsd:integer"/>
<xsd:element name="Row" type="xsd:integer"/>
<xsd:element name="Column" type="xsd:integer"/>
<xsd:element name="Visible" type="s:ST_TrueFalseBlank"/>
<xsd:element name="RowHidden" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ColHidden" type="s:ST_TrueFalseBlank"/>
<xsd:element name="VTEdit" type="xsd:integer"/>
<xsd:element name="MultiLine" type="s:ST_TrueFalseBlank"/>
<xsd:element name="VScroll" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ValidIds" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaRange" type="xsd:string"/>
<xsd:element name="WidthMin" type="xsd:integer"/>
<xsd:element name="Sel" type="xsd:integer"/>
<xsd:element name="NoThreeD2" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SelType" type="xsd:string"/>
<xsd:element name="MultiSel" type="xsd:string"/>
<xsd:element name="LCT" type="xsd:string"/>
<xsd:element name="ListItem" type="xsd:string"/>
<xsd:element name="DropStyle" type="xsd:string"/>
<xsd:element name="Colored" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DropLines" type="xsd:integer"/>
<xsd:element name="Checked" type="xsd:integer"/>
<xsd:element name="FmlaLink" type="xsd:string"/>
<xsd:element name="FmlaPict" type="xsd:string"/>
<xsd:element name="NoThreeD" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FirstButton" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaGroup" type="xsd:string"/>
<xsd:element name="Val" type="xsd:integer"/>
<xsd:element name="Min" type="xsd:integer"/>
<xsd:element name="Max" type="xsd:integer"/>
<xsd:element name="Inc" type="xsd:integer"/>
<xsd:element name="Page" type="xsd:integer"/>
<xsd:element name="Horiz" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Dx" type="xsd:integer"/>
<xsd:element name="MapOCX" type="s:ST_TrueFalseBlank"/>
<xsd:element name="CF" type="ST_CF"/>
<xsd:element name="Camera" type="s:ST_TrueFalseBlank"/>
<xsd:element name="RecalcAlways" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoScale" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DDE" type="s:ST_TrueFalseBlank"/>
<xsd:element name="UIObj" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ScriptText" type="xsd:string"/>
<xsd:element name="ScriptExtended" type="xsd:string"/>
<xsd:element name="ScriptLanguage" type="xsd:nonNegativeInteger"/>
<xsd:element name="ScriptLocation" type="xsd:nonNegativeInteger"/>
<xsd:element name="FmlaTxbx" type="xsd:string"/>
</xsd:choice>
<xsd:attribute name="ObjectType" type="ST_ObjectType" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_CF">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_ObjectType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Button"/>
<xsd:enumeration value="Checkbox"/>
<xsd:enumeration value="Dialog"/>
<xsd:enumeration value="Drop"/>
<xsd:enumeration value="Edit"/>
<xsd:enumeration value="GBox"/>
<xsd:enumeration value="Label"/>
<xsd:enumeration value="LineA"/>
<xsd:enumeration value="List"/>
<xsd:enumeration value="Movie"/>
<xsd:enumeration value="Note"/>
<xsd:enumeration value="Pict"/>
<xsd:enumeration value="Radio"/>
<xsd:enumeration value="RectA"/>
<xsd:enumeration value="Scroll"/>
<xsd:enumeration value="Spin"/>
<xsd:enumeration value="Shape"/>
<xsd:enumeration value="Group"/>
<xsd:enumeration value="Rect"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:word"
targetNamespace="urn:schemas-microsoft-com:office:word" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="bordertop" type="CT_Border"/>
<xsd:element name="borderleft" type="CT_Border"/>
<xsd:element name="borderright" type="CT_Border"/>
<xsd:element name="borderbottom" type="CT_Border"/>
<xsd:complexType name="CT_Border">
<xsd:attribute name="type" type="ST_BorderType" use="optional"/>
<xsd:attribute name="width" type="xsd:positiveInteger" use="optional"/>
<xsd:attribute name="shadow" type="ST_BorderShadow" use="optional"/>
</xsd:complexType>
<xsd:element name="wrap" type="CT_Wrap"/>
<xsd:complexType name="CT_Wrap">
<xsd:attribute name="type" type="ST_WrapType" use="optional"/>
<xsd:attribute name="side" type="ST_WrapSide" use="optional"/>
<xsd:attribute name="anchorx" type="ST_HorizontalAnchor" use="optional"/>
<xsd:attribute name="anchory" type="ST_VerticalAnchor" use="optional"/>
</xsd:complexType>
<xsd:element name="anchorlock" type="CT_AnchorLock"/>
<xsd:complexType name="CT_AnchorLock"/>
<xsd:simpleType name="ST_BorderType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="single"/>
<xsd:enumeration value="thick"/>
<xsd:enumeration value="double"/>
<xsd:enumeration value="hairline"/>
<xsd:enumeration value="dot"/>
<xsd:enumeration value="dash"/>
<xsd:enumeration value="dotDash"/>
<xsd:enumeration value="dashDotDot"/>
<xsd:enumeration value="triple"/>
<xsd:enumeration value="thinThickSmall"/>
<xsd:enumeration value="thickThinSmall"/>
<xsd:enumeration value="thickBetweenThinSmall"/>
<xsd:enumeration value="thinThick"/>
<xsd:enumeration value="thickThin"/>
<xsd:enumeration value="thickBetweenThin"/>
<xsd:enumeration value="thinThickLarge"/>
<xsd:enumeration value="thickThinLarge"/>
<xsd:enumeration value="thickBetweenThinLarge"/>
<xsd:enumeration value="wave"/>
<xsd:enumeration value="doubleWave"/>
<xsd:enumeration value="dashedSmall"/>
<xsd:enumeration value="dashDotStroked"/>
<xsd:enumeration value="threeDEmboss"/>
<xsd:enumeration value="threeDEngrave"/>
<xsd:enumeration value="HTMLOutset"/>
<xsd:enumeration value="HTMLInset"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_BorderShadow">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="false"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_WrapType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="topAndBottom"/>
<xsd:enumeration value="square"/>
<xsd:enumeration value="none"/>
<xsd:enumeration value="tight"/>
<xsd:enumeration value="through"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_WrapSide">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="both"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="largest"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_HorizontalAnchor">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="text"/>
<xsd:enumeration value="char"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_VerticalAnchor">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="text"/>
<xsd:enumeration value="line"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
<?xml version='1.0'?>
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xml:lang="en">
<xs:annotation>
<xs:documentation>
See http://www.w3.org/XML/1998/namespace.html and
http://www.w3.org/TR/REC-xml for information about this namespace.
This schema document describes the XML namespace, in a form
suitable for import by other schema documents.
Note that local names in this namespace are intended to be defined
only by the World Wide Web Consortium or its subgroups. The
following names are currently defined in this namespace and should
not be used with conflicting semantics by any Working Group,
specification, or document instance:
base (as an attribute name): denotes an attribute whose value
provides a URI to be used as the base for interpreting any
relative URIs in the scope of the element on which it
appears; its value is inherited. This name is reserved
by virtue of its definition in the XML Base specification.
lang (as an attribute name): denotes an attribute whose value
is a language code for the natural language of the content of
any element; its value is inherited. This name is reserved
by virtue of its definition in the XML specification.
space (as an attribute name): denotes an attribute whose
value is a keyword indicating what whitespace processing
discipline is intended for the content of the element; its
value is inherited. This name is reserved by virtue of its
definition in the XML specification.
Father (in any context at all): denotes Jon Bosak, the chair of
the original XML Working Group. This name is reserved by
the following decision of the W3C XML Plenary and
XML Coordination groups:
In appreciation for his vision, leadership and dedication
the W3C XML Plenary on this 10th day of February, 2000
reserves for Jon Bosak in perpetuity the XML name
xml:Father
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>This schema defines attributes and an attribute group
suitable for use by
schemas wishing to allow xml:base, xml:lang or xml:space attributes
on elements they define.
To enable this, such a schema must import this schema
for the XML namespace, e.g. as follows:
&lt;schema . . .>
. . .
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
Subsequently, qualified reference to any of the attributes
or the group defined below will have the desired effect, e.g.
&lt;type . . .>
. . .
&lt;attributeGroup ref="xml:specialAttrs"/>
will define a type which will schema-validate an instance
element with any of those attributes</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>In keeping with the XML Schema WG's standard versioning
policy, this schema document will persist at
http://www.w3.org/2001/03/xml.xsd.
At the date of issue it can also be found at
http://www.w3.org/2001/xml.xsd.
The schema document at that URI may however change in the future,
in order to remain compatible with the latest version of XML Schema
itself. In other words, if the XML Schema namespace changes, the version
of this document at
http://www.w3.org/2001/xml.xsd will change
accordingly; the version at
http://www.w3.org/2001/03/xml.xsd will not change.
</xs:documentation>
</xs:annotation>
<xs:attribute name="lang" type="xs:language">
<xs:annotation>
<xs:documentation>In due course, we should install the relevant ISO 2- and 3-letter
codes as the enumerated possible values . . .</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="space" default="preserve">
<xs:simpleType>
<xs:restriction base="xs:NCName">
<xs:enumeration value="default"/>
<xs:enumeration value="preserve"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="base" type="xs:anyURI">
<xs:annotation>
<xs:documentation>See http://www.w3.org/TR/xmlbase/ for
information about this attribute.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup name="specialAttrs">
<xs:attribute ref="xml:base"/>
<xs:attribute ref="xml:lang"/>
<xs:attribute ref="xml:space"/>
</xs:attributeGroup>
</xs:schema>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.openxmlformats.org/package/2006/content-types"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://schemas.openxmlformats.org/package/2006/content-types"
elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all">
<xs:element name="Types" type="CT_Types"/>
<xs:element name="Default" type="CT_Default"/>
<xs:element name="Override" type="CT_Override"/>
<xs:complexType name="CT_Types">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="Default"/>
<xs:element ref="Override"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="CT_Default">
<xs:attribute name="Extension" type="ST_Extension" use="required"/>
<xs:attribute name="ContentType" type="ST_ContentType" use="required"/>
</xs:complexType>
<xs:complexType name="CT_Override">
<xs:attribute name="ContentType" type="ST_ContentType" use="required"/>
<xs:attribute name="PartName" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:simpleType name="ST_ContentType">
<xs:restriction base="xs:string">
<xs:pattern
value="(((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))/((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))((\s+)*;(\s+)*(((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))=((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+)|(&quot;(([\p{IsLatin-1Supplement}\p{IsBasicLatin}-[\p{Cc}&#127;&quot;\n\r]]|(\s+))|(\\[\p{IsBasicLatin}]))*&quot;))))*)"
/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_Extension">
<xs:restriction base="xs:string">
<xs:pattern
value="([!$&amp;'\(\)\*\+,:=]|(%[0-9a-fA-F][0-9a-fA-F])|[:@]|[a-zA-Z0-9\-_~])+"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" blockDefault="#all">
<xs:import namespace="http://purl.org/dc/elements/1.1/"
schemaLocation="http://dublincore.org/schemas/xmls/qdc/2003/04/02/dc.xsd"/>
<xs:import namespace="http://purl.org/dc/terms/"
schemaLocation="http://dublincore.org/schemas/xmls/qdc/2003/04/02/dcterms.xsd"/>
<xs:import id="xml" namespace="http://www.w3.org/XML/1998/namespace"/>
<xs:element name="coreProperties" type="CT_CoreProperties"/>
<xs:complexType name="CT_CoreProperties">
<xs:all>
<xs:element name="category" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="contentStatus" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element ref="dcterms:created" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:creator" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:description" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:identifier" minOccurs="0" maxOccurs="1"/>
<xs:element name="keywords" minOccurs="0" maxOccurs="1" type="CT_Keywords"/>
<xs:element ref="dc:language" minOccurs="0" maxOccurs="1"/>
<xs:element name="lastModifiedBy" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="lastPrinted" minOccurs="0" maxOccurs="1" type="xs:dateTime"/>
<xs:element ref="dcterms:modified" minOccurs="0" maxOccurs="1"/>
<xs:element name="revision" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element ref="dc:subject" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:title" minOccurs="0" maxOccurs="1"/>
<xs:element name="version" minOccurs="0" maxOccurs="1" type="xs:string"/>
</xs:all>
</xs:complexType>
<xs:complexType name="CT_Keywords" mixed="true">
<xs:sequence>
<xs:element name="value" minOccurs="0" maxOccurs="unbounded" type="CT_Keyword"/>
</xs:sequence>
<xs:attribute ref="xml:lang" use="optional"/>
</xs:complexType>
<xs:complexType name="CT_Keyword">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute ref="xml:lang" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:schema>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://schemas.openxmlformats.org/package/2006/digital-signature"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://schemas.openxmlformats.org/package/2006/digital-signature"
elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all">
<xsd:element name="SignatureTime" type="CT_SignatureTime"/>
<xsd:element name="RelationshipReference" type="CT_RelationshipReference"/>
<xsd:element name="RelationshipsGroupReference" type="CT_RelationshipsGroupReference"/>
<xsd:complexType name="CT_SignatureTime">
<xsd:sequence>
<xsd:element name="Format" type="ST_Format"/>
<xsd:element name="Value" type="ST_Value"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RelationshipReference">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="SourceId" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="CT_RelationshipsGroupReference">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="SourceType" type="xsd:anyURI" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:simpleType name="ST_Format">
<xsd:restriction base="xsd:string">
<xsd:pattern
value="(YYYY)|(YYYY-MM)|(YYYY-MM-DD)|(YYYY-MM-DDThh:mmTZD)|(YYYY-MM-DDThh:mm:ssTZD)|(YYYY-MM-DDThh:mm:ss.sTZD)"
/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Value">
<xsd:restriction base="xsd:string">
<xsd:pattern
value="(([0-9][0-9][0-9][0-9]))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2))))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1))))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])):(((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))\.[0-9])(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))"
/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

Some files were not shown because too many files have changed in this diff Show More