mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
Compare commits
4 Commits
feat/slide
...
sun/lark-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c992e8ec | ||
|
|
f0b6f35fee | ||
|
|
91d785f92f | ||
|
|
e621c6e50f |
@@ -65,13 +65,13 @@ func NewCmdSchema(f *cmdutil.Factory, runF func(*SchemaOptions) error) *cobra.Co
|
||||
return cmd
|
||||
}
|
||||
|
||||
// completeSchemaPath is a thin adapter over the embedded catalog's Complete.
|
||||
// It uses the embedded source so completion candidates match what `schema`
|
||||
// execution can resolve (both overlay-free).
|
||||
// completeSchemaPath is a thin adapter over the schema catalog's Complete.
|
||||
// It uses the same source as schema execution so completion candidates match
|
||||
// what `schema` can resolve.
|
||||
func completeSchemaPath(f *cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
mode := f.ResolveStrictMode(cmd.Context())
|
||||
completions, noSpace := registry.EmbeddedCatalog().Complete(args, toComplete, registry.FilterForStrictMode(mode))
|
||||
completions, noSpace := registry.SchemaCatalog().Complete(args, toComplete, registry.FilterForStrictMode(mode))
|
||||
directive := cobra.ShellCompDirectiveNoFileComp
|
||||
if noSpace {
|
||||
directive |= cobra.ShellCompDirectiveNoSpace
|
||||
@@ -86,13 +86,19 @@ func schemaRun(opts *SchemaOptions) error {
|
||||
return runSchema(out, apicatalog.ParsePath(opts.Args), mode)
|
||||
}
|
||||
|
||||
// runSchema resolves the path through the embedded catalog and renders the
|
||||
// runSchema resolves the path through the schema catalog and renders the
|
||||
// matching envelope(s). The catalog owns navigation (Resolve + MethodRefs) and
|
||||
// schema owns rendering (Envelope/Envelopes); this adapter only chooses the
|
||||
// output shape — a single resolved method renders as one envelope object,
|
||||
// anything broader as an array — and maps resolve failures to hints.
|
||||
func runSchema(out io.Writer, parts []string, mode core.StrictMode) error {
|
||||
catalog := registry.EmbeddedCatalog()
|
||||
catalog := registry.SchemaCatalog()
|
||||
if len(catalog.Services()) == 0 {
|
||||
// No embedded metadata and the runtime fallback is empty too: offline
|
||||
// with a cold cache, remote meta off, or an unwritable cache dir.
|
||||
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "No API metadata available").
|
||||
WithHint("this binary has no embedded API metadata; run any command with network access to the open platform once so metadata can be fetched and cached")
|
||||
}
|
||||
target, err := catalog.Resolve(parts)
|
||||
if err != nil {
|
||||
return resolveError(err)
|
||||
|
||||
@@ -102,7 +102,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
Long: `Update lark-cli to the latest version.
|
||||
|
||||
Detects the installation method automatically:
|
||||
- npm install: runs npm install -g @larksuite/cli@<version>
|
||||
- npm install: runs npm install -g @larksuite/cli@<version>
|
||||
- pnpm install: runs pnpm add -g @larksuite/cli@<version>
|
||||
- manual/other: shows GitHub Releases download URL
|
||||
|
||||
Use --json for structured output (for AI agents and scripts).
|
||||
@@ -164,7 +165,7 @@ func updateRun(opts *UpdateOptions) error {
|
||||
if !detect.CanAutoUpdate() {
|
||||
return doManualUpdate(opts, io, cur, latest, detect, updater)
|
||||
}
|
||||
return doNpmUpdate(opts, io, cur, latest, updater)
|
||||
return doAutoUpdate(opts, io, cur, latest, detect, updater)
|
||||
}
|
||||
|
||||
// --- Output helpers ---
|
||||
@@ -226,12 +227,23 @@ func doManualUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest stri
|
||||
fmt.Fprintf(io.ErrOut, "To update manually, download the latest release:\n")
|
||||
fmt.Fprintf(io.ErrOut, " Release: %s\n", releaseURL(latest))
|
||||
fmt.Fprintf(io.ErrOut, " Changelog: %s\n", changelogURL())
|
||||
fmt.Fprintf(io.ErrOut, "\nOr install via npm (note: skills will not be synced):\n npm install -g %s@%s\n npx skills add larksuite/cli -y -g # sync skills separately\n", selfupdate.NpmPackage, latest)
|
||||
if detect.Method == selfupdate.InstallPnpm {
|
||||
fmt.Fprintf(io.ErrOut, "\nOr install via pnpm (note: skills will not be synced):\n pnpm add -g %s@%s\n pnpm dlx skills add larksuite/cli -y -g # sync skills separately\n", selfupdate.NpmPackage, latest)
|
||||
} else {
|
||||
fmt.Fprintf(io.ErrOut, "\nOr install via npm (note: skills will not be synced):\n npm install -g %s@%s\n npx skills add larksuite/cli -y -g # sync skills separately\n", selfupdate.NpmPackage, latest)
|
||||
}
|
||||
emitSkillsTextHints(io, skillsResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, updater *selfupdate.Updater) error {
|
||||
func doAutoUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, detect selfupdate.DetectResult, updater *selfupdate.Updater) error {
|
||||
pm := "npm"
|
||||
install := updater.RunNpmInstall
|
||||
if detect.Method == selfupdate.InstallPnpm {
|
||||
pm = "pnpm"
|
||||
install = updater.RunPnpmInstall
|
||||
}
|
||||
|
||||
restore, err := updater.PrepareSelfReplace()
|
||||
if err != nil {
|
||||
return reportError(opts, io, "update_error",
|
||||
@@ -239,19 +251,19 @@ func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string,
|
||||
}
|
||||
|
||||
if !opts.JSON {
|
||||
fmt.Fprintf(io.ErrOut, "Updating lark-cli %s %s %s via npm ...\n", cur, symArrow(), latest)
|
||||
fmt.Fprintf(io.ErrOut, "Updating lark-cli %s %s %s via %s ...\n", cur, symArrow(), latest, pm)
|
||||
}
|
||||
|
||||
npmResult := updater.RunNpmInstall(latest)
|
||||
npmResult := install(latest)
|
||||
if npmResult.Err != nil {
|
||||
restore()
|
||||
combined := npmResult.CombinedOutput()
|
||||
if opts.JSON {
|
||||
output.PrintJson(io.Out, map[string]interface{}{
|
||||
"ok": false, "error": map[string]interface{}{
|
||||
"type": "update_error", "message": fmt.Sprintf("npm install failed: %s", npmResult.Err),
|
||||
"type": "update_error", "message": fmt.Sprintf("%s install failed: %s", pm, npmResult.Err),
|
||||
"detail": selfupdate.Truncate(combined, maxNpmOutput),
|
||||
"hint": permissionHint(combined),
|
||||
"hint": permissionHint(combined, pm),
|
||||
},
|
||||
})
|
||||
return output.ErrBare(output.ExitAPI)
|
||||
@@ -263,7 +275,7 @@ func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string,
|
||||
fmt.Fprint(io.ErrOut, npmResult.Stderr.String())
|
||||
}
|
||||
fmt.Fprintf(io.ErrOut, "\n%s Update failed: %s\n", symFail(), npmResult.Err)
|
||||
if hint := permissionHint(combined); hint != "" {
|
||||
if hint := permissionHint(combined, pm); hint != "" {
|
||||
fmt.Fprintf(io.ErrOut, " %s\n", hint)
|
||||
}
|
||||
return output.ErrBare(output.ExitAPI)
|
||||
@@ -274,7 +286,7 @@ func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string,
|
||||
if err := updater.VerifyBinary(latest); err != nil {
|
||||
restore()
|
||||
msg := fmt.Sprintf("new binary verification failed: %s", err)
|
||||
hint := verificationFailureHint(updater, latest)
|
||||
hint := verificationFailureHint(updater, latest, pm)
|
||||
if opts.JSON {
|
||||
output.PrintJson(io.Out, map[string]interface{}{
|
||||
"ok": false,
|
||||
@@ -304,23 +316,33 @@ func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string,
|
||||
fmt.Fprintf(io.ErrOut, "\n%s Successfully updated lark-cli from %s to %s\n", symOK(), cur, latest)
|
||||
fmt.Fprintf(io.ErrOut, " Changelog: %s\n", changelogURL())
|
||||
if skillsResult != nil {
|
||||
fmt.Fprintf(io.ErrOut, "\nUpdating skills ...\n")
|
||||
skillsPM := "npx"
|
||||
if detect.Method == selfupdate.InstallPnpm && detect.PnpmAvailable {
|
||||
skillsPM = "pnpm dlx"
|
||||
}
|
||||
fmt.Fprintf(io.ErrOut, "\nUpdating skills via %s ...\n", skillsPM)
|
||||
}
|
||||
emitSkillsTextHints(io, skillsResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
func permissionHint(npmOutput string) string {
|
||||
if strings.Contains(npmOutput, "EACCES") && !isWindows() {
|
||||
return "Permission denied. Try: sudo lark-cli update, or adjust your npm global prefix: https://docs.npmjs.com/resolving-eacces-permissions-errors"
|
||||
func permissionHint(pmOutput, pm string) string {
|
||||
if !strings.Contains(pmOutput, "EACCES") || isWindows() {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
if pm == "pnpm" {
|
||||
return "Permission denied. Ensure your pnpm global directory is writable — re-run `pnpm setup`, or see https://pnpm.io/pnpm-cli"
|
||||
}
|
||||
return "Permission denied. Try: sudo lark-cli update, or adjust your npm global prefix: https://docs.npmjs.com/resolving-eacces-permissions-errors"
|
||||
}
|
||||
|
||||
func verificationFailureHint(updater *selfupdate.Updater, latest string) string {
|
||||
func verificationFailureHint(updater *selfupdate.Updater, latest, pm string) string {
|
||||
if updater.CanRestorePreviousVersion() {
|
||||
return "the previous version has been restored"
|
||||
}
|
||||
if pm == "pnpm" {
|
||||
return fmt.Sprintf("automatic rollback is unavailable on this platform; reinstall manually (skills will not be synced): pnpm add -g %s@%s && pnpm dlx skills add larksuite/cli -y -g, or download %s", selfupdate.NpmPackage, latest, releaseURL(latest))
|
||||
}
|
||||
return fmt.Sprintf("automatic rollback is unavailable on this platform; reinstall manually (skills will not be synced): npm install -g %s@%s && npx skills add larksuite/cli -y -g, or download %s", selfupdate.NpmPackage, latest, releaseURL(latest))
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,27 @@ func mockDetectAndNpm(t *testing.T, result selfupdate.DetectResult, npmFn func(s
|
||||
t.Cleanup(func() { newUpdater = origNew })
|
||||
}
|
||||
|
||||
// mockDetectAndPnpm mirrors mockDetectAndNpm but wires the pnpm install path
|
||||
// and fails the test if the npm install path is invoked.
|
||||
func mockDetectAndPnpm(t *testing.T, result selfupdate.DetectResult, pnpmFn func(string) *selfupdate.NpmResult) {
|
||||
t.Helper()
|
||||
origNew := newUpdater
|
||||
newUpdater = func() *selfupdate.Updater {
|
||||
u := selfupdate.New()
|
||||
u.DetectOverride = func() selfupdate.DetectResult { return result }
|
||||
u.PnpmInstallOverride = pnpmFn
|
||||
u.NpmInstallOverride = func(string) *selfupdate.NpmResult {
|
||||
t.Errorf("npm install must not be called for a pnpm install")
|
||||
return &selfupdate.NpmResult{}
|
||||
}
|
||||
u.VerifyOverride = func(string) error { return nil }
|
||||
u.SkillsIndexFetchOverride = successfulSkillsIndexFetch()
|
||||
u.SkillsCommandOverride = successfulSkillsCommand()
|
||||
return u
|
||||
}
|
||||
t.Cleanup(func() { newUpdater = origNew })
|
||||
}
|
||||
|
||||
func successfulSkillsIndexFetch() func() *selfupdate.NpmResult {
|
||||
return func() *selfupdate.NpmResult {
|
||||
r := &selfupdate.NpmResult{}
|
||||
@@ -81,6 +102,110 @@ func successfulSkillsCommand() func(args ...string) *selfupdate.NpmResult {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePnpm_JSON(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, stdout, _ := newTestFactory(t)
|
||||
cmd := NewCmdUpdate(f)
|
||||
cmd.SetArgs([]string{"--json"})
|
||||
origFetch := fetchLatest
|
||||
fetchLatest = func() (string, error) { return "2.0.0", nil }
|
||||
defer func() { fetchLatest = origFetch }()
|
||||
origVersion := currentVersion
|
||||
currentVersion = func() string { return "1.0.0" }
|
||||
defer func() { currentVersion = origVersion }()
|
||||
mockDetectAndPnpm(t,
|
||||
selfupdate.DetectResult{Method: selfupdate.InstallPnpm, ResolvedPath: "/x/node_modules/.pnpm/@larksuite+cli@1.0.0/node_modules/@larksuite/cli/bin/lark-cli", PnpmAvailable: true},
|
||||
func(string) *selfupdate.NpmResult { return &selfupdate.NpmResult{} },
|
||||
)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out := stdout.String(); !strings.Contains(out, `"action": "updated"`) {
|
||||
t.Errorf("expected updated in output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePnpm_Human(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, stderr := newTestFactory(t)
|
||||
cmd := NewCmdUpdate(f)
|
||||
cmd.SetArgs([]string{})
|
||||
origFetch := fetchLatest
|
||||
fetchLatest = func() (string, error) { return "2.0.0", nil }
|
||||
defer func() { fetchLatest = origFetch }()
|
||||
origVersion := currentVersion
|
||||
currentVersion = func() string { return "1.0.0" }
|
||||
defer func() { currentVersion = origVersion }()
|
||||
mockDetectAndPnpm(t,
|
||||
selfupdate.DetectResult{Method: selfupdate.InstallPnpm, ResolvedPath: "/x/node_modules/.pnpm/@larksuite+cli@1.0.0/node_modules/@larksuite/cli/bin/lark-cli", PnpmAvailable: true},
|
||||
func(string) *selfupdate.NpmResult { return &selfupdate.NpmResult{} },
|
||||
)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
out := stderr.String()
|
||||
if !strings.Contains(out, "via pnpm") {
|
||||
t.Errorf("expected 'via pnpm' in stderr, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Updating skills via pnpm dlx ...") {
|
||||
t.Errorf("expected skills sync to report pnpm dlx launcher, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Successfully updated") {
|
||||
t.Errorf("expected success message, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePnpm_InstallError_JSON(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, stdout, _ := newTestFactory(t)
|
||||
cmd := NewCmdUpdate(f)
|
||||
cmd.SetArgs([]string{"--json"})
|
||||
origFetch := fetchLatest
|
||||
fetchLatest = func() (string, error) { return "2.0.0", nil }
|
||||
defer func() { fetchLatest = origFetch }()
|
||||
origVersion := currentVersion
|
||||
currentVersion = func() string { return "1.0.0" }
|
||||
defer func() { currentVersion = origVersion }()
|
||||
mockDetectAndPnpm(t,
|
||||
selfupdate.DetectResult{Method: selfupdate.InstallPnpm, ResolvedPath: "/x/node_modules/.pnpm/@larksuite+cli@1.0.0/node_modules/@larksuite/cli/bin/lark-cli", PnpmAvailable: true},
|
||||
func(string) *selfupdate.NpmResult { return &selfupdate.NpmResult{Err: errors.New("pnpm boom")} },
|
||||
)
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error exit")
|
||||
}
|
||||
if out := stdout.String(); !strings.Contains(out, `"ok": false`) || !strings.Contains(out, "update_error") {
|
||||
t.Errorf("expected failure envelope, got: %s", out)
|
||||
}
|
||||
if out := stdout.String(); !strings.Contains(out, "pnpm install failed") {
|
||||
t.Errorf("expected message to report pnpm as the package manager, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePnpm_Unavailable_ManualFallback(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, stderr := newTestFactory(t)
|
||||
cmd := NewCmdUpdate(f)
|
||||
cmd.SetArgs([]string{})
|
||||
origFetch := fetchLatest
|
||||
fetchLatest = func() (string, error) { return "2.0.0", nil }
|
||||
defer func() { fetchLatest = origFetch }()
|
||||
origVersion := currentVersion
|
||||
currentVersion = func() string { return "1.0.0" }
|
||||
defer func() { currentVersion = origVersion }()
|
||||
mockDetect(t, selfupdate.DetectResult{Method: selfupdate.InstallPnpm, ResolvedPath: "/x/node_modules/.pnpm/@larksuite+cli@1.0.0/node_modules/@larksuite/cli/bin/lark-cli", PnpmAvailable: false})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
out := stderr.String()
|
||||
if !strings.Contains(out, "installed via pnpm, but pnpm is not available in PATH") {
|
||||
t.Errorf("expected pnpm manual reason, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "pnpm add -g") {
|
||||
t.Errorf("expected pnpm add -g hint, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
@@ -266,6 +391,9 @@ func TestUpdateNpm_Human(t *testing.T) {
|
||||
if !strings.Contains(out, "Successfully updated") {
|
||||
t.Errorf("expected success message in stderr, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Updating skills via npx ...") {
|
||||
t.Errorf("expected skills sync to report npx launcher for npm install, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateForce_JSON(t *testing.T) {
|
||||
@@ -739,9 +867,9 @@ func TestPermissionHint(t *testing.T) {
|
||||
origOS := currentOS
|
||||
defer func() { currentOS = origOS }()
|
||||
|
||||
// Linux: EACCES should produce a hint with npm prefix guidance.
|
||||
// Linux + npm: EACCES should produce a hint with npm prefix guidance.
|
||||
currentOS = "linux"
|
||||
hint := permissionHint("EACCES: permission denied, access '/usr/local/lib'")
|
||||
hint := permissionHint("EACCES: permission denied, access '/usr/local/lib'", "npm")
|
||||
if !strings.Contains(hint, "npm global prefix") {
|
||||
t.Errorf("expected npm prefix hint on linux, got: %s", hint)
|
||||
}
|
||||
@@ -749,16 +877,25 @@ func TestPermissionHint(t *testing.T) {
|
||||
t.Errorf("should not suggest raw sudo npm install, got: %s", hint)
|
||||
}
|
||||
|
||||
// Linux + pnpm: EACCES should point at pnpm setup, not npm prefix/sudo.
|
||||
pnpmHint := permissionHint("EACCES: permission denied, access '/Users/x/Library/pnpm'", "pnpm")
|
||||
if !strings.Contains(pnpmHint, "pnpm setup") {
|
||||
t.Errorf("expected pnpm setup hint, got: %s", pnpmHint)
|
||||
}
|
||||
if strings.Contains(pnpmHint, "npm global prefix") || strings.Contains(pnpmHint, "sudo") {
|
||||
t.Errorf("pnpm hint must not reference npm prefix or sudo, got: %s", pnpmHint)
|
||||
}
|
||||
|
||||
// Windows: EACCES hint is suppressed (no EACCES on Windows).
|
||||
currentOS = "windows"
|
||||
hint = permissionHint("EACCES: permission denied")
|
||||
hint = permissionHint("EACCES: permission denied", "npm")
|
||||
if hint != "" {
|
||||
t.Errorf("expected empty hint on Windows, got: %s", hint)
|
||||
}
|
||||
|
||||
// Non-EACCES error: always empty.
|
||||
currentOS = "linux"
|
||||
if got := permissionHint("some other error"); got != "" {
|
||||
if got := permissionHint("some other error", "npm"); got != "" {
|
||||
t.Errorf("expected empty hint for non-EACCES, got: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,14 +77,10 @@ func loadService(service string) map[string]json.RawMessage {
|
||||
// space→dot fallback covers domains where the two already coincide.
|
||||
func commandFormResolver(service string) func(string) string {
|
||||
byForm := map[string]string{}
|
||||
for _, svc := range registry.EmbeddedServicesTyped() {
|
||||
if svc.Name != service {
|
||||
continue
|
||||
}
|
||||
if svc, ok := registry.SchemaCatalog().Service(service); ok {
|
||||
for _, ref := range apicatalog.ServiceMethods(svc, nil) {
|
||||
byForm[strings.Join(ref.CommandPath()[1:], " ")] = ref.Method.ID
|
||||
}
|
||||
break
|
||||
}
|
||||
return func(h string) string {
|
||||
h = strings.TrimSpace(h)
|
||||
|
||||
@@ -6,12 +6,10 @@ package cmdutil
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
@@ -40,8 +38,6 @@ const (
|
||||
BuildKindUnknown = "unknown"
|
||||
|
||||
officialModulePath = "github.com/larksuite/cli"
|
||||
|
||||
agentTraceMaxLen = 1024
|
||||
)
|
||||
|
||||
// UserAgentValue returns the User-Agent value: "lark-cli/{version}".
|
||||
@@ -49,25 +45,6 @@ func UserAgentValue() string {
|
||||
return SourceValue + "/" + build.Version
|
||||
}
|
||||
|
||||
// AgentTraceValue returns a header-safe value from the
|
||||
// LARKSUITE_CLI_AGENT_TRACE environment variable. It trims
|
||||
// surrounding whitespace, rejects values containing any Unicode
|
||||
// control character or exceeding agentTraceMaxLen, and returns ""
|
||||
// for any invalid or empty value. Callers can use the result
|
||||
// directly in HTTP headers without further sanitisation.
|
||||
func AgentTraceValue() string {
|
||||
v := strings.TrimSpace(os.Getenv(envvars.CliAgentTrace))
|
||||
if v == "" || len(v) > agentTraceMaxLen {
|
||||
return ""
|
||||
}
|
||||
for _, r := range v {
|
||||
if unicode.IsControl(r) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// BaseSecurityHeaders returns headers that every request must carry.
|
||||
func BaseSecurityHeaders() http.Header {
|
||||
h := make(http.Header)
|
||||
@@ -75,7 +52,7 @@ func BaseSecurityHeaders() http.Header {
|
||||
h.Set(HeaderVersion, build.Version)
|
||||
h.Set(HeaderBuild, DetectBuildKind())
|
||||
h.Set(HeaderUserAgent, UserAgentValue())
|
||||
if v := AgentTraceValue(); v != "" {
|
||||
if v := envvars.AgentTrace(); v != "" {
|
||||
h.Set(HeaderAgentTrace, v)
|
||||
}
|
||||
return h
|
||||
|
||||
@@ -6,7 +6,6 @@ package cmdutil
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/extension/credential"
|
||||
@@ -264,88 +263,9 @@ func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentTraceValue / HeaderAgentTrace
|
||||
// HeaderAgentTrace injection (via BaseSecurityHeaders)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestAgentTraceValue_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "trace-abc-123")
|
||||
if got := AgentTraceValue(); got != "trace-abc-123" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %q", got, "trace-abc-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, " trace-trim ")
|
||||
if got := AgentTraceValue(); got != "trace-trim" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %q (whitespace trimmed)", got, "trace-trim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_OnlyWhitespace_ReturnsEmpty(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, " ")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for whitespace-only value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsCRLF(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\r\nX-Evil: attack")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsLF(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\nX-Evil: attack")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsTab(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\tinjected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for tab value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\x01injected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsDEL(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\x7finjected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for DEL value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentTraceMaxLen+1)
|
||||
t.Setenv(envvars.CliAgentTrace, longVal)
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_AcceptsMaxLengthValue(t *testing.T) {
|
||||
val := strings.Repeat("a", agentTraceMaxLen)
|
||||
t.Setenv(envvars.CliAgentTrace, val)
|
||||
if got := AgentTraceValue(); got != val {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %d-byte value accepted", got, agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseSecurityHeaders_NoAgentTraceHeaderWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "")
|
||||
h := BaseSecurityHeaders()
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
// Content safety scanning mode
|
||||
CliContentSafetyMode = "LARKSUITE_CLI_CONTENT_SAFETY_MODE"
|
||||
|
||||
CliAgentName = "LARKSUITE_CLI_AGENT_NAME"
|
||||
CliAgentTrace = "LARKSUITE_CLI_AGENT_TRACE"
|
||||
|
||||
CliProxyEnable = "LARKSUITE_CLI_PROXY_ENABLE"
|
||||
|
||||
36
internal/envvars/read.go
Normal file
36
internal/envvars/read.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envvars
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
agentNameMaxLen = 128
|
||||
agentTraceMaxLen = 1024
|
||||
)
|
||||
|
||||
func AgentName() string {
|
||||
return sanitizeSingleLine(os.Getenv(CliAgentName), agentNameMaxLen)
|
||||
}
|
||||
|
||||
func AgentTrace() string {
|
||||
return sanitizeSingleLine(os.Getenv(CliAgentTrace), agentTraceMaxLen)
|
||||
}
|
||||
|
||||
func sanitizeSingleLine(raw string, maxLen int) string {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" || len(v) > maxLen {
|
||||
return ""
|
||||
}
|
||||
for _, r := range v {
|
||||
if unicode.IsControl(r) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
131
internal/envvars/read_test.go
Normal file
131
internal/envvars/read_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envvars
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgentName_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "claude-code")
|
||||
if got := AgentName(); got != "claude-code" {
|
||||
t.Fatalf("AgentName() = %q, want %q", got, "claude-code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(CliAgentName, " cursor ")
|
||||
if got := AgentName(); got != "cursor" {
|
||||
t.Fatalf("AgentName() = %q, want %q (whitespace trimmed)", got, "cursor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsCRLFInjection(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "agent\r\nX-Evil: attack")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "agent\x01injected")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentNameMaxLen+1)
|
||||
t.Setenv(CliAgentName, longVal)
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() returned non-empty for %d-byte value (max %d)", len(longVal), agentNameMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "trace-abc-123")
|
||||
if got := AgentTrace(); got != "trace-abc-123" {
|
||||
t.Fatalf("AgentTrace() = %q, want %q", got, "trace-abc-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, " trace-trim ")
|
||||
if got := AgentTrace(); got != "trace-trim" {
|
||||
t.Fatalf("AgentTrace() = %q, want %q (whitespace trimmed)", got, "trace-trim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_OnlyWhitespace_ReturnsEmpty(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, " ")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for whitespace-only value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsCRLF(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\r\nX-Evil: attack")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsLF(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\nX-Evil: attack")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsTab(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\tinjected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for tab value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\x01injected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsDEL(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\x7finjected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for DEL value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentTraceMaxLen+1)
|
||||
t.Setenv(CliAgentTrace, longVal)
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_AcceptsMaxLengthValue(t *testing.T) {
|
||||
val := strings.Repeat("a", agentTraceMaxLen)
|
||||
t.Setenv(CliAgentTrace, val)
|
||||
if got := AgentTrace(); got != val {
|
||||
t.Fatalf("AgentTrace() = %q, want %d-byte value accepted", got, agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ package registry
|
||||
import "github.com/larksuite/cli/internal/apicatalog"
|
||||
|
||||
// EmbeddedCatalog returns a navigation catalog over the embedded (overlay-free)
|
||||
// metadata — deterministic across machines, for `lark-cli schema`, golden tests
|
||||
// and schema lint.
|
||||
// metadata — deterministic across machines, for golden tests and schema lint.
|
||||
func EmbeddedCatalog() apicatalog.Catalog {
|
||||
return apicatalog.New(apicatalog.SourceEmbedded, EmbeddedServicesTyped())
|
||||
}
|
||||
@@ -18,3 +17,14 @@ func EmbeddedCatalog() apicatalog.Catalog {
|
||||
func RuntimeCatalog() apicatalog.Catalog {
|
||||
return apicatalog.New(apicatalog.SourceRuntime, ServicesTyped())
|
||||
}
|
||||
|
||||
// SchemaCatalog returns the embedded catalog when metadata is compiled in,
|
||||
// otherwise the merged runtime catalog. Binaries built from the bare Go module
|
||||
// embed only the empty meta_data_default.json stub, so the embedded view has
|
||||
// nothing to resolve; the merged view is the only data such binaries have.
|
||||
func SchemaCatalog() apicatalog.Catalog {
|
||||
if len(EmbeddedServicesTyped()) > 0 {
|
||||
return EmbeddedCatalog()
|
||||
}
|
||||
return RuntimeCatalog()
|
||||
}
|
||||
|
||||
67
internal/registry/catalog_test.go
Normal file
67
internal/registry/catalog_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/apicatalog"
|
||||
)
|
||||
|
||||
// swapEmbeddedMeta replaces the compiled-in metadata bytes for one test and
|
||||
// restores them (with a full state reset) on cleanup.
|
||||
func swapEmbeddedMeta(t *testing.T, data []byte) {
|
||||
t.Helper()
|
||||
resetInit()
|
||||
orig := embeddedMetaJSON
|
||||
embeddedMetaJSON = data
|
||||
t.Cleanup(func() {
|
||||
waitBackgroundRefresh()
|
||||
embeddedMetaJSON = orig
|
||||
resetInit()
|
||||
})
|
||||
}
|
||||
|
||||
func TestSchemaCatalog_EmbeddedWhenCompiledIn(t *testing.T) {
|
||||
swapEmbeddedMeta(t, testCacheJSON("embedded_svc"))
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "off")
|
||||
|
||||
c := SchemaCatalog()
|
||||
|
||||
if c.Source() != apicatalog.SourceEmbedded {
|
||||
t.Fatalf("Source = %q, want %q", c.Source(), apicatalog.SourceEmbedded)
|
||||
}
|
||||
if _, ok := c.Service("embedded_svc"); !ok {
|
||||
t.Fatal("expected embedded_svc from embedded metadata")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSchemaCatalog_FallsBackToRuntimeWhenNoEmbedded simulates a binary built
|
||||
// from the bare Go module (plugin builds): only the empty meta_data_default.json
|
||||
// stub is compiled in, so SchemaCatalog must serve the merged runtime view that
|
||||
// Init seeds via sync fetch.
|
||||
func TestSchemaCatalog_FallsBackToRuntimeWhenNoEmbedded(t *testing.T) {
|
||||
swapEmbeddedMeta(t, embeddedMetaDataDefaultJSON)
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
w.Write(testEnvelopeJSON("remote_svc"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
testMetaURL = ts.URL
|
||||
|
||||
c := SchemaCatalog()
|
||||
|
||||
if c.Source() != apicatalog.SourceRuntime {
|
||||
t.Fatalf("Source = %q, want %q", c.Source(), apicatalog.SourceRuntime)
|
||||
}
|
||||
if _, ok := c.Service("remote_svc"); !ok {
|
||||
t.Fatal("expected remote_svc from runtime fallback")
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/meta"
|
||||
"github.com/larksuite/cli/internal/update"
|
||||
)
|
||||
|
||||
//go:embed scope_priorities.json scope_overrides.json
|
||||
@@ -85,7 +86,9 @@ func InitWithBrand(brand core.LarkBrand) {
|
||||
brandChanged := metaErr == nil && cm.Brand != "" && cm.Brand != string(brand)
|
||||
|
||||
if !brandChanged {
|
||||
if cached, err := loadCachedMerged(); err == nil {
|
||||
// After a CLI upgrade the embedded data can be fresher than an old
|
||||
// cache; an equal/older cache must not shadow it.
|
||||
if cached, err := loadCachedMerged(); err == nil && update.IsNewer(cached.Version, embeddedVersion) {
|
||||
overlayMergedServices(cached)
|
||||
}
|
||||
}
|
||||
|
||||
102
internal/registry/loader_test.go
Normal file
102
internal/registry/loader_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/meta"
|
||||
)
|
||||
|
||||
// seedCache writes a cache file + cache meta for one service whose Title is
|
||||
// marker, tagged with the given top-level data version and brand.
|
||||
func seedCache(t *testing.T, dir, name, marker, version, brand string) {
|
||||
t.Helper()
|
||||
cDir := filepath.Join(dir, "cache")
|
||||
if err := os.MkdirAll(cDir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reg := MergedRegistry{
|
||||
Version: version,
|
||||
Services: []meta.Service{{Name: name, Version: "cache", Title: marker}},
|
||||
}
|
||||
data, _ := json.Marshal(reg)
|
||||
if err := os.WriteFile(filepath.Join(cDir, "remote_meta.json"), data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cm := CacheMeta{LastCheckAt: time.Now().Unix(), Version: version, Brand: brand}
|
||||
mData, _ := json.Marshal(cm)
|
||||
if err := os.WriteFile(filepath.Join(cDir, "remote_meta.meta.json"), mData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// initWithCache runs a fresh feishu-brand init with remote on, a high TTL and a
|
||||
// recent LastCheckAt (so no refresh fires), embedded meta at embeddedVer and a
|
||||
// pre-seeded cache at cacheVer — the overlay version gate is the only variable.
|
||||
func initWithCache(t *testing.T, embeddedVer, cacheVer string) {
|
||||
t.Helper()
|
||||
embedded, _ := json.Marshal(MergedRegistry{
|
||||
Version: embeddedVer,
|
||||
Services: []meta.Service{{Name: "svc", Version: "embedded", Title: "EMBEDDED"}},
|
||||
})
|
||||
swapEmbeddedMeta(t, embedded)
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", tmp)
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
t.Setenv("LARKSUITE_CLI_META_TTL", "3600")
|
||||
seedCache(t, tmp, "svc", "CACHE", cacheVer, "feishu")
|
||||
InitWithBrand(core.BrandFeishu)
|
||||
}
|
||||
|
||||
func titleOf(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
svc, ok := ServiceTyped(name)
|
||||
if !ok {
|
||||
t.Fatalf("service %q not loaded", name)
|
||||
}
|
||||
return svc.Title
|
||||
}
|
||||
|
||||
func TestOverlayGate_EqualVersion_UsesEmbedded(t *testing.T) {
|
||||
initWithCache(t, "1.0.0", "1.0.0")
|
||||
if got := titleOf(t, "svc"); got != "EMBEDDED" {
|
||||
t.Errorf("equal version: got %q, want EMBEDDED (cache must not overlay)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayGate_OlderCache_UsesEmbedded(t *testing.T) {
|
||||
initWithCache(t, "2.0.0", "1.0.0")
|
||||
if got := titleOf(t, "svc"); got != "EMBEDDED" {
|
||||
t.Errorf("older cache: got %q, want EMBEDDED", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayGate_NewerCache_OverlaysCache(t *testing.T) {
|
||||
initWithCache(t, "1.0.0", "2.0.0")
|
||||
if got := titleOf(t, "svc"); got != "CACHE" {
|
||||
t.Errorf("newer cache: got %q, want CACHE", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayGate_UnparseableCacheVersion_UsesEmbedded(t *testing.T) {
|
||||
initWithCache(t, "1.0.0", "not-a-semver")
|
||||
if got := titleOf(t, "svc"); got != "EMBEDDED" {
|
||||
t.Errorf("unparseable cache version: got %q, want EMBEDDED", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayGate_StubEmbedded_OverlaysRealCache(t *testing.T) {
|
||||
// The bare-module stub baseline is "0.0.0"; a real cache version must win so
|
||||
// plugin builds without compiled meta_data.json still get remote data.
|
||||
initWithCache(t, "0.0.0", "1.0.0")
|
||||
if got := titleOf(t, "svc"); got != "CACHE" {
|
||||
t.Errorf("stub-embedded baseline: got %q, want CACHE", got)
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,11 @@ func hasEmbeddedServices() bool {
|
||||
}
|
||||
|
||||
// testRegistry returns a minimal MergedRegistry with one service.
|
||||
// The version is a real semver newer than the embedded stub baseline ("0.0.0")
|
||||
// so cache overlay passes the version gate in InitWithBrand.
|
||||
func testRegistry(name string) MergedRegistry {
|
||||
return MergedRegistry{
|
||||
Version: "test-1.0",
|
||||
Version: "1.0.0",
|
||||
Services: []meta.Service{
|
||||
{
|
||||
Name: name,
|
||||
@@ -160,7 +162,7 @@ func TestRemoteOff_SkipsRemoteLogic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCacheHit_WithinTTL(t *testing.T) {
|
||||
resetInit()
|
||||
swapEmbeddedMeta(t, nil) // overlay must depend only on the cache version, not the ambient embedded meta
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", tmp)
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
@@ -197,7 +199,7 @@ func TestCacheHit_WithinTTL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNetworkError_SilentDegradation(t *testing.T) {
|
||||
resetInit()
|
||||
swapEmbeddedMeta(t, nil) // overlay must depend only on the cache version, not the ambient embedded meta
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", tmp)
|
||||
t.Setenv("LARKSUITE_CLI_REMOTE_META", "on")
|
||||
@@ -371,8 +373,8 @@ func TestFetchRemoteMerged_200(t *testing.T) {
|
||||
if data == nil {
|
||||
t.Fatal("expected non-nil data")
|
||||
}
|
||||
if reg.Version != "test-1.0" {
|
||||
t.Errorf("expected version test-1.0, got %s", reg.Version)
|
||||
if reg.Version != "1.0.0" {
|
||||
t.Errorf("expected version 1.0.0, got %s", reg.Version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ type InstallMethod int
|
||||
|
||||
const (
|
||||
InstallNpm InstallMethod = iota
|
||||
InstallPnpm
|
||||
InstallManual
|
||||
)
|
||||
|
||||
@@ -53,22 +54,32 @@ var (
|
||||
|
||||
// DetectResult holds installation detection results.
|
||||
type DetectResult struct {
|
||||
Method InstallMethod
|
||||
ResolvedPath string
|
||||
NpmAvailable bool
|
||||
Method InstallMethod
|
||||
ResolvedPath string
|
||||
NpmAvailable bool
|
||||
PnpmAvailable bool
|
||||
}
|
||||
|
||||
// CanAutoUpdate returns true if the CLI can update itself automatically.
|
||||
func (d DetectResult) CanAutoUpdate() bool {
|
||||
return d.Method == InstallNpm && d.NpmAvailable
|
||||
switch d.Method {
|
||||
case InstallNpm:
|
||||
return d.NpmAvailable
|
||||
case InstallPnpm:
|
||||
return d.PnpmAvailable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ManualReason returns a human-readable explanation of why auto-update is unavailable.
|
||||
func (d DetectResult) ManualReason() string {
|
||||
if d.Method == InstallNpm && !d.NpmAvailable {
|
||||
switch {
|
||||
case d.Method == InstallNpm && !d.NpmAvailable:
|
||||
return "installed via npm, but npm is not available in PATH"
|
||||
case d.Method == InstallPnpm && !d.PnpmAvailable:
|
||||
return "installed via pnpm, but pnpm is not available in PATH"
|
||||
}
|
||||
return "not installed via npm"
|
||||
return "not installed via npm or pnpm"
|
||||
}
|
||||
|
||||
// NpmResult holds the result of an npm install or skills update execution.
|
||||
@@ -92,6 +103,7 @@ func (r *NpmResult) CombinedOutput() string {
|
||||
type Updater struct {
|
||||
DetectOverride func() DetectResult
|
||||
NpmInstallOverride func(version string) *NpmResult
|
||||
PnpmInstallOverride func(version string) *NpmResult
|
||||
SkillsIndexFetchOverride func() *NpmResult
|
||||
SkillsCommandOverride func(args ...string) *NpmResult
|
||||
VerifyOverride func(expectedVersion string) error
|
||||
@@ -101,17 +113,38 @@ type Updater struct {
|
||||
// running binary is successfully renamed to .old. Used by
|
||||
// CanRestorePreviousVersion to report whether rollback is possible.
|
||||
backupCreated bool
|
||||
|
||||
// detectCache memoizes the first real DetectInstallMethod result. How this
|
||||
// binary was installed cannot change during a single process, so caching is
|
||||
// the correct semantics — and it is required for correctness: the update
|
||||
// flow mutates the install (pnpm add -g / npm install -g) before syncing
|
||||
// skills, so a re-detection at skills time could resolve a now-stale
|
||||
// os.Executable path and misclassify. Seeded pre-update by the first call
|
||||
// (updateRun), it keeps the post-update skills launcher consistent with the
|
||||
// launcher reported to the user. Not goroutine-safe; the update flow is
|
||||
// sequential.
|
||||
detectCache *DetectResult
|
||||
}
|
||||
|
||||
// New creates an Updater with default (real) behavior.
|
||||
func New() *Updater { return &Updater{} }
|
||||
|
||||
// DetectInstallMethod determines how the CLI was installed and whether
|
||||
// npm is available for auto-update.
|
||||
// DetectInstallMethod determines how the CLI was installed and whether the
|
||||
// owning package manager is available for auto-update.
|
||||
func (u *Updater) DetectInstallMethod() DetectResult {
|
||||
if u.DetectOverride != nil {
|
||||
return u.DetectOverride()
|
||||
}
|
||||
if u.detectCache != nil {
|
||||
return *u.detectCache
|
||||
}
|
||||
result := u.detectInstallMethod()
|
||||
u.detectCache = &result
|
||||
return result
|
||||
}
|
||||
|
||||
// detectInstallMethod performs the real (uncached) detection.
|
||||
func (u *Updater) detectInstallMethod() DetectResult {
|
||||
exe, err := vfs.Executable()
|
||||
if err != nil {
|
||||
return DetectResult{Method: InstallManual}
|
||||
@@ -120,24 +153,54 @@ func (u *Updater) DetectInstallMethod() DetectResult {
|
||||
if err != nil {
|
||||
return DetectResult{Method: InstallManual, ResolvedPath: exe}
|
||||
}
|
||||
_, npmErr := exec.LookPath("npm")
|
||||
_, pnpmErr := exec.LookPath("pnpm")
|
||||
return detectFromResolved(resolved, npmErr == nil, pnpmErr == nil)
|
||||
}
|
||||
|
||||
// detectFromResolved classifies the resolved binary path into an install
|
||||
// method and records package-manager availability. Split out from
|
||||
// DetectInstallMethod so the classification is unit-testable without touching
|
||||
// the filesystem or PATH.
|
||||
func detectFromResolved(resolved string, npmOnPath, pnpmOnPath bool) DetectResult {
|
||||
method := InstallManual
|
||||
if strings.Contains(resolved, "node_modules") {
|
||||
method = InstallNpm
|
||||
}
|
||||
|
||||
npmAvailable := false
|
||||
if method == InstallNpm {
|
||||
if _, err := exec.LookPath("npm"); err == nil {
|
||||
npmAvailable = true
|
||||
if containsPnpmMarker(resolved) {
|
||||
method = InstallPnpm
|
||||
} else {
|
||||
method = InstallNpm
|
||||
}
|
||||
}
|
||||
|
||||
return DetectResult{
|
||||
Method: method,
|
||||
ResolvedPath: resolved,
|
||||
NpmAvailable: npmAvailable,
|
||||
d := DetectResult{Method: method, ResolvedPath: resolved}
|
||||
switch method {
|
||||
case InstallNpm:
|
||||
d.NpmAvailable = npmOnPath
|
||||
case InstallPnpm:
|
||||
d.PnpmAvailable = pnpmOnPath
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// containsPnpmMarker reports whether the resolved binary path belongs to a
|
||||
// pnpm-managed install. pnpm exposes two layouts: the classic virtual store
|
||||
// (a ".pnpm" directory segment) and the global content-addressable store,
|
||||
// whose resolved path runs through pnpm's home directory (e.g.
|
||||
// "~/Library/pnpm/store/v11/links/...") — a "pnpm" segment immediately
|
||||
// followed by "store". Matching only these two shapes (rather than any bare
|
||||
// "pnpm" segment) avoids misclassifying an npm install that merely lives under
|
||||
// a directory named "pnpm". Windows separators are normalized to "/" so the
|
||||
// classification is OS-independent and unit-testable anywhere.
|
||||
func containsPnpmMarker(p string) bool {
|
||||
parts := strings.Split(strings.ReplaceAll(p, `\`, "/"), "/")
|
||||
for i, part := range parts {
|
||||
if part == ".pnpm" {
|
||||
return true
|
||||
}
|
||||
if part == "pnpm" && i+1 < len(parts) && parts[i+1] == "store" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RunNpmInstall executes npm install -g @larksuite/cli@<version>.
|
||||
@@ -163,6 +226,29 @@ func (u *Updater) RunNpmInstall(version string) *NpmResult {
|
||||
return r
|
||||
}
|
||||
|
||||
// RunPnpmInstall executes pnpm add -g @larksuite/cli@<version>.
|
||||
func (u *Updater) RunPnpmInstall(version string) *NpmResult {
|
||||
if u.PnpmInstallOverride != nil {
|
||||
return u.PnpmInstallOverride(version)
|
||||
}
|
||||
r := &NpmResult{}
|
||||
pnpmPath, err := exec.LookPath("pnpm")
|
||||
if err != nil {
|
||||
r.Err = fmt.Errorf("pnpm not found in PATH: %w", err)
|
||||
return r
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), npmInstallTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, pnpmPath, "add", "-g", NpmPackage+"@"+version)
|
||||
cmd.Stdout = &r.Stdout
|
||||
cmd.Stderr = &r.Stderr
|
||||
r.Err = cmd.Run()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
r.Err = fmt.Errorf("pnpm install timed out after %s", npmInstallTimeout)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (u *Updater) ListOfficialSkillsIndex() *NpmResult {
|
||||
if u.SkillsIndexFetchOverride != nil {
|
||||
return u.SkillsIndexFetchOverride()
|
||||
@@ -261,19 +347,40 @@ func (u *Updater) runSkillsInstall(source string, nameList []string) *NpmResult
|
||||
return u.runSkillsCommand(args...)
|
||||
}
|
||||
|
||||
// skillsInvocation decides how to launch the `skills` CLI. When the lark-cli
|
||||
// itself was installed via pnpm and pnpm is available, it uses `pnpm dlx` so
|
||||
// pnpm-only environments (pnpm's standalone installer bundles Node without
|
||||
// putting npm/npx on PATH) can still sync skills after a self-update.
|
||||
// Otherwise it uses `npx`. The npx auto-confirm flag "-y", when present as the
|
||||
// leading arg, maps to `pnpm dlx`'s default non-interactive behavior and is
|
||||
// dropped for the pnpm launcher. Kept pure (no exec/PATH access) so the
|
||||
// launcher selection is unit-testable on any platform.
|
||||
func skillsInvocation(method InstallMethod, pnpmAvailable bool, args []string) (launcher string, rest []string) {
|
||||
if method == InstallPnpm && pnpmAvailable {
|
||||
r := args
|
||||
if len(r) > 0 && r[0] == "-y" {
|
||||
r = r[1:]
|
||||
}
|
||||
return "pnpm", append([]string{"dlx"}, r...)
|
||||
}
|
||||
return "npx", args
|
||||
}
|
||||
|
||||
func (u *Updater) runSkillsCommand(args ...string) *NpmResult {
|
||||
if u.SkillsCommandOverride != nil {
|
||||
return u.SkillsCommandOverride(args...)
|
||||
}
|
||||
r := &NpmResult{}
|
||||
npxPath, err := exec.LookPath("npx")
|
||||
det := u.DetectInstallMethod()
|
||||
launcher, cmdArgs := skillsInvocation(det.Method, det.PnpmAvailable, args)
|
||||
binPath, err := exec.LookPath(launcher)
|
||||
if err != nil {
|
||||
r.Err = fmt.Errorf("npx not found in PATH: %w", err)
|
||||
r.Err = fmt.Errorf("%s not found in PATH: %w", launcher, err)
|
||||
return r
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), skillsUpdateTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, npxPath, args...)
|
||||
cmd := exec.CommandContext(ctx, binPath, cmdArgs...)
|
||||
cmd.Stdout = &r.Stdout
|
||||
cmd.Stderr = &r.Stderr
|
||||
r.Err = cmd.Run()
|
||||
|
||||
@@ -371,3 +371,147 @@ func TestListOfficialSkillsFallsBack(t *testing.T) {
|
||||
t.Fatalf("fallback call = %q, want larksuite/cli --list", called[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsPnpmMarker(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
// Classic virtual-store layout (.pnpm segment).
|
||||
{"/Users/x/Library/pnpm/global/5/node_modules/.pnpm/@larksuite+cli@1.0.44/node_modules/@larksuite/cli/bin/lark-cli", true},
|
||||
{`C:\Users\x\AppData\Local\pnpm\global\5\node_modules\.pnpm\@larksuite+cli@1.0.44\node_modules\@larksuite\cli\bin\lark-cli.exe`, true},
|
||||
// Global content-addressable store layout (pnpm 11): resolved path runs
|
||||
// through the pnpm home store, a "pnpm" segment with no ".pnpm".
|
||||
{"/Users/x/Library/pnpm/store/v11/links/@larksuite/cli/1.0.59/abc123/node_modules/@larksuite/cli/bin/lark-cli", true},
|
||||
{"/home/x/.local/share/pnpm/store/v10/@larksuite/cli/node_modules/@larksuite/cli/bin/lark-cli", true},
|
||||
{`C:\Users\x\AppData\Local\pnpm\store\v11\links\@larksuite\cli\node_modules\@larksuite\cli\bin\lark-cli.exe`, true},
|
||||
// npm and non-package installs — no pnpm/.pnpm segment.
|
||||
{"/usr/local/lib/node_modules/@larksuite/cli/bin/lark-cli", false},
|
||||
{"/usr/local/bin/lark-cli", false},
|
||||
// Substrings that must NOT match: segment must be exactly .pnpm, or
|
||||
// "pnpm" immediately followed by "store".
|
||||
{"/opt/homebrew/.pnpmfoo/node_modules/@larksuite/cli/bin/lark-cli", false},
|
||||
{"/opt/pnpmfoo/node_modules/@larksuite/cli/bin/lark-cli", false},
|
||||
// A bare "pnpm" directory NOT followed by "store" (e.g. an npm install
|
||||
// living under a dir named pnpm) must not be misclassified as pnpm.
|
||||
{"/opt/pnpm/lib/node_modules/@larksuite/cli/bin/lark-cli", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := containsPnpmMarker(c.path); got != c.want {
|
||||
t.Errorf("containsPnpmMarker(%q) = %v, want %v", c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectInstallMethod_Pnpm(t *testing.T) {
|
||||
u := &Updater{DetectOverride: nil}
|
||||
u.DetectOverride = func() DetectResult {
|
||||
// Exercise the real classification by feeding a resolved path via a small shim.
|
||||
return detectFromResolved("/x/node_modules/.pnpm/@larksuite+cli@1.0.44/node_modules/@larksuite/cli/bin/lark-cli", true, true)
|
||||
}
|
||||
got := u.DetectInstallMethod()
|
||||
if got.Method != InstallPnpm {
|
||||
t.Errorf("Method = %v, want InstallPnpm", got.Method)
|
||||
}
|
||||
if !got.PnpmAvailable {
|
||||
t.Errorf("PnpmAvailable = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectInstallMethod_NpmVsManual(t *testing.T) {
|
||||
if m := detectFromResolved("/usr/local/lib/node_modules/@larksuite/cli/bin/lark-cli", true, false).Method; m != InstallNpm {
|
||||
t.Errorf("npm path Method = %v, want InstallNpm", m)
|
||||
}
|
||||
if m := detectFromResolved("/usr/local/bin/lark-cli", false, false).Method; m != InstallManual {
|
||||
t.Errorf("manual path Method = %v, want InstallManual", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAutoUpdate_Pnpm(t *testing.T) {
|
||||
if !(DetectResult{Method: InstallPnpm, PnpmAvailable: true}).CanAutoUpdate() {
|
||||
t.Error("pnpm available should CanAutoUpdate")
|
||||
}
|
||||
if (DetectResult{Method: InstallPnpm, PnpmAvailable: false}).CanAutoUpdate() {
|
||||
t.Error("pnpm unavailable should not CanAutoUpdate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualReason_Pnpm(t *testing.T) {
|
||||
if got := (DetectResult{Method: InstallPnpm, NpmAvailable: false, PnpmAvailable: false}).ManualReason(); got != "installed via pnpm, but pnpm is not available in PATH" {
|
||||
t.Errorf("pnpm reason = %q", got)
|
||||
}
|
||||
if got := (DetectResult{Method: InstallManual}).ManualReason(); got != "not installed via npm or pnpm" {
|
||||
t.Errorf("manual reason = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPnpmInstall_Override(t *testing.T) {
|
||||
u := &Updater{PnpmInstallOverride: func(version string) *NpmResult {
|
||||
r := &NpmResult{}
|
||||
r.Stdout.WriteString("added @larksuite/cli@" + version)
|
||||
return r
|
||||
}}
|
||||
got := u.RunPnpmInstall("2.0.0")
|
||||
if got.Err != nil {
|
||||
t.Fatalf("unexpected err: %v", got.Err)
|
||||
}
|
||||
if !strings.Contains(got.CombinedOutput(), "2.0.0") {
|
||||
t.Errorf("output = %q, want version echoed", got.CombinedOutput())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPnpmInstall_Error(t *testing.T) {
|
||||
wantErr := errors.New("boom")
|
||||
u := &Updater{PnpmInstallOverride: func(string) *NpmResult { return &NpmResult{Err: wantErr} }}
|
||||
if got := u.RunPnpmInstall("2.0.0"); !errors.Is(got.Err, wantErr) {
|
||||
t.Errorf("err = %v, want %v", got.Err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillsInvocation(t *testing.T) {
|
||||
addArgs := []string{"-y", "skills", "add", "https://open.feishu.cn", "-g", "-y"}
|
||||
cases := []struct {
|
||||
name string
|
||||
method InstallMethod
|
||||
pnpmAvailable bool
|
||||
args []string
|
||||
wantLauncher string
|
||||
wantRest []string
|
||||
}{
|
||||
{"pnpm install + pnpm available → pnpm dlx, drop leading -y", InstallPnpm, true, addArgs,
|
||||
"pnpm", []string{"dlx", "skills", "add", "https://open.feishu.cn", "-g", "-y"}},
|
||||
{"pnpm install but pnpm unavailable → npx unchanged", InstallPnpm, false, addArgs,
|
||||
"npx", addArgs},
|
||||
{"npm install → npx unchanged", InstallNpm, false, addArgs,
|
||||
"npx", addArgs},
|
||||
{"manual install → npx unchanged", InstallManual, false, []string{"-y", "skills", "ls", "-g"},
|
||||
"npx", []string{"-y", "skills", "ls", "-g"}},
|
||||
{"pnpm without a leading -y → prepend dlx only", InstallPnpm, true, []string{"skills", "ls", "-g"},
|
||||
"pnpm", []string{"dlx", "skills", "ls", "-g"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
gotLauncher, gotRest := skillsInvocation(c.method, c.pnpmAvailable, c.args)
|
||||
if gotLauncher != c.wantLauncher {
|
||||
t.Errorf("launcher = %q, want %q", gotLauncher, c.wantLauncher)
|
||||
}
|
||||
if strings.Join(gotRest, " ") != strings.Join(c.wantRest, " ") {
|
||||
t.Errorf("rest = %v, want %v", gotRest, c.wantRest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDetectInstallMethod_Caches locks the fix for the post-update re-detection
|
||||
// hazard: DetectInstallMethod must return the first (pre-update) detection on
|
||||
// subsequent calls, so the skills launcher chosen after the binary is replaced
|
||||
// stays consistent with what was detected — and reported — before the update.
|
||||
func TestDetectInstallMethod_Caches(t *testing.T) {
|
||||
u := New()
|
||||
cached := DetectResult{Method: InstallPnpm, PnpmAvailable: true, ResolvedPath: "/x/pnpm/store/v11/links/@larksuite/cli/1.0.0/node_modules/@larksuite/cli/bin/lark-cli"}
|
||||
u.detectCache = &cached
|
||||
got := u.DetectInstallMethod()
|
||||
if got.Method != InstallPnpm || !got.PnpmAvailable {
|
||||
t.Errorf("expected cached pnpm result to be returned, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,25 +21,38 @@ lark-cli docs +update --doc "文档URL或token" --command append --content '<p>
|
||||
|
||||
## 前置条件 — 执行操作前必读
|
||||
|
||||
**CRITICAL — 执行对应操作前,MUST 先用 Read 工具读取以下文件,缺一不可:**
|
||||
1. [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) — 认证、权限处理、全局参数(所有操作通用)
|
||||
2. **读取文档(`docs +fetch`)** → 必读 [`lark-doc-fetch.md`](references/lark-doc-fetch.md)(`--scope` / `--detail` 选择、局部读取策略、`<fragment>` / `<excerpt>` 输出结构)
|
||||
3. **创建或编辑文档内容** → 必读 [`lark-doc-xml.md`](references/lark-doc-xml.md)(XML 语法规则,仅当用户明确要求 Markdown 时改读 [`lark-doc-md.md`](references/lark-doc-md.md))和必读 [`lark-doc-style.md`](references/style/lark-doc-style.md)(写作原则:默认段落、按体裁、组件克制);从零创建时加读 [`lark-doc-create-workflow.md`](references/style/lark-doc-create-workflow.md);编辑已有文档时加读 [`lark-doc-update.md`](references/lark-doc-update.md) 和 [`lark-doc-update-workflow.md`](references/style/lark-doc-update-workflow.md)
|
||||
**CRITICAL — 按任务命中以下阅读规则;执行对应操作前,MUST 先用 Read 工具读取命中的文件,缺一不可:**
|
||||
|
||||
**未读完以上文件就执行相应操作会导致参数选择错误或格式错误。**
|
||||
1. **所有文档操作** → 必读 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md)
|
||||
用于认证、权限处理、身份选择和全局参数。
|
||||
2. **读取文档(`docs +fetch`)** → 必读 [`lark-doc-fetch.md`](references/lark-doc-fetch.md)
|
||||
用于选择 `--scope` / `--detail`、局部读取策略,以及理解 `<fragment>` / `<excerpt>` 输出结构。
|
||||
3. **创建文档**(从零创建 / 导入)→ 按顺序读取:
|
||||
1. [`lark-doc-design-philosophy.md`](references/lark-doc-design-philosophy.md)。
|
||||
2. [`lark-doc-create.md`](references/lark-doc-create.md)。
|
||||
3. 格式文件二选一:默认读 [`lark-doc-xml.md`](references/lark-doc-xml.md);用户明确 Markdown、提供 `.md` 文件或要求导入 Markdown 时,改读 [`lark-doc-md.md`](references/lark-doc-md.md)。
|
||||
4. **编辑已有文档** → 按顺序读取:
|
||||
1. 先判断是否需要 Philosophy:简单替换、错别字、日期 / 人名 / 数字替换不读;重写、润色、排版、补正文 / 图表、调结构、保真改写时,加读 [`lark-doc-design-philosophy.md`](references/lark-doc-design-philosophy.md),并按需只读一个体裁 router。
|
||||
2. [`lark-doc-update.md`](references/lark-doc-update.md)。
|
||||
3. 需要读取现状或写后验证时,按第 2 条读取 [`lark-doc-fetch.md`](references/lark-doc-fetch.md)。
|
||||
4. 格式文件二选一:默认读 [`lark-doc-xml.md`](references/lark-doc-xml.md);用户明确 Markdown、提供 `.md` 文件或要求写入 Markdown 时,改读 [`lark-doc-md.md`](references/lark-doc-md.md)。
|
||||
|
||||
**Philosophy 负责创建与复杂编辑的设计判断;Router 负责一级大类模板与必要二类分流;Genre 只在 workplace/report 命中时补充细分偏好;Create / Update 负责执行;XML / Markdown 负责合法表达。**
|
||||
|
||||
**未读完命中的必读文件就执行相应操作,容易导致参数选择错误、格式错误、结构失焦或内容破坏。**
|
||||
|
||||
> **格式选择规则(全局):**
|
||||
> - **创建 / 导入场景**(`docs +create`,或 `docs +update --command append/overwrite` 的整段写入):XML 和 Markdown 都可以。用户提供 `.md` 本地文件、或明确说"导入 Markdown"时,直接用 Markdown;否则默认 XML。
|
||||
> - **创建 / 导入场景**(`docs +create`,或 `docs +update --command append/overwrite` 的整段写入):XML 和 Markdown 都可以。用户提供 `.md` 本地文件、或明确说"导入 Markdown"时,直接用 Markdown;否则默认 XML(可用 callout、grid、checkbox 等富 block)。
|
||||
> - **精准编辑场景**(`docs +update` 的 `str_replace` / `block_insert_after` / `block_replace` / `block_delete` / `block_move_after` 等局部精修指令):优先使用 XML(`--doc-format xml`,即默认值)。XML 能稳定表达 block 结构和样式,局部精修更可控;不要因为 Markdown 更简单就自行切换。
|
||||
|
||||
## 快速决策
|
||||
- 用户要**复制文档 / 创建文档副本 / 另存为副本**时,切到 [`lark-drive`](../lark-drive/SKILL.md),按其中的复制指引使用 `lark-cli drive files copy`;不要用 `docs +fetch` + `docs +create` 重建正文,也不要走 `drive +export` / `drive +import`。
|
||||
- 先判定任务路径:找文档 / 导入导出走 [`lark-drive`](../lark-drive/SKILL.md);只读 / 摘要用 `docs +fetch` 默认 `simple`;明确旧文本 → 新文本直接 `str_replace`;只有 block 链接、评论锚点、插入 / 替换 / 删除 / 移动才局部 fetch `with-ids`;保真改写已有内容才读 `full`
|
||||
- 先判定任务路径:找文档 / 导入导出走 [`lark-drive`](../lark-drive/SKILL.md);只读 / 摘要用 `docs +fetch` 默认 `simple`;已有文档改写按 [`lark-doc-update.md`](references/lark-doc-update.md) 的 Observe-Diagnose-Patch Loop 先 fetch 再局部 patch;明确旧文本 → 新文本的简单替换可直接 `str_replace`,但写后必须 fetch 验证;只有 block 链接、评论锚点、插入 / 替换 / 删除 / 移动才局部 fetch `with-ids`;保真改写已有内容才读 `full`
|
||||
- block 直达链接格式:`文档基础 URL#block_id`;没有 block_id 时局部 fetch `with-ids`
|
||||
- 连续执行多个文档写操作时,必须按 [`lark-doc-update.md`](references/lark-doc-update.md) 的「Block ID 生命周期」判断旧 block ID 是否还能复用;`overwrite` / `block_replace` / `block_delete` 后不要复用受影响的旧 ID,插入 / 复制后要重新 fetch 才能拿到新 block ID
|
||||
- 连续执行多个文档写操作时,必须按 [`lark-doc-update.md`](references/lark-doc-update.md) 的「Block ID 生命周期」处理:每次更新后都按 block ID 已变更处理;需要继续或重复修改时,先重新 fetch 最新内容和 block ID,不要复用旧 fetch 结果
|
||||
- 用户需要在文档内**创建、复制或移动**资源块(画板、电子表格、多维表格等)时,必须先读取 [`lark-doc-xml.md`](references/lark-doc-xml.md) 的「三、资源块」章节
|
||||
- 写文档时,由内容和用户意图决定表达形式;流程、架构、路线图、关键指标等信息可以使用画板,但不要默认把重要信息都画板化
|
||||
- 新增或更新画板时,按 [`lark-doc-whiteboard.md`](references/lark-doc-whiteboard.md) 选型;Mermaid 可由主 Agent 直接插入,SVG / 复杂图 / 已有画板更新按其中流程隔离到 SubAgent
|
||||
- 新增画板按复杂度处理:简单 Mermaid / SVG 图可由主 Agent 直接写入草稿;复杂图或需要专门视觉设计的 SVG 交给 SubAgent 产出完整 `<whiteboard type="svg">...</whiteboard>`;特别复杂或已有画板更新,主 Agent 先建 `<whiteboard type="blank"></whiteboard>`,再启动 SubAgent 读取 `lark-whiteboard` 写入
|
||||
- 用户说"看一下文档里的图片/附件/素材""预览素材" → 用 `lark-cli docs +media-preview`
|
||||
- 用户明确说"下载素材" → 用 `lark-cli docs +media-download`
|
||||
- 用户想把文档回滚到某个 `revision_id` 或某一时刻 → 先读 [`lark-doc-history.md`](references/lark-doc-history.md),按其中流程操作
|
||||
|
||||
50
skills/lark-doc/references/genres/README.md
Normal file
50
skills/lark-doc/references/genres/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Lark Doc Genre Routers
|
||||
|
||||
本目录按“一级 router 大类模板 →(仅 report/workplace 可选)二类体裁文件”组织文档体裁指南。不要一次读取整个目录。
|
||||
|
||||
使用流程:
|
||||
|
||||
1. 先读 `../lark-doc-design-philosophy.md` 的 `Route by genre`。
|
||||
2. 选择一个最匹配的 `_router-*.md`。
|
||||
3. 多数 router 本身就是一级大类模板,提供写作思路、风格、结构、元素、反模板和 final check;读完即停。
|
||||
4. 只有 `_router-report.md` 和 `_router-workplace.md` 保留二类 routing。它们明确命中具体体裁时,只读取一个具体体裁文件。
|
||||
5. 体裁文件提供结构偏好,不是固定模板。用户明确要求、已有文档风格和事实材料优先于体裁默认结构。
|
||||
|
||||
## Routers
|
||||
|
||||
- `_router-creative.md` — 文学故事大类模板
|
||||
- `_router-media.md` — 资讯媒体大类模板
|
||||
- `_router-opinion.md` — 观点评论大类模板
|
||||
- `_router-knowledge.md` — 知识教程大类模板
|
||||
- `_router-report.md` — 报告研究大类模板 + 二类 routing
|
||||
- `_router-consumer.md` — 种草消费大类模板
|
||||
- `_router-marketing.md` — 营销转化大类模板
|
||||
- `_router-workplace.md` — 职场协作大类模板 + 二类 routing
|
||||
- `_router-personal-brand.md` — 个人品牌大类模板
|
||||
- `_router-platform.md` — 平台原生大类模板
|
||||
|
||||
## Retained second-level genre files
|
||||
|
||||
`_router-report.md` may route to:
|
||||
|
||||
- `research-report.md`
|
||||
- `data-report.md`
|
||||
- `white-paper.md`
|
||||
- `business-analysis.md`
|
||||
|
||||
`_router-workplace.md` may route to:
|
||||
|
||||
- `memo-brief.md`
|
||||
- `weekly-report.md`
|
||||
- `proposal.md`
|
||||
- `formal-doc.md`
|
||||
- `official-redhead.md`
|
||||
- `meeting-minutes.md`
|
||||
- `retrospective.md`
|
||||
- `prd.md`
|
||||
- `technical-doc.md`
|
||||
- `sop-tutorial.md`
|
||||
|
||||
## Expansion rule
|
||||
|
||||
新增二类体裁前,先确认一级大类模板无法满足当前任务;新增后必须同步更新对应 router 和 `../lark-doc-design-philosophy.md` 的路由说明,避免 agent 读取多个无关文件。
|
||||
44
skills/lark-doc/references/genres/_router-consumer.md
Normal file
44
skills/lark-doc/references/genres/_router-consumer.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Genre Template: Consumer / 种草消费大类
|
||||
|
||||
本文件是一级大类模板。适用:小红书笔记、测评、合集、好物分享、探店、生活方式内容等以体验、选择和信任为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先确定读者场景:想买、想避坑、想比较、想收藏、想获得生活灵感,还是想验证真实体验。
|
||||
- 把推荐理由写成“场景 → 标准 → 体验 / 证据 → 适合谁 / 不适合谁”。
|
||||
- 没有真实材料时,不要伪装“亲测”“真实体验”“用了一周”。可以写成选购框架、测评提纲或待验证清单。
|
||||
- 对价格、库存、功效、安全、资质等易变或高风险信息必须标注来源或待确认。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **体验笔记**:场景钩子 → 核心感受 → 关键细节 → 适合 / 不适合 → 总结建议。
|
||||
- **测评 / 横评**:评测标准 → 对象对比 → 优缺点 → 场景匹配 → 结论。
|
||||
- **合集 / 清单**:推荐标准 → 分类 → 单项理由 → 选择建议 → 注意事项。
|
||||
- **探店 / 旅行体验**:位置 / 场景 → 体验动线 → 亮点 / 雷点 → 人群建议 → 实用信息。
|
||||
|
||||
## 风格
|
||||
|
||||
- 可信、具体、有生活感;少用夸张口号。
|
||||
- 可以轻松,但不能为了种草编造体验、效果或用户反馈。
|
||||
- 标题可强调场景和利益点,但不要制造绝对化承诺。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 对比表、适合 / 不适合列表、评分维度、清单、注意事项 callout。
|
||||
- 多产品、多店铺、多路线时用 table;情绪化体验可用短段落和列表。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要“闭眼入”“天花板”“全网最强”等无证据绝对话术。
|
||||
- 不要只有夸赞,没有限制、缺点或适用人群。
|
||||
- 不要把广告文案伪装成真实测评。
|
||||
- 不要堆 emoji 和感叹号掩盖信息不足。
|
||||
- 不要忽略价格、规格、地点、时间等关键上下文。
|
||||
|
||||
## Final check
|
||||
|
||||
- 读者能否判断这是否适合自己?
|
||||
- 推荐标准和证据是否明确?
|
||||
- 是否说明缺点、限制或不适合人群?
|
||||
- 是否避免伪造体验、销量、功效和背书?
|
||||
- 收藏、购买、到店或下一步行动是否清楚?
|
||||
44
skills/lark-doc/references/genres/_router-creative.md
Normal file
44
skills/lark-doc/references/genres/_router-creative.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Genre Template: Creative / 文学故事大类
|
||||
|
||||
本文件是一级大类模板。适用:网文、短篇故事、同人、互动小说、剧本、故事大纲等以故事体验为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先判断读者要获得什么体验:爽感、悬疑、治愈、幽默、代入、角色陪伴、世界观探索,还是创作协助。
|
||||
- 用“人物欲望 → 阻力 → 选择 → 代价 → 变化”组织故事,不要只堆设定。
|
||||
- 开篇优先给处境、冲突、问题或画面;设定应在行动中露出,而不是先写百科。
|
||||
- 若用户给了 IP、角色或世界观,只使用用户提供或可确认的信息;不确定的设定标注为创作假设。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **故事正文**:钩子场景 → 人物目标 → 冲突升级 → 关键选择 → 结果 / 余味。
|
||||
- **故事大纲**:一句话 premise → 主角与欲望 → 世界规则 → 主线冲突 → 阶段节点 → 结局方向。
|
||||
- **剧本 / 分镜**:场景信息 → 动作线 → 对白 → 情绪 / 镜头提示;不要把解释塞进对白。
|
||||
- **互动叙事**:当前状态 → 选择项 → 后果预期 → 分支收束点。
|
||||
|
||||
## 风格
|
||||
|
||||
- 具体画面优先于抽象评价;用动作、细节、对话呈现情绪。
|
||||
- 叙述节奏服务体裁:悬疑保留信息差,爽文强化目标和反馈,治愈文放慢感受,剧本保持可表演。
|
||||
- 保持角色声音一致;角色不能只替作者讲道理。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 人物卡、关系表、时间线、章节节点、场景清单、对白块、分支表。
|
||||
- 长篇或复杂世界观可用 table 管理人物 / 阵营 / 线索;流程复杂时再考虑 whiteboard。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要用“在一个……的世界里”替代具体开场。
|
||||
- 不要先写大段设定、背景、人物履历,再进入故事。
|
||||
- 不要把人物写成价值观传声筒。
|
||||
- 不要用连续反转掩盖动机不足。
|
||||
- 不要在同一段里混用小说叙述、剧本格式和大纲语气。
|
||||
|
||||
## Final check
|
||||
|
||||
- 第一屏是否有足够明确的钩子或人物处境?
|
||||
- 主角想要什么、阻力是什么、代价是什么是否清楚?
|
||||
- 角色语言、行动和情绪是否一致?
|
||||
- 设定是否服务冲突,而不是独立堆砌?
|
||||
- 结尾是否留下完成感、悬念或下一步创作入口?
|
||||
44
skills/lark-doc/references/genres/_router-knowledge.md
Normal file
44
skills/lark-doc/references/genres/_router-knowledge.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Genre Template: Knowledge / 知识教程大类
|
||||
|
||||
本文件是一级大类模板。适用:科普、教程、指南、攻略、FAQ、资源合集、知识库等以理解、学习和执行为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先确定读者起点:零基础、已有经验、正在排错、准备决策,还是要复用为知识库。
|
||||
- 把读者任务拆成“理解概念 / 完成操作 / 做选择 / 排查问题 / 找资源”。
|
||||
- 先给最短可行路径,再解释原理和例外;不要先讲完整理论体系。
|
||||
- 对风险、前置条件、适用范围和不适用范围要明确。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **科普解释**:问题 / 误区 → 核心概念 → 例子 → 机制 → 边界。
|
||||
- **教程 / 指南**:目标 → 前置条件 → 步骤 → 验证方式 → 常见问题 → 下一步。
|
||||
- **FAQ**:问题分组 → 短答案 → 详细解释 / 操作 → 相关链接或注意事项。
|
||||
- **资源合集**:使用场景 → 推荐标准 → 分类清单 → 选择建议 → 维护说明。
|
||||
|
||||
## 风格
|
||||
|
||||
- 具体、可操作、少炫技。术语第一次出现要解释。
|
||||
- 每一步尽量可验证:读者应知道自己是否做对。
|
||||
- 不要为了显得专业而增加读者不需要的理论。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 步骤列表、checkbox、FAQ 表、术语表、对比表、代码块、风险 callout。
|
||||
- 流程复杂、依赖多或排错路径多时可用 whiteboard;简单操作用列表更好。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要用“首先我们来了解背景”拖延进入任务。
|
||||
- 不要只有原则,没有步骤或例子。
|
||||
- 不要把 FAQ 写成文章段落,让读者无法检索。
|
||||
- 不要省略前置条件、版本、权限、环境差异。
|
||||
- 不要把资源合集写成无标准的链接堆砌。
|
||||
|
||||
## Final check
|
||||
|
||||
- 读者是否能从第一屏知道自己能完成什么?
|
||||
- 前置条件、步骤、验证方式是否清楚?
|
||||
- 概念解释是否服务任务,而不是单独炫技?
|
||||
- FAQ / 表格 / 代码块是否便于检索和复用?
|
||||
- 是否标注了限制、例外和常见错误?
|
||||
45
skills/lark-doc/references/genres/_router-marketing.md
Normal file
45
skills/lark-doc/references/genres/_router-marketing.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Genre Template: Marketing / 营销转化大类
|
||||
|
||||
本文件是一级大类模板。适用:广告、软文、详情页、带货、活动、公关、私域话术、转化文案等以认知、信任和行动为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先明确目标:认知、兴趣、咨询、报名、购买、复购、安抚舆情,还是统一口径。
|
||||
- 写清楚受众、场景、痛点、承诺、证据、行动入口;不要先追求文采。
|
||||
- 卖点必须来自产品、服务、案例或用户提供资料;没有证据时写成待补充,不要编造。
|
||||
- 公关和声明类优先事实边界、责任边界和下一步动作,不能只做情绪安抚。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **广告 / 短文案**:受众场景 → 核心利益 → 记忆点 → 行动。
|
||||
- **软文 / 内容营销**:问题场景 → 误区 / 机会 → 解决方案 → 证据 → CTA。
|
||||
- **详情页 / 落地页**:首屏承诺 → 痛点 → 方案 → 卖点模块 → 证明 → FAQ → CTA。
|
||||
- **活动 / 私域话术**:对象 → 利益点 → 规则 → 时间 / 门槛 → 引导动作。
|
||||
- **公关沟通**:事实 → 影响 → 态度 / 原则 → 措施 → 后续同步机制。
|
||||
|
||||
## 风格
|
||||
|
||||
- 明确、有推动力,但不过度承诺。
|
||||
- 面向外部传播时保持品牌一致;面向私域时更像真人沟通,但不油腻。
|
||||
- 话术要短、可发送、可执行,避免大段官腔。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 卖点表、受众-痛点-利益表、FAQ、CTA、时间线、风险 callout、话术分支。
|
||||
- 多渠道活动可用 table;复杂转化链路可用 whiteboard。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要编造功效、销量、资质、奖项、背书、用户评价。
|
||||
- 不要只有华丽形容词,没有利益点和证据。
|
||||
- 不要把所有内容都写成“限时抢购”。
|
||||
- 不要忽视合规、隐私、价格、适用范围和退改规则。
|
||||
- 不要把公关稿写成自夸稿,回避核心问题。
|
||||
|
||||
## Final check
|
||||
|
||||
- 目标动作是否明确?
|
||||
- 受众、场景、痛点和核心利益是否具体?
|
||||
- 每个重要承诺是否有证据或待补充标记?
|
||||
- 结构是否降低理解和行动成本?
|
||||
- 是否避免夸大、误导和不合规表达?
|
||||
44
skills/lark-doc/references/genres/_router-media.md
Normal file
44
skills/lark-doc/references/genres/_router-media.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Genre Template: Media / 资讯媒体大类
|
||||
|
||||
本文件是一级大类模板。适用:新闻、快讯、深度报道、人物报道、访谈、媒体稿等以事实传递和公共理解为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先区分任务是“快速告知”“解释背景”“呈现人物”“整理访谈”还是“对外发布”。
|
||||
- 明确事实层级:已确认事实、来自材料的说法、推断、待确认信息必须分开。
|
||||
- 重要信息前置:谁、何时、何地、发生了什么、影响是什么、下一步是什么。
|
||||
- 多方材料冲突时,不要替任何一方下结论;呈现来源、口径和不确定性。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **快讯 / 短消息**:核心事实 → 影响 / 背景一句 → 后续进展。
|
||||
- **常规报道**:导语 → 关键事实 → 背景 → 相关方回应 → 影响与后续。
|
||||
- **解释性报道**:问题 → 为什么重要 → 时间线 / 机制 → 各方观点 → 尚待确认。
|
||||
- **人物 / 访谈**:人物切口 → 关键经历 / 观点 → 代表性细节 → 语录 / 问答 → 现实意义。
|
||||
|
||||
## 风格
|
||||
|
||||
- 准确、克制、可核验;少用情绪化形容词。
|
||||
- 标题可以清楚有信息量,但不要标题党。
|
||||
- 引述材料时区分直接引用、转述和总结;没有来源不要编造“相关人士表示”。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 时间线、事实表、人物 / 机构关系表、问答块、blockquote、待确认 callout。
|
||||
- 多事件、多主体、多时间点时优先 table;因果或时间推进复杂时可用 whiteboard。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要把评论写成新闻,不要把宣传稿写成报道。
|
||||
- 不要使用“引发热议”“业内人士认为”等无来源表达。
|
||||
- 不要为了完整性补齐不存在的细节。
|
||||
- 不要用大量背景压住最新事实。
|
||||
- 不要把采访整理成流水账;要保留问题线索和观点递进。
|
||||
|
||||
## Final check
|
||||
|
||||
- 关键事实是否前置,且可区分已确认 / 待确认?
|
||||
- 是否清楚标注来源、材料口径和事实边界?
|
||||
- 标题、导语和正文是否一致,不夸大?
|
||||
- 背景是否帮助理解,而不是稀释重点?
|
||||
- 读者读完是否知道事件影响和后续关注点?
|
||||
43
skills/lark-doc/references/genres/_router-opinion.md
Normal file
43
skills/lark-doc/references/genres/_router-opinion.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Genre Template: Opinion / 观点评论大类
|
||||
|
||||
本文件是一级大类模板。适用:时评、商业评论、文化评论、专栏、随笔、杂文、观点文章等以判断、立场和论证为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先写清楚核心判断:你到底赞成、反对、解释、警惕还是重新定义一个问题。
|
||||
- 评论不是事实复述。事实只承担证据、背景或反例功能。
|
||||
- 用“观点 → 理由 → 证据 → 反方可能意见 → 回应 → 结论”推进,而不是堆观点。
|
||||
- 观点强度要匹配证据强度;证据不足时用假设、可能、值得观察,而不是确定断言。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **直接评论**:切入点 → 核心观点 → 2-3 个论点 → 反方视角 → 结论 / 建议。
|
||||
- **商业 / 文化观察**:现象 → 误读 → 深层机制 → 案例 / 证据 → 可迁移启发。
|
||||
- **随笔 / 专栏**:具体经验或场景 → 观察 → 转折 → 思考展开 → 收束到一个余味。
|
||||
|
||||
## 风格
|
||||
|
||||
- 可以有锋芒,但必须清楚、公允、有边界。
|
||||
- 少用宏大词,多用具体判断;避免“时代洪流”“底层逻辑”等空泛表达。
|
||||
- 随笔可保留个人声音,但不能编造经历或把感受包装成事实。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 论点列表、论据表、正反观点表、引用块、结论 callout。
|
||||
- 长文可用小标题表达论证推进;不要为了整齐把每节写成同样长度。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要只有态度,没有论证。
|
||||
- 不要把单个案例扩大成普遍规律。
|
||||
- 不要把复杂问题写成二元对立。
|
||||
- 不要用情绪化标签替代分析。
|
||||
- 不要开头抛概念,正文不断换概念。
|
||||
|
||||
## Final check
|
||||
|
||||
- 核心观点能否用一句话复述?
|
||||
- 每个论点是否有事实、案例或逻辑支撑?
|
||||
- 是否处理了最强的反方意见?
|
||||
- 语气是否有分寸,避免过度断言?
|
||||
- 结尾是否完成判断,而不是只留下口号?
|
||||
45
skills/lark-doc/references/genres/_router-personal-brand.md
Normal file
45
skills/lark-doc/references/genres/_router-personal-brand.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Genre Template: Personal Brand / 个人品牌大类
|
||||
|
||||
本文件是一级大类模板。适用:简历、自我介绍、作品集、朋友圈、个人主页、成长复盘等以可信呈现个人经历、能力和态度为核心的文档。读完本文件即可开始写作;当前不继续读取具体体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先判断目标读者:招聘方、面试官、合作方、潜在客户、同事、朋友,还是未来的自己。
|
||||
- 个人表达要围绕“身份定位 → 关键经历 → 可验证成果 → 做事方式 → 下一步意图”。
|
||||
- 求职 / 合作场景优先事实、证据、结果;社交 / 复盘场景可以保留个性和情绪。
|
||||
- 不要夸大 title、职责、数据、奖项或项目归属;不确定的成果用“参与 / 支持 / 负责部分”区分。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **简历 / 个人主页**:定位摘要 → 核心能力 → 代表经历 / 项目 → 成果证据 → 联系 / 下一步。
|
||||
- **自我介绍**:一句话身份 → 相关经历 → 能提供的价值 → 与当前场景的连接。
|
||||
- **作品集**:选择标准 → 项目卡片 → 背景 / 角色 / 动作 / 结果 → 反思或方法论。
|
||||
- **成长复盘**:阶段目标 → 关键事件 → 做对 / 做错 → 认知变化 → 下一阶段计划。
|
||||
- **朋友圈 / 社交动态**:具体场景 → 真实感受 → 一个观点或行动,而不是履历堆砌。
|
||||
|
||||
## 风格
|
||||
|
||||
- 真实、具体、有选择。让事实说话,少用“优秀、负责、抗压、热爱”等自评词。
|
||||
- 面向职业机会时克制清晰;面向社交表达时自然但不失分寸。
|
||||
- 数据、案例和作品链接比泛泛形容更有说服力。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 项目表、能力矩阵、STAR / CAR 结构、时间线、作品卡、成果清单、下一步 checkbox。
|
||||
- 作品集或个人主页可用 grid;简历类避免过度装饰。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要把经历包装到失真。
|
||||
- 不要堆 buzzword,缺少具体项目和结果。
|
||||
- 不要把自我介绍写成长篇自传。
|
||||
- 不要把成长复盘写成鸡汤或年度流水账。
|
||||
- 不要在不同章节使用冲突的人设和定位。
|
||||
|
||||
## Final check
|
||||
|
||||
- 读者是否能快速理解“这个人是谁、能做什么、证据是什么”?
|
||||
- 关键经历是否具体到角色、动作、结果?
|
||||
- 是否避免夸大或模糊个人贡献?
|
||||
- 语气是否符合场景,既不过度自夸也不过度谦虚?
|
||||
- 下一步联系方式、作品入口或行动是否清楚?
|
||||
45
skills/lark-doc/references/genres/_router-platform.md
Normal file
45
skills/lark-doc/references/genres/_router-platform.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Genre Template: Platform Native / 平台原生大类
|
||||
|
||||
本文件是一级大类模板。适用:用户明确要求适配公众号、知乎、微博、B站、抖音、豆瓣、Newsletter 等平台形态、语气、结构或发布习惯的任务。读完本文件即可开始写作;当前不继续读取具体平台文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先判断平台是否真的决定结构。若用户只是说“发到某平台”,但内容体裁更明确,优先内容体裁;只有明确要求平台写法时才使用本模板。
|
||||
- 平台适配是“重组入口、节奏和互动方式”,不是改变事实。
|
||||
- 保留核心信息:目标读者、事实材料、观点、证据和行动;再调整标题、开头、段落、互动和 CTA。
|
||||
- 平台口吻不能替代内容质量;不要为了热度编造经历、冲突或数据。
|
||||
|
||||
## 结构偏好
|
||||
|
||||
- **公众号 / Newsletter**:标题 → 导语 / 问题 → 主体分节 → 总结 → 行动或订阅入口。
|
||||
- **知乎**:问题复述 → 立场 / 结论 → 论据展开 → 反例 / 边界 → 总结。
|
||||
- **微博 / 短帖**:一句核心观点 → 1-3 个支撑点 → 话题 / 互动。
|
||||
- **短视频 / 口播**:前三秒钩子 → 场景 / 冲突 → 观点或步骤 → 转折 → 结尾行动。
|
||||
- **小红书 / 豆瓣**:具体体验或清单 → 个人判断 → 适合 / 不适合 → 互动或收藏点。
|
||||
|
||||
## 风格
|
||||
|
||||
- 平台语气要适度:更口语、更有节奏,但不降低事实密度。
|
||||
- 标题和开头可以强化钩子,但不能制造与正文不一致的期待。
|
||||
- 长平台重逻辑和可信度,短平台重入口和记忆点。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 标题备选、开头钩子、分节小标题、互动问题、CTA、话题标签、脚本分镜表。
|
||||
- 多平台改写时用 table 对比标题、开头、结构、CTA。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要只把文章切短就叫平台适配。
|
||||
- 不要为了“平台感”堆 emoji、网络梗和夸张词。
|
||||
- 不要把专业内容改到事实失真。
|
||||
- 不要忽略平台长度、互动入口和读者预期。
|
||||
- 不要多个平台共用同一个标题和开头。
|
||||
|
||||
## Final check
|
||||
|
||||
- 是否确认平台需求真的决定写法?
|
||||
- 核心事实和观点是否保真?
|
||||
- 标题、开头和 CTA 是否符合平台读者任务?
|
||||
- 语气是否平台化但不过度表演?
|
||||
- 是否保留了足够的信息密度和可验证性?
|
||||
65
skills/lark-doc/references/genres/_router-report.md
Normal file
65
skills/lark-doc/references/genres/_router-report.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Genre Router: Report / 报告研究大类
|
||||
|
||||
本文件既是一级大类模板,也保留报告类二类 routing。适用:行业报告、数据报告、调研报告、白皮书、商业分析、竞品分析等以证据、洞察和决策为核心的文档。
|
||||
|
||||
读取规则:先使用本文件完成大类判断;只有当用户意图明确命中下方二类,且需要更细结构时,才额外读取一个具体体裁文件。不要读取多个报告体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先确定报告任务:解释现状、发现问题、评估机会、支持决策、对外建立权威,还是沉淀研究。
|
||||
- 区分事实、数据、样本、假设、洞察和建议;不要把相关性写成因果。
|
||||
- 结论要前置,证据要可追溯,建议要说明取舍、风险和适用边界。
|
||||
- 如果资料不足,先列出缺口和假设,不要填充看似完整但不可验证的结论。
|
||||
|
||||
## 默认结构
|
||||
|
||||
- Executive summary / 一句话结论
|
||||
- 背景与问题定义
|
||||
- 方法、样本或数据口径
|
||||
- 关键发现 / 洞察
|
||||
- 影响评估 / 机会与风险
|
||||
- 建议动作 / 决策项
|
||||
- 附录:数据、访谈、定义、来源
|
||||
|
||||
## 风格
|
||||
|
||||
- 客观、克制、证据驱动;少用营销语和未经验证的趋势判断。
|
||||
- 标题要表达发现,而不是只写主题。例如“留存下滑主要来自新用户首周激活不足”优于“留存分析”。
|
||||
- 建议必须能落到对象、动作、优先级或决策。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 指标表、样本说明、对比矩阵、风险雷达、假设清单、引用块、路线图、决策表。
|
||||
- 数据或多对象对比优先 table;趋势、流程、行业结构复杂时可用 whiteboard。
|
||||
|
||||
## 二类 routing
|
||||
|
||||
| 用户意图 / 信号词 | 读取文件 |
|
||||
|---|---|
|
||||
| 行业报告、调研报告、用户研究、市场研究、研究报告 | `research-report.md` |
|
||||
| 数据报告、指标分析、经营数据、漏斗分析、仪表盘解读 | `data-report.md` |
|
||||
| 白皮书、趋势报告、方法论报告、权威长报告 | `white-paper.md` |
|
||||
| 商业分析、战略分析、竞品分析、商业计划分析 | `business-analysis.md` |
|
||||
|
||||
冲突规则:
|
||||
|
||||
- 基于访谈、资料、样本或文献形成洞察时,优先 `research-report.md`。
|
||||
- 核心证据是指标、时间序列、分布、漏斗或实验时,优先 `data-report.md`。
|
||||
- 面向外部传播、体系化观点和品牌权威时,优先 `white-paper.md`。
|
||||
- 面向商业决策、竞争格局、策略选项和资源取舍时,优先 `business-analysis.md`。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要只有宏观背景,没有明确问题和结论。
|
||||
- 不要把资料摘要伪装成洞察。
|
||||
- 不要只展示数据,不解释口径、原因和行动含义。
|
||||
- 不要使用“趋势明显、空间巨大、潜力无限”等无证据判断。
|
||||
- 不要给出建议却没有优先级、风险和验证方式。
|
||||
|
||||
## Final check
|
||||
|
||||
- 结论、证据、假设和建议是否分层清楚?
|
||||
- 数据口径、样本和来源是否说明?
|
||||
- 每个洞察是否能追溯到材料或逻辑?
|
||||
- 建议是否明确对象、动作、优先级和风险?
|
||||
- 是否只额外读取了一个命中的报告二类文件?
|
||||
70
skills/lark-doc/references/genres/_router-workplace.md
Normal file
70
skills/lark-doc/references/genres/_router-workplace.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Genre Router: Workplace / 职场协作大类
|
||||
|
||||
本文件既是一级大类模板,也保留职场类二类 routing。适用:周报、方案、纪要、复盘、PRD、技术文档、SOP、公文、正式协作文档等以协作、决策、执行和留档为核心的文档。
|
||||
|
||||
读取规则:先使用本文件完成大类判断;只有当用户意图明确命中下方二类,且需要更细结构时,才额外读取一个具体体裁文件。不要读取多个职场体裁文件。
|
||||
|
||||
## 写作思路
|
||||
|
||||
- 先判断文档要推动什么:同步进展、争取决策、分配责任、固化流程、解释技术、沉淀复盘,还是正式留档。
|
||||
- 职场文档默认服务协作执行:结论、责任人、时间、状态、风险、依赖和下一步要清楚。
|
||||
- 面向领导 / 决策者时前置结论和选项;面向执行者时前置目标、范围、步骤和验收;面向归档时前置口径和可追溯性。
|
||||
- 改写已有职场文档时,保留原有事实、评论锚点、资源块和组织口径;不要为了“更完整”全文覆盖。
|
||||
|
||||
## 默认结构
|
||||
|
||||
- 目标 / 结论 / 待决策事项
|
||||
- 背景或现状,只保留影响判断的信息
|
||||
- 方案、进展、事实或流程主体
|
||||
- 风险、依赖、待确认事项
|
||||
- 行动项、责任人、时间点、验收标准
|
||||
- 附录:材料、数据、链接、术语
|
||||
|
||||
## 风格
|
||||
|
||||
- 直接、克制、可执行;少用“赋能、抓手、闭环、沉淀、拉通”等空泛词。
|
||||
- 标题表达动作和状态,例如“本周完成 X,Y 因依赖 Z 延后”优于“工作进展”。
|
||||
- 对不确定事项明确标注 owner 和截止时间。
|
||||
|
||||
## 常用元素
|
||||
|
||||
- 状态表、RACI / owner 表、时间线、风险 callout、决策矩阵、checkbox、接口表、代码块、流程图。
|
||||
- 排期、责任、指标、接口参数优先 table;流程、架构、依赖复杂时可用 whiteboard。
|
||||
|
||||
## 二类 routing
|
||||
|
||||
| 用户意图 / 信号词 | 读取文件 |
|
||||
|---|---|
|
||||
| 备忘录、领导简报、决策简报、情况说明、内部分析摘要 | `memo-brief.md` |
|
||||
| 周报、月报、进展同步、工作总结 | `weekly-report.md` |
|
||||
| 方案、项目方案、工作方案、正式方案、跨团队协作方案 | `proposal.md` |
|
||||
| 公司通知、制度规范、正式汇报、对外说明、内部正式文档 | `formal-doc.md` |
|
||||
| 红头文件、公文、请示、批复、函、通报、主送机关、抄送机关 | `official-redhead.md` |
|
||||
| 会议纪要、会议记录、议定事项、行动项 | `meeting-minutes.md` |
|
||||
| 复盘、事故总结、项目总结、阶段总结 | `retrospective.md` |
|
||||
| PRD、需求说明、用户故事、产品方案、功能设计 | `prd.md` |
|
||||
| 架构、接口、API、部署、迁移、排障、技术设计 | `technical-doc.md` |
|
||||
| SOP、操作手册、培训材料、入门指南、使用说明 | `sop-tutorial.md` |
|
||||
|
||||
冲突规则:
|
||||
|
||||
- 目标是快速帮助高层判断时,优先 `memo-brief.md`。
|
||||
- 目标是协作推进时,优先读能定义责任、时间、状态和下一步的体裁。
|
||||
- 技术 / PRD / SOP 是职场文档的专门体裁,不要用普通方案覆盖。
|
||||
- 明确公文、红头时读 `official-redhead.md`,不要用 `formal-doc.md`。
|
||||
|
||||
## 反模板
|
||||
|
||||
- 不要默认套“背景 / 目标 / 方案 / 风险 / 下一步”。
|
||||
- 不要只有愿景,没有 owner、时间和验收标准。
|
||||
- 不要把风险写成笼统提醒,没有影响、概率和应对动作。
|
||||
- 不要把技术文档写成汇报稿,或把汇报写成技术细节堆砌。
|
||||
- 不要为了排版完整而压低关键信息的优先级。
|
||||
|
||||
## Final check
|
||||
|
||||
- 读者是否知道要判断、执行或确认什么?
|
||||
- 责任人、时间点、状态、风险和下一步是否明确?
|
||||
- 结构是否匹配场景,而不是套通用模板?
|
||||
- 表格、callout、whiteboard 是否确实降低协作成本?
|
||||
- 是否只额外读取了一个命中的职场二类文件?
|
||||
35
skills/lark-doc/references/genres/business-analysis.md
Normal file
35
skills/lark-doc/references/genres/business-analysis.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Business Analysis / 商业分析
|
||||
|
||||
适用:商业分析、战略分析、竞品分析、商业计划分析、公司研究。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先确定商业问题:增长、利润、效率、竞争、组织、产品、渠道还是资本。用事实、模型和约束推导建议。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
问题 → 背景 → 市场 / 用户 → 竞争格局 → 商业模型 → 关键判断 → 方案 / 建议 → 风险。
|
||||
|
||||
## Style
|
||||
|
||||
分析导向、结论前置、避免管理黑话。模型服务判断,不是装饰。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 商业模型:whiteboard
|
||||
- 竞品对比:table
|
||||
- SWOT / 取舍:grid / table
|
||||
- 关键判断:callout
|
||||
|
||||
## Avoid
|
||||
|
||||
- 套模型不回答问题。
|
||||
- 只有观点没有证据。
|
||||
- 忽略成本、约束和风险。
|
||||
- 建议不可执行。
|
||||
|
||||
## Final check
|
||||
|
||||
- 商业问题是否明确?
|
||||
- 判断是否有证据和模型支撑?
|
||||
- 建议是否可执行?
|
||||
35
skills/lark-doc/references/genres/data-report.md
Normal file
35
skills/lark-doc/references/genres/data-report.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Data Report / 数据报告
|
||||
|
||||
适用:数据报告、指标分析、经营数据、漏斗分析、实验结果、仪表盘解读。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先确认指标口径、时间范围、分群维度和业务问题。数据报告不是罗列数字,而是解释变化、定位原因、给出动作。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
结论摘要 → 指标口径 → 总览 → 分维度分析 → 异常 / 归因 → 建议动作 → 限制。
|
||||
|
||||
## Style
|
||||
|
||||
准确、可复算、少形容词。所有关键数字都要有口径和时间范围。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 指标表:table
|
||||
- 趋势图 / 漏斗:whiteboard
|
||||
- 异常点:callout
|
||||
- 行动项:checkbox
|
||||
|
||||
## Avoid
|
||||
|
||||
- 只列数据,不解释意义。
|
||||
- 口径不清。
|
||||
- 把相关性写成因果。
|
||||
- 选择性展示有利数据。
|
||||
|
||||
## Final check
|
||||
|
||||
- 指标口径是否清楚?
|
||||
- 变化是否有解释?
|
||||
- 建议是否能落到动作?
|
||||
35
skills/lark-doc/references/genres/formal-doc.md
Normal file
35
skills/lark-doc/references/genres/formal-doc.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Formal Document / 企业正式文档
|
||||
|
||||
适用:企业或组织内部正式文档,如公司通知、制度规范、正式汇报、立项材料、正式方案、对外说明、跨团队协作文档。不适用于党政机关公文 / 红头文件;相关任务使用 `official-redhead.md`。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先确认文档是否需要归档、追责或跨团队执行。正式文档优先事项、范围、责任、时间和口径,不追求个性化表达。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
事项型:事项 → 范围 → 安排 → 时间 → 责任。决策型:结论 → 背景 → 对比 → 风险 → 待决策。制度型:目的 → 范围 → 规则 → 例外 → 执行。
|
||||
|
||||
## Style
|
||||
|
||||
准确、克制、明确责任边界。少用夸张修饰和网感标题。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 时间 / 责任 / 范围:table
|
||||
- 风险 / 待确认:callout
|
||||
- 方案对比:table / grid
|
||||
- 流程 / 里程碑:whiteboard
|
||||
|
||||
## Avoid
|
||||
|
||||
- 开头背景过长。
|
||||
- 缺少责任人、时间、范围。
|
||||
- 为了正式感堆抽象词。
|
||||
- 为了美观强行加组件。
|
||||
|
||||
## Final check
|
||||
|
||||
- 事项、范围、责任、时间是否明确?
|
||||
- 结论和待决策是否前置?
|
||||
- 是否适合归档和追责?
|
||||
35
skills/lark-doc/references/genres/meeting-minutes.md
Normal file
35
skills/lark-doc/references/genres/meeting-minutes.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Meeting Minutes / 会议纪要
|
||||
|
||||
适用:会议纪要、会议记录、评审结论、项目例会、决策会、同步会。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先区分“已决议、待确认、个人观点、行动项”。会议纪要不是流水账,重点是结论、分歧、责任和下一步。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
会议信息 → 背景 → 讨论要点 → 决议 → 行动项 → 风险 / 待确认。
|
||||
|
||||
## Style
|
||||
|
||||
中立、准确、面向行动。尽量保留关键原话和分歧依据。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 会议信息:table
|
||||
- 决议:callout / list
|
||||
- 行动项:checkbox / table
|
||||
- 原话:blockquote
|
||||
|
||||
## Avoid
|
||||
|
||||
- 只按发言顺序记录。
|
||||
- 责任人和截止时间缺失。
|
||||
- 把未确认观点写成决议。
|
||||
- 删除关键分歧。
|
||||
|
||||
## Final check
|
||||
|
||||
- 决议、待确认、行动项是否分开?
|
||||
- 每个行动项是否有负责人和时间?
|
||||
- 关键分歧是否保留?
|
||||
39
skills/lark-doc/references/genres/memo-brief.md
Normal file
39
skills/lark-doc/references/genres/memo-brief.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Genre: Memo / Briefing Note
|
||||
|
||||
适用:备忘录、领导简报、决策简报、情况说明、问题简报、内部分析摘要。目标是帮助读者快速掌握情况并做判断。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
按任务选择结构:
|
||||
|
||||
- **决策简报**:一句话结论 → 背景 → 选项对比 → 推荐方案 → 风险 → 待决策问题。
|
||||
- **情况说明**:事项概述 → 当前状态 → 影响范围 → 已采取措施 → 后续安排。
|
||||
- **问题简报**:问题 → 证据 → 原因 → 影响 → 建议动作。
|
||||
- **备忘录**:To / From / Date / Subject → Purpose → Context → Discussion → Action / Recommendation。
|
||||
|
||||
## Style
|
||||
|
||||
短、准、直接。先给结论,再给依据。面向高层或跨团队读者时减少细枝末节,把关键判断、影响和动作前置。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 结论 / 建议:短段落或 callout。
|
||||
- 方案对比:table。
|
||||
- 风险和影响:table / callout。
|
||||
- 待决策问题:checkbox / list。
|
||||
- 背景材料:附录或折叠到后文,不要压住结论。
|
||||
|
||||
## Avoid
|
||||
|
||||
- 背景过长,读者看不到结论。
|
||||
- 只有信息罗列,没有判断。
|
||||
- 只说“建议推进”,没有选项和取舍。
|
||||
- 风险轻描淡写或没有责任边界。
|
||||
- 把简报写成完整报告。
|
||||
|
||||
## Final check
|
||||
|
||||
- 第一屏是否能读到结论或建议?
|
||||
- 是否清楚说明需要读者做什么决定?
|
||||
- 选项、风险、影响和下一步是否明确?
|
||||
- 是否足够短,适合快速阅读?
|
||||
35
skills/lark-doc/references/genres/official-redhead.md
Normal file
35
skills/lark-doc/references/genres/official-redhead.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Official Redhead / 公文红头
|
||||
|
||||
适用:党政机关、事业单位、国企或正式组织语境下的公文 / 红头文件,包括通知、通报、请示、批复、函、纪要、公告、通告、报告、意见等。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先判定文种,再按文种写;公文是强约束文书,不是“更正式的通知”。缺少发文机关、主送机关、文号、依据等信息时标注占位或待确认,不要伪造红头、印章、文号或法定依据。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
通知:依据 → 事项 → 要求 → 时间。请示:缘由 → 请示事项 → 依据 → “妥否,请批示”。批复:依据 → 明确意见 → 执行要求。报告:情况 → 成效 → 问题 → 下一步,报告不得夹带请示。纪要:会议情况 → 会议明确 → 议定事项。
|
||||
|
||||
## Style
|
||||
|
||||
庄重、准确、规范、简洁。少用修饰,不用 emoji、网感表达和花哨排版。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 正文:段落 + 条款
|
||||
- 责任分工 / 附件:table
|
||||
- 议定事项:table / checkbox
|
||||
- 原则上少用 callout / grid / 颜色
|
||||
|
||||
## Avoid
|
||||
|
||||
- 文种混乱。
|
||||
- 报告夹带请示。
|
||||
- 伪造红头、印章、文号、签发人或依据。
|
||||
- 口语化、营销化。
|
||||
|
||||
## Final check
|
||||
|
||||
- 文种是否正确?
|
||||
- 发文对象、事项、依据、期限、责任是否明确?
|
||||
- 是否避免伪造格式要素?
|
||||
35
skills/lark-doc/references/genres/prd.md
Normal file
35
skills/lark-doc/references/genres/prd.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: PRD / 产品需求
|
||||
|
||||
适用:PRD、需求说明、用户故事、产品方案、功能设计、版本范围说明。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先确认用户、场景、问题和验收标准。PRD 不是愿望清单,要能被设计、研发、测试和业务评审。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
背景与问题 → 用户与场景 → 目标与非目标 → 方案概述 → 功能范围 → 交互 / 流程 → 数据与埋点 → 边界异常 → 验收标准 → 风险依赖。
|
||||
|
||||
## Style
|
||||
|
||||
具体、可评审、可验收。多写用户行为、系统响应、状态变化和规则。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 用户故事 / 验收:列表 / checkbox
|
||||
- 字段 / 规则 / 状态:table
|
||||
- 流程 / 状态机:whiteboard
|
||||
- 风险 / 待确认:callout
|
||||
|
||||
## Avoid
|
||||
|
||||
- 只有需求描述,没有验收标准。
|
||||
- 没有非目标。
|
||||
- 缺少异常和边界。
|
||||
- 用愿景替代规则。
|
||||
|
||||
## Final check
|
||||
|
||||
- 目标、非目标和范围是否清楚?
|
||||
- 规则和状态是否可评审?
|
||||
- 验收标准是否可验证?
|
||||
35
skills/lark-doc/references/genres/proposal.md
Normal file
35
skills/lark-doc/references/genres/proposal.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Proposal / 方案
|
||||
|
||||
适用:项目方案、工作方案、正式方案、跨团队协作方案。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先定义问题、目标、约束和决策点。方案的价值在取舍,不是堆功能和计划。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
结论 / 推荐方案 → 问题定义 → 目标与范围 → 方案设计 → 资源排期 → 风险 → 待决策事项。
|
||||
|
||||
## Style
|
||||
|
||||
明确、可评审、可执行。每个方案点要能回答“为什么这样做”。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 方案对比:table
|
||||
- 路线图:whiteboard
|
||||
- 风险:callout
|
||||
- 任务拆解:checkbox
|
||||
|
||||
## Avoid
|
||||
|
||||
- 没有问题定义。
|
||||
- 目标和方案混在一起。
|
||||
- 不写取舍。
|
||||
- 没有资源、排期和风险。
|
||||
|
||||
## Final check
|
||||
|
||||
- 问题和目标是否明确?
|
||||
- 方案取舍是否说明?
|
||||
- 是否可执行和评审?
|
||||
35
skills/lark-doc/references/genres/research-report.md
Normal file
35
skills/lark-doc/references/genres/research-report.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Research Report / 调研分析报告
|
||||
|
||||
适用:调研报告、行业分析、用户研究、市场研究、研究报告、竞品分析。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先定义研究问题和结论使用场景,再搜集资料、数据、访谈或样本。事实、推断、建议要分开;没有来源、样本或口径的数据不要写成确定事实。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
摘要 → 研究问题 → 方法 / 样本 → 核心发现 → 分析 → 建议 → 限制 / 待验证。
|
||||
|
||||
## Style
|
||||
|
||||
证据驱动、结构清楚、结论克制。不要为了报告感堆图表。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 核心发现:callout
|
||||
- 样本 / 数据:table
|
||||
- 趋势 / 关系:whiteboard
|
||||
- 引用材料:blockquote
|
||||
|
||||
## Avoid
|
||||
|
||||
- 没有研究问题。
|
||||
- 资料堆砌没有洞察。
|
||||
- 推测写成事实。
|
||||
- 数据无来源或口径。
|
||||
|
||||
## Final check
|
||||
|
||||
- 研究问题是否明确?
|
||||
- 事实、推断、建议是否分开?
|
||||
- 结论是否支撑行动?
|
||||
35
skills/lark-doc/references/genres/retrospective.md
Normal file
35
skills/lark-doc/references/genres/retrospective.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Retrospective / 复盘总结
|
||||
|
||||
适用:项目复盘、事故复盘、阶段总结、周报/月报中的复盘部分。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先还原事实,再给判断,最后形成动作。复盘不是甩锅,也不是表功;价值在找到可验证的改进点。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
结论 → 事实时间线 → 影响范围 → 原因分析 → 做得好的地方 → 问题教训 → 后续动作。
|
||||
|
||||
## Style
|
||||
|
||||
事实和判断分开,语气克制。行动项必须有负责人、时间和验收方式。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 时间线:table / whiteboard
|
||||
- 根因:列表 / 鱼骨图
|
||||
- 行动项:checkbox / table
|
||||
- 风险:callout
|
||||
|
||||
## Avoid
|
||||
|
||||
- 只有情绪评价。
|
||||
- 只有“加强对齐”没有动作。
|
||||
- 结论与证据不匹配。
|
||||
- 复盘写成甩锅材料。
|
||||
|
||||
## Final check
|
||||
|
||||
- 事实、判断、动作是否分开?
|
||||
- 根因是否有证据?
|
||||
- 行动项是否可追踪?
|
||||
36
skills/lark-doc/references/genres/sop-tutorial.md
Normal file
36
skills/lark-doc/references/genres/sop-tutorial.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Genre: SOP / 操作手册
|
||||
|
||||
适用:SOP、操作手册、培训材料、入门指南、使用说明、FAQ、流程规范。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先明确适用对象、前置条件和成功标准。步骤写作遵循“一步一个动作”:有顺序用有序列表,无顺序用项目符号;给出校验结果和异常处理。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
适用范围 → 前置条件 → 操作步骤 → 检查标准 → 异常处理 → 责任边界 / 参考链接。
|
||||
|
||||
## Style
|
||||
|
||||
直接、清楚、面向操作者。按用户看到的界面命名,不按内部实现命名。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 步骤:有序列表
|
||||
- 检查项:checkbox
|
||||
- 命令 / 配置 / 日志:pre / code
|
||||
- 错误处理:table / callout
|
||||
- 复杂流程:whiteboard
|
||||
|
||||
## Avoid
|
||||
|
||||
- 缺少前置条件。
|
||||
- 步骤跳跃。
|
||||
- 一个步骤里塞多个动作。
|
||||
- 没有失败处理。
|
||||
|
||||
## Final check
|
||||
|
||||
- 新手能否完成任务?
|
||||
- 输入、输出和校验是否清楚?
|
||||
- 每步是否只做一件事?
|
||||
35
skills/lark-doc/references/genres/technical-doc.md
Normal file
35
skills/lark-doc/references/genres/technical-doc.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Technical Document / 技术文档
|
||||
|
||||
适用:技术方案、架构设计、接口说明、API 文档、部署手册、迁移方案、排障指南、研发交接。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先确认读者要实现、集成、排查还是评审。技术文档必须准确、可复现、可验证;事实不确定要标注假设。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
问题 / 背景 → 目标与非目标 → 约束 → 总体设计 → 模块设计 → 数据 / 接口 → 风险 → 验证 → 灰度 / 回滚。
|
||||
|
||||
## Style
|
||||
|
||||
精确、朴素、可实现、可验证。术语保持一致,不为了通俗牺牲准确性。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 命令 / 代码 / 日志:pre / code
|
||||
- 参数 / 字段 / 错误码:table
|
||||
- 架构 / 调用链:whiteboard / Mermaid
|
||||
- 步骤 / 验收:列表 / checkbox
|
||||
|
||||
## Avoid
|
||||
|
||||
- 只有概念没有接口和边界。
|
||||
- 忽略失败场景、监控和回滚。
|
||||
- 图很多但不说明数据流。
|
||||
- 营销腔描述技术收益。
|
||||
|
||||
## Final check
|
||||
|
||||
- 能否据此实现或排查?
|
||||
- 目标、非目标、边界是否清楚?
|
||||
- 是否有验证、灰度和回滚?
|
||||
35
skills/lark-doc/references/genres/weekly-report.md
Normal file
35
skills/lark-doc/references/genres/weekly-report.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: Weekly Report / 周报月报
|
||||
|
||||
适用:周报、月报、进展同步、工作总结。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先明确读者关心什么:进展、结果、风险、资源诉求还是下周计划。周报不是流水账,要把成果、阻塞和下一步前置。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
核心结论 → 本期进展 → 关键结果 / 数据 → 风险阻塞 → 下期计划 → 需要支持。
|
||||
|
||||
## Style
|
||||
|
||||
简洁、可扫描、面向协作。多写结果和动作,少写忙碌过程。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 进展表:table
|
||||
- 行动项:checkbox
|
||||
- 风险:callout
|
||||
- 数据:table
|
||||
|
||||
## Avoid
|
||||
|
||||
- 流水账。
|
||||
- 只有做了什么,没有结果。
|
||||
- 风险不前置。
|
||||
- 没有下一步和需要支持。
|
||||
|
||||
## Final check
|
||||
|
||||
- 结论是否前置?
|
||||
- 结果和风险是否清楚?
|
||||
- 下一步是否可执行?
|
||||
35
skills/lark-doc/references/genres/white-paper.md
Normal file
35
skills/lark-doc/references/genres/white-paper.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Genre: White Paper / 白皮书
|
||||
|
||||
适用:白皮书、趋势报告、方法论报告、权威长报告、品牌研究报告。
|
||||
|
||||
## Thinking guide
|
||||
|
||||
先建立一个可复用框架:背景问题、关键趋势、分析模型、案例证据和行动方法。白皮书要有体系,而不是长篇软文。
|
||||
|
||||
## Structure patterns
|
||||
|
||||
摘要 → 背景 → 核心框架 → 趋势 / 章节分析 → 案例 / 数据 → 方法建议 → 结论。
|
||||
|
||||
## Style
|
||||
|
||||
权威、系统、克制。允许有品牌观点,但必须由证据和框架支撑。
|
||||
|
||||
## Component bias
|
||||
|
||||
- 框架图:whiteboard
|
||||
- 趋势 / 数据:table
|
||||
- 案例:blockquote / card
|
||||
- 章节摘要:callout
|
||||
|
||||
## Avoid
|
||||
|
||||
- 变成宣传册。
|
||||
- 没有方法论框架。
|
||||
- 案例和数据不可核验。
|
||||
- 篇幅很长但结构松散。
|
||||
|
||||
## Final check
|
||||
|
||||
- 是否有清晰框架?
|
||||
- 结论是否由证据支撑?
|
||||
- 是否适合长期传播或下载?
|
||||
@@ -1,15 +1,24 @@
|
||||
# docs +create(创建飞书云文档)
|
||||
|
||||
> **前置条件(MUST READ):** 生成文档内容前,必须先用 Read 工具读取以下文件,缺一不可:
|
||||
> 1. [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规则(使用 Markdown 格式时改读 [`lark-doc-md.md`](lark-doc-md.md))
|
||||
> 2. [`lark-doc-style.md`](style/lark-doc-style.md) — 写作原则(默认段落、按体裁、组件克制)
|
||||
> 3. [`lark-doc-create-workflow.md`](style/lark-doc-create-workflow.md) — 从零创作工作流(Code-Act Loop、单 Agent 串行撰写)
|
||||
>
|
||||
> **未读完以上文件就生成内容会导致格式错误。**
|
||||
|
||||
从 XML(默认)或 Markdown 内容创建一个新的飞书云文档。
|
||||
|
||||
> **⚠️ 格式选择规则:** 创建 / 导入场景下 XML 和 Markdown 都可以——用户提供 `.md` 本地文件、或明确说"导入 Markdown"时,直接用 Markdown;没有明确指示时默认 XML(表达能力更强,可承载更丰富的结构化内容)。不要在用户没要求的情况下主动从 XML 切到 Markdown,也不要在用户已给出 Markdown 时强行改成 XML。
|
||||
> **⚠️ 格式选择规则:** 创建 / 导入场景下 XML 和 Markdown 都可以——用户提供 `.md` 本地文件、或明确说"导入 Markdown"时,直接用 Markdown;没有明确指示时默认 XML(表达能力更强,支持 callout、grid、checkbox 等富 block 类型)。不要在用户没要求的情况下主动从 XML 切到 Markdown,也不要在用户已给出 Markdown 时强行改成 XML。
|
||||
|
||||
## Document Authoring Loop
|
||||
|
||||
从零创作文档时,以本地草稿文件作为唯一正文源(默认 XML;用户明确要求 Markdown 或提供 `.md` 文件时使用 Markdown),主 Agent 串行完成写作、审阅和修订,草稿达标后再调用 `docs +create`。不要先创建线上骨架再让多个 Agent 并行拆章节写入;这容易造成风格漂移、重复铺陈、结构断流和事实口径不一致。
|
||||
|
||||
1. **Plan(规划)**:明确受众、目的、范围、体裁、结构和验收标准;事实来源或产品口径不确定时先询问用户,能安全假设时说明假设并继续。
|
||||
2. **Draft(起草)**:在当前工作目录创建相对路径草稿文件,主 Agent 按文档顺序串行写作;
|
||||
3. **Observe(观察)**:读取当前草稿,检查结构、重复、断流、事实、语气和格式;
|
||||
4. **Revise(修订)**:局部修订本地文件,优先调整顺序、补桥接句、删重复、统一术语和修正 XML;不追求第一稿一次完美。
|
||||
5. **Enrich(增强)**:决定一篇文档是否真正好读、可信、可传播。必要时加入表格、列表、callout、grid、画板、引用或附件;增强必须降低理解成本,不为了“丰富”而装饰。
|
||||
- 新增画板按复杂度处理:简单 Mermaid 或 SVG 图可由主 Agent 直接写入本地 XML 草稿;复杂 SVG 可启动 SubAgent 只产出完整 `<whiteboard type="svg">...</whiteboard>` 片段;特别复杂或已有画板更新,再创建空白画板并让 SubAgent 读取 `lark-whiteboard` 完成写入。
|
||||
- 创建后才能插入的图片、附件或资源,先在草稿中标记位置和素材路径,Deliver 后用 `docs +media-insert` 等命令补齐并 fetch 验证。
|
||||
6. **Validate(验证)**:检查用户要求、字数、格式、事实、可读性和风格;任何一项不满足,就回到 Observe / Revise 继续 loop。
|
||||
7. **Deliver(交付)**:本地草稿通过验证后,用 `docs +create --content @lark-doc-draft.xml` 创建 XML 文档;Markdown 草稿用 `docs +create --doc-format markdown --content @lark-doc-draft.md`。如需创建后插入图片、附件或资源,完成后再次 fetch 确认。
|
||||
|
||||
`@file` 只接受当前工作目录下的相对路径,不要传 `@/tmp/xxx.xml` 这类绝对路径。SubAgent 只用于边界清晰的视觉或资源片段(如完整 SVG whiteboard XML),主 Agent 必须审核并合并结果;不要让 SubAgent 并行撰写正文。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -65,15 +74,9 @@ lark-cli docs +create --doc-format markdown --title "项目计划" --content $'#
|
||||
| `--parent-token` | 否 | 父文件夹或知识库节点 token(与 `--parent-position` 互斥) |
|
||||
| `--parent-position` | 否 | 父节点位置,如 `my_library`(与 `--parent-token` 互斥) |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- **较长文档**:参考 [`lark-doc-create-workflow.md`](style/lark-doc-create-workflow.md) 先建骨架再分段写入;短文档可一次写完整内容
|
||||
- **表达形式**:由用户目标和内容决定。需要结构化表达时可参考 [`lark-doc-style.md`](style/lark-doc-style.md),但不要默认套用固定开头、固定富 block 比例或固定图表
|
||||
|
||||
## 参考
|
||||
|
||||
- [`lark-doc-create-workflow.md`](style/lark-doc-create-workflow.md) — 从零创作工作流(Code-Act Loop、单 Agent 串行撰写)
|
||||
- [`lark-doc-style.md`](style/lark-doc-style.md) — 文档写作原则(默认段落、按体裁、组件克制)
|
||||
- [`lark-doc-design-philosophy.md`](lark-doc-design-philosophy.md) — 文档设计判断与组件取舍
|
||||
- [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规范
|
||||
- [`lark-doc-fetch.md`](lark-doc-fetch.md) — 获取文档
|
||||
- [`lark-doc-update.md`](lark-doc-update.md) — 更新文档
|
||||
|
||||
120
skills/lark-doc/references/lark-doc-design-philosophy.md
Normal file
120
skills/lark-doc/references/lark-doc-design-philosophy.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Lark Doc Design Philosophy
|
||||
|
||||
- 把飞书云文档当作帮助读者完成理解、判断、决策或行动的界面,而不是内容容器。你的任务不是把信息铺满页面,而是为具体读者设计一条清晰、可信、可执行的阅读路径。
|
||||
- 本文件负责文档设计判断:读者任务、写作循环、结构取舍、表达组件、一级体裁路由、反模板化和质量自检。具体执行流程见 `lark-doc-create.md` 和 `lark-doc-update.md`;XML / Markdown 语法见 `lark-doc-xml.md` / `lark-doc-md.md`;体裁路由按下方表格读取一个 `genres/_router-*.md`。除 `genres/_router-workplace.md` 和 `genres/_router-report.md` 外,router 本身就是一级大类模板,不再继续读取具体体裁文件。
|
||||
|
||||
## 0. Priority order
|
||||
|
||||
当目标冲突时,按此顺序取舍:**事实准确性 > 信息层级 > 可扫描性 > 协作可执行性 > 视觉整洁 > 个性化表达**。事实不确定就标注假设 / 待确认;结构混乱先调信息层级;视觉和个性只能增强理解,不能牺牲事实、结构和执行。
|
||||
|
||||
## 1. Authoring loop
|
||||
|
||||
非微编辑的创建、重写、润色、排版或生成较多正文任务,先走一轮按复杂度缩放的写作循环。简单润色 / 排版只做快速内部检查;复杂创建、重写、汇报、决策或传播文档才展开候选方案。
|
||||
|
||||
1. **集思广益**:基于用户目标和材料,在内部快速构思 2-3 个候选组织方式;复杂文档可比较决策稿、执行说明、知识库、传播稿等形态,简单任务只保留一个最自然的结构。
|
||||
2. **探索**:确认读者、文档工作、事实来源、已有风格和协作约束;编辑已有文档时,额外确认必须保留的评论、图片、附件、画板或资源块。事实不确定时标注假设 / 待确认,不要编造确定性。
|
||||
3. **计划**:按下方体裁路由选择一个最匹配的 router,确定信息顺序、证据密度、语气、组件集合,以及是否需要且最多一个主组织锚点。每个 rich block 都必须能回答:它降低了什么理解、比较、判断或行动成本?
|
||||
4. **批判性思考(写前)**:检查计划是否像任何类似请求都能套用的通用模板,是否跳过了关键读者任务,是否过度组件化,是否会破坏已有结构或资源块;发现问题先修改计划。
|
||||
5. **构建**:按修改后的计划执行 create / update。标题、顺序、列表、表格、callout、grid、whiteboard 都从真实信息关系推导,不为了“丰富”“高级”“专业”临时加装饰。
|
||||
6. **再次批判性思考(写后)**:创建后复读草稿 / 结果,更新后 fetch 验证;检查事实保真、结构推进、组件必要性、术语一致、阅读负担和下一步是否清晰;发现不服务当前读者、任务或体裁的亮点 / 装饰,就删除或降级。
|
||||
|
||||
## 2. Ground it in the document job
|
||||
|
||||
写作或改写前,先判断这篇文档的工作:
|
||||
|
||||
- 读者是谁:决策者、执行者、评审者、客户、开发者、运营者,还是未来维护者?
|
||||
- 读者要完成什么:理解背景、做出决策、执行任务、评审方案、排查问题、学习方法、传播内容,还是留档备查?
|
||||
- 读完后读者应该能做什么:拍板、执行、确认风险、复用、转发、评论、排查或完成任务?
|
||||
- 文档寿命是什么:一次性沟通、项目周期内维护、长期知识库、正式归档,还是外部传播?
|
||||
|
||||
如果用户没有明确这些信息,能安全假设时说明假设并继续;事实来源、产品口径、结论归属不确定时,先询问或标注待确认,不要编造确定性。
|
||||
|
||||
## 3. Route by genre
|
||||
|
||||
体裁决定结构、语气、证据密度和表达方式。不要用同一套“背景 / 目标 / 方案 / 风险 / 下一步”应对所有任务。
|
||||
|
||||
读取规则:
|
||||
|
||||
1. 只选择一个最匹配的路径读取。
|
||||
2. 多数 `_router-*` 文件本身就是一级大类模板,读完即停。
|
||||
3. 只有 `genres/_router-workplace.md` 和 `genres/_router-report.md` 保留二类 routing;当 router 明确命中具体体裁时,只额外读取一个具体体裁文件。
|
||||
4. 简单编辑已有文档时,优先沿用原文体裁和风格,不额外读取体裁文件。
|
||||
|
||||
| 路径 | 类别 | 用户意图 |
|
||||
|---|---|---|
|
||||
| `genres/_router-workplace.md` | 职场协作 | 周报、方案、纪要、复盘、PRD、技术文档、SOP、公文、正式协作文档 |
|
||||
| `genres/_router-creative.md` | 文学故事 | 网文、短篇故事、同人、互动小说、剧本、故事大纲 |
|
||||
| `genres/_router-media.md` | 资讯媒体 | 新闻、快讯、深度报道、人物报道、访谈、媒体稿 |
|
||||
| `genres/_router-opinion.md` | 观点评论 | 时评、商业评论、文化评论、专栏、随笔、杂文、观点文章 |
|
||||
| `genres/_router-knowledge.md` | 知识教程 | 科普、教程、指南、攻略、FAQ、资源合集、知识库 |
|
||||
| `genres/_router-report.md` | 报告研究 | 行业报告、数据报告、调研报告、白皮书、商业分析、竞品分析 |
|
||||
| `genres/_router-consumer.md` | 种草消费 | 小红书笔记、测评、合集、好物分享、探店、生活方式 |
|
||||
| `genres/_router-marketing.md` | 营销转化 | 广告、软文、详情页、带货、活动、公关、私域话术、转化文案 |
|
||||
| `genres/_router-personal-brand.md` | 个人品牌 | 简历、自我介绍、作品集、朋友圈、个人主页、成长复盘 |
|
||||
| `genres/_router-platform.md` | 平台原生 | 用户明确要求适配公众号、知乎、微博、B站、抖音、豆瓣、Newsletter 等平台形态 |
|
||||
|
||||
如果任务混合多个方向,优先选择决定**内容结构**的 router;平台只是发布载体,除非用户明确要求“改成某平台内容 / 按某平台写法”,否则不要为了平台语气读取平台 router。例如“写给领导看的技术方案”优先读 `_router-workplace.md`,并采用正式克制语气;“把技术方案改成公众号文章”优先读 `_router-platform.md`,同时保留技术事实准确性。
|
||||
|
||||
## 4. Structure is information
|
||||
|
||||
- 结构不是装饰。标题、顺序、列表、表格、callout、grid、画板和引用块,都应该表达真实的信息关系。
|
||||
- 选择结构前先判断:这是顺序、并列、对比、因果、层级、状态、证据,还是行动关系?读者需要先知道什么,才能理解后面的内容?哪些信息应该前置为结论,哪些适合压缩或放到附录?
|
||||
- 文档开头不是固定的“背景”。决策文档先给结论和待拍板问题;执行文档先给目标、范围和时间线;复盘文档先给事实结论、影响和后续动作;技术文档先给适用范围、前置条件和快速路径;传播文档先给痛点、利益点或钩子。
|
||||
- 较长、复杂或面向传播 / 汇报 / 决策的文档,最好有一个主组织锚点,如一句话结论、决策矩阵、风险雷达、路线图、架构图、时间线、对比表或 checklist。只保留一个,不要到处制造亮点;短文档、微编辑、正式归档或简单 SOP 不必强行设计。锚点必须服务事实准确、信息层级和协作执行,不能为了个性化表达增加理解成本。
|
||||
|
||||
## 5. Rich blocks must earn their place
|
||||
|
||||
飞书富 block 是表达手段,不是装饰指标。不要为了“丰富”“高级”“专业”而加入固定比例的 callout、grid、table 或 whiteboard,优先选择最能降低理解成本的表达方式。
|
||||
|
||||
| 信息关系 / 场景 | 优先表达 |
|
||||
|---|---|
|
||||
| 判断、背景、解释、理由 | 段落 / 小标题 |
|
||||
| 并列项、短步骤、注意点 | 列表 |
|
||||
| 待办、检查项、验收项 | checkbox |
|
||||
| 多对象、多字段、排期、指标、接口参数 | table |
|
||||
| 少数组并列信息、轻量对比、模块卡片 | grid |
|
||||
| 风险、限制、例外、强约束、待确认 | callout |
|
||||
| 外部意见、原话、会议纪要、引用材料 | blockquote |
|
||||
| 代码、命令、配置、日志 | pre / code |
|
||||
| 流程、架构、依赖、路线图、因果链路、复杂模型 | whiteboard |
|
||||
|
||||
- 组件由信息关系决定,不由重要度决定。重要结论可以是段落、callout、表格或画板,选择标准是它能否让读者更快理解、比较、判断或行动。
|
||||
- 关键章节需要判断是否存在图示机会,画板用于图示明显降低理解成本的关系:流程、架构、依赖、时间线、因果、分层、组织关系、复杂对比。结构简单或文字更清楚时,不要强行画板化。具体 Mermaid / SVG / SubAgent 路由见 `lark-doc-whiteboard.md`。
|
||||
- 颜色只在帮助识别语义时使用;正式文档、技术文档、长期知识库和归档材料优先保持朴素。
|
||||
|
||||
## 6. Writing style
|
||||
|
||||
- 从读者一侧写,而不是从系统实现一侧写。用读者能识别和控制的事物命名,默认使用具体、主动、可执行的表达。
|
||||
- 具体比聪明更好。一致比花哨更重要。文档词汇是读者导航的路标,同一动作、状态、对象在全文中保持同名。
|
||||
|
||||
## 7. Calibrate against AI-doc defaults
|
||||
|
||||
写作或改写前,主动避开这些 AI 文档默认值:
|
||||
|
||||
1. 不管任务是什么都套“背景 / 目标 / 方案 / 风险 / 下一步”。
|
||||
2. 用“赋能、闭环、抓手、沉淀、拉通、对齐”等空泛词替代具体动作。
|
||||
3. 开头写摘要,但没有结论、取舍、风险或行动。
|
||||
4. 为了“丰富”强行加入 callout、table、grid 或 whiteboard。
|
||||
5. 把所有重要信息都画板化,或者把简单关系复杂图示化。
|
||||
6. 每篇文档都强行设计一句话结论、决策矩阵、风险雷达、路线图等主组织锚点。
|
||||
7. 标题整齐,但章节之间没有真实推进关系。
|
||||
8. 章节之间像多个 Agent 拼接,术语、语气、事实口径不一致。
|
||||
9. 改写已有文档时全文覆盖,破坏评论、引用、图片、画板或资源块。
|
||||
|
||||
如果你的计划看起来像面对任何类似请求都会写出的文档,就修改它,使结构、语气和组件选择更贴合当前读者、任务和体裁。
|
||||
|
||||
## 8. Final self-check
|
||||
|
||||
交付前检查:
|
||||
|
||||
| 指标 | 自检问题 |
|
||||
|---|---|
|
||||
| 读者任务 | 这篇文档是否帮助目标读者完成理解、判断、决策或行动? |
|
||||
| 体裁匹配 | 结构、语气和证据密度是否符合当前体裁? |
|
||||
| 信息结构 | 标题、顺序、列表、表格和组件是否表达真实关系? |
|
||||
| 阅读负担 | 是否有段落过长、层级过深、表格过宽或组件过多? |
|
||||
| 组件必要性 | callout、grid、table、whiteboard 是否真的降低理解成本? |
|
||||
| 风格一致 | 是否延续用户给定样例或已有文档风格? |
|
||||
| 事实保真 | 改写时是否保留事实、引用、图片、附件、资源块和关键措辞? |
|
||||
| 行动清晰 | 读者下一步应该做什么是否明确? |
|
||||
| 反模板化 | 是否拒绝加入或删除了不服务当前读者、任务或体裁的通用亮点 / 装饰? |
|
||||
@@ -1,13 +1,6 @@
|
||||
|
||||
# docs +update(更新飞书云文档)
|
||||
|
||||
> **前置条件(MUST READ):** 生成文档内容前,必须先用 Read 工具读取以下文件,缺一不可:
|
||||
> 1. [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规则(使用 Markdown 格式时改读 [`lark-doc-md.md`](lark-doc-md.md))
|
||||
> 2. [`lark-doc-style.md`](style/lark-doc-style.md) — 写作原则(默认段落、按体裁、组件克制)
|
||||
> 3. [`lark-doc-update-workflow.md`](style/lark-doc-update-workflow.md) — 改写增强工作流(Code-Act Loop、单 Agent 串行改写)
|
||||
>
|
||||
> **未读完以上文件就生成内容会导致格式错误。**
|
||||
|
||||
通过八种指令精确更新飞书云文档。支持字符串级别和 block 级别的操作。
|
||||
|
||||
> **⚠️ 格式选择规则:**
|
||||
@@ -16,6 +9,26 @@
|
||||
>
|
||||
> **Markdown 局限 & block ID 前提:** Markdown 不携带 block ID,也无样式(颜色、对齐、callout 等)。需要按 block ID 定位(`block_*` 指令的 `--block-id`)时,先 `docs +fetch --detail with-ids` **配合 `--scope`(`outline` / `range` / `keyword` / `section`)局部获取**目标段落,不要全量 fetch。拿到 block ID 后 `--content` 仍可用 Markdown,只是写入内容不带样式。
|
||||
|
||||
## Observe-Diagnose-Patch Loop
|
||||
|
||||
适用于修改已有飞书文档:调整语气、精简冗余、增补章节、修复结构混乱、按领导意见修改,或在已有图片、引用、表格、评论、资源块的文档上做保真改写。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 核心原则:先观察,再诊断,再局部 patch,最后 fetch 验证。这个流程比全文重写安全;除非用户明确要求完全重建,或文档确实已无保留价值,不要轻易使用 `overwrite`,否则会丢失评论和未支持的资源。
|
||||
> 每次 `docs +update` 后,都按 block ID 已发生变更处理。如果需要继续修改、重复修改同一处内容,或引用刚插入 / 替换后的内容,必须先重新 `docs +fetch --detail with-ids` 拉取最新内容和 block ID,再执行下一轮 patch。
|
||||
|
||||
1. **Observe(读取现状)**:先 `docs +fetch` 读取当前文档状态,并按意图选择最小范围。
|
||||
- 改某一节或大文档:先 `--scope outline --max-depth 2` 找章节,再 `--scope section --start-block-id <标题id> --detail with-ids`
|
||||
- 精确跨节区间:用 `--scope range --start-block-id xxx --end-block-id yyy`
|
||||
- 只有模糊关键词:用 `--scope keyword --keyword xxx --context-before 1 --context-after 1 --detail with-ids`
|
||||
- 明确整篇重构才读 `--detail with-ids` 全文;只读摘要或确认事实时用更轻的 fetch
|
||||
2. **Diagnose(诊断问题)**:判断用户目标、当前结构、语气、重复、断流、事实口径和需要保留的资源;识别哪些 block 必须原样保留。
|
||||
3. **Patch Plan(制定局部计划)**:把修改拆成最小安全操作:简单行内替换用 `str_replace`;整段/整块重写用 `block_replace`;增补章节用 `block_insert_after`;删冗余用 `block_delete`;调整顺序用 `block_move_after`。
|
||||
4. **Patch(精确修改)**:按 block / section 执行局部命令。保护 `<cite>`、`<img>`、`<source>`、`<whiteboard>`、`<sheet>`、`<bitable>`、`<synced_reference>` 等 token 化内容,不要改成纯文本或占位符。同一 block 的多处修改合并成一次 `block_replace`。
|
||||
5. **Verify(fetch 验证)**:每轮写操作后按影响范围重新 fetch,检查用户要求、结构、语气、事实、资源块和 block ID 是否符合预期;不满足就基于最新 fetch 结果继续 Diagnose / Patch,不要沿用上一轮 block ID。
|
||||
|
||||
复杂结构重组时,优先“先插入新结构,再删除旧 block”:用 `block_insert_after` 插入 grid / table / callout / 新章节,再用 `block_delete` 删除旧段落。这样比 `overwrite` 更能保住图片、评论、引用、资源块和不相关内容。`str_replace` 的匹配范围取决于格式:XML 模式只适合行内匹配;Markdown 模式可跨行和使用 `前缀...后缀`,但跨 block 或容器级重写仍优先用 block 指令。
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
@@ -45,12 +58,12 @@
|
||||
|
||||
## Block ID 生命周期
|
||||
|
||||
写操作后不要默认复用之前 fetch 到的 block ID:
|
||||
安全规则:每次写操作后都按 block ID 已变更处理。需要连续修改、重复修改或操作新插入内容时,必须重新 fetch 最新内容和 block ID;不要默认复用之前 fetch 到的 block ID。
|
||||
|
||||
- `overwrite` / `block_replace` / `block_delete`:受影响旧 ID 失效,继续 block 级操作前重新 fetch
|
||||
- `block_insert_after` / `append` / `block_copy_insert_after`:锚点 / 源 ID 通常保留,新内容是新 ID;要操作新内容先重新 fetch
|
||||
- `block_move_after`:被移动 ID 通常保留,但位置、章节、range 语义变化;后续依赖位置时重新 fetch
|
||||
- `str_replace`:简单行内替换通常不改变 ID;跨行 / 大段替换后如继续 block 级操作,先重新 fetch
|
||||
- `overwrite` / `block_replace` / `block_delete`:受影响旧 ID 失效,继续 block 级操作前必须重新 fetch
|
||||
- `block_insert_after` / `append` / `block_copy_insert_after`:新内容一定是新 ID;要操作新内容或继续编辑插入点附近内容,先重新 fetch
|
||||
- `block_move_after`:位置、章节、range 语义已变化;后续依赖位置或章节边界时重新 fetch
|
||||
- `str_replace`:即使是简单行内替换,也不要在后续 block 级操作中假设旧 ID 仍正确;跨行 / 大段替换后必须重新 fetch
|
||||
|
||||
## 指令示例
|
||||
|
||||
@@ -197,63 +210,15 @@ lark-cli docs +update --doc "<doc_id>" --command block_move_after \
|
||||
| `warnings` | 警告信息列表 |
|
||||
| `document.new_blocks` | 本次操作新增的 block 列表(如画板)。`block_id` 可用于后续精确编辑;`block_token` 是资源块 token(如画板)可交给 `lark-whiteboard` 等 skill 继续操作 |
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 精确 block 级更新
|
||||
|
||||
1. **获取文档内容和 block ID**:
|
||||
```bash
|
||||
lark-cli docs +fetch --doc "<doc_id>" --detail with-ids
|
||||
```
|
||||
|
||||
2. **定位目标 block**:从返回的 XML 中找到要修改的 block 及其 `id` 属性
|
||||
|
||||
3. **执行更新**:
|
||||
```bash
|
||||
# 替换特定 block
|
||||
lark-cli docs +update --doc "<doc_id>" --command block_replace \
|
||||
--block-id "blkcnXXXX" --content "<p>新内容</p>"
|
||||
|
||||
# 在某 block 后插入
|
||||
lark-cli docs +update --doc "<doc_id>" --command block_insert_after \
|
||||
--block-id "blkcnXXXX" --content "<h2>追加的章节</h2>"
|
||||
```
|
||||
|
||||
### 简单文本替换
|
||||
|
||||
不需要 block ID,直接匹配替换:
|
||||
|
||||
```bash
|
||||
lark-cli docs +update --doc "<doc_id>" --command str_replace \
|
||||
--pattern "v1.0" --content "v2.0"
|
||||
```
|
||||
|
||||
## 画板处理
|
||||
|
||||
> **`docs +update` 不能直接编辑已有画板的内容。** 本命令只能**新增**画板块;要修改已有画板,先用 `docs +fetch` 取到 `<whiteboard token="...">`,再按 [`lark-doc-whiteboard.md`](lark-doc-whiteboard.md) 启动 SubAgent 读取 [`lark-whiteboard`](../../lark-whiteboard/SKILL.md) 并写入。
|
||||
|
||||
画板的语法选型与插入示例见 [`lark-doc-xml.md`](lark-doc-xml.md) 与 [`lark-doc-whiteboard.md`](lark-doc-whiteboard.md)。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- **精确操作优于全文覆盖**:使用 `block_replace`/`block_insert_after` 精确修改,避免 `overwrite` 全文覆盖
|
||||
- **str_replace 的匹配范围取决于格式**:
|
||||
- **XML 模式(默认)**:`--pattern` 只支持**行内**匹配,不支持跨行 / 跨 block。段落、整块或容器级(列表、表格、分栏、引用块等)改动请改用 `block_replace` 指定 block_id 重建。
|
||||
- **Markdown 模式**(`--doc-format markdown`):`--pattern` 同时支持**行内和跨行**匹配,还支持 `前缀...后缀` 省略号语法(用 `...` 串联首尾片段匹配一大段内容),可以一次替换多行文本;但仍建议优先按最小片段匹配,跨 block 容器级重写仍优先用 `block_replace`,避免副作用。
|
||||
- **保护不可重建的内容**:图片、画板、电子表格等以 token 形式存储,替换时避开这些 block
|
||||
- **str_replace 的 replacement 支持富文本**:可以用行内标签 `<b>`、`<a>`、`<cite>`、`<latex>` 等替换普通文本为富文本
|
||||
- **同一 block 只能被 replace 一次**:多次修改同一 block 请合并为一次 block_replace
|
||||
- **block_delete 支持批量**:用逗号分隔多个 block_id 一次删除
|
||||
- **复杂结构重组**:将多个段落转换为 grid / table 等复杂布局时,分步操作比 overwrite 更安全:
|
||||
1. 用 `block_insert_after` 在目标位置插入新的富文本结构
|
||||
2. 用 `block_delete` 批量删除旧的 block
|
||||
3. 这样可以保留文档中其他不相关的内容(图片、评论等)
|
||||
- **表达形式**:插入或替换内容时,优先沿用用户要求和已有文档风格;需要结构化表达时可参考 [`lark-doc-style.md`](style/lark-doc-style.md),但不要为了固定丰富度主动添加组件
|
||||
新增画板的语法选型见 [`lark-doc-xml.md`](lark-doc-xml.md) 的资源块说明;插入和复杂画板处理见 [`lark-doc-whiteboard.md`](lark-doc-whiteboard.md)。
|
||||
|
||||
## 参考
|
||||
|
||||
- [`lark-doc-update-workflow.md`](style/lark-doc-update-workflow.md) — 改写增强工作流(Code-Act Loop、单 Agent 串行改写)
|
||||
- [`lark-doc-style.md`](style/lark-doc-style.md) — 文档写作原则(默认段落、按体裁、组件克制)
|
||||
- [`lark-doc-design-philosophy.md`](lark-doc-design-philosophy.md) — 文档设计判断与组件取舍
|
||||
- [`lark-doc-xml.md`](lark-doc-xml.md) — XML 语法规范
|
||||
- [`lark-doc-fetch.md`](lark-doc-fetch.md) — 获取文档
|
||||
- [`lark-doc-create.md`](lark-doc-create.md) — 创建文档
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
| Skill | 核心职责 | 约束 |
|
||||
|-------------------|-----------------------------------------------------------|---------------------------------|
|
||||
| `lark-doc` | 识别画板机会、使用 Mermaid/SVG 创建图表、调度 SubAgent、插入简单 SVG 画板或复杂空白画板 | 主 Agent 不直接创作画板内容; |
|
||||
| `lark-doc` | 识别画板机会、使用 Mermaid/SVG 创建图表、调度 SubAgent、插入简单图表或复杂空白画板 | 简单图可由主 Agent 直接写入;复杂图再隔离到 SubAgent |
|
||||
| `lark-whiteboard` | 查询/导出已有画板;复杂图表生成(Mermaid/DSL/SVG 路由、场景选型、渲染验证);写入已有/空白画板 | 仅特别复杂的图表或已有画板更新时由独立 SubAgent 读取 |
|
||||
|
||||
## 画板适用规则
|
||||
@@ -29,11 +29,9 @@
|
||||
> [!IMPORTANT]
|
||||
> ⚠️ **分别对每个图表进行决策**
|
||||
|
||||
如果有多个位置需要插入图表,你需要根据每个图表的内容**分别决定**采用步骤 2A 还是 2B
|
||||
中的方式插入这个图表。在需要插入思维导图、时序图、类图、饼图、甘特图的时候可以插入 mermaid 块,在需要插入其他类型图表时启动
|
||||
SubAgent 插入 SVG。
|
||||
如果有多个位置需要插入图表,你需要根据每个图表的内容**分别决定**采用步骤 2A 还是 2B。思维导图、时序图、类图、饼图、甘特图可插入 mermaid 块;其他类型图表使用 SVG,简单图由主 Agent 直接写入,复杂图再启动 SubAgent。
|
||||
|
||||
建议优先使用 SVG 插入图表,除非其属于思维导图、时序图、类图、饼图、甘特图这类可以直接使用 mermaid 语法描述,且不适宜用 SVG 绘制的图表
|
||||
简单 Mermaid / SVG 图可由主 Agent 直接写入本地 XML;需要专门视觉设计、信息密度较高或容易布局翻车的 SVG,再启动 SubAgent 产出完整片段。
|
||||
|
||||
### 步骤 2A: 使用 mermaid 插入图表
|
||||
|
||||
@@ -44,9 +42,9 @@ SubAgent 插入 SVG。
|
||||
</whiteboard>
|
||||
```
|
||||
|
||||
### 步骤 2B: SubAgent 使用 SVG 插入图表
|
||||
### 步骤 2B: 使用 SVG 插入图表
|
||||
|
||||
主 Agent 启动 SubAgent,让它用 `docs +create` / `docs +update` 插入:
|
||||
简单 SVG 可由主 Agent 直接写入草稿;复杂 SVG 则启动 SubAgent,让它用 `docs +create` / `docs +update` 插入:
|
||||
|
||||
```xml
|
||||
|
||||
@@ -56,7 +54,7 @@ SubAgent 插入 SVG。
|
||||
</whiteboard>
|
||||
```
|
||||
|
||||
Sub Agent 需要携带以下的最小上下文,以及后续的 [SVG 设计 Workflow] 章节指南:
|
||||
复杂 SVG SubAgent 需要携带以下的最小上下文,以及后续的 [SVG 设计 Workflow] 章节指南:
|
||||
|
||||
- doc token、插入位置(标题 / block_id / command)
|
||||
- 图表目标、受众、源段落或数据
|
||||
|
||||
@@ -41,7 +41,7 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
|
||||
文档中可嵌入外部资源块(属于容器标签的特殊形式),需要额外语法创建:
|
||||
|
||||
- `<img>` — `<img href="https://..."/>` 上传网络图片
|
||||
- `<whiteboard>` — 简单图由 SubAgent 直接插入 `<whiteboard type="svg">完整自包含 SVG</whiteboard>`;复杂图使用 `<whiteboard type="blank"></whiteboard>` 先创建空白画板,再按 [`lark-doc-whiteboard.md`](lark-doc-whiteboard.md) 启动 SubAgent 调用 `lark-whiteboard` 写入;
|
||||
- `<whiteboard>` — 简单 Mermaid / SVG 图可由主 Agent 直接写入;复杂 SVG 可启动 SubAgent 产出完整 `<whiteboard type="svg">完整自包含 SVG</whiteboard>`;特别复杂或已有画板更新,使用 `<whiteboard type="blank"></whiteboard>` 先创建空白画板,再按 [`lark-doc-whiteboard.md`](lark-doc-whiteboard.md) 启动 SubAgent 调用 `lark-whiteboard` 写入;
|
||||
- `<sheet>` — `<sheet type="blank"></sheet>` 空白;`<sheet sheet-id="SID" token="TOKEN"></sheet>` 复制已有
|
||||
- `<task>` — `<task task-id="GUID"></task>`,必传 task-id(任务 guid)
|
||||
- `<chat_card>` — `<chat_card chat-id="CHAT_ID"></chat_card>`,必传 chat-id
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# 从零创作工作流
|
||||
|
||||
用户提供主题、需求或简要说明,需要生成一份新的飞书文档时,遵循本工作流。
|
||||
|
||||
## 核心方法论 — Code-Act Loop
|
||||
|
||||
通过自适应的 **Code-Act Loop** 驱动文档创作,而非固定模板式的工作流。每次任务都循环执行:
|
||||
|
||||
1. **Plan(规划)** — 根据用户目标和文档当前状态,评估下一步该做什么
|
||||
2. **Execute(执行)** — 由主 Agent 自己运行 `lark-cli docs` 命令推进正文;仅画板渲染按需隔离到 SubAgent(见步骤三)
|
||||
3. **Observe(观察)** — 检查命令输出,验证正确性,确认内容是否满足用户目标
|
||||
4. **Iterate(迭代)** — 如需调整,回到 Plan 继续循环
|
||||
|
||||
循环在文档达到质量标准且满足用户需求时结束。不要试图一次性产出完美内容——迭代打磨效果更好。根据用户实际需求灵活决定文档结构和版块,而不是套用固定模板。
|
||||
|
||||
|
||||
## 典型 Code-Act Loop 流程
|
||||
|
||||
### 步骤一:规划与撰写(单 Agent 串行)
|
||||
|
||||
正文由主 Agent 串行维护,**不按章节拆给并行 Agent**,避免上下文割裂、重复矛盾和全文级约束失效。
|
||||
|
||||
1. 分析用户需求:受众、目的、范围
|
||||
2. 设计大纲:根据任务自然选择结构。可以是短文、纪要、FAQ、方案、报告、清单或其他形式;不要默认套固定章节、固定开头或固定富 block 配比
|
||||
3. `docs +create` 创建并撰写:
|
||||
- **短文档**:一次写入完整内容。使用 Markdown 时,避免同时传入 `--title` 和同名 `# 标题`
|
||||
- **长文档**:先建骨架(标题 + 各级标题),再由主 Agent **顺序逐节**用 `block_insert_after --block-id <章节标题 block_id>` 补全正文;写完一节再写下一节,始终带着已写内容的上下文,保证衔接、不重复
|
||||
- ⚠️ 不要一次性把超长完整内容塞进 `--content`,容易触发字符/参数限制;长文按节分次写入
|
||||
- ⚠️ 同一节内多次插入时,要锚到**上一个新插入的 block**(按 [`lark-doc-update.md`](../lark-doc-update.md) 的「Block ID 生命周期」),否则反复锚同一个标题会让段落顺序颠倒
|
||||
- ⚠️ 若先建骨架写了占位摘要,补正文时**删除占位摘要**,不要留残渣
|
||||
- ⚠️ **`@file` 路径限制**:`--content @file` 只接受当前工作目录下的相对路径,传绝对路径(如 `@/tmp/xxx.md`)会报 `unsafe file path`。需要落盘时,将文件写在 cwd 下,用完自行清理
|
||||
|
||||
### 步骤二:整合审查与画板识别(串行)
|
||||
|
||||
4. `docs +fetch --api-version v2 --detail with-ids` 获取文档,审查整体效果
|
||||
5. 评估内容是否满足用户目标:事实是否完整、结构是否清楚、语气是否匹配、是否保留必要素材;检查跨节有无重复、矛盾或断流。再按 `lark-doc-style.md` 的「写完自检」快速核对,发现问题就地定向修正
|
||||
6. **画板识别**:逐章节扫描,判断是否有段落用图明显比文字更易懂(流程 / 架构 / 时间线 / 对比 / 占比等,见 `lark-doc-style.md` 的画板原则)。默认用文字,只有确需图示才记录需要插图的章节、推荐画板类型、mermaid/SVG 路径和用于画图的源内容
|
||||
|
||||
### 步骤三:画板处理与润色
|
||||
|
||||
7. **优先处理步骤二识别出的画板需求**:读取并按 [lark-doc-whiteboard.md](../lark-doc-whiteboard.md) 选型和插入;正文本身不交给 SubAgent
|
||||
8. 由**主 Agent 自行润色**(不另起内容子 Agent,正文始终一人维护):文字密集且不易读时,优先拆段、加小标题或调整顺序——叙述内容保持成段,**不要默认改成列表**,只有确属并列要点 / 步骤才用列表(见 `lark-doc-style.md`);只有确实存在行列数据时才用 `<table>`。其余富 block 的取舍一律遵循 `lark-doc-style.md` 的写作原则,不主动堆叠。需要明显分隔的主题可补充 `<hr/>`,不强制章节间都使用。本地图片使用 `docs +media-insert` 插入
|
||||
|
||||
### 步骤四:专项校验
|
||||
|
||||
9. **字数门禁**:如果用户给出任何明确字数要求(如“700-800 字”“1000 字左右”“不少于 500 字”“控制在 800 字以内”),本步骤必须执行,不属于按需项。读取并执行 [`lark-doc-word-stat.md`](../lark-doc-word-stat.md) 的「字数遵循校验」;未得到脚本统计结果前,不得向用户声明“符合字数要求”。若没有明确字数要求,则跳过本项,不读取该 workflow。若执行了专项校验,向用户呈现目标区间、`word_count` 和达标结论
|
||||
10. **重复标题检查**:文档生成后,检查文档标题和正文第一个标题块是否重复;若重复,删除或改写正文第一个标题块,避免读者看到同一标题连续出现
|
||||
@@ -1,68 +0,0 @@
|
||||
# 飞书文档写作原则
|
||||
|
||||
写飞书文档,像一个该领域资深的人类作者那样写,而不是把内容"装配"成组件。
|
||||
本文只讲"何时用、什么风格";具体标签 / 命令语法见 [`lark-doc-xml.md`](../lark-doc-xml.md)。
|
||||
|
||||
## 一、用户明确要求优先
|
||||
|
||||
用户点名要某种格式——高亮块、分栏、列表、某编号体例、表格、画板、某模板、某已有文档的风格——**一律照用户的来,下面的"默认克制"全部让位**。用户给了样例或已有文档,就沿用它的结构与语气。
|
||||
|
||||
## 二、默认写连贯段落
|
||||
|
||||
用户没指定时,**默认是连贯段落**;其余按内容类型分流,别一律"少用结构",也别什么都升标题:
|
||||
|
||||
| 内容 | 用什么 | ❌ 别 |
|
||||
|---|---|---|
|
||||
| 叙述、论证、分析、说明 | **连贯段落** | 拆成列举 |
|
||||
| 真·行列数据(预算、指标、对比、排期、字段说明) | **表格** | 写成段落或把字段堆成一行 |
|
||||
| 字段:值(主题、时长、负责人等,少量) | **加粗标签行**或一句话 | 每字段一个标题 |
|
||||
| 方法 / 措施 + 每项一段描述 | **加粗引导句段落**(「**全程督导。**…」) | 每项升标题 |
|
||||
| 任务清单 / 检查项 / 待办事项 | **`<checkbox>`** | 用普通列表替代可交互待办 |
|
||||
| 纯短并列项(无描述,如材料清单) | 列表 | — |
|
||||
| 章节(内容成块、需在目录导航) | 标题层级 | — |
|
||||
|
||||
- 判断标准:**去掉结构后能顺成段落,就用段落;成行成列的数据,就用表格。**
|
||||
- **红线一:标题层级只给"章节"。** "小标题 + 一两句话"的小项(字段、方法、要点)不该占标题层级——按上表降成标签行 / 加粗引导句段落(否则目录里全是没信息量的条目)。
|
||||
- **红线二:列举(「一是 / 二是」「第一 / 第二」「(1)(2)(3)」)只给真正并列的具体项,且别每节都用。**
|
||||
- 「一是 / 二是」是党务列举的措辞——只用在列具体的**问题 / 措施**那一处;背景、现状、认识、分析、过渡、总结**一律成段**。
|
||||
- **整篇每段 / 每节都"一是 / 二是",和"每段一个 bullet"是同一个骨架化的错——不因为是党务就变对**(纯清单 / 台账类除外)。
|
||||
|
||||
## 三、按体裁写
|
||||
|
||||
- **公文 / 法律 / 学术 / 申报 / 项目方案等严肃正式提交物**:靠规范的标题层级、段落与编号体系表达;**默认不用高亮块、分栏**,要强调用加粗或规范小标题。
|
||||
- **面向公众号、微信等外部平台粘贴 / 发布的内容**:不用飞书特有富 block(高亮块、分栏等),粘出去会丢样式 / 错乱;改用标准标题、段落、列表、引用。
|
||||
- **一般文档**:以可读为先,不堆砌结构。
|
||||
|
||||
## 四、编号与层级
|
||||
|
||||
- **一套编号体例、全篇一致;最忌中文大层级与阿拉伯小数编号混用。**
|
||||
- 公文 / 正式材料常用:「一、→(一)→ 1.→(1)」(中文大层级 + 阿拉伯细分层级)。
|
||||
- 学术 / 技术 / 商业报告:「1 → 1.1 → 1.1.1」或「一、→(一)→ 1.」,**择一**。
|
||||
- ⚠️ **「一、」只能配「(一)」;要用阿拉伯小数就从顶层全用「1 / 1.1」。绝不「一、」配「1.1 / 2.1」**——这是最常见的混用。
|
||||
- **不混用**多套(别"第X部分"+"一、"+"1."混着来);**同级不跳号**;**不跳级**。
|
||||
- **编号 / 标题层级只给"章节"**,不要为了凑齐体例把每个小项都编上「(一)」、升成标题(小项处理方式见上文「二、默认写连贯段落」)。
|
||||
- 简单的 1.2.3 并列项用原生 `<ol><li seq="auto">…</li></ol>` 让飞书自动编号、自动对齐;「一、(一)」原生产不出,才手打成文字——此时用标题级别表达层次,**不靠手动缩进**、各级顶格(全角括号「()」叠手动缩进会视觉错位)。
|
||||
|
||||
## 五、飞书特有组件,克制使用
|
||||
|
||||
- **高亮块 `<callout>`**:很重的强提醒信号,**默认不用**;只给"不提醒就会出错 / 遗漏"的关键项,全文极少(0~1 个),不要每节导语 / 结论都做成高亮块。
|
||||
- **分栏 `<grid>`**:仅左右信息量相当、确需并排对照的短内容;否则用段落或表格。
|
||||
- **画板**:默认用文字,只在**图示明显比文字更易懂**(流程、架构、时间线、对比、占比等)或用户要求时才用。怎么插、用哪种类型见 [`lark-doc-xml.md`](../lark-doc-xml.md) 与 [`lark-doc-whiteboard.md`](../lark-doc-whiteboard.md)。
|
||||
- **颜色**:默认朴素、不上色;需要时保持语义一致,按下表选择对应颜色,不为装饰上色。可用色见 [`lark-doc-xml.md`](../lark-doc-xml.md) 的「美化系统」。
|
||||
|
||||
| 语义 | 背景色 | 文字色 |
|
||||
|-|-|-|
|
||||
| 信息、说明 | `light-blue` | `blue` |
|
||||
| 成功、推荐 | `light-green` | `green` |
|
||||
| 警告 / 错误 / 风险 | `light-red` | `red` |
|
||||
| 注意、待确认 | `light-yellow` | `yellow` |
|
||||
| 中性、辅助 | `light-gray` | — |
|
||||
|
||||
## 六、写完自检
|
||||
|
||||
交付前快速回看:
|
||||
- **叙述是否被列举化**:背景 / 现状 / 认识 / 分析 / 成效 / 过渡 / 总结等应成段;列举只用于同层级、可并列处理的信息,如问题、措施、步骤、任务或材料清单。若正文反复使用连续编号、项目符号或固定并列句式,导致内容缺少叙述,应把背景 / 认识 / 分析 / 过渡改写成有承接关系的段落(纯清单 / 台账类除外)。
|
||||
- **数据是否正确呈现**:成行成列的数据应使用表格呈现,不要写成段落,也不要用分隔符把多个字段硬串在一起。
|
||||
- **标题是否滥用**:"小标题 + 一句话"的小项不要升成标题;应改成标签行、加粗引导句段落或普通段落。
|
||||
- **编号是否统一**:全篇一套、不跳号、不跳级,尤其不要中文 + 阿拉伯混用(如「一、」配「1.1」)。
|
||||
- **组件是否克制且保真**:高亮块 / 分栏 / 画板 / 颜色应符合体裁和用户要求;引用 / 图片 / 资源块必须保留。
|
||||
@@ -1,48 +0,0 @@
|
||||
# 改写增强工作流
|
||||
|
||||
用户提供已有文档链接或 token,需要改写、润色、补充或重排版时,遵循本工作流。
|
||||
|
||||
## 核心方法论 — Code-Act Loop
|
||||
通过自适应的 **Code-Act Loop** 驱动文档改写,而非固定模板式的工作流。每次任务都循环执行:
|
||||
1. **Plan(规划)** — 根据用户目标和文档当前状态,评估下一步该做什么
|
||||
2. **Execute(执行)** — 由主 Agent 自己运行 `lark-cli docs` 命令推进改写;仅画板渲染按需隔离到 SubAgent(见步骤二)
|
||||
3. **Observe(观察)** — 检查命令输出,验证正确性,确认内容是否满足用户目标
|
||||
4. **Iterate(迭代)** — 如需调整,回到 Plan 继续循环
|
||||
|
||||
## 核心原则:精准手术优于全量覆盖
|
||||
1. **精准手术**:只改用户指定的 block,不改其他 block。
|
||||
2. **全量覆盖**:如果用户明确要改整篇,才用 `overwrite` 命令。
|
||||
3. **保真约束**:改写时原文里的 `<cite type="user">`(@人)、`<cite type="doc">`(@文档)、`<img>`、`<source>`、`<whiteboard>`、`<sheet>`、`<bitable>`、`<synced_reference>` 等行内组件和资源块一律原样保留(含所有 token / user-id / doc-id 属性),不许替换成纯文本姓名、链接或占位符。
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 步骤一:分析与画板识别(串行)
|
||||
|
||||
1. **选择读取范围**(节省上下文的关键):
|
||||
- 用户只改某一节 / 文档较大 → 先 `docs +fetch --scope outline --max-depth 2` 拿目录,再 `docs +fetch --scope section --start-block-id <目标标题id> --detail with-ids` 精读该节(`section` 会自动展开到下一个同级/更高级标题前,不用手动算结束 block id)
|
||||
- 需要精确跨节区间 → `docs +fetch --scope range --start-block-id xxx --end-block-id yyy`(或 `--end-block-id -1` 读到末尾)
|
||||
- 用户只给了模糊关键词 → `docs +fetch --scope keyword --keyword xxx --context-before 1 --context-after 1 --detail with-ids`
|
||||
- 用户明确要改整篇 → `docs +fetch --detail with-ids`
|
||||
- 详见 [`lark-doc-fetch.md`](../lark-doc-fetch.md) 中「选 `--scope`(读取范围)」小节
|
||||
2. 系统性评估:用户想改什么、现有文档风格是什么、哪些内容需要保留、哪些问题影响理解
|
||||
3. **画板识别**:逐章节扫描,判断是否有段落用图明显比文字更易懂(流程 / 架构 / 时间线 / 对比 / 占比等,见 `lark-doc-style.md` 的画板原则)。默认用文字,只有确需图示才记录需要插图的章节(block ID)、推荐画板类型、mermaid/SVG路径和源内容片段
|
||||
4. 向用户简要说明改进计划(包含识别出的画板机会)
|
||||
|
||||
### 步骤二:定向改写(单 Agent 串行)
|
||||
|
||||
5. **优先处理步骤一识别出的画板候选段落**:读取并按 [lark-doc-whiteboard.md](../lark-doc-whiteboard.md) 选型和插入;正文本身不交给 SubAgent
|
||||
6. 由主 Agent **顺序逐节**改写,**不按章节拆给并行 Agent**,避免上下文割裂、重复矛盾和全文级约束失效:
|
||||
- 沿用或轻微调整已有文档风格,除非用户要求彻底重排版
|
||||
- 优先通过重写段落、调整标题、补充小标题提升可读性;叙述内容保持成段,**不要默认改成列表**,只有确属并列要点 / 步骤才用列表(见 `lark-doc-style.md`)
|
||||
- 富 block 是可选表达手段,不因固定比例而添加,取舍遵循 `lark-doc-style.md` 的写作原则;画板类需求只走第 5 步
|
||||
|
||||
### 步骤三:验证(串行)
|
||||
|
||||
7. 获取更新后文档局部内容,检查是否符合用户目标和已有风格
|
||||
8. 检查是否满足用户目标并保留原有关键内容。再按 `lark-doc-style.md` 的「写完自检」快速核对,发现问题则定向修正
|
||||
|
||||
### 步骤四:专项校验(按需执行)
|
||||
|
||||
9. 仅当用户预期需要校验字数时,才读取并执行 [`lark-doc-word-stat.md`](../lark-doc-word-stat.md) 的「字数遵循校验」;否则跳过本项,不读取该 workflow。若执行了专项校验,向用户呈现结果
|
||||
|
||||
**上下文节省提示**:主 Agent 改某节时如需重新读取,优先用 `docs +fetch --scope section --start-block-id <章节标题id>`(自动覆盖整节),或 `--scope range --start-block-id xxx --end-block-id yyy` 精确区间,只拉当前章节,不要重复拉全文。
|
||||
Reference in New Issue
Block a user