mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Compare commits
1 Commits
codex/html
...
feat/docs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
322768a280 |
227
shortcuts/doc/docs_cover.go
Normal file
227
shortcuts/doc/docs_cover.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// docxDocumentAPIPath is the docx v1 document endpoint used for cover GET/PATCH.
|
||||
const docxDocumentAPIPath = "/open-apis/docx/v1/documents/%s"
|
||||
|
||||
// resolveCoverDocumentID returns the docx document_id for cover operations.
|
||||
// The cover OpenAPI (GET/PATCH /open-apis/docx/v1/documents/:document_id) only
|
||||
// accepts a docx document_id. wiki/doc refs are rejected with a structured,
|
||||
// actionable error — this iteration does not resolve wiki → docx.
|
||||
func resolveCoverDocumentID(runtime *common.RuntimeContext) (string, error) {
|
||||
ref, err := parseDocumentRef(runtime.Str("doc"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ref.Kind != "docx" {
|
||||
return "", errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--doc kind %q is not supported for cover operations; pass a docx document URL or token (the cover API needs a docx document_id)", ref.Kind).WithParam("--doc")
|
||||
}
|
||||
return ref.Token, nil
|
||||
}
|
||||
|
||||
// parseOptionalOffset reads an optional float flag. Returns (value, present, error).
|
||||
// Not provided (empty) → present=false so the caller omits the field entirely
|
||||
// (no default is injected). Provided → only finite numbers pass; NaN/Inf/non-numeric
|
||||
// are rejected client-side. The accepted numeric range is left to the server.
|
||||
func parseOptionalOffset(runtime *common.RuntimeContext, name string) (float64, bool, error) {
|
||||
raw := strings.TrimSpace(runtime.Str(name))
|
||||
if raw == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
v, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil || math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0, false, errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"--%s must be a finite number, got %q", name, raw).WithParam("--" + name)
|
||||
}
|
||||
return v, true, nil
|
||||
}
|
||||
|
||||
// extractCover pulls data.document.cover out of the docx document response envelope.
|
||||
func extractCover(data map[string]interface{}) interface{} {
|
||||
doc, ok := data["document"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return doc["cover"]
|
||||
}
|
||||
|
||||
// ---------------- cover-get ----------------
|
||||
|
||||
func validateCoverDoc(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
_, err := resolveCoverDocumentID(runtime)
|
||||
return err
|
||||
}
|
||||
|
||||
func dryRunCoverGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
return common.NewDryRunAPI().
|
||||
GET(fmt.Sprintf(docxDocumentAPIPath, id)).
|
||||
Desc("OpenAPI: get document (cover in data.document.cover)").
|
||||
Set("document_id", id)
|
||||
}
|
||||
|
||||
func executeCoverGet(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
data, err := doDocAPI(runtime, "GET", fmt.Sprintf(docxDocumentAPIPath, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cover := extractCover(data)
|
||||
runtime.OutFormatRaw(map[string]interface{}{"cover": cover}, nil, func(w io.Writer) {
|
||||
if cover == nil {
|
||||
fmt.Fprintln(w, "(no cover)")
|
||||
return
|
||||
}
|
||||
if m, ok := cover.(map[string]interface{}); ok {
|
||||
fmt.Fprintf(w, "token=%v offset_ratio_x=%v offset_ratio_y=%v\n", m["token"], m["offset_ratio_x"], m["offset_ratio_y"])
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var DocsCoverGet = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+cover-get",
|
||||
Description: "Get a docx document cover image (token + offset ratios)",
|
||||
Risk: "read",
|
||||
Scopes: []string{"docx:document:readonly"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "docx document URL or token", Required: true},
|
||||
},
|
||||
Validate: validateCoverDoc,
|
||||
DryRun: dryRunCoverGet,
|
||||
Execute: executeCoverGet,
|
||||
}
|
||||
|
||||
// ---------------- cover-update ----------------
|
||||
|
||||
func validateCoverUpdate(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := resolveCoverDocumentID(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("token")) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--token is required").WithParam("--token")
|
||||
}
|
||||
if _, _, err := parseOptionalOffset(runtime, "offset-ratio-x"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := parseOptionalOffset(runtime, "offset-ratio-y"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCoverUpdateBody assembles {update_cover:{cover:{token, offset_ratio_x?, offset_ratio_y?}}}.
|
||||
// Offsets are written only when explicitly provided; no default is injected so the
|
||||
// server applies its existing default crop behavior when omitted.
|
||||
func buildCoverUpdateBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
cover := map[string]interface{}{"token": strings.TrimSpace(runtime.Str("token"))}
|
||||
if v, ok, _ := parseOptionalOffset(runtime, "offset-ratio-x"); ok {
|
||||
cover["offset_ratio_x"] = v
|
||||
}
|
||||
if v, ok, _ := parseOptionalOffset(runtime, "offset-ratio-y"); ok {
|
||||
cover["offset_ratio_y"] = v
|
||||
}
|
||||
return map[string]interface{}{"update_cover": map[string]interface{}{"cover": cover}}
|
||||
}
|
||||
|
||||
func dryRunCoverUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
return common.NewDryRunAPI().
|
||||
PATCH(fmt.Sprintf(docxDocumentAPIPath, id)).
|
||||
Desc("OpenAPI: update document cover").
|
||||
Body(buildCoverUpdateBody(runtime)).
|
||||
Set("document_id", id)
|
||||
}
|
||||
|
||||
func executeCoverUpdate(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
data, err := doDocAPI(runtime, "PATCH", fmt.Sprintf(docxDocumentAPIPath, id), buildCoverUpdateBody(runtime))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.OutFormatRaw(map[string]interface{}{"cover": extractCover(data)}, nil, func(w io.Writer) {
|
||||
fmt.Fprintln(w, "cover updated")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var DocsCoverUpdate = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+cover-update",
|
||||
Description: "Update a docx document cover image (token must have docx_image relation to the doc)",
|
||||
Risk: "write",
|
||||
Scopes: []string{"docx:document"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "docx document URL or token", Required: true},
|
||||
{Name: "token", Desc: "cover image file_token; must be uploaded with docx_image relation to this doc (use `docs +media-upload --parent-type docx_image --parent-node <doc-id> --doc-id <doc-id>`); a `docs +media-insert` body image token will be rejected with a relation mismatch", Required: true},
|
||||
{Name: "offset-ratio-x", Type: "float64", Desc: "optional horizontal cover offset ratio (aligns with Docx OpenAPI document.cover.offset_ratio_x); omit to keep server default; only finite numbers accepted, range validated server-side"},
|
||||
{Name: "offset-ratio-y", Type: "float64", Desc: "optional vertical cover offset ratio (aligns with Docx OpenAPI document.cover.offset_ratio_y); omit to keep server default; only finite numbers accepted, range validated server-side"},
|
||||
},
|
||||
Validate: validateCoverUpdate,
|
||||
DryRun: dryRunCoverUpdate,
|
||||
Execute: executeCoverUpdate,
|
||||
}
|
||||
|
||||
// ---------------- cover-delete ----------------
|
||||
|
||||
// buildCoverDeleteBody assembles {update_cover:{cover:null}} per the OpenAPI delete convention.
|
||||
func buildCoverDeleteBody() map[string]interface{} {
|
||||
return map[string]interface{}{"update_cover": map[string]interface{}{"cover": nil}}
|
||||
}
|
||||
|
||||
func dryRunCoverDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
return common.NewDryRunAPI().
|
||||
PATCH(fmt.Sprintf(docxDocumentAPIPath, id)).
|
||||
Desc("OpenAPI: delete document cover (cover:null)").
|
||||
Body(buildCoverDeleteBody()).
|
||||
Set("document_id", id)
|
||||
}
|
||||
|
||||
func executeCoverDelete(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
id, _ := resolveCoverDocumentID(runtime)
|
||||
data, err := doDocAPI(runtime, "PATCH", fmt.Sprintf(docxDocumentAPIPath, id), buildCoverDeleteBody())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.OutFormatRaw(map[string]interface{}{"cover": extractCover(data)}, nil, func(w io.Writer) {
|
||||
fmt.Fprintln(w, "cover deleted")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var DocsCoverDelete = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+cover-delete",
|
||||
Description: "Delete a docx document cover image (sends cover:null)",
|
||||
Risk: "write",
|
||||
Scopes: []string{"docx:document"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "doc", Desc: "docx document URL or token", Required: true},
|
||||
},
|
||||
Validate: validateCoverDoc,
|
||||
DryRun: dryRunCoverDelete,
|
||||
Execute: executeCoverDelete,
|
||||
}
|
||||
155
shortcuts/doc/docs_cover_test.go
Normal file
155
shortcuts/doc/docs_cover_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCoverTestRuntime() *common.RuntimeContext {
|
||||
cmd := &cobra.Command{Use: "+cover"}
|
||||
cmd.Flags().String("doc", "", "")
|
||||
cmd.Flags().String("token", "", "")
|
||||
cmd.Flags().String("offset-ratio-x", "", "")
|
||||
cmd.Flags().String("offset-ratio-y", "", "")
|
||||
return common.TestNewRuntimeContextWithCtx(context.Background(), cmd, nil)
|
||||
}
|
||||
|
||||
func TestResolveCoverDocumentID(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
doc string
|
||||
wantID string
|
||||
wantErr bool
|
||||
}{
|
||||
{"raw token", "doxcnAbc123", "doxcnAbc123", false},
|
||||
{"docx url", "https://x.larkoffice.com/docx/doxcnAbc123", "doxcnAbc123", false},
|
||||
{"wiki url rejected", "https://x.larkoffice.com/wiki/wikAbc123", "", true},
|
||||
{"empty rejected", "", "", true},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rt := newCoverTestRuntime()
|
||||
_ = rt.Cmd.Flags().Set("doc", tt.doc)
|
||||
id, err := resolveCoverDocumentID(rt)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %q, got id=%q", tt.doc, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if id != tt.wantID {
|
||||
t.Fatalf("id = %q, want %q", id, tt.wantID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOptionalOffset(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
val string
|
||||
wantPresent bool
|
||||
wantVal float64
|
||||
wantErr bool
|
||||
}{
|
||||
{"not provided", "", false, 0, false},
|
||||
{"valid float", "0.25", true, 0.25, false},
|
||||
{"valid negative", "-0.5", true, -0.5, false},
|
||||
{"non-numeric", "abc", false, 0, true},
|
||||
{"NaN", "NaN", false, 0, true},
|
||||
{"Inf", "Inf", false, 0, true},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rt := newCoverTestRuntime()
|
||||
_ = rt.Cmd.Flags().Set("offset-ratio-x", tt.val)
|
||||
v, present, err := parseOptionalOffset(rt, "offset-ratio-x")
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %q", tt.val)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if present != tt.wantPresent {
|
||||
t.Fatalf("present = %v, want %v", present, tt.wantPresent)
|
||||
}
|
||||
if present && v != tt.wantVal {
|
||||
t.Fatalf("val = %v, want %v", v, tt.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCoverUpdateBodyOmitsOffsetWhenUnset(t *testing.T) {
|
||||
rt := newCoverTestRuntime()
|
||||
_ = rt.Cmd.Flags().Set("token", "filetokenABC")
|
||||
|
||||
body := buildCoverUpdateBody(rt)
|
||||
cover := body["update_cover"].(map[string]interface{})["cover"].(map[string]interface{})
|
||||
if cover["token"] != "filetokenABC" {
|
||||
t.Fatalf("token = %#v, want filetokenABC", cover["token"])
|
||||
}
|
||||
if _, ok := cover["offset_ratio_x"]; ok {
|
||||
t.Fatalf("offset_ratio_x must be omitted when unset: %#v", cover)
|
||||
}
|
||||
if _, ok := cover["offset_ratio_y"]; ok {
|
||||
t.Fatalf("offset_ratio_y must be omitted when unset: %#v", cover)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCoverUpdateBodyIncludesOffsetWhenSet(t *testing.T) {
|
||||
rt := newCoverTestRuntime()
|
||||
_ = rt.Cmd.Flags().Set("token", "filetokenABC")
|
||||
_ = rt.Cmd.Flags().Set("offset-ratio-x", "0.1")
|
||||
_ = rt.Cmd.Flags().Set("offset-ratio-y", "0.2")
|
||||
|
||||
body := buildCoverUpdateBody(rt)
|
||||
cover := body["update_cover"].(map[string]interface{})["cover"].(map[string]interface{})
|
||||
if cover["offset_ratio_x"] != 0.1 {
|
||||
t.Fatalf("offset_ratio_x = %#v, want 0.1", cover["offset_ratio_x"])
|
||||
}
|
||||
if cover["offset_ratio_y"] != 0.2 {
|
||||
t.Fatalf("offset_ratio_y = %#v, want 0.2", cover["offset_ratio_y"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCoverDeleteBodyIsNull(t *testing.T) {
|
||||
body := buildCoverDeleteBody()
|
||||
cover, ok := body["update_cover"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("update_cover missing: %#v", body)
|
||||
}
|
||||
v, present := cover["cover"]
|
||||
if !present {
|
||||
t.Fatalf("cover key must be present (explicit null): %#v", cover)
|
||||
}
|
||||
if v != nil {
|
||||
t.Fatalf("cover must be nil for delete, got %#v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCoverUpdateRequiresToken(t *testing.T) {
|
||||
rt := newCoverTestRuntime()
|
||||
_ = rt.Cmd.Flags().Set("doc", "doxcnAbc123")
|
||||
// no --token
|
||||
if err := validateCoverUpdate(context.Background(), rt); err == nil {
|
||||
t.Fatal("expected error when --token missing")
|
||||
}
|
||||
|
||||
_ = rt.Cmd.Flags().Set("token", "filetokenABC")
|
||||
if err := validateCoverUpdate(context.Background(), rt); err != nil {
|
||||
t.Fatalf("unexpected error with token set: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,9 @@ func Shortcuts() []common.Shortcut {
|
||||
DocMediaUpload,
|
||||
DocMediaPreview,
|
||||
DocMediaDownload,
|
||||
DocsCoverGet,
|
||||
DocsCoverUpdate,
|
||||
DocsCoverDelete,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ Shortcut 是对常用操作的高级封装(`lark-cli docs +<verb> [flags]`)
|
||||
| [`+media-insert`](references/lark-doc-media-insert.md) | Insert a local image or file at the end of a Lark document (4-step orchestration + auto-rollback). Prefer `--from-clipboard` when the image is already on the system clipboard (screenshots, copy from Feishu/browser); use `--file` only for on-disk sources. |
|
||||
| [`+media-download`](references/lark-doc-media-download.md) | Download document media or whiteboard thumbnail (auto-detects extension) |
|
||||
| [`+media-preview`](references/lark-doc-media-preview.md) | Preview document media file (auto-detects extension) |
|
||||
| [`+cover-get`](references/lark-doc-cover.md) | Get a docx document cover image (token + offset ratios) |
|
||||
| [`+cover-update`](references/lark-doc-cover.md) | Update a docx document cover image (token must have docx_image relation to the doc) |
|
||||
| [`+cover-delete`](references/lark-doc-cover.md) | Delete a docx document cover image (sends cover:null) |
|
||||
| [`+whiteboard-update`](../lark-whiteboard/references/lark-whiteboard-update.md) | Alias of `whiteboard +update`. Update an existing whiteboard with DSL, Mermaid or PlantUML. Prefer `whiteboard +update`; refer to lark-whiteboard skill for details. |
|
||||
|
||||
## 不在本 Skill 范围
|
||||
|
||||
57
skills/lark-doc/references/lark-doc-cover.md
Normal file
57
skills/lark-doc/references/lark-doc-cover.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# docs 封面图(cover-get / cover-update / cover-delete)
|
||||
|
||||
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
新版文档 Docx 封面图的获取 / 更新 / 删除。底层走 docx OpenAPI:
|
||||
|
||||
- 获取:`GET /open-apis/docx/v1/documents/:document_id`,封面在 `data.document.cover`
|
||||
- 更新 / 删除:`PATCH /open-apis/docx/v1/documents/:document_id`,body `update_cover.cover`(删除时为 `null`)
|
||||
|
||||
```bash
|
||||
# 获取封面(输出 cover.token / offset_ratio_x / offset_ratio_y)
|
||||
lark-cli docs +cover-get --doc "https://xxx.larkoffice.com/docx/Z1Fj...tnAc"
|
||||
|
||||
# 更新封面(token 必须是与该 docx 建立 docx_image relation 的图片 token)
|
||||
lark-cli docs +cover-update --doc Z1Fj...tnAc --token <file_token>
|
||||
# 可选偏移比例(不传则用服务端默认裁剪;只接受有限浮点数)
|
||||
lark-cli docs +cover-update --doc Z1Fj...tnAc --token <file_token> --offset-ratio-x 0.1 --offset-ratio-y 0.2
|
||||
|
||||
# 删除封面(发送 cover:null)
|
||||
lark-cli docs +cover-delete --doc Z1Fj...tnAc
|
||||
```
|
||||
|
||||
## ⚠️ 封面 token 的 relation 规则(关键)
|
||||
|
||||
封面更新接口**只接受与目标 Docx 建立了 `docx_image` relation 的图片 token**。不能复用正文图片块 token、IM 图片 token、普通 Drive file token。
|
||||
|
||||
本地图片走**两步式**:先上传为绑定到目标文档的 docx_image 资源,再把返回的 file_token 传给 `+cover-update --token`:
|
||||
|
||||
```bash
|
||||
# 1) 上传封面图片,建立 docx_image relation
|
||||
lark-cli docs +media-upload \
|
||||
--file ./cover.png \
|
||||
--parent-type docx_image \
|
||||
--parent-node <document_id> \
|
||||
--doc-id <document_id>
|
||||
# 2) 用返回的 file_token 更新封面
|
||||
lark-cli docs +cover-update --doc <document_id> --token <file_token>
|
||||
```
|
||||
|
||||
**不要**用 `docs +media-insert` 返回的 token 当封面——那是正文 image block 的 relation(parent_node=<image_block_id>),调 cover-update 会被 OpenAPI 拒绝(relation mismatch)。
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 命令 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--doc` | get/update/delete | 是 | docx 文档 URL 或 token;当前仅支持 docx,wiki/doc URL 会返回结构化错误(请传 docx document_id)|
|
||||
| `--token` | update | 是 | 封面图 file_token,须有 docx_image relation(见上文)|
|
||||
| `--offset-ratio-x` | update | 否 | 水平方向偏移比例(对齐 Docx OpenAPI `document.cover.offset_ratio_x`);不传则用服务端默认;只接受有限浮点数,范围由服务端校验 |
|
||||
| `--offset-ratio-y` | update | 否 | 垂直方向偏移比例(同上)|
|
||||
|
||||
## 输出与约定
|
||||
|
||||
- stdout 输出 JSON(`{"cover": {...}}`),stderr 给人读提示,AI Agent 友好。
|
||||
- `cover-get` 原样输出服务端返回的 `cover.token` / `offset_ratio_x` / `offset_ratio_y`,不补默认值。
|
||||
- 未传 offset 时,请求体 `update_cover.cover` 不写入 offset 字段(不替用户补 0 / 0.5)。
|
||||
- offset 非数值 / NaN / Inf 在 CLI 侧前置拒绝;数值范围由服务端校验,下游错误结构化透出。
|
||||
- 用 `--dry-run` 可只查看将要发出的 method / path / body,不真正调用。
|
||||
Reference in New Issue
Block a user