From e221e6fb398d5cb705d5e0e816b9b50edbd91673 Mon Sep 17 00:00:00 2001 From: mobaijie Date: Tue, 14 Apr 2026 16:52:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20cli=20=E6=94=AF=E6=8C=81=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=88=86=E4=BA=AB=20no-meego?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ie78da99096cc1fc8a4671d8178176f4c587466ba --- lark-env.sh | 260 ++++++++++++++++++ shortcuts/base/base_shortcuts_test.go | 2 +- shortcuts/base/record_ops.go | 67 +++++ .../base/record_share_link_batch_create.go | 36 +++ shortcuts/base/record_share_link_create.go | 31 +++ shortcuts/base/shortcuts.go | 2 + shortcuts/common/runner.go | 8 + ...ark-base-record-share-link-batch-create.md | 73 +++++ .../lark-base-record-share-link-create.md | 45 +++ .../lark-base/references/lark-base-record.md | 2 + 10 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 lark-env.sh create mode 100644 shortcuts/base/record_share_link_batch_create.go create mode 100644 shortcuts/base/record_share_link_create.go create mode 100644 skills/lark-base/references/lark-base-record-share-link-batch-create.md create mode 100644 skills/lark-base/references/lark-base-record-share-link-create.md diff --git a/lark-env.sh b/lark-env.sh new file mode 100644 index 00000000..729eab13 --- /dev/null +++ b/lark-env.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env sh + +RULE_NAME="lark-cli-openapi-env-switch" +RULE_GROUP="lark-cli" + +script_name() { + if [ -n "${BASH_VERSION-}" ]; then + basename "${BASH_SOURCE[0]}" + return + fi + if [ -n "${ZSH_VERSION-}" ]; then + eval 'basename "${(%):-%x}"' + return + fi + basename "$0" +} + +usage() { + cat < [--boe-env-name ] [--host ] [--port ] + +Examples: + . ./$(script_name) pre + . ./$(script_name) boe --boe-env-name boe_bitable_openapi_v2 + . ./$(script_name) off + +Notes: + - Use '.' (or 'source' in bash/zsh) to apply proxy env vars in current shell. + - The script always updates Whistle rule '${RULE_NAME}'. +USAGE +} + +fail() { + printf '%s\n' "[lark-cli-env] $*" >&2 + return 1 2>/dev/null || exit 1 +} + +info() { + printf '%s\n' "[lark-cli-env] $*" >&2 +} + +print_whistle_setup_guide() { + info "missing required command: w2 (or whistle)" + if ! command -v npm >/dev/null 2>&1; then + info "prerequisite missing: npm (install Node.js first)" + fi + info "preflight setup:" + info " 1) install whistle: npm install -g whistle" + info " 2) start whistle: w2 start" + info " 3) install TLS CA: w2 ca" + info "note: TLS CA install may require system trust permission/admin authorization" +} + +is_sourced() { + if [ -n "${BASH_VERSION-}" ]; then + [ "${BASH_SOURCE[0]}" != "$0" ] + return + fi + if [ -n "${ZSH_VERSION-}" ]; then + case "${ZSH_EVAL_CONTEXT-}" in + *:file:*) return 0 ;; + *) return 1 ;; + esac + fi + return 1 +} + +pick_whistle_cmd() { + if command -v w2 >/dev/null 2>&1; then + printf '%s\n' "w2" + return 0 + fi + if command -v whistle >/dev/null 2>&1; then + printf '%s\n' "whistle" + return 0 + fi + print_whistle_setup_guide + fail "missing required command: w2 (or whistle)" +} + +ensure_whistle_running() { + wcmd="$1" + whistle_status="$($wcmd status 2>&1 || true)" + case "$whistle_status" in + *"No running Whistle instances"*) + info "Whistle is not running, starting it now..." + "$wcmd" start >/dev/null 2>&1 || fail "failed to start Whistle" + ;; + esac +} + +build_rules() { + mode="$1" + boe_env_name="$2" + + case "$mode" in + pre) + cat <<'RULES' +/^https:\/\/open\.feishu\.cn\/(.*)$/ https://open.feishu-pre.cn/$1 +https://open.feishu.cn/ reqHeaders://Env=Pre_release +/^https:\/\/accounts\.feishu\.cn\/(.*)$/ https://accounts.feishu-pre.cn/$1 +RULES + ;; + boe) + cat <"$tmp_js" <<'JS' +module.exports = function (callback) { + callback({ + name: process.env.LARK_CLI_WHISTLE_RULE_NAME || 'lark-cli-openapi-env-switch', + groupName: process.env.LARK_CLI_WHISTLE_RULE_GROUP || 'lark-cli', + rules: process.env.LARK_CLI_WHISTLE_RULES || '' + }); +}; +JS +} + +apply_env() { + mode="$1" + host="$2" + port="$3" + + case "$mode" in + pre) + proxy_url="http://${host}:${port}" + export HTTPS_PROXY="$proxy_url" + export https_proxy="$proxy_url" + export HTTP_PROXY="$proxy_url" + export http_proxy="$proxy_url" + export ALL_PROXY="$proxy_url" + export all_proxy="$proxy_url" + unset LARKSUITE_CLI_CONFIG_DIR + unset LARK_CLI_NO_PROXY + info "proxy env enabled: ${proxy_url}" + ;; + boe) + proxy_url="http://${host}:${port}" + export HTTPS_PROXY="$proxy_url" + export https_proxy="$proxy_url" + export HTTP_PROXY="$proxy_url" + export http_proxy="$proxy_url" + export ALL_PROXY="$proxy_url" + export all_proxy="$proxy_url" + unset LARK_CLI_NO_PROXY + + [ -n "${HOME:-}" ] || fail "HOME is empty, cannot set boe config dir" + boe_config_dir="${HOME}/.lark-cli-boe" + mkdir -p "$boe_config_dir" || fail "failed to create boe config dir: ${boe_config_dir}" + export LARKSUITE_CLI_CONFIG_DIR="$boe_config_dir" + + info "proxy env enabled: ${proxy_url}" + info "LARKSUITE_CLI_CONFIG_DIR=${LARKSUITE_CLI_CONFIG_DIR}" + info "hint: if BOE app is not initialized, run: lark-cli config init" + ;; + off) + unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy + unset LARKSUITE_CLI_CONFIG_DIR + info "proxy env cleared" + ;; + esac +} + +main() { + if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ] || [ "${1:-}" = "help" ] || [ "${1:-}" = "" ]; then + usage + return 0 + fi + + mode="$1" + shift + + case "$mode" in + cancel) + mode="off" + ;; + pre|boe|off) + ;; + *) + fail "invalid mode: ${mode}; expected pre|boe|off|cancel" + ;; + esac + + boe_env_name="boe_bitable_openapi_v2" + host="127.0.0.1" + port="8899" + + while [ "$#" -gt 0 ]; do + case "$1" in + --boe-env-name) + [ "$#" -ge 2 ] || fail "--boe-env-name requires a value" + boe_env_name="$2" + shift 2 + ;; + --host) + [ "$#" -ge 2 ] || fail "--host requires a value" + host="$2" + shift 2 + ;; + --port) + [ "$#" -ge 2 ] || fail "--port requires a value" + port="$2" + shift 2 + ;; + *) + fail "unknown arg: $1" + ;; + esac + done + + [ "$mode" != "boe" ] || [ -n "$boe_env_name" ] || fail "boe mode requires non-empty --boe-env-name" + + wcmd="$(pick_whistle_cmd)" + ensure_whistle_running "$wcmd" + + rules="$(build_rules "$mode" "$boe_env_name")" + + tmp_js="$(mktemp "${TMPDIR:-/tmp}/lark-env-whistle-rule.XXXXXX")" || fail "failed to create temp file" + trap 'rm -f "$tmp_js"' EXIT HUP INT TERM + write_rule_js "$tmp_js" + + LARK_CLI_WHISTLE_RULE_NAME="$RULE_NAME" \ + LARK_CLI_WHISTLE_RULE_GROUP="$RULE_GROUP" \ + LARK_CLI_WHISTLE_RULES="$rules" \ + "$wcmd" add "$tmp_js" --force >/dev/null 2>&1 || fail "failed to apply Whistle rules" + + if ! is_sourced; then + info "Whistle rule updated, but current shell env is unchanged." + info "Run with source to apply env vars in current shell:" + info " . ./$(script_name) ${mode}" + return 0 + fi + + apply_env "$mode" "$host" "$port" + + if [ "$mode" = "boe" ]; then + info "active mode=boe, x-tt-env=${boe_env_name}" + else + info "active mode=${mode}" + fi +} + +main "$@" diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index ab711a59..6e13c986 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -132,7 +132,7 @@ func TestShortcutsCatalog(t *testing.T) { "+table-list", "+table-get", "+table-create", "+table-update", "+table-delete", "+field-list", "+field-get", "+field-create", "+field-update", "+field-delete", "+field-search-options", "+view-list", "+view-get", "+view-create", "+view-delete", "+view-get-filter", "+view-set-filter", "+view-get-visible-fields", "+view-set-visible-fields", "+view-get-group", "+view-set-group", "+view-get-sort", "+view-set-sort", "+view-get-timebar", "+view-set-timebar", "+view-get-card", "+view-set-card", "+view-rename", - "+record-list", "+record-search", "+record-get", "+record-upsert", "+record-batch-create", "+record-batch-update", "+record-upload-attachment", "+record-delete", + "+record-list", "+record-search", "+record-get", "+record-upsert", "+record-batch-create", "+record-batch-update", "+record-share-link-create", "+record-share-link-batch-create", "+record-upload-attachment", "+record-delete", "+record-history-list", "+base-get", "+base-copy", "+base-create", "+role-create", "+role-delete", "+role-update", "+role-list", "+role-get", "+advperm-enable", "+advperm-disable", diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index f91ca579..313bd74c 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -112,6 +112,73 @@ func dryRunRecordHistoryList(_ context.Context, runtime *common.RuntimeContext) Set("base_token", runtime.Str("base-token")) } +func dryRunRecordShareBatch(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + recordIDs := deduplicateRecordIDs(runtime) + return common.NewDryRunAPI(). + POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/share_links/batch"). + Body(map[string]interface{}{"records": recordIDs}). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)) +} + +func dryRunRecordShare(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id/share_links"). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("record_id", runtime.Str("record-id")) +} + +func validateRecordShareBatch(runtime *common.RuntimeContext) error { + recordIDs := runtime.StrSlice("record-ids") + if len(recordIDs) == 0 { + return common.FlagErrorf("--record-ids is required and must not be empty") + } + if len(recordIDs) > maxShareBatchSize { + return common.FlagErrorf("--record-ids exceeds maximum limit of %d (got %d)", maxShareBatchSize, len(recordIDs)) + } + return nil +} + +func deduplicateRecordIDs(runtime *common.RuntimeContext) []string { + raw := runtime.StrSlice("record-ids") + seen := make(map[string]bool, len(raw)) + result := make([]string, 0, len(raw)) + for _, id := range raw { + if id != "" && !seen[id] { + seen[id] = true + result = append(result, id) + } + } + return result +} + +func executeRecordShareBatch(runtime *common.RuntimeContext) error { + recordIDs := deduplicateRecordIDs(runtime) + body := map[string]interface{}{ + "record_ids": recordIDs, + } + data, err := baseV3Call(runtime, "POST", + baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", "share_links", "batch"), + nil, body) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + +func executeRecordShare(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "POST", + baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id"), "share_links"), + nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + func validateRecordJSON(runtime *common.RuntimeContext) error { return nil } diff --git a/shortcuts/base/record_share_link_batch_create.go b/shortcuts/base/record_share_link_batch_create.go new file mode 100644 index 00000000..717d9d53 --- /dev/null +++ b/shortcuts/base/record_share_link_batch_create.go @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +const maxShareBatchSize = 100 + +var BaseRecordShareLinkBatchCreate = common.Shortcut{ + Service: "base", + Command: "+record-share-link-batch-create", + Description: "Batch generate record share links (max 100 records per request)", + Risk: "read", + Scopes: []string{"base:record:read"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + baseTokenFlag(true), + tableRefFlag(true), + {Name: "record-ids", Type: "string_slice", Desc: "record IDs to generate share links for (comma-separated or repeatable, max 100)", Required: true}, + }, + Tips: []string{ + `Example: --base-token xxx --table-id tblxxx --record-ids rec001,rec002,rec003`, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateRecordShareBatch(runtime) + }, + DryRun: dryRunRecordShareBatch, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeRecordShareBatch(runtime) + }, +} diff --git a/shortcuts/base/record_share_link_create.go b/shortcuts/base/record_share_link_create.go new file mode 100644 index 00000000..4e3adc3e --- /dev/null +++ b/shortcuts/base/record_share_link_create.go @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseRecordShareLinkCreate = common.Shortcut{ + Service: "base", + Command: "+record-share-link-create", + Description: "Generate a share link for a single record", + Risk: "read", + Scopes: []string{"base:record:read"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + baseTokenFlag(true), + tableRefFlag(true), + recordRefFlag(true), + }, + Tips: []string{ + `Example: --base-token xxx --table-id tblxxx --record-id recxxx`, + }, + DryRun: dryRunRecordShare, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeRecordShare(runtime) + }, +} diff --git a/shortcuts/base/shortcuts.go b/shortcuts/base/shortcuts.go index 32ca8406..491137aa 100644 --- a/shortcuts/base/shortcuts.go +++ b/shortcuts/base/shortcuts.go @@ -42,6 +42,8 @@ func Shortcuts() []common.Shortcut { BaseRecordUpsert, BaseRecordBatchCreate, BaseRecordBatchUpdate, + BaseRecordShareLinkCreate, + BaseRecordShareLinkBatchCreate, BaseRecordUploadAttachment, BaseRecordDelete, BaseRecordHistoryList, diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 1ddb69e5..ae957418 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -181,6 +181,12 @@ func (ctx *RuntimeContext) StrArray(name string) []string { return v } +// StrSlice returns a string-slice flag value (supports CSV splitting and repeated flags). +func (ctx *RuntimeContext) StrSlice(name string) []string { + v, _ := ctx.Cmd.Flags().GetStringSlice(name) + return v +} + // ── API helpers ── // CallAPI uses an internal HTTP wrapper with limited control over request/response. @@ -849,6 +855,8 @@ func registerShortcutFlags(cmd *cobra.Command, s *Shortcut) { cmd.Flags().Int(fl.Name, d, desc) case "string_array": cmd.Flags().StringArray(fl.Name, nil, desc) + case "string_slice": + cmd.Flags().StringSlice(fl.Name, nil, desc) default: cmd.Flags().String(fl.Name, fl.Default, desc) } diff --git a/skills/lark-base/references/lark-base-record-share-link-batch-create.md b/skills/lark-base/references/lark-base-record-share-link-batch-create.md new file mode 100644 index 00000000..f658a257 --- /dev/null +++ b/skills/lark-base/references/lark-base-record-share-link-batch-create.md @@ -0,0 +1,73 @@ +# base +record-share-link-batch-create + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +批量生成记录分享链接(单次调用最多传入 100 条)。 + +## 适用场景(重点) + +- 适合需要一次性获取多条记录分享链接的场景,例如批量导出分享链接、批量发送通知等。 +- 当需要处理的记录数超过 100 条时,需分批调用。 + +## 推荐命令 + +```bash +# 请使用“,”来分隔多个 record id +lark-cli base +record-share-link-batch-create \ + --base-token xxx \ + --table-id tbl_xxx \ + --record-ids rec001,rec002,rec003 +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|----| +| `--base-token ` | 是 | Base Token | +| `--table-id ` | 是 | 表 ID | +| `--record-ids ` | 是 | 记录 ID 列表,需要使用逗号分隔,最多 100 条 | + +## API 入参详情 + +**HTTP 方法和路径:** + +```http +POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records/share_links/batch +``` + +**请求体:** + +```json +{ + "record_ids": ["rec001", "rec002", "rec003"] +} +``` + +> CLI 会自动对 `--record-ids` 去重后再调用接口。 + +## 返回重点 + +- 成功时直接返回接口 `data` 字段内容,包含 `record_share_links` 映射(key 为 record_id,value 为分享链接)。结构如下: + +```json +{ + "record_share_links": { + "rec001": "https://example.feishu.cn/record/TW2wrdbkoeoYXYcwvyIczJ2ZnFb", + "rec002": "https://example.feishu.cn/record/aB3xKmNpQrStUvWxYz123456789", + "rec003": "https://example.feishu.cn/record/cD4yLmNoPqRsTuVwXz987654321" + } +} +``` +- 若部分记录ID无权限/不存在,则 record_share_links 中只会包括有效的记录ID对应的分享链接 +- 若全部记录ID都无权限/不存在,则会返回错误信息 `records do not exist or no read permission` + +## 坑点 + +- ⚠️ 单次最多 100 条记录,超出会被 CLI 校验拦截。 +- ⚠️ 重复的 record_id 会在调用前自动去重。 +- ⚠️ `--record-ids` 为空时会被校验拦截。 + +## 参考 + +- [lark-base-record.md](lark-base-record.md) — record 索引页 +- [lark-base-record-share-link-create.md](lark-base-record-share-link-create.md) — 单条生成分享链接 diff --git a/skills/lark-base/references/lark-base-record-share-link-create.md b/skills/lark-base/references/lark-base-record-share-link-create.md new file mode 100644 index 00000000..ff63554e --- /dev/null +++ b/skills/lark-base/references/lark-base-record-share-link-create.md @@ -0,0 +1,45 @@ +# base +record-share-link-create + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +为单条记录生成分享链接。 + +## 推荐命令 + +```bash +lark-cli base +record-share-link-create \ + --base-token xxx \ + --table-id tbl_xxx \ + --record-id rec_xxx +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--base-token ` | 是 | Base Token | +| `--table-id ` | 是 | 表 ID | +| `--record-id ` | 是 | 记录 ID | + +## API 入参详情 + +**HTTP 方法和路径:** + +``` +POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id/share_links +``` + +## 返回重点 + +- 成功时直接返回接口 `data` 字段内容,包含该记录的分享链接信息。结构如下: +```json +{ + "record_share_link": "https://example.feishu.cn/record/TW2wrdbkoeoYXYcwvyIczJ2ZnFb" +} +``` +- 若记录ID无权限/不存在,则会返回错误信息 `records do not exist or no read permission` + +## 参考 + +- [lark-base-record.md](lark-base-record.md) — record 索引页 +- [lark-base-record-share-link-batch-create.md](lark-base-record-share-link-batch-create.md) — 批量生成分享链接 diff --git a/skills/lark-base/references/lark-base-record.md b/skills/lark-base/references/lark-base-record.md index 078f8499..3ff6c907 100644 --- a/skills/lark-base/references/lark-base-record.md +++ b/skills/lark-base/references/lark-base-record.md @@ -17,6 +17,8 @@ record 相关命令索引。 | [lark-base-record-upload-attachment.md](lark-base-record-upload-attachment.md) | `+record-upload-attachment` | 上传本地文件到附件字段并更新记录 | | [`../../lark-doc/references/lark-doc-media-download.md`](../../lark-doc/references/lark-doc-media-download.md) | `lark-cli docs +media-download` | 下载 Base 附件到本地(附件的 `file_token` 来自 `+record-get` 的附件字段) | | [lark-base-record-delete.md](lark-base-record-delete.md) | `+record-delete` | 删除记录 | +| [lark-base-record-share-link-create.md](lark-base-record-share-link-create.md) | `+record-share-link-create` | 为单条记录生成分享链接 | +| [lark-base-record-share-link-batch-create.md](lark-base-record-share-link-batch-create.md) | `+record-share-link-batch-create` | 批量生成记录分享链接(最多 100 条)| ## 说明