Files
probably-rendering-one-more…/content/studio-tools/layouts-api.md
T
codex-bot 94932418ca
studio CI and deployment / verify-and-deploy (push) Successful in 5m55s
waterfall-wiki CI and deployment / verify-and-deploy (push) Failing after 18m23s
Connect all 27 source layouts to parameterized Studio pages and shared design storage
2026-10-27 14:51:17 +08:00

7.6 KiB
Raw Blame History

27 套排版 · Studio 参数 API 与保存教程

把这篇 post 交给 agent,即可选排版、预览、调整参数并保存可再次打开的效果。每套 ref-01…ref-27 都有独立页面,与原图的 T(文字)、I(图位)、D(装饰)构件对应。

先看效果,再通过 API 调整

{{LAYOUTS}}

以上页面都可以修改文字、图位颜色或图片、装饰颜色和显隐、纸色,也能输入完整参数 JSON。打开“显示 T/I/D 构件边界”核对位置;“原图与构件对应表”可查看原图及坐标。页面下方可找到已保存的此排版效果。

Agent 调用顺序

  1. 读取 全部 27 套排版目录与接口,选择 ID。
  2. GET /studio/api/layouts/ref-01 获取当前模板的真实构件 ID、坐标、方向与可辨识文字。也可以先读 渲染接口说明。不要把其他模板的 T/I/D ID 混用。
  3. POST 同地址传入参数,得到 SVG。渲染不会自动保存;迭代参数直到效果合适。
  4. POST /api/studio/designs 保存新效果,返回 design.id、design.url 与 design.revision。通过 design.url 可直接看效果。
  5. GET /api/studio/designs/{id} 读取上次参数;PATCH 同地址携带 revision 更新同一 ID。要保留两个不同版本,使用 POST 保存两个新 ID。

所有 API 与 Wiki 共用现有登录 Cookie 或 Bearer JWT。浏览器修改请求必须同源。Studio 本身不配置数据库连接,保存由 Wiki 写入同一个 PostgreSQL。

参数示例:ref-01

{
  "texts": {
    "T01": {"value": "STUDY", "color": "#26362f", "scale": 1, "align": "start"},
    "T02": {"value": "知识合集\n从记录到创作"}
  },
  "images": {
    "I01": {"color": "#7f9188"},
    "I02": {"color": "#b7c4bb"}
  },
  "decorations": {"D01": {"color": "#53645b", "hidden": false}},
  "paper": "#f4f2eb"
}

文字最多 500 字、8 行,scale 范围 0.25–2,align 为 start/middle/end。文字 value 为空则移除这一组;删除其参数键则恢复原图字形。图片可以只改色,也可先通过 /api/upload 的 assetOnly=true 上传,再传入 assetId UUID 与 fit: cover/contain;不接受任意图片网址。装饰支持颜色与 hidden。参数在画板内裁切,未知字段、错误构件 ID、非法颜色和超限输入返回 400。请求最大 64KB。

原始参考来自低分辨率拼图。默认效果保留原图;未替换的构件继续使用原始分层,图位使用原始形状蒙版。可替换文字生成 SVG 文字;无法辨识的小字标为 unreadable,保留原图层而不编造文字。纸色作用于原有底层,不替代图位色块。页面预览通过同站媒体读取图片;API 导出时会使用当前登录读取 Wiki 有尺寸上限的 WebP 预览,并内嵌到 SVG,使下载结果不依赖外部图片链接。原始大图仍保留在 Wiki,导出使用最长 960 像素的预览。

HTML / JavaScript:渲染与保存

<div id="preview"></div>
<script type="module">
const layoutId = 'ref-01';
const parameters = {texts:{T01:{value:'STUDY'}},images:{I01:{color:'#7f9188'}}};
const response = await fetch(`/studio/api/layouts/${layoutId}`, {
  method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(parameters)
});
if (!response.ok) throw new Error(await response.text());
const previewUrl = URL.createObjectURL(await response.blob());
const image = new Image(); image.src = previewUrl;
document.querySelector('#preview').replaceChildren(image);
// 预览完成后可 URL.revokeObjectURL(previewUrl)。
const saved = await fetch('/api/studio/designs', {
  method:'POST', headers:{'Content-Type':'application/json'},
  body:JSON.stringify({layoutId,name:'知识合集第一版',parameters})
});
if (!saved.ok) throw new Error(await saved.text());
const {design} = await saved.json();
console.log(design.id, design.url, design.revision);
</script>

React:参数变化重新预览

import {useEffect,useState} from 'react';
export function Preview(){
 const [title,setTitle]=useState('STUDY'),[src,setSrc]=useState(''),[error,setError]=useState('');
 useEffect(()=>{
  const controller=new AbortController();let url='';
  fetch('/studio/api/layouts/ref-01',{method:'POST',headers:{'Content-Type':'application/json'},
   body:JSON.stringify({texts:{T01:{value:title}}}),signal:controller.signal})
   .then(async r=>{if(!r.ok)throw Error(await r.text());return r.blob()})
   .then(blob=>{if(!controller.signal.aborted){url=URL.createObjectURL(blob);setSrc(url);setError('')}})
   .catch(e=>{if(!controller.signal.aborted)setError(e.message)});
  return()=>{controller.abort();if(url)URL.revokeObjectURL(url)};
 },[title]);
 return <><input value={title} onChange={e=>setTitle(e.target.value)}/>{error&&<p>{error}</p>}{src&&<img src={src} alt="当前排版效果"/>}</>;
}

Next.js:客户端编辑组件

// app/layout-playground/page.tsx
'use client';
import {useState} from 'react';
export default function Playground(){
 const [title,setTitle]=useState('STUDY'),[link,setLink]=useState(''),[status,setStatus]=useState('');
 async function save(){
  const response=await fetch('/api/studio/designs',{method:'POST',headers:{'Content-Type':'application/json'},
   body:JSON.stringify({layoutId:'ref-01',name:title||'排版效果',parameters:{texts:{T01:{value:title}}}})});
  const data=await response.json();if(!response.ok){setStatus(data.error);return}setLink(data.design.url);setStatus('已保存');
 }
 return <><input value={title} onChange={e=>setTitle(e.target.value)}/><button onClick={save}>保存新效果</button><p>{status}</p>{link&&<a href={link}>打开效果</a>}</>;
}

这些前端示例应放在已登录的 PROMPT 同站环境。服务端 agent 使用既有 Bearer JWT,只在进程内读取凭据,勿把令牌写入 post、源码、日志或 URL。接口错误不可当作渲染结果处理。

按 ID 读取、修改、保留分支

const headers = {'Content-Type':'application/json'};
const read = await fetch(`/api/studio/designs/${id}`);
if (!read.ok) throw Error(await read.text());
const {design} = await read.json();
const parameters = {...design.parameters,texts:{...design.parameters.texts,T01:{value:'第二版'}}};
const update = await fetch(`/api/studio/designs/${id}`, {
 method:'PATCH',headers,
 body:JSON.stringify({name:'知识合集第二版',parameters,revision:design.revision})
});
if (update.status===409) throw Error('有人已经修改,请重新读取后合并');
if (!update.ok) throw Error(await update.text());
// 想保留旧版时,改用 POST /api/studio/designs,传 {layoutId:design.layoutId,name,parameters}。

PATCH 的 parameters 整体替换,因此先 GET 并合并未修改的构件;layoutId 不可改变。错误:400 参数不合法、401 未登录、404 不存在、409 修订冲突、413 超限。效果保存接口说明。列表可用 layoutId 筛选、limit=1..50;返回 nextCursor 后用 before=<nextCursor> 继续。

验收标准

  • 27 个 ID 都有独立效果页、参数说明与渲染 API,来源链接与 T/I/D 构件一一对应。
  • 默认效果展示原图;替换一组文字、一个图位或装饰后,只改变指定构件。
  • API 渲染与界面使用相同模板、验证和渲染器;导出的 SVG 不包含辅助边界。
  • 保存返回独立 UUID,刷新或再次访问效果 URL 能恢复参数;更新后 revision 增加,过期修订返回 409。
  • 图片 assetId 必须是 Wiki 中已上传的图片;不接受未知构件或外站 URL。
  • Wiki 与 Studio 共用现有登录和数据库,Studio 无独立持久化或数据库凭据。