diff --git a/.env.example b/.env.example index ae6d434..19f50d4 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,5 @@ DB_NAME=aiameiba_com_new DB_USER=admin DB_PASSWORD=填入本地密码 ADMIN_INITIAL_PASSWORD=首次部署时设置的管理员初始密码 +CONTENT_INGEST_TOKEN= +PUBLIC_SITE_URL=https://www.aiameiba.com diff --git a/docs/CONTENT_INGEST_API.md b/docs/CONTENT_INGEST_API.md new file mode 100644 index 0000000..8376488 --- /dev/null +++ b/docs/CONTENT_INGEST_API.md @@ -0,0 +1,160 @@ +# 阿美巴 AI 官网内容发布 API + +用于“阿美巴内容判断引擎”将已经人工确认的终稿发布到 AI 官网知识中心。网站只保存和展示终稿,不会调用 AI 改写正文。 + +## 发布目标配置 + +```text +base_url: https://www.aiameiba.com +health_path: /api/internal/content/v1/health +upsert_path: /api/internal/content/v1/articles/upsert +auth_type: bearer +token_environment_name: CONTENT_INGEST_TOKEN +site_profile: ai +public_url_rule: https://www.aiameiba.com/knowledge/{slug-or-content_id} +``` + +服务端环境变量: + +```dotenv +CONTENT_INGEST_TOKEN=请设置独立的长随机密钥 +PUBLIC_SITE_URL=https://www.aiameiba.com +``` + +`CONTENT_INGEST_TOKEN` 只能存放在网站服务端环境变量中,不能放进前端环境变量、请求日志或 Git。 + +## 健康检查 + +```http +GET /api/internal/content/v1/health +Authorization: Bearer +``` + +成功响应: + +```json +{ + "status": "ok", + "service": "amaba-ai-website", + "site_profile": "ai", + "api_version": "v1" +} +``` + +## 新增或更新文章 + +```http +POST /api/internal/content/v1/articles/upsert +Authorization: Bearer +Content-Type: application/json +Idempotency-Key: ::ai +``` + +示例: + +```json +{ + "request_id": "publish-request-001", + "source_system": "amaba-content-core", + "source_task_id": "task-001", + "source_version_id": "version-002", + "source_version_number": 2, + "content_hash": "09d7e1cd4d3f8b3d3748bb8b4b3d6d4df97195d45d3e15f1d47dd38614c0fa11", + "site_profile": "ai", + "publish_mode": "publish", + "article": { + "title": "企业如何开始 AI 进化", + "body_plain": "第一自然段。\n\n第二自然段。", + "summary": "面向企业经营者的 AI 进化起步方法。", + "slug": "how-to-start-ai-evolution", + "primary_keyword": "企业AI进化", + "keywords": ["企业AI", "企业进化"], + "content_type": "methodology", + "direction": "方法论", + "target_audience": "企业经营者与管理者", + "author_name": "阿美巴", + "cover_image_url": null, + "canonical_url": "https://www.ameiba.cn/insights/how-to-start-ai-evolution" + }, + "quality": { + "human_confirmed": true, + "confirmed_at": "2026-09-11T10:00:00+08:00", + "prompt_version": "AmAba-Audit-V1.2", + "judgment_version": "AmAba-Judgment-V1.1", + "model": "approved-model", + "remaining_issue_levels": ["P3"] + }, + "provenance": { "knowledge_refs": [] }, + "semantic": { + "entities": ["阿美巴", "企业进化"], + "questions": ["企业应该如何开始AI进化?"], + "facts": [], + "suggested_schema_type": "Article" + } +} +``` + +首次成功发布返回 `201 Created`,更新返回 `200 OK`: + +```json +{ + "success": true, + "publication_id": "官网文章ID", + "status": "published", + "url": "https://www.aiameiba.com/knowledge/how-to-start-ai-evolution", + "revision": 1, + "content_hash": "09d7e1cd4d3f8b3d3748bb8b4b3d6d4df97195d45d3e15f1d47dd38614c0fa11", + "published_at": "2026-09-11T02:00:00.000Z" +} +``` + +## 校验与限制 + +- `source_system` 必须为 `amaba-content-core`,`site_profile` 必须为 `ai`。 +- `quality.human_confirmed` 必须为 `true`;`publish_mode` 仅支持 `draft` 和 `publish`。 +- 标题最长 255 字符,摘要最长 1,200 字符,正文最长 200,000 字符;数组最多 50 项。 +- 正文以纯文本保存并按空行渲染为段落,页面不会执行正文 HTML。 +- `canonical_url` 可指向传统官网的同文 URL;AI 官网仍会保留可访问正文。 +- 单个 IP 每分钟最多 30 次内部接口请求。 + +## 幂等与更新规则 + +- `Idempotency-Key` 必须唯一。相同键且相同 `content_hash` 会返回同一文章,不重复创建。 +- 相同键但不同哈希返回 `409 CONTENT_CONFLICT`。 +- 同一 `source_system + source_task_id + ai` 的新确认版本更新原文章,保持原详情 URL,并将 `revision` 加一。 +- 首次发布使用请求中的合法 `slug`;后续更新不会改变已有 URL。 + +## 错误响应 + +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "message": "字段不合法", + "retryable": false + } +} +``` + +| HTTP | code | 含义 | +| --- | --- | --- | +| 400 | `VALIDATION_ERROR` | 请求字段不合法 | +| 401 | `UNAUTHORIZED` | Token 缺失或错误 | +| 409 | `CONTENT_CONFLICT` | 幂等键哈希冲突或 slug 冲突 | +| 413 | `CONTENT_TOO_LARGE` | 正文或数组超出限制 | +| 429 | `RATE_LIMITED` | 请求频率超限 | +| 503 | `SERVICE_UNAVAILABLE` | 网站未配置发布 Token | +| 500 | `INTERNAL_ERROR` | 临时服务故障,可重试 | + +## 公开读取 + +不需要 Token: + +```text +GET /api/public/articles +GET /api/public/articles/{content_id-or-slug} +GET /knowledge/{content_id-or-slug} +``` + +仅返回已发布文章;不暴露任务 ID、审核模型、提示词、来源内部日志或 Token。 diff --git a/frontend/src/app/knowledge/[identifier]/page.tsx b/frontend/src/app/knowledge/[identifier]/page.tsx new file mode 100644 index 0000000..f597ac1 --- /dev/null +++ b/frontend/src/app/knowledge/[identifier]/page.tsx @@ -0,0 +1,75 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { articleUrl, getPublishedArticle } from '@/lib/articles'; + +const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || process.env.PUBLIC_SITE_URL || 'https://www.aiameiba.com').replace(/\/$/, ''); + +const formatDate = (value: string) => { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString('zh-CN'); +}; + +const paragraphs = (body: string) => body.split(/\n\s*\n/).map(item => item.trim()).filter(Boolean); + +export async function generateMetadata({ params }: { params: { identifier: string } }): Promise { + const article = await getPublishedArticle(params.identifier); + if (!article) return { title: '文章不存在' }; + const localUrl = `${siteUrl}${articleUrl(article)}`; + const canonical = article.canonical_url || localUrl; + return { + title: article.title, + description: article.summary, + alternates: { canonical }, + openGraph: { + type: 'article', + title: article.title, + description: article.summary, + url: canonical, + images: article.cover_image_url ? [{ url: article.cover_image_url }] : undefined, + publishedTime: article.published_at, + modifiedTime: article.updated_at, + authors: [article.author_name], + }, + }; +} + +export default async function KnowledgeArticlePage({ params }: { params: { identifier: string } }) { + const article = await getPublishedArticle(params.identifier); + if (!article) notFound(); + const localUrl = `${siteUrl}${articleUrl(article)}`; + const canonical = article.canonical_url || localUrl; + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'Article', + headline: article.title, + description: article.summary, + url: canonical, + mainEntityOfPage: localUrl, + author: { '@type': 'Organization', name: article.author_name }, + publisher: { '@type': 'Organization', name: '阿美巴AI', url: siteUrl }, + datePublished: article.published_at, + dateModified: article.updated_at, + image: article.cover_image_url || undefined, + articleSection: article.category, + }; + + return ( +
+ +
+