# FNA_V3 独立文章 API
状态:2026-10-27本地实施并联调;2026-10-29已随独立test预览提交、推送和部署,10篇教程真实API迁移通过后,test编辑接口已关闭且临时密钥移除。正式站现已切换,正式10篇教程亦通过同一API迁移;生产临时密钥用后删除,编辑接口默认关闭,后续明确写作/发布任务再按绑定环境启用并完成后关闭。test执行和回执沿[发布记录](RELEASE_PREFLIGHT_2026-10-28.md#当前门槛先test预览确认后再切正式2026-10-29),日常本地调用不受影响。用户明确采用本站独立接收 API,不经过 ATOM;不新增 ATOM 依赖。入口为 `http://fna-v3.loc/api/editorial/v1`,生产地址尚未指定。
## 职责与数据
AI负责选题、正文、图解、官方资料核对与自然内链;本站负责保存草稿、生成实际模板预览、按冻结版本发布、记录结果和回退。文章目录和关联登记仍以[写作索引](../content/WRITING.md)为准;API不自动改写该Markdown,也不自动创造反向链接。将相关旧文的修改显式放进同一批次,预览后原子发布。
项目正文源保留可编辑稿;`editorial_drafts`保存收到并渲染后的冻结版本,发布读取该快照,不临时重新读取文件。`articles`为公开显示数据,草稿不写入它。`editorial_documents`按稳定`content_key`绑定本站文章;`editorial_versions`保留已发布版本和首次接管旧文的基线版本0;`editorial_publications`保存独立发布回执及后续处理状态。图片元信息在`editorial_assets`。
新API不调用旧接收器,不发送旧Telegram通知、IndexNow提交或ATOM请求。旧资源接收接口继续沿用原流程,不在本次改造范围;普通编辑API拒绝免费节点与小火箭ID两个自动资源栏目。旧接口/人工直接编辑同一文章会造成草稿基线变化,新API发布时返回409,不能静默覆盖并行修改。
## 凭据与环境
配置:`config/editorial.php`。
- 本地默认启用,但必须先有有效凭据。生产默认关闭,需部署时明确设置`EDITORIAL_API_ENABLED`、`EDITORIAL_BASE_URL`和独立搜索索引;本次没有生产凭据或上线操作。
- 本地安装命令`php artisan editorial:setup`仅允许本机`fna_v3`,已存在凭据不覆盖。凭据在`storage/app/private/editorial/credential.json`(目录700、文件600,Git忽略),结构使用`gemdocs.credential.v1`及`fields.api_key`,绑定项目、环境和base_url。程序在内存中读取,禁止打印、复制进正文、提交Git或放到命令参数。
- 认证为`Authorization: Bearer <专用密钥>`。应用提供下述CLI代为读取和发送,不需要AI将密钥带入工具输出。
- 所有编辑接口拒绝整页缓存;预览和私有图片使用30分钟签名地址,返回`noindex, nofollow`、`private, no-store`。持有签名链接的人在有效期内可预览,链接不应公开转载;失效后通过查询草稿重新取得链接。
- 发布接口按分钟限流;API错误不输出SQL、凭据或请求头。生产认证、HTTPS与访问路线仍按项目部署约束核实,不能因代码已存在就认为生产已可调用。
## 调用方式
以下命令调用当前环境绑定的API,`path`相对于`/api/editorial/v1/`,输出JSON结构为`http_status`与`data`。
```sh
php artisan editorial:request GET catalog
php artisan editorial:request GET source-articles/362
php artisan editorial:request POST assets --image=/绝对路径/操作图.png
php artisan editorial:request POST drafts --payload=/绝对路径/draft.json
php artisan editorial:request GET drafts/草稿UUID
php artisan editorial:request POST drafts/草稿UUID/publish --payload=/绝对路径/publish.json
php artisan editorial:request GET publications/发布请求UUID
```
示例中的中文路径和UUID均需替换。请求正文放文件,不把多行正文或密钥拼接进shell。CLI关闭HTTP代理和重定向,本地模式拒绝非本地目标;外部环境的调用仍需既有访问授权。
### 1 保存草稿
`POST /drafts`:
```json
{
"request_id": "29e2f78a-7adc-4bf6-9eef-1c5f4a586b13",
"articles": [
{
"content_key": "verge-windows-start",
"expected_revision": 0,
"adopt_article_id": 362,
"slug": "clash-verge-rev-windows",
"title": "Clash Verge Rev 使用教程:从安装到第一次连接(Windows)",
"summary": "下载安装、导入订阅、选择节点和开启系统代理。",
"category_slug": "windows",
"format": "markdown",
"body": "
下载与安装
\n\n此处为实际正文。",
"cover": null,
"show_cover": false
}
]
}
```
这是结构示例,不可拿占位正文覆盖现有教程。完整正文应来自项目源文件或已核对的接收端原文。
- `request_id`为本次保存动作的新UUID;同UUID同正文重试返回同草稿,不递增版本;同UUID改正文返回409。每批最多10篇,单篇正文最多500000字符。
- `content_key`为写作索引中的稳定键。首次为0,后续`expected_revision`必须等于`GET /articles/{key}`返回的`latest_revision`,成功后递增。新草稿会使同文章的旧草稿失去发布资格,但不会改变线上已有正文。
- 新文章省略`adopt_article_id`。接管现有文章必须显式指定准确ID并沿用原slug;不因标题相似自动绑定。首次接管仅建立映射和旧版快照,不改公开正文。不能把已有键转绑另一篇文章,也不能改已绑定文章URL。
- `category_slug`必须为本站已启用的编辑栏目。`tag_ids`可选,必须是已有标签ID;省略时保留旧文标签,新文默认为空,不自动创建标签。
- `format`为`markdown`(默认)或`html`。Markdown支持基础HTML图解,但过滤危险链接、脚本等;富文本不接受任意脚本、事件属性或内联样式。段落锚点使用显式`id`。
- `cover`支持已存在的本站`/images/...`路径,或上传后的`asset:SHA256`;显式null可清除旧封面,省略时保留。`show_cover`控制正文头图,省略时保留旧值,新文默认不展示头图。
- 站内文章链接为`/a/slug.html#anchor`。本站完整域名链接不作为站内链接接受;引用公共官方资料可用外部HTTPS链接。文章目标必须已发布或在同批草稿内,锚点必须存在。分类链接校验真实父子关系,避免宽松路由把错误URL也返回200。
返回`draft_id`、`hash`、`status`及每篇文章的版本、预期路径和`preview_url`。查询草稿会生成新的短期预览链接;草稿内容不变。预览复用本站文章模板,关闭统计、点赞和热门列表;同批次文章之间的链接改为签名预览,避免跳到不存在的正式页。
### 新文章点赞设置
2026-10-29起,创建新文时支持可选`like_settings`,例如:
```json
"like_settings": {"seven_day": 12, "yearly": 50, "one_year": 200}
```
省略时采用[AI写作规范的默认参数](../content/WRITING.md#ai-新文章的点赞参数2026-10-29),在草稿保存时随机取值并冻结;草稿响应`articles[].like_settings`返回确认值。旧文改稿、接管及内容回退保留原点赞参数/进度,普通改稿显式传入此对象返回422;0可在新建时关闭对应项。旧冻结草稿不追加新默认值。
发布后鉴权`GET source-articles/{id}`的`like_growth`可核对剩余额度、持续年度参数及一次性年度目标、已支付量和开始时间。年度额度按发布时间计时,累计增加该额度后停止,与真实点赞和持续年度随机增长独立。迁移、调度、字段范围与回退见[文章自动点赞维护](ARTICLE_LIKES.md);先执行指定新增迁移,再使用新代码。
### 2 图片
`POST /assets`使用multipart的`image`字段。支持PNG/JPEG/WebP/GIF,最多8MB,并限制宽高和像素数;不接收SVG/HTML上传,不主动抓取远端图片URL。返回SHA256、尺寸和`asset:SHA256`引用:
```markdown

```
原图先在私有存储,不生成公开地址。签名预览通过临时图片路由读取;正式发布时校验哈希并生成本站`/images/editorial/SHA256.ext`地址,正文替换为此路径。上传文件无同名覆盖问题;失败批次可能留下已准备但未引用的内容寻址图片,不自动删除可能仍被其他版本使用的素材。
### 3 发布确认过的版本
`POST /drafts/{draft_id}/publish`:
```json
{
"request_id": "0cf94c73-bafb-4a6c-9cef-39d72e5bc24e",
"expected_hash": "替换成保存草稿返回的64位hash"
}
```
发布UUID与草稿UUID分开。确认所看版本后使用它的hash;不要重建另一份正文来发布。发布前再次检查最新版本、现有文章基线、栏目/标签、图片、目标链接及受影响旧文的入向锚点。批次中任一篇冲突或失效,整批文章与版本/回执事务回滚。
成功后按稳定映射创建/更新文章,保留已有URL、创建时间、计数、置顶和身份。批次里的所有正文修改同时提交;冻结发布回执与内容写入在同一数据库事务中。重复提交同一发布UUID返回原回执;同UUID换目标/版本返回409。同一草稿换一个发布UUID重试也只返回已有回执,不重复创建文章。
响应包含:
- `status: published`:本次内容已写入本站公开文章表;这是所调用环境的状态,本地成功不代表生产成功。
- `articles`:各文章的`content_key`、`article_id`、`revision`和URL。
- `effects.cache/search/sitemap`:各后续处理的独立结果。搜索暂时失败不撤销已提交正文,返回`pending`,不能重发正文来弥补搜索失败。
后续处理为刷新本站响应缓存与热门数据缓存、同步本站搜索,以及使用数据库驱动的sitemap。缓存未刷新时sitemap也标记pending。不调用搜索引擎主动提交。
### 4 查询、重试与回退
| 方法与路径 | 作用 |
| --- | --- |
| `GET /catalog` | 当前可用编辑栏目与文章元信息,含已接管键和版本 |
| `GET /source-articles/{id}` | 接管旧文前读取实际HTML、封面、标签、栏目与管理状态 |
| `GET /articles/{key}` | 最新草稿版本、已发布版本、稳定ID/路径、已发布历史列表 |
| `GET /articles/{key}/versions/{revision}` | 读取某个已发布版本的冻结内容;旧文接管基线为0 |
| `GET /drafts/{id}` | 查询保存结果,刷新预览地址 |
| `GET /publications/{id}` | 查询发布UUID的原回执和当前后续处理状态 |
| `POST /publications/{id}/synchronize` | 只重试未完成的缓存/搜索处理,不重写文章;搜索读取当前公开版本,避免旧重试覆盖新索引 |
| `POST /articles/{key}/rollback` | 从指定历史版本建立新草稿,供预览后另行发布 |
回退请求同样包含新`request_id`,并给出当前`expected_revision`和历史`target_revision`。它不会马上覆盖当前文章;返回新草稿后,沿同一预览/发布步骤恢复,版本继续递增、URL不变。若旧版锚点不能满足现有入向链接,需要将关联文的修正一起组批,不能强制制造断链。
HTTP401认证失败,403签名无效/过期,404对象不存在,409幂等/版本/所有权冲突,422格式或链接不满足条件,503功能关闭或凭据绑定无效。网络结果未知时,先按原UUID查询,不盲目生成新UUID重发。
## 搜索隔离
`Articles::searchableAs()`优先使用`editorial.search_index`。本地为独立`fna_v3_articles`,已把169篇现有公开文章初始化到新索引,未改原继承的`articles`索引。正常站内搜索和新API写入使用同一索引。`editorial:reindex`用于初始化/补全;后续每次发布只更新本批当前文章。
本地仅允许回环Meilisearch和`fna_v3_`前缀。生产必须明确专属索引及服务地址,不能沿用未知共享索引。集合/数据库搜索直接读取当前文章;未支持或未配置的驱动返回pending。Meilisearch任务等待成功后才报告done。
## 安装、验收与恢复
追加迁移:`database/migrations/2026_10_27_190000_create_editorial_tables.php`。执行时只指定此迁移,不使用`migrate:fresh`或批量重置现有数据库。后续代码部署遵守Git发布流程;本次本地安装不授权生产部署。
```sh
php artisan migrate --path=database/migrations/2026_10_27_190000_create_editorial_tables.php --force
php artisan editorial:setup
php artisan editorial:reindex
```
本地已完成:
- SQLite内存隔离功能测试:认证/环境绑定、缓存隔离、草稿私有、签名到期、幂等、版本冲突、原子发布、显式接管、并行修改保护、入向锚点、图片和代码换行、回退与搜索重试;结果见`output/editorial-api/tests.xml`。
- 真实本地HTTP:无认证401、图片上传与草稿预览、两文章互链整批发布、同UUID重试、新版、回退、正式HTTP200、真实站内搜索和sitemap;见`output/editorial-api/http-verification.json`。
- 1360/390预览截图:签名图片、同批跳转、无点赞、noindex、无横向溢出;浏览器只允许本地来源,未发线上请求。见`output/editorial-api/browser-verification.json`和同目录PNG。
- 临时文章2篇及本次测试版本/回执/图片已清理,原169篇文章完整行哈希一致。安装验收结束时接收表为空,后续正常稿件会保留版本和回执;凭据留在私有存储。
恢复时先关闭`EDITORIAL_API_ENABLED`,保留5张接收表、版本和回执及图片。已发布正文仍在`articles`可正常显示;撤销某次文章更新优先走历史版本回退。代码回退不得顺带drop表或清理可能被正式文章引用的图片。若撤回本地搜索切换,应在核对原索引后调整`EDITORIAL_SEARCH_INDEX`与模型配置,不删除未知共享索引。