# Harborline CRM API

平台提供两类接口：

- `/api/v1/*`：给外部表单、采集工具或其他系统导入线索和资料。
- `/api/worker/*`：给本机网站制作窗口领取任务和回传制作状态。

CRM 生产地址：`https://www.bportai.com`

客户预览地址：`https://{site_slug}.evoranta.com/`

## 认证

外部导入接口使用 API Key。请求头可使用下面任一种形式：

```http
X-API-Key: YOUR_IMPORT_API_KEY
```

或：

```http
Authorization: Bearer YOUR_IMPORT_API_KEY
```

制作窗口使用单独的请求头：

```http
X-Worker-Token: YOUR_WORKER_TOKEN
```

两种密钥不能混用。密钥只放在调用方的环境变量中，不要写进前端页面。

## 统一返回格式

成功返回 `data`：

```json
{
  "data": {},
  "task": null
}
```

失败返回：

```json
{
  "error": "资料还不完整，无法创建制作任务",
  "missing": ["至少 5 张菜品图片"]
}
```

常见状态码：`201` 创建成功，`400` 参数或资料不完整，`401` 密钥无效，`404` 资源不存在，`409` `external_ref` 重复。

## 导入一条线索和完整资料

```http
POST /api/v1/leads
Content-Type: application/json
X-API-Key: YOUR_IMPORT_API_KEY
```

请求示例：

```json
{
  "external_ref": "sales-2026-00017",
  "restaurant_name": "Morrow Kitchen",
  "site_slug": "morrow-kitchen",
  "contact": {
    "name": "Jamie Lee",
    "phone": "+1 212 555 0198",
    "whatsapp": "+1 212 555 0198",
    "email": "jamie@example.com"
  },
  "location": {
    "country": "United States",
    "city": "New York",
    "address": "12 Mercer Street, New York, NY",
    "maps_url": "https://maps.google.com/?q=12+Mercer+Street",
    "timezone": "America/New_York"
  },
  "content": {
    "language": "Spanish",
    "cuisine": "Modern American",
    "summary": "Seasonal cooking in a warm downtown room.",
    "story": "A neighborhood kitchen built around local produce.",
    "highlights": "Seasonal plates | Open kitchen | Late supper",
    "hours": {"Mon-Thu": "11:30-22:00", "Fri-Sat": "11:30-23:00", "Sun": "Closed"},
    "menu": [
      {"name": "Roasted carrots", "description": "Citrus, herbs", "price": "$14"}
    ]
  },
  "design": {
    "style": "Warm modern",
    "notes": "Lead with the food and make Reserve a Table visible above the fold."
  },
  "reservation": {"enabled": true, "mode": "request"},
  "sales": {
    "source": "Instagram",
    "assigned_to": "Alex",
    "next_follow_up": "2026-09-28",
    "sales_notes": "Send the first preview after the build is ready."
  },
  "assets": [
    {
      "category": "logo",
      "filename": "morrow-logo.png",
      "mime": "image/png",
      "data_base64": "BASE64_DATA",
      "alt_text": "Morrow Kitchen logo"
    },
    {
      "category": "storefront",
      "filename": "storefront.jpg",
      "mime": "image/jpeg",
      "data_base64": "BASE64_DATA"
    }
  ],
  "create_build_task": false,
  "acceptance_criteria": "Mobile layout; call, directions and reservation actions; no placeholder assets."
}
```

`restaurant_name` 是唯一必填的线索字段。图片可以随导入请求一起传入，也可以先导入线索，再用素材接口逐张上传。`external_ref` 用于防止外部系统重复提交同一条线索；再次使用相同值会返回 `409`。

`content.language` 是制作窗口的主语言。例如填写 `Spanish`、`Italian` 或 `西语` 时，任务包会自动生成 `production.primary_language` 和 `production.languages: ["Spanish", "English"]`（或对应语言）。当语言字段仍是默认 `English` 时，平台会优先采用任务备注中的“西语站”或 `Spanish site`，没有备注时再根据国家推断当地语言。任务的 `acceptance_criteria` 中出现的语言指令优先级最高。除英语本身外，所有网站任务都要求当地语言 + English，并在网站中提供可见的语言切换入口。

制作窗口还必须先用真实餐厅素材生成 `design/reference-board.png`，再用 ImageGen 定向优化实际使用的图片并保存到 `assets/optimized/`，然后按 `design/design-spec.md` 复现真实网站。只有这些文件、`design/asset-plan.json`、`BUILD_NOTES.md` 和网站成品都存在时，桥接器才会上传。

## 分步上传素材

```http
POST /api/v1/leads/{lead_id}/assets
Content-Type: application/json
X-API-Key: YOUR_IMPORT_API_KEY
```

```json
{
  "category": "dishes",
  "filename": "signature-pasta.jpg",
  "mime": "image/jpeg",
  "data_base64": "BASE64_DATA",
  "caption": "Signature pasta",
  "alt_text": "Signature pasta with herbs",
  "sort_order": 1
}
```

图片分类建议使用：`logo`、`storefront`、`interior`、`dishes`、`menu`、`drinks`、`team`、`references`。单张素材最大 `18MB`。

## 查询线索

```http
GET /api/v1/leads/{lead_id}
X-API-Key: YOUR_IMPORT_API_KEY
```

查询全部线索：

```http
GET /api/v1/leads?stage=build_pending
X-API-Key: YOUR_IMPORT_API_KEY
```

## 提交网站制作任务

提交前平台会检查素材是否齐全：Logo、门头图片、至少 2 张室内图片、至少 5 张菜品图片，以及菜单文件或菜单图片。

```http
POST /api/v1/leads/{lead_id}/build-tasks
Content-Type: application/json
X-API-Key: YOUR_IMPORT_API_KEY
```

```json
{
  "task_type": "new_demo",
  "acceptance_criteria": "首页有电话、Google Maps、订位入口；移动端可用。"
}
```

如果想在导入资料后立即创建任务，也可以在 `POST /api/v1/leads` 中设置 `create_build_task: true`。

## 更新销售状态和跟进记录

更新销售状态、负责人或下次跟进时间：

```http
PATCH /api/v1/leads/{lead_id}
Content-Type: application/json
X-API-Key: YOUR_IMPORT_API_KEY
```

```json
{
  "sales": {
    "sales_stage": "demo",
    "assigned_to": "Alex",
    "next_follow_up": "2026-09-30",
    "sales_notes": "客户已看过预览，等待报价。"
  }
}
```

记录一次电话、演示或客户反馈：

```http
PATCH /api/v1/leads/{lead_id}/activities
Content-Type: application/json
X-API-Key: YOUR_IMPORT_API_KEY
```

```json
{
  "activity_type": "call",
  "content": "已通过 WhatsApp 发送样板网站。"
}
```

## 查询制作任务

```http
GET /api/v1/tasks/{task_id}
X-API-Key: YOUR_IMPORT_API_KEY
```

## 制作窗口接口

本机制作程序使用 `X-Worker-Token`：

```http
GET /api/worker/tasks/next
X-Worker-Token: YOUR_WORKER_TOKEN
```

有任务时，平台会原子地把它标记为 `building`，并返回冻结后的餐厅资料、制作要求和素材清单。没有任务时返回：

```json
{"task": null}
```

### 上传制作完成的网站文件

制作窗口完成本地检查后，先把静态网站 ZIP 上传到平台，再回传 `review` 状态：

```http
POST /api/worker/tasks/{task_id}/site
Content-Type: application/zip
X-Worker-Token: YOUR_WORKER_TOKEN
```

ZIP 根目录必须直接包含 `index.html`，其余 CSS、JavaScript 和图片可以使用相对目录。平台会把文件保存到该餐厅的二级域名目录；上传成功后，客户预览地址 `https://{site_slug}.evoranta.com/` 会直接提供这套文件，`/preview/{site_slug}/` 只作为内部兼容入口。单个文件包最大 64MB，解压后最大 80MB。上传接口会拒绝目录穿越、符号链接和缺少 `index.html` 的文件包。

上传示例（PowerShell）：

```powershell
Compress-Archive -Path .\site\* -DestinationPath .\site.zip -Force
Invoke-WebRequest `
  -Uri "https://www.bportai.com/api/worker/tasks/17/site" `
  -Method Post `
  -Headers @{ "X-Worker-Token" = $env:CRM_WORKER_TOKEN } `
  -ContentType "application/zip" `
  -InFile .\site.zip
```

制作完成后回传：

```http
POST /api/worker/tasks/{task_id}/status
Content-Type: application/json
X-Worker-Token: YOUR_WORKER_TOKEN
```

```json
{
  "status": "review",
  "preview_url": "https://morrow-kitchen.evoranta.com/",
  "output_note": "Local Codex finished the first website preview."
}
```

制作状态可以是：`queued`、`building`、`review`、`published`、`failed`。回传 `review` 后，CRM 会把销售状态更新为“已演示”，销售可以直接打开预览地址。

## 生产环境变量

服务器端需要设置：

```text
CRM_ADMIN_PASSWORD=后台登录密码
CRM_IMPORT_API_KEY=外部导入密钥
CRM_WORKER_TOKEN=本机制作窗口密钥
```

所有接口请求都应使用 HTTPS。图片和资料属于线索数据，调用方不要把 API Key 暴露在浏览器前端代码中。
