mirror of
https://github.com/larksuite/cli.git
synced 2026-07-04 06:29:52 +08:00
Compare commits
3 Commits
fix/instal
...
v1.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69cf9f206e | ||
|
|
99b8aaa556 | ||
|
|
b4a26b2cdc |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -2,6 +2,40 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.0.7] - 2026-04-09
|
||||
|
||||
### Features
|
||||
|
||||
- Auto-grant current user access for bot-created docs, sheets, imports, and uploads (#360)
|
||||
- **mail**: Add `send_as` alias support, mailbox/sender discovery APIs, and mail rules API
|
||||
- **vc**: Extract note doc tokens from calendar event relation API (#333)
|
||||
- **wiki**: Add wiki node create shortcut (#320)
|
||||
- **sheets**: Add `+write-image` shortcut (#343)
|
||||
- **docs**: Add media-preview shortcut (#334)
|
||||
- **docs**: Add support for additional search filters (#353)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **api**: Support stdin and quoted JSON inputs on Windows (#367)
|
||||
- **doc**: Post-process `docs +fetch` output to improve round-trip fidelity (#214)
|
||||
- **run**: Add missing binary check for lark-cli execution (#362)
|
||||
- **config**: Validate appId and appSecret keychain key consistency (#295)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Route base import guidance to drive `+import` (#368)
|
||||
- Migrate mail shortcuts to FileIO (#356)
|
||||
- Migrate drive/doc/sheets shortcuts to FileIO (#339)
|
||||
- Migrate base shortcuts to FileIO (#347)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **lark-doc**: Document advanced boolean and intitle search syntax for AI agents (#210)
|
||||
|
||||
### Chore
|
||||
|
||||
- Add depguard and forbidigo rules to guide FileIO adoption (#342)
|
||||
|
||||
## [v1.0.6] - 2026-04-08
|
||||
|
||||
### Features
|
||||
@@ -222,6 +256,7 @@ Bundled AI agent skills for intelligent assistance:
|
||||
- Bilingual documentation (English & Chinese).
|
||||
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
|
||||
|
||||
[v1.0.7]: https://github.com/larksuite/cli/releases/tag/v1.0.7
|
||||
[v1.0.6]: https://github.com/larksuite/cli/releases/tag/v1.0.6
|
||||
[v1.0.5]: https://github.com/larksuite/cli/releases/tag/v1.0.5
|
||||
[v1.0.4]: https://github.com/larksuite/cli/releases/tag/v1.0.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@larksuite/cli",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"description": "The official CLI for Lark/Feishu open platform",
|
||||
"bin": {
|
||||
"lark-cli": "scripts/run.js"
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseFieldCreate = common.Shortcut{
|
||||
{Name: "json", Desc: "field property JSON object", Required: true},
|
||||
{Name: "i-have-read-guide", Type: "bool", Desc: "set only after you have read the formula/lookup guide for those field types", Hidden: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"name":"Status","type":"text"}'`,
|
||||
"Agent hint: use the lark-base skill's field-create guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateFieldCreate(runtime)
|
||||
},
|
||||
|
||||
@@ -23,6 +23,10 @@ var BaseFieldUpdate = common.Shortcut{
|
||||
{Name: "json", Desc: "field property JSON object", Required: true},
|
||||
{Name: "i-have-read-guide", Type: "bool", Desc: "acknowledge reading formula/lookup guide before creating or updating those field types", Hidden: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"name":"Status","type":"text"}'`,
|
||||
"Agent hint: use the lark-base skill's field-update guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateFieldUpdate(runtime)
|
||||
},
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseRecordUpsert = common.Shortcut{
|
||||
recordRefFlag(false),
|
||||
{Name: "json", Desc: "record JSON object", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"Name":"Alice"}'`,
|
||||
"Agent hint: use the lark-base skill's record-upsert guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateRecordJSON(runtime)
|
||||
},
|
||||
|
||||
@@ -21,6 +21,10 @@ var BaseViewCreate = common.Shortcut{
|
||||
tableRefFlag(true),
|
||||
{Name: "json", Desc: "view JSON object/array", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"name":"Main","type":"grid"}'`,
|
||||
"Agent hint: use the lark-base skill's view-create guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewCreate(runtime)
|
||||
},
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseViewSetCard = common.Shortcut{
|
||||
viewRefFlag(true),
|
||||
{Name: "json", Desc: "card JSON object", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"cover_field":"fldCover"}'`,
|
||||
"Agent hint: use the lark-base skill's view-set-card guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewJSONObject(runtime)
|
||||
},
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseViewSetFilter = common.Shortcut{
|
||||
viewRefFlag(true),
|
||||
{Name: "json", Desc: "filter JSON object", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"logic":"and","conditions":[["fldStatus","==","Todo"]]}'`,
|
||||
"Agent hint: use the lark-base skill's view-set-filter guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewJSONObject(runtime)
|
||||
},
|
||||
|
||||
@@ -20,7 +20,11 @@ var BaseViewSetGroup = common.Shortcut{
|
||||
baseTokenFlag(true),
|
||||
tableRefFlag(true),
|
||||
viewRefFlag(true),
|
||||
{Name: "json", Desc: "group JSON object/array", Required: true},
|
||||
{Name: "json", Desc: "group JSON object", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"group_config":[{"field":"fldStatus","desc":false}]}'`,
|
||||
"Agent hint: use the lark-base skill's view-set-group guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewJSONValue(runtime)
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseViewSetSort = common.Shortcut{
|
||||
viewRefFlag(true),
|
||||
{Name: "json", Desc: "sort JSON object/array", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '[{"field":"fldPriority","desc":true}]'`,
|
||||
"Agent hint: use the lark-base skill's view-set-sort guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewJSONValue(runtime)
|
||||
},
|
||||
|
||||
@@ -22,6 +22,10 @@ var BaseViewSetTimebar = common.Shortcut{
|
||||
viewRefFlag(true),
|
||||
{Name: "json", Desc: "timebar JSON object", Required: true},
|
||||
},
|
||||
Tips: []string{
|
||||
`Example: --json '{"start_time":"fldStart","end_time":"fldEnd","title":"fldTitle"}'`,
|
||||
"Agent hint: use the lark-base skill's view-set-timebar guide for usage and limits.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateViewJSONObject(runtime)
|
||||
},
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
|
||||
@@ -18,17 +16,6 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var mimeToExt = map[string]string{
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"image/svg+xml": ".svg",
|
||||
"application/pdf": ".pdf",
|
||||
"video/mp4": ".mp4",
|
||||
"text/plain": ".txt",
|
||||
}
|
||||
|
||||
var DocMediaDownload = common.Shortcut{
|
||||
Service: "docs",
|
||||
Command: "+media-download",
|
||||
@@ -90,19 +77,11 @@ var DocMediaDownload = common.Shortcut{
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Auto-detect extension from Content-Type
|
||||
finalPath := outputPath
|
||||
currentExt := filepath.Ext(outputPath)
|
||||
if currentExt == "" {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
mimeType := strings.Split(contentType, ";")[0]
|
||||
mimeType = strings.TrimSpace(mimeType)
|
||||
if ext, ok := mimeToExt[mimeType]; ok {
|
||||
finalPath = outputPath + ext
|
||||
} else if mediaType == "whiteboard" {
|
||||
finalPath = outputPath + ".png"
|
||||
}
|
||||
fallbackExt := ""
|
||||
if mediaType == "whiteboard" {
|
||||
fallbackExt = ".png"
|
||||
}
|
||||
finalPath, _ := autoAppendDocMediaExtension(outputPath, resp.Header, fallbackExt)
|
||||
|
||||
// Validate final path after extension append
|
||||
if finalPath != outputPath {
|
||||
|
||||
105
shortcuts/doc/doc_media_ext.go
Normal file
105
shortcuts/doc/doc_media_ext.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package doc
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
)
|
||||
|
||||
type docMediaExtensionResolution struct {
|
||||
Ext string
|
||||
Source string
|
||||
Detail string
|
||||
}
|
||||
|
||||
var docMediaMimeToExt = map[string]string{
|
||||
"application/msword": ".doc",
|
||||
"application/pdf": ".pdf",
|
||||
"application/vnd.ms-excel": ".xls",
|
||||
"application/vnd.ms-powerpoint": ".ppt",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"application/xml": ".xml",
|
||||
"application/zip": ".zip",
|
||||
"image/bmp": ".bmp",
|
||||
"image/gif": ".gif",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/svg+xml": ".svg",
|
||||
"image/webp": ".webp",
|
||||
"text/csv": ".csv",
|
||||
"text/html": ".html",
|
||||
"text/plain": ".txt",
|
||||
"text/xml": ".xml",
|
||||
"video/mp4": ".mp4",
|
||||
}
|
||||
|
||||
func autoAppendDocMediaExtension(outputPath string, header http.Header, fallbackExt string) (string, *docMediaExtensionResolution) {
|
||||
if docMediaHasExplicitExtension(outputPath) {
|
||||
return outputPath, nil
|
||||
}
|
||||
normalizedPath := outputPath
|
||||
if filepath.Ext(outputPath) == "." {
|
||||
normalizedPath = strings.TrimSuffix(outputPath, ".")
|
||||
}
|
||||
if resolution := docMediaExtensionByContentType(header.Get("Content-Type")); resolution != nil {
|
||||
return normalizedPath + resolution.Ext, resolution
|
||||
}
|
||||
if resolution := docMediaExtensionByContentDisposition(header); resolution != nil {
|
||||
return normalizedPath + resolution.Ext, resolution
|
||||
}
|
||||
if fallbackExt != "" {
|
||||
return normalizedPath + fallbackExt, &docMediaExtensionResolution{
|
||||
Ext: fallbackExt,
|
||||
Source: "fallback",
|
||||
Detail: "default fallback",
|
||||
}
|
||||
}
|
||||
return outputPath, nil
|
||||
}
|
||||
|
||||
func docMediaHasExplicitExtension(path string) bool {
|
||||
ext := filepath.Ext(path)
|
||||
return ext != "" && ext != "."
|
||||
}
|
||||
|
||||
func docMediaExtensionByContentType(contentType string) *docMediaExtensionResolution {
|
||||
if contentType == "" {
|
||||
return nil
|
||||
}
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
mediaType = strings.TrimSpace(strings.Split(contentType, ";")[0])
|
||||
}
|
||||
if ext, ok := docMediaMimeToExt[strings.ToLower(mediaType)]; ok {
|
||||
return &docMediaExtensionResolution{
|
||||
Ext: ext,
|
||||
Source: "Content-Type",
|
||||
Detail: contentType,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func docMediaExtensionByContentDisposition(header http.Header) *docMediaExtensionResolution {
|
||||
filename := strings.TrimSpace(larkcore.FileNameByHeader(header))
|
||||
if filename == "" {
|
||||
return nil
|
||||
}
|
||||
ext := filepath.Ext(filename)
|
||||
if ext == "" || ext == "." {
|
||||
return nil
|
||||
}
|
||||
return &docMediaExtensionResolution{
|
||||
Ext: ext,
|
||||
Source: "Content-Disposition",
|
||||
Detail: filename,
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
|
||||
@@ -18,17 +16,6 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var previewMimeToExt = map[string]string{
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpg",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"image/svg+xml": ".svg",
|
||||
"application/pdf": ".pdf",
|
||||
"video/mp4": ".mp4",
|
||||
"text/plain": ".txt",
|
||||
}
|
||||
|
||||
const PreviewType_SOURCE_FILE = "16"
|
||||
|
||||
var DocMediaPreview = common.Shortcut{
|
||||
@@ -82,16 +69,7 @@ var DocMediaPreview = common.Shortcut{
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
finalPath := outputPath
|
||||
currentExt := filepath.Ext(outputPath)
|
||||
if currentExt == "" {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
mimeType := strings.Split(contentType, ";")[0]
|
||||
mimeType = strings.TrimSpace(mimeType)
|
||||
if ext, ok := previewMimeToExt[mimeType]; ok {
|
||||
finalPath = outputPath + ext
|
||||
}
|
||||
}
|
||||
finalPath, _ := autoAppendDocMediaExtension(outputPath, resp.Header, "")
|
||||
|
||||
// Validate final path after extension append
|
||||
if finalPath != outputPath {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
@@ -285,6 +286,77 @@ func TestDocMediaDownloadRejectsHTTPErrorBeforeWrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaDownloadAppendsExtensionFromContentDispositionFilename(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-download-disposition-app"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/medias/tok_123/download",
|
||||
Status: 200,
|
||||
Body: []byte("a,b,c\n1,2,3\n"),
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
"Content-Disposition": []string{`attachment; filename="drive_registry_config_addition.csv"`},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDocsWorkingDir(t, tmpDir)
|
||||
|
||||
err := mountAndRunDocs(t, DocMediaDownload, []string{
|
||||
"+media-download",
|
||||
"--token", "tok_123",
|
||||
"--output", "download",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := decodeDocCommandOutput(t, stdout)
|
||||
wantPath := mustDocSafeOutputPath(t, "download.csv")
|
||||
if got.Data.SavedPath != wantPath {
|
||||
t.Fatalf("saved_path = %q, want %q", got.Data.SavedPath, wantPath)
|
||||
}
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected downloaded file at %q: %v", wantPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaDownloadAppendsExtensionForTrailingDotOutput(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-download-trailing-dot-app"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/medias/tok_123/download",
|
||||
Status: 200,
|
||||
Body: []byte("a,b,c\n1,2,3\n"),
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"text/csv; charset=utf-8"},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDocsWorkingDir(t, tmpDir)
|
||||
|
||||
err := mountAndRunDocs(t, DocMediaDownload, []string{
|
||||
"+media-download",
|
||||
"--token", "tok_123",
|
||||
"--output", "typed.",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := decodeDocCommandOutput(t, stdout)
|
||||
wantPath := mustDocSafeOutputPath(t, "typed.csv")
|
||||
if got.Data.SavedPath != wantPath {
|
||||
t.Fatalf("saved_path = %q, want %q", got.Data.SavedPath, wantPath)
|
||||
}
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected downloaded file at %q: %v", wantPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaPreviewDryRunUsesMediaEndpoint(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "docs +media-preview"}
|
||||
cmd.Flags().String("token", "", "")
|
||||
@@ -371,6 +443,113 @@ func TestDocMediaPreviewRejectsHTTPErrorBeforeWrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaPreviewAppendsExtensionFromRFC5987Filename(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-preview-disposition-app"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/medias/tok_123/preview_download?preview_type=" + PreviewType_SOURCE_FILE,
|
||||
Status: 200,
|
||||
Body: []byte("a,b,c\n1,2,3\n"),
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
"Content-Disposition": []string{`attachment; filename*=UTF-8''drive_registry_config_addition.csv`},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDocsWorkingDir(t, tmpDir)
|
||||
|
||||
err := mountAndRunDocs(t, DocMediaPreview, []string{
|
||||
"+media-preview",
|
||||
"--token", "tok_123",
|
||||
"--output", "preview",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := decodeDocCommandOutput(t, stdout)
|
||||
wantPath := mustDocSafeOutputPath(t, "preview.csv")
|
||||
if got.Data.SavedPath != wantPath {
|
||||
t.Fatalf("saved_path = %q, want %q", got.Data.SavedPath, wantPath)
|
||||
}
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected preview file at %q: %v", wantPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaPreviewAppendsExtensionForTrailingDotOutput(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-preview-trailing-dot-app"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/medias/tok_123/preview_download?preview_type=" + PreviewType_SOURCE_FILE,
|
||||
Status: 200,
|
||||
Body: []byte("a,b,c\n1,2,3\n"),
|
||||
Headers: http.Header{
|
||||
"Content-Disposition": []string{`attachment; filename*=UTF-8''drive_registry_config_addition.csv`},
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDocsWorkingDir(t, tmpDir)
|
||||
|
||||
err := mountAndRunDocs(t, DocMediaPreview, []string{
|
||||
"+media-preview",
|
||||
"--token", "tok_123",
|
||||
"--output", "preview.",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := decodeDocCommandOutput(t, stdout)
|
||||
wantPath := mustDocSafeOutputPath(t, "preview.csv")
|
||||
if got.Data.SavedPath != wantPath {
|
||||
t.Fatalf("saved_path = %q, want %q", got.Data.SavedPath, wantPath)
|
||||
}
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected preview file at %q: %v", wantPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocMediaDownloadAppendsExtensionFromContentTypeMapping(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-download-content-type-app"))
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/drive/v1/medias/tok_123/download",
|
||||
Status: 200,
|
||||
Body: []byte("a,b,c\n1,2,3\n"),
|
||||
Headers: http.Header{
|
||||
"Content-Type": []string{"text/csv; charset=utf-8"},
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
withDocsWorkingDir(t, tmpDir)
|
||||
|
||||
err := mountAndRunDocs(t, DocMediaDownload, []string{
|
||||
"+media-download",
|
||||
"--token", "tok_123",
|
||||
"--output", "typed",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := decodeDocCommandOutput(t, stdout)
|
||||
wantPath := mustDocSafeOutputPath(t, "typed.csv")
|
||||
if got.Data.SavedPath != wantPath {
|
||||
t.Fatalf("saved_path = %q, want %q", got.Data.SavedPath, wantPath)
|
||||
}
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected downloaded file at %q: %v", wantPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
type docDryRunOutput struct {
|
||||
Description string `json:"description"`
|
||||
API []struct {
|
||||
@@ -381,6 +560,15 @@ type docDryRunOutput struct {
|
||||
} `json:"api"`
|
||||
}
|
||||
|
||||
type docCommandOutput struct {
|
||||
OK bool `json:"ok"`
|
||||
Data struct {
|
||||
SavedPath string `json:"saved_path"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
ContentType string `json:"content_type"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func writeSizedDocTestFile(t *testing.T, name string, size int64) {
|
||||
t.Helper()
|
||||
|
||||
@@ -410,3 +598,23 @@ func decodeDocDryRun(t *testing.T, dryAPI *common.DryRunAPI) docDryRunOutput {
|
||||
}
|
||||
return dry
|
||||
}
|
||||
|
||||
func decodeDocCommandOutput(t *testing.T, stdout *bytes.Buffer) docCommandOutput {
|
||||
t.Helper()
|
||||
|
||||
var out docCommandOutput
|
||||
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
|
||||
t.Fatalf("decode command output: %v; output=%s", err, stdout.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mustDocSafeOutputPath(t *testing.T, output string) string {
|
||||
t.Helper()
|
||||
|
||||
path, err := validate.SafeOutputPath(output)
|
||||
if err != nil {
|
||||
t.Fatalf("SafeOutputPath(%q) error: %v", output, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -11,15 +11,17 @@ lark-cli base +view-set-group \
|
||||
--base-token app_xxx \
|
||||
--table-id tbl_xxx \
|
||||
--view-id viw_xxx \
|
||||
--json [{"field":"fld_status","desc":false}]
|
||||
--json '{"group_config":[{"field":"fldStatus","desc":false}]}'
|
||||
```
|
||||
|
||||
## JSON 结构
|
||||
|
||||
```json
|
||||
[
|
||||
{ "field": "fld_status", "desc": false }
|
||||
]
|
||||
{
|
||||
"group_config": [
|
||||
{ "field": "fldStatus", "desc": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 参数
|
||||
@@ -29,7 +31,7 @@ lark-cli base +view-set-group \
|
||||
| `--base-token <token>` | 是 | Base Token |
|
||||
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
|
||||
| `--view-id <id_or_name>` | 是 | 视图 ID 或视图名 |
|
||||
| `--json <body>` | 是 | JSON 对象或数组 |
|
||||
| `--json <body>` | 是 | JSON 对象 |
|
||||
|
||||
## API 入参详情
|
||||
|
||||
@@ -49,14 +51,12 @@ PUT /open-apis/base/v3/bases/:base_token/tables/:table_id/views/:view_id/group
|
||||
- 每项:
|
||||
- `field`:字段 id 或字段名,长度 `1..100`
|
||||
- `desc`:可选,默认 `false`
|
||||
- `--json` 既可传对象 `{"group_config":[...]}`,也可直接传数组 `[...]`
|
||||
- 直接传数组时,CLI 会自动包装成 `group_config`
|
||||
|
||||
|
||||
## JSON Schema(原文)
|
||||
|
||||
```json
|
||||
{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":100,"description":"Field id or name"},"desc":{"type":"boolean","default":false,"description":"define how to sort group headers"}},"required":["field"],"additionalProperties":false},"minItems":0,"maxItems":3,"$schema":"http://json-schema.org/draft-07/schema#"}
|
||||
{"type":"object","properties":{"group_config":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","minLength":1,"maxLength":100,"description":"Field id or name"},"desc":{"type":"boolean","default":false,"description":"define how to sort group headers"}},"required":["field"],"additionalProperties":false},"minItems":0,"maxItems":3}},"required":["group_config"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}
|
||||
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user