Skip to content
Merged
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
13 changes: 11 additions & 2 deletions shortcuts/calendar/calendar_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package calendar
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -72,6 +73,7 @@ var CalendarCreate = common.Shortcut{
Risk: "write",
Scopes: []string{"calendar:calendar.event:create", "calendar:calendar.event:update"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Flags: []common.Flag{
{Name: "summary", Desc: "event title"},
{Name: "start", Desc: "start time (ISO 8601)", Required: true},
Expand Down Expand Up @@ -279,12 +281,19 @@ var CalendarCreate = common.Shortcut{
}
}

runtime.Out(map[string]interface{}{
resultData := map[string]interface{}{
"event_id": eventId,
"summary": event["summary"],
"start": startStr,
"end": endStr,
}, nil)
}

runtime.OutFormat(resultData, nil, func(w io.Writer) {
var rows []map[string]interface{}
rows = append(rows, resultData)
output.PrintTable(w, rows)
fmt.Fprintln(w, "\nEvent created successfully")
})
return nil
},
}
27 changes: 24 additions & 3 deletions shortcuts/calendar/calendar_room_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,24 @@ func parseRoomFindAttendees(attendeesStr string, currentUserID string) ([]string
return userIDs, chatIDs, nil
}

func normalizeCommaSeparatedNames(raw string) string {
parts := strings.Split(raw, ",")
var cleaned []string
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
cleaned = append(cleaned, p)
}
}
return strings.Join(cleaned, ",")
}

func buildRoomFindBaseRequest(runtime *common.RuntimeContext) (*roomFindRequest, error) {
req := &roomFindRequest{
City: strings.TrimSpace(runtime.Str(flagCity)),
Building: strings.TrimSpace(runtime.Str(flagBuilding)),
Floor: strings.TrimSpace(runtime.Str(flagFloor)),
RoomName: strings.TrimSpace(runtime.Str(flagRoomName)),
RoomName: normalizeCommaSeparatedNames(runtime.Str(flagRoomName)),
Comment thread
hugang-lark marked this conversation as resolved.
MinCapacity: runtime.Int(flagMinCapacity),
MaxCapacity: runtime.Int(flagMaxCapacity),
Timezone: strings.TrimSpace(runtime.Str(flagTimezone)),
Expand Down Expand Up @@ -272,7 +284,7 @@ var CalendarRoomFind = common.Shortcut{
{Name: flagCity, Type: "string", Desc: "meeting room city constraint"},
{Name: flagBuilding, Type: "string", Desc: "meeting room building constraint"},
{Name: flagFloor, Type: "string", Desc: "meeting room floor constraint (e.g., F2)"},
{Name: flagRoomName, Type: "string", Desc: "meeting room name constraint (e.g., 木星, 02)"},
{Name: flagRoomName, Type: "string", Desc: "meeting room name constraint; comma-separated for multiple names (e.g., 01,02,03)"},
{Name: flagMinCapacity, Type: "int", Desc: "minimum meeting room capacity"},
{Name: flagMaxCapacity, Type: "int", Desc: "maximum meeting room capacity"},
{Name: flagAttendees, Type: "string", Desc: "attendee IDs, comma-separated (supports user ou_, chat oc_)"},
Expand Down Expand Up @@ -303,13 +315,22 @@ var CalendarRoomFind = common.Shortcut{
if err := rejectCalendarAutoBotFallback(runtime); err != nil {
return err
}
for _, flag := range []string{flagCity, flagBuilding, flagFloor, flagRoomName, flagEventRrule, flagTimezone} {
for _, flag := range []string{flagCity, flagBuilding, flagFloor, flagEventRrule, flagTimezone} {
if val := strings.TrimSpace(runtime.Str(flag)); val != "" {
if err := common.RejectDangerousChars("--"+flag, val); err != nil {
return output.ErrValidation(err.Error())
}
}
}
for _, name := range strings.Split(runtime.Str(flagRoomName), ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if err := common.RejectDangerousChars("--"+flagRoomName, name); err != nil {
return output.ErrValidation(err.Error())
}
}
if _, err := parseRoomFindSlots(runtime); err != nil {
return err
}
Expand Down
22 changes: 22 additions & 0 deletions shortcuts/calendar/calendar_room_find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ import (
"time"
)

func TestNormalizeCommaSeparatedNames(t *testing.T) {
tests := []struct {
input string
want string
}{
{"木星", "木星"},
{"01,02,03", "01,02,03"},
{" 01 , 02 , 03 ", "01,02,03"},
{"16,17,18,19,20", "16,17,18,19,20"},
{"", ""},
{" , , ", ""},
{"01,,03", "01,03"},
{" 木星 ", "木星"},
}
for _, tt := range tests {
got := normalizeCommaSeparatedNames(tt.input)
if got != tt.want {
t.Errorf("normalizeCommaSeparatedNames(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}

func TestCollectRoomFindResults_LimitsConcurrency(t *testing.T) {
slots := []roomFindSlot{
{Start: "2026-03-27T14:00:00+08:00", End: "2026-03-27T15:00:00+08:00"},
Expand Down
45 changes: 45 additions & 0 deletions shortcuts/calendar/calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,51 @@ func TestCreate_CreateEventOnly(t *testing.T) {
}
}

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

reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"event": map[string]interface{}{
"event_id": "evt_001",
"summary": "Test Meeting",
"start_time": map[string]interface{}{
"timestamp": "1742515200",
},
"end_time": map[string]interface{}{
"timestamp": "1742518800",
},
},
},
},
})

err := mountAndRun(t, CalendarCreate, []string{
"+create",
"--summary", "Test Meeting",
"--start", "2025-03-21T00:00:00+08:00",
"--end", "2025-03-21T01:00:00+08:00",
"--calendar-id", "cal_test123",
"--as", "bot",
"--format", "pretty",
}, f, stdout)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "evt_001") {
t.Errorf("stdout should contain event_id, got: %s", out)
}
if !strings.Contains(out, "Event created successfully") {
t.Errorf("stdout should contain success message, got: %s", out)
}
}

func TestBuildEventData_DefaultVChat(t *testing.T) {
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("summary", "", "")
Expand Down
4 changes: 2 additions & 2 deletions skills/lark-calendar/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ lark-cli calendar <resource> <method> [flags] # 调用 API
- `get` — 获取日程
- `instance_view` — 查询日程视图
- `patch` — 更新日程
- `search` — 搜索日程
- `search_event` — 搜索日程(注:目前只会返回日程id、日程主题、日程时间的信息,需要更多的日程详情,需要走 `events get` 命令)
- `share_info` — 获取日程分享链接

### freebusys
Expand All @@ -146,7 +146,7 @@ lark-cli calendar <resource> <method> [flags] # 调用 API
| `events.get` | `calendar:calendar.event:read` |
| `events.instance_view` | `calendar:calendar.event:read` |
| `events.patch` | `calendar:calendar.event:update` |
| `events.search` | `calendar:calendar.event:read` |
| `events.search_event` | `calendar:calendar.event:read` |
| `events.share_info` | `calendar:calendar.event:read` |
| `freebusys.list` | `calendar:calendar.free_busy:read` |

Expand Down
28 changes: 26 additions & 2 deletions skills/lark-calendar/references/lark-calendar-room-find.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
## 适用场景

- 已知一个或多个待选时间块,需要查找可用会议室
- 需要在一组连续编号的会议室中批量搜索可用房间(如"帮我约一个 16~20 号之间的会议室")

## 命令

Expand All @@ -23,6 +24,24 @@ lark-cli calendar +room-find \
--event-rrule "FREQ=DAILY;INTERVAL=1"
```

### 批量会议室名称查询

当用户想在一组编号会议室中挑选可用房间时,可用英文逗号拼接多个会议室名称传入 `--room-name`:

```bash
# 场景:帮我约一个 16~20 号之间的会议室
lark-cli calendar +room-find \
--slot "2026-03-27T14:00:00+08:00~2026-03-27T15:00:00+08:00" \
--room-name "16,17,18,19,20"
```

```bash
# 场景:查找 木星 或 火星 会议室
lark-cli calendar +room-find \
--slot "2026-03-27T14:00:00+08:00~2026-03-27T15:00:00+08:00" \
--room-name "木星,火星"
```

## 参数

| 参数 | 必填 | 说明 |
Expand All @@ -31,7 +50,7 @@ lark-cli calendar +room-find \
| `--city <text>` | 否 | 会议室所在城市强约束。**仅当**用户明确说出具体城市(如北京、上海)时才提取,**严禁**根据园区或楼宇名称自行联想或补全。 |
| `--building <text>` | 否 | 会议室所在楼宇强约束,承载城市以下、楼层以上的办公区/园区/楼栋描述。|
| `--floor <text>` | 否 | 仅用于筛选会议室所在楼层。应先做归一化,再传递规范值;例如 `2楼` / `二楼` / `2F` 统一为 `F2`。注意:此参数只筛选楼层,不可混入区域定位(如“A区”)或具体会议室号。 |
| `--room-name <text>` | 否 | 会议室名强约束。仅当用户明确提到会议室专名或会议室号(如“木星”“02”)时使用。应优先传递去后缀、去冗余后的规范名,例如 `木星会议室` → `木星`,`会议室 02` / `02会议室` → `02`。 |
| `--room-name <text>` | 否 | 会议室名称约束,支持以**英文逗号**分隔传入多个名称。仅当用户明确提到会议室专名或会议室号(如"木星""02")时使用。当用户需要在一组编号会议室中搜索时(如"帮我约 16~20 号的会议室"),应将编号展开为逗号分隔列表,如 `"16,17,18,19,20"`。应优先传递去后缀、去冗余后的规范名,例如 `木星会议室` → `木星`,`会议室 02` / `02会议室` → `02`。 |
| `--min-capacity <n>` | 否 | 会议室最小容纳人数。当用户明确参会人数或提出“至少容纳N人”等要求时,提取数字放入此参数,必须为正整数。 |
| `--max-capacity <n>` | 否 | 会议室最大容纳人数。用于过滤过大空间,必须为正整数。 |
| `--attendee-ids <id_list>` | 否 | 参会对象 ID 列表。支持用户 ID(`ou_` 前缀)和群组 ID(`oc_` 前缀),多个 ID 以逗号分隔。 |
Expand All @@ -48,6 +67,10 @@ lark-cli calendar +room-find \
- 同一语义槽位只保留一个规范值。例如用户说“2楼”,应转换为 `--floor "F2"`;**禁止**同时传 `2楼 F2` 这类重复楼层信息。
- 参数归类顺序应为:`city/building/floor` > `floor + room-name` 复合表达 > `room-name`。若短词更像楼层/区域定位(如 `2L`、`2F`),优先落到 `--floor`,不要默认落到 `--room-name`。像 `学清2层` 这种表达,通常拆为 `--building "学清"` 与 `--floor "F2"`。
- 对会议室名要做轻量归一化:`木星会议室` 应提取为 `--room-name "木星"`;`会议室 02` / `02会议室` 应提取为 `--room-name "02"`。
- **多会议室名称场景**:当用户表达"帮我约 XX 到 YY 号之间的会议室"或一次提及多个会议室名称时,应将所有目标名称用英文逗号拼接传入 `--room-name`。例如:
- "帮我约 16~20 号的会议室" → `--room-name "16,17,18,19,20"`
- "查下木星和火星是否有空" → `--room-name "木星,火星"`
- "看看 01、02、03 会议室" → `--room-name "01,02,03"`
- 对复合会议室号要优先拆分结构化信息:`F3-05` / `F5-07` / `3楼-08` 这类表达,若可稳定识别楼层与会议室号,应优先提取为 `--floor "F3"` + `--room-name "05"`、`--floor "F5"` + `--room-name "07"`、`--floor "F3"` + `--room-name "08"`,不要把整段直接作为 `--room-name`。
- 当提供了会议室搜索筛选条件时,返回结果也**不保证**与搜索词完全字面匹配。底层可能会结合邻近楼层做推荐,例如用户搜索 `2层`,即使 `2层` 没有空闲会议室,也可能返回相近的 `3层` 候选。这不应被误判为接口返回异常。

Expand All @@ -68,7 +91,8 @@ lark-cli calendar +room-find \

> **AI 行为指导:**
> - **结构化展示时间块与会议室**:默认按“时间块 -> 会议室候选”的层级结构展示。**严禁将时间与会议室名称输出在同一行**。以清晰的分行列表呈现可用会议室,并直接询问用户意向。默认原样展示完整 `room_name`;不要擅自缩写、截断、改写,或仅提取楼层及会议室号替代完整名称。
> - **`room_name` 必须逐字透传**:展示给用户的会议室名称,必须直接使用 CLI/API 返回的 `room_name` 原值。禁止提取楼层、会议室号、容量、视频能力后重组成新的名称,禁止意译、缩写、去前缀、去后缀,或仅保留“便于阅读”的摘要名。
> - **`room_name` 必须逐字透传**:展示给用户的会议室名称,必须直接使用 CLI/API 返回的 `room_name` 原值。禁止提取楼层、会议室号、容量、视频能力后重组成新的名称,禁止意译、缩写、去前缀、去后缀,或仅保留"便于阅读"的摘要名。
> - **主动识别区间/多名称意图**:当用户提到"帮我约 XX 到 YY 号的会议室""XX~YY 之间的会议室"或一次列出多个会议室名称时,将所有目标名称展开为英文逗号分隔列表,传入 `--room-name`。例如"帮我约 16 到 20 号的会议室"应生成 `--room-name "16,17,18,19,20"`。
> - **重复日程要明确阻断原因与自动缩短**:若某候选会议室的 `reserve_until_time` 无法覆盖重复性日程,**必须**向用户明确说明该会议室最长可约至何时。若用户确认继续选用该会议室,你必须**自动将日程的重复规则结束时间缩短**至该 `reserve_until_time`,以防止会议室预约失败。不能直接按原规则继续。
> - **正确解释推荐结果**:如果返回结果与用户输入条件不完全字面一致,先说明底层可能返回邻近位置或相近条件的推荐候选,不要直接将其判定为异常。
> - **默认减少用户输入成本**:应主动引导用户不必一开始就提供很详细的会议室搜索条件。只要时间块已明确,用户直接表达“想约会议室”即可,先基于当前信息查询候选;只有在用户对结果不满意时,再引导其补充更具体的楼宇、楼层、会议室名或容量条件。
Expand Down
Loading