ihtml

package module
v0.0.0-...-7bb80a6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 34 Imported by: 0

README

ihtml — AI 动态前端 UI 功能库

一个 Go 库:内嵌一张"空壳页面"(微内核),按用户加载服务端记录的一组 UI Item(HTML/JS/CSS 片段)并在浏览器里执行,拼装出该用户的完整界面。内置 AI 助手既能在对话中直接回答和分析业务数据,也能在用户明确要求时生成并持久化 Item——用户说"加一张行情卡片",页面实时出现,下次打开依然在。

浏览器空壳页(内核)  ──参数──▶  Go 服务端  ──UserResolver──▶  用户
      ▲                        │
      │  Items(html/js/css)    │ Store(内存/文件/自定义数据库)
      │◀── REST + SSE 实时 ────┤
      │                        │
      └── WebSocket 对话 ──▶ AI 后端(eino: OpenAI 兼容/Claude) ──工具──▶ 改 Item/主题/KV/回滚

特性

  • 微内核空壳页:无构建、无框架、内嵌二进制;Item 层想用什么技术都行(JS 可自由 fetch
  • 按用户持久化:同一个 URL,不同用户是完全不同的应用
  • 实时上屏:SSE 推送,AI 改完不用刷新
  • 主题系统:CSS 变量 tokens + 明暗模式 + 任意主色(color-mix 派生),一处换肤全局生效
  • 安全网:默认拒绝未接入鉴权的请求;每次变更自动快照可回滚;?safe=1 安全模式救砖;Item 报错自动收集
  • AI 对话:eino ADK 多轮会话,默认低延迟 Fast、可显式开放 DeepAgent 多 Agent 编排;语义工具 + 源码写入/高危操作 HMAC 确认卡 + 有界历史与独立写限流
SDK 包边界
第三方用途
ihtml 稳定核心:HTTP Handler、Store/Service、模板、事件与 ChatBackend 协议;AI 完全可选
ihtml/chat 内置 Eino AI 后端;程序化入口是 chat.Config + chat.NewChecked
ihtml/chat/config 严格、版本化、可嵌入宿主配置的 AI JSON loader;SecretResolver 显式注入
ihtml/sqlstore database/sql 持久化实现;驱动与 *sql.DB 生命周期由宿主拥有
ihtml/redisbroadcast 多副本实时事件:非阻塞 Redis Pub/Sub + 用户级全局单调版本;*redis.Client 与 Broadcaster 生命周期均由宿主关闭
ihtml/storetest 第三方 Store 实现的官方一致性验收套件(storetest.Run),内置 Store 同样由它验收

AI 所有权只有一种。 宿主已有自己的 Agent、会话、记忆或权限系统时,应实现 ihtml.ChatBackend 并通过 WithChatBackend 注入;此时不要再创建 ihtml/chat.Backendihtml/chat 只服务没有既有 AI 中枢的独立部署,负责 ihtml 领域的 Fast/Deep Agent、UI 工具、确认边界与会话治理。两种模式共享同一套前端协议,但不会在一个进程里叠加两套模型循环。

旧的 ihtml.New / chat.New 为兼容保留;新集成推荐 NewHandler / NewChecked,让所有配置错误在开始监听前返回。公共协议字符串均有导出常量,模型能力响应带 protocol_version。当前仍处于 pre-v1 阶段;发布 v1 前会继续保持新增优先、破坏性调整集中并提供迁移说明。

完整兼容约定见 COMPATIBILITY.md,未发布变更见 CHANGELOG.md。许可证为 MIT,最低 Go 版本 1.25(CI 同时验证最低版与最新稳定版);正式发版前仍需由项目所有者打首个 semver tag。

嵌入宿主应用(SDK 形态)

宿主页面只需引入内核 js/css 并声明一个挂载点——零 JS 代码

<link rel="stylesheet" href="https://ui.example.com/ihtml.css">
<script src="https://ui.example.com/ihtml.js"></script>

<div id="their-nav"><!-- 宿主自己的侧栏里留给 ihtml 菜单的位置 --></div>
<div id="customer-ui" data-ihtml
     data-base="https://ui.example.com/"  
     data-uid="alice"
     data-ihtml-storage-key="customer-ui"
     data-theme="inherit"
     data-menu="#their-nav"></div>
  • data-theme="inherit":挂载时探测宿主计算样式(背景亮度→明暗、正文色、字体、链接色→主色)自动融入宿主观感;也可显式 data-theme='{"primary":"var(--brand-600)","mode":"dark"}' 直接映射宿主设计变量
  • data-menu:多页时菜单渲染进宿主指定容器(单页/无页面时自动隐藏);嵌入默认内存路由,不占宿主 location.hash
  • 内核样式全部收在 .ihtml-root 作用域内;嵌入时 Item 的普通选择器默认精确限定到所属实例的 data-ihtml-instance@keyframes / @property 等文档级命名仍需 Item 自行使用唯一前缀;跨域部署时服务端加 ihtml.WithCORS("https://host.example.com")
  • 等价的编程式:ihtml.mount(el, {base, auth, connectionAuth, theme, routing, menu, chatEntry, storageKey});宿主随时可调 ihtml.theme.set/nav.go/chat.open
  • base 在挂载前严格解析,只接受不含用户名/密码的 HTTP(S) 目录 URL;其中 query/hash 属于页面状态,会被丢弃,身份参数请放入 params,凭据请放入 auth / connectionAuth
  • 同页多实例mount 可多次调用(或写多个 data-ihtml 声明),内核生成的 CSS 选择器边界、Item 词法 ihtml API、主题/模型/Agent 模式/对话会话缓存和内核 ARIA ID 均按实例隔离,各自 destroy()。Item 正文自带的 id 不会被重写,操作 Item DOM 必须从返回 API 的 root 开始查询。兼容层的 window.ihtml 动态指向当前主实例,主实例销毁后自动提升仍存活的第一个实例;多实例宿主仍必须保留每次 mount() 的返回值,并保持默认内存路由(hash 是页面级单资源)
  • storageKey / data-ihtml-storage-key 为实例提供跨刷新稳定的本地缓存身份;宿主元素有稳定 id 时可省略,否则建议显式设置。键还会按 base 和已解析用户隔离,切换身份不会复用上一用户的对话。升级时,首个成功解析身份的实例会一次性复制旧的无作用域主题、模型、Agent 模式和会话键;旧键保留供回滚,但不会再被第二个实例认领
  • 也支持 iframe 嵌入独立页。跨域父页面必须用 WithMessageOrigins("https://host.example.com") 显式放行 postMessage 与 CSP frame-ancestors
  • Item 中声明式的外部 <script src> / <link rel=stylesheet> 默认拒绝;只有受控宿主可显式设置 allowExternalAssets:true。启用严格 script-src CSP 的嵌入宿主应把当前响应的随机 nonce 传给 mount({scriptNonce}),内核会把它附加到所有动态 Item script,无需放开 unsafe-inline。外链 stylesheet 是文档级资源,不能被内核 @scope 重写;外链 classic Item 脚本若需异步使用实例 API,必须在脚本同步顶层先捕获 const instance = window.ihtml,回调只使用 instance。外链 ES Module 没有可靠的 document.currentScript;多实例宿主应由自己的初始化函数显式传入 mount() 返回 API,或使用能注入词法 ihtml 的内联 module Item。这不是沙箱:Item 是可信可执行代码,仍可通过 JS、图片或 CSS 发起网络请求。兼容旧 CSS 时可临时设置 scopeStyles:false

快速开始

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/zdypro888/ihtml"
    "github.com/zdypro888/ihtml/chat"
)

func main() {
    store, err := ihtml.NewFileStore("./uidata") // 或 NewMemoryStore() / 自己实现 Store
    if err != nil {
        log.Fatal(err)
    }

    ai, err := chat.NewChecked(chat.Config{
        Provider: chat.ProviderOpenAI,
        BaseURL:  "https://api.openai.com/v1",
        APIKey:   os.Getenv("OPENAI_API_KEY"), // 或由宿主秘密管理系统注入
        Model:    "gpt-4o-mini",
        Stream:   true,
    })
    if err != nil {
        log.Fatal(err) // 在监听端口前发现配置问题
    }

    handler, err := ihtml.NewHandler(store,
        // 仅用于本地体验;生产必须换成验证宿主会话/令牌的 resolver。
        // 不配置 resolver 时 ihtml 会安全地返回 401。
        ihtml.WithUserResolver(ihtml.DevelopmentUserResolver),
        ihtml.WithChatBackend(ai), // AI 对话完全可选
    )
    if err != nil {
        log.Fatal(err)
    }
    defer handler.Close()

    mux := http.NewServeMux()
    mux.Handle("/ui/", http.StripPrefix("/ui", handler)) // 挂任意子路径或根路径
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Print(err)
    }
}

DevelopmentUserResolver 信任客户端可修改的 ?uid= / header / cookie,只能用于演示和测试。生产 resolver 应从宿主已经验签的会话或令牌中返回用户,空用户或错误会得到 401。

体验演示(含预置数据;未配置真实模型时自动挂载"演示助手",只预览富文本对话展示,不会伪造模型思考、业务结果或页面写入):

go run ./example                 # http://127.0.0.1:8899/?uid=demo,仅监听本机
go run ./example -data ./uidata  # 文件持久化

推荐用版本化服务端配置文件管理 Provider、模型、Thinking、Fast/Deep 预算、历史和确认策略。AI section 自带独立的 schema_version,不会被宿主自己的配置版本绑死:

cp example/ihtml.example.json example/ihtml.local.json
# 编辑 ihtml.local.json;该文件已加入 .gitignore。

# 示例文件只引用密钥名称,不保存密钥值。
export ANTHROPIC_AUTH_TOKEN=your-token
go run ./example -config example/ihtml.local.json

-config 优先于 IHTML_CONFIG。这是 demo 启动器的策略,不是核心库的全局行为。只要指定配置文件,JSON 就是全部非密钥 AI 字段的唯一权威,不再被 ambient IHTML_AI_* / ANTHROPIC_* 标量覆盖;只有 auth.env / confirmation_secret.env 会读取明确命名的环境变量。也可以完全不使用环境变量,改用只读密钥文件:

"auth": {
  "type": "auth_token",
  "file": "/run/secrets/ihtml-anthropic-token"
}

密钥文件必须是绝对路径、普通非符号链接文件,Unix 权限为 06000400。配置采用严格 JSON:限制 64 KiB、拒绝未知字段/重复键/尾随文档,并在监听端口前校验 Provider、HTTPS BaseURL、模型、认证、Thinking、Deep 预算与超时;显式配置无效会终止启动,绝不会静默降级到模拟助手。当前 AI schema 是 v2,loader 仍严格读取旧 v1 文档并补上安全默认值;运行时资源上限、模型重试、工具发现/额度、会话 namespace 等新增字段只属于 v2,使用这些字段时必须把 schema_version 升为 2,不会在 v1 下被悄悄接受。IHTML_AI_DISABLED=1 可作为紧急熔断。

这套加载能力不是 example 私有代码。第三方宿主可直接导入 github.com/zdypro888/ihtml/chat/config

loader, err := chatconfig.NewLoaderChecked(chatconfig.Options{
    // 必须显式注入;nil 时 loader 绝不会偷偷读取 ANTHROPIC_* / IHTML_*。
    Secrets: chatconfig.NewOSSecretResolver(),
})
if err != nil {
    return err // 包括 typed-nil SecretResolver 等宿主依赖错误
}
result, err := loader.DecodeSection(ctx, appConfig.AI) // appConfig.AI 是 json.RawMessage
if err != nil {
    // errors.Is(err, chatconfig.ErrInvalidConfig / ErrSecretUnavailable)
    // errors.As(err, *chatconfig.Error) 可取得稳定的 Kind 与 Path。
    return err
}
if result.Enabled {
    // SessionStore、拦截器、Probe 身份等代码能力仍由宿主注入,不从 JSON 构造。
    backend, err := chat.NewChecked(result.Config)
    // ... ihtml.WithChatBackend(backend)
}

Secret Manager/Vault/KMS 用户实现 chatconfig.SecretResolver 即可;JSON 使用 {"ref":"vault://..."},loader 只把该显式引用交给宿主 resolver。公共 loader 没有 provider 环境变量推断、全局注册表或静默 fallback。

不使用配置文件时仍兼容旧环境变量启动方式:

# 接真实模型(任意 OpenAI 兼容网关 / Ollama / exo):
export IHTML_AI_BASE_URL=https://api.openai.com/v1
export IHTML_AI_API_KEY=sk-...
export IHTML_AI_MODEL=gpt-4o-mini   # 可省: 会自动选网关上第一个已加载模型

# 可选:服务端开放 Deep;不设置时模型列表只公布 fast。
export IHTML_AI_ENABLE_DEEP=1
# 可选:Deep 根 Agent 与只读子 Agent 共享的硬预算(以下为默认值)。
export IHTML_AI_MAX_MODEL_CALLS=24 IHTML_AI_MAX_SUBTASKS=3 IHTML_AI_MAX_TURN_SECONDS=120
go run ./example

Anthropic Messages 协议(Eino Claude adapter)也可直接复用 Claude Code 风格环境变量。BaseURL 写协议根路径,SDK 会自动追加 /v1/messages

export ANTHROPIC_BASE_URL=https://gateway.example/api/anthropic
export ANTHROPIC_AUTH_TOKEN=your-token
export ANTHROPIC_DEFAULT_HAIKU_MODEL=your-fast-model

# IHTML_AI_* 始终具有更高优先级;用它明确选择本应用的单一默认模型。
export IHTML_AI_MODEL=your-model
export IHTML_AI_THINKING=1
export IHTML_AI_THINKING_BUDGET_TOKENS=1024
export IHTML_AI_MAX_TOKENS=4096
export IHTML_AI_ENABLE_DEEP=1
go run ./example

也可使用 ANTHROPIC_API_KEYx-api-key)或 IHTML_AI_AUTH_TOKEN(Bearer)。未设置 IHTML_AI_MODEL 时,演示程序依次读取 ANTHROPIC_MODELANTHROPIC_DEFAULT_HAIKU_MODEL、Sonnet、Opus 映射;Fast 是默认模式,因此优先 Haiku 映射。

核心概念

UI Item
{
  "id": "btc-card",          // 语义化唯一标识, 同 id 重写 = 原地覆盖
  "type": "html",            // html | js | css
  "title": "BTC 价格卡片",
  "page": "dash",            // 所属页面; 留空 = 全局(菜单/顶栏/全局样式)
  "order": 20,               // 应用顺序
  "content": "<div class=\"ui-card\">…</div>",
  "meta": {"slot": "sidebar"} // 可选; html 绑定当前 Template Slot.id; js 可设 module
}
  • html:渲染进 <section data-ihtml-id="…">,按 order 排列,内嵌 <script> 会执行
  • css:注入 <style>;嵌入模式默认限定在 .ihtml-rootjs:注入 <script> 执行(宽容处理误带的 <script> 标签包裹)
  • 单 Item 的 content ≤ 512 KiB;单用户最多 512 个 Item,且全部 Item 源字段合计 ≤ 8 MiB。每个 Item 的 meta 最多 64 项,key ≤ 128 bytes、value ≤ 1 KiB
页面与路由(多页应用)

默认是单页形态(Item 全部 page:"",无菜单无 shell)——适合作为宿主产品里的一个页面;AI 也被教导只有用户明确要"多页/菜单"时才升级。多页时:注册页面 {name, title, icon, order}(页面列表=菜单数据源),路由切页卸载当前页 Item(触发 teardown)→ 加载目标页;独立页面用 hash 路由(#/bills),嵌入模式用内存路由。菜单可以是 AI 生成的 sidebar Item(独立页面),也可以渲染进宿主容器(data-menu 菜单槽)。

插槽布局与 PC/Mobile

未启用已发布 Template 时,html Item 仍可用旧兼容名 meta.slot: sidebar|header|footer 挂入 app-shell 对应区域;存在这类 Item 时内核自动启用 app-shell 网格布局。容器宽度 <768px 时 sidebar 自动变成左侧抽屉 + 汉堡按钮(支持左缘右滑打开、左滑关闭的触摸手势),浮动操作也切成底部操作栏;.ihtml-root.ihtml-mobile 类与 ihtml.env 供 Item 侧适配。

业务 API 目录(AI 取数)

宿主用 WithAPIs(...) 声明业务接口(路径+参数/返回说明),目录进入 AI 系统提示。AI 的 api_probe 默认禁用;只有同时配置 chat.Config.ProbeBaseURL 后才能探测目录白名单内的 GET 路径。身份不会隐式转发,可用 ProbeForwardHeadersProbeForwardQueryProbeRequest 显式注入。Item 的 JS 直接 fetch 同源接口;配合 WithPageConfig 可注入每用户动态参数(ihtml.config)。

页面运行时 ihtml

Item 内联代码会在无同名声明冲突时获得指向所属实例的词法 ihtml;内核不会用正则改写 Item 源码。内联 classic 脚本在 Item 局部函数作用域执行,若确实要导出全局能力,须显式写成 window.foo = foo,不要依赖顶层 var/函数声明。下表是 Item 的"标准库"(也是 AI system prompt 里的 API 文档):

API 用途
ihtml.root 当前实例根元素;用 ihtml.root.querySelector(...) 查 Item,不要从全局 document 查同名 Item
ihtml.nav.go/current/pages/onChange 路由:切页、当前页、页面列表(=菜单数据源)、监听
ihtml.env / env.onChange(fn) `{mode:'pc'
ihtml.config 宿主经 WithPageConfig 注入的动态参数(只读)
ihtml.layout.sidebar(open) 移动端抽屉菜单开合
ihtml.http.get/post/put/del(path) 同源请求封装,自动透传用户参数
ihtml.kv.get/set/del(key) 用户级持久化键值(服务端存储)
ihtml.bus.on/emit(name, data) Item 间解耦通讯
ihtml.theme.get/set(patch)/onChange(fn) 主题读改,如 set({mode:'dark', primary:'#059669'})
ihtml.ui.toast(msg, type) / ui.confirm({...}) 通知与确认
ihtml.items.onTeardown(id, fn) 必须:定时器/监听器的清理钩子,Item 被覆盖时调用
ihtml.items.apply/remove/refresh/save/delete 运行时增删 Item
ihtml.errors.report/list 错误上报与查看
ihtml.chat.open() 唤起 AI 对话抽屉
主题 tokens

Item 样式引用 CSS 变量即可自动适配换肤与明暗模式:

--ui-primary / --ui-primary-contrast / --ui-primary-soft / --ui-primary-border / --ui-bg / --ui-surface / --ui-surface-muted / --ui-surface-hover / --ui-text / --ui-text-muted / --ui-border / --ui-control-border / --ui-shadow / --ui-ok / --ui-err / --ui-warn / --ui-radius / --ui-gap / --ui-font

基础形态类:.ui-card .ui-btn .ui-btn.primary .ui-input .ui-select .ui-badge .ui-title .ui-muted

HTTP API

挂载点相对路径(全部要求 UserResolver 能解析出用户):

方法与路径 说明
GET / 空壳页面(?safe=1 安全模式:不执行 Item,可回滚)
GET/PUT/POST /api/items 列出 / 增改 Item(?page=x 过滤:全局+该页;&global=0 仅该页)
DELETE /api/items?id=a&id=b 删除
GET/PUT /api/pages · DELETE /api/pages/{name} 页面注册表(删除页面连带删除页内 Item)
GET/POST /api/templates · GET/PUT/DELETE /api/templates/{id} 一等布局模板 CRUD;写入使用 expected_revision 做 CAS
GET/PUT /api/templates/current?page=x 读取/保存 root 或页面当前模板;空模板合法,旧 meta.slot/layout.* 只读渐进迁移
POST /api/templates/{id}/publish 把当前 Draft 发布为不可变值快照(体含 expected_revision
DELETE /api/templates/{id}/slots/{slot}?expected_revision=N 删除 Slot;Item 保留,关联 Placement 自动迁入未分配区
POST /api/items/replace 整体替换
GET /api/revisions · GET /api/revisions/{id} 快照列表 / 内容
POST /api/revisions/{id}/rollback 回滚(回滚前自动再存一份快照)
GET /api/export 导出该用户全量数据(v3 备份契约,与 ExportUser 一致)
GET/PUT/DELETE /api/kv/{key} 用户级 KV(GET 返回 value + revision;PUT 可传 expected_revision 做原子条件写,冲突 409;缺失读取 404;__ihtml 前缀统一拒绝)
GET /api/events SSE:带单调 version 的变更事件;订阅、溢出或周期校准会发送 resync
GET/POST /api/errors 页面错误查看 / 上报
GET /api/config {"user":"…","chat":true}
GET /api/chat/models · GET /api/chat/ws AI 模型列表 / 对话 WebSocket

每次 Item、Page 或 Template 变更(含 AI 发起)都会:先快照(默认保留 50 份,WithMaxRevisions 调整)→ 落库 → SSE 广播。WithMaxRevisions(0) 只取消默认的 50 份保留目标,不能关闭 4096 份 / 64 MiB 的 revision 硬上限。

持久化资源边界

这些限制由服务端强制执行,不能由 Store、导入文件或配置关闭;名称对应可供第三方宿主引用的公开 Go 常量。

数据 每对象限制 单用户硬上限
Item content ≤ 512 KiB (MaxItemContentBytes);每个 meta ≤ 64 项、key ≤ 128 bytes、value ≤ 1 KiB (MaxItemMetaEntries / MaxItemMetaKeyBytes / MaxItemMetaValueBytes) ≤ 512 项 (MaxItemsPerUser);所有 ID/title/page/content/meta 源字节合计 ≤ 8 MiB (MaxUserItemBytes)
Template 每个 Draft 或 Published TemplateVersion 的保守结构估算 ≤ 1 MiB (MaxTemplateVersionBytes) 全部模板 Draft/Published 逻辑结构合计 ≤ 8 MiB (MaxTemplateCatalogBytes);内部目录 JSON 编码 envelope ≤ 16 MiB (MaxTemplateCatalogStorageBytes)
KV 公共 KV value ≤ 64 KiB (MaxKVValueBytes) 公共与内核 key 合计 ≤ 4096 项 (MaxKVEntriesPerUser);key + 原始 JSON value 合计 ≤ 32 MiB (MaxUserKVBytes);Limits.MaxKVKeysPerUser 仍是更低的可配置公共 KV 准入配额
Revision 每份快照仍逐项满足 Item/Page/Template 契约 ≤ 4096 份 (MaxRevisionsPerUser);全部快照的 Item/Page/Template 源数据保守估算合计 ≤ 64 MiB (MaxUserRevisionBytes)

第三方 Store / UserStateStore 的返回值也会在复制、返回或继续写入前重新校验。任一结构损坏或超过上述硬上限时,Handler 会 fail-closed 返回错误,不会静默截断或暴露部分状态。升级已有部署前应离线扫描并迁移、裁剪或清理超限记录;超限后不能依赖普通 Handler API 自行“读出来再清理”。

Template 只定义可复用结构。新业务 Item 通过 Item.Meta["slot"] == Slot.id 绑定;TemplateVersion.Placements 仅用于旧 layout.* 迁移或明确的页面实例覆盖,模板编辑器不应因创建 Container/Slot 而写入业务 ItemID。Slot 缺失、已删除或 Placement 标记 unassigned 时,运行时把 Item 追加到 Root 末尾,不能静默隐藏。

POST /api/templates 创建的是未绑定模板,不会仅因 ID 恰好为 tpl-roottpl-page-* 就自动上线;必须通过 PUT /api/templates/currentPage.template_id 显式绑定。普通运行时只应用 template.published,绝不回退到 Draft;没有 Published 时继续走旧兼容渲染。

AI 对话

  • 传输:WebSocket 上的可扩展流式事件协议(user_message / confirm_tool / cancel_tool / cancel_tool_result / assistant_delta / tool_start / confirmation_required / assistant_done …),第三方 ChatBackend 可以沿用同一套通用前端。所有服务端输出(包括第三方 Backend)在写线前统一受限:单个编码后事件 ≤ 1 MiB - 1 byte (MaxChatOutboundFrameBytes),每个已接受用户轮次 ≤ 4096 个事件且 ≤ 4 MiB,每条连接累计 ≤ 65536 个事件且 ≤ 64 MiB;越界会发送固定错误并以 WebSocket 1009 关闭连接。confirm_toolcancel_tool 进入同一有界 FIFO,按帧顺序争用一次性 nonce;前端以携带工具 ID + nonce 的真实回执为准,连接断开时会先重连,只有成功回执才显示“已撤销、未执行”,失败或状态未知绝不伪装成取消成功
  • 通用助手:普通问答、解释和业务数据查询直接在对话中回答;只有用户明确要求创建或修改界面时才调用 UI 写工具。多记录、时间序列和横向对比优先使用语义表格,但不硬编码业务字段或固定模板
  • 执行模式:未指定 agent_mode 或指定 fast 时走单 Agent 低延迟路径;只有服务端设置 EnableDeepAgent=true 后,客户端才可对复杂任务发送 agent_mode:"deep"。因此开放 Deep 不会改变普通对话的默认行为
  • DeepAgent 边界:使用 eino ADK DeepAgent 做规划与委派,内置 ui_inspector / ui_reviewer 子 Agent 只能读取(含 ui_template_get,不会获得模板写工具);所有 UI/Template 写入仍集中在根 Agent,且没有文件系统、Shell 或任意网络工具
  • 上下文与工具目录:Deep 默认用 Eino tool_search 按需装载语义工具,Fast 仍直接绑定完整目录;长会话任一超过 24000 tokens 或 80 条消息时自动压缩。摘要模型被禁止调用工具,旧系统消息不会进入其输入,工具结果按不可信数据处理;压缩记忆也带显式不可信边界。宿主可分别通过 DisableDynamicToolSearchDisableContextSummarization 或严格 JSON 配置调整
  • 能力发现:GET /api/chat/modelsagent_modes 是服务端实际开放的模式(默认 ["fast"],显式开放后为 ["fast","deep"]);deep_approval_resume 明示确认卡后的恢复能力。AllowModelSelect=true 时才可查网关模型目录并由用户选择;生产默认隐藏模型元数据并锁定服务端 Config.Model,客户端不能覆盖
  • 有界执行:MaxModelCallsPerTurn(默认 24)、MaxSubtasksPerTurn(默认 3)与 MaxTurnDuration(默认 2 分钟)由根/子 Agent 共享;每次模型调用只对网络、限流和明确上游瞬时错误重试 1 次(MaxModelRetries,认证/参数/策略/主动取消不重试,且重试继续计入模型额度)。不信任 provider 的 token/usage 声明:每次 HTTP 响应体默认硬限 8 MiB,模型输出按单次/整轮默认限制为 1 MiB / 4 MiB,整轮最多 4096 个模型输出事件(流 chunk 或非流 response;MaxProviderResponseBytes / MaxModelOutputBytesPerCall / MaxModelOutputBytesPerTurn / MaxModelOutputEventsPerTurn);正文、reasoning、tool-call 与扩展载荷都计入,Deep 子 Agent、摘要与答复修复共享同一 turn 额度。可见最终回答和整轮 reasoning 另默认硬限 256 KiB(MaxAssistantMessageBytes / MaxReasoningBytesPerTurn,reasoning 单 segment 最多 64 KiB),超限会给出截断标记,不会把巨型快照交给浏览器。严格 JSON 与 NewChecked 接受的范围依次为:provider 响应 1..64 MiB、单次模型输出 64 KiB..16 MiB、整轮模型输出 64 KiB..64 MiB(且不得小于单次)、整轮模型事件 16..65536、最终回答 16..512 KiB、整轮 reasoning 16..768 KiB;0 选择上述默认值,边界不能关闭。语义工具另受整轮 32 次、单工具 8 次、相同规范化参数 2 次的默认三层共享限额(MaxToolCallsPerTurn / MaxToolCallsPerTool / MaxRepeatedToolCalls,负数均表示不限);相同读调用可按配置重试,相同规范化写调用每轮最多执行一次,避免模型重放产生重复副作用。耗尽后 assistant_budget 快照带稳定 code/limit/tool 且不执行超额工具;ui_plan_update、动态 tool_search 与子任务委派是内部编排,分别受目录、计划校验与子任务额度约束,不重复计入语义工具额度。Backend 还分别限制真实未返回的关键宿主回调、观察回调和模型/Service 操作,默认 32/32/64,可用 MaxOutstandingHostCallbacks / MaxOutstandingObservations / MaxOutstandingOperations 配置;0 使用默认值,合法显式值为 1..4096
  • 回答格式:assistant_delta.deltaassistant_done.message 可通过可选 content_format 声明 text / markdown / html。内置 Eino 助手输出语义 HTML 片段,由前端统一清洗后展示;非空的 assistant_done.content_format 对最终快照具有权威性,空字段保留流式格式或旧版自动检测,未知值必须按纯文本处理。assistant_done 还可附加 provider 原始 finish_reason、规范化 usagerepaired 标记;这些是可选诊断元数据,不应被客户端当作授权或最终计费依据。若模型在工具执行后返回空答复或因输出上限截断,服务端只用已完成工具证据整理一次最终 HTML,不重新执行工具
  • 演示能力:GET /api/chat/models 可用 preview_only:true 表示仅提供确定性界面预览;它不等同于 model_ready,前端会保持预览可操作,同时明确提示回答并非来自真实 AI
  • 过程事件:assistant_thinking 是增量流,不是一次性快照;同一次模型输出共享 thinking_id,最后一条带 thinking_done:true。客户端应按 ID 追加并在 done 后折叠。中途流式重试前服务端发送 assistant_reset,新客户端应清除本轮已投影草稿;旧客户端可忽略。assistant_done.message 始终是权威最终快照,可覆盖此前的流式草稿;工具按 tool_call.id 原位更新,tool_result.tool_summary 是面向用户的结果,tool_result / tool_debug 只适合放在折叠技术详情中
  • 确认后恢复:当前 Deep 能力值为 manual_tool_only——确认卡只执行卡片中的单个工具调用,整体计划保持暂停,不会在用户不知情时自动续跑;需要继续时由客户端发起新的用户轮次。未来接入 checkpoint 后再通过能力字段升级
  • 工具:ui_item_list/get/put/deleteui_items_replaceui_page_list/set/deleteui_theme_get/setui_kv_get/setui_revision_list/rollbackui_page_errorsui_page_inspect(经会话 WS 请求内核回传实时渲染状态:按视口交集、祖先计算样式与 overflow 裁剪估算每个 Item 的可见性,并返回尺寸、文本摘要与报错;支持 ids 精确过滤及 offset/limit 分页,每页最多 20 项,按 next_offset 继续;页面离线时数秒超时返回)、api_probe(只读探测业务 API)、ui_plan_update(Deep 模式的结构化计划工具),以及结构专用的 ui_template_get/put_current/publish/slot_delete
  • Template 工具契约:ui_template_get(page) 只返回 Draft/Published 结构和 CAS revision,不读取 Item content;put_current 只接受完整结构 Draft(服务端严格拒绝未知字段、业务正文、HTML/CSS/JS/URL),保存后不会改变运行时;publish 才把 Draft 上线;slot_delete 保留 Item 并把引用迁入 Root fallback。所有 Template 写入都必须带 expected_revision 并确认
  • 风险控制:读操作直接执行;安装 HTML/JS/CSS 的 Item 写入、全部 Template 写入及所有高风险操作默认必须点确认卡(HMAC 签名 + 原子一次性 nonce + 15 分钟 TTL)。签名同时绑定用户、逻辑 SessionID、应用/租户能力指纹及语义工具协议版本,同密钥下也不能跨会话或跨应用执行/撤销。只有完全受控环境可用 AllowUnconfirmedItemWrites=true 放开普通 Item 写;ConfirmWrites=true 会进一步确认其余写操作
  • 多轮历史默认在 Backend 级内存中按 csid 恢复,并受会话数、每会话事件数、字节数与 TTL 四重上限约束;同一 Backend 内相同作用域 SessionID 同时只允许一个 WS owner,新连接会非阻塞接管,并在旧连接的模型、工具和确认操作真实返回后才开始执行。生产可注入持久化 SessionStore 或共享 ConfirmNonceStore;任一外部 Store 存在时,NewChecked 都强制要求稳定的 SessionNamespace(应用/租户命名空间),避免同一密钥/Store 被多个应用复用时确认卡串域,非结构化授权或回调语义变化时递增 SessionCapabilityVersion。兼容构造器 New 遇到未命名的任一外部 Store 会生成实例级临时 namespace,安全隔离但不会跨重启/副本续接。内部 SessionID 同时绑定这些值、只读策略、工具/API 目录、确认策略、探测边界与宿主提示词的能力指纹,权限或能力变化会自动切换历史作用域,且不会把 user/csid 明文暴露给外部 Store。该 owner 协调只保证单进程;多副本必须由宿主提供 sticky routing 或分布式 session lease/锁,单独共享 SessionStore 不能防止两个 Backend 并发执行同一会话
  • 修 bug 闭环:内核收集 Item 报错 → AI 用 ui_page_errors 读取、ui_page_inspect 核对真实屏幕 → 重写 Item 修复

生产治理(SDK 内建)

能力 用法
数据库存储 sqlstore.New(db, sqlstore.Postgres/MySQL/SQLite)(纯 database/sql,Init() 建表;PostgreSQL/MySQL 的整状态读取使用一致性快照。旧库迁移前仍可读,写入会返回明确错误,升级后须先运行一次 Init()
审计留痕 WithAudit(fn) —— 全部写操作(含 AI 发起)同步回调 AuditEvent{User,Action,By,Note,Detail}
访问策略 WithAccessPolicy(fn) → Access{ReadOnly, ChatDisabled} —— 只读视图/按用户禁 AI,HTTP 与 AI 工具双层拦截
宿主直调 Handler.ServiceForRequest(r) 返回已绑定用户的 ScopedService,复用身份重验、访问策略和写限流;Handler 自身的 Service 方法是受信任管理 API
限流配额 WithLimits{MaxSSEPerUser:8, MaxWSPerUser:4, ChatTurnsPerMinute:20, WritesPerMinute:240, MaxKVKeysPerUser:256}(默认即生效;对话轮次=真金白银)
指标 WithMetrics(m) 或零依赖 Handler.Stats():sse_connections/ws_sessions/chat_turns/writes/rate_limited…
备份迁移 ExportUser/ImportUser/PurgeUser(Go API)+ GET /api/export;v3 契约完整包含 Items/Pages/Templates/KV/Revisions,导入先按当前硬资源边界全量校验并原子替换。升级前应先离线清理旧 Store 中的超限/无效状态,否则新版会 fail-closed,而非静默截断。PurgeUser 清空业务载荷但为防旧 CAS 复活会保留版本墓碑;SQL 版本表仍含用户键,不等同于完整物理擦除/GDPR 流程
优雅停机 Handler.Close()(断 SSE/WS);SSE 55 分钟强制到期配合 LB 排空;资源 ETag 304 协商缓存
多副本 写一致性:VersionedUserStateStore 条件写(内置三种 Store 均实现,含配额、错误日志和 Template expected_revision 的跨副本真 CAS),冲突自动重读重试,无需 sticky 路由;EraseUser 清空数据但保留递增版本墓碑。实时事件:外部 Broadcaster 默认只发送权威 resync,避免提交与发布反序回滚页面;ihtml/redisbroadcast 使用非阻塞有界发布队列、Redis Cluster 同槽键和哈希用户标识。它的新 Redis namespace 与旧版不互通;滚动升级时仅在过渡窗口显式开 redisbroadcast.Options{LegacyNamespaceMigration:true} 双发/双订阅,旧实例退完立即关闭并清理旧 <prefix>:ver:<user> 键。只有宿主把同一用户的完整写调用跨副本全局串行化时才可用 WithOrderedBroadcaster 保留增量;Broadcaster 自身排序不够
对话治理 chat.Config{ConfirmSecret, ConfirmNonceStore, SessionStore, SessionNamespace, SessionCapabilityVersion, TurnInterceptor, WriteInterceptor, WritesPerMinute, AllowModelSelect, EnableDeepAgent, MaxModelCallsPerTurn, MaxModelRetries, MaxSubtasksPerTurn, MaxToolCallsPerTurn, MaxToolCallsPerTool, MaxRepeatedToolCalls, MaxProviderResponseBytes, MaxModelOutputBytesPerCall, MaxModelOutputBytesPerTurn, MaxModelOutputEventsPerTurn, MaxAssistantMessageBytes, MaxReasoningBytesPerTurn, MaxTurnDuration, MaxOutstandingHostCallbacks, MaxOutstandingObservations, MaxOutstandingOperations};Fast 默认启用,Deep 显式开放;会话经能力作用域化的 csid 跨断线恢复上下文;宿主拦截器受 context 与 5 秒上限保护,只应通过 chat.NewUserFacingError 标记已经审查、允许发给终端用户的错误文案,其他内部错误会被泛化
AI 运行时扩展 RuntimeConfigProvider 在每轮模型创建前只覆盖 generation 字段,不能改变凭据、URL、提示词、工具权限或预算,等待时间受 5 秒和本轮 deadline 的较短者约束;TurnObserver 以 best-effort 异步接收不含正文、工具参数和原始错误的 started/completed/failed 指标快照,同一轮内已接收的快照保持顺序,但过载或不遵守 context 的回调可导致后续快照丢弃。它使用独立观察额度,回调错误、阻塞与 panic 不影响对话,也不会挤占授权/确认等关键宿主回调

前端内核能力(v0.6)

  • SPA 生命周期ihtml.destroy() 回收监听、请求、定时器、SSE、WS 与 DOM,支持 mount→destroy→mount 与同页多实例并存ihtml.instances() 枚举);ready() Promise 与 onReady/onAuthError/onConnection 回调
  • 鉴权刷新auth() 只在每次 REST 请求时求值并写入 X-Ihtml-User header;原生 EventSource/WebSocket 无法设置自定义 header,可选 connectionAuth() 只返回一次性或短期连接票据并放入连接 query(同源 cookie 场景无需提供)。不要返回会进入 URL 的长期 bearer;票据的签发与校验由宿主负责。refreshAuth() 会清理旧用户 UI 与对话状态,再按新身份完整重载
  • 实时一致性:首帧 resync 建立基线,事件版本缺口、队列溢出、外部副本通知与页面注册表变化自动触发全量同步;单进程内置 hub 仍保留低延迟增量更新
  • 管理与 AI:桌面为紧凑浮动操作组,移动端为安全区底部操作栏;AI 处理记录采用原生可折叠时间线,折叠后与正式回复保持紧凑间距
  • AI 标准库(prompt 已教,AI 生成质量的地基):ihtml.chart.line/bars/spark 零依赖 SVG 图表(tokens 配色、自动重绘、随 Item 回收)、ihtml.fmt.num/money/date/ago/pctihtml.util.esc/debounce/throttleihtml.poll/listen(自动绑定 Item 生命周期)、ihtml.ui.modal/paginate
  • i18n:内核文案 zh/en 随 locale 自动切换,mount({strings:{...}}) 逐条覆写
  • 无障碍与细节:toast aria-live、Escape 关闭、prefers-reduced-motion、safe-area、100dvh、移动端触摸目标下限

安全模型

  • 鉴权默认拒绝:没有显式 WithUserResolver 时所有用户 API 返回 401;开发 resolver 不应进入生产
  • 数据边界在服务端 API:Item 是与宿主同源、可执行的可信代码,它的 JS 以浏览者身份运行;服务端接口必须自行鉴权,Item 本身不应包含敏感信息。若内容来自不受信任的作者,应改用独立 origin + sandbox iframe,而不是依赖 allowExternalAssets:false
  • 非安全 HTTP 方法只接受同源或 CORS 白名单 Origin,并要求 JSON Content-Type;WebSocket 同样执行 Origin 校验
  • postMessage 默认只接受同源父页面;跨域嵌入必须同时显式配置允许来源
  • Item 默认只来自用户自己的 AI 会话与 API 写入;如果将来做"分享给他人",请对分享内容走 iframe 沙箱或审核后再放行
  • 对话回复的语义 HTML 在前端经白名单清洗后渲染,模型输出本身不被视为可信代码;工具返回的数据在 prompt 里声明为不可信数据
  • 内核资源刻意 no-cache:升级库后旧内核不会与新 API 脱节

扩展点

type Store interface { … }        // 换数据库: 实现 Items/ReplaceItems/KV*/Revision*
type UserStateStore interface { … }          // 可选: 整状态原子读写(强烈建议实现)
type VersionedUserStateStore interface { … } // 可选: 版本条件写, 多副本部署必需
type Broadcaster interface { … }  // 换事件通道: redisbroadcast 提供跨副本实现
type UserResolver func(*http.Request) (string, error)
type ChatBackend interface {      // 换 AI 实现(自研/别的编排框架)
    NewSession(ctx, *ChatSessionContext) (ChatSession, error)
}
type ItemService / KVService / PageService / TemplateService interface { … }
type VersionedKVService interface { … } // 可选: 单键读取版本 + 原子 CompareAndSet
type Service interface { … }      // 完整受信任管理 API;*Handler 实现
type ScopedService interface { … } // 已绑定一个用户;请求路由优先用 ServiceForRequest
type ScopedVersionedKVService interface { … } // 对 ScopedService 做类型断言取得

VersionedUserStateStore 的版本 0 只表示“从未写入”。每个成功写操作(含删除不存在的 KV)都必须推进版本;EraseUser 也必须留下更新后的空状态墓碑,不能重置或复用版本。公共 KV 遇到 ReservedKVPrefix 会返回可用 errors.Is(err, ErrReservedKVKey) 判断的错误。第三方 Store 应运行 storetest.Run,它会同时验证并发 CAS、RawMessage 字节保真、输入/输出所有权和删除后的版本语义。FileStore 用可响应 context 取消的进程级 gate 与系统文件锁支持同目录多句柄/协作进程 CAS,新数据文件为 0600,v2 文件同时写精确 kv_bytes 和旧版可读的语义 kv;它适合小规模单版本部署,升级/回滚应停写切换,不能让不识别锁协议的旧二进制与新版并行写。生产数据库仍优先使用 sqlstore

编辑器或多标签页应使用 KVGetWithRevision + KVCompareAndSet,不要自行实现“先 GET、再无条件 PUT”。版本令牌是服务端基于 RawMessage 完整字节生成的不透明 SHA-256 表示;首次创建使用 MissingKVRevision。冲突返回 KVConflictError(同时匹配 ErrKVConflict / ErrConflict),其中包含最新版本和值。HTTP GET /api/kv/{key} 会增量返回 revisionPUT 可选传 expected_revision;字段缺失仍保持旧版无条件写语义。内置 Versioned Store 可跨 Handler/副本原子比较并写入;第三方 legacy Store 仅保证单 Handler 锁内语义。DeleteItems 会在同一用户状态事务中清理所有 item-layout.* 引用。

Documentation

Overview

Package ihtml 提供"AI 动态前端 UI"功能库: 一个内嵌的空壳页面(微内核) 按用户加载服务端记录的一组 UI Item(HTML/JS/CSS 片段)并在浏览器里执行, 拼装出该用户的完整界面。Item 由 AI 助手通过对话生成并持久化, 页面通过 SSE 实时应用变更, 无需刷新。

库对外的三个扩展点:

  • Store: 持久化接口, 内置 Memory/File 两个实现, 生产可换数据库;
  • UserResolver: 从宿主已验证的会话或令牌解析当前用户;
  • ChatBackend: AI 对话后端, 子包 chat 提供基于 eino 的实现。

Index

Examples

Constants

View Source
const (
	// ChatProtocolVersion identifies the additive WebSocket/capabilities contract.
	ChatProtocolVersion = 1
	// MaxChatOutboundFrameBytes is the public wire ceiling for one server-to-client
	// chat event. Backends that pre-build signed or otherwise atomic payloads
	// should reserve envelope headroom below this value.
	MaxChatOutboundFrameBytes = (1 << 20) - 1

	ChatClientUserMessage = "user_message"
	ChatClientConfirmTool = "confirm_tool"
	// ChatClientCancelTool revokes one signed confirmation card without running
	// it. The backend verifies the card and atomically consumes its nonce, so a
	// later confirm_tool replay is rejected.
	ChatClientCancelTool = "cancel_tool"
	ChatClientCancel     = "cancel"
	// ChatClientInspectResult 是内核对 inspect_request 的应答帧,
	// 携带 inspect_id 与实时页面渲染状态(不可信数据)。
	ChatClientInspectResult = "inspect_result"

	ChatStreamTurnStart         = "turn_start"
	ChatStreamAssistantStatus   = "assistant_status"
	ChatStreamAssistantThinking = "assistant_thinking"
	ChatStreamAssistantDelta    = "assistant_delta"
	// ChatStreamAssistantReset discards assistant deltas already projected for
	// the current turn. The built-in backend uses it before a streaming retry;
	// legacy clients may ignore it because assistant_done remains authoritative.
	ChatStreamAssistantReset       = "assistant_reset"
	ChatStreamAssistantPlan        = "assistant_plan"
	ChatStreamAssistantAgent       = "assistant_agent"
	ChatStreamAssistantBudget      = "assistant_budget"
	ChatStreamToolStart            = "tool_start"
	ChatStreamConfirmationRequired = "confirmation_required"
	// ChatStreamCancelToolResult is the authoritative acknowledgement for one
	// cancel_tool request. ToolID and ConfirmationNonce correlate the result to
	// the exact card without echoing its signed token or arguments.
	ChatStreamCancelToolResult = "cancel_tool_result"
	ChatStreamToolResult       = "tool_result"
	ChatStreamAssistantDone    = "assistant_done"
	ChatStreamTurnError        = "turn_error"
	// ChatStreamInspectRequest 由服务端(ui_page_inspect 工具)发起, 请求内核
	// 回传当前页面的实时渲染状态; 内核以 inspect_result 帧应答同一 inspect_id。
	ChatStreamInspectRequest = "inspect_request"

	ChatToolPending   = "pending"
	ChatToolRunning   = "running"
	ChatToolSuccess   = "success"
	ChatToolError     = "error"
	ChatToolCancelled = "cancelled"

	// ChatToolBudget* 是 assistant_budget 快照和内置语义工具
	// 返回的稳定机器可读值;客户端不应匹配本地化错误文案。
	ChatToolBudgetExhaustedCode   = "tool_call_budget_exhausted"
	ChatToolBudgetStatusExhausted = "budget_exhausted"
	ChatToolBudgetLimitTotal      = "total_tool_calls"
	ChatToolBudgetLimitPerTool    = "per_tool_calls"
	ChatToolBudgetLimitIdentical  = "identical_arguments"

	ChatRiskLow     = "low"
	ChatRiskMedium  = "medium"
	ChatRiskHigh    = "high"
	ChatRiskBlocked = "blocked"

	// ChatContentFormat* describe assistant answer bodies. Status, reasoning,
	// errors and tool summaries remain plain text regardless of this field.
	ChatContentFormatText     = "text"
	ChatContentFormatMarkdown = "markdown"
	ChatContentFormatHTML     = "html"
)
View Source
const (
	// EventItems 表示 Item 集合变化。
	EventItems = "items"
	// EventTemplates 表示布局模板变化。
	EventTemplates = "templates"
	// EventKV 表示某个用户级 KV 键变化(Event.Key 为键名)。
	EventKV = "kv"
	// EventPages 表示页面注册表变化。
	EventPages = "pages"
	// EventResync 要求客户端放弃增量、全量刷新(Event.Reason 说明原因)。
	EventResync = "resync"

	// EventOpPut 是 items/templates 事件的"写入/更新"操作。
	EventOpPut = "put"
	// EventOpPublish 是模板 Draft 发布为 Published 的操作。
	EventOpPublish = "publish"
	// EventOpDelete 是删除操作(Event.IDs 为目标)。
	EventOpDelete = "delete"
	// EventOpReplace 是 Item 全集整体替换操作。
	EventOpReplace = "replace"
	// EventOpRollback 是快照回滚操作。
	EventOpRollback = "rollback"
	// EventOpImport 是全量导入操作。
	EventOpImport = "import"
	// EventOpPurge 是用户数据清除操作。
	EventOpPurge = "purge"

	// ResyncSubscribe: 订阅建立时的基线 resync。
	ResyncSubscribe = "subscribe"
	// ResyncOverflow: 订阅积压溢出、增量队列已被清空。
	ResyncOverflow = "overflow"
	// ResyncPeriodic: 周期性校准(兜底不能报告丢包的外部 Broadcaster)。
	ResyncPeriodic = "periodic"
	// ResyncExternal: 外部/跨副本 Broadcaster 收到变更通知后要求全量刷新。
	// 持久化提交与消息发布不在同一事务内,因此外部通道不携带可能
	// 因发布反序而过期的增量载荷,客户端改为重读已提交的权威状态。
	ResyncExternal = "external"
)

SSE 事件协议常量。这些字符串是公共线格式契约(见 COMPATIBILITY.md): 同一协议版本内只做增量扩展, 消费者可安全地按常量比较。

View Source
const (
	// MaxItemContentBytes 是单个 Item 内容的上限。
	MaxItemContentBytes = 512 << 10
	// MaxUserItemBytes bounds the aggregate serialized source carried by all
	// Items for one user. Without an aggregate bound, repeated valid 512 KiB
	// writes could make every list, snapshot and AI inventory operation copy
	// hundreds of MiB at once.
	MaxUserItemBytes = 8 << 20
	// MaxUserRevisionBytes bounds the aggregate Item/Template source retained in
	// automatic snapshots for one user. Count-only retention is insufficient
	// when a few otherwise-valid snapshots are several MiB each.
	MaxUserRevisionBytes = 64 << 20
	// MaxRevisionsPerUser is the hard safety ceiling even when count-based
	// retention is disabled with WithMaxRevisions(0). The byte ceiling remains
	// the primary limit for content-heavy snapshots; this one bounds empty or
	// nearly-empty revision metadata and third-party Store implementations.
	MaxRevisionsPerUser = 4096
	// MaxItemsPerUser 是单个用户 Item 数量上限。
	MaxItemsPerUser = 512
	// Item metadata is intended for compact runtime hints such as slot/module.
	// Bound it separately so small/empty content cannot hide an unbounded map.
	MaxItemMetaEntries    = 64
	MaxItemMetaKeyBytes   = 128
	MaxItemMetaValueBytes = 1024
	// DefaultMaxRevisions 是默认保留的历史快照条数。
	DefaultMaxRevisions = 50
	// MaxPageErrors 是每个用户保留的页面错误条数。
	MaxPageErrors = 50
	// CurrentDataVersion 是 Revision 与 UserExport 当前使用的数据契约版本。
	// v1/零值快照只包含 Items;v2 增加页面注册表;v3 增加模板目录。
	CurrentDataVersion = 3
)
View Source
const (
	// MaxKVEntriesPerUser is a hard structural ceiling across public and
	// kernel-owned keys. Limits.MaxKVKeysPerUser remains the configurable public
	// quota; this ceiling keeps an explicitly unlimited or malformed Store from
	// creating an unbounded map/list operation.
	MaxKVEntriesPerUser = 4096
	// MaxUserKVBytes bounds aggregate key/value source bytes copied in a complete
	// UserState. It is deliberately above the default 256 x 64 KiB public quota
	// while remaining finite for third-party Store implementations.
	MaxUserKVBytes = 32 << 20
)
View Source
const (
	// MaxTemplateVersionBytes bounds one Draft or Published structural document.
	// The estimate counts every source string/RawMessage plus conservative JSON
	// framing, preventing individually valid properties from multiplying into a
	// hundreds-of-MiB template.
	MaxTemplateVersionBytes = 1 << 20
	// MaxTemplateCatalogBytes bounds all Draft/Published structures retained for
	// one user. Per-document limits alone would still allow 128 maximum-sized
	// templates to multiply every catalog load and snapshot.
	MaxTemplateCatalogBytes = 8 << 20
	// MaxTemplateCatalogStorageBytes bounds the encoded internal catalog record.
	// It is intentionally larger than the logical structural budget to allow for
	// JSON field framing and escaping, while remaining below the whole-user KV
	// ceiling. Writers in this package disable unnecessary HTML escaping.
	MaxTemplateCatalogStorageBytes = 16 << 20
)
View Source
const ItemLayoutKVPrefix = "item-layout."

ItemLayoutKVPrefix identifies editor-owned per-Item layout records. These records remain ordinary KV so the browser can persist them, but lifecycle operations such as deleting a page must keep them in sync with that page.

View Source
const MaxKVValueBytes = 64 << 10

MaxKVValueBytes 是单个 KV 值的上限。

View Source
const MissingKVRevision = "missing"

MissingKVRevision 是 KVGetWithRevision 读取一个不存在的键时,调用方在首次 KVCompareAndSet 中使用的版本前置条件。它与已有值的 SHA-256 版本空间分离。

View Source
const ReservedKVPrefix = "__ihtml"

ReservedKVPrefix identifies kernel-owned keys that are intentionally hidden from the public KVService. Use the dedicated Page, Template, Error, and layout APIs instead of reading or mutating these records directly.

Variables

View Source
var (
	// ErrServiceScope 表示会话后端尝试访问非当前用户数据。
	ErrServiceScope = errors.New("service user scope mismatch")
	// ErrAccessDenied 表示连接建立后鉴权/访问策略已撤销。
	ErrAccessDenied = errors.New("access denied")
	// ErrWriteRateLimited 表示 HTTP/AI 共享的用户写频率额度已用尽。
	ErrWriteRateLimited = errors.New("write rate limit exceeded")
)
View Source
var (
	// ErrConflict indicates a failed optimistic-concurrency (CAS) check.
	ErrConflict = errors.New("ihtml: revision conflict")
)
View Source
var ErrInvalidHandlerConfig = errors.New("ihtml: invalid handler configuration")

ErrInvalidHandlerConfig matches constructor failures reported by NewHandler.

View Source
var ErrInvalidServiceScope = errors.New("ihtml: invalid service scope")

ErrInvalidServiceScope is returned when a user-scoped service cannot be constructed safely.

View Source
var ErrKVConflict = errors.New("ihtml: kv revision conflict")

ErrKVConflict 表示一次带版本前置条件的 KV 写入所依据的值已经变化。 调用方必须重新读取该键,并决定是重放本地修改还是让用户处理冲突。

View Source
var ErrNotFound = errors.New("ihtml: not found")

ErrNotFound 表示请求的对象(KV 键/快照)不存在。

View Source
var ErrReservedKVKey = errors.New("ihtml: reserved kv key")

ErrReservedKVKey is returned by the public KVService when a caller attempts to read, write, or delete a kernel-owned key.

View Source
var ErrVersionConflict = errors.New("ihtml: user state version conflict")

ErrVersionConflict 由 VersionedUserStateStore.ReplaceUserStateVersioned 返回, 表示存储中的用户状态版本已不等于调用方读取时的版本(通常是另一个副本先写入)。 调用方应重读状态、重放变更后重试。

Functions

func DefaultUserResolver

func DefaultUserResolver(*http.Request) (string, error)

DefaultUserResolver 是安全默认: 在宿主未显式接入认证时拒绝请求。 ihtml 无法自行判断哪个 cookie/header 代表已验证会话,因此生产 服务必须通过 WithUserResolver 提供宿主的验证逻辑。

func DevelopmentUserResolver

func DevelopmentUserResolver(r *http.Request) (string, error)

DevelopmentUserResolver 从 uid query、X-Ihtml-User header 或 ihtml_uid cookie 直接取用户名。这些值都由客户端控制,只能用于本地演示/测试, 绝不应用于生产环境。

func ValidatePageName

func ValidatePageName(name string) error

ValidatePageName 校验页面名(小写字母数字开头, 允许 -_)。

Types

type APISpec

type APISpec struct {
	Name        string `json:"name"`            // 标识, 如 player_search
	Title       string `json:"title,omitempty"` // 一句话名称
	Method      string `json:"method"`          // 目前探测工具只放行 GET
	Path        string `json:"path"`            // 页面同源绝对路径, 如 /capi/players
	Description string `json:"description"`     // 参数与返回结构说明(给 AI 看)
}

APISpec 描述宿主提供的一个业务 API, 用于告知 AI "有哪些接口可用"。

type Access

type Access struct {
	// ReadOnly 拒绝一切写操作(HTTP 写接口与 AI 工具写), 用于
	// 客服代查/演示账号/免费套餐只读等场景。
	ReadOnly bool
	// ChatDisabled 对该用户禁用 AI 对话(入口隐藏, WS 拒绝)。
	ChatDisabled bool
}

Access 是一次请求的访问策略结果。

type AuditEvent

type AuditEvent struct {
	Time   time.Time       `json:"time"`
	User   string          `json:"user"`
	Action string          `json:"action"` // items.put/items.delete/items.replace/items.rollback/kv.set/kv.delete/page.set/page.delete/user.import/user.purge
	By     string          `json:"by"`     // api / chat / import ...
	Note   string          `json:"note,omitempty"`
	Detail json.RawMessage `json:"detail,omitempty"` // ids/key/page 等结构化明细
}

AuditEvent 是一次数据变更的审计记录, 经 WithAudit 同步交给宿主留痕。 与滚动淘汰的快照不同, 审计流不丢失历史。

type Breakpoint

type Breakpoint struct {
	ID       string  `json:"id"`
	Name     string  `json:"name,omitempty"`
	MinWidth int     `json:"min_width,omitempty"`
	MaxWidth int     `json:"max_width,omitempty"`
	Columns  int     `json:"columns,omitempty"`
	Gap      float64 `json:"gap,omitempty"`
}

Breakpoint defines a responsive editing surface. A zero MinWidth/MaxWidth is open-ended; Columns and Gap apply to flow layout.

type Broadcaster

type Broadcaster interface {
	// Publish 向该用户的所有订阅者投递事件。实现应保证不阻塞调用方。
	// 若实现会丢弃积压事件,必须改为投递 Type="resync" 以显式
	// 告知订阅者,不得静默丢弃。
	Publish(user string, ev Event)
	// Subscribe 订阅该用户的事件流; cancel 幂等, 调用后通道会被关闭。
	Subscribe(user string) (ch <-chan Event, cancel func())
}

Broadcaster 把变更事件广播给某用户的全部页面订阅者。 默认实现是进程内 hub(单副本); 多副本部署时用 WithBroadcaster 接入 Redis Pub/Sub / NATS 等跨节点通道, 否则副本 A 的写入无法推到 连接在副本 B 上的页面。

Handler 只在内置进程内 hub 上发送低延迟增量。通过 WithBroadcaster 注入的外部 Broadcaster 收到 reason=external 的 resync 通知:Store 提交顺序与外部消息发布顺序可能相反, 全量重读才能保证客户端不应用过期载荷。只有宿主把同一用户在所有 副本上的完整写调用(含提交到 Publish)全局串行化时,才可显式使用 WithOrderedBroadcaster 保留增量载荷;Broadcaster 内部排序本身不够。

type ChatAgentStage

type ChatAgentStage struct {
	Mode         string `json:"mode"`  // fast / deep
	Agent        string `json:"agent"` // root / ui_inspector / ui_reviewer
	TaskID       string `json:"task_id,omitempty"`
	ParentTaskID string `json:"parent_task_id,omitempty"`
	Phase        string `json:"phase,omitempty"` // turn / plan / inspect / review / approval
	Status       string `json:"status"`          // running / completed / paused / error
	Summary      string `json:"summary,omitempty"`
	// ApprovalResume 明示确认后的恢复能力。当前 deep 使用 manual_tool_only:
	// 只执行确认卡,不会静默恢复或宣称整个计划完成。
	ApprovalResume string `json:"approval_resume,omitempty"`
}

ChatAgentStage 描述主 Agent 或只读子 Agent 的阶段状态。它独立于工具卡, 前端可据此渲染层级任务,而无需解析模型文本。

type ChatBackend

type ChatBackend interface {
	NewSession(ctx context.Context, sctx *ChatSessionContext) (ChatSession, error)
}

ChatBackend 为每条 WS 连接创建一个会话。子包 chat 提供基于 eino 的实现。

type ChatClientContext

type ChatClientContext struct {
	Viewport  ChatClientViewport `json:"viewport"`
	Page      ChatClientPage     `json:"page"`
	Locale    string             `json:"locale,omitempty"`
	TimeZone  string             `json:"time_zone,omitempty"`
	UserAgent string             `json:"user_agent,omitempty"`
}

ChatClientContext 是随 user_message 上送的页面上下文。

type ChatClientEvent

type ChatClientEvent struct {
	Type     string        `json:"type"`
	Messages []ChatMessage `json:"messages,omitempty"`
	Model    string        `json:"model,omitempty"`
	// AgentMode 为空或 fast 时走低延迟单 Agent;deep 仅在服务端显式开启后可用。
	AgentMode     string            `json:"agent_mode,omitempty"`
	ToolsEnabled  *bool             `json:"tools_enabled,omitempty"`
	ClientContext ChatClientContext `json:"client_context"`
	ToolCall      *ProposedToolCall `json:"tool_call,omitempty"`
	// InspectID/Inspect 仅用于 inspect_result: 应答服务端的同 ID inspect_request。
	InspectID string                `json:"inspect_id,omitempty"`
	Inspect   *ChatClientInspection `json:"inspect,omitempty"`
}

ChatClientEvent 是客户端发来的 WS 事件: type = user_message | confirm_tool | cancel_tool | cancel | inspect_result。

type ChatClientInspection

type ChatClientInspection struct {
	Page        string                     `json:"page,omitempty"` // 当前页面名, 空 = 单页/根
	Viewport    ChatClientViewport         `json:"viewport"`
	ThemeMode   string                     `json:"theme_mode,omitempty"` // light / dark
	Total       int                        `json:"total,omitempty"`      // 过滤后的 Item 总数
	Offset      int                        `json:"offset,omitempty"`
	Limit       int                        `json:"limit,omitempty"`
	NextOffset  int                        `json:"next_offset,omitempty"`
	Truncated   bool                       `json:"truncated,omitempty"` // 仍有后续分页
	Items       []ChatClientInspectionItem `json:"items,omitempty"`
	Errors      []string                   `json:"errors,omitempty"`       // 最近页面错误摘要
	VisibleText string                     `json:"visible_text,omitempty"` // 全页可见文本(截断)
}

ChatClientInspection 是内核对 inspect_request 的完整应答载荷(不可信数据)。

type ChatClientInspectionItem

type ChatClientInspectionItem struct {
	ID      string `json:"id"`
	Type    string `json:"type,omitempty"`
	Page    string `json:"page,omitempty"`
	Slot    string `json:"slot,omitempty"`
	Present bool   `json:"present"`           // 是否有已连接的 DOM 节点
	Visible bool   `json:"visible,omitempty"` // 是否实际占据可见空间
	Width   int    `json:"width,omitempty"`
	Height  int    `json:"height,omitempty"`
	Text    string `json:"text,omitempty"`  // 渲染文本摘要(截断)
	Error   string `json:"error,omitempty"` // 该 Item 最近一条运行错误
}

ChatClientInspectionItem 是内核回传的单个 Item 实时渲染状态(不可信数据)。

type ChatClientPage

type ChatClientPage struct {
	Path        string `json:"path,omitempty"`
	Title       string `json:"title,omitempty"`
	VisibleText string `json:"visible_text,omitempty"`
}

ChatClientPage 描述当前页面状态(路径/标题/可见文本), 属不可信数据。

type ChatClientViewport

type ChatClientViewport struct {
	Width            int    `json:"width,omitempty"`
	Height           int    `json:"height,omitempty"`
	DevicePixelRatio any    `json:"device_pixel_ratio,omitempty"`
	Mode             string `json:"mode,omitempty"` // pc / mobile
}

ChatClientViewport 描述前端视口, 供 AI 做屏幕自适应。

type ChatMessage

type ChatMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

ChatMessage 是一条对话消息。

type ChatModels

type ChatModels struct {
	ProtocolVersion int    `json:"protocol_version"`
	Provider        string `json:"provider,omitempty"`
	ModelReady      bool   `json:"model_ready"`
	// PreviewOnly 表示后端只提供确定性的界面预览,不代表真实模型已连接。
	// 客户端可继续开放输入以演示交互,但必须向用户说明回答并非来自 AI。
	PreviewOnly  bool     `json:"preview_only,omitempty"`
	CurrentModel string   `json:"current_model"`
	Models       []string `json:"models"`
	AgentModes   []string `json:"agent_modes,omitempty"`
	// DeepApprovalResume 当前为 manual_tool_only;未来安全接入 ADK checkpoint
	// 后可升级为 checkpoint,而无需客户端猜测。
	DeepApprovalResume string `json:"deep_approval_resume,omitempty"`
	RequestTimeoutMS   int64  `json:"request_timeout_ms"`
	// ModelSelect 为 true 时前端展示模型选择器(默认隐藏——
	// 模型是运维概念, 不该暴露给终端用户)。
	ModelSelect bool `json:"model_select,omitempty"`
}

ChatModels 是 GET /api/chat/models 的响应。

type ChatPlanItem

type ChatPlanItem struct {
	ID      string `json:"id"`
	Content string `json:"content"`
	Status  string `json:"status"` // pending / in_progress / completed
}

ChatPlanItem 是 Deep 模式向前端投影的一条结构化任务。

type ChatSession

type ChatSession interface {
	HandleEvent(ctx context.Context, ev *ChatClientEvent)
	Close()
}

ChatSession 处理一条连接上的客户端事件。实现应自行做好并发控制: HandleEvent 会在读循环 goroutine 里被顺序调用, 不应长时间阻塞。

type ChatSessionContext

type ChatSessionContext struct {
	// User 是 UserResolver 解析出的用户。
	User string
	// Service 用于读改 UI 数据。为兼容已有 ChatBackend 保留;新实现应优先
	// 使用 ScopedService,避免意外传入其他用户标识。
	Service Service
	// ScopedService 已绑定并持续校验当前用户,不暴露跨租户 user 参数。
	ScopedService ScopedService
	// Emit 向前端推流式事件, 并发安全; 连接断开后返回错误。
	Emit func(ChatStreamEvent) error
	// Request 是发起 WS 的原始请求, 供后端取额外参数/推导同源地址。
	Request *http.Request
	// APIs 是 WithAPIs 声明的业务 API 目录。
	APIs []APISpec
	// Access 是建立会话时的访问策略快照(只读时后端应隐藏/
	// 拒绝写工具)。Service 还会在每次调用时动态重验,防止撤权空窗。
	Access Access
}

ChatSessionContext 是为一条 WS 连接建立会话时交给后端的上下文。

type ChatStreamEvent

type ChatStreamEvent struct {
	Type   string `json:"type"`
	RunID  string `json:"run_id,omitempty"`
	TurnID string `json:"turn_id,omitempty"`
	Seq    int64  `json:"seq,omitempty"`
	Delta  string `json:"delta,omitempty"`
	// ContentFormat applies only to assistant_delta.delta and
	// assistant_done.message. Empty preserves the legacy auto-detected format;
	// clients must render unknown values as plain text. HTML is always untrusted
	// model output: third-party clients must parse it inertly and rebuild it
	// through a strict element/attribute/URL allowlist before attaching to DOM.
	ContentFormat string `json:"content_format,omitempty"`
	Thinking      string `json:"thinking,omitempty"`
	// ThinkingID groups the incremental reasoning events produced by one model
	// output. Legacy servers and clients may omit or ignore it.
	ThinkingID string `json:"thinking_id,omitempty"`
	// ThinkingDone marks the last event in a reasoning segment. A non-streamed
	// reasoning response can carry both Thinking and ThinkingDone in one event.
	ThinkingDone bool               `json:"thinking_done,omitempty"`
	Status       string             `json:"status,omitempty"`
	Message      string             `json:"message,omitempty"`
	ModelReady   bool               `json:"model_ready,omitempty"`
	ToolCalls    []ProposedToolCall `json:"tool_calls,omitempty"`
	ToolCall     *ProposedToolCall  `json:"tool_call,omitempty"`
	// ToolID/ConfirmationNonce are used by cancel_tool_result. They deliberately
	// expose only the identifiers the client already sent, never the signed card.
	ToolID            string          `json:"tool_id,omitempty"`
	ConfirmationNonce string          `json:"confirmation_nonce,omitempty"`
	ToolResult        json.RawMessage `json:"tool_result,omitempty"`
	ToolName          string          `json:"tool_name,omitempty"`
	ToolTitle         string          `json:"tool_title,omitempty"`
	ToolStatus        string          `json:"tool_status,omitempty"`
	ToolInput         string          `json:"tool_input,omitempty"`
	ToolSummary       string          `json:"tool_summary,omitempty"`
	ToolDebug         json.RawMessage `json:"tool_debug,omitempty"`
	Plan              []ChatPlanItem  `json:"plan,omitempty"`
	Agent             *ChatAgentStage `json:"agent,omitempty"`
	Budget            *ChatTurnBudget `json:"budget,omitempty"`
	// FinishReason and Usage are populated on assistant_done when the provider
	// exposes them. FinishReason is provider-defined; clients should display it
	// only as diagnostic metadata and must tolerate unknown values.
	FinishReason string     `json:"finish_reason,omitempty"`
	Usage        *ChatUsage `json:"usage,omitempty"`
	// Repaired reports that the visible answer was safely regenerated from
	// already-completed tool evidence after the original final answer was empty
	// or truncated. No tool is re-executed during this repair.
	Repaired bool   `json:"repaired,omitempty"`
	Error    string `json:"error,omitempty"`
	// InspectID 仅用于 inspect_request: 内核应答 inspect_result 时原样带回。
	InspectID     string   `json:"inspect_id,omitempty"`
	InspectOffset int      `json:"inspect_offset,omitempty"`
	InspectLimit  int      `json:"inspect_limit,omitempty"`
	InspectIDs    []string `json:"inspect_ids,omitempty"`
}

ChatStreamEvent 是服务端推给前端的流式事件, type 取值: turn_start / assistant_status / assistant_thinking / assistant_delta / assistant_reset / assistant_plan / assistant_agent / assistant_budget / tool_start / confirmation_required / cancel_tool_result / tool_result / assistant_done / turn_error。

type ChatTurnBudget

type ChatTurnBudget struct {
	ModelCalls           int64 `json:"model_calls"`
	MaxModelCalls        int64 `json:"max_model_calls,omitempty"`
	Subtasks             int64 `json:"subtasks"`
	MaxSubtasks          int64 `json:"max_subtasks,omitempty"`
	ToolCalls            int64 `json:"tool_calls"`
	MaxToolCalls         int64 `json:"max_tool_calls,omitempty"`
	MaxToolCallsPerTool  int64 `json:"max_tool_calls_per_tool,omitempty"`
	MaxRepeatedToolCalls int64 `json:"max_repeated_tool_calls,omitempty"`
	// ToolBudgetCode/Limit/Tool 仅在语义工具预算已锁存耗尽时设置。
	ToolBudgetCode  string `json:"tool_budget_code,omitempty"`
	ToolBudgetLimit string `json:"tool_budget_limit,omitempty"`
	ToolBudgetTool  string `json:"tool_budget_tool,omitempty"`
	DeadlineUnixMS  int64  `json:"deadline_unix_ms,omitempty"`
}

ChatTurnBudget 是一轮共享预算的快照;Deep 根 Agent 与所有子 Agent 共用。

type ChatUsage

type ChatUsage struct {
	InputTokens       int64 `json:"input_tokens,omitempty"`
	OutputTokens      int64 `json:"output_tokens,omitempty"`
	TotalTokens       int64 `json:"total_tokens,omitempty"`
	CachedInputTokens int64 `json:"cached_input_tokens,omitempty"`
	ReasoningTokens   int64 `json:"reasoning_tokens,omitempty"`
}

ChatUsage is the normalized token accounting reported by the model provider. Providers may omit individual counters; zero therefore means either zero usage or unavailable data. Usage is observational and must never be used as an authorization or billing source without reconciliation at the provider.

type CurrentTemplate

type CurrentTemplate struct {
	Template  Template `json:"template"`
	Persisted bool     `json:"persisted"`
	Migrated  bool     `json:"migrated,omitempty"`
}

CurrentTemplate is returned by the page-scoped convenience API. Persisted is false for a deterministic empty/legacy-derived draft that has not been saved.

type ErrorService

type ErrorService interface {
	// PageErrors 返回内核上报的最近页面错误(新的在前)。
	PageErrors(ctx context.Context, user string) ([]PageError, error)
	// ReportErrors 追加页面错误记录。
	ReportErrors(ctx context.Context, user string, errs []PageError) error
}

ErrorService is the browser runtime error capability.

type Event

type Event struct {
	// Type: "items" 表示 Item 集合变化; "templates" 表示模板变化;
	// "kv" 表示某个 KV 键变化; "pages" 表示页面注册表变化;
	// "resync" 表示客户端必须全量刷新。
	Type string `json:"type"`
	// Op 是 items/templates 事件的操作: put / publish / delete / replace /
	// rollback / import / purge。
	Op string `json:"op,omitempty"`
	// Items 携带 put 的新内容, 内核原地应用。
	Items []Item `json:"items,omitempty"`
	// Templates 携带模板 put/publish 的最新完整值。
	Templates []Template `json:"templates,omitempty"`
	// IDs 携带 delete 的目标。
	IDs []string `json:"ids,omitempty"`
	// Key 是 kv 事件变化的键名。
	Key string `json:"key,omitempty"`
	// By 是变更来源(api / chat / import 等), 供宿主联动与前端展示。
	By string `json:"by,omitempty"`
	// Version 是某用户事件流的单调版本。默认 hub 总会填充;
	// 外部 Broadcaster 若能提供全局单调版本也应填充。
	Version uint64 `json:"version,omitempty"`
	// Reason 说明 resync 原因: subscribe / overflow / periodic / external。
	Reason string `json:"reason,omitempty"`
}

Event 是推送给页面内核的实时事件(经 SSE 下发)。

type FileStore

type FileStore struct {
	// contains filtered or unexported fields
}

FileStore 把每个用户的数据存成一个 JSON 文件(<dir>/u<sha256>.json), 写入走临时文件+fsync+rename+目录同步并保持原子替换;在平台/文件系统 支持文件与目录同步语义时同时提供崩溃/掉电持久性。不支持目录同步的 平台只能保证原子可见性,不能承诺掉电后目录项已落盘。 适合单机小规模部署与本地开发; 大规模生产建议使用 sqlstore 或自行实现 Store。

func NewFileStore

func NewFileStore(dir string) (*FileStore, error)

NewFileStore 创建文件存储, dir 不存在时自动创建。

func (*FileStore) AddRevision

func (s *FileStore) AddRevision(ctx context.Context, user string, rev Revision, keep int) error

AddRevision 实现 Store。

func (*FileStore) EraseUser

func (s *FileStore) EraseUser(ctx context.Context, user string) error

EraseUser 实现 Store。

func (*FileStore) Items

func (s *FileStore) Items(ctx context.Context, user string) ([]Item, error)

Items 实现 Store。

func (*FileStore) KVDelete

func (s *FileStore) KVDelete(ctx context.Context, user, key string) error

KVDelete 实现 Store。

func (*FileStore) KVGet

func (s *FileStore) KVGet(ctx context.Context, user, key string) (json.RawMessage, error)

KVGet 实现 Store。

func (*FileStore) KVKeys

func (s *FileStore) KVKeys(ctx context.Context, user string) ([]string, error)

KVKeys 实现 Store。

func (*FileStore) KVSet

func (s *FileStore) KVSet(ctx context.Context, user, key string, value json.RawMessage) error

KVSet 实现 Store。

func (*FileStore) ReplaceItems

func (s *FileStore) ReplaceItems(ctx context.Context, user string, items []Item) error

ReplaceItems 实现 Store。

func (*FileStore) ReplaceUserState

func (s *FileStore) ReplaceUserState(ctx context.Context, user string, state UserState) error

ReplaceUserState 原子替换用户的完整状态。

func (*FileStore) ReplaceUserStateVersioned

func (s *FileStore) ReplaceUserStateVersioned(ctx context.Context, user string, state UserState, expected uint64) error

ReplaceUserStateVersioned 实现 VersionedUserStateStore。

func (*FileStore) Revision

func (s *FileStore) Revision(ctx context.Context, user, id string) (Revision, error)

Revision 返回指定完整快照。

func (*FileStore) RevisionItems

func (s *FileStore) RevisionItems(ctx context.Context, user, id string) ([]Item, error)

RevisionItems 实现 Store。

func (*FileStore) Revisions

func (s *FileStore) Revisions(ctx context.Context, user string) ([]RevisionInfo, error)

Revisions 实现 Store。

func (*FileStore) UserState

func (s *FileStore) UserState(ctx context.Context, user string) (UserState, error)

UserState 原子读取用户的完整状态。

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler 是库的 HTTP 入口: 服务空壳页面、内核资源与全部 /api/* 接口。 可挂在根路径, 也可经 http.StripPrefix 挂在任意子路径(内核用相对路径请求)。

func New

func New(store Store, opts ...Option) *Handler

New creates a Handler and preserves the original panic-on-programmer-error behavior for source compatibility. NewHandler is recommended for SDK users that want startup errors returned to their application.

func NewHandler

func NewHandler(store Store, opts ...Option) (*Handler, error)

NewHandler validates dependencies and options, then creates a Handler.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/zdypro888/ihtml"
)

func main() {
	handler, err := ihtml.NewHandler(
		ihtml.NewMemoryStore(),
		ihtml.WithUserResolver(func(*http.Request) (string, error) { return "tenant-user", nil }),
		ihtml.WithPageTitle("Embedded workspace"),
	)
	if err != nil {
		panic(err)
	}
	defer handler.Close()

	request := httptest.NewRequest(http.MethodGet, "/api/config", nil)
	response := httptest.NewRecorder()
	handler.ServeHTTP(response, request)
	fmt.Println(response.Code)
}
Output:
200

func (*Handler) Close

func (h *Handler) Close() error

Close 优雅停机: 拒绝新连接、断开全部 SSE/WS。应在 http.Server.Shutdown 前调用(WS 是被劫持的连接, Server.Shutdown 不会等待它们)。

func (*Handler) DeleteItems

func (h *Handler) DeleteItems(ctx context.Context, user string, ids []string, by, note string) ([]string, error)

DeleteItems 实现 Service。

func (*Handler) DeletePage

func (h *Handler) DeletePage(ctx context.Context, user, name, by string) ([]string, error)

DeletePage 实现 Service。

func (*Handler) DeleteTemplate

func (h *Handler) DeleteTemplate(ctx context.Context, user, id string, expectedRevision uint64, by string) error

DeleteTemplate deletes only the template and clears its page/root bindings. Items and their source code remain untouched.

func (*Handler) DeleteTemplateSlot

func (h *Handler) DeleteTemplateSlot(ctx context.Context, user, id, slotID string, expectedRevision uint64, by string) (Template, error)

DeleteTemplateSlot removes a slot from the draft and moves all its Item placements to the unassigned tray. It never mutates the Item collection.

func (*Handler) ExportUser

func (h *Handler) ExportUser(ctx context.Context, user string) (*UserExport, error)

ExportUser 导出该用户全量数据(UserExport 即备份契约)。

func (*Handler) GetCurrentTemplate

func (h *Handler) GetCurrentTemplate(ctx context.Context, user, page string) (CurrentTemplate, error)

GetCurrentTemplate resolves the root or a registered page's stable binding.

func (*Handler) GetTemplate

func (h *Handler) GetTemplate(ctx context.Context, user, id string) (Template, error)

GetTemplate returns one persisted template.

func (*Handler) ImportUser

func (h *Handler) ImportUser(ctx context.Context, user string, export *UserExport) error

ImportUser 用 UserExport 整体替换该用户数据(先清场再写入)。 用途: 备份恢复、环境迁移、新用户套模板。

func (*Handler) KVCompareAndSet

func (h *Handler) KVCompareAndSet(ctx context.Context, user, key string, value json.RawMessage, expectedRevision string) (string, error)

KVCompareAndSet 仅当 key 当前值仍等于 expectedRevision 所代表的快照时写入。 VersionedUserStateStore 路径把比较和写入放在同一次状态 CAS 中,因此跨 Handler / 跨副本也不会静默覆盖;旧 Store 仍在当前 Handler 的用户锁内提供原子语义。

func (*Handler) KVDelete

func (h *Handler) KVDelete(ctx context.Context, user, key string) error

KVDelete 实现 Service。

func (*Handler) KVGet

func (h *Handler) KVGet(ctx context.Context, user, key string) (json.RawMessage, error)

KVGet 实现 Service。

func (*Handler) KVGetWithRevision

func (h *Handler) KVGetWithRevision(ctx context.Context, user, key string) (json.RawMessage, string, error)

KVGetWithRevision 原子读取语义由底层 KVGet 提供,并为逐字节值生成一个 不透明版本令牌。令牌只用于 KVCompareAndSet,调用方不应解析或自行构造。

func (*Handler) KVSet

func (h *Handler) KVSet(ctx context.Context, user, key string, value json.RawMessage) error

KVSet 实现 Service。

func (*Handler) ListItems

func (h *Handler) ListItems(ctx context.Context, user string) ([]Item, error)

ListItems 实现 Service。

func (*Handler) ListPages

func (h *Handler) ListPages(ctx context.Context, user string) ([]Page, error)

ListPages 实现 Service。

func (*Handler) ListRevisions

func (h *Handler) ListRevisions(ctx context.Context, user string) ([]RevisionInfo, error)

ListRevisions 实现 Service。

func (*Handler) ListTemplates

func (h *Handler) ListTemplates(ctx context.Context, user string) ([]Template, error)

ListTemplates returns all persisted templates sorted by stable ID.

func (*Handler) PageErrors

func (h *Handler) PageErrors(ctx context.Context, user string) ([]PageError, error)

PageErrors 实现 Service。

func (*Handler) PublishTemplate

func (h *Handler) PublishTemplate(ctx context.Context, user, id string, expectedRevision uint64, by string) (Template, error)

PublishTemplate copies the current draft into Published with CAS.

func (*Handler) PurgeUser

func (h *Handler) PurgeUser(ctx context.Context, user string) error

PurgeUser 清空该用户的全部业务载荷。VersionedUserStateStore 会保留一个 递增版本墓碑以阻止旧 CAS 复活数据;SQL 实现因此仍保留版本表中的用户键。 需要物理抹除身份标识时,宿主必须先停止该用户的全部 writer,再按自己的 保留策略清理墓碑;不能把本方法单独当作完整的 GDPR 擦除流程。

func (*Handler) PutCurrentTemplate

func (h *Handler) PutCurrentTemplate(ctx context.Context, user, page string, input Template, expectedRevision uint64, by string) (Template, error)

PutCurrentTemplate saves and binds the root/page-scoped template atomically.

func (*Handler) PutItems

func (h *Handler) PutItems(ctx context.Context, user string, items []Item, by, note string) error

PutItems 实现 Service。

func (*Handler) PutTemplate

func (h *Handler) PutTemplate(ctx context.Context, user string, input Template, expectedRevision uint64, by string) (Template, error)

PutTemplate creates at expectedRevision=0 or updates a persisted template with CAS.

func (*Handler) ReplaceItems

func (h *Handler) ReplaceItems(ctx context.Context, user string, items []Item, by, note string) error

ReplaceItems 实现 Service。

func (*Handler) ReportErrors

func (h *Handler) ReportErrors(ctx context.Context, user string, errs []PageError) error

ReportErrors 实现 Service。

func (*Handler) RevisionItems

func (h *Handler) RevisionItems(ctx context.Context, user, id string) ([]Item, error)

RevisionItems 实现 Service。

func (*Handler) Rollback

func (h *Handler) Rollback(ctx context.Context, user, revisionID, by string) error

Rollback 实现 Service。

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP 实现 http.Handler。

func (*Handler) ServiceForRequest

func (h *Handler) ServiceForRequest(r *http.Request) (ScopedService, error)

ServiceForRequest resolves and binds the authenticated user, then returns a service that rechecks identity, AccessPolicy, Handler.Close state, and shared write limits on every call. This is the recommended API for host HTTP routes.

func (*Handler) SetPage

func (h *Handler) SetPage(ctx context.Context, user string, page Page) error

SetPage 实现 Service。

func (*Handler) Stats

func (h *Handler) Stats() map[string]int64

Stats 返回内置计数快照(sse_connections/ws_sessions/events_published/ chat_turns/writes/rate_limited 等), 便于零依赖接入 expvar。

type HandlerConfigError

type HandlerConfigError struct {
	Field   string
	Problem string
}

HandlerConfigError identifies the option or dependency that failed startup validation. Callers should use errors.As instead of matching Error strings.

func (*HandlerConfigError) Error

func (e *HandlerConfigError) Error() string

func (*HandlerConfigError) Unwrap

func (e *HandlerConfigError) Unwrap() error

type Item

type Item struct {
	ID    string   `json:"id"`
	Type  ItemType `json:"type"`
	Title string   `json:"title,omitempty"` // 人类/AI 可读的用途说明
	Order int      `json:"order"`           // 应用顺序, 小的先执行
	// Page 是所属页面名。空 = 全局 Item(菜单/顶栏/全局样式, 始终加载);
	// 非空 = 仅在内核路由切到该页时加载执行, 离开时 teardown。
	Page string `json:"page,omitempty"`
	// Content 是片段源码。html 类型可内嵌 <script>/<style>。
	Content string `json:"content"`
	// Meta 是附加元数据。约定:
	//   - js 类型 meta["module"]=="true" 时以 <script type="module"> 执行;
	//   - html 类型 meta["slot"] 是 Item 对当前 Template Slot.id 的规范绑定;
	//     sidebar/header/footer/main 是旧内核兼容名称。绑定缺失或 Slot 已删除时,
	//     模板运行时必须把 Item 放到 Root 末尾的未分配区,而不是丢弃内容。
	Meta      map[string]string `json:"meta,omitempty"`
	UpdatedAt time.Time         `json:"updated_at"`
}

Item 是一段由 AI 生成(或手工写入)的前端代码片段。 同一用户名下 ID 唯一, 以相同 ID 写入即覆盖(内核会原地替换并触发 teardown)。

func (*Item) Validate

func (it *Item) Validate() error

Validate 校验 Item 的基本合法性。

type ItemService

type ItemService interface {
	// ListItems 返回按 Order 升序(同序按 ID)排好的 Item 全集。
	ListItems(ctx context.Context, user string) ([]Item, error)
	// PutItems 按 ID 增改 Item(变更前自动快照), note/by 记入快照说明。
	PutItems(ctx context.Context, user string, items []Item, by, note string) error
	// DeleteItems 删除指定 Item, 返回实际删除的 ID。
	DeleteItems(ctx context.Context, user string, ids []string, by, note string) ([]string, error)
	// ReplaceItems 整体替换 Item 全集。
	ReplaceItems(ctx context.Context, user string, items []Item, by, note string) error
}

ItemService is the independently implementable Item capability. New public capabilities are added as separate interfaces so SDK consumers can depend on the smallest stable contract they need.

type ItemType

type ItemType string

ItemType 是 UI Item 的种类。

const (
	// ItemHTML 是 HTML 片段, 内核按 order 顺序挂载到页面 #app 容器,
	// 片段内嵌的 <script> 会被重新创建以保证真正执行。
	ItemHTML ItemType = "html"
	// ItemJS 是 JS 脚本, 内核以 <script> 注入执行, 可自由 fetch。
	ItemJS ItemType = "js"
	// ItemCSS 是样式片段, 内核以 <style> 注入 head。
	ItemCSS ItemType = "css"
)

type KVConflictError

type KVConflictError struct {
	Key      string
	Expected string
	Actual   string
	Current  json.RawMessage
	Exists   bool
}

KVConflictError reports both KV revisions and, when the key currently exists, includes a defensive copy of its latest value so editors can offer a reload/merge flow without an extra round trip.

func (*KVConflictError) Error

func (e *KVConflictError) Error() string

func (*KVConflictError) Unwrap

func (e *KVConflictError) Unwrap() error

type KVService

type KVService interface {
	// KVGet 读取用户级 KV, 不存在返回 ErrNotFound。ReservedKVPrefix 下的
	// 内核键不属于公共 KV 能力,返回 ErrReservedKVKey。
	KVGet(ctx context.Context, user, key string) (json.RawMessage, error)
	// KVSet 写入用户级 KV 并广播变更事件;保留键返回 ErrReservedKVKey。
	KVSet(ctx context.Context, user, key string, value json.RawMessage) error
	// KVDelete 删除用户级 KV 并广播变更事件;保留键返回 ErrReservedKVKey。
	KVDelete(ctx context.Context, user, key string) error
}

KVService is the per-user JSON key/value capability.

type LayoutMode

type LayoutMode string

LayoutMode describes how a placement participates in layout. Flow placements are responsive grid children; absolute placements use canvas coordinates.

const (
	LayoutFlow     LayoutMode = "flow"
	LayoutAbsolute LayoutMode = "absolute"
)

type Limits

type Limits struct {
	MaxSSEPerUser      int // 每用户 SSE 连接数, 默认 8
	MaxWSPerUser       int // 每用户对话 WebSocket 连接数, 默认 4
	ChatTurnsPerMinute int // 每用户每分钟对话轮次(模型调用是真金白银), 默认 20
	WritesPerMinute    int // 每用户每分钟写操作次数, 默认 240
	MaxKVKeysPerUser   int // 每用户 KV 键数量(排除内核保留键), 默认 256
}

Limits 是连接与频率限制。字段为 0 时使用默认值, 为负时不限制。

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore 是纯内存 Store 实现, 适合测试与临时体验, 进程退出即丢失。

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore 创建内存存储。

func (*MemoryStore) AddRevision

func (s *MemoryStore) AddRevision(_ context.Context, user string, rev Revision, keep int) error

AddRevision 实现 Store。

func (*MemoryStore) EraseUser

func (s *MemoryStore) EraseUser(_ context.Context, user string) error

EraseUser 实现 Store。

func (*MemoryStore) Items

func (s *MemoryStore) Items(_ context.Context, user string) ([]Item, error)

Items 实现 Store。

func (*MemoryStore) KVDelete

func (s *MemoryStore) KVDelete(_ context.Context, user, key string) error

KVDelete 实现 Store。

func (*MemoryStore) KVGet

func (s *MemoryStore) KVGet(_ context.Context, user, key string) (json.RawMessage, error)

KVGet 实现 Store。

func (*MemoryStore) KVKeys

func (s *MemoryStore) KVKeys(_ context.Context, user string) ([]string, error)

KVKeys 实现 Store。

func (*MemoryStore) KVSet

func (s *MemoryStore) KVSet(_ context.Context, user, key string, value json.RawMessage) error

KVSet 实现 Store。

func (*MemoryStore) ReplaceItems

func (s *MemoryStore) ReplaceItems(_ context.Context, user string, items []Item) error

ReplaceItems 实现 Store。

func (*MemoryStore) ReplaceUserState

func (s *MemoryStore) ReplaceUserState(_ context.Context, user string, state UserState) error

ReplaceUserState 原子替换用户的完整状态。

func (*MemoryStore) ReplaceUserStateVersioned

func (s *MemoryStore) ReplaceUserStateVersioned(_ context.Context, user string, state UserState, expected uint64) error

ReplaceUserStateVersioned 实现 VersionedUserStateStore。

func (*MemoryStore) Revision

func (s *MemoryStore) Revision(_ context.Context, user, id string) (Revision, error)

Revision 返回指定完整快照。

func (*MemoryStore) RevisionItems

func (s *MemoryStore) RevisionItems(_ context.Context, user, id string) ([]Item, error)

RevisionItems 实现 Store。

func (*MemoryStore) Revisions

func (s *MemoryStore) Revisions(_ context.Context, user string) ([]RevisionInfo, error)

Revisions 实现 Store。

func (*MemoryStore) UserState

func (s *MemoryStore) UserState(_ context.Context, user string) (UserState, error)

UserState 原子读取用户的完整状态。

type Metrics

type Metrics interface {
	Add(name string, delta float64)
	Observe(name string, value float64)
}

Metrics 是可选的指标接收器, 宿主用它接入自己的监控(Prometheus 等)。 指标名: sse_connections(±1)/ws_sessions(±1)/events_published/chat_turns/ writes/errors_reported/rate_limited/auth_rejected。

type ModelLister

type ModelLister interface {
	Models(ctx context.Context) (ChatModels, error)
}

ModelLister 是 ChatBackend 的可选扩展: 提供可用模型列表。

type Option

type Option func(*options)

Option 配置 Handler。

func WithAPIs

func WithAPIs(apis ...APISpec) Option

WithAPIs 声明宿主提供的业务 API 目录: 写进 AI 的系统提示, AI 生成的 Item 会直接 fetch 这些接口取数; 同时 AI 可用只读探测工具实调它们 以了解真实返回结构。Path 是相对页面同源的绝对路径(如 /capi/players)。

func WithAccessPolicy

func WithAccessPolicy(fn func(r *http.Request, user string) Access) Option

WithAccessPolicy 设置按请求/用户的访问策略。长连接会重复且可能并发 调用 fn 以感知权限撤销,宿主实现必须并发安全。

func WithAccessPolicyE

func WithAccessPolicyE(fn func(r *http.Request, user string) (Access, error)) Option

WithAccessPolicyE is the fail-closed form of WithAccessPolicy. It is intended for policies backed by databases or authorization services that can fail. An error rejects the request instead of silently granting the zero Access.

func WithAudit

func WithAudit(fn func(ctx context.Context, e AuditEvent)) Option

WithAudit 注册审计回调。回调同步调用(需要异步的话由宿主自理), 覆盖全部写操作(含 AI 工具发起的)。

func WithBroadcaster

func WithBroadcaster(b Broadcaster) Option

WithBroadcaster 替换事件广播通道(多副本部署必须, 见 Broadcaster 文档)。 为了避免数据库提交与消息发布反序,该安全默认只向外部通道 发送不携带状态载荷的 resync。Handler.Close 不接管外部 Broadcaster 的生命周期;宿主仍须关闭它及其客户端。

func WithCORS

func WithCORS(origins ...string) Option

WithCORS 允许跨域访问(宿主页面把内核 mount 到自己域名下时必须开启)。 origins 为允许的 Origin 列表, "*" 表示全部。同时放行对应 Origin 的 对话 WebSocket。跨域认证应使用 UserResolver 验证的 Authorization 等非 cookie 凭据;本库不会下发 Access-Control-Allow-Credentials。 TLS 在反向代理终止时,还应把外部 https Origin 显式列入, 因为库不会信任客户端可伪造的 X-Forwarded-Proto。

func WithChatBackend

func WithChatBackend(b ChatBackend) Option

WithChatBackend 挂载 AI 对话后端(见子包 chat)。不设置时页面隐藏对话入口。

func WithLimits

func WithLimits(l Limits) Option

WithLimits 覆盖默认限额。

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger 设置日志器, 默认 slog.Default()。

func WithMaxRevisions

func WithMaxRevisions(n int) Option

WithMaxRevisions 设置每个用户保留的历史快照条数, 默认 DefaultMaxRevisions。

func WithMessageOrigins

func WithMessageOrigins(origins ...string) Option

WithMessageOrigins 显式允许哪些父页面 Origin 通过 postMessage 与 iframe 中的 ihtml 通信,并同步用于 CSP frame-ancestors。 默认不配置时只允许同源。"*" 会允许任意父页面,仅应在明确 接受任意站点嵌入和消息交互风险时使用。非法 Origin 会被安全地忽略。

func WithMetrics

func WithMetrics(m Metrics) Option

WithMetrics 注册指标接收器。无论是否注册, Handler.Stats() 都可读内置计数。

func WithOrderedBroadcaster

func WithOrderedBroadcaster(b Broadcaster) Option

WithOrderedBroadcaster 显式声明宿主能保证同一用户的所有 Store 写入 (跨全部 Handler/副本)从提交前到 Publish 返回都处于一个全局串行 边界内,因而允许 Handler 保留 items/templates/kv/pages 增量载荷。

这是一个需要宿主主动选择的危险能力:Broadcaster 自己按到达顺序 Publish 并不能修复两个副本“先提交、后发布”的反序。普通 Redis Pub/Sub、NATS 或“数据库提交后再 publish”的实现都不满足条件,应继续 使用 WithBroadcaster。典型安全场景是每个用户只有一个全局 writer,或宿主 的分布式锁覆盖完整 Handler 写调用。Handler 无法验证该前提。

func WithPageConfig

func WithPageConfig(fn func(r *http.Request, user string) map[string]any) Option

WithPageConfig 注入"宿主动态参数": fn 的返回值会出现在 GET /api/config 的 params 字段里, 页面内核暴露为 ihtml.config, Item 代码可直接使用 (如业务昵称/权限/上下文)。fn 按请求调用, 返回 nil 表示无参数。

func WithPageTitle

func WithPageTitle(title string) Option

WithPageTitle 设置空壳页面的 <title>, 默认 "ihtml"。

func WithUserResolver

func WithUserResolver(fn UserResolver) Option

WithUserResolver 设置用户解析器。

func WithWSOriginCheck

func WithWSOriginCheck(fn func(*http.Request) bool) Option

WithWSOriginCheck 覆盖对话 WebSocket 的 Origin 校验, 默认仅允许同源。

type Page

type Page struct {
	Name       string `json:"name"`                  // 路由名, 如 bills; 出现在 #/bills
	Title      string `json:"title"`                 // 菜单显示名
	Icon       string `json:"icon,omitempty"`        // 菜单图标(emoji 或短文本)
	Order      int    `json:"order,omitempty"`       // 菜单排序
	TemplateID string `json:"template_id,omitempty"` // 稳定引用的布局模板
}

Page 是页面注册表条目: 应用的一个"页", 也是侧边菜单的数据源。 页面下的 Item 通过 Item.Page 归属。

func (*Page) Validate

func (p *Page) Validate() error

Validate 校验 Page。

type PageError

type PageError struct {
	ItemID  string    `json:"item_id,omitempty"` // 归属的 Item, 无法归属时为空
	Message string    `json:"message"`
	Stack   string    `json:"stack,omitempty"`
	URL     string    `json:"url,omitempty"`
	Time    time.Time `json:"time"`
}

PageError 是内核上报的一条前端运行错误, 保留下来供 AI 修复参考。

type PageService

type PageService interface {
	// ListPages 返回页面注册表(按 Order 排序), 即侧边菜单的数据源。
	ListPages(ctx context.Context, user string) ([]Page, error)
	// SetPage 新建或更新页面(按 Name 合并)。
	SetPage(ctx context.Context, user string, page Page) error
	// DeletePage 删除页面及其名下全部 Item, 返回连带删除的 Item ID。
	DeletePage(ctx context.Context, user, name, by string) ([]string, error)
}

PageService is the page registry capability.

type Placement

type Placement struct {
	ItemID     string     `json:"item_id"`
	SlotID     string     `json:"slot_id,omitempty"`
	Unassigned bool       `json:"unassigned,omitempty"`
	Breakpoint string     `json:"breakpoint,omitempty"`
	Mode       LayoutMode `json:"mode"`
	Order      int        `json:"order,omitempty"`
	Column     int        `json:"column,omitempty"`
	ColumnSpan int        `json:"column_span,omitempty"`
	Row        int        `json:"row,omitempty"`
	RowSpan    int        `json:"row_span,omitempty"`
	X          float64    `json:"x,omitempty"`
	Y          float64    `json:"y,omitempty"`
	Width      float64    `json:"width,omitempty"`
	Height     float64    `json:"height,omitempty"`
	ZIndex     int        `json:"z_index,omitempty"`
	Locked     bool       `json:"locked,omitempty"`
	Hidden     bool       `json:"hidden,omitempty"`
}

Placement is an optional page-instance/legacy override that binds one concrete Item at one breakpoint. Because ItemID makes a document non-portable, new reusable templates should normally leave Placements empty and bind Items with Item.Meta["slot"] == Slot.ID. An empty SlotID with Unassigned=true represents the compatibility/editor tray; runtimes append it to Root rather than hiding it.

type ProposedToolCall

type ProposedToolCall struct {
	ID                   string         `json:"id,omitempty"`
	Name                 string         `json:"name,omitempty"`
	Title                string         `json:"title,omitempty"`
	Method               string         `json:"method"`
	Path                 string         `json:"path"`
	Query                map[string]any `json:"query,omitempty"`
	Body                 any            `json:"body,omitempty"`
	Risk                 string         `json:"risk"` // low / medium / high / blocked
	RequiresConfirmation bool           `json:"requires_confirmation"`
	Reason               string         `json:"reason"`
	AgentMode            string         `json:"agent_mode,omitempty"`
	ApprovalResume       string         `json:"approval_resume,omitempty"`
	ConfirmationToken    string         `json:"confirmation_token,omitempty"`
	ConfirmationNonce    string         `json:"confirmation_nonce,omitempty"`
	ConfirmationExpires  int64          `json:"confirmation_expires,omitempty"`
}

ProposedToolCall 是一次工具调用的描述; 需人工确认的调用带 HMAC 签名的 confirmation_token, 前端确认后原样带回执行。

type Revision

type Revision struct {
	Version               int        `json:"version,omitempty"`
	ID                    string     `json:"id"`
	Note                  string     `json:"note,omitempty"` // 变更说明, 如 "put 2 items"
	By                    string     `json:"by,omitempty"`   // 变更来源: api / chat
	CreatedAt             time.Time  `json:"created_at"`
	Items                 []Item     `json:"items"`
	Pages                 []Page     `json:"pages,omitempty"`
	Templates             []Template `json:"templates,omitempty"`
	RootTemplateID        string     `json:"root_template_id,omitempty"`
	TemplateRevisionClock uint64     `json:"template_revision_clock,omitempty"`
}

Revision 是某用户 UI 状态的一次历史快照, 每次变更前自动保存, 用于回滚。 Version 为 0/1 的旧快照只有 Items;Version >= 2 同时恢复 Pages; Version >= 3 同时恢复模板目录与 root 模板绑定。

func (*Revision) Info

func (r *Revision) Info() RevisionInfo

Info 返回快照的元信息。

type RevisionInfo

type RevisionInfo struct {
	ID            string    `json:"id"`
	Note          string    `json:"note,omitempty"`
	By            string    `json:"by,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	ItemCount     int       `json:"item_count"`
	TemplateCount int       `json:"template_count,omitempty"`
}

RevisionInfo 是快照的元信息(不含 Item 内容), 用于列表展示。

type RevisionService

type RevisionService interface {
	// ListRevisions 返回快照列表(新的在前)。
	ListRevisions(ctx context.Context, user string) ([]RevisionInfo, error)
	// RevisionItems 返回某快照的 Item 全集。
	RevisionItems(ctx context.Context, user, id string) ([]Item, error)
	// Rollback 把 Item 全集回滚到指定快照(回滚前同样自动快照)。
	Rollback(ctx context.Context, user, revisionID, by string) error
}

RevisionService is the revision history capability.

type RevisionStore

type RevisionStore interface {
	Revision(ctx context.Context, user, id string) (Revision, error)
}

RevisionStore 是 Store 的可选增强接口,用于读取完整快照(包括较新契约中的 页面注册表)。旧 Store 只有 RevisionItems 时仍可读取旧版仅含 Item 的快照。

type ScopedErrorService

type ScopedErrorService interface {
	PageErrors(context.Context) ([]PageError, error)
	ReportErrors(context.Context, []PageError) error
}

ScopedErrorService is ErrorService with the user already bound.

type ScopedItemService

type ScopedItemService interface {
	ListItems(context.Context) ([]Item, error)
	PutItems(context.Context, []Item, string, string) error
	DeleteItems(context.Context, []string, string, string) ([]string, error)
	ReplaceItems(context.Context, []Item, string, string) error
}

ScopedItemService is ItemService with the authenticated user already bound.

type ScopedKVService

type ScopedKVService interface {
	KVGet(context.Context, string) (json.RawMessage, error)
	KVSet(context.Context, string, json.RawMessage) error
	KVDelete(context.Context, string) error
}

ScopedKVService is KVService with the user already bound.

type ScopedPageService

type ScopedPageService interface {
	ListPages(context.Context) ([]Page, error)
	SetPage(context.Context, Page) error
	DeletePage(context.Context, string, string) ([]string, error)
}

ScopedPageService is PageService with the user already bound.

type ScopedRevisionService

type ScopedRevisionService interface {
	ListRevisions(context.Context) ([]RevisionInfo, error)
	RevisionItems(context.Context, string) ([]Item, error)
	Rollback(context.Context, string, string) error
}

ScopedRevisionService is RevisionService with the user already bound.

type ScopedService

ScopedService is the cross-tenant-safe form of Service. It removes every user argument, preventing accidental reads or writes to a different tenant.

func ScopeService

func ScopeService(service Service, user string) (ScopedService, error)

ScopeService binds a trusted Service to one user. It does not add an access policy by itself; use Handler.ServiceForRequest when handling an HTTP request.

type ScopedTemplateService

type ScopedTemplateService interface {
	ListTemplates(context.Context) ([]Template, error)
	GetTemplate(context.Context, string) (Template, error)
	GetCurrentTemplate(context.Context, string) (CurrentTemplate, error)
	PutTemplate(context.Context, Template, uint64, string) (Template, error)
	PutCurrentTemplate(context.Context, string, Template, uint64, string) (Template, error)
	PublishTemplate(context.Context, string, uint64, string) (Template, error)
	DeleteTemplateSlot(context.Context, string, string, uint64, string) (Template, error)
	DeleteTemplate(context.Context, string, uint64, string) error
}

ScopedTemplateService is TemplateService with the user already bound.

type ScopedVersionedKVService

type ScopedVersionedKVService interface {
	KVGetWithRevision(context.Context, string) (json.RawMessage, string, error)
	KVCompareAndSet(context.Context, string, json.RawMessage, string) (string, error)
}

ScopedVersionedKVService 是绑定用户后的可选条件 KV 能力。底层 Service 同时 实现 VersionedKVService 时,ScopeService 返回值才可通过类型断言取得它, 因而无需扩展既有 ScopedService 或破坏第三方旧实现。

type Service

Service is the complete trusted administration API implemented by *Handler. Direct calls intentionally bypass HTTP authentication and AccessPolicy; use Handler.ServiceForRequest for request-scoped, policy-enforced access.

The interface embeds smaller capability interfaces so downstream code can avoid implementing or mocking unrelated future features.

type Slot

type Slot struct {
	ID      string     `json:"id"`
	NodeID  string     `json:"node_id"`
	Name    string     `json:"name,omitempty"`
	Accepts []ItemType `json:"accepts,omitempty"`
	Locked  bool       `json:"locked,omitempty"`
}

Slot is a named drop target owned by a node. Removing a slot never removes an Item; DeleteTemplateSlot moves its placements to the unassigned tray.

type Store

type Store interface {
	// Items 返回该用户的全部 Item(顺序不限, 上层会按 Order 排序)。
	// 用户不存在时返回空切片而不是错误。
	Items(ctx context.Context, user string) ([]Item, error)
	// ReplaceItems 用 items 整体替换该用户的 Item 全集。
	ReplaceItems(ctx context.Context, user string, items []Item) error

	// KVGet 读取用户级键值, 键不存在时返回 ErrNotFound。KVSet/KVGet
	// 必须逐字节保留有效 JSON(包括无意义空白与 HTML 字符),不得规范化、
	// compact 或重新编码 RawMessage。
	KVGet(ctx context.Context, user, key string) (json.RawMessage, error)
	KVSet(ctx context.Context, user, key string, value json.RawMessage) error
	// KVDelete 删除键, 键不存在时不报错。
	KVDelete(ctx context.Context, user, key string) error

	// KVKeys 返回该用户的全部 KV 键(含保留键), 顺序不限。
	KVKeys(ctx context.Context, user string) ([]string, error)

	// Revisions 返回该用户的快照元信息, 新的在前。
	Revisions(ctx context.Context, user string) ([]RevisionInfo, error)
	// RevisionItems 返回指定快照的 Item 全集, 不存在时返回 ErrNotFound。
	RevisionItems(ctx context.Context, user, id string) ([]Item, error)
	// AddRevision 追加一条快照。keep 是保留上限, 实现应删除最旧的超额快照。
	AddRevision(ctx context.Context, user string, rev Revision, keep int) error

	// EraseUser 清空该用户的业务数据(Item/KV/快照), 用于注销与导入前清场。
	// VersionedUserStateStore 必须额外保留一个不可见的递增版本墓碑。
	EraseUser(ctx context.Context, user string) error
}

Store 是持久化接口。实现只需要保证单个方法的原子性; "读-改-写"的并发合并在单副本内由上层(Handler)按用户加锁串行化。 多副本部署要求 Store 同时实现 VersionedUserStateStore(内置 Store 均已实现), 上层会用它做跨进程的条件写与冲突重试。

user 是 UserResolver 解析出的用户标识, 实现应按它隔离数据。所有返回的 slice/map/RawMessage 必须是调用方可修改的值快照;实现不得在返回后继续修改 它们。所有传入的 slice/map/RawMessage 也必须在方法返回前复制或持久化,不能 保留并随后修改调用方内存。

type Template

type Template struct {
	ID          string           `json:"id"`
	Name        string           `json:"name"`
	Revision    uint64           `json:"revision"`
	Draft       TemplateVersion  `json:"draft"`
	Published   *TemplateVersion `json:"published,omitempty"`
	CreatedAt   time.Time        `json:"created_at"`
	UpdatedAt   time.Time        `json:"updated_at"`
	PublishedAt *time.Time       `json:"published_at,omitempty"`
}

Template is a reusable layout document. Revision is the CAS token for every mutation, independent from Draft.Version and Published.Version.

func (*Template) Validate

func (t *Template) Validate() error

Validate checks the complete template document, including tree acyclicity and all node/slot/breakpoint references. Empty Draft arrays are intentionally valid.

type TemplateConflictError

type TemplateConflictError struct {
	ID       string
	Expected uint64
	Actual   uint64
	Current  *Template
}

TemplateConflictError reports both revisions and includes the latest value so an editor can offer reload/merge without another round trip.

func (*TemplateConflictError) Error

func (e *TemplateConflictError) Error() string

func (*TemplateConflictError) Unwrap

func (e *TemplateConflictError) Unwrap() error

type TemplateNode

type TemplateNode struct {
	ID       string                     `json:"id"`
	ParentID string                     `json:"parent_id,omitempty"`
	Kind     TemplateNodeKind           `json:"kind"`
	Name     string                     `json:"name,omitempty"`
	Order    int                        `json:"order,omitempty"`
	Props    map[string]json.RawMessage `json:"props,omitempty"`
}

TemplateNode forms a tree through ParentID. Props is reserved for renderer-specific JSON hints; the built-in layouts/frames shapes receive the same finite/range validation as typed Placement and Breakpoint geometry.

type TemplateNodeKind

type TemplateNodeKind string

TemplateNodeKind is the structural role of a template node. Nodes contain no page content: reusable Item bindings remain on Item.Meta, with Placement only as an optional instance override. A completely empty template is valid.

const (
	NodeCanvas TemplateNodeKind = "canvas"
	NodeGrid   TemplateNodeKind = "grid"
	NodeStack  TemplateNodeKind = "stack"
	NodeGroup  TemplateNodeKind = "group"
)

type TemplateService

type TemplateService interface {
	// ListTemplates 返回一等布局模板;模板内容与动态页面 Item 分离。
	ListTemplates(ctx context.Context, user string) ([]Template, error)
	GetTemplate(ctx context.Context, user, id string) (Template, error)
	GetCurrentTemplate(ctx context.Context, user, page string) (CurrentTemplate, error)
	PutTemplate(ctx context.Context, user string, template Template, expectedRevision uint64, by string) (Template, error)
	PutCurrentTemplate(ctx context.Context, user, page string, template Template, expectedRevision uint64, by string) (Template, error)
	PublishTemplate(ctx context.Context, user, id string, expectedRevision uint64, by string) (Template, error)
	DeleteTemplateSlot(ctx context.Context, user, id, slotID string, expectedRevision uint64, by string) (Template, error)
	DeleteTemplate(ctx context.Context, user, id string, expectedRevision uint64, by string) error
}

TemplateService is the reusable layout template capability.

type TemplateVersion

type TemplateVersion struct {
	Version     uint64         `json:"version"`
	RootID      string         `json:"root_id,omitempty"`
	Nodes       []TemplateNode `json:"nodes"`
	Slots       []Slot         `json:"slots"`
	Placements  []Placement    `json:"placements"`
	Breakpoints []Breakpoint   `json:"breakpoints"`
}

TemplateVersion is a complete immutable-by-value template document. Draft is edited in place by creating a new Version; Published is a copy of a draft.

func (*TemplateVersion) Validate

func (v *TemplateVersion) Validate() error

Validate checks a template version. Version zero is accepted only so a virtual, not-yet-persisted current template can be round-tripped by the API.

type UserExport

type UserExport struct {
	Version               int                        `json:"version,omitempty"`
	Items                 []Item                     `json:"items"`
	Pages                 []Page                     `json:"pages,omitempty"`
	Templates             []Template                 `json:"templates,omitempty"`
	RootTemplateID        string                     `json:"root_template_id,omitempty"`
	TemplateRevisionClock uint64                     `json:"template_revision_clock,omitempty"`
	KV                    map[string]json.RawMessage `json:"kv,omitempty"`
	Revisions             []Revision                 `json:"revisions,omitempty"`
}

UserExport 是单用户全量数据包(备份/迁移/模板预置的 JSON 契约)。 Version 为 0/1 的包按旧版契约读取;当前导出写入 CurrentDataVersion。

type UserResolver

type UserResolver func(r *http.Request) (string, error)

UserResolver 从宿主已验证的会话/令牌解析当前用户标识。 返回空串或错误都会让请求以 401 结束。为了让 SSE/WS 能感知会话撤销,同一请求的 resolver 可能被重复调用;实现应并发安全。

type UserState

type UserState struct {
	Items     []Item
	KV        map[string]json.RawMessage
	Revisions []Revision // 新的在前

	// Version 是整个用户状态的乐观并发令牌,由实现 VersionedUserStateStore 的
	// Store 在 UserState() 时填充;不支持版本的 Store 保持为 0。调用方应视为
	// 不透明值:读到什么,条件写时就传什么。
	Version uint64
}

UserState 是 Store 中一个用户的完整持久化状态。它主要供需要跨 Items、KV、 Revisions 原子更新的操作使用(例如导入、页面删除和带页面注册表的回滚)。 调用方和实现都必须把它当作值快照,不能保留或修改传入切片/RawMessage。

type UserStateStore

type UserStateStore interface {
	UserState(ctx context.Context, user string) (UserState, error)
	ReplaceUserState(ctx context.Context, user string, state UserState) error
}

UserStateStore 是 Store 的可选增强接口。内置 Store 均实现它;第三方旧 Store 无需立即修改,Service 会使用备份+补偿恢复,但不能提供与该接口同等的强原子性。

ReplaceUserState 必须在单个原子操作中用 state 整体替换指定用户的数据。

type VersionedBroadcaster

type VersionedBroadcaster interface {
	Broadcaster
	CurrentVersion(user string) uint64
}

VersionedBroadcaster 是 Broadcaster 的可选扩展。多副本实现可用 Redis INCR/流序号等返回用户级全局版本,使 SSE resync 事件 能建立准确基线。Publish 投递的 Event.Version 应与此版本一致。

type VersionedKVService

type VersionedKVService interface {
	// KVGetWithRevision 返回值及其不透明版本令牌;键不存在仍返回 ErrNotFound。
	KVGetWithRevision(ctx context.Context, user, key string) (json.RawMessage, string, error)
	// KVCompareAndSet 仅在当前版本等于 expectedRevision 时原子写入,并返回
	// 新版本。读取时不存在的键使用 MissingKVRevision 作为 expectedRevision。
	KVCompareAndSet(ctx context.Context, user, key string, value json.RawMessage, expectedRevision string) (string, error)
}

VersionedKVService 是 KVService 的可选增强能力。它把单个键的读取版本与 条件写暴露给 SDK 消费者,适合编辑器、多标签页等不能接受静默覆盖的场景。 这是独立接口,避免给已有第三方 KVService 实现增加破坏性方法要求。

type VersionedUserStateStore

type VersionedUserStateStore interface {
	UserStateStore
	// ReplaceUserStateVersioned 仅当该用户当前版本等于 expected 时原子替换完整
	// 状态并推进版本;否则返回 ErrVersionConflict 且不产生任何修改。
	ReplaceUserStateVersioned(ctx context.Context, user string, state UserState, expected uint64) error
}

VersionedUserStateStore 是 UserStateStore 的可选增强接口,为多副本部署提供 跨进程的乐观并发控制。内置 Store 均实现它。

注意: 上层在两个替换方法中优先调用 ReplaceUserStateVersioned。若你通过嵌入 内置 Store 做包装(审计/加密等),Go 的方法提升会把版本化方法一并暴露—— 必须同时覆写 ReplaceUserState 与 ReplaceUserStateVersioned,否则包装逻辑会被绕过。

实现要求:

  • 用户状态版本从 0 开始(0 仅表示该用户从未被任何写操作触及);
  • 每个成功的写方法(ReplaceItems/KVSet/KVDelete/AddRevision/ReplaceUserState 等)都必须单调推进该用户的版本,使并发的整状态条件写能察觉任何交错写入;
  • EraseUser 必须清空 Items/KV/Revisions,但仍单调推进并持久化版本。 不得把版本重置为 0 或复用旧值,否则 purge 前的过期 CAS 可通过 ABA 覆盖 purge 后重建的新状态。

Directories

Path Synopsis
Package chat 是 ihtml 的 AI 对话后端: 基于 CloudWeGo eino ADK, 通过语义工具直接增删改用户的 UI Item / 主题 / KV, 并把过程以流式事件 推回页面内核的对话抽屉。
Package chat 是 ihtml 的 AI 对话后端: 基于 CloudWeGo eino ADK, 通过语义工具直接增删改用户的 UI Item / 主题 / KV, 并把过程以流式事件 推回页面内核的对话抽屉。
config
Package chatconfig loads the data-only portion of an ihtml chat backend configuration.
Package chatconfig loads the data-only portion of an ihtml chat backend configuration.
演示程序: 起一个带演示数据的多页 ihtml 服务。
演示程序: 起一个带演示数据的多页 ihtml 服务。
internal
nilcheck
Package nilcheck contains the SDK's interface-boundary nil guard.
Package nilcheck contains the SDK's interface-boundary nil guard.
Package redisbroadcast 提供基于 Redis Pub/Sub 的 ihtml.Broadcaster 实现, 用于多副本部署: 任一副本的写事件会推送到连接在其他副本上的页面, 并以 Redis INCR 维护用户级全局单调版本(实现 ihtml.VersionedBroadcaster)。
Package redisbroadcast 提供基于 Redis Pub/Sub 的 ihtml.Broadcaster 实现, 用于多副本部署: 任一副本的写事件会推送到连接在其他副本上的页面, 并以 Redis INCR 维护用户级全局单调版本(实现 ihtml.VersionedBroadcaster)。
Package sqlstore 提供基于 database/sql 的 ihtml.Store 实现, 用于生产部署。
Package sqlstore 提供基于 database/sql 的 ihtml.Store 实现, 用于生产部署。
Package storetest 是 ihtml.Store 实现的官方一致性验收套件。
Package storetest 是 ihtml.Store 实现的官方一致性验收套件。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL