Compare commits

...

1 Commits

Author SHA1 Message Date
zhangjun.1
58466a3d94 feat: replace summary and todo 2026-06-03 10:52:42 +08:00
8 changed files with 671 additions and 1 deletions

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package minutes
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
const minutesSummaryMarkdownTip = "Summary accepts any text; unsupported Markdown is saved but may display as literal raw text in Minutes. For best rendering, prefer plain text, line breaks, headings (#, ##, ###), bold (**text**), and lists (-, *, or 1.)."
// MinutesSummary replaces the AI summary of a minute.
var MinutesSummary = common.Shortcut{
Service: "minutes",
Command: "+summary",
Description: "Replace the AI summary of a minute",
Risk: "write",
Scopes: []string{"minutes:minutes:update"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "minute-token", Desc: "minute token", Required: true},
{Name: "summary", Desc: "replacement summary text (Markdown subset renders best in Minutes)", Required: true, Input: []string{common.File, common.Stdin}},
},
Tips: []string{
minutesSummaryMarkdownTip,
"Use `lark-cli vc +notes --minute-tokens <token>` to read the current summary before replacing it.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
minuteToken := runtime.Str("minute-token")
if minuteToken == "" {
return output.ErrValidation("--minute-token is required")
}
if err := validate.ResourceName(minuteToken, "--minute-token"); err != nil {
return output.ErrValidation("%s", err)
}
summary := strings.TrimSpace(runtime.Str("summary"))
if summary == "" {
return output.ErrValidation("--summary is required")
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
PUT(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/summary", validate.EncodePathSegment(runtime.Str("minute-token")))).
Body(map[string]interface{}{"summary": "<summary markdown>"})
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
minuteToken := runtime.Str("minute-token")
summary := strings.TrimSpace(runtime.Str("summary"))
path := fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/summary", validate.EncodePathSegment(minuteToken))
body := map[string]interface{}{
"summary": summary,
}
if _, err := runtime.CallAPI(http.MethodPut, path, nil, body); err != nil {
return err
}
runtime.OutFormat(map[string]interface{}{
"minute_token": minuteToken,
"updated": true,
}, nil, nil)
return nil
},
}

View File

@@ -0,0 +1,221 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package minutes
import (
"encoding/json"
"strings"
"testing"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
)
func todoStub(token string) *httpmock.Stub {
return &httpmock.Stub{
Method: "PUT",
URL: "/open-apis/minutes/v1/minutes/" + token + "/todo",
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{}},
}
}
func firstTodoItem(t *testing.T, raw []byte) map[string]any {
t.Helper()
if len(raw) == 0 {
t.Fatal("request body was not captured")
}
var body map[string]any
if err := json.Unmarshal(raw, &body); err != nil {
t.Fatalf("failed to parse captured body: %v", err)
}
items, _ := body["todo_items"].([]any)
if len(items) != 1 {
t.Fatalf("todo_items: want 1 item, got %d (%v)", len(items), body["todo_items"])
}
item, _ := items[0].(map[string]any)
return item
}
func TestMinutesSummary_DryRun(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
err := mountAndRun(t, MinutesSummary, []string{
"+summary",
"--minute-token", "obcn123456789",
"--summary", "**Weekly sync**\n- follow up",
"--dry-run",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "PUT") || !strings.Contains(out, "/open-apis/minutes/v1/minutes/obcn123456789/summary") {
t.Fatalf("dry-run output = %q", out)
}
}
func TestMinutesTodo_DryRun(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
err := mountAndRun(t, MinutesTodo, []string{
"+todo",
"--minute-token", "obcn123456789",
"--todo", "- finish deck",
"--is-done",
"--dry-run",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "PUT") || !strings.Contains(out, "/open-apis/minutes/v1/minutes/obcn123456789/todo") {
t.Fatalf("dry-run output = %q", out)
}
if !strings.Contains(out, "todo_items") {
t.Fatalf("dry-run output should contain todo_items, got %q", out)
}
}
func TestMinutesTodo_RequiresIsDone(t *testing.T) {
f, _, stderr, _ := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
err := mountAndRun(t, MinutesTodo, []string{
"+todo",
"--minute-token", "obcn123456789",
"--todo", "finish deck",
"--as", "user",
}, f, stderr)
if err == nil {
t.Fatal("expected validation error for missing --is-done")
}
if !strings.Contains(err.Error(), "is-done") && !strings.Contains(err.Error(), "todo-list") {
t.Fatalf("error = %q, want message mentioning is-done or todo-list", err.Error())
}
}
func TestMinutesTodo_Add_RequestBody(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
stub := todoStub("obcn123456789")
reg.Register(stub)
err := mountAndRun(t, MinutesTodo, []string{
"+todo", "--minute-token", "obcn123456789",
"--todo", "finish deck", "--is-done=false", "--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
item := firstTodoItem(t, stub.CapturedBody)
if item["content"] != "finish deck" {
t.Errorf("content = %v, want finish deck", item["content"])
}
if item["is_done"] != false {
t.Errorf("is_done = %v, want false", item["is_done"])
}
if _, ok := item["todo_id"]; ok {
t.Errorf("add should not send todo_id, got %v", item["todo_id"])
}
if !strings.Contains(stdout.String(), "add") {
t.Errorf("output should report add operation, got %q", stdout.String())
}
}
func TestMinutesTodo_Update_RequestBody(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
stub := todoStub("obcn123456789")
reg.Register(stub)
err := mountAndRun(t, MinutesTodo, []string{
"+todo", "--minute-token", "obcn123456789",
"--todo-id", "99", "--todo", "updated deck", "--is-done", "--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
item := firstTodoItem(t, stub.CapturedBody)
if item["todo_id"] != "99" {
t.Errorf("todo_id = %v, want 99", item["todo_id"])
}
if item["content"] != "updated deck" {
t.Errorf("content = %v, want updated deck", item["content"])
}
if item["is_done"] != true {
t.Errorf("is_done = %v, want true", item["is_done"])
}
}
func TestMinutesTodo_Delete_RequestBody(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
stub := todoStub("obcn123456789")
reg.Register(stub)
err := mountAndRun(t, MinutesTodo, []string{
"+todo", "--minute-token", "obcn123456789",
"--todo-id", "88", "--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
item := firstTodoItem(t, stub.CapturedBody)
if item["todo_id"] != "88" {
t.Errorf("todo_id = %v, want 88", item["todo_id"])
}
if _, ok := item["content"]; ok {
t.Errorf("delete should not send content, got %v", item["content"])
}
if _, ok := item["is_done"]; ok {
t.Errorf("delete should not send is_done, got %v", item["is_done"])
}
// the todo id must never be surfaced to the user in the command output
if strings.Contains(stdout.String(), "88") {
t.Errorf("output must not expose the todo id, got %q", stdout.String())
}
}
func TestMinutesTodo_DeleteRejectsIsDone(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
err := mountAndRun(t, MinutesTodo, []string{
"+todo", "--minute-token", "obcn123456789",
"--todo-id", "88", "--is-done", "--as", "user",
}, f, nil)
if err == nil {
t.Fatal("expected validation error when --is-done is used to delete")
}
}
func TestMinutesTodo_RequiresAnyInput(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, defaultConfig())
warmTokenCache(t)
err := mountAndRun(t, MinutesTodo, []string{
"+todo", "--minute-token", "obcn123456789", "--as", "user",
}, f, nil)
if err == nil {
t.Fatal("expected validation error when neither --todo nor --todo-id is provided")
}
}
func TestMinutesSummaryAndTodo_HelpMetadata(t *testing.T) {
for _, tip := range MinutesSummary.Tips {
if strings.Contains(tip, "raw text") {
return
}
}
t.Fatal("MinutesSummary tips should mention unsupported markdown display behavior")
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package minutes
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
// minuteTodoOp describes the resolved single-todo operation derived from flags.
type minuteTodoOp struct {
operation string // add | update | delete
item map[string]interface{} // the single todo_items entry sent to the API
}
// MinutesTodo adds, updates, or deletes a single todo item on a minute.
var MinutesTodo = common.Shortcut{
Service: "minutes",
Command: "+todo",
Description: "Add, update, or delete a single todo item on a minute",
Risk: "write",
Scopes: []string{"minutes:minutes:update"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "minute-token", Desc: "minute token (required)", Required: true},
{Name: "todo", Desc: "todo plain-text content; required (with --is-done) to add or update", Input: []string{common.File, common.Stdin}},
{Name: "is-done", Type: "bool", Desc: "completion flag; required together with --todo"},
{Name: "todo-id", Desc: "id of an existing todo; provide to update (with --todo) or delete (without --todo); omit to add"},
},
Tips: []string{
"Add a todo: `--todo \"...\" --is-done=false`.",
"Update a todo: `--todo-id <id> --todo \"...\" --is-done`.",
"Delete a todo: `--todo-id <id>` (omit --todo).",
"`content` is plain text only; markdown formatting is not supported.",
"Use `lark-cli vc +notes --minute-tokens <token>` to read current todos before writing.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
minuteToken := runtime.Str("minute-token")
if minuteToken == "" {
return output.ErrValidation("--minute-token is required")
}
if err := validate.ResourceName(minuteToken, "--minute-token"); err != nil {
return output.ErrValidation("%s", err)
}
if _, err := resolveMinuteTodoOp(runtime); err != nil {
return err
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
PUT(fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/todo", validate.EncodePathSegment(runtime.Str("minute-token")))).
Body(map[string]interface{}{
"todo_items": "<todo_items array>",
})
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
minuteToken := runtime.Str("minute-token")
op, err := resolveMinuteTodoOp(runtime)
if err != nil {
return err
}
path := fmt.Sprintf("/open-apis/minutes/v1/minutes/%s/todo", validate.EncodePathSegment(minuteToken))
body := map[string]interface{}{
"todo_items": []interface{}{op.item},
}
if _, err := runtime.CallAPI(http.MethodPut, path, nil, body); err != nil {
return err
}
// Intentionally omit the todo id from the output: users never see it.
runtime.OutFormat(map[string]interface{}{
"minute_token": minuteToken,
"operation": op.operation,
"updated": true,
}, nil, nil)
return nil
},
}
func resolveMinuteTodoOp(runtime *common.RuntimeContext) (*minuteTodoOp, error) {
todo := strings.TrimSpace(runtime.Str("todo"))
todoID := strings.TrimSpace(runtime.Str("todo-id"))
hasTodo := todo != ""
hasTodoID := todoID != ""
hasIsDone := runtime.Changed("is-done")
item := map[string]interface{}{}
if hasTodoID {
item["todo_id"] = todoID
}
switch {
case hasTodo:
// add or update: content and is_done must appear together
if !hasIsDone {
return nil, output.ErrValidation("--todo requires --is-done")
}
item["content"] = todo
item["is_done"] = runtime.Bool("is-done")
if hasTodoID {
return &minuteTodoOp{operation: "update", item: item}, nil
}
return &minuteTodoOp{operation: "add", item: item}, nil
case hasTodoID:
// delete: only the id is needed; content/is_done are not allowed
if hasIsDone {
return nil, output.ErrValidation("--is-done cannot be used to delete a todo (omit it, and provide only --todo-id)")
}
return &minuteTodoOp{operation: "delete", item: item}, nil
default:
return nil, output.ErrValidation("provide --todo (with --is-done) to add/update, or --todo-id alone to delete")
}
}

View File

@@ -11,5 +11,7 @@ func Shortcuts() []common.Shortcut {
MinutesSearch,
MinutesDownload,
MinutesUpload,
MinutesSummary,
MinutesTodo,
}
}

View File

@@ -99,6 +99,9 @@ Minutes (妙记) ← minute_token 标识
> - 用户说"通过文件生成妙记 / 把音视频转妙记" → 先上传获取 `file_token`,然后使用 `minutes +upload`
> - 用户说"把音视频文件转成纪要 / 逐字稿 / 文字稿 / 撰写文字 / 总结 / 待办 / 章节" → 先上传获取 `file_token`,调用 `minutes +upload` 生成 `minute_url`,再提取 `minute_token` 走 `vc +notes --minute-tokens`
> - 用户说"替换 / 更新这个妙记的总结" → [`minutes +summary`](references/lark-minutes-summary.md)
> - 用户说"新增 / 更新 / 删除这个妙记的某条待办" → [`minutes +todo`](references/lark-minutes-todo.md)
## Shortcuts推荐优先使用
Shortcut 是对常用操作的高级封装(`lark-cli minutes +<verb> [flags]`)。有 Shortcut 的操作优先使用。
@@ -108,10 +111,14 @@ Shortcut 是对常用操作的高级封装(`lark-cli minutes +<verb> [flags]`
| [`+search`](references/lark-minutes-search.md) | Search minutes by keyword, owners, participants, and time range |
| [`+download`](references/lark-minutes-download.md) | Download audio/video media file of a minute |
| [`+upload`](references/lark-minutes-upload.md) | Upload a media file token to generate a minute |
| [`+summary`](references/lark-minutes-summary.md) | Replace the AI summary of a minute |
| [`+todo`](references/lark-minutes-todo.md) | Add, update, or delete a single todo item |
- 使用 `+search` 命令时,必须阅读 [references/lark-minutes-search.md](references/lark-minutes-search.md),了解搜索参数和返回值结构。
- 使用 `+download` 命令时,必须阅读 [references/lark-minutes-download.md](references/lark-minutes-download.md),了解下载参数和返回值结构。
- 使用 `+upload` 命令时,必须阅读 [references/lark-minutes-upload.md](references/lark-minutes-upload.md),了解生成参数和返回值结构。
- 使用 `+summary` 时,必须阅读 [references/lark-minutes-summary.md](references/lark-minutes-summary.md);妙记端通常可良好展示:一级/二级/三级标题(`#` / `##` / `###`)、加粗(`**text**`)、无序列表(`-` / `*`)、有序列表(`1.`),以及纯文本与换行;不支持的 Markdown 语法会按原始文本展示接口不会因此拒绝Agent 写入时应优先使用可展示子集。
- 使用 `+todo` 时,必须阅读 [references/lark-minutes-todo.md](references/lark-minutes-todo.md);对单条待办做增删改:新增传 `--todo`(纯文本)+`--is-done`,更新再加 `--todo-id`,删除只传 `--todo-id`。待办 id 通过 `vc +notes` 读取,不向用户展示。
<!-- AUTO-GENERATED-START — gen-skills.py 管理,勿手动编辑 -->

View File

@@ -0,0 +1,122 @@
# minutes +summary
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
替换妙记的 AI 总结内容。写操作,会覆盖当前总结。
本 skill 对应 shortcut`lark-cli minutes +summary`(调用 `PUT /open-apis/minutes/v1/minutes/{minute_token}/summary`)。
## 典型触发表达
- "把这条妙记的总结改成……"
- "更新 / 替换妙记的 AI 总结"
- "修正总结内容后写回妙记"
## 命令
```bash
# 直接传入总结内容Markdown 子集)
lark-cli minutes +summary --minute-token obcnxxxxxxxxxxxxxxxxxxxx --summary "**会议结论**\n- 方案 A 通过\n- 下周跟进排期"
# 从文件读取总结内容
lark-cli minutes +summary --minute-token obcnxxxxxxxxxxxxxxxxxxxx --summary @summary.md
# 从 stdin 读取
echo "**结论**" | lark-cli minutes +summary --minute-token obcnxxxxxxxxxxxxxxxxxxxx --summary @-
# 预览 API 调用
lark-cli minutes +summary --minute-token obcnxxxxxxxxxxxxxxxxxxxx --summary @summary.md --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--minute-token <token>` | 是 | 妙记 Token |
| `--summary <text>` | 是 | 替换后的总结内容,支持 `@file` / `@-`stdin |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
## 核心约束
### 1. 先读后写
替换前建议先用 `lark-cli vc +notes --minute-tokens <token>` 读取当前总结,确认 `minute_token` 与待替换内容无误。
### 2. Markdown 展示说明
接口接受任意总结文本,**不会因 Markdown 格式校验失败而拒绝请求**。妙记客户端通常只能良好渲染以下 Markdown 子集;不支持的语法(如链接、代码块、四级标题等)会**按原始文本展示**(保留 Markdown 标记字符不会渲染成对应样式。Agent 写入时应优先使用可展示语法,避免用户在妙记里看到字面量的 `[链接](url)`、`` `code` `` 等:
| 支持 | 写法 | 示例 |
|------|------|------|
| 纯文本 | 普通段落 | `本次会议讨论了 Q2 预算` |
| 换行 | `\n` 或空行 | 分段落书写 |
| 一级标题 | `# ` + 标题文字 | `# 会议结论` |
| 二级标题 | `## ` + 标题文字 | `## 行动项` |
| 三级标题 | `### ` + 标题文字 | `### 跟进事项` |
| 加粗 | `**文字**` | `**重点结论**` |
| 无序列表 | `- ` 或 `* ` | `- 跟进预算审批` |
| 有序列表 | `1. ` | `1. 确认需求` |
> 标题语法建议:`#` 后保留空格,并优先使用 13 级(`#` / `##` / `###`)。四级及以上(`####`)无法渲染,会以原始文本形式展示。
**不建议使用**(会按原始文本展示):链接、图片、代码块、表格、引用块、斜体、删除线、四级及以上标题等。
合法示例:
```markdown
# 会议结论
## 核心讨论
**方案 A 通过**,下周启动排期。
### 待跟进
- 预算审批
- 排期确认
1. 张三负责预算
2. 李四负责排期
```
### 3. 所需权限
| 身份 | 所需权限 |
|------|---------|
| user | `minutes:minutes:update` |
## 输出结果
```json
{
"minute_token": "obcnxxxxxxxxxxxxxxxxxxxx",
"updated": true
}
```
| 字段 | 说明 |
|------|------|
| `minute_token` | 妙记 Token |
| `updated` | 是否已成功更新 |
## 如何获取 minute_token
| 来源 | 获取方式 |
|------|---------|
| 妙记 URL | 从 URL 末尾提取,如 `https://sample.feishu.cn/minutes/obcnxxxxxxxxxxxxxxxxxxxx` |
| 妙记搜索 | `lark-cli minutes +search --query "关键词"` |
| 会议产物查询 | `lark-cli vc +notes --minute-tokens <token>` |
## 常见错误与排查
| 错误现象 | 错误码 | 根本原因 | 解决方案 |
|---------|--------|---------|---------|
| 总结展示为原始 Markdown 文本 | — | 总结含链接、四级标题等妙记端无法渲染的语法 | 改用标题(####)、加粗、列表等可展示格式;接口不会因此报错 |
| 参数无效 | — | `minute_token` 缺失或格式错误 | 检查 token 是否完整 |
| 权限不足 | — | 缺少 `minutes:minutes:update` | 运行 `auth login --scope "minutes:minutes:update"` |
## 参考
- [lark-minutes](../SKILL.md) — 妙记全部命令
- [minutes +todo](lark-minutes-todo.md) — 替换待办项
- [lark-vc-notes](../../lark-vc/references/lark-vc-notes.md) — 读取总结、待办等 AI 产物
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数

View File

@@ -0,0 +1,122 @@
# minutes +todo
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
对妙记中的**单条**待办做新增 / 更新 / 删除。写操作。
本 skill 对应 shortcut`lark-cli minutes +todo`(调用 `PUT /open-apis/minutes/v1/minutes/{minute_token}/todo`)。
## 典型触发表达
- "给这条妙记加一条待办"
- "把某条待办改成……"
- "标记某条待办为已完成 / 取消完成"
- "删除某条待办"
## 命令
```bash
# 新增一条待办(不带 idcontent 与 is_done 成对)
lark-cli minutes +todo --minute-token obcnxxxxxxxxxxxxxxxxxxxx --todo "跟进预算审批" --is-done=false
# 更新已有待办(带 id覆盖内容与完成状态
lark-cli minutes +todo --minute-token obcnxxxxxxxxxxxxxxxxxxxx --todo-id 1234567890 --todo "整理会议纪要" --is-done
# 删除已有待办(只带 id不带 --todo
lark-cli minutes +todo --minute-token obcnxxxxxxxxxxxxxxxxxxxx --todo-id 1234567890
# 预览 API 调用
lark-cli minutes +todo --minute-token obcnxxxxxxxxxxxxxxxxxxxx --todo "新待办" --is-done --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--minute-token <token>` | 是 | 妙记 Token |
| `--todo <text>` | 视操作 | 待办纯文本;新增 / 更新时必填,且必须与 `--is-done` 成对出现;删除时不传 |
| `--is-done` | 视操作 | 完成状态布尔值;传 `--is-done` 表示 `true`,传 `--is-done=false` 表示 `false`;仅在带 `--todo` 时使用 |
| `--todo-id <id>` | 视操作 | 已有待办的 id更新 / 删除时必填,新增时不传 |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
## 三种操作的判定
| `--todo-id` | `--todo` | 行为 |
|-------------|----------|------|
| 不传 | 有内容 | **新增**一条待办(需 `--is-done` |
| 传 | 有内容 | **更新** id 对应的待办(需 `--is-done` |
| 传 | 不传 | **删除** id 对应的待办 |
## 核心约束
### 1. 先读后写,待办 id 如何获取
更新 / 删除前先用 `lark-cli vc +notes --minute-tokens <token>` 读取当前待办。返回的每条待办带 `todo_id` 字段,用作 `--todo-id` 的取值。
> 待办 id 仅用于程序内部定位某条待办,不必展示给用户;本命令的输出也不会回显 id。
读取与写入均使用 `is_done` 布尔字段。已删除的待办不会出现在读取结果中。
### 2. 待办内容为纯文本
`content` **不是 Markdown**,请直接传入待办描述文字。
- 不要写 `# 标题``**加粗**``- 列表` 等 Markdown 语法
- 如需多行内容,可直接使用换行;但不会被渲染为 Markdown 格式
### 3. 请求体字段
请求体 `todo_items` 始终只包含**一条**待办:
| CLI | JSON 字段 | 说明 |
|-----|-----------|------|
| `--todo` | `content` | 纯文本待办描述(新增 / 更新必填;删除不传) |
| `--is-done` | `is_done` | 是否已完成(新增 / 更新必填;删除不传) |
| `--todo-id` | `todo_id` | 已有待办 id更新 / 删除必填;新增不传) |
### 4. 所需权限
| 身份 | 所需权限 |
|------|---------|
| user | `minutes:minutes:update` |
## 输出结果
```json
{
"minute_token": "obcnxxxxxxxxxxxxxxxxxxxx",
"operation": "add",
"updated": true
}
```
| 字段 | 说明 |
|------|------|
| `minute_token` | 妙记 Token |
| `operation` | 本次操作类型:`add` / `update` / `delete` |
| `updated` | 是否已成功提交 |
## 如何获取 minute_token
| 来源 | 获取方式 |
|------|---------|
| 妙记 URL | 从 URL 末尾提取,如 `https://sample.feishu.cn/minutes/obcnxxxxxxxxxxxxxxxxxxxx` |
| 妙记搜索 | `lark-cli minutes +search --query "关键词"` |
| 会议产物查询 | `lark-cli vc +notes --minute-tokens <token>` |
## 常见错误与排查
| 错误现象 | 根本原因 | 解决方案 |
|---------|---------|---------|
| 参数无效 | `minute_token` 缺失 | 检查 token |
| 未指定操作 | 既没传 `--todo` 也没传 `--todo-id` | 新增 / 更新需 `--todo`,删除需 `--todo-id` |
| 缺少 `is_done` | 传了 `--todo` 未传 `--is-done` | `--todo``--is-done` 必须成对出现 |
| 删除时多传了 `--is-done` | 删除只需 `--todo-id` | 删除时不要传 `--todo` / `--is-done` |
| 权限不足 | 缺少 `minutes:minutes:update` | 运行 `auth login --scope "minutes:minutes:update"` |
## 参考
- [lark-minutes](../SKILL.md) — 妙记全部命令
- [minutes +summary](lark-minutes-summary.md) — 替换 AI 总结(不支持的 Markdown 会按原始文本展示,详见该文档)
- [lark-vc-notes](../../lark-vc/references/lark-vc-notes.md) — 读取总结、待办等 AI 产物
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数

View File

@@ -91,7 +91,7 @@ lark-cli vc +notes --meeting-ids 69xxxxxxxxxxxxx28 --dry-run
| 字段 | 说明 |
|------|------|
| `artifacts.summary` | AI 总结JSON 内联) |
| `artifacts.todos` | 待办事项JSON 内联) |
| `artifacts.todos` | 待办事项JSON 内联);每条含 `content``is_done``todo_id``todo_id` 仅供 `minutes +todo` 更新/删除单条待办时使用,不必展示给用户 |
| `artifacts.chapters` | 章节纪要JSON 内联) |
| `artifacts.transcript_file` | 逐字稿本地文件路径。默认落到 `./minutes/{minute_token}/transcript.txt`(与 `minutes +download` 聚合);显式 `--output-dir` 时走旧布局 `./{output-dir}/artifact-{title}-{token}/transcript.txt` |