Compare commits

...

1 Commits

Author SHA1 Message Date
zhangjun.1
1a5ec8659f feat: 0727 fix rich text 2026-07-28 17:10:08 +08:00
13 changed files with 82 additions and 98 deletions

View File

@@ -250,7 +250,7 @@ var CalendarAgenda = common.Shortcut{
}
}
backfillDescriptionRich(e)
collapseDescription(e)
filtered = append(filtered, e)
}

View File

@@ -32,8 +32,8 @@ func buildEventData(runtime *common.RuntimeContext, startTs, endTs string) map[s
if rrule := runtime.Str("rrule"); rrule != "" {
eventData["recurrence"] = rrule
}
if descriptionRich := descriptionRichToSend(runtime); descriptionRich != "" {
eventData["description_rich"] = descriptionRich
if description := descriptionToSend(runtime); description != "" {
eventData["description_rich"] = description
}
return eventData
}
@@ -120,8 +120,7 @@ var CalendarCreate = common.Shortcut{
{Name: "summary", Desc: "event title"},
{Name: "start", Desc: "start time (ISO 8601)", Required: true},
{Name: "end", Desc: "end time (ISO 8601)", Required: true},
{Name: "description", Desc: "deprecated: plain-text description; use --description-rich (Markdown) instead", Hidden: true},
{Name: "description-rich", Desc: "event description as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (`![name](url)`; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `![p](url)<br>**bold**`).", Input: []string{common.File, common.Stdin}},
{Name: "description", Desc: "event description as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (`![name](url)`; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `![p](url)<br>**bold**`).", Input: []string{common.File, common.Stdin}},
{Name: "attendee-ids", Desc: "attendee IDs, comma-separated (supports user ou_, chat oc_, room omm_)"},
{Name: "calendar-id", Desc: "calendar ID (default: primary)"},
{Name: "rrule", Desc: "recurrence rule (rfc5545)"},
@@ -234,7 +233,7 @@ var CalendarCreate = common.Shortcut{
if err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end")
}
if err := resolveDescriptionRichImages(runtime, calendarId); err != nil {
if err := resolveDescriptionImages(runtime, calendarId); err != nil {
return err
}

View File

@@ -170,7 +170,7 @@ func buildCalendarEventOutput(event *calendarEvent) (map[string]interface{}, err
if status, _ := out["status"].(string); status != "cancelled" {
delete(out, "status")
}
backfillDescriptionRich(out)
collapseDescription(out)
return out, nil
}

View File

@@ -988,8 +988,9 @@ func TestUpdate_PatchEventOnly(t *testing.T) {
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
t.Fatalf("unmarshal captured patch body: %v", err)
}
// The deprecated, hidden --description folds into description_rich; the CLI
// never sends the plain description field (mutually exclusive downstream).
// --description is the unified field, treated as rich text and sent as
// description_rich; the CLI never sends the plain description field
// (mutually exclusive downstream).
if body["summary"] != "Updated Meeting" || body["description_rich"] != "Updated description" {
t.Fatalf("unexpected patch body: %#v", body)
}
@@ -1411,18 +1412,17 @@ func TestAgenda_UnifiesDescriptionRich(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
// Read keeps both fields: description (plain) and description_rich (rich).
if !strings.Contains(out, "\"description\": \"[测试]\\n友情提醒\"") {
t.Errorf("expected plain description retained, got: %s", out)
}
if !strings.Contains(out, "\"description_rich\": \"友情提醒\"") {
t.Errorf("expected rich description surfaced, got: %s", out)
// Read exposes a single unified description field: it carries the rich
// (Markdown) value when present, and the plain text otherwise. The internal
// description_rich key is never surfaced.
if !strings.Contains(out, "\"description\": \"友情提醒\"") {
t.Errorf("expected rich value surfaced under description, got: %s", out)
}
if !strings.Contains(out, "\"description\": \"just text\"") {
t.Errorf("expected plain description retained for plain-only event, got: %s", out)
t.Errorf("expected plain description surfaced for plain-only event, got: %s", out)
}
if !strings.Contains(out, "\"description_rich\": \"just text\"") {
t.Errorf("expected description_rich backfilled from plain, got: %s", out)
if strings.Contains(out, "description_rich") {
t.Errorf("description_rich must not appear in output, got: %s", out)
}
}
@@ -3438,7 +3438,8 @@ func TestGet_Success_FlattensAndConvertsTimes(t *testing.T) {
}
func TestGet_UnifiesDescriptionRich(t *testing.T) {
// Read keeps both description (plain) and description_rich (rich).
// Read exposes a single unified description field carrying the rich value
// when present, and the plain text otherwise; description_rich is dropped.
t.Run("rich present", func(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
@@ -3462,17 +3463,16 @@ func TestGet_UnifiesDescriptionRich(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "\"description\": \"[表格]\"") {
t.Errorf("expected plain description retained, got: %s", out)
if !strings.Contains(out, "| a | b |") {
t.Errorf("expected rich value surfaced under description, got: %s", out)
}
if !strings.Contains(out, "\"description_rich\":") {
t.Errorf("expected description_rich in output, got: %s", out)
if strings.Contains(out, "description_rich") {
t.Errorf("description_rich must not appear in output, got: %s", out)
}
})
// When only a plain description exists, description_rich is backfilled from it
// and the plain description is still returned.
t.Run("only plain backfills rich", func(t *testing.T) {
// When only a plain description exists, it is surfaced under description.
t.Run("only plain surfaces under description", func(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
@@ -3495,10 +3495,10 @@ func TestGet_UnifiesDescriptionRich(t *testing.T) {
}
out := stdout.String()
if !strings.Contains(out, "\"description\": \"just text\"") {
t.Errorf("expected plain description retained, got: %s", out)
t.Errorf("expected plain description surfaced, got: %s", out)
}
if !strings.Contains(out, "\"description_rich\": \"just text\"") {
t.Errorf("expected description_rich backfilled from plain, got: %s", out)
if strings.Contains(out, "description_rich") {
t.Errorf("description_rich must not appear in output, got: %s", out)
}
})
}

View File

@@ -29,8 +29,7 @@ var CalendarUpdate = common.Shortcut{
{Name: "event-id", Desc: "event ID to update", Required: true},
{Name: "calendar-id", Desc: "calendar ID (default: primary)"},
{Name: "summary", Desc: "event title"},
{Name: "description", Desc: "deprecated: plain-text description; use --description-rich (Markdown) instead", Hidden: true},
{Name: "description-rich", Desc: "event description as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (`![name](url)`; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `![p](url)<br>**bold**`).", Input: []string{common.File, common.Stdin}},
{Name: "description", Desc: "event description as Markdown (@file or - for stdin); the unified description field. Supports bold/italic/underline/strikethrough, links, headings (`#`..`###`), blockquotes (`>`), ordered/unordered lists, horizontal rules (`---`), GFM tables, and images (`![name](url)`; a remote URL is used as-is, and a local image path relative to and inside the current working directory is auto-uploaded to Lark drive and rendered inline — absolute/out-of-cwd paths are rejected). A Lark doc URL (bare or as a Markdown link) is auto-resolved to an inline doc-mention chip showing its title. Inside a GFM table cell, stack multiple lines with `<br>`; each line may itself be an ordered/unordered list item, image or styled text (e.g. `1. a<br>2. b`, `- x<br>- y`, `![p](url)<br>**bold**`). Passing an empty string clears the description.", Input: []string{common.File, common.Stdin}},
{Name: "start", Desc: "new start time (ISO 8601); requires --end"},
{Name: "end", Desc: "new end time (ISO 8601); requires --start"},
{Name: "rrule", Desc: "recurrence rule (rfc5545)"},
@@ -72,7 +71,7 @@ func validateCalendarUpdate(runtime *common.RuntimeContext) error {
return err
}
if !hasCalendarUpdateOperation(runtime) {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "nothing to update: specify at least one of --summary, --description, --description-rich, --start/--end, --rrule, --add-attendee-ids, or --remove-attendee-ids")
return errs.NewValidationError(errs.SubtypeInvalidArgument, "nothing to update: specify at least one of --summary, --description, --start/--end, --rrule, --add-attendee-ids, or --remove-attendee-ids")
}
return nil
}
@@ -114,10 +113,7 @@ func buildCalendarUpdateEventData(runtime *common.RuntimeContext) (map[string]in
body["summary"] = runtime.Str("summary")
hasFields = true
}
if runtime.Cmd.Flags().Changed("description-rich") {
body["description_rich"] = runtime.Str("description-rich")
hasFields = true
} else if runtime.Cmd.Flags().Changed("description") {
if runtime.Cmd.Flags().Changed("description") {
body["description_rich"] = runtime.Str("description")
hasFields = true
}
@@ -362,11 +358,8 @@ func executeCalendarUpdate(ctx context.Context, runtime *common.RuntimeContext)
return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --event-id").WithParam("--event-id")
}
// Upload any local images referenced in --description-rich and rewrite them
// to drive URLs before the description is sent (the service cannot read
// local files).
if runtime.Cmd.Flags().Changed("description-rich") {
if err := resolveDescriptionRichImages(runtime, calendarID); err != nil {
if runtime.Cmd.Flags().Changed("description") {
if err := resolveDescriptionImages(runtime, calendarID); err != nil {
return err
}
}
@@ -443,19 +436,10 @@ func calendarUpdateResult(eventID string, event map[string]interface{}, addedCou
if summary, _ := event["summary"].(string); summary != "" {
result["summary"] = summary
}
// Surface both description fields on read: description holds plain text,
// description_rich holds the rich (Markdown) version, backfilled from plain
// when the service returned no rich value.
description, _ := event["description"].(string)
if description != "" {
result["description"] = description
}
descriptionRich, _ := event["description_rich"].(string)
if descriptionRich == "" {
descriptionRich = description
}
if descriptionRich != "" {
result["description_rich"] = descriptionRich
if rich, _ := event["description_rich"].(string); rich != "" {
result["description"] = rich
} else if plain, _ := event["description"].(string); plain != "" {
result["description"] = plain
}
if start := formatCalendarEventTime(event["start_time"]); start != "" {
result["start"] = start

View File

@@ -27,8 +27,8 @@ const calendarMediaParentType = "calendar"
var markdownImageRe = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]*)\)`)
func resolveDescriptionRichImages(runtime *common.RuntimeContext, calendarID string) error {
md := runtime.Str("description-rich")
func resolveDescriptionImages(runtime *common.RuntimeContext, calendarID string) error {
md := runtime.Str("description")
if md == "" || !strings.Contains(md, "![") {
return nil
}
@@ -37,8 +37,8 @@ func resolveDescriptionRichImages(runtime *common.RuntimeContext, calendarID str
return err
}
if changed {
if err := runtime.Cmd.Flags().Set("description-rich", rewritten); err != nil {
return errs.NewInternalError(errs.SubtypeUnknown, "failed to update --description-rich after image upload: %v", err).WithCause(err)
if err := runtime.Cmd.Flags().Set("description", rewritten); err != nil {
return errs.NewInternalError(errs.SubtypeUnknown, "failed to update --description after image upload: %v", err).WithCause(err)
}
}
return nil
@@ -85,8 +85,8 @@ func resolveLocalImage(runtime *common.RuntimeContext, calendarID, src, alt stri
safePath, err := validate.SafeInputPath(localPath)
if err != nil {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument,
"--description-rich image %q could not be read: %v", src, err).
WithParam("--description-rich").
"--description image %q could not be read: %v", src, err).
WithParam("--description").
WithHint("reference local images by a path inside the current working directory (e.g. ./images/pic.png; cd there first), or use an already-uploaded Lark image URL").
WithCause(err)
}
@@ -157,7 +157,7 @@ func localImagePath(src string) string {
}
func buildCalendarImagePreviewURL(brand core.LarkBrand, fileToken string, width, height int, size int64) string {
host := "internal-api-drive-stream.larkoffice.com"
host := "internal-api-drive-stream.feishu.cn"
if brand == core.BrandLark {
host = "internal-api-drive-stream.larksuite.com"
}

View File

@@ -158,7 +158,7 @@ func TestCreate_UploadsLocalDescriptionImage(t *testing.T) {
"--start", "2025-03-21T00:00:00+08:00",
"--end", "2025-03-21T01:00:00+08:00",
"--calendar-id", "cal_test123",
"--description-rich", "![pic](./pic.png)",
"--description", "![pic](./pic.png)",
"--as", "bot",
}, f, stdout)
if runErr != nil {
@@ -233,7 +233,7 @@ func TestCreate_LocalImageCarriesDimensions(t *testing.T) {
"--start", "2025-03-21T00:00:00+08:00",
"--end", "2025-03-21T01:00:00+08:00",
"--calendar-id", "cal_test123",
"--description-rich", "![pic](./pic.png)",
"--description", "![pic](./pic.png)",
"--as", "bot",
}, f, stdout)
if runErr != nil {
@@ -253,7 +253,7 @@ func TestCreate_LocalImageCarriesDimensions(t *testing.T) {
}
// TestCreate_LocalImageAbsolutePathRejected verifies an out-of-cwd absolute path
// yields a typed --description-rich validation error before any API call.
// yields a typed --description validation error before any API call.
func TestCreate_LocalImageAbsolutePathRejected(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
@@ -263,7 +263,7 @@ func TestCreate_LocalImageAbsolutePathRejected(t *testing.T) {
"--start", "2025-03-21T00:00:00+08:00",
"--end", "2025-03-21T01:00:00+08:00",
"--calendar-id", "cal_test123",
"--description-rich", "![p](/etc/hosts)",
"--description", "![p](/etc/hosts)",
"--as", "bot",
}, f, stdout)
if runErr == nil {
@@ -273,7 +273,7 @@ func TestCreate_LocalImageAbsolutePathRejected(t *testing.T) {
if !errors.As(runErr, &ve) {
t.Fatalf("expected *errs.ValidationError, got %T: %v", runErr, runErr)
}
if ve.Param != "--description-rich" {
t.Errorf("param = %q, want --description-rich", ve.Param)
if ve.Param != "--description" {
t.Errorf("param = %q, want --description", ve.Param)
}
}

View File

@@ -30,20 +30,23 @@ func resolveStartEnd(runtime *common.RuntimeContext) (string, string) {
return startInput, endInput
}
func backfillDescriptionRich(event map[string]interface{}) {
func collapseDescription(event map[string]interface{}) {
if event == nil {
return
}
if descRich, _ := event["description_rich"].(string); descRich == "" {
if desc, _ := event["description"].(string); desc != "" {
event["description_rich"] = desc
}
rich, _ := event["description_rich"].(string)
plain, _ := event["description"].(string)
delete(event, "description_rich")
switch {
case rich != "":
event["description"] = rich
case plain != "":
event["description"] = plain
default:
delete(event, "description")
}
}
func descriptionRichToSend(runtime *common.RuntimeContext) string {
if v := runtime.Str("description-rich"); v != "" {
return v
}
func descriptionToSend(runtime *common.RuntimeContext) string {
return runtime.Str("description")
}

View File

@@ -48,7 +48,7 @@ lark-cli calendar +agenda --as user
lark-cli calendar +get --calendar-id <calendar_id> --event-id <event_id>
```
读取日程时同时返回 `description`(纯文本)和 `description_rich`**Markdown** 富文本)两个字段:`description` 存纯文本,`description_rich`富文本仅有纯文本描述时`description_rich` 会用该纯文本兜底填充(两字段值相同)。创建/更新日程时只传 `description_rich`Markdown
日程描述统一使用 `description` 一个字段,按 **Markdown** 富文本处理。读取日程时 `description` 返回 Markdown 富文本仅有纯文本描述时返回该纯文本);创建/更新日程时也通过 `--description` 传入 Markdown。
### `+search-event` — 按关键词、时间范围和参会人搜索日程

View File

@@ -32,14 +32,14 @@ lark-cli calendar +create --summary "..." --start "..." --end "..." \
| `--summary <text>` | 否 | 日程标题。注意:标题中不应该出现时间、地点、人物信息 |
| `--start <time>` | 是 | 开始时间ISO 8601`2026-03-12T14:00+08:00` |
| `--end <time>` | 是 | 结束时间ISO 8601 |
| `--description-rich <markdown>` | 否 | 日程描述,统一使用此字段,格式为 **Markdown**。提供会议议程、活动内容、注意事项或链接等。支持加粗、斜体、下划线(`<u>...</u>`)、删除线、链接 `[文本](url)`、标题(`# ``### `,最多三级)、引用(`> `)、有序/无序列表、GFM 表格(`\| 列1 \| 列2 \|` + 分隔行 `\| --- \| --- \|`)、以及图片 `![图片名](图片URL)`(标准 Markdown 图片语法:远程 URL 原样使用;**本地图片路径**(相对路径、且位于当前工作目录内)会自动上传到云盘并在端上内联渲染——绝对路径或工作目录之外的路径会报错;端上已有图片读回为 Markdown 图片)。飞书文档 URL直接粘贴裸链接或写成 `[文本](url)`)会自动解析为内联文档,端上展示文档标题而非裸链接。支持 `@文件路径``-`stdin读取。**禁止**用 `***文本***` 同时表示加粗+斜体(端上会残留 `*`);应嵌套书写,如 `**<u>*~~文本~~*</u>**``*<u>**~~文本~~**</u>*`。|
| `--description <markdown>` | 否 | 日程描述,统一使用此字段,格式为 **Markdown**。提供会议议程、活动内容、注意事项或链接等。支持加粗、斜体、下划线(`<u>...</u>`)、删除线、链接 `[文本](url)`、标题(`# ``### `,最多三级)、引用(`> `)、有序/无序列表、GFM 表格(`\| 列1 \| 列2 \|` + 分隔行 `\| --- \| --- \|`)、以及图片 `![图片名](图片URL)`(标准 Markdown 图片语法:远程 URL 原样使用;**本地图片路径**(相对路径、且位于当前工作目录内)会自动上传到云盘并在端上内联渲染——绝对路径或工作目录之外的路径会报错;端上已有图片读回为 Markdown 图片)。飞书文档 URL直接粘贴裸链接或写成 `[文本](url)`)会自动解析为内联文档,端上展示文档标题而非裸链接。支持 `@文件路径``-`stdin读取。**禁止**用 `***文本***` 同时表示加粗+斜体(端上会残留 `*`);应嵌套书写,如 `**<u>*~~文本~~*</u>**``*<u>**~~文本~~**</u>*`。|
| `--attendee-ids <id_list>` | 否 | 参与人 ID 列表(逗号分隔)。支持用户(`ou_`)、群组(`oc_`)和会议室(`omm_`。AI 提取时请务必保留对应前缀 |
| `--calendar-id <id>` | 否 | 日历 ID省略则使用主日历 |
| `--rrule <rrule>` | 否 | 重复日程的重复性规则规则设置方式参考rfc5545。示例值"FREQ=DAILY;INTERVAL=1;UNTIL=<具体日期>" |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
> 当用户表达'每周 X'、'每周重复'、'连续 N 周'时,必须使用 rrule 创建重复性日程,而非创建多个独立日程
> `--description-rich` 行内同时加粗和斜体时,**禁止**写 `***文本***`(端上会残留 `*`);必须让 `**` 与 `*` 各自成对嵌套,例如 `**<u>*~~文本~~*</u>**` 或 `*<u>**~~文本~~**</u>*`。
> `--description` 行内同时加粗和斜体时,**禁止**写 `***文本***`(端上会残留 `*`);必须让 `**` 与 `*` 各自成对嵌套,例如 `**<u>*~~文本~~*</u>**` 或 `*<u>**~~文本~~**</u>*`。
> 自动设置 `attendee_ability: "can_modify_event"`,参会人可查看彼此并编辑日程。
> 自动设置 `free_busy_status: "busy"`,默认日程忙闲状态为忙碌。
> 自动设置 `reminders: [{"minutes": 5}]`,默认日程开始前 5 分钟提醒。

View File

@@ -28,8 +28,8 @@
| 步骤 | 命令 | 说明 |
|------|------|------|
| 1 | `lark-cli calendar +update --event-id <原重复日程ID> --summary ... --description-rich ...` | 更新原重复性日程的标题/描述等 |
| 2 | `lark-cli calendar +update --event-id <例外ID> --summary ... --description-rich ...` (逐个) | 同步更新例外日程的对应字段 |
| 1 | `lark-cli calendar +update --event-id <原重复日程ID> --summary ... --description ...` | 更新原重复性日程的标题/描述等 |
| 2 | `lark-cli calendar +update --event-id <例外ID> --summary ... --description ...` (逐个) | 同步更新例外日程的对应字段 |
> 理由:例外已脱离原重复性日程独立存在,不会自动继承原日程的更新。

View File

@@ -12,7 +12,7 @@
lark-cli calendar +update \
--event-id "<EVENT_ID>" \
--summary "产品评审" \
--description-rich "评审需求范围、排期与风险" \
--description "评审需求范围、排期与风险" \
--start "2026-03-12T14:00+08:00" \
--end "2026-03-12T15:00+08:00"
@@ -43,7 +43,7 @@ lark-cli calendar +update \
| `--event-id <id>` | 是 | 要更新的日程 ID。重复性日程请根据操作范围选择 ID详见 [重复性日程操作规范](lark-calendar-recurring.md) |
| `--calendar-id <id>` | 否 | 日历 ID省略则使用 `primary` |
| `--summary <text>` | 否 | 新日程标题。仅在显式传入 `--summary` 时更新;若传空字符串,会把标题清空 |
| `--description-rich <markdown>` | 否 | 新日程描述,统一使用此字段,格式为 **Markdown**(加粗、斜体、下划线 `<u>...</u>`、删除线、链接 `[文本](url)`、标题 `# `~`### `(最多三级)、引用 `> `、有序/无序列表、GFM 表格 `\| 列1 \| 列2 \|` + 分隔行 `\| --- \| --- \|`、以及图片 `![图片名](图片URL)`(标准 Markdown 图片语法:远程 URL 原样使用;**本地图片路径**(相对路径、且位于当前工作目录内)会自动上传到云盘并在端上内联渲染——绝对路径或工作目录之外的路径会报错;端上已有图片读回为 Markdown 图片)。飞书文档 URL裸链接或 `[文本](url)`)会自动解析为内联文档,端上展示文档标题。支持 `@文件路径``-`stdin读取。仅在显式传入时更新传空字符串 `""` 会清空描述。**禁止**用 `***文本***` 同时表示加粗+斜体(端上会残留 `*`);应嵌套书写,如 `**<u>*~~文本~~*</u>**``*<u>**~~文本~~**</u>*`。 |
| `--description <markdown>` | 否 | 新日程描述,统一使用此字段,格式为 **Markdown**(加粗、斜体、下划线 `<u>...</u>`、删除线、链接 `[文本](url)`、标题 `# `~`### `(最多三级)、引用 `> `、有序/无序列表、GFM 表格 `\| 列1 \| 列2 \|` + 分隔行 `\| --- \| --- \|`、以及图片 `![图片名](图片URL)`(标准 Markdown 图片语法:远程 URL 原样使用;**本地图片路径**(相对路径、且位于当前工作目录内)会自动上传到云盘并在端上内联渲染——绝对路径或工作目录之外的路径会报错;端上已有图片读回为 Markdown 图片)。飞书文档 URL裸链接或 `[文本](url)`)会自动解析为内联文档,端上展示文档标题。支持 `@文件路径``-`stdin读取。仅在显式传入时更新传空字符串 `""` 会清空描述。**禁止**用 `***文本***` 同时表示加粗+斜体(端上会残留 `*`);应嵌套书写,如 `**<u>*~~文本~~*</u>**``*<u>**~~文本~~**</u>*`。 |
| `--start <time>` | 否 | 新开始时间ISO 8601`2026-03-12T14:00+08:00`)。更新日程时间时必须同时传 `--end` |
| `--end <time>` | 否 | 新结束时间ISO 8601。更新日程时间时必须同时传 `--start` |
| `--rrule <rrule>` | 否 | 新重复规则RFC5545。**不要使用 COUNT如需限制次数推算后转为 UNTIL** |
@@ -52,13 +52,13 @@ lark-cli calendar +update \
| `--notify` | 否 | 是否发送更新通知,默认 `true`。可用 `--notify=false` 静默更新 |
| `--dry-run` | 否 | 预览 API 调用,不执行 |
至少需要提供一个动作:`--summary``--description-rich``--start/--end``--rrule``--add-attendee-ids``--remove-attendee-ids`
至少需要提供一个动作:`--summary``--description``--start/--end``--rrule``--add-attendee-ids``--remove-attendee-ids`
## 使用规则
- `--add-attendee-ids` 是**增量添加**,不是替换最终参与人列表。不要用它表达“只保留这些人”。
-`--summary``--description-rich`CLI 以“是否显式传入该 flag”判断是否更新而不是以“值是否为空”判断如果显式传入空字符串会把对应字段清空。
- 日程描述统一走 `--description-rich`Markdown)。`--description`(纯文本)已废弃并从帮助中隐藏
-`--summary``--description`CLI 以“是否显式传入该 flag”判断是否更新而不是以“值是否为空”判断如果显式传入空字符串会把对应字段清空。
- 日程描述统一走 `--description`Markdown 富文本处理)
- 行内同时加粗和斜体时,**禁止**写 `***文本***`(端上会残留 `*`);必须让 `**``*` 各自成对嵌套,例如 `**<u>*~~文本~~*</u>**``*<u>**~~文本~~**</u>*`
- 只想增删参会人或会议室时,不需要同时传 `--summary``--start``--end` 等日程字段。
- 只想修改标题、描述、时间或重复规则时,不需要同时传 `--add-attendee-ids``--remove-attendee-ids`

View File

@@ -17,10 +17,9 @@ import (
// converts Markdown <-> ClientVars.
const richTextMarkdown = "见 [设计文档](https://bytedance.feishu.cn/docx/abc) 和 **重点**"
// TestCalendar_CreateDescriptionRichDryRun verifies that +create forwards the
// rich-text payload as-is under the description_rich body field, and that even
// when the deprecated --description is also supplied, the CLI sends only
// description_rich (rich wins) and omits the plain description field — the
// TestCalendar_CreateDescriptionRichDryRun verifies that +create treats
// --description as Markdown rich text and forwards it as-is under the
// description_rich body field, omitting the plain description field — the
// service treats the two as mutually exclusive.
func TestCalendar_CreateDescriptionRichDryRun(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
@@ -38,8 +37,7 @@ func TestCalendar_CreateDescriptionRichDryRun(t *testing.T) {
"--summary", "rich dry-run",
"--start", "2026-04-25T10:00:00+08:00",
"--end", "2026-04-25T11:00:00+08:00",
"--description", "plain fallback",
"--description-rich", richTextMarkdown,
"--description", richTextMarkdown,
"--dry-run",
},
DefaultAs: "bot",
@@ -54,10 +52,10 @@ func TestCalendar_CreateDescriptionRichDryRun(t *testing.T) {
require.False(t, clie2e.DryRunGet(out, "api.0.body.description").Exists(), "plain description must not be sent; stdout:\n%s", out)
}
// TestCalendar_CreateDescriptionRichOnlyDryRun verifies that when only
// --description-rich is provided (no --description), +create omits the
// description body field entirely. Sending an empty description would suppress
// the server's plain-preview backfill and break first-load rendering.
// TestCalendar_CreateDescriptionRichOnlyDryRun verifies that +create forwards
// --description under description_rich and omits the plain description body
// field entirely. Sending an empty description would suppress the server's
// plain-preview backfill and break first-load rendering.
func TestCalendar_CreateDescriptionRichOnlyDryRun(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
@@ -74,7 +72,7 @@ func TestCalendar_CreateDescriptionRichOnlyDryRun(t *testing.T) {
"--summary", "rich only dry-run",
"--start", "2026-04-25T10:00:00+08:00",
"--end", "2026-04-25T11:00:00+08:00",
"--description-rich", richTextMarkdown,
"--description", richTextMarkdown,
"--dry-run",
},
DefaultAs: "bot",
@@ -103,7 +101,7 @@ func TestCalendar_UpdateDescriptionRichDryRun(t *testing.T) {
"calendar", "+update",
"--calendar-id", "cal_dry",
"--event-id", "evt_dry",
"--description-rich", richTextMarkdown,
"--description", richTextMarkdown,
"--notify=false",
"--dry-run",
},