Skip to content

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/agentConversationStore seam 的 fetch 后端(REFACTOR §8 / §8.1);全部需会话、按 sub 作用域

方法 + 路径语义
GET /api/conversationslist 我的会话(仅元数据),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/:idintrospection:返回 recordSchema(自描述数据契约)+ collects
POST /api/data/:id类型化查询({ where?, select?, limit?, orderBy? })→ 真列 SQL 记录
GET /api/forms/:id/submissionslist 一个 form 的提交(作者私有)

公开写仍开放: POST /api/forms/:id/submissions 保持匿名(公开 submit),经 checkRecord 把关 —— 本次加固 只收紧面,不影响公开提交。


1. Studio 协议(@agentaily/studio-protocol)【冻结】

宿主(Agent 或任何 app)驱动一个内嵌 Studio 的垂直无关契约。纯消息信封,无传输、无逻辑。今天走 postMessage,日后 1:1 映射到 MCP(invoke ≅ tools/calldescribe ≅ 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 时存在。
  • paramsresult 在本层不透明

1.2 操作底座(StudioMethod)

每个 Studio 都支持这些。领域操作不扩展这个方法联合 —— 它们骑在 invoke 上。

opparamsresult语义
setDocument{ document: string, schemaVersion? }MutationResult替换整份文档(JSON 串)。schemaVersion 标这份文档针对哪个 schema 版本(宿主加载时传);省略 ⇒ 当前版本(agent 写的)
applyEdit{ patch: { find, replace } }MutationResultv0:对文档做首次匹配的文本替换
getDocumentstring当前文档文本
getState结构化状态(见 1.4)agent 的首要感知面
validateStudioDiagnostic[]当前文档的诊断
getSchemaJSON Schema(不透明)机器可读的内容契约(是形状,不是实例)
getSchemaDiff{ fromVersion: number }SchemaDifffromVersion 到当前的 schema 变更(见 §4)
screenshot{ target? }string(PNG data URL)只用于视觉判断 / 给人看
describeStudioCapabilities能力自述
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 markdown

agent 运行时保持通用,产品在启动时把 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, boolkey 唯一;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 写的 省略它 ⇒ 盖成当前。
  • getStatecurrentSchemaVersion + 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)。

toolinput包住
studio_get_stategetState
studio_set_document{ document }setDocument
studio_apply_edit{ find, replace }applyEdit
studio_validatevalidate
studio_schema_diff{ fromVersion }getSchemaDiff
studio_invoke{ op, params? }invoke
studio_screenshotscreenshot — 仅当 opts.vision
studio_set_renderer 🔁{ html }setRenderer将随收敛退役(R6/R7)
studio_get_renderer 🔁getRenderer将随收敛退役(R6/R7)

约束解码(opts.documentSchema)。 产品提供 vertical 的 JSON Schema(来自 getSchema())时, studio_set_documentdocument 参数就是那个 schema(去掉 $schema/$id)—— 让模型直接吐 符合契约的对象,而不是自由打字 JSON、瞎猜字段名。消除「猜→校验→重试」的循环。

🔁 运行时收敛(R6): 收敛后 LLM 在聊天里只产内容文档(走 studio_set_document 的约束解码), 不再 set_renderer / 不再生成页面studio_set_renderer / studio_get_renderer 从运行时工具集 移除;系统提示改为「只生成 schema 对应的内容」。


6. 不变量(承重规则)

  1. Agent 永不 import 任何 vertical。 它经协议驱动任意 Studio;词汇经 describe 的 manifest 抵达。
  2. 协议信封冻结。 领域特定的东西骑 invoke / describe;params / result 保持不透明。内容格式 与渲染层的变化(含本次收敛的 AML)都不碰它。
  3. 🔁 一个 vertical = 三件套(目标态): schema(zod 内容契约)+ aml(后端数据模型)+ index.html(静态渲染页)。三者皆开发期写死。
  4. 🔁 运行时 LLM 只生成内容文档。 不生成渲染页 / 渲染器 / 任意页面(R6)。内容文档是单一真相源。
  5. 底层链: vertical 的 aml →(框架自动)D1 真表 + 自动生成的 GraphQL → 静态 index.html 调用。 平台跑在 Cloudflare。
  6. 感知优先级: getState / getSchema / getDocument(结构化)→ validate 诊断 → screenshot(只给视觉 / 人看)。对结构推理,不对像素推理。