mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
Compare commits
1 Commits
fix/skill-
...
feat/bot-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a367d2c02 |
18
README.md
18
README.md
@@ -233,24 +233,6 @@ lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_i
|
||||
--format csv # Comma-separated values
|
||||
```
|
||||
|
||||
### JSON Output Contract
|
||||
|
||||
With `--format json` (the default), success and error envelopes are distinct.
|
||||
|
||||
Success goes to **stdout**, exit code `0`:
|
||||
|
||||
```json
|
||||
{ "ok": true, "identity": "user", "data": { "guid": "..." }, "meta": { "count": 1 } }
|
||||
```
|
||||
|
||||
Errors go to **stderr**, non-zero exit code:
|
||||
|
||||
```json
|
||||
{ "ok": false, "identity": "user", "error": { "type": "api", "subtype": "...", "code": 99991679, "message": "...", "hint": "..." } }
|
||||
```
|
||||
|
||||
To check whether a command succeeded, test `ok == true` (or the exit code) — **not** `code == 0`. Unlike raw OpenAPI responses (`{"code": 0, "msg": "ok", ...}`), the success envelope carries no `code` or `msg` field; `code` appears only inside `error` as the upstream OpenAPI code. See [errs/ERROR_CONTRACT.md](errs/ERROR_CONTRACT.md) for the full error taxonomy.
|
||||
|
||||
### Pagination
|
||||
|
||||
```bash
|
||||
|
||||
18
README.zh.md
18
README.zh.md
@@ -234,24 +234,6 @@ lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_i
|
||||
--format csv # 逗号分隔值
|
||||
```
|
||||
|
||||
### JSON 输出契约
|
||||
|
||||
`--format json`(默认)下,成功与错误的信封结构不同。
|
||||
|
||||
成功信封写入 **stdout**,退出码 0:
|
||||
|
||||
```json
|
||||
{ "ok": true, "identity": "user", "data": { "guid": "..." }, "meta": { "count": 1 } }
|
||||
```
|
||||
|
||||
错误信封写入 **stderr**,退出码非 0:
|
||||
|
||||
```json
|
||||
{ "ok": false, "identity": "user", "error": { "type": "api", "subtype": "...", "code": 99991679, "message": "...", "hint": "..." } }
|
||||
```
|
||||
|
||||
判断命令是否成功,请检查 `ok == true`(或进程退出码),**不要用 `code == 0`**。与原始 OpenAPI 响应(`{"code": 0, "msg": "ok", ...}`)不同,成功信封没有 `code` 和 `msg` 字段;`code` 只出现在错误信封的 `error` 内,含义是上游 OpenAPI 的 numeric code。完整错误分类见 [errs/ERROR_CONTRACT.md](errs/ERROR_CONTRACT.md)。
|
||||
|
||||
### 分页
|
||||
|
||||
```bash
|
||||
|
||||
@@ -72,28 +72,6 @@ other category. `error.type` is `"policy"`, `error.subtype` is one of
|
||||
`challenge_required` / `access_denied`, and process exit is `6` via
|
||||
`CategoryPolicy`.
|
||||
|
||||
### Success envelope (stdout)
|
||||
|
||||
For contrast: success responses render to **stdout** as an
|
||||
`output.Envelope` (`internal/output/envelope.go`), exit code `0`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": { "guid": "e297d3d0-..." },
|
||||
"meta": { "count": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
Consumers must branch on `ok` (or the process exit code). The success
|
||||
envelope has **no top-level `code` or `msg` field** — `code` exists only
|
||||
inside `error`, where it is the upstream numeric code (invariant 4).
|
||||
Wrappers that follow the raw OpenAPI convention and test `code == 0`
|
||||
will misclassify every successful call as a failure, which is
|
||||
especially dangerous around write commands (e.g. retrying a create that
|
||||
already succeeded).
|
||||
|
||||
## Categories
|
||||
|
||||
| Category | When | Exit | Typed struct |
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScopeIncludesChangedSkillAndRelatedDomain(t *testing.T) {
|
||||
@@ -52,7 +51,7 @@ func TestFileAtRevisionMissingClassifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChangedFilesIncludingWorktree(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -84,7 +83,7 @@ func TestChangedFilesIncludingWorktree(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChangedFilesHandlesWhitespacePaths(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -140,22 +139,3 @@ func gitOutput(t *testing.T, repo string, args ...string) string {
|
||||
}
|
||||
return string(out[:len(out)-1])
|
||||
}
|
||||
|
||||
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
|
||||
// on this machine (trace2 hooks, etc.) can asynchronously write into a
|
||||
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
|
||||
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
|
||||
// past that transient window) makes the later t.TempDir cleanup a no-op.
|
||||
func newGitTestRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
repo := t.TempDir()
|
||||
t.Cleanup(func() {
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := os.RemoveAll(repo); err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
return repo
|
||||
}
|
||||
|
||||
@@ -10,11 +10,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCollectScansOnlyCurrentContributionAndMetadata(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -61,7 +60,7 @@ api_`+`key = "example-public-key"
|
||||
}
|
||||
|
||||
func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -93,7 +92,7 @@ func TestCollectScansOnlyChangedLinesInChangedFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectSemanticCandidatesStoreSanitizedReviewText(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -609,7 +608,7 @@ func TestCollectIgnoresDeletedPrivateKeyLine(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectSkipsOnlyKnownQualityGateFixtureFiles(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -700,7 +699,7 @@ func TestCollectScansAddedLinesInSpecialPathNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectScansBranchNameAsWarning(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
metadataPath := filepath.Join(repo, "pr-metadata.json")
|
||||
writeFile(t, metadataPath, `{"branch":"bot/public-doc-update"}`)
|
||||
got, err := Collect(context.Background(), Options{
|
||||
@@ -800,7 +799,7 @@ func TestAppendUniqueFindingsDeduplicatesByRuleFileLineAndSource(t *testing.T) {
|
||||
|
||||
func newGitRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -841,7 +840,7 @@ func requireFinding(t *testing.T, got []Finding, file, rule string) {
|
||||
}
|
||||
|
||||
func TestCollectRequiresValidMetadataJSON(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
metadataPath := filepath.Join(repo, "pr-metadata.json")
|
||||
writeFile(t, metadataPath, `{"title":`)
|
||||
|
||||
@@ -875,25 +874,6 @@ func runGitOutput(t *testing.T, repo string, args ...string) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
|
||||
// on this machine (trace2 hooks, etc.) can asynchronously write into a
|
||||
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
|
||||
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
|
||||
// past that transient window) makes the later t.TempDir cleanup a no-op.
|
||||
func newGitTestRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
repo := t.TempDir()
|
||||
t.Cleanup(func() {
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := os.RemoveAll(repo); err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
return repo
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, data string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qdiff "github.com/larksuite/cli/internal/qualitygate/diff"
|
||||
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
||||
@@ -70,7 +69,7 @@ func TestReferenceCommandSurfaceNormalizesShortcutDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
manifestPath := filepath.Join(repo, "command-manifest.json")
|
||||
indexPath := filepath.Join(repo, "command-index.json")
|
||||
m := manifest.Manifest{SchemaVersion: 1, Commands: []manifest.Command{{
|
||||
@@ -105,7 +104,7 @@ func TestRunRequiresCommandIndexToCoverManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunReadsManifestFilesAndAcceptsServiceReferences(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -191,7 +190,7 @@ description: Manage Drive comments with service command references.
|
||||
}
|
||||
|
||||
func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -284,7 +283,7 @@ func TestRunCollectsPublicContentFindingsIntoDiagnosticsAndFacts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -324,7 +323,7 @@ func TestLoadBaseReferenceManifestReadsCommandGolden(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -365,7 +364,7 @@ func TestLoadBaseReferenceManifestReadsCommandIndexGolden(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -385,7 +384,7 @@ func TestLoadBaseReferenceManifestRejectsEmptyGolden(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadBaseReferenceManifestRejectsInvalidGoldenKind(t *testing.T) {
|
||||
repo := newGitTestRepo(t)
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "test@example.com")
|
||||
runGit(t, repo, "config", "user.name", "Test User")
|
||||
@@ -607,22 +606,3 @@ func runGit(t *testing.T, repo string, args ...string) {
|
||||
t.Fatalf("git %v failed: %v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// newGitTestRepo returns a temp dir for a test-created git repo. Git tooling
|
||||
// on this machine (trace2 hooks, etc.) can asynchronously write into a
|
||||
// repo's .git/ shortly after a git command runs, racing with t.TempDir's
|
||||
// automatic RemoveAll cleanup. Removing the tree ourselves first (retrying
|
||||
// past that transient window) makes the later t.TempDir cleanup a no-op.
|
||||
func newGitTestRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
repo := t.TempDir()
|
||||
t.Cleanup(func() {
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := os.RemoveAll(repo); err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
return repo
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func SyncSkills(opts SyncOptions) *SyncResult {
|
||||
UpdatedSkills: plan.ToUpdate,
|
||||
AddedOfficialSkills: plan.Added,
|
||||
SkippedDeletedSkills: plan.SkippedDeleted,
|
||||
UpdatedAt: opts.Now().Format(time.RFC3339),
|
||||
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if err := WriteState(state); err != nil {
|
||||
result.Action = "failed"
|
||||
@@ -428,7 +428,7 @@ func fallbackFullInstall(opts SyncOptions, reason string, official []string) *Sy
|
||||
UpdatedSkills: official,
|
||||
AddedOfficialSkills: official,
|
||||
SkippedDeletedSkills: []string{},
|
||||
UpdatedAt: opts.Now().Format(time.RFC3339),
|
||||
UpdatedAt: opts.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if writeErr := WriteState(state); writeErr != nil {
|
||||
return &SyncResult{
|
||||
|
||||
@@ -853,65 +853,3 @@ func TestSyncSkills_FallbackBreaksDegradationLoop(t *testing.T) {
|
||||
t.Fatalf("second sync: installedAll = %d, want 0 (incremental, not fallback)", runner2.installedAll)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSkills_WritesLocalTimezoneUpdatedAt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
||||
|
||||
runner := &fakeSkillsRunner{
|
||||
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail", "lark-new"),
|
||||
officialOut: officialSkillsOutput("lark-calendar", "lark-mail", "lark-new"),
|
||||
globalJSONOut: globalSkillsJSONOutput("lark-calendar"),
|
||||
globalOut: globalSkillsOutput("lark-mail"),
|
||||
}
|
||||
localZone := time.FixedZone("UTC+8", 8*60*60)
|
||||
result := SyncSkills(SyncOptions{
|
||||
Version: "1.0.33",
|
||||
Runner: runner,
|
||||
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
|
||||
})
|
||||
if result.Err != nil {
|
||||
t.Fatalf("SyncSkills() err = %v, want nil", result.Err)
|
||||
}
|
||||
|
||||
state, readable, err := ReadState()
|
||||
if err != nil || !readable {
|
||||
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
|
||||
}
|
||||
want := "2026-05-18T12:00:00+08:00"
|
||||
if state.UpdatedAt != want {
|
||||
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSkills_FallbackWritesLocalTimezoneUpdatedAt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
||||
|
||||
runner := &fakeSkillsRunner{
|
||||
officialIndexOut: officialSkillsIndexOutput("lark-calendar", "lark-mail"),
|
||||
officialOut: officialSkillsOutput("lark-calendar", "lark-mail"),
|
||||
globalJSONOut: globalSkillsJSONOutput("lark-calendar", "lark-mail"),
|
||||
globalOut: globalSkillsOutput("lark-calendar", "lark-mail"),
|
||||
installErr: fmt.Errorf("incremental boom"),
|
||||
installAllErr: nil,
|
||||
}
|
||||
localZone := time.FixedZone("UTC+8", 8*60*60)
|
||||
result := SyncSkills(SyncOptions{
|
||||
Version: "1.0.33",
|
||||
Runner: runner,
|
||||
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, localZone) },
|
||||
})
|
||||
if result.Action != "fallback_synced" {
|
||||
t.Fatalf("SyncSkills() action = %q, want fallback_synced", result.Action)
|
||||
}
|
||||
|
||||
state, readable, err := ReadState()
|
||||
if err != nil || !readable {
|
||||
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
|
||||
}
|
||||
want := "2026-05-18T12:00:00+08:00"
|
||||
if state.UpdatedAt != want {
|
||||
t.Fatalf("state.UpdatedAt = %q, want %q (local timezone offset, not UTC Z-suffix)", state.UpdatedAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ var VCMeetingJoin = common.Shortcut{
|
||||
{Name: "meeting-number", Required: true, Desc: "meeting number to join"},
|
||||
{Name: "password", Desc: "meeting password (if required)"},
|
||||
{Name: "call-id", Desc: "correlation id forwarded from invite event"},
|
||||
{Name: "view-url", Desc: "view URL forwarded to meeting participants"},
|
||||
{Name: "view-url-status", Desc: "view URL status"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
mn := strings.TrimSpace(runtime.Str("meeting-number"))
|
||||
@@ -95,5 +97,11 @@ func buildMeetingJoinBody(runtime *common.RuntimeContext) map[string]interface{}
|
||||
if cid := strings.TrimSpace(runtime.Str("call-id")); cid != "" {
|
||||
body["call_id"] = cid
|
||||
}
|
||||
if viewURL := strings.TrimSpace(runtime.Str("view-url")); viewURL != "" {
|
||||
body["view_url"] = viewURL
|
||||
}
|
||||
if viewURLStatus := strings.TrimSpace(runtime.Str("view-url-status")); viewURLStatus != "" {
|
||||
body["view_url_status"] = viewURLStatus
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
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 生命周期」),否则反复锚同一个标题会让段落顺序颠倒
|
||||
@@ -41,7 +41,6 @@
|
||||
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. **重复标题检查**:文档生成后,检查文档标题和正文第一个标题块是否重复;若重复,删除或改写正文第一个标题块,避免读者看到同一标题连续出现
|
||||
9. 仅当用户预期需要校验字数时,才读取并执行 [`lark-doc-word-stat.md`](../lark-doc-word-stat.md) 的「字数遵循校验」;否则跳过本项,不读取该 workflow。若执行了专项校验,向用户呈现结果
|
||||
|
||||
@@ -146,24 +146,6 @@ lark-cli update
|
||||
|
||||
**重要**:始终使用 `lark-cli update` 更新,它会同时更新 CLI 和 AI Skills。
|
||||
|
||||
## JSON 输出契约
|
||||
|
||||
`--format json`(默认)下,成功与错误的信封结构不同:
|
||||
|
||||
成功信封写入 **stdout**(退出码 0):
|
||||
|
||||
```json
|
||||
{ "ok": true, "identity": "user", "data": { "guid": "..." }, "meta": { "count": 1 } }
|
||||
```
|
||||
|
||||
错误信封写入 **stderr**(退出码非 0):
|
||||
|
||||
```json
|
||||
{ "ok": false, "identity": "user", "error": { "type": "api", "subtype": "...", "code": 99991679, "message": "...", "hint": "..." } }
|
||||
```
|
||||
|
||||
**判断成功必须用 `ok == true`(或进程退出码 0),不要用 `code == 0`**:成功信封没有顶层 `code` / `msg` 字段,`code` 只出现在错误信封的 `error` 内,含义是上游 OpenAPI 的 numeric code。按 OpenAPI 老格式 `{"code": 0, "msg": "ok"}` 判断会把所有成功调用误判为失败;封装写入类命令(如 `task +create`)时尤其危险,误判会绕过幂等逻辑导致重复创建。
|
||||
|
||||
## 安全规则
|
||||
|
||||
- **禁止输出密钥**(appSecret、accessToken)到终端明文。
|
||||
|
||||
@@ -46,20 +46,7 @@ lark-cli task +create --summary "Test Task" --dry-run
|
||||
1. Confirm with the user: task summary, due date, assignee, and tasklist if necessary.
|
||||
- **Crucial Rule for Assignee**: If the user explicitly or implicitly says "create a task for me" (给我创建一个任务), or "help me create a task" (帮我新建/创建一个任务), you MUST assign the task to the current logged-in user. You can get the current user's `open_id` by executing `lark-cli auth status` (it already outputs JSON by default, so do not add `--json`) or `lark-cli contact +get-user` first, extracting `.identities.user.openId` (from `auth status`) or `.data.user.open_id` (from `contact +get-user`), and then passing it to the `--assignee` parameter.
|
||||
2. Execute `lark-cli task +create --summary "..." ...`
|
||||
3. Judge success by `ok == true` in the stdout JSON (the success envelope has no `code` field — do not test `code == 0`), then report the result: task ID (`data.guid`) and summary.
|
||||
|
||||
Example success response:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"identity": "user",
|
||||
"data": {
|
||||
"guid": "e297d3d0-4b60-4a5f-a4d4-xxxxxxxxxxxx",
|
||||
"url": "https://applink.larkoffice.com/client/todo/detail?guid=e297d3d0-4b60-4a5f-a4d4-xxxxxxxxxxxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Report the result: task ID and summary.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** -- You must confirm the user's intent before executing.
|
||||
|
||||
Reference in New Issue
Block a user