# 小红书 天津/北京 Upcoming Events Daily Summary — Implementation Plan

Project slug: `xhs-city-events-daily`

Status: plan only. Do **not** begin implementation until explicitly approved.

## Goal

Create a daily pipeline that uses Apify to collect 小红书 posts about upcoming events in 天津 and 北京, then produces an English summary containing **only newly discovered upcoming events**.

Rules:

- Include 天津 and 北京.
- Include only future/upcoming events at summary time, with no fixed horizon in v0.
- Do not repeat events already reported in prior daily summaries.
- Same event name on a different date counts as a different event.
- Include paid/promotional posts if they describe real public events.
- Exclude family/kids-focused events.
- Translate descriptions to English, while keeping proper nouns in Chinese.
- Always link back to the source 小红书 post.
- Use Apify for collection.
- Send the finished daily summary to a chat via an OpenClaw cron job.

## Proposed output

Daily summary format, sent to chat and also saved as markdown:

```md
# Upcoming events from 小红书 — YYYY-MM-DD

## 北京

### Event title / proper noun in Chinese
- Date/time: ...
- Venue/location: ...
- What it is: English summary, preserving Chinese proper nouns
- Source: https://www.xiaohongshu.com/...

## 天津
...

## No new upcoming events
If no qualifying new events are found, send: “no new events today” plus a short count summary of total known upcoming events, including events already seen/reported.
```

Store each generated summary under:

```text
projects/xhs-city-events-daily/summaries/YYYY-MM-DD.md
```

Also maintain machine-readable state under:

```text
projects/xhs-city-events-daily/state/events.jsonl
projects/xhs-city-events-daily/state/reported_keys.json
projects/xhs-city-events-daily/runs/YYYY-MM-DD/raw_apify.json
projects/xhs-city-events-daily/runs/YYYY-MM-DD/candidates.jsonl
projects/xhs-city-events-daily/runs/YYYY-MM-DD/final_events.jsonl
```

## Event identity / dedupe rule

An event is considered already reported if its canonical key has appeared before.

Canonical key:

```text
city | normalized_event_name | normalized_event_date | normalized_venue_or_location
```

For recurring events, each date is treated as a separate reportable event.

Notes:

- `normalized_event_name`: trim whitespace, normalize punctuation, remove generic suffixes only when safe.
- `normalized_event_date`: ISO date if known; if date range, use start date + end date.
- `normalized_venue_or_location`: optional but useful to avoid false merges.
- Same event name on a different date becomes a different key.
- If date is ambiguous or missing, do **not** report the event unless the post clearly says it is upcoming and enough date info can be inferred safely.
- Keep source post URL separately; do not use URL as the primary event identity because reposts and multiple posts can describe the same event.

## Data model

Each candidate event should normalize to:

```json
{
  "event_key": "...",
  "city": "北京|天津",
  "event_name_original": "Chinese name/title",
  "event_name_en": "English translation if useful, but proper nouns preserved",
  "date_start": "YYYY-MM-DD",
  "date_end": "YYYY-MM-DD|null",
  "time_text_original": "...",
  "venue_original": "...",
  "location_original": "...",
  "summary_en": "English summary with Chinese proper nouns preserved",
  "source_url": "https://www.xiaohongshu.com/...",
  "source_title": "...",
  "source_author": "...",
  "source_posted_at": "...",
  "extracted_from": "title|body|image_ocr|comments|mixed",
  "confidence": 0.0,
  "first_seen_at": "ISO timestamp",
  "reported_at": "ISO timestamp|null"
}
```

## Collection approach

### 1. Apify actor selection spike

Before implementation, choose an Apify actor that can collect 小红书 search results and/or post details. Evaluation criteria:

- Can search 小红书 by keyword.
- Returns canonical post URLs.
- Returns title/body text and ideally image OCR or image URLs.
- Can filter or sort recent posts.
- Works without manual login.
- Has predictable pricing and a max-charge guard.

If no reliable 小红书 actor exists, use a two-stage approach:

1. Search engine / web search actor for 小红书 URLs.
2. Detail scraper actor for each URL.

### 2. Search queries

Run multiple Chinese queries per city because event posts are inconsistent.

北京 query seeds:

- `北京 周末 活动 小红书`
- `北京 展览 活动 小红书`
- `北京 市集 活动 小红书`
- `北京 演出 活动 小红书`
- `北京 近期活动 小红书`
- `北京 本周 活动 小红书`

天津 query seeds:

- `天津 周末 活动 小红书`
- `天津 展览 活动 小红书`
- `天津 市集 活动 小红书`
- `天津 演出 活动 小红书`
- `天津 近期活动 小红书`
- `天津 本周 活动 小红书`

Add query categories later if recall is weak: 音乐节, 脱口秀, 艺术, 讲座, 快闪, 咖啡, 戏剧, 市集, 展览. Do not include family/kids-focused query lanes such as 亲子 unless explicitly enabled later.

### 3. Run limits

Start conservative:

- 10–30 posts per query.
- Deduplicate source URLs before detail enrichment.
- Hard cap per daily run, e.g. 300 raw posts total.
- Apify max charge cap configured per run.

## Extraction and classification

For each raw post:

1. Keep only posts related to 天津 or 北京.
2. Extract candidate event names, dates, venues, and source URLs.
3. Reject if:
   - event is already past;
   - date is absent or too ambiguous;
   - post is family/kids-focused;
   - post is purely restaurant/shop recommendation with no event;
   - post is travel itinerary, not a discrete event;
   - source URL is missing;
   - confidence is below threshold.
4. Normalize dates relative to the run date and China timezone.
5. Generate canonical event key.
6. Compare against `reported_keys.json`.
7. Keep only new upcoming events.

## Translation policy

Translate to English, but preserve proper nouns in Chinese.

Examples:

- Keep venue names like `今日美术馆`, `UCCA尤伦斯当代艺术中心`, `天津大剧院`.
- Keep event/brand/person names in Chinese unless there is a widely used English name.
- Translate generic descriptions: “市集” → “market”, “展览” → “exhibition”, “演出” → “performance”.
- Do not invent details not present in the post.

## State management

`reported_keys.json` shape:

```json
{
  "version": 1,
  "reported": {
    "北京|event|2026-07-01|venue": {
      "first_reported_at": "...",
      "source_url": "...",
      "event_name_original": "..."
    }
  }
}
```

After each successful summary:

1. Append all final events to `state/events.jsonl`.
2. Add final event keys to `state/reported_keys.json`.
3. Update known-upcoming status for previously seen events.
4. Write daily summary markdown.
5. Save raw and normalized run artifacts for auditability.

Important: update reported state only after the summary is successfully written/sent. This prevents losing events if a run fails halfway.

## Scheduling

Once implementation is approved, schedule a daily OpenClaw cron job.

Confirmed schedule/delivery defaults:

- Daily at 08:00 Asia/Shanghai.
- Send the summary to whichever chat is easiest to wire first; current group chat is acceptable unless a better target is chosen during implementation.
- Use an OpenClaw cron job, likely an isolated agent turn or script runner depending on final architecture.
- Empty days should still send: “no new events today” plus a short summary of total known upcoming events, including previously seen ones.

Cron payload should say it is a daily run for the 天津/北京 小红书 events summary.

## Implementation phases

### Phase 0 — Confirmed decisions and remaining questions

Confirmed:

1. Daily run time: 08:00 Asia/Shanghai.
2. Delivery: send to a chat, likely via OpenClaw cron.
3. Upcoming horizon: v0 includes everything in the future, not just the next 7/14/30 days.
4. v1 follow-up task: if an event was first seen more than 14 days out, send a reminder when it enters the 14-day window.
5. Paid/promotional posts: include them if they describe real events.
6. Recurring events: report each date separately.
7. Categories: include broad event categories, excluding family/kids.

Remaining before scheduling:

1. Confirm exact delivery target if implementation discovers multiple easy options; otherwise use the easiest available chat target.

### Phase 1 — Apify actor spike

Deliverable: short note choosing the actor and estimating cost.

Tasks:

- Search Apify actors for 小红书 support.
- Test a dry/small run if permitted.
- Inspect output fields.
- Confirm post URL availability.
- Confirm cost guard.

### Phase 2 — Local project scaffolding

Deliverable: runnable local scripts, no schedule yet.

Suggested files:

```text
projects/xhs-city-events-daily/
  IMPLEMENTATION_PLAN.md
  README.md
  config/queries.json
  src/run_daily.py
  src/apify_client.py
  src/extract_events.py
  src/dedupe.py
  src/render_summary.py
  state/.gitkeep
  runs/.gitkeep
  summaries/.gitkeep
```

### Phase 3 — Extraction prototype

Deliverable: `candidates.jsonl` from raw Apify output.

Tasks:

- Normalize raw actor output.
- Extract city/date/location/title/source URL.
- Build event key.
- Reject past/ambiguous/non-event posts.
- Preserve audit fields and confidence.

### Phase 4 — Summary renderer

Deliverable: daily markdown summary from `final_events.jsonl`.

Tasks:

- Group by city.
- Sort by date, then city, then event name.
- Translate summaries to English with proper nouns preserved.
- Always include source links.
- Render explicit “no new upcoming events” message when empty.

### Phase 5 — State and idempotency

Deliverable: repeatable runs that do not duplicate reported events.

Tests:

- Same raw input twice → second summary is empty.
- Same event name different date → included as new.
- Same date/name from different source URL → not duplicated.
- Failed render does not mutate reported state.

### Phase 6 — Scheduling and delivery

Deliverable: daily automated run after explicit approval.

Tasks:

- Add cron schedule.
- Decide destination.
- Include run logs.
- Add failure alert.

## Verification checklist

Before calling implementation done:

- A small Apify run succeeds under max charge.
- Raw artifacts are saved.
- Every final event has a source 小红书 URL.
- Past events are excluded.
- Deduping works across repeated runs.
- Same name/different date remains separate.
- English output preserves Chinese proper nouns.
- Empty result is handled cleanly.
- State is updated only after summary success.

## Risks and mitigations

### 小红书 scraping reliability

Risk: actor availability or anti-bot failures.

Mitigation: keep actor choice swappable behind `apify_client.py`; save raw data; use conservative caps.

### Weak date extraction

Risk: posts say “this weekend” or “tomorrow” without clear reference.

Mitigation: resolve relative dates using post time and Asia/Shanghai timezone; reject when ambiguous.

### Duplicate events from multiple posts

Risk: same event promoted by many accounts.

Mitigation: event key based on name/date/location, not URL.

### Translation errors

Risk: proper nouns get over-translated.

Mitigation: explicit translation prompt/policy; keep original title and venue fields visible.

### False positives from lifestyle posts

Risk: restaurants, shops, or travel guides appear as events.

Mitigation: require event-like date/time + activity category + location.

## Recommended defaults, unless changed

- Project slug: `xhs-city-events-daily`
- Cities: 北京, 天津
- Timezone: Asia/Shanghai
- Upcoming window: all future events in v0
- v1 reminder: re-surface events inside the same daily summary when they enter the 14-day window if first seen earlier
- Categories: broad event coverage excluding family/kids
- Paid/promotional real events: include
- Recurring events: each date separately
- Schedule: 08:00 Asia/Shanghai daily
- Delivery: write markdown first; send to confirmed chat via OpenClaw cron after implementation approval
- Initial Apify cap: low-cost exploratory run, then daily cap based on observed actor pricing
