Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions shortcuts/calendar/calendar_agenda.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ var CalendarAgenda = common.Shortcut{
}
}

backfillDescriptionRich(e)

filtered = append(filtered, e)
}
}
Expand Down
10 changes: 8 additions & 2 deletions shortcuts/calendar/calendar_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
func buildEventData(runtime *common.RuntimeContext, startTs, endTs string) map[string]interface{} {
eventData := map[string]interface{}{
"summary": runtime.Str("summary"),
"description": runtime.Str("description"),
"start_time": map[string]string{"timestamp": startTs},
"end_time": map[string]string{"timestamp": endTs},
"attendee_ability": "can_modify_event",
Expand All @@ -33,6 +32,9 @@ 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
}
return eventData
}

Expand Down Expand Up @@ -118,7 +120,8 @@ 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: "event description"},
{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: "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)"},
Expand Down Expand Up @@ -231,6 +234,9 @@ var CalendarCreate = common.Shortcut{
if err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end")
}
if err := resolveDescriptionRichImages(runtime, calendarId); err != nil {
return err
}

eventData := buildEventData(runtime, startTs, endTs)

Expand Down
3 changes: 2 additions & 1 deletion shortcuts/calendar/calendar_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type calendarEvent struct {
OrganizerCalendarID string `json:"organizer_calendar_id,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
DescriptionRich string `json:"description_rich,omitempty"`
StartTime *calendarEventTime `json:"start_time,omitempty"`
EndTime *calendarEventTime `json:"end_time,omitempty"`
VChat *calendarEventVChat `json:"vchat,omitempty"`
Expand Down Expand Up @@ -169,7 +170,7 @@ func buildCalendarEventOutput(event *calendarEvent) (map[string]interface{}, err
if status, _ := out["status"].(string); status != "cancelled" {
delete(out, "status")
}

backfillDescriptionRich(out)
return out, nil
}

Expand Down
130 changes: 129 additions & 1 deletion shortcuts/calendar/calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,9 +988,14 @@ func TestUpdate_PatchEventOnly(t *testing.T) {
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
t.Fatalf("unmarshal captured patch body: %v", err)
}
if body["summary"] != "Updated Meeting" || body["description"] != "Updated description" {
// The deprecated, hidden --description folds into 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)
}
if _, ok := body["description"]; ok {
t.Fatalf("plain description must not be sent, got: %#v", body)
}
if body["need_notification"] != false {
t.Fatalf("need_notification = %#v, want false", body["need_notification"])
}
Expand Down Expand Up @@ -1364,6 +1369,63 @@ func TestAgenda_Success(t *testing.T) {
}
}

func TestAgenda_UnifiesDescriptionRich(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())

reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/events/instance_view",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"event_id": "evt_rich",
"summary": "Rich",
"status": "confirmed",
"description": "[测试]\n友情提醒",
"description_rich": "友情提醒",
"start_time": map[string]interface{}{"timestamp": "1742515200"},
"end_time": map[string]interface{}{"timestamp": "1742518800"},
},
map[string]interface{}{
"event_id": "evt_plain",
"summary": "Plain",
"status": "confirmed",
"description": "just text",
"start_time": map[string]interface{}{"timestamp": "1742515200"},
"end_time": map[string]interface{}{"timestamp": "1742518800"},
},
},
},
},
})

err := mountAndRun(t, CalendarAgenda, []string{
"+agenda",
"--start", "2025-03-21",
"--end", "2025-03-21",
"--as", "bot",
}, f, stdout)
Comment thread
zhangjun-bytedance marked this conversation as resolved.
if err != nil {
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)
}
if !strings.Contains(out, "\"description\": \"just text\"") {
t.Errorf("expected plain description retained 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)
}
}

func TestAgenda_EmptyResult(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())

Expand Down Expand Up @@ -3375,6 +3437,72 @@ func TestGet_Success_FlattensAndConvertsTimes(t *testing.T) {
}
}

func TestGet_UnifiesDescriptionRich(t *testing.T) {
// Read keeps both description (plain) and description_rich (rich).
t.Run("rich present", func(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_rich",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"event": map[string]interface{}{
"event_id": "evt_rich",
"summary": "Rich",
"description": "[表格]",
"description_rich": "| a | b |\n| --- | --- |\n| c | d |",
"start_time": map[string]interface{}{"timestamp": "1742515200", "timezone": "Asia/Shanghai"},
"end_time": map[string]interface{}{"timestamp": "1742518800", "timezone": "Asia/Shanghai"},
},
},
},
})
if err := mountAndRun(t, CalendarGet, []string{"+get", "--calendar-id", "cal_test123", "--event-id", "evt_rich", "--as", "bot"}, f, stdout); err != nil {
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, "\"description_rich\":") {
t.Errorf("expected description_rich 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) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/calendar/v4/calendars/cal_test123/events/evt_plain",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"event": map[string]interface{}{
"event_id": "evt_plain",
"summary": "Plain",
"description": "just text",
"start_time": map[string]interface{}{"timestamp": "1742515200", "timezone": "Asia/Shanghai"},
"end_time": map[string]interface{}{"timestamp": "1742518800", "timezone": "Asia/Shanghai"},
},
},
},
})
if err := mountAndRun(t, CalendarGet, []string{"+get", "--calendar-id", "cal_test123", "--event-id", "evt_plain", "--as", "bot"}, f, stdout); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "\"description\": \"just text\"") {
t.Errorf("expected plain description retained, got: %s", out)
}
if !strings.Contains(out, "\"description_rich\": \"just text\"") {
t.Errorf("expected description_rich backfilled from plain, got: %s", out)
}
})
}

func TestGet_CancelledStatus_PreservesStatus(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())

Expand Down
42 changes: 34 additions & 8 deletions shortcuts/calendar/calendar_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
{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: "event description"},
{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: "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)"},
Expand Down Expand Up @@ -71,7 +72,7 @@
return err
}
if !hasCalendarUpdateOperation(runtime) {
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 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 nil
}
Expand Down Expand Up @@ -109,11 +110,16 @@
body := map[string]interface{}{}
hasFields := false

for _, field := range []string{"summary", "description"} {
if runtime.Cmd.Flags().Changed(field) {
body[field] = runtime.Str(field)
hasFields = true
}
if runtime.Cmd.Flags().Changed("summary") {
body["summary"] = runtime.Str("summary")
hasFields = true
}
if runtime.Cmd.Flags().Changed("description-rich") {
body["description_rich"] = runtime.Str("description-rich")
hasFields = true

Check warning on line 119 in shortcuts/calendar/calendar_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/calendar/calendar_update.go#L118-L119

Added lines #L118 - L119 were not covered by tests
} else if runtime.Cmd.Flags().Changed("description") {
body["description_rich"] = runtime.Str("description")
hasFields = true
}
Comment thread
zhangjun-bytedance marked this conversation as resolved.
if runtime.Cmd.Flags().Changed("rrule") {
rrule := strings.TrimSpace(runtime.Str("rrule"))
Expand Down Expand Up @@ -356,6 +362,15 @@
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 {
return err

Check warning on line 370 in shortcuts/calendar/calendar_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/calendar/calendar_update.go#L369-L370

Added lines #L369 - L370 were not covered by tests
}
}

body, hasEventFields, err := buildCalendarUpdateEventData(runtime)
if err != nil {
return err
Expand Down Expand Up @@ -428,9 +443,20 @@
if summary, _ := event["summary"].(string); summary != "" {
result["summary"] = summary
}
if description, _ := event["description"].(string); description != "" {
// 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

Check warning on line 458 in shortcuts/calendar/calendar_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/calendar/calendar_update.go#L458

Added line #L458 was not covered by tests
}
if start := formatCalendarEventTime(event["start_time"]); start != "" {
result["start"] = start
}
Expand Down
Loading
Loading