前台投稿页改为沉浸式创作工作台:Gutenberg 负责区块解析与序列化, React + TypeScript 负责工作台外壳与发布抽屉,Jio 服务端继续作为权限、 审核与付费的唯一权威。 - 新增 assets/creator-studio 独立构建(wp-scripts,锁定 WP 6.4 时代包), 仅在投稿模板加载,构建缺失或游客访问时回退原有 TinyMCE 表单 - 自动保存(4s 防抖 + rev 冲突 409)、本地快照恢复、发布配置抽屉、 移动端全屏单列与安全区适配 - 图片插入走现有 front-submit 上传管线,保留能力校验与待认领生命周期 - 已发布文章的改稿写入私有版本,审核通过前线上内容/链接/付费条款不变; 原生后台「改稿审核」一次性应用或退回,并校验源文章并发指纹 - 私有附件新表 jio_private_asset + jio_dl_token.asset_id,schema 升到 3.4.0; 文件存放 ABSPATH 之外,未配置私有存储时 fail closed - 下载令牌绑定文章/附件/用户/IP/UA/次数,每次交付复核 Visibility 的 订单、VIP 与配额判定;中断传输归还次数 - 付费字段改为写入前校验,非法价格不再留下半保存文章 - 新增 creator-studio 契约测试;更新受影响的 schema/下载/投稿契约 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
80 lines
2.1 KiB
TypeScript
80 lines
2.1 KiB
TypeScript
/**
|
|
* Creator Studio 的 REST 访问层。
|
|
*
|
|
* 所有请求都带 `X-WP-Nonce`;服务端仍以 Jio 能力矩阵与文章归属做最终判定。
|
|
*
|
|
* @package Jio
|
|
*/
|
|
|
|
export type ApiError = Error & { status?: number; code?: string; data?: unknown };
|
|
|
|
function toError(payload: any, status: number): ApiError {
|
|
const message =
|
|
(payload && (payload.message || payload.error)) ||
|
|
(status === 401
|
|
? '登录状态已失效,请刷新页面重新登录。'
|
|
: '请求失败,请稍后重试。');
|
|
const error = new Error(String(message)) as ApiError;
|
|
error.status = status;
|
|
error.code = payload && payload.code ? String(payload.code) : '';
|
|
error.data = payload;
|
|
return error;
|
|
}
|
|
|
|
async function readJson(response: Response): Promise<any> {
|
|
try {
|
|
return await response.json();
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function apiGet(url: string, nonce: string): Promise<any> {
|
|
const response = await fetch(url, {
|
|
credentials: 'same-origin',
|
|
headers: { 'X-WP-Nonce': nonce },
|
|
});
|
|
const payload = await readJson(response);
|
|
if (!response.ok) {
|
|
throw toError(payload, response.status);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
export async function apiPost(url: string, body: Record<string, unknown>, nonce: string): Promise<any> {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-WP-Nonce': nonce,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
const payload = await readJson(response);
|
|
if (!response.ok) {
|
|
throw toError(payload, response.status);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
export async function apiUpload(url: string, file: File, nonce: string, fields: Record<string, string> = {}): Promise<any> {
|
|
const body = new FormData();
|
|
body.append('file', file);
|
|
Object.entries(fields).forEach(([key, value]) => body.append(key, value));
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'X-WP-Nonce': nonce },
|
|
body,
|
|
});
|
|
const payload = await readJson(response);
|
|
if (!response.ok) {
|
|
throw toError(payload, response.status);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
export const wpJson = (value: unknown): string => JSON.stringify(value);
|