Compare commits

...

5 Commits

Author SHA1 Message Date
shanglei
67ee0defab fix(format): handle typed slices in table formatting and add regression tests 2026-04-14 16:38:06 +08:00
mazhe-nerd
2a301246f9 feat: skip auth check (#451)
The secondary confirmation step in the interactive login process has been removed (Phase 2: After the user selects the complete domain name, permission level, and scope, they no longer need to confirm "authorize" again and can directly proceed to the authorization process).
2026-04-14 11:38:39 +08:00
Schumi Lin
abc374f1a3 docs(readme): add Attendance to Features table (#460)
* docs(readme): add lark-attendance to Agent Skills table and update counts

- Add lark-attendance to Agent Skills table in both EN and ZH READMEs
- Add Attendance to ZH Features table
- Update skill count 21 → 22 and domain count 13 → 14
2026-04-14 10:55:33 +08:00
caojie0621
2910cde73a feat(sheets): add value format documentation for formula and special types (#456)
Document the correct object format for writing formulas, URLs with
text, mentions, and dropdown lists via --values parameter. Add
examples contrasting correct object format vs incorrect plain string.
2026-04-14 00:07:45 +08:00
liangshuo-1
7fdc162ff7 chore: bump version to v1.0.10 and update changelog (#457)
Change-Id: I6f8f6b474e2bcedec4646c69b35235c52906c74e
2026-04-13 22:58:20 +08:00
9 changed files with 260 additions and 33 deletions

View File

@@ -2,6 +2,31 @@
All notable changes to this project will be documented in this file.
## [v1.0.10] - 2026-04-13
### Features
- **im**: Support im oapi range download for large files (#283)
- **sheets**: Add filter view and condition shortcuts (#422)
- **wiki**: Add wiki move shortcut with async task polling (#436)
- **drive**: Add drive `+create-shortcut` shortcut (#432)
- **drive**: Add drive files patch metadata API (#444)
- **task**: Support `--section-guid` flag in tasklist-task-add shortcut (#430)
### Bug Fixes
- **base**: Support large base attachment uploads (#441)
- **config**: Clarify init copy for TTY, preserve original for AI (#448)
- **im**: Reject `--user-id` under bot identity for chat-messages-list (#340)
- **mail**: Add missing scopes for mail `+watch` shortcut (#357)
- **mail**: Restrict `--output-dir` to current working directory (#376)
### Documentation
- **wiki**: Add wiki member operations to lark-wiki skill (#417)
- **task**: Document sections API resources, permissions, and URL parsing (#430)
- **doc**: Clarify when markdown escaping is needed (#312)
## [v1.0.9] - 2026-04-11
### Features
@@ -303,6 +328,7 @@ Bundled AI agent skills for intelligent assistance:
- Bilingual documentation (English & Chinese).
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
[v1.0.10]: https://github.com/larksuite/cli/releases/tag/v1.0.10
[v1.0.9]: https://github.com/larksuite/cli/releases/tag/v1.0.9
[v1.0.8]: https://github.com/larksuite/cli/releases/tag/v1.0.8
[v1.0.7]: https://github.com/larksuite/cli/releases/tag/v1.0.7

View File

@@ -6,14 +6,14 @@
[中文版](./README.zh.md) | [English](./README.md)
The official [Lark/Feishu](https://www.larksuite.com/) CLI tool, maintained by the [larksuite](https://github.com/larksuite) team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Slides, Calendar, Mail, Tasks, Meetings, and more, with 200+ commands and 21 AI Agent [Skills](./skills/).
The official [Lark/Feishu](https://www.larksuite.com/) CLI tool, maintained by the [larksuite](https://github.com/larksuite) team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Slides, Calendar, Mail, Tasks, Meetings, and more, with 200+ commands and 22 AI Agent [Skills](./skills/).
[Install](#installation--quick-start) · [AI Agent Skills](#agent-skills) · [Auth](#authentication) · [Commands](#three-layer-command-system) · [Advanced](#advanced-usage) · [Security](#security--risk-warnings-read-before-use) · [Contributing](#contributing)
## Why lark-cli?
- **Agent-Native Design** — 21 structured [Skills](./skills/) out of the box, compatible with popular AI tools — Agents can operate Lark with zero extra setup
- **Wide Coverage** — 13 business domains, 200+ curated commands, 21 AI Agent [Skills](./skills/)
- **Agent-Native Design** — 22 structured [Skills](./skills/) out of the box, compatible with popular AI tools — Agents can operate Lark with zero extra setup
- **Wide Coverage** — 14 business domains, 200+ curated commands, 22 AI Agent [Skills](./skills/)
- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output to maximize Agent call success rates
- **Open Source, Zero Barriers** — MIT license, ready to use, just `npm install`
- **Up and Running in 3 Minutes** — One-click app creation, interactive login, from install to first API call in just 3 steps
@@ -36,6 +36,7 @@ The official [Lark/Feishu](https://www.larksuite.com/) CLI tool, maintained by t
| 👤 Contact | Search users by name/email/phone, get user profiles |
| 📧 Mail | Browse, search, read emails, send, reply, forward, manage drafts, watch new mail |
| 🎥 Meetings | Search meeting records, query meeting minutes & recordings |
| 🕐 Attendance | Query personal attendance check-in records |
| ✍️ Approval | Query approval tasks, approve/reject/transfer tasks, cancel and CC instances |
## Installation & Quick Start
@@ -149,6 +150,7 @@ lark-cli auth status
| `lark-minutes` | Minutes metadata & AI artifacts (summary, todos, chapters) |
| `lark-openapi-explorer` | Explore underlying APIs from official docs |
| `lark-skill-maker` | Custom skill creation framework |
| `lark-attendance` | Query personal attendance check-in records |
| `lark-approval` | Query approval tasks, approve/reject/transfer tasks, cancel and CC instances |
| `lark-workflow-meeting-summary` | Workflow: meeting minutes aggregation & structured report |
| `lark-workflow-standup-report` | Workflow: agenda & todo summary |

View File

@@ -6,14 +6,14 @@
[中文版](./README.zh.md) | [English](./README.md)
飞书官方 CLI 工具,由 [larksuite](https://github.com/larksuite) 团队维护 — 让人类和 AI Agent 都能在终端中操作飞书。覆盖消息、文档、多维表格、电子表格、幻灯片、日历、邮箱、任务、会议等核心业务域,提供 200+ 命令及 21 个 AI Agent [Skills](./skills/)。
飞书官方 CLI 工具,由 [larksuite](https://github.com/larksuite) 团队维护 — 让人类和 AI Agent 都能在终端中操作飞书。覆盖消息、文档、多维表格、电子表格、幻灯片、日历、邮箱、任务、会议等核心业务域,提供 200+ 命令及 22 个 AI Agent [Skills](./skills/)。
[安装](#安装与快速开始) · [AI Agent Skills](#agent-skills) · [认证](#认证) · [命令](#三层命令调用) · [进阶用法](#进阶用法) · [安全](#安全与风险提示使用前必读) · [贡献](#贡献)
## 为什么选 lark-cli
- **为 Agent 原生设计** — 21 个 [Skills](./skills/) 开箱即用,适配主流 AI 工具Agent 无需额外适配即可操作飞书
- **覆盖面广** — 13 大业务域、200+ 精选命令、21 个 AI Agent [Skills](./skills/)
- **为 Agent 原生设计** — 22 个 [Skills](./skills/) 开箱即用,适配主流 AI 工具Agent 无需额外适配即可操作飞书
- **覆盖面广** — 14 大业务域、200+ 精选命令、22 个 AI Agent [Skills](./skills/)
- **AI 友好调优** — 每条命令经过 Agent 实测验证,提供更友好的参数、智能默认值和结构化输出,大幅提升 Agent 调用成功率
- **开源零门槛** — MIT 协议,开箱即用,`npm install` 即可使用
- **三分钟上手** — 一键创建应用、交互式登录授权,从安装到第一次 API 调用只需三步
@@ -36,6 +36,7 @@
| 👤 通讯录 | 按姓名/邮箱/手机号搜索用户、获取用户信息 |
| 📧 邮箱 | 浏览、搜索、阅读邮件,发送、回复、转发邮件,管理草稿,监听新邮件 |
| 🎥 视频会议 | 搜索会议记录、查询会议纪要与录制 |
| 🕐 考勤打卡 | 查询个人考勤打卡记录 |
| ✍️ 审批 | 查询审批任务、同意/拒绝/转交审批任务、撤回与抄送审批实例 |
## 安装与快速开始
@@ -150,6 +151,7 @@ lark-cli auth status
| `lark-minutes` | 妙记元数据与 AI 产物(总结、待办、章节) |
| `lark-openapi-explorer` | 从官方文档探索底层 API |
| `lark-skill-maker` | 自定义 skill 创建框架 |
| `lark-attendance` | 查询个人考勤打卡记录 |
| `lark-approval` | 审批任务查询、同意/拒绝/转交审批任务、撤回与抄送审批实例 |
| `lark-workflow-meeting-summary` | 工作流:会议纪要汇总与结构化报告 |
| `lark-workflow-standup-report` | 工作流:日程待办摘要 |

View File

@@ -184,27 +184,6 @@ func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg) (*i
}
fmt.Fprintf(ios.ErrOut, msg.SummaryScopes, len(scopes), scopePreview)
// Phase 2: confirmation
var confirmed bool
form2 := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title(msg.ConfirmAuth).
Value(&confirmed),
),
).WithTheme(cmdutil.ThemeFeishu())
if err := form2.Run(); err != nil {
if err == huh.ErrUserAborted {
return nil, output.ErrBare(1)
}
return nil, err
}
if !confirmed {
return nil, output.ErrBare(1)
}
return &interactiveResult{
Domains: selectedDomains,
ScopeLevel: permLevel,

View File

@@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"reflect"
"sort"
)
@@ -17,13 +18,39 @@ var knownArrayFields = []string{
"members", "departments", "calendar_list", "acl_list", "freebusy_list",
}
// isSliceLike reports whether v is any kind of slice (e.g. []interface{},
// []map[string]interface{}, []string, etc.), using reflect so that the
// check is not limited to a single concrete slice type.
func isSliceLike(v interface{}) bool {
if v == nil {
return false
}
return reflect.TypeOf(v).Kind() == reflect.Slice
}
// toGenericSlice converts any slice type to []interface{} by re-boxing each
// element. This only changes the outer container type; individual elements
// retain their original dynamic type (e.g. map[string]interface{} stays as-is).
// Returns nil if v is not a slice.
func toGenericSlice(v interface{}) []interface{} {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
return nil
}
out := make([]interface{}, rv.Len())
for i := 0; i < rv.Len(); i++ {
out[i] = rv.Index(i).Interface()
}
return out
}
// FindArrayField finds the primary array field in a response's data object.
// It first checks knownArrayFields in priority order, then falls back to
// the lexicographically smallest unknown array field for deterministic results.
func FindArrayField(data map[string]interface{}) string {
for _, name := range knownArrayFields {
if arr, ok := data[name]; ok {
if _, isArr := arr.([]interface{}); isArr {
if isSliceLike(arr) {
return name
}
}
@@ -31,7 +58,7 @@ func FindArrayField(data map[string]interface{}) string {
// Fallback: lexicographically first array field (deterministic)
var candidates []string
for k, v := range data {
if _, isArr := v.([]interface{}); isArr {
if isSliceLike(v) {
candidates = append(candidates, k)
}
}
@@ -81,7 +108,7 @@ func ExtractItems(data interface{}) []interface{} {
// Strategy 1: Lark API envelope — result["data"][arrayField]
if dataObj, ok := resultMap["data"].(map[string]interface{}); ok {
if field := FindArrayField(dataObj); field != "" {
if items, ok := dataObj[field].([]interface{}); ok {
if items := toGenericSlice(dataObj[field]); items != nil {
return items
}
}
@@ -90,7 +117,7 @@ func ExtractItems(data interface{}) []interface{} {
// Strategy 2: direct map — result[arrayField]
// Covers shortcut-level data like {"members":[…], "total":5, "has_more":false}
if field := FindArrayField(resultMap); field != "" {
if items, ok := resultMap[field].([]interface{}); ok {
if items := toGenericSlice(resultMap[field]); items != nil {
return items
}
}

View File

@@ -266,6 +266,129 @@ func TestExtractItems(t *testing.T) {
}
}
// --- Typed-slice regression tests ---
// These cover the scenario where shortcut code uses []map[string]interface{}
// (or other typed slices) instead of []interface{} in outData.
func TestExtractItems_TypedMapSlice(t *testing.T) {
// Simulates shortcut pattern: outData["chats"] = []map[string]interface{}{...}
data := map[string]interface{}{
"chats": []map[string]interface{}{
{"chat_id": "oc_abc", "name": "Test Chat"},
{"chat_id": "oc_def", "name": "Dev Chat"},
},
"total": 2,
"has_more": false,
}
items := ExtractItems(data)
if len(items) != 2 {
t.Fatalf("expected 2 items from typed map slice, got %d", len(items))
}
// Verify elements are still map[string]interface{} (flattenItem can handle them)
for i, item := range items {
if _, ok := item.(map[string]interface{}); !ok {
t.Errorf("item[%d] should be map[string]interface{}, got %T", i, item)
}
}
}
func TestExtractItems_TypedMapSlice_InEnvelope(t *testing.T) {
// Typed slice inside a Lark API envelope: result["data"]["items"] = []map[string]interface{}{...}
data := map[string]interface{}{
"code": float64(0),
"data": map[string]interface{}{
"items": []map[string]interface{}{
{"id": "1", "name": "Alice"},
},
"has_more": false,
},
}
items := ExtractItems(data)
if len(items) != 1 {
t.Fatalf("expected 1 item from typed slice in envelope, got %d", len(items))
}
}
func TestFormatValue_Table_TypedMapSlice(t *testing.T) {
// The core bug: --format table with []map[string]interface{} should render
// multi-column table, not a key-value two-column fallback.
data := map[string]interface{}{
"chats": []map[string]interface{}{
{"chat_id": "oc_abc", "name": "Lark Dev"},
},
"total": 1,
"has_more": false,
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatTable)
out := buf.String()
// Should have column headers from the data fields
if !strings.Contains(out, "chat_id") {
t.Errorf("table should contain 'chat_id' column header, got:\n%s", out)
}
if !strings.Contains(out, "name") {
t.Errorf("table should contain 'name' column header, got:\n%s", out)
}
if !strings.Contains(out, "Lark Dev") {
t.Errorf("table should contain data value 'Lark Dev', got:\n%s", out)
}
// Should NOT render as key-value fallback (metadata as rows)
if strings.Contains(out, "has_more") {
t.Errorf("table should not contain metadata 'has_more' as a row, got:\n%s", out)
}
}
func TestFormatValue_CSV_TypedMapSlice(t *testing.T) {
data := map[string]interface{}{
"messages": []map[string]interface{}{
{"message_id": "om_abc", "content": "hello"},
{"message_id": "om_def", "content": "world"},
},
"total": 2,
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatCSV)
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines) != 3 {
t.Fatalf("CSV should have header + 2 rows, got %d lines:\n%s", len(lines), buf.String())
}
// Header should contain data field names, not top-level map keys
header := lines[0]
if !strings.Contains(header, "message_id") {
t.Errorf("CSV header should contain 'message_id', got: %s", header)
}
}
func TestFormatValue_NDJSON_TypedMapSlice(t *testing.T) {
data := map[string]interface{}{
"tasks": []map[string]interface{}{
{"guid": "t1", "url": "https://example.com/t1"},
{"guid": "t2", "url": "https://example.com/t2"},
},
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatNDJSON)
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("NDJSON should output 2 lines, got %d:\n%s", len(lines), buf.String())
}
for i, line := range lines {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(line), &obj); err != nil {
t.Errorf("NDJSON line %d should be valid JSON: %s", i, line)
}
if _, ok := obj["guid"]; !ok {
t.Errorf("NDJSON line %d should contain 'guid' field, got: %s", i, line)
}
}
}
func TestFormatValue_LegacyFormats(t *testing.T) {
data := map[string]interface{}{
"data": map[string]interface{}{

View File

@@ -1,6 +1,6 @@
{
"name": "@larksuite/cli",
"version": "1.0.9",
"version": "1.0.10",
"description": "The official CLI for Lark/Feishu open platform",
"bin": {
"lark-cli": "scripts/run.js"

View File

@@ -126,4 +126,38 @@ lark-cli sheets spreadsheet.sheet.filters update \
**常见错误:**
- `Wrong Filter Value`:筛选已存在,需要先 delete 再 create
- `Excess Limit`update 时重复添加同一列条件
- `Excess Limit`update 时重复添加同一列条件
### 单元格数据类型
接受二维数组的 shortcut`+write`/`+append` 的 `--values`、`+create` 的 `--data`)中,每个单元格值支持以下类型。**公式、带文本链接、@人、@文档、下拉列表必须使用对象格式**,直接传字符串会被当作纯文本存储。
| 类型 | 写入格式 | 示例 |
|------|---------|------|
| 字符串 | `"文本"` | `"hello"` |
| 数字 | `数字` | `123`、`3.14` |
| 日期 | `数字`(自 1899-12-30 起的天数,需先设单元格日期格式) | `42101` |
| 链接(纯 URL | `"URL 字符串"` | `"https://example.com"` |
| 链接(带文本) | `{"type":"url","text":"显示文本","link":"URL"}` | `{"type":"url","text":"飞书","link":"https://www.feishu.cn"}` |
| 邮箱 | `"邮箱字符串"` | `"user@example.com"` |
| **公式** | `{"type":"formula","text":"=公式"}` | `{"type":"formula","text":"=SUM(A1:A10)"}` |
| @人 | `{"type":"mention","text":"标识","textType":"email\|openId\|unionId","notify":false}` | `{"type":"mention","text":"user@example.com","textType":"email","notify":false}`notify 可选,默认 false仅在用户明确要求通知时设为 true |
| @文档 | `{"type":"mention","textType":"fileToken","text":"token","objType":"类型"}` | `{"type":"mention","textType":"fileToken","text":"shtXXX","objType":"sheet"}` |
| 下拉列表 | `{"type":"multipleValue","values":[值1,值2]}` | `{"type":"multipleValue","values":["选项A","选项B"]}` |
**写入公式示例**
```bash
# ✅ 正确:使用对象格式
lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \
--values '[[{"type":"formula","text":"=SUM(C2:C5)"}]]'
# ❌ 错误:直接传字符串,会被存为纯文本
lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \
--values '[["=SUM(C2:C5)"]]'
```
**限制**
- 公式不支持跨表引用IMPORTRANGE
- @人仅支持同租户用户,单次最多 50 人
- 下拉列表需先调用设置下拉列表接口,值中的字符串不能包含逗号

View File

@@ -141,6 +141,40 @@ lark-cli sheets spreadsheet.sheet.filters update \
- `Wrong Filter Value`:筛选已存在,需要先 delete 再 create
- `Excess Limit`update 时重复添加同一列条件
### 单元格数据类型
接受二维数组的 shortcut`+write`/`+append` 的 `--values`、`+create` 的 `--data`)中,每个单元格值支持以下类型。**公式、带文本链接、@人、@文档、下拉列表必须使用对象格式**,直接传字符串会被当作纯文本存储。
| 类型 | 写入格式 | 示例 |
|------|---------|------|
| 字符串 | `"文本"` | `"hello"` |
| 数字 | `数字` | `123`、`3.14` |
| 日期 | `数字`(自 1899-12-30 起的天数,需先设单元格日期格式) | `42101` |
| 链接(纯 URL | `"URL 字符串"` | `"https://example.com"` |
| 链接(带文本) | `{"type":"url","text":"显示文本","link":"URL"}` | `{"type":"url","text":"飞书","link":"https://www.feishu.cn"}` |
| 邮箱 | `"邮箱字符串"` | `"user@example.com"` |
| **公式** | `{"type":"formula","text":"=公式"}` | `{"type":"formula","text":"=SUM(A1:A10)"}` |
| @人 | `{"type":"mention","text":"标识","textType":"email\|openId\|unionId","notify":false}` | `{"type":"mention","text":"user@example.com","textType":"email","notify":false}`notify 可选,默认 false仅在用户明确要求通知时设为 true |
| @文档 | `{"type":"mention","textType":"fileToken","text":"token","objType":"类型"}` | `{"type":"mention","textType":"fileToken","text":"shtXXX","objType":"sheet"}` |
| 下拉列表 | `{"type":"multipleValue","values":[值1,值2]}` | `{"type":"multipleValue","values":["选项A","选项B"]}` |
**写入公式示例**
```bash
# ✅ 正确:使用对象格式
lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \
--values '[[{"type":"formula","text":"=SUM(C2:C5)"}]]'
# ❌ 错误:直接传字符串,会被存为纯文本
lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \
--values '[["=SUM(C2:C5)"]]'
```
**限制**
- 公式不支持跨表引用IMPORTRANGE
- @人仅支持同租户用户,单次最多 50 人
- 下拉列表需先调用设置下拉列表接口,值中的字符串不能包含逗号
## Shortcuts推荐优先使用
Shortcut 是对常用操作的高级封装(`lark-cli sheets +<verb> [flags]`)。有 Shortcut 的操作优先使用。