Feishu Meeting Room Book
基于飞书日历事件学习个人常用会议室,并按本地偏好列表预定会议室。
Paths
- State file:
state/feishu-meeting-room-book.json
- Helper script:
skills/feishu-meeting-room-book/scripts/meeting_room_booker.py
- Refresh plan file:
tmp/meeting-room-refresh-plan.json
Dependencies And Permissions
- Required OpenClaw plugin:
openclaw-lark or any equivalent Feishu/Lark plugin that exposes:
feishu_calendar_event
feishu_calendar_event_attendee
feishu_calendar_freebusy
- Required local runtime:
python3
- the bundled helper script uses Python standard library only and does not require extra pip packages
- Local file access:
- persistent state is written to
state/feishu-meeting-room-book.json
- temp working files may be written under
tmp/meeting-room-events-*.json and tmp/meeting-room-candidates-*.json
- Required Feishu OAuth scopes:
calendar:calendar.event:read for reading recent calendar events during init/refresh
calendar:calendar.event:create for creating new calendar events
calendar:calendar.event:update for attaching meeting rooms and updating event attendees
calendar:calendar.free_busy:read for checking room availability
- Booking model:
- this skill does not call a separate meeting-room booking service
- room booking is performed by creating or updating Feishu calendar events and attaching the room as a
resource attendee
Required Tools
feishu_calendar_event
feishu_calendar_event_attendee
feishu_calendar_freebusy
If any Feishu tool returns auth or scope errors, finish the OAuth flow first and retry. The normal scopes for this skill are:
calendar:calendar.event:read
calendar:calendar.event:create
calendar:calendar.event:update
calendar:calendar.free_busy:read
Product Rules
- Manual refresh only. Never auto-refresh cached rooms.
- The cached room list is single-city v1. Store one base location (default city) and one ordered room list. The internal state field name remains
default_city.
- On first use, if
state/feishu-meeting-room-book.json does not exist and the user asked to create/book a room or book an existing event, do not ask the user to initialize separately. Tell the user you are starting first-time initialization, run init immediately, and continue the original request after the default room order has been saved.
- During init/refresh, the extracted candidate order is the default priority order. Do not reshuffle it unless the user explicitly changes it.
- When creating a new event, use defaults instead of asking follow-up questions for common omissions:
- missing title: use summary
会议
- missing attendees: create a self-only event with no extra attendees
- missing duration/end time: default to 1 hour
- If the user does not specify a city, use the saved base location (default city) from
default_city.
- If the user specifies a different city from the saved base location (default city), create the event but do not auto-book a room. Explain that the current cache only covers the base location (default city), and tell the user to seed that city manually and run refresh.
- When no preferred room is free:
create_and_book: still create the event without a room
book_existing: keep the event unchanged and tell the user to refresh after manually seeding more rooms
Updating Candidate Rooms
Use these rules when the user asks how to update the candidate room list.
- To add a new candidate room:
- Ask the user to create any calendar event within the next 7 days.
- Tell the user to attach the desired meeting room to that event as a
resource attendee.
- Then tell the user to say
刷新会议室列表, 更新会议室列表, 重新学习会议室, or 更新会议室候选表.
- Default refresh scans only a narrow seeding window and includes that room if it appears in the event payload.
- To remove a room from the candidate list:
- Run refresh and keep only the rooms the user still wants.
- Or keep the current list and overwrite the state with a smaller subset and a new order.
- To change room priority:
- Run refresh and let the user reply with the final room order.
- Or keep the current rooms and overwrite the state with a reordered
--room-id list.
- Window limitation:
- Only events in the chosen scan window affect candidates.
- Default refresh uses
now - 1 day to now + 7 days.
- First-use init uses
now - 7 days to now + 7 days.
- Only explicit full rebuild uses
now - 20 days to now + 20 days - 1 minute.
- A seeding event outside the current scan window will not update the candidate list.
Init Or Refresh
Use this flow when the user says:
- "初始化会议室偏好"
- "初始化会议室预定技能"
- "刷新会议室列表"
- "更新会议室列表"
- "重新学习会议室"
- "更新会议室候选表"
- "全量刷新会议室列表"
- "重建会议室列表"
- or the state file does not exist
When init is triggered implicitly because the state file does not exist, explicitly tell the user:
- this is the first use of the skill
- you are learning their recent meeting room history now
- you will save a default room priority order first
- you will continue the current booking request automatically after init finishes
Steps:
- Choose one scan mode before calling any Feishu tool:
default_refresh: when state already exists and the user says 刷新会议室列表, 更新会议室列表, 重新学习会议室, or 更新会议室候选表
- scan window:
now - 1 day to now + 7 days
- save behavior: incremental update; keep existing rooms and order, append newly discovered rooms, refresh metadata for rooms seen again
init: when the state file does not exist or the user explicitly says 初始化会议室偏好 or 初始化会议室预定技能
- scan window:
now - 7 days to now + 7 days
- save behavior: rebuild the state from the extracted candidates
full_rebuild: only when the user explicitly says 全量刷新会议室列表 or 重建会议室列表
- scan window:
now - 20 days to now + 20 days - 1 minute
- save behavior: rebuild the state from the extracted candidates
- Build the refresh plan with the helper script.
Never compute the slice boundaries manually:
python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py plan-refresh \
--mode <default_refresh|init|full_rebuild> \
--output tmp/meeting-room-refresh-plan.json
- Read
tmp/meeting-room-refresh-plan.json.
Use exactly the slices[].start_time, slices[].end_time, slices[].event_file, and slices[].candidate_file values from that plan.
Do not invent your own temp filenames or time windows.
- For each slice from the plan:
- call
feishu_calendar_event with action=list
- immediately save only that slice result to the exact
event_file from the plan
- immediately run extract on that single slice:
python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py extract \
--input <event_file_from_plan> \
--output <candidate_file_from_plan>
- If
extract fails with invalid JSON in <event_file> or any other parse error, treat that slice file as damaged or truncated.
- Stop immediately. Tell the user refresh failed because one slice result was not valid JSON.
- Do not continue to later slices and do not run
finalize-refresh.
- After all slices succeed, finalize refresh with one helper command.
This command is the only allowed way to write
state/feishu-meeting-room-book.json during refresh:
python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py finalize-refresh \
--input <candidate_file_from_plan_1> \
--input <candidate_file_from_plan_2> \
--mode <default_refresh|init|full_rebuild> \
--state state/feishu-meeting-room-book.json \
--output tmp/meeting-room-candidates.json
- Read
tmp/meeting-room-candidates.json.
If the helper command failed or returned saved != true, stop, tell the user refresh failed, and keep the previous state unchanged.
- Show the candidate rooms to the user with explicit priority numbers. Tell the user:
- this is the current default priority order
- the current base location (default city) is
<default_city>
- if this was
default_refresh, explain that the existing list was preserved and newly discovered rooms were appended
- it has already been saved as the fallback order
- if the user does not reply, future bookings will use this default order
- if this init was triggered by a booking request, the current request will proceed with this default order
- if the user wants to adjust it, reply with:
- the base location (default city) if they want to change it
- which rooms to keep
- the final room order
- If the user replies with a custom order or subset, overwrite the state with:
python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py save \
--input tmp/meeting-room-candidates.json \
--state state/feishu-meeting-room-book.json \
--default-city "<city>" \
--output tmp/meeting-room-candidates-confirmed.json \
--room-id omm_first \
--room-id omm_second
Rules:
- Learn only from resource attendees already attached to real events.
- Prefer resource attendees with
rsvp_status=accept.
- If
rsvp_status is missing in the event payload, keep the room candidate.
- The helper script's candidate order is already the default priority order.
- If the user gives no reordering instructions, keep the saved default order unchanged.
default_refresh is incremental:
- keep existing rooms even if they did not appear in the current narrow scan window
- keep the existing order for already saved rooms
- append newly discovered rooms after the existing list
- only refresh metadata such as
display_name, score, last_seen_at, and location_hints for rooms that were seen again
- Room removal is not part of incremental refresh. To remove rooms, the user must explicitly reorder/trim the list or run
full_rebuild and then confirm a smaller subset.
- The current official
feishu_calendar_event tool does not expose reliable paging controls for list, so keep each time slice small enough that a single slice result remains manageable.
- Never fetch a whole multi-week scan window in one tool call.
- Never widen the scan window beyond
now - 20 days to now + 20 days - 1 minute.
- Never use
write, edit, or any other direct file-writing tool on state/feishu-meeting-room-book.json.
- During refresh,
write is allowed only for raw per-slice temp files under tmp/meeting-room-events-*.json.
- Never use shell redirection like
> with meeting_room_booker.py JSON commands. Use the helper's --output flag so it writes atomically.
- If any
event_file is truncated or malformed, extract will fail. Treat that as the real failure cause; do not continue with an empty or partially written candidate file.
- Never hand-copy, paraphrase, or manually reconstruct the
feishu_calendar_event JSON into another file.
- Never invent candidate rooms or scores from memory if parsing fails.
- The only valid persistent state file for this skill is
state/feishu-meeting-room-book.json.
- Never write or read
state/meeting-room-preferences.json or any other alternate state filename.
- If writing any slice result or running
extract / plan-refresh / finalize-refresh / save fails, stop immediately, tell the user refresh failed, and keep the previous state unchanged.
- Never overwrite the state with a guessed list when no new candidates were extracted.
Create And Book
Use this flow when the user wants to create a new event and also book a meeting room.
Steps:
- If
state/feishu-meeting-room-book.json is missing, run init first.
- Tell the user you detected first use and are initializing meeting room preferences before booking.
- After init saves the default order, continue this create-and-book flow automatically.
- Load the state:
python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py show --state state/feishu-meeting-room-book.json --json
- Parse:
- title
- start time
- end time or duration
- attendees
- city
- Apply defaults before asking any follow-up:
- If title is missing, set it to
会议.
- If attendees are missing, use no extra attendees. The event is still created for the current user because
user_open_id is always passed.
- If both end time and duration are missing, set end time to
start_time + 1 hour.
- If city is missing, use the saved base location (default city) from
default_city.
- If city is not the saved base location (default city), create the event without a room and explain the single-city v1 limitation.
- Otherwise, iterate the cached rooms in rank order. For each room, call
feishu_calendar_freebusy with action=list and room_id=<room_id>.
- Pick the first room whose
freebusy_items is empty.
- Create the event with
feishu_calendar_event action=create.
- Always pass
user_open_id
- Put normal attendees in
attendees only when the user explicitly provided them
- Do not put the room into the initial create call
- If a free room was found, attach it with
feishu_calendar_event_attendee action=create.
- Immediately call
feishu_calendar_event_attendee action=list once to inspect the room attendee status.
Do not tell the user that topic, attendees, and duration are required. The user can simply give a start time; the defaults above fill the rest.
Interpretation:
accept: room booked successfully
needs_action: event created and room booking submitted; tell the user it is pending
decline or attendee-create failure: keep the event and tell the user no room was booked
Book Existing Event
Use this flow when the user wants to add a room to an existing event.
Steps:
- If
state/feishu-meeting-room-book.json is missing, run init first.
- Tell the user you detected first use and are initializing meeting room preferences before booking.
- After init saves the default order, continue this booking flow automatically.
- Resolve the target event:
- Prefer an explicit
event_id
- Otherwise search by title/time with
feishu_calendar_event action=search or action=list
- If multiple plausible matches remain, ask the user to choose before mutating
- Read attendees first with
feishu_calendar_event_attendee action=list.
- If the event already has a resource attendee in
accept or needs_action, stop and report the current room state.
- If the requested city is different from the saved base location (default city), do not try booking; explain the single-city v1 limitation.
- Otherwise, iterate cached rooms with
feishu_calendar_freebusy and attach the first free room with feishu_calendar_event_attendee.
- Re-read attendees once and report the resulting room RSVP state.
Local State Shape
The helper script writes:
{
"version": 1,
"default_city": "Shanghai",
"updated_at": "2026-03-17T20:00:00+08:00",
"rooms": [
{
"room_id": "omm_xxx",
"display_name": "A5-01",
"rank": 1,
"score": 18,
"last_seen_at": "2026-03-16T10:00:00+08:00",
"location_hints": ["Shanghai HQ"]
}
]
}
This file is renewable runtime state. Keep it in state/.
1---2name: feishu-meeting-room-book3description: 飞书会议室偏好初始化与预定。适用于:初始化/刷新个人会议室列表、创建会议并预定会议室、 给已有会议补订会议室。使用本地状态文件保存首选会议室列表;第一版只维护一个 base 地(默认城市,内部字段为 default_city) 的会议室列表,跨城市时只创建会议不自动订房。4---56# Feishu Meeting Room Book78基于飞书日历事件学习个人常用会议室,并按本地偏好列表预定会议室。910## Paths1112- State file: `state/feishu-meeting-room-book.json`13- Helper script: `skills/feishu-meeting-room-book/scripts/meeting_room_booker.py`14- Refresh plan file: `tmp/meeting-room-refresh-plan.json`1516## Dependencies And Permissions1718- Required OpenClaw plugin:19 - `openclaw-lark` or any equivalent Feishu/Lark plugin that exposes:20 - `feishu_calendar_event`21 - `feishu_calendar_event_attendee`22 - `feishu_calendar_freebusy`23- Required local runtime:24 - `python3`25 - the bundled helper script uses Python standard library only and does not require extra pip packages26- Local file access:27 - persistent state is written to `state/feishu-meeting-room-book.json`28 - temp working files may be written under `tmp/meeting-room-events-*.json` and `tmp/meeting-room-candidates-*.json`29- Required Feishu OAuth scopes:30 - `calendar:calendar.event:read` for reading recent calendar events during init/refresh31 - `calendar:calendar.event:create` for creating new calendar events32 - `calendar:calendar.event:update` for attaching meeting rooms and updating event attendees33 - `calendar:calendar.free_busy:read` for checking room availability34- Booking model:35 - this skill does not call a separate meeting-room booking service36 - room booking is performed by creating or updating Feishu calendar events and attaching the room as a `resource attendee`3738## Required Tools3940- `feishu_calendar_event`41- `feishu_calendar_event_attendee`42- `feishu_calendar_freebusy`4344If any Feishu tool returns auth or scope errors, finish the OAuth flow first and retry. The normal scopes for this skill are:4546- `calendar:calendar.event:read`47- `calendar:calendar.event:create`48- `calendar:calendar.event:update`49- `calendar:calendar.free_busy:read`5051## Product Rules5253- Manual refresh only. Never auto-refresh cached rooms.54- The cached room list is single-city v1. Store one base location (default city) and one ordered room list. The internal state field name remains `default_city`.55- On first use, if `state/feishu-meeting-room-book.json` does not exist and the user asked to create/book a room or book an existing event, do not ask the user to initialize separately. Tell the user you are starting first-time initialization, run init immediately, and continue the original request after the default room order has been saved.56- During init/refresh, the extracted candidate order is the default priority order. Do not reshuffle it unless the user explicitly changes it.57- When creating a new event, use defaults instead of asking follow-up questions for common omissions:58 - missing title: use summary `会议`59 - missing attendees: create a self-only event with no extra attendees60 - missing duration/end time: default to 1 hour61- If the user does not specify a city, use the saved base location (default city) from `default_city`.62- If the user specifies a different city from the saved base location (default city), create the event but do not auto-book a room. Explain that the current cache only covers the base location (default city), and tell the user to seed that city manually and run refresh.63- When no preferred room is free:64 - `create_and_book`: still create the event without a room65 - `book_existing`: keep the event unchanged and tell the user to refresh after manually seeding more rooms6667## Updating Candidate Rooms6869Use these rules when the user asks how to update the candidate room list.7071- To add a new candidate room:72 1. Ask the user to create any calendar event within the next 7 days.73 2. Tell the user to attach the desired meeting room to that event as a `resource attendee`.74 3. Then tell the user to say `刷新会议室列表`, `更新会议室列表`, `重新学习会议室`, or `更新会议室候选表`.75 4. Default refresh scans only a narrow seeding window and includes that room if it appears in the event payload.76- To remove a room from the candidate list:77 - Run refresh and keep only the rooms the user still wants.78 - Or keep the current list and overwrite the state with a smaller subset and a new order.79- To change room priority:80 - Run refresh and let the user reply with the final room order.81 - Or keep the current rooms and overwrite the state with a reordered `--room-id` list.82- Window limitation:83 - Only events in the chosen scan window affect candidates.84 - Default refresh uses `now - 1 day` to `now + 7 days`.85 - First-use init uses `now - 7 days` to `now + 7 days`.86 - Only explicit full rebuild uses `now - 20 days` to `now + 20 days - 1 minute`.87 - A seeding event outside the current scan window will not update the candidate list.8889## Init Or Refresh9091Use this flow when the user says:9293- "初始化会议室偏好"94- "初始化会议室预定技能"95- "刷新会议室列表"96- "更新会议室列表"97- "重新学习会议室"98- "更新会议室候选表"99- "全量刷新会议室列表"100- "重建会议室列表"101- or the state file does not exist102103When init is triggered implicitly because the state file does not exist, explicitly tell the user:104105- this is the first use of the skill106- you are learning their recent meeting room history now107- you will save a default room priority order first108- you will continue the current booking request automatically after init finishes109110Steps:1111121. Choose one scan mode before calling any Feishu tool:113 - `default_refresh`: when state already exists and the user says `刷新会议室列表`, `更新会议室列表`, `重新学习会议室`, or `更新会议室候选表`114 - scan window: `now - 1 day` to `now + 7 days`115 - save behavior: incremental update; keep existing rooms and order, append newly discovered rooms, refresh metadata for rooms seen again116 - `init`: when the state file does not exist or the user explicitly says `初始化会议室偏好` or `初始化会议室预定技能`117 - scan window: `now - 7 days` to `now + 7 days`118 - save behavior: rebuild the state from the extracted candidates119 - `full_rebuild`: only when the user explicitly says `全量刷新会议室列表` or `重建会议室列表`120 - scan window: `now - 20 days` to `now + 20 days - 1 minute`121 - save behavior: rebuild the state from the extracted candidates1222. Build the refresh plan with the helper script.123 Never compute the slice boundaries manually:124125```bash126python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py plan-refresh \127 --mode <default_refresh|init|full_rebuild> \128 --output tmp/meeting-room-refresh-plan.json129```1301313. Read `tmp/meeting-room-refresh-plan.json`.132 Use exactly the `slices[].start_time`, `slices[].end_time`, `slices[].event_file`, and `slices[].candidate_file` values from that plan.133 Do not invent your own temp filenames or time windows.1344. For each slice from the plan:135 - call `feishu_calendar_event` with `action=list`136 - immediately save only that slice result to the exact `event_file` from the plan137 - immediately run extract on that single slice:138139```bash140python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py extract \141 --input <event_file_from_plan> \142 --output <candidate_file_from_plan>143```144145 - If `extract` fails with `invalid JSON in <event_file>` or any other parse error, treat that slice file as damaged or truncated.146 - Stop immediately. Tell the user refresh failed because one slice result was not valid JSON.147 - Do not continue to later slices and do not run `finalize-refresh`.1481495. After all slices succeed, finalize refresh with one helper command.150 This command is the only allowed way to write `state/feishu-meeting-room-book.json` during refresh:151152```bash153python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py finalize-refresh \154 --input <candidate_file_from_plan_1> \155 --input <candidate_file_from_plan_2> \156 --mode <default_refresh|init|full_rebuild> \157 --state state/feishu-meeting-room-book.json \158 --output tmp/meeting-room-candidates.json159```1601616. Read `tmp/meeting-room-candidates.json`.162 If the helper command failed or returned `saved != true`, stop, tell the user refresh failed, and keep the previous state unchanged.1637. Show the candidate rooms to the user with explicit priority numbers. Tell the user:164 - this is the current default priority order165 - the current base location (default city) is `<default_city>`166 - if this was `default_refresh`, explain that the existing list was preserved and newly discovered rooms were appended167 - it has already been saved as the fallback order168 - if the user does not reply, future bookings will use this default order169 - if this init was triggered by a booking request, the current request will proceed with this default order170 - if the user wants to adjust it, reply with:171 - the base location (default city) if they want to change it172 - which rooms to keep173 - the final room order1748. If the user replies with a custom order or subset, overwrite the state with:175176```bash177python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py save \178 --input tmp/meeting-room-candidates.json \179 --state state/feishu-meeting-room-book.json \180 --default-city "<city>" \181 --output tmp/meeting-room-candidates-confirmed.json \182 --room-id omm_first \183 --room-id omm_second184```185186Rules:187188- Learn only from resource attendees already attached to real events.189- Prefer resource attendees with `rsvp_status=accept`.190- If `rsvp_status` is missing in the event payload, keep the room candidate.191- The helper script's candidate order is already the default priority order.192- If the user gives no reordering instructions, keep the saved default order unchanged.193- `default_refresh` is incremental:194 - keep existing rooms even if they did not appear in the current narrow scan window195 - keep the existing order for already saved rooms196 - append newly discovered rooms after the existing list197 - only refresh metadata such as `display_name`, `score`, `last_seen_at`, and `location_hints` for rooms that were seen again198- Room removal is not part of incremental refresh. To remove rooms, the user must explicitly reorder/trim the list or run `full_rebuild` and then confirm a smaller subset.199- The current official `feishu_calendar_event` tool does not expose reliable paging controls for `list`, so keep each time slice small enough that a single slice result remains manageable.200- Never fetch a whole multi-week scan window in one tool call.201- Never widen the scan window beyond `now - 20 days` to `now + 20 days - 1 minute`.202- Never use `write`, `edit`, or any other direct file-writing tool on `state/feishu-meeting-room-book.json`.203- During refresh, `write` is allowed only for raw per-slice temp files under `tmp/meeting-room-events-*.json`.204- Never use shell redirection like `>` with `meeting_room_booker.py` JSON commands. Use the helper's `--output` flag so it writes atomically.205- If any `event_file` is truncated or malformed, `extract` will fail. Treat that as the real failure cause; do not continue with an empty or partially written candidate file.206- Never hand-copy, paraphrase, or manually reconstruct the `feishu_calendar_event` JSON into another file.207- Never invent candidate rooms or scores from memory if parsing fails.208- The only valid persistent state file for this skill is `state/feishu-meeting-room-book.json`.209- Never write or read `state/meeting-room-preferences.json` or any other alternate state filename.210- If writing any slice result or running `extract` / `plan-refresh` / `finalize-refresh` / `save` fails, stop immediately, tell the user refresh failed, and keep the previous state unchanged.211- Never overwrite the state with a guessed list when no new candidates were extracted.212213## Create And Book214215Use this flow when the user wants to create a new event and also book a meeting room.216217Steps:2182191. If `state/feishu-meeting-room-book.json` is missing, run init first.220 - Tell the user you detected first use and are initializing meeting room preferences before booking.221 - After init saves the default order, continue this create-and-book flow automatically.2222. Load the state:223224```bash225python3 skills/feishu-meeting-room-book/scripts/meeting_room_booker.py show --state state/feishu-meeting-room-book.json --json226```2272283. Parse:229 - title230 - start time231 - end time or duration232 - attendees233 - city2344. Apply defaults before asking any follow-up:235 - If title is missing, set it to `会议`.236 - If attendees are missing, use no extra attendees. The event is still created for the current user because `user_open_id` is always passed.237 - If both end time and duration are missing, set end time to `start_time + 1 hour`.2385. If city is missing, use the saved base location (default city) from `default_city`.2396. If city is not the saved base location (default city), create the event without a room and explain the single-city v1 limitation.2407. Otherwise, iterate the cached rooms in rank order. For each room, call `feishu_calendar_freebusy` with `action=list` and `room_id=<room_id>`.2418. Pick the first room whose `freebusy_items` is empty.2429. Create the event with `feishu_calendar_event` `action=create`.243 - Always pass `user_open_id`244 - Put normal attendees in `attendees` only when the user explicitly provided them245 - Do not put the room into the initial create call24610. If a free room was found, attach it with `feishu_calendar_event_attendee` `action=create`.24711. Immediately call `feishu_calendar_event_attendee` `action=list` once to inspect the room attendee status.248249Do not tell the user that topic, attendees, and duration are required. The user can simply give a start time; the defaults above fill the rest.250251Interpretation:252253- `accept`: room booked successfully254- `needs_action`: event created and room booking submitted; tell the user it is pending255- `decline` or attendee-create failure: keep the event and tell the user no room was booked256257## Book Existing Event258259Use this flow when the user wants to add a room to an existing event.260261Steps:2622631. If `state/feishu-meeting-room-book.json` is missing, run init first.264 - Tell the user you detected first use and are initializing meeting room preferences before booking.265 - After init saves the default order, continue this booking flow automatically.2662. Resolve the target event:267 - Prefer an explicit `event_id`268 - Otherwise search by title/time with `feishu_calendar_event` `action=search` or `action=list`269 - If multiple plausible matches remain, ask the user to choose before mutating2703. Read attendees first with `feishu_calendar_event_attendee` `action=list`.2714. If the event already has a resource attendee in `accept` or `needs_action`, stop and report the current room state.2725. If the requested city is different from the saved base location (default city), do not try booking; explain the single-city v1 limitation.2736. Otherwise, iterate cached rooms with `feishu_calendar_freebusy` and attach the first free room with `feishu_calendar_event_attendee`.2747. Re-read attendees once and report the resulting room RSVP state.275276## Local State Shape277278The helper script writes:279280```json281{282 "version": 1,283 "default_city": "Shanghai",284 "updated_at": "2026-03-17T20:00:00+08:00",285 "rooms": [286 {287 "room_id": "omm_xxx",288 "display_name": "A5-01",289 "rank": 1,290 "score": 18,291 "last_seen_at": "2026-03-16T10:00:00+08:00",292 "location_hints": ["Shanghai HQ"]293 }294 ]295}296```297298This file is renewable runtime state. Keep it in `state/`.