# CoolMe Agent Blog — Hướng dẫn đồng bộ cho Grok Bot

> Hub Blog + Ideas + Resources: [`docs/agent-content.md`](./agent-content.md) · `GET /api/agent/docs`

## Trước khi gọi

1. User tạo/rotate key tại **Admin → Cài đặt → Agent Content API**
2. Nhận key qua secret channel (không commit git)
3. `GET /api/agent/blog/health` → cần `keyConfigured=true`, `r2=true`
4. Đọc lại file này: `GET /api/agent/blog/docs`

Base URL: `https://www.coolme.vn` (hoặc staging).

## Auth

```http
Authorization: Bearer <AGENT_BLOG_API_KEY>
```

Hoặc header: `x-agent-blog-key: <AGENT_BLOG_API_KEY>`

Optional: `Idempotency-Key: <uuid>` trên POST upsert/bulk/upload (replay 24h).

## Thumbnail / featured — quan trọng

| Luồng | Có tự upload thumbnail? |
|-------|-------------------------|
| `POST /posts/upsert` | **Không.** Chỉ set nếu gửi `coverUrl` (URL public/R2 đã có). |
| `POST /posts/:slug/featured` | **Có** — upload file/base64 → R2 → `featured_image_url`. |
| `POST /generate` | **Không** — trả `coverUrl: null` + `coverPrompt`. |
| Admin ZIP import | **Có** nếu ảnh cùng stem với MD **hoặc** YAML `featured_image`. |

**Bot phải luôn 2 bước:** (1) upsert draft → (2) `POST .../featured`.  
Upsert **không** nhận file ảnh. Ảnh trong body markdown (`![](../images/...)`) **chưa** được rewrite lên R2 trong v1.

## Publish & Edit (Bot)

| Mục tiêu | API |
|----------|-----|
| Tạo/cập nhật **draft** | `POST /posts/upsert` (mặc định) |
| Tạo/cập nhật + **publish ngay** | `POST /posts/upsert` + `"publishNow": true` (hoặc `"status":"published"`) |
| **Lên lịch** | `POST /posts/upsert` + `"scheduledAt": "2026-09-20T09:00:00+07:00"` |
| Đổi / đặt lịch bài có sẵn | `POST /posts/:slug/schedule` `{ "scheduledAt": "..." }` |
| Lên lịch nhiều bài (≤10) | `POST /posts/schedule-many` `{ "items": [{ "slug", "scheduledAt" }] }` |
| Publish draft đến hạn | `POST /publish-due` (và tự chạy khi Bot gọi API blog khác) |
| Publish **draft bất kỳ** (1 bài) | `POST /posts/:slug/publish` |
| Publish **nhiều draft** (≤10) | `POST /posts/publish-many` `{ "slugs": ["a","b"] }` |
| **Sửa live** bài đang published | `PUT` / `PATCH /posts/:slug` → tự refresh snapshot + Turso |
| Đổi thumbnail bài published | `POST /posts/:slug/featured` → tự refresh snapshot |

Response publish/upsert luôn có `publicUrl`, `tursoMirrorOk` (nếu mirror fail, public vẫn có thể fallback Supabase).

`/generate*` **vẫn draft** — muốn lên live phải `publishNow` / `/publish` / `/publish-many` / lịch + `publish-due`.

## Lên lịch (scheduledAt) — quan trọng cho Bot

- Field: `scheduledAt` | `scheduleAt` | `scheduledFor` (ISO 8601, **nên kèm timezone** vd `+07:00`)
- Upsert + `scheduledAt` **tương lai** → `status=scheduled`, `published_at` = giờ hẹn, **chưa** lên `/blog`
- `scheduledAt` ≤ now hoặc `publishNow` → publish ngay
- **Không có Vercel cron** — bài đến hạn được publish khi:
  1. Bot gọi **bất kỳ** Agent Blog API đã auth (drain opportunistic ~45s/lần), hoặc
  2. Bot gọi tường minh `POST /api/agent/blog/publish-due`
- Sau schedule hàng loạt: **nên gọi `publish-due`** (hoặc gọi lại API sau giờ hẹn)

### Curl schedule

```bash
# Upsert + lên lịch
curl -s "$BASE/api/agent/blog/posts/upsert" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "slug": "bai-len-lich",
    "title": "Bai len lich",
    "contentMarkdown": "## Mo dau\n\nNoi dung...",
    "scheduledAt": "2026-09-20T09:00:00+07:00"
  }'

# Doi lich bai da co
curl -s -X POST "$BASE/api/agent/blog/posts/bai-len-lich/schedule" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"scheduledAt":"2026-09-21T08:30:00+07:00"}'

# Nhieu bai
curl -s "$BASE/api/agent/blog/posts/schedule-many" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"items":[{"slug":"a","scheduledAt":"2026-09-20T09:00:00+07:00"},{"slug":"b","scheduledAt":"2026-09-20T10:00:00+07:00"}]}'

# Publish tat ca bai da den han
curl -s -X POST "$BASE/api/agent/blog/publish-due" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"limit":20}'
```

YAML pack: map `date` / `scheduled_at` → `scheduledAt`.

## Quy trình sync 1 bài (khuyến nghị)

1. `POST /posts/upsert` — markdown + meta  
2. `POST /posts/:slug/featured` — thumbnail (nếu cần)  
3. Publish: `"publishNow": true` lúc upsert **hoặc** `POST .../publish` / `publish-many`  
4. Dùng `publicUrl` trong response

### 404 trên /blog/{slug} sau publish?

Admin: `POST /api/admin/articles/remirror-turso` + `repair-snapshots` nếu cần.

## Quy trình sync pack (vd 36 bài)

- `POST /posts/bulk-upsert` ≤10 / lần (`publishNow` optional)  
- Featured từng slug  
- Hoặc giữ draft rồi `POST /posts/publish-many` khi sẵn sàng

## Field mapping YAML → API

| YAML / pack | API JSON |
|-------------|----------|
| title | title |
| h1 | h1 |
| slug | slug |
| focus_keyword | focusKeyword |
| excerpt | excerpt |
| seo_title | seoTitle |
| seo_description | seoDescription |
| cta_product | ctaProduct |
| body (sau frontmatter) | contentMarkdown |
| status / publishNow | mặc định draft; `publishNow:true` hoặc `status:published` → publish |
| date / scheduled_at | → `scheduledAt` (ISO + timezone) |
| featured_image | upload riêng qua `/featured` |

## Ví dụ curl

```bash
BASE=https://www.coolme.vn
KEY=cmb_live_...

# health (public)
curl -s "$BASE/api/agent/blog/health"

# docs (public markdown)
curl -s "$BASE/api/agent/blog/docs"

# upsert draft
curl -s "$BASE/api/agent/blog/posts/upsert" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: demo-upsert-1" \
  -d '{
    "slug": "tu-dong-dang-bai-da-kenh",
    "title": "Tự động đăng bài đa kênh",
    "h1": "Tự động đăng bài đa kênh với CoolMe",
    "excerpt": "Hướng dẫn ngắn.",
    "contentMarkdown": "## Giới thiệu\n\nNội dung...",
    "seoTitle": "Tự động đăng bài đa kênh",
    "seoDescription": "Cách lên lịch và đăng bài đa kênh hiệu quả với CoolMe AI.",
    "focusKeyword": "đăng bài đa kênh",
    "ctaProduct": "/automation"
  }'

# featured multipart
curl -s "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh/featured" \
  -H "Authorization: Bearer $KEY" \
  -F "file=@./cover.jpg"

# featured base64
curl -s "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh/featured" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"imageBase64":"<base64>","contentType":"image/jpeg","filename":"cover.jpg"}'

# get
curl -s "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh" \
  -H "Authorization: Bearer $KEY"

# upsert + publish ngay
curl -s "$BASE/api/agent/blog/posts/upsert" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"slug":"tu-dong-dang-bai-da-kenh","title":"Tự động đăng bài","contentMarkdown":"## Hi\\n\\nBody","publishNow":true}'

# publish nhiều draft bất kỳ
curl -s "$BASE/api/agent/blog/posts/publish-many" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"slugs":["slug-a","slug-b"]}'

# edit live (bài đã published)
curl -s -X PUT "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title":"Title mới","contentMarkdown":"## Updated\\n\\nNội dung mới"}'

# publish 1 bài
curl -s -X POST "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh/publish" \
  -H "Authorization: Bearer $KEY"

# unpublish
curl -s -X POST "$BASE/api/agent/blog/posts/tu-dong-dang-bai-da-kenh/unpublish" \
  -H "Authorization: Bearer $KEY"
```

## Downloads / Releases (phần mềm & extension)

Bot **được phép** list / cập nhật / xoá bản phát hành (cùng Bearer Blog key).

Base: `/api/agent/downloads` (không nằm dưới `/blog/`).

| Method | Việc |
|--------|------|
| `GET /api/agent/downloads` | Catalog + current release mọi sản phẩm |
| `GET /api/agent/downloads?product=fb-automate` | Chi tiết 1 sản phẩm |
| `POST /api/agent/downloads` | Phát hành / cập nhật `{ product, version, downloadUrl, size?, description?, changelog?, r2Key? }` |
| `POST` + `"promoteOnly": true` | Đặt bản trong history làm current |
| `DELETE ?product=&version=&deleteR2=1` | Xoá phiên bản (+ R2 nếu có) |

**product id:** `fb-automate` · `premiere-automation` · `video-factory` · `chrome-image-saver` · `chrome-auto-post-groups` · `wordpress-auto-seo`

```bash
# List
curl -s "$BASE/api/agent/downloads" -H "Authorization: Bearer $KEY"

# Publish / update
curl -s "$BASE/api/agent/downloads" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "product": "wordpress-auto-seo",
    "version": "1.0.1",
    "downloadUrl": "https://cdn.example.com/wp-auto-seo-1.0.1.zip",
    "size": "2.2 MB",
    "description": "Fix meta AI tiếng Việt",
    "changelog": "- Fix meta length\n- Improve chat tone"
  }'

# Xoá bản
curl -s -X DELETE "$BASE/api/agent/downloads?product=wordpress-auto-seo&version=1.0.0&deleteR2=1" \
  -H "Authorization: Bearer $KEY"
```

Landing `/coolme-desktop` và CTA sản phẩm đọc bản **current** sau khi Bot lưu (revalidate).

Upload file lên R2 vẫn qua **Admin → Downloads** (presigned); Bot chủ yếu set `downloadUrl` public.

## Categories / Tags (Bot được tạo mới)

Bot **được phép tạo** category, sub-category (`parent_id`) và tag.

| API | Việc |
|-----|------|
| `GET /categories` | List (`?tree=1` → cây cha→con) |
| `POST /categories` | Tạo `{ name, slug?, parent?, parentId?, description?, seoTitle?, seoDescription? }` |
| `GET/PATCH/DELETE /categories/:idOrSlug` | Chi tiết / sửa / xóa (`?force=1` gỡ bài rồi xóa) |
| `GET /tags` | List (`?q=`) |
| `POST /tags` | `{ name, slug? }` hoặc bulk `{ tags: ["a","b"] }` |
| `GET/DELETE /tags/:idOrSlug` | Chi tiết / xóa (+ gỡ `article_tags`) |

### Auto-create khi upsert bài

Khi `POST /posts/upsert` (và bulk):

- `tags: ["upscale", "ai ảnh"]` → tag chưa có sẽ **tự tạo** rồi gắn bài
- `category` chưa có → **tự tạo** category
- Sub-category:
  - Chuỗi: `"SEO ảnh AI > Upscale 8K"` (ưu tiên `>`)
  - Hoặc: `"categoryParent": "seo-anh-ai", "category": "upscale-8k"`

```bash
# Tạo category cha + con
curl -s "$BASE/api/agent/blog/categories" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name":"SEO ảnh AI","slug":"seo-anh-ai"}'

curl -s "$BASE/api/agent/blog/categories" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name":"Upscale 8K","slug":"upscale-8k","parent":"seo-anh-ai"}'

# Tạo tags
curl -s "$BASE/api/agent/blog/tags" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"tags":["upscale","lam-net-anh"]}'

# Upsert bài — tự tạo category/tag nếu thiếu
curl -s "$BASE/api/agent/blog/posts/upsert" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "slug": "upscale-anh-8k-huong-dan",
    "title": "Upscale ảnh 8K",
    "contentMarkdown": "## Mo dau\n\n...",
    "category": "SEO ảnh AI > Upscale 8K",
    "tags": ["upscale","ai ảnh"]
  }'
```

## Response

Success:

```json
{ "ok": true, "post": { "id": "...", "slug": "...", "status": "draft", "featuredUrl": null, "updatedAt": "..." }, "idempotentReplay": false }
```

Error:

```json
{ "ok": false, "error": { "code": "UNAUTHORIZED|VALIDATION|NOT_FOUND|CONFLICT|R2|RATE_LIMIT", "message": "..." } }
```

## Không được làm

- Auto-publish hàng loạt / publish từ `/generate*`
- Ghi đè live published snapshot trừ khi user bảo update rồi publish lại
- Gọi Supabase service role trực tiếp
- Hứa “đậu visa 100%” trong content generate
- Commit API key vào git

## Machine schema (rút gọn)

- `POST /posts/upsert` body: `{ slug, title, h1?, excerpt?, contentMarkdown?, seoTitle?, seoDescription?, focusKeyword?, ctaProduct?, category?, categoryParent?, tags?, scheduledFor?, coverUrl? }`
- `POST /posts/bulk-upsert` body: `{ posts: UpsertBody[] }` max 10
- `GET|POST /categories` · `GET|PATCH|DELETE /categories/:idOrSlug`
- `GET|POST /tags` · `GET|DELETE /tags/:idOrSlug`
- `GET|POST|DELETE /api/agent/downloads` — releases `{ product, version, downloadUrl, description?, changelog?, size?, promoteOnly? }`
- `POST /posts/:slug/featured` multipart `file` | JSON `{ imageBase64, contentType, filename? }`
- `DELETE /posts/:slug/featured`
- `POST /posts/:slug/publish` | `/unpublish`
