Agentaily —— 规格(Specification)
平台的规范性契约真相源:精确的通信协议、数据形状、操作语义、不变量。它是 ARCHITECTURE.md(为什么这么设计 / 设计叙事)与 ROADMAP.md(能力进度)的精确配套。散文与本规格冲突时,契约以本文件为准; 两者要一起更新。
🔁 2026-06-19 起架构收敛中。 老板定调:每个垂直插件 =
schema+aml+index.html三件套(都在开发期写死);运行时 LLM 只生成「内容文档」,不再生成渲染页。完整工单见ROADMAP.md「🔁 架构收敛重构」。本规格已对齐到该目标态,并按这两类标注区分:
- 【冻结】/【当前有效】 = 不受本次收敛影响、当前就生效的契约,可直接据此实现。
- 🔁 重构中(随 R… 落地) = 目标态契约,尚未完全冻结;同时如实保留「现状(过渡)」, 说明现在代码里实际是什么样,直到对应 R 工单落地后才以目标态为准。
⚠️ 2026-06-19 晚再收敛(以
REFACTOR.md为当前真相源):R6/R7 已作废。 本规格里所有 「运行时 LLM 只生成内容文档、不生成渲染页」「setRenderer/getRenderer随 R6/R7 退役」的 🔁 标注 不再是计划 —— REFACTOR §6 决策 2 拍板:LLM 仍然生成index.html(+ fork + 预览),市场是核心价值。 渲染器 / 市场相关契约的终态以REFACTOR.md(M0–M8)为准,本文件待 M8「文档对齐」整体重写;在那之前下方 涉及 R6/R7 的内联标注请按「已作废」理解。
约定:? = 可选;"opaque / 不透明" = 协议层不约束其形状(由产品收窄);版本号都是单调递增整数。
0. 已落地的新端点契约【当前有效 · 随 REFACTOR M1–M6】
顶部 banner 指向的是目标态(待 M8 整体重写);本节登记收敛过程中已落地、今天就生效的 HTTP 端点契约 —— 它们真实存在、可直接据此实现,与下方旧正文(§1–§6 的协议信封 / schema / 派生数据层)并存。完整工单 / 分层 / 安全模型见
REFACTOR.md(§2–§5),此处不复述。响应统一是{ ok, … }JSON;「会话」鉴权 = 平台会话(requireSession,Bearer JWT,见记忆 [[agentaily-session-bearer-not-cookie]])。
0.1 publish 数据桥(apps/publish 独立源 Worker)
已发布的 app 在独立源 <sep-origin>/a/:appId/* 被路径式 serve(整组 VFS 文件,数据取自 market_apps)。每个 HTML 页注入 window.agentaily 桥,给沙箱内的 app 一个同源、自我作用域的后端客户端:
window.agentaily.submit(values[, { entity }]) → POST /a/:appId/_data → 201 { ok, id, record }
window.agentaily.query([{ entity, where, select, limit, orderBy }]) → POST /a/:appId/_data/query → 200 { ok, records }
GET /a/:appId/_data?entity=&limit=&orderBy=&dir= → 200 { ok, records }(简单 list)不变量(REFACTOR §5): appId 永远来自 URL,服务端每次都把 projectId = appId 强行注入 SQL(insert 覆盖 body、query AND 进 WHERE)—— body 里夹带的 projectId 被忽略,一个 app 只能读写自己的行(共享单库、行级隔离)。 匿名写放行(公开 submit),读按 app 作用域。app 的 AML 在首次访问时幂等 provision 建表。客户端可设字段 = 非 scope 列、非主键、非 app 生成默认(now/cuid/uuid/autoincrement);命不中任何字段的键收进模型唯一的 Json 列(「答卷」 约定,REFACTOR §4)。
0.2 市场端点(apps/web Pages Functions)
一个扁平、共享的 app 市场(REFACTOR §3,市场 = 核心价值);返回形状对齐 AppEntry(files = 多文件 VFS)。
| 方法 + 路径 | 鉴权 | 语义 |
|---|---|---|
GET /api/market | 公开 | list / search 全部已发布 app;?q= 对 name / description / tags 不区分大小写子串匹配,createdAt 倒序 |
GET /api/market/:id | 公开 | 读单个 app;404 不存在 |
POST /api/market | 会话 | 发布(upsert)。ownerId 钉死会话 sub(非 body);必须带 index.html 入口(在 files,或经 legacy indexHtml 注入)否则 400;改写他人 id → 403;createdAt 不可变 |
POST /api/market/:id/fork | 会话 | fork 任意公开 app 成我的新 app:整组 files + aml / description / preview / tags 逐字复制,新 id + forkedFrom 指向源 + 新时间戳 + owner 钉死会话 |
DELETE /api/market/:id | 会话 | 删我的 app(幂等);他人 → 403 |
0.3 会话端点(apps/web Pages Functions · owner-scoped)
@agentaily/agent 的 ConversationStore seam 的 fetch 后端(REFACTOR §8 / §8.1);全部需会话、按 sub 作用域。
| 方法 + 路径 | 语义 |
|---|---|
GET /api/conversations | list 我的会话(仅元数据),updatedAt 倒序 |
POST /api/conversations | 建新会话({ id?, title?, projectId? });owner 钉死会话,他人 id → 403 |
GET /api/conversations/:id | 载入会话 + 完整 transcript;非我 / 不存在都 → 404(不泄露他人会话) |
DELETE /api/conversations/:id | 删会话 + 其全部消息(幂等);他人 → 403 |
POST /api/conversations/:id/messages | 追加 { messages: Message[] }(upsert,缺则建会话);他人 → 403 |
0.4 数据读端点鉴权加固(M4 — 堵「open for now」裸洞)
作者侧的数据读取端点已补 会话 + owner scope(401 无会话 / 404 不存在 / 403 他人;对齐 /api/projects/:id):
| 方法 + 路径 | 语义 |
|---|---|
GET /api/data/:id | introspection:返回 recordSchema(自描述数据契约)+ collects |
POST /api/data/:id | 类型化查询({ where?, select?, limit?, orderBy? })→ 真列 SQL 记录 |
GET /api/forms/:id/submissions | list 一个 form 的提交(作者私有) |
公开写仍开放:
POST /api/forms/:id/submissions保持匿名(公开 submit),经checkRecord把关 —— 本次加固 只收紧读面,不影响公开提交。
1. Studio 协议(@agentaily/studio-protocol)【冻结】
宿主(Agent 或任何 app)驱动一个内嵌 Studio 的垂直无关契约。纯消息信封,无传输、无逻辑。今天走 postMessage,日后 1:1 映射到 MCP(invoke ≅ tools/call、describe ≅ tools/list)。
不变量 —— 信封冻结。 本包永不 import 任何 vertical。所有领域特定的东西都骑在 invoke / describe / manifest 上,绝不改信封。换内容格式(手搓 DSL → JSON 文档,乃至本次收敛新增的 aml 数据模型)都不会碰这个信封。
1.1 信封
每条消息都带 source: 'agentaily-studio'(STUDIO_SOURCE);外来的 postMessage 一律忽略。
StudioRequest { source, kind: 'request', id, method, params?, baseVersion? }
StudioResponse { source, kind: 'response', id, ok, result?, error?, version? }
StudioEvent { source, kind: 'event', event: 'ready'|'change'|'submit', payload?, version? }id把响应关联回它的请求。result仅当ok时存在;error仅当!ok时存在。params与result在本层不透明。
1.2 操作底座(StudioMethod)
每个 Studio 都支持这些。领域操作不扩展这个方法联合 —— 它们骑在 invoke 上。
| op | params | result | 语义 |
|---|---|---|---|
setDocument | { document: string, schemaVersion? } | MutationResult | 替换整份文档(JSON 串)。schemaVersion 标这份文档针对哪个 schema 版本(宿主加载时传);省略 ⇒ 当前版本(agent 写的) |
applyEdit | { patch: { find, replace } } | MutationResult | v0:对文档做首次匹配的文本替换 |
getDocument | — | string | 当前文档文本 |
getState | — | 结构化状态(见 1.4) | agent 的首要感知面 |
validate | — | StudioDiagnostic[] | 当前文档的诊断 |
getSchema | — | JSON Schema(不透明) | 机器可读的内容契约(是形状,不是实例) |
getSchemaDiff | { fromVersion: number } | SchemaDiff | 从 fromVersion 到当前的 schema 变更(见 §4) |
screenshot | { target? } | string(PNG data URL) | 只用于视觉判断 / 给人看 |
describe | — | StudioCapabilities | 能力自述 |
invoke | { op: string, params? } | 不透明 | 命名空间领域操作的逃生舱,如 form.fieldCount |
setRenderer 🔁 | { html: string | null, schemaVersion? } | { ok } | 设渲染器(一份完整 index.html),或 null = 空白。将随收敛退役 → 见下 |
getRenderer 🔁 | — | string | null | 当前渲染器(index.html),或 null。将随收敛退役 → 见下 |
🔁
setRenderer/getRenderer将退役(R6 / R7)。 收敛后渲染页是每个 vertical 自带的静态index.html,不再由 LLM 在运行时生成 / 设置。协议层是否保留这两个 op(改作宿主加载静态页用) 由 R7 拍板;在 R6/R7 落地前,它们仍按上表语义有效。
1.3 诊断、变更结果、并发
StudioDiagnostic { code, message, hint?, line?, col?, path? } // path = 非行号格式时的 JSON 指针
MutationResult { ok, diagnostics: StudioDiagnostic[], version: DocVersion }
DocVersion = number // 单调递增;每个变更型 op 都 +1乐观并发。 变更型请求可带 baseVersion;若 Studio 当前 version !== baseVersion(陈旧编辑) 则拒绝。防止人和 agent 同时改文档时盲目互相覆盖。(与 §4 的 schema 版本是两回事。)
1.4 getState 结果
协议层不透明;按约定各 Studio 返回:
{ version, document, valid, schema,
currentSchemaVersion, rendererSchemaVersion, docSchemaVersion }schema 是解析后的文档实例(渲染器的输入)。三个 *SchemaVersion 驱动陈旧检测(§4)。
1.5 能力与 manifest(describe)
StudioCapabilities { vertical, ops: string[], schemaKind, schemaVersion?, screenshotTargets?, manifest? }
StudioManifest { tools?: StudioToolSpec[], skills?: StudioSkillSpec[] }
StudioToolSpec { op, name, description, inputSchema }
StudioSkillSpec { name, description, instructions } // Claude-Code 风格的 skill markdownagent 运行时保持通用,产品在启动时把 manifest 合并进 agent 的工具 / 技能清单。词汇从 vertical 向上流入,绝不写死进 agent。
🔁 收敛新增的一个 LLM 技能(R6):agent 要有一个技能 = 知道当前加载的是哪个插件 / 哪种类型 + 它的 schema 契约,从而只生成合规的内容文档。
2. 垂直插件 = 三件套(目标态)🔁
R0 / R3 / R4 落地前,这是目标态;现状见各小节「现状(过渡)」。
一个 vertical = 开发期写死的三样东西,用户 / 运行时 LLM 都不碰这三样:
| 产物 | 是什么 |
|---|---|
verticals/<x>/schema | 内容 schema(zod)—— 定义聊天时 LLM 要生成的那份内容文档(§2.1) |
verticals/<x>/aml | 后端数据模型(@agentaily/aml DSL)—— 框架吃它 → 自动建数据表 + 自动生成 GraphQL(§2.2、§3) |
verticals/<x>/index.html | 静态渲染页 —— 读内容文档 + 直接调那层自动生成的 GraphQL(§2.3) |
2.1 内容 schema(zod)【当前有效】
一个 vertical 的内容 = 一份 JSON 文档,由一份 zod schema 校验。无手搓语法。这一份 schema 干三件事: 校验文档、定型(z.infer)、生成 JSON Schema(z.toJSONSchema)—— 交给 LLM / 渲染器的契约。 文档是单一真相源;一份文档 → 两个投影(渲染 UI + 数据)。
每个 schema 包(@agentaily/form-schema、@agentaily/slides-schema)导出:
check(jsonStr): { <doc>, diagnostics: Diagnostic[] } // JSON.parse + zod safeParse;诊断按 path 定位
<DOC>_JSON_SCHEMA // z.toJSONSchema(...) — 机器契约
EXAMPLE_DOC // 一份合法示例(防漂移测试用)
SCHEMA_VERSION, SCHEMA_CHANGELOG // 见 §4- form:
{ title, desc?, fields[]: { type, key, label, required?, options?, range? } };type ∈ text, long, email, number, choice, multi, rating, date, bool。key唯一;options仅 choice/multi;range仅 number/rating(min<max)。 - slides:
{ title, theme?, slides[]: { layout, heading?, subtitle?, body?, bullets? } };layout ∈ title, bullets, text。deck ≥ 1 张;bullets仅 bullets 版式。文档即 schema。
2.2 数据模型(aml)🔁 重构中(R0 / R2 / R3 / R4)
目标态: 每个 vertical 手写一份 .aml(用 @agentaily/aml —— 我们自家的 Prisma 风格建模 DSL, parse / validate / print,详见 aml/README.md)声明它后端存储的样子。框架吃这份 AML → 自动建 D1 真表 + 自动生成 GraphQL 层(§3)。
待 R0 拍板的设计点:
form的提交表列是终端用户运行时跟 LLM 聊出来的(动态),静态 AML 怎么共存?(候选:AML 定固定 实体 + 系统列骨架,字段级约束仍由checkRecord把关;或 AML 作模板、框架按文档实例化。)- AML → 真表的路径(AML AST → JSON Schema 复用现编译器,还是 → DDL/Drizzle 直出);与
@agentaily/db「Drizzle 为单一真相源」如何并存 / 取代。
现状(过渡)—— 数据接口是「派生」的,不是手写的: 当前没有 .aml;数据形状从内容文档派生。 协议类型(doc-generic):
JsonSchema = Record<string, unknown> // 一个 JSON Schema 对象
RecordCheckResult = { ok, diagnostics: StudioDiagnostic[] }
DataInterface<Doc> = { collects: false }
| { collects: true, recordLabel?, recordSchema(doc): JsonSchema,
checkRecord(doc, record): RecordCheckResult }recordSchema(doc)从文档计算出来(零漂移):form 的fields[]→ 以field.key为键、field.type定类型的对象 schema;checkRecord(doc, record)是入库前的服务端闸门。@agentaily/form-schema导出formDataInterface(collects: true);@agentaily/slides-schema导出slidesDataInterface = { collects: false }(deck 是展示,不收集)。- 每条入库记录都盖上它被采集时的 schema 版本(§4),老数据能挺过 schema 演进。
🔁 收敛要把这层从「派生 / 中央 Drizzle」改成「每个 vertical 一份手写 AML」。R0 拍板二者如何衔接 (尤其 form 的动态字段)。
2.3 渲染页 index.html(静态)🔁 重构中(R3 / R4 / R6 / R7)
目标态: 每个 vertical 自带一份开发期写死的静态 index.html —— 读注入的内容文档 + 直接调 §3 那层自动生成的 GraphQL 读写数据。运行时 LLM 不生成它。
现状(过渡)—— 渲染器现在是 LLM 生成的 DATA,即将退役:
- 渲染器 = 一份完整自包含的
index.html串(结构 + 内联 CSS + 内联 JS;可引 CDN 库 / 字体),跑在 Studio 内层 iframe。无内置渲染器(null ⇒ 空白制品),无独立主题(观感就在这份 HTML 里)。 - 注入契约(保单一真相源): 渲染器是个模板,必须含一个空的
<script id="agentaily-doc" type="application/json"></script>。每次变更 Studio 把当前文档 JSON 注入此处(转义</script>),再 set 内层 iframe 的srcdoc(等于重载)。渲染器加载时读取:JSON.parse(document.getElementById('agentaily-doc').textContent || '{}')。- 文档非法 ⇒ Studio 保留上一次好的渲染 + 显示诊断浮层。
- 安全: 内层 iframe 今天同源(故 CDN + html2canvas 截图可用、有完整网页能力)。自己项目尚可; 在 preset 跨用户共享前要隔离到独立沙箱源。
- Preset(市场) ——
@agentaily/presets:每条目一份自包含渲染器。每个 vertical 一个市场,无独立 主题市场、无内置条目。
🔁 整套「LLM 生成渲染器 + preset 市场」随收敛退役(R7): 收敛后渲染页是静态
index.html,@agentaily/presets、注入契约、iframe-host-renderer 一并砍 / 改。R7 拍板细节。
3. 数据层:AML → 真表 → GraphQL 🔁 重构中(R0 / R2 / R5)
目标态: 框架读 vertical 的 aml → 自动 provision D1 真表 → 在真表上自动生成一套 GraphQL 接口 (查 + 写),供静态 index.html 调用。平台跑在 Cloudflare(Pages + Pages Functions / Workers + D1)。
现状(过渡)—— 已有「GraphQL 精神」的派生数据 API(db-layer 的 @agentaily/db):
- 静态固定表在
db/src/schema.ts(中央一份 Drizzle:projects/users/submissions/customHostnames/authTokens);drizzle-kit生成迁移。 - 编译器(
db/src/compiler.ts,vertical 无关):recordSchema(JSON Schema)→ 每项目一张真表data_<projectId>(真列c_<key>+ 系统列_id/_created_at/_schema_version)+ 迁移 diff。 - 数据 API
/api/data/:id:GET = introspection(返回recordSchema)、POST = 类型化查询 (where/select/limit/orderBy,编译成对真列的 SQL)。这是「GraphQL 精神」的 REST, 不是 graphql-yoga。 - 公开运行时
/f/:id:把渲染器 + 注入文档 +window.agentaily(submit/query)桥组装成页面。
🔁 R5 把「GraphQL 精神 REST」升成「自动生成的 GraphQL」(引擎选型 graphql-yoga vs 延续精神 + 自描述,由 R0 拍板),并把数据模型来源从「派生 recordSchema」切到「vertical 的 AML」。详见
db/DESIGN.md。
4. Schema 版本化与 LLM 驱动迁移【当前有效】
内容 schema 在 vertical 级演进(平台出品)。平台让演进可观察,由 agent 决定并执行升级 (我们出 seam,不出策略)。
4.1 数据形状(协议)
SchemaChange { version, kind: 'add'|'remove'|'rename'|'modify', target, breaking, summary, migrationHint? }
SchemaDiff { from, to, changes: SchemaChange[], hasBreaking }
schemaDiff(changelog, from, to) = { from, to, changes: changelog 中 from < version ≤ to 的项, hasBreaking }vertical 维护 SCHEMA_VERSION(当前)+ SCHEMA_CHANGELOG(每次变更一条,标它落在哪个版本)。 breaking = 老文档不再校验通过(需迁移);migrationHint 告诉 agent 怎么迁。v1 = 初始(空 changelog)。
4.2 盖戳与陈旧检测
制品记录自己针对哪个 schema 版本构建:
setDocument/setRenderer接受schemaVersion:宿主加载老制品时传入存档版本;agent 写的 省略它 ⇒ 盖成当前。getState报currentSchemaVersion+rendererSchemaVersion+docSchemaVersion。任一*SchemaVersion < currentSchemaVersion⇒ 该制品陈旧。
4.3 升级流程(agent 编排,可选)
agent 工具 studio_schema_diff(fromVersion) 暴露变更列表。系统提示:若制品陈旧,你可升级 —— 这是 可选的,告诉用户并自行决定。 流程:
1. studio_get_state → 看 currentSchemaVersion vs builtFor(陈旧?)
2. studio_schema_diff(builtFor) → SchemaChange[](+ hasBreaking)
3. (内容)studio_set_document → 把老文档迁移到新 schema(约束解码 §5 强制其符合)
4. Studio 把文档重新盖成当前版本。无新增变更型 op —— 升级复用 set_document。人在产品里看到同一份 diff 作为 banner。
🔁 旧流程里还有「
studio_set_renderer重写渲染器」一步;随渲染器退役(R7),升级只剩内容文档这条。
5. Agent 工具集(@agentaily/agent)
studioTools(bridge, opts) 返回一套固定、通用、1:1 包住协议的工具;不随 vertical 增长(领域 op 骑 studio_invoke)。
| tool | input | 包住 |
|---|---|---|
studio_get_state | — | getState |
studio_set_document | { document } | setDocument |
studio_apply_edit | { find, replace } | applyEdit |
studio_validate | — | validate |
studio_schema_diff | { fromVersion } | getSchemaDiff |
studio_invoke | { op, params? } | invoke |
studio_screenshot | — | screenshot — 仅当 opts.vision |
studio_set_renderer 🔁 | { html } | setRenderer — 将随收敛退役(R6/R7) |
studio_get_renderer 🔁 | — | getRenderer — 将随收敛退役(R6/R7) |
约束解码(opts.documentSchema)。 产品提供 vertical 的 JSON Schema(来自 getSchema())时, studio_set_document 的 document 参数就是那个 schema(去掉 $schema/$id)—— 让模型直接吐 符合契约的对象,而不是自由打字 JSON、瞎猜字段名。消除「猜→校验→重试」的循环。
🔁 运行时收敛(R6): 收敛后 LLM 在聊天里只产内容文档(走
studio_set_document的约束解码), 不再set_renderer/ 不再生成页面。studio_set_renderer/studio_get_renderer从运行时工具集 移除;系统提示改为「只生成 schema 对应的内容」。
6. 不变量(承重规则)
- Agent 永不 import 任何 vertical。 它经协议驱动任意 Studio;词汇经
describe的 manifest 抵达。 - 协议信封冻结。 领域特定的东西骑
invoke/describe;params/result保持不透明。内容格式 与渲染层的变化(含本次收敛的 AML)都不碰它。 - 🔁 一个 vertical = 三件套(目标态):
schema(zod 内容契约)+aml(后端数据模型)+index.html(静态渲染页)。三者皆开发期写死。 - 🔁 运行时 LLM 只生成内容文档。 不生成渲染页 / 渲染器 / 任意页面(R6)。内容文档是单一真相源。
- 底层链: vertical 的
aml→(框架自动)D1 真表 + 自动生成的 GraphQL → 静态index.html调用。 平台跑在 Cloudflare。 - 感知优先级:
getState/getSchema/getDocument(结构化)→validate诊断 →screenshot(只给视觉 / 人看)。对结构推理,不对像素推理。