项目文件夹

文件
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

179 行
8.3 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Reliable HTTP Messages API (v3)
## Overview
Replace Socket.IO-based message read/write with simple HTTP endpoints optimized for CLI usage. The new v3 API provides:
- **Cursor-based message reading** using the existing `seq` field — fetch from start, then poll for new messages after last known seq
- **Batch message sending** — CLI buffers messages locally and sends them in a single HTTP POST
- **Guaranteed order** — seq is allocated atomically per session, messages always returned in seq order
- **Reliable delivery** — no messages lost; client can always catch up by polling with last known seq
This is server-side only. CLI client migration will happen separately. Existing Socket.IO message flow remains fully functional (backward compatibility). The plan is to replace Socket.IO with SSE later.
## Context (from discovery)
- **Message storage**: `SessionMessage` table with per-session `seq` (allocated via `allocateSessionSeq` in `storage/seq.ts`)
- **Deduplication**: `localId` field with `@@unique([sessionId, localId])` constraint
- **Current read endpoint**: `GET /v1/sessions/:id/messages` — limited to 150, no cursor, ordered by `createdAt desc`
- **Current write**: Socket.IO `message` event in `socket/sessionUpdateHandler.ts` — single message at a time with `AsyncLock`
- **Event broadcasting**: `eventRouter.emitUpdate()` sends `new-message` updates to Socket.IO clients
- **Existing cursor pattern**: `GET /v2/sessions` uses ID-based cursor — we'll follow a similar pattern but use `seq`
- **Existing batch pattern**: KV store `POST /v1/kv` does atomic batch mutations — good reference
## Development Approach
- **Testing approach**: Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- **CRITICAL: every task MUST include new/updated tests**
- **CRITICAL: all tests must pass before starting next task**
- **CRITICAL: update this plan file when scope changes during implementation**
- Maintain backward compatibility with existing Socket.IO message flow
## API Design
### Read Messages: `GET /v3/sessions/:sessionId/messages`
**Query Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `after_seq` | number | 0 | Return messages with seq > after_seq |
| `limit` | number | 100 | Max messages to return (1-500) |
**Response:**
```json
{
"messages": [
{
"id": "cuid",
"seq": 1,
"content": { "t": "encrypted", "c": "base64..." },
"localId": "optional-dedup-id",
"createdAt": 1234567890,
"updatedAt": 1234567890
}
],
"hasMore": true
}
```
**Behavior:**
- Messages ordered by `seq ASC` (oldest first, natural reading order)
- Client stores the highest `seq` received, polls with `after_seq=<lastSeq>` to get new messages
- `hasMore=true` means there are more messages beyond the returned batch — fetch again with `after_seq` set to last message's seq
- Initial load: `after_seq=0` fetches from the very beginning
- Catch-up: `after_seq=<lastKnownSeq>` fetches only new messages
- Uses existing index `@@index([sessionId, seq])` for efficient queries
### Send Messages: `POST /v3/sessions/:sessionId/messages`
**Request Body:**
```json
{
"messages": [
{
"content": "base64-encrypted-content",
"localId": "client-generated-dedup-id"
}
]
}
```
**Constraints:**
- Max 100 messages per batch
- Each message must have a `localId` for deduplication
- `content` is the base64-encoded encrypted message (same format as Socket.IO `message` event)
**Response:**
```json
{
"messages": [
{
"id": "cuid",
"seq": 5,
"localId": "client-dedup-id",
"createdAt": 1234567890,
"updatedAt": 1234567890
}
]
}
```
**Behavior:**
- Messages are created atomically — all succeed or all fail
- `seq` numbers are allocated sequentially within the session
- Duplicate `localId` messages are skipped (idempotent) — their existing record is returned
- After persisting, emits `new-message` updates via Socket.IO eventRouter for backward compatibility
- Returns all messages (including deduplicated ones) with their seq numbers
## Progress Tracking
- Mark completed items with `[x]` immediately when done
- Add newly discovered tasks with prefix
- Document issues/blockers with ⚠️ prefix
## Implementation Steps
### Task 1: Add cursor-based message read endpoint
- [x] Create `GET /v3/sessions/:sessionId/messages` route in a new `v3SessionRoutes.ts` file
- [x] Add Zod schema for query params: `after_seq` (number, default 0), `limit` (number, 1-500, default 100)
- [x] Verify session belongs to authenticated user (same pattern as v1)
- [x] Query `SessionMessage` where `sessionId` and `seq > after_seq`, order by `seq ASC`, take `limit + 1`
- [x] Return messages array + `hasMore` boolean (based on whether limit+1 rows returned)
- [x] Register route in `api.ts`
- [x] Write tests for read endpoint: basic fetch, cursor pagination, empty session, limit bounds
- [x] Write tests for edge cases: invalid session, unauthorized access, after_seq beyond latest
- [x] Run tests — must pass before next task
### Task 2: Add batch message send endpoint
- [x] Add `POST /v3/sessions/:sessionId/messages` route in `v3SessionRoutes.ts`
- [x] Add Zod schema for body: `messages` array (max 100), each with `content` (string) and `localId` (string)
- [x] Verify session belongs to authenticated user
- [x] For each message in the batch: check if `localId` already exists (dedup), skip if so
- [x] Allocate seq numbers sequentially for new messages using `allocateSessionSeq`
- [x] Create all new messages in DB
- [x] Emit `new-message` updates via `eventRouter.emitUpdate()` for each new message (backward compat with Socket.IO clients)
- [x] Return all messages (new + deduplicated) with their seq/id/timestamps
- [x] Write tests for batch send: single message, multiple messages, deduplication via localId
- [x] Write tests for edge cases: empty batch, exceeds 100 limit, invalid session, partial dedup (some new, some existing)
- [x] Run tests — must pass before next task
### Task 3: Verify acceptance criteria
- [x] Verify cursor-based reading works end-to-end: fetch from seq 0, paginate, catch up
- [x] Verify batch sending works: messages get sequential seq numbers, dedup works
- [x] Verify backward compatibility: sent messages trigger Socket.IO `new-message` updates
- [x] Verify order guarantee: messages always returned in seq order
- [ ] Run full test suite
- [ ] Run linter — all issues must be fixed
⚠️ Full package test suite currently fails on an existing unrelated fixture issue: `sources/storage/processImage.spec.ts` expects `sources/storage/__testdata__/image.jpg` which is missing in this workspace.
⚠️ No lint script is defined in `packages/happy-server/package.json`, so linter execution is currently not available.
## Technical Details
### Seq Allocation for Batches
The existing `allocateSessionSeq` increments by 1. For batch sends, we need N sequential seq numbers. Two options:
1. **Call `allocateSessionSeq` N times** — simple, uses existing code, but N DB roundtrips
2. **New `allocateSessionSeqBatch(sessionId, count)` function** — single `UPDATE sessions SET seq = seq + N` returning the new seq, then assign `(newSeq - N + 1)` through `newSeq`
Option 2 is preferred — single DB roundtrip regardless of batch size.
### Deduplication Strategy
For batch sends with mixed new/existing messages:
1. Query all existing messages with matching `localId` values in a single query
2. Filter out already-existing messages
3. Only create new messages and allocate seq for them
4. Return combined results (existing + newly created) sorted by seq
### Database Index Usage
- Read endpoint uses existing `@@index([sessionId, seq])` — efficient range scan
- Dedup lookup uses existing `@@unique([sessionId, localId])` — efficient point lookups
## Post-Completion
**CLI Migration (separate effort):**
- Update `happy-agent` to use `POST /v3/sessions/:id/messages` instead of Socket.IO `message` event
- Update `happy-agent` to poll `GET /v3/sessions/:id/messages` for receiving messages
- Eventually replace polling with SSE for real-time delivery
**SSE Migration (future):**
- Add `GET /v3/sessions/:id/messages/stream` SSE endpoint
- Client connects with `Last-Event-ID` = last seq for catch-up
- Replaces polling for real-time message delivery