Skip to content

plugin-webui-client — WebUI 前端 ​

包名: @aalis/plugin-webui-client
源码: packages/plugin-webui-client/src/main.tsx(前端入口;托管方见 packages/plugin-webui-server/src/client-discovery.ts)

概述 ​

Aalis 默认 WebUI 前端:React SPA 的纯静态资源包。它不是插件——没有 apply、没有 meta, runtime 不加载它;打包产物由 @aalis/plugin-webui-server 托管。

包声明 ​

jsonc
// package.json
{
  "aalis": { "client": true },   // 前端标记:webui-server 据此发现
  "keywords": ["aalis", "aalis-interface"],
  "files": ["dist"]              // 构建产物 dist/index.html + 静态资源
}

没有 dependencies:发布的 dist 已经打好,React、mermaid、KaTeX 这些打包用的库都在 devDependencies,装进项目时不再跟着装一遍。

配置 ​

无配置项。

工作方式 ​

  1. 构建脚本 tsc --noEmit && vite build 先做类型检查,再产出 dist/index.html 及静态资源,并写出 dist/aalis-texts.json (api-webui 的 CLIENT_TEXTS_FILE):随前端打包的字典(外壳的与打进包里的库的)的目录条目,webui-server 挂载这个前端时 登记进服务进程的目录。这些字典由 vite.config.ts 顶部的导入登记:打进包里的库多了带字典的,要在那里补一行导入 (test/plugins/webui-client-texts.test.ts 核文件与产物一致)
  2. webui-server 在 app:ready 时由 client-discovery.ts 扫描:package.json 标了 aalis.client: true 且存在 dist/index.html 的包即为前端候选(不认任何具体包名)
  3. webui-server 把每个候选注册成一条 webui-client 服务 provider(getClientDir())
  4. 活跃前端 = webui-client 的服务解析结果(servicePreferences['webui-client'] 偏好 > provider 优先级 > 注册顺序),由 webui-server 挂成 Express 静态目录;切换偏好即实时重挂, 并经 /admin 推 client.reload 让页面重载,无需重启

第三方要换前端,正解就是照上面打 aalis.client 标记(详见 docs/services/webui.md 第 4b 节), 不需要写插件。

主题与首帧 ​

  • 当前主题由 <html data-theme> 决定,值是主题 id。内置主题(见 api-webui 的「主题」一节)在构建时经 parseTheme 校验,写成按 data-theme 取值的规则内联进 index.html 的头部,第一帧就有颜色。
  • public/theme.js 是同源的首帧主题脚本,紧跟在内联的内置主题规则后面、在打包的脚本之前同步执行:按存着的外观选择写上 data-theme。外观选择存在本浏览器的 aalis.appearance(JSON):{ follow: true, dark, light } 分别指定深色、浅色用哪个主题,{ follow: false, theme, kind } 固定一个(kind 是它的深浅);选中的是导入或插件提供的主题时,palettes 里按主题 id 存着它的名字、深浅与令牌,脚本逐个 setProperty 写进根元素的内联样式,并设上 color-scheme。没存过、存坏了或存储不可用时跟随系统,用 calm-dark 与 calm-light;选中的主题没有规则也没有令牌(改了名、没写主题)时按系统深浅用这两个之一,页面不会没有颜色。
  • 页面起来以后由 src/appearance.tsx 接手:系统切换深浅、在设置「外观」里改选择时换主题,内置主题只换 data-theme,导入与插件提供的主题另把全部令牌写进内联样式(换回内置主题时撤掉);选择与选中的非内置主题的令牌写回 aalis.appearance。插件提供的主题取自 GET /api/themes,plugins.changed 与管理连接重连后重取,取回之前先用 palettes 里存着的;正在用的主题不在了(导入的移除、插件停用)时用同深浅的偏中性,回来了就回到它,存着的选择不改。
  • 字号、间距、尺寸、圆角、阴影与层级的令牌在 src/styles/tokens.css,尺寸用 rem,跟随浏览器与系统的字号设置;触屏(pointer: coarse)上点按范围、按钮、行高、图标栏、输入框字号与分隔条的按住宽度换成触屏值。主题只改颜色,这些令牌不随主题变。
  • 控件的尺寸只有两个令牌:--control-h 是按钮看得见的高(28 像素,触屏 36 像素);--hit 是点按范围的下限(28 像素,触屏 44 像素),图标按钮、开关的点按区、复选框所在的 label 与输入框、选择框的高也取它。桌面上两者相同,同一处的控件一样高。按钮只有一种尺寸,有底色的字重常规,文字按钮中等。src/styles/base.css 给按钮、按钮样式的链接与表格单元格里的链接一条共用规则:框不到 --hit 见方时由 ::before 向四周补足点按范围,不占排版位置,所以触屏上有底色的按钮看得见的不必放大到 44 像素。
  • 选择一律用 src/components/Select.tsx,不用原生的选择框:按钮写选中的那一项,点开是一列选项,到 10 项起顶上有过滤框(按名字过滤,不分大小写,模型列表可以有几百项);方向键移动、回车选中、Esc 关闭并把焦点还给按钮;桌面上在按钮下方左对齐弹出,手机上从底部弹出、占满宽;弹层挂在 body 上,按钮在打开着的模态对话框里时挂在对话框里。弹层的定位与「更多」菜单共用 src/components/popover.ts,是样式规则里唯一用脚本定位的地方。
  • 各栏的头部(图标栏顶上的「会话」一格、左栏、主区与右侧面板的标签栏)都用 src/styles/base.css 的 .header-bar:高度取 --header-h,下边线只在这一处定义,几栏的头部连成从图标栏贯穿到右侧面板的一条横线。组件不另给头部定高或描边。

浏览器存储 ​

界面偏好只经 src/storage.ts 读写,键带 aalis. 前缀,值是 JSON;隐私模式或封装禁用存储时读写失败按没存过处理,页面照常可用。首帧主题脚本是唯一直接读存储的地方。现有的键:

键内容
aalis.appearance外观选择与选中的非内置主题的令牌,见上一节
aalis.themes导入的主题:主题文件的数组,读出时逐个过 parseTheme,认不出的丢掉
aalis.panels{ left, right },每侧是 { open, width? }:常驻宽度下是否展开,调过的栏宽(rem,没有就是默认宽度)。存坏了的一侧按展开、默认宽度,超出上下限的宽度按上下限
aalis.drafts输入框的草稿:会话 id(首页新会话是空串)→ { text, at },只留最近改过的 50 个;形状不对的条目丢掉

外壳与地址 ​

从左到右是图标栏、左栏、主区、右侧面板。左栏默认 16.5rem、可调 13.75–25rem,右侧面板默认 22.5rem、可调 18.75–37.5rem(src/shell/Shell.tsx 的 WIDTHS),栏宽写成外壳上的 CSS 变量;主区至少留 30rem(--main-min-w),窗口变窄放不下时两侧按栏宽的比例收,收到下限为止。当前会话、右侧面板的标签、房间表的组、打开的页面、插件详情与日志的筛选都由地址决定(src/routes.ts),能收藏、能刷新:

地址内容
/首页:桌面上是空白的新会话,上方是「开始使用」(没有能对话的模型时)与「需要处理」的摘要;手机上是第一层(这两块、宿主的入口、固定的页面与会话列表)
/new空白的新会话;手机上是第一层之后的第二层,桌面上重定向到 /
/s/:sessionId、/s/:sessionId/:tab会话;带标签时右侧面板打开在那个标签(plan、settings、usage、background)
/s/:sessionId/p/:instance/:pageKey会话,右侧面板打开在插件的这个会话面板页面
/rooms?instance=、/rooms?platform=一组房间的房间表;还没记下实例的房间按平台成一组
/p/:instance/:pageKey位置为插件详情的页面(声明式或插件前端),占整个主区,没有左栏;查询串里的 tab 是选中的标签,detail 与 row 是选中看详情的组件与行
/plugins、/plugins/:instance插件区:左栏是实例树,主区是这个实例的详情(见下文「插件区」);地址片段 #config 落在配置区
/market市场(属插件区,不占 /plugins/ 下的插件名)
/settings设置:桌面上落到第一节,手机上是设置的目录(见下文「设置」)
/settings/:section设置里宿主的一节:session-defaults(会话默认)、appearance(外观)、system(系统);认不出的回到 /settings
/settings/p/:instance/:pageKey设置里插件的一节:位置为设置的页面
/health、/health/logs运行状况:概况与日志两个标签,占整个主区,没有左栏;日志的查询串 level、plugin、text 是筛选(见下文「运行状况」)

认不出的地址回首页。

  • 图标栏:上方是「会话」,一条细线下面是 owner 固定的页面,按记录的顺序,多了在这一段里纵向滚动;底部是「运行状况」「插件」「设置」(src/shell/entries.ts);插件不能自己进图标栏。每项是图标,悬停提示与读屏标签写名字(多实例插件带实例名),触屏上图标下方写短名。当前项左侧一条金色指示条,另有底色与 aria-current。角标只有一个:运行状况右上角是「需要处理」的条数,读屏标签写成「运行状况,N 项需要处理」(看得见的角标是名字的一部分,不放进括号),为 0 时没有角标。会话、运行状况、插件、设置各是一区:从别处(另一区或插件页面)点它回到刚才在这一区的地址;已在这一区时点它收起或展开左栏,运行状况没有左栏,点它不动。页面表或布局记录没取到时,固定区的末尾多一个警示图标,读屏标签与悬停提示写明原因,点它重试;手机第一层的「固定的页面」里写出原因与「重试」。

  • 分隔条:两侧常驻时,栏与主区之间各有一条可以获得焦点的分隔条(role="separator",aria-valuenow、aria-valuemin、aria-valuemax 是以 rem 计的栏宽与上下限)。按住拖动改宽度,拖动中的宽度只写在那一栏上,松手才记下;左右方向键每次调 1rem,Home、End 到上下限;双击恢复默认宽度(记录里去掉宽度)。拖到下限以下松手即收起,与按钮、快捷键收起是同一个状态,栏宽留着拖动前的,再展开时回到它;没有移动的点按与被打断的拖动不改栏宽。覆盖式抽屉按记住的栏宽盖进来,不出分隔条;手机上没有。

  • 宽度(断点用 em,跟随字号设置):

    宽度布局
    ≥ 80em全部常驻;Cmd/Ctrl+B、Cmd/Ctrl+Alt+B 收起或展开左栏与右侧面板,收起状态与栏宽存在 aalis.panels
    64em–80em图标栏、左栏常驻;右侧面板从右边覆盖进来,不挤压主区
    48em–64em图标栏与主区常驻;左栏与右侧面板是覆盖式抽屉
    < 48em手机:没有图标栏,按地址逐层进入,每层占满一屏:/ 是第一层,顶栏左边写「会话」,右边是搜索与新会话两个图标按钮,会话列表上面依次是「运行状况」(带「需要处理」的数,读屏名字与图标栏同一写法)「插件」「设置」三个入口与固定的页面;会话、新会话、房间表、页面、运行状况、插件树与设置的目录是第二层,右侧面板的标签(含插件的会话面板)、插件详情、市场与设置的一节是第三层(用量除外,它从底部弹出,仍在第二层);系统返回手势与浏览器后退按地址逐层退回,每层头部左边有回上一层的按钮:上一条历史就是上一层时等于后退一步,否则换掉当前这条,两种都不多留历史;用量弹层关上时同样退回。外壳上的 data-phone 写着手机上占满一屏的是哪一栏(left、main、right)

    抽屉打开时焦点移进去,Esc、点遮罩或在左栏里跳到别的地址都会关上,关上时焦点还给打开前的元素。

  • 连接状态:窗口顶部一条横条(跨过图标栏与各栏,各栏头部一起往下让,横线不断开),会话连接或管理连接断开、重连中时写「连接断开,正在重连…」,重连上写「已恢复」两秒后收起,平时不出现。会话服务不在(/session 以 1013 关闭)不算断线,主区写「会话服务未启用」(手机第一层没有主区,写在会话列表里):提供会话协议的插件(按插件列表的 provides 找)缺服务时写它的链与根(如「会话服务 → SQLite 记忆(失败:…)」;同一个根只写一次,留最短的那条链),在等、失败或待配置时写它的状态与原因(「等待启动:<在等的插件>」「失败:…」「待配置:…」);停用着或启用着时写启用后会自动连上。按钮与「需要处理」里它那一行同一个去处:缺服务的去根插件(根待配置的去根的配置区),它自己待配置的去它的配置区,其余「去处理」进它的详情;没装时链到市场;服务回来后自动连上。连接一次也没连上就断开、而页面的请求照常(/api/auth/status 查得到已登录)连着两回时,多半是升级请求被来源校验拒了(只回裸 403,浏览器读不到原因),横条改写「连接一直被服务端拒绝,页面的其他请求却正常:经反向代理访问时,检查代理是否转发了原始的 Host 请求头」。收到 app.restarting 时横条写明正在重启,管理连接重连上以后整页重新加载;收到 client.reload 立即重新加载。

  • 快捷键:按平台取 ⌘ 或 Ctrl,另一个修饰键按下时不算;不占浏览器保留的 ⌘N、⌘T、⌘W、Ctrl+Tab。

    键作用
    Cmd/Ctrl+K搜索:会话按名称、id 与「平台:群号」搜(会话服务搜,不含已归档的);输入了字时另按名字找有自己地址的页面(位置为插件详情或设置的,会话面板的不列)与插件(显示名、包名、实例 id),不分大小写。会话、页面、插件三组依次列出,方向键跨组选、回车打开;输入法组字时的回车不算。会话服务不在时照样找页面与插件,页面表或插件列表没取到时写明原因
    Cmd/Ctrl+Shift+O新会话
    Cmd/Ctrl+B、Cmd/Ctrl+Alt+B收起或展开左栏、右侧面板
    Esc正在生成时停止这一轮;有弹层(对话框、菜单、抽屉)或就地编辑(设置行、拒绝的理由)时先收起它们,输入法组字时的 Esc 不算
    Enter、Shift+Enter在输入框里发送、换行;输入法组字时的回车只上屏;触屏上回车换行,用按钮发送
  • 标签页标题:握手给出的应用名(宿主配置的 name,握手之前是页面自带的「Aalis」),前面带上「(N)」:N 是「需要处理」的条数加「我的」区完成未读的会话数(不在会话区时也算),都为 0 时只写应用名。

  • 需要处理(src/health/):GET /api/attention 整页取一份,收到 attention.changed 或管理连接重连后重取。每项一行:行首的标记(需要处理是金色,出错是红色),标题与一句说明,行尾的跳转。审批写会话的标题与「等你批准:<工具>」,「去处理」进那个会话;插件故障写插件名与状态:失败写「失败:原因」,必填配置没填的用提醒色写「待配置:<键>」、按钮「去配置」进它的配置区;缺服务写「缺服务:」加每条链与根(链是从直接提供者到根插件的显示名,括号里写根的情形:失败的原因(只写报错的第一行,全文在根插件自己那一行与它的详情里)、「待配置:<键>」、「已停用」、「在运行但没有提供 <服务>」,没有提供者写「没有已装的插件提供 <服务>,可以在插件市场按服务搜 <服务>」,成环点名环上的插件),按钮直达第一条链的根插件(「去处理 <根>」,根待配置时「去配置 <根>」),没有根插件可去时进这个插件的详情。按钮放不下时换到说明下面一行,不把说明挤窄。只在等别的插件启动的插件不在这里。链与根由服务端算好(插件列表与「需要处理」的 lacking、waiting、unconfigured),界面照字段写,根插件的显示名与待配置项从插件列表取;提示写它的标题与出处(插件名,有 detail 时接在后面),跳转按钮由宿主推出:open 是 config 的写「去配置」、落在插件详情的配置区,指向页面的写「打开<页面标题>」,没有 open 的没有按钮。首页的摘要最多五条,标题写总数,「全部」链到运行状况(/health);空了整条不出现,没取到时写明原因与「重试」,重取失败时上一次的列表留着,另写一行「没刷新成功:原因」与「重试」。桌面在首页新会话的上方,手机在第一层的顶部。

  • 开始使用(src/health/GetStarted.tsx):GET /api/models/llm 里没有一个带 chat 能力的模型时,在首页「需要处理」摘要上方(手机上在第一层顶部)出现「选一个对话模型」,按状态出现,不设首次启动的标记。先列已装的、提供 llm 服务的插件与它为什么不能用:待配置的「去配置」,停用的一键「启用」(没启用成时 Toast 的「查看日志」去这个插件),缺服务的写链与根、去根插件,失败的写原因、去它的详情,在运行但没有能对话的模型的照实写;再列市场(GET /api/marketplace?q=)里提供 llm、还没装的包,「安装」走与市场同一个确认框,装上后跳到它详情的配置区。市场取不到时写原因、已装的照常列,市场的说明(离线降级)写在下面;模型列表没取到时写明原因与「重试」。有了能对话的模型就消失;plugins.changed、attention.changed 与管理连接重连后重取模型列表。市场只在卡片出现时与装完之后取(服务端取一次要向安装源发几十个请求),在别处装上的按插件列表的包名排除。

  • 出错(src/components/FetchError.tsx、src/components/Toast.tsx):一块取数失败时原地写「<哪里>没取到」、原因与「重试」,指得出出错的插件实例时另有「查看日志」,链到运行状况的日志标签、按这个实例筛好(/health/logs?plugin=,不按时间筛,日志打开就是最近的)。这一块没有别的内容时占这一块;还显示着上一次的内容或别的项时写成一行,上一次的内容还在的写「没刷新成功」。出错的 Toast 不自动消失,同样带「查看日志」。来源按插件列表找,不认插件名:声明式页面与插件前端是页面所属的实例;插件详情里的管理动作是这个插件;经会话协议取的(会话列表、房间表、会话、「后台」、运行状况的「正在运行」)与会话里的操作是提供会话协议的实例(provides 里有 session-protocol);界面自己的接口(插件列表、页面表、布局记录、「需要处理」、平台连接、宿主配置、市场、扫描与新建实例)是提供这个界面的实例(插件列表里带 self 的);市场与插件详情里的安装、卸载、更新是提供包管理服务的实例(provides 里有 package-manager,npm 的输出与失败的原因记在它那里;没有它时是提供这个界面的实例),市场的更新条没过时同样带「查看日志」;刷新模型列表是那个模型服务。插件列表还没取到时指不出来源,没有「查看日志」;日志本身没取到时也没有。只写原因、不能重试的几处(会话默认的层与行、会话的设置标签、外观里插件提供的主题与布局记录、用量与设置里的插件显示名)照旧;图标栏、面板标签栏与标题栏里放不下的,是一个写明原因、点它重试的图标按钮。

运行状况 ​

/health 与 /health/logs 是同一页的两个标签(src/health/),手机上是第一层之后的第二层。

  • 概况:「需要处理」列出全部,写法与首页摘要相同,空了写「没有需要处理的事」,没取到时原地写明原因与「重试」,重取失败时同首页摘要。
    • 正在运行:在「需要处理」下面,按 sessions/list { background: true } 列出有后台工作的会话(归档的照样列出,行尾写「已归档」),随 sessions/changed 即时更新。每个会话一行,写「后台 N」,链到这个会话的「后台」标签,行尾的「全部停止」调 background/stopAll;下面按会话树列出它在跑的后台任务与在跑的子会话(子会话再往下照样列,状态写法同「后台」标签,在等审批的写「等你批准」),后台任务能停(background/stop),子会话的「停止」停它的回合并对它全部停止。没有在跑的就不出现,会话服务不在时也不出现。
    • 平台连接:GET /api/platforms 在打开时取一次,标签栏右侧的「刷新平台连接」再取(平台连接没有变化事件);WebUI 那一行数的是会话连接,本页的会话连接接上与断开时(含断线重连)也再取一次,直接打开这一页时不会因为会话连接还没接上写成「没有连接」。每个适配器实例的每条连接一行:标题是适配器名(多实例带实例名),第二行是账号(昵称后面括着 id),右侧写在线、连接中、离线,离线的行首标出错;没有连接的适配器一行写「没有连接」;适配器的 detail 不显示(各平台自己的字段,界面不认)。行链到适配器所在插件的详情。没有适配器时这一块不出现,没取到时写明原因与「重试」,刷新失败时上一次的连接留着,另写一行「没刷新成功:原因」与「重试」。
  • 日志:顶上一行是两个预设、级别、插件与文字。预设「只看错误」是 level=error;「审批」是提供审批服务的实例(按插件列表的 provides 找 approvals),会话服务在每个审批结算时记的那一行就在里面,没有这样的插件时不出现。这个预设筛的是那个实例的全部日志,会话服务别的 info 行也在里面:只出结算行就得让界面认会话服务的日志文字或子作用域名(界面只认契约),或者为这一处新立「服务的提供者为这个服务记的日志用服务名作子作用域」这样的跨插件约定,判不做。级别按「这一级及以上」筛(地址里的 level=debug 与不筛一样,当没给);插件从插件列表里选,地址上的实例不在列表里时照样列出;文字回车生效。改了筛选就换掉地址(不多留历史),有筛选时出现「清除筛选」。筛选在服务端筛:先在 /admin 上发带筛选的 log.subscribe,再带同一份筛选取 GET /api/logs/tail 的最后 200 条,取回之前推来的按序号接在后面(历史里已有的不重复);筛选变了、管理连接重连或点「重试」时重新订阅、重取,离开时退订。最新的在底部,停在底部时新来的出现在视野里,往上翻看时不被拽走;最近一次取满了 200 条时顶上有「更早的日志」,经 GET /api/logs/range 带同一份筛选往前翻,没取到时出错 Toast 写原因。页面上最多留 2000 条,实时推来的多了丢最早的。每条写时分秒(悬停看完整时刻)、级别(调试、信息、警告、错误,警告与错误上色)、作用域与正文,宽时同一行、各列对齐,窄时正文在下一行。没有日志写「还没有日志」,筛选造成的空写「没有符合筛选的日志」;取历史失败时原地写明原因与「重试」。

会话列表 ​

左栏的会话列表经 sessions/list 翻页取全(一页 200 条),之后按 sessions/changed 的增量即时更新,断线重连后重取。翻页途中收到推送时排序可能变了、后面的行会挪进取过的页,所以重取,最多三遍。

左栏只列根会话;子会话不在这里展开,在会话面板的「后台」标签里按会话树列出。

  • 我的:owner 自己的会话,需要我处理(它或它的子会话有推出、还没结的审批,摘要的 attention)、出错、完成未读、运行中依次排前,其余按最后活动。这份列表整页一份(src/sessions/mine.tsx),不在会话区时也保持更新,标签页标题的完成未读数取自它。行首标记只给需要我处理(金)、出错(红)、完成未读(强调色);需要我处理、出错、运行中在第二行写一句状态,出错写最近一轮的原因(摘要的 reason,没有时写「出错」);没有这几种状态而有后台工作(摘要的 background 大于 0)时写「后台 N」,带转动。
  • 房间:按适配器实例分组,组头写通用图标、组名(RoomGroup.label)、房间数与需要处理的数(房间或它的子会话在等审批的都算;不按平台分图标,房间表的标题同样),另有「表格」按钮打开这一组的房间表。组默认折叠,有需要处理的自动展开;展开时只露出需要处理、出错、运行中、有后台工作、正在看的与近 7 天她开过口的房间,其余收在「其余 N 个」里。同一实例带多个账号时行内标出账号。房间行是单行,不正常时状态(或「后台 N」)代替时间写在右侧。
  • 已归档:列表底部,默认折叠,展开时按 archived: true 取。
  • 行整行可点;行尾的「更多」收改名、归档、删除(已归档的是恢复、删除),悬停、选中、键盘聚焦时出现,触屏上常显,手机上不带。归档直接做,Toast 带「撤销」;删除先确认,确认框点名对象、写明会清空长期记忆,焦点落在「取消」上。

房间表 ​

/rooms?instance=<实例>(或 ?platform=<平台>)一次取全这一组的房间,列有名称、类型、账号(多账号时才有)、状态、最后活动、她最近开口。类型是摘要里聊天窗口的类型名(平台自定),原样写,筛选项取本组出现过的,前端不认具体平台的类型名。类型与状态的筛选写进地址(type、status),排序、搜索与分页在客户端做,行数超过一页才出现搜索框与分页;没排序的列只在指着或聚焦表头时露出排序图标,触屏上常显。批量动作只有归档与删除,全选只作用于当前筛选结果。窄时只留名称与状态两列,名称长了折行。场所是窗口的行尾有「场所设置」(行尾的操作窄时也在):在页内的对话框里打开这一行所在窗口的设置,读写走按层读写(层为 { scope: 'place', key }),行与「会话默认」同一套,每项写来源;同一窗口的新会话照样取到。表格下面一句话:统一设置一个平台或一种类型去「会话默认」,单个窗口在这里设。

会话工作台 ​

/s/:sessionId 是一个会话:头部、时间线、输入框,右侧面板跟着会话出现。订阅一律先取快照,之后按推送更新:同一 itemId 原地覆盖,item/delta 接在正文或工具输出后面,completed 的全文是正本;计划、用量与排着的输入每个会话一条、整份替换,不进时间线。重新订阅(重连、epoch 变了、session/resync)时快照整份取代。会话被删掉(出现在 sessions/changed 的 removes 里)时回首页。看着「我的」区的会话时,她的回复不算未读:页面在前台时调 session/markSeen。

  • 首页与新会话:/ 与 /new 是空白的新会话,发出第一条消息时先 session/create 再 turn/send,然后打开它。
  • 头部:标题、所在场所与平台(场所是窗口的写「<类型的显示名> · 平台」,如「群 · onebot」,类型的显示名同设置的层列表、没声明时写类型名,是工作区的写场所的显示名,出生平台是 IM 的另写平台,如 owner 的 IM 私聊写「默认工作区 · onebot」;子会话不写,标题前的各级父会话已表明它是子会话)、回合进行中的运行指示(转动的图标,读屏名「正在运行」,跟着会话摘要的 running,回合结束就消失)、有后台工作时的「后台 N」(带转动,数取摘要的 background,点开「后台」标签,与回合的运行指示是两样)、用量百分比、「全部展开」或「全部折叠」、会话面板(常驻宽度下收起或展开)、「更多」(改名、归档或恢复、压缩对话、删除;压缩按功能位出现)。手机上顶栏右边只有用量百分比与「更多」,「后台 N」「全部展开」与会话面板(进第三层)收进「更多」,有后台工作时「更多」上有一个点,标题不被挤掉。子会话的标题前写各级父会话(摘要的 ancestors,根在前),点哪一级回到哪一级,手机上头部的返回回到父会话。驱动房间时头部下面一条提示:「你正在以 owner 身份在「房间名」发言」,群与私聊同一句。
  • 时间线:条目按回合成段,同一回合挨在一起(按 turnId;这一轮的输入也带它,在段里),中间夹着别人的消息时一轮分成几段;相隔超过 10 分钟的两段之间插一行时间。
    • 进了一轮的提问与通知下面一行写它怎么进的(复用消息下面那一行的样式):「开了这一轮 · 被点名」「开了这一轮 · 插话」「并入这一轮」,按条目的 entry 与 addressed;addressed 为空的开轮不写(如 WebUI 与终端会话、默认设置下私聊里的提问,定时任务与宿主通知开的一轮)。不显示分数,不认任何触发插件。没有输入条目的一轮(如空闲开话题,输入不归档)在第一段开头写「来自 <来源>」(回合条目的 source,与内部注入的消息同一写法)。
    • 用户消息在「我的」会话里靠右、浅底;房间是多人对话,逐条写说话人与时间,她的回复的说话人写握手给出的应用名;内部注入的消息写明来源。提问随附的转发(条目的 forwards)各是一个默认收起的块,按钮写条数与参与者(「转发的聊天记录:N 条,甲、乙等 M 人」),点开是转发的文字(摘要或截断的原文,截断的注明只显示了开头),开合跟着「全部展开」「全部折叠」;没有展开内容的写一行「转发的聊天记录(没有展开内容)」。
    • 她的回复按 Markdown 渲染,下面一行写模型与这一轮的 token(「输入」是这一轮最后一次调用的上下文,「输出」是这一轮各次调用的输出合计,服务商没报的不写);房间里没发出去的写明原因(数据来自送达回执)。
    • 思考默认收起,点开是正文。宿主通知(压缩、工具调用达到上限等)是一行说明,进行中带旋转图标,多行的照原有的换行与缩进逐行显示;指令的回复(topic 为 command-reply,如 /help 的列表)是单独的一张卡,读屏名「指令的回复」:正文正常字号,只认段落与列表(含嵌套列表、空行分段与裸换行),链接、图片、强调、行内代码、标题、表格、引用与 HTML 等写法照原文显示(回复里会回显别人敲的原话),附件同普通通知,不折叠,有边框、不带说话人,不画成她的消息;工具调用达到上限且这一轮已收尾时有「继续」(turn/continue,有对话服务时才出现)。后台任务的结束通知(种类是 api-tools 的 BACKGROUND_TASK_NOTICE)按通知带的事实画成一行状态:「后台任务」与工具名、结局与用时(结局的写法同「后台」标签,失败用危险色、命令写退出码),写给模型的那句话不显示;登记表里还有这个任务时点开是它的日志(同工具卡的「输出」),没有了的照样写这一行、不能点。事实不合约定的按普通通知显示原文。
    • 回合中止写「已停止」,出错写「这一轮出错了」,没有回复(结局 silent)写一行淡色「没有回复」,都写在这一轮最后一段的末尾(回合条目推在哪一段都一样):什么都没做就停下或没回的一轮,结局紧跟在它的消息下面。没有回复、最后一段只有输入的几轮,中间只隔着不在回合里的提问时合成一行「N 轮没有回复」,写在最后一轮的位置,被并掉的几轮只藏结局行、输入照常显示,房间与「我的」会话一样(房间不刷屏靠合并)。历史里的回合条目由回合记录还原,刷新之后这几行照样在。收尾了的回合右上角有回滚(悬停、键盘聚焦时出现,触屏上常显,按功能位出现):只回滚文件,或连这一轮的对话一起删。回滚先确认,打开即可确认:确认里固定写明「回滚只撤回经存储的文件改动,命令与外部工具的效果撤不回」;这一轮有转到后台的调用(工具卡带 taskId)时另写「它们在这一轮结束之后写的文件不会撤回」。结果如实回报:恢复与删除了几个文件、删掉几条消息,之后的回合也改过的文件点名写出,没能回滚的写原因。连对话一起回滚、真的删掉了消息时,把那一轮 owner 的各条输入(开轮的与插进来的,别人的不算)的文字空行连起来放回输入框,不自动发,附件不放回;输入框里已有字时接在前面。没有回复、只有输入的一轮没有回滚(按整轮判,被别人的话隔成几段时哪一段都不出)。
    • 「更早的消息」按快照给的游标取 session/history,接在前面。
    • 工具卡、思考与提问随附的转发默认收起,可以逐张展开,头部的「全部展开」「全部折叠」一起改。
    • 连续 3 个以上的工具调用(夹着的思考一起)收成一行,按展示种类计数:只有一种时写「执行了 3 条命令」「改动了 3 次文件」「调用了 3 个工具」,几种时写「调用了 5 个工具:执行命令 2、改动文件 1、其他 2」(没声明 display 与种类认不出的算「其他」),点开才渲染里面的条目,开合也跟着「全部展开」「全部折叠」。其中有等审批、失败、正在生成、运行中,或转到后台、后台任务还在跑的不收;还在跑的这一轮最后一段末尾那一组不收,收尾后才收;被停止的(aborted)与结算过的审批照收。写给用户的正文、通知把一组隔开,不收进去。终端照同一条规则(见 plugin-cli)。
    • 附件:图片、音频、视频按媒体地址取,外站图片经图片代理;图片取不到时换成图标加「图片取不到」,读屏名字是替代文字(Markdown 与声明式页面的图片单元格一样)。文件与通知带的附件是一行图标、名字与类型(扩展名,没有时写「文件」,只是名字的显示),有句柄的点开时弹出对话框、取来(/api/session-file,取完之前对话框写着正在读取,关掉对话框就停下这次读取)按内容判,与文件读取同一个判定:UTF-8 文本(含没有扩展名的 README、源码与配置)在对话框里预览,名字是 md、markdown 的照常渲染(内容判不出是不是 Markdown,名字只决定怎么渲染)、其余原样,预览里也能下载;不是文本的关掉对话框、用读到的字节直接下载,超过 512 KB 的不读完、关掉对话框直接下载;取不到的在对话框里写明原因。网址的文件链到网址(新标签页打开),没有句柄的只写名字与类型。
  • 工具卡:一行写动作与对象、右侧写状态(正在生成、运行中、失败、已停止、已发出(经工具发进房间、平台收下)、耗时,结算过的审批写成一句),点开看细节。状态最多占一行的一半,工具名始终看得见;放不下时句末省略,悬停看全文。失败用危险色,命令的退出码不为 0 时写「退出码 N」;回合停止时还没结束的调用(状态 aborted)写「已停止」,与回合的结局同一个说法,不用危险色。模型还在生成参数时(状态 generating)只写工具名与「正在生成」,不能展开;开始执行后同一张卡照常。按条目里的 display 渲染:command 写命令与工作目录,file-change 写路径与删、加两侧;没有 display 的,以及种类认不出的(服务端比客户端新时),走通用卡,逐字列出参数。前端不认识任何工具名。转到后台的调用(条目带 taskId)保持活的:状态与耗时取后台任务(在跑的转动、耗时实时走,结束的写结局与用时,失败用危险色、命令写退出码),下面一行是输出的最后一行与「输出」「停止」;登记表里找不到的(超出保留被清掉的)只画这次调用本身。来自参数的文字显示前经 @aalis/api-approval 的 sanitizeForCard 清洗。
  • 审批卡:挂在要批准的那张工具卡下面,只在审批推到这条连接(快照或 approval/requested)时出现。标题写要做什么与工具名(「要改动文件 · file_write」)。事实取会话服务给出的命令、目录与 diff,没声明展示的逐字列出全部参数,显示前一律清洗;命令、参数与 diff 完整列出,不设行数上限、不在框内滚动,长串空白后面的半截照样在卡上(服务端已按长度截断并写明省略多少)。工具卡的细节照旧限高、在框内滚动。只有「批准」「拒绝」,拒绝可附理由(approval/respond 带回内容哈希;理由框里按 Esc 收起理由框);不显示倒计时,出现时不抢输入框的焦点,没有批准的快捷键。结算后卡片收起,工具行写成一句:「已批准 · 10:32」「已拒绝 · 10:32」「已按拒绝 · 没人在场 · 10:32」「已撤销 · 回合已结束 · 10:32」,不是 owner 批的、拒的写出是谁(「已批准 · <账号> · 10:32」,按结算的应答人 by 判,会话服务只给平台与账号);客户端不判谁能批,owner 照样能批这一轮里别人做不了的动作的参与者审批;发起时就被拒(没人在场、超过频率上限等)的同样写成一句;刷新后从条目的结算恢复。
  • 排着的消息:输入框上方一行一条列出排着、还没进一轮的输入(queue 条目):说话人(内部注入写「来自 <来源>」,同时间线上的消息)、写的话(放不下时句末省略)、附件个数;进了一轮就从列表移走,作为这一轮的输入出现在时间线上。排得多时列表在自己的框里滚动,不挤掉时间线;列表空时不占位置,手机上同样。
  • 输入框:附件一律接收(按钮选文件、粘贴,或把文件拖到这一栏的时间线与输入框上,拖着时输入框描边),只有附件也能发,读成 data URL 随 turn/send 发出(只带内容与名字,类型一律写 application/octet-stream、不带浏览器报的类型,种类由会话服务按内容判定;空文件照常发出),一条消息的附件合计不超过 64 MB。回合在跑、又没有要发的内容时发送键换成停止(带圆圈的停止图标,turn/abort 带这一轮的 turnId)。没有对话服务时不可用并写明原因;连接断开时也不能发,这时拖进来的文件照样拦下,不让浏览器离开页面去打开它,也不加成附件。发送成功后只清掉发出去的文字与附件,请求在途时接着打的字与新加的附件留着。打的字是按会话的草稿,存在本浏览器(aalis.drafts,只记最近改过的 50 个会话,打开不算改过),切会话、刷新后还在,发出去就清掉(请求回来时清,发送在途时切走了会话也一样);首页新会话的草稿也一样;浏览器存储不可用时照常能打字、发送,只是不记。左下是 composer 档设置项的快捷切换,只写值(模型名、档位名),名字在读屏标签与悬停提示里:能从选项里选的是选择器,改了即存;其余是链接,写按可选项标签显示的值,没有值时写登记方给的「没设时」(unset),都没有才写名字,点了去「设置」标签;可选项没取到时悬停提示与选项末尾写明原因;收到 session/config/changed 重取。
  • 右侧面板:宿主的标签(计划、设置、用量、后台)与插件的会话面板在一个标签栏里,走同一套机制(src/workbench/panel-tabs.ts)。标签的顺序与开关取自布局记录的 panelTabs(见 api-webui 的「布局记录」),一个合成函数按「此刻可用」把表里开着的排成标签栏,把表里关着的与适用、还没进表的插件面板(按页面的先后)列进标签栏末尾的「+」,「+」里没东西时不出现。宿主标签的可用性写在一张宿主表里,按数据出现,条件见 api-webui 的「布局记录」里会话面板的标签一条;用量另在手机上不出现。标签列表之后是关闭当前标签的按钮(标签列表里只能有标签),在 Tab 顺序与读屏树里;选中的标签上按 Delete 也能关(Mac 上标着 delete 的键送出 Backspace,两个都认),按钮的悬停提示按平台写着这个键(Mac 上是 ⌫),关掉后焦点交给新选中的标签。关掉的标签(宿主的也一样)留在原位、进「+」,从「+」打开回到原位;关掉后回到不带标签的会话地址,上一条历史就是它时退回去,不多留一条历史;打开与关闭按操作写进布局记录(open-tab、close-tab),写不进去时出错 Toast;布局记录没取到时宿主标签按都开着显示,记录里有前端不认识的宿主标签(webui-server 比前端新)时按不可用跳过。别处指向标签的入口(桌面上头部的用量百分比与「后台 N」、手机「更多」里的「后台 N」、工具卡的「输出」、输入框旁的设置项)在标签关着时先打开它,写入成功再跳过去,右侧面板收起着或是抽屉时一并展开;输入框旁的设置项是链接,不是左键或按着修饰键的点按交给浏览器(新标签页打开)。标签由地址决定(/s/:id/plan、settings、usage、background,插件面板是 /s/:id/p/:instance/:pageKey),没写或写的标签不在标签栏里时停在第一个;标签都关了时只剩「+」,手机上从「更多」的「会话面板」照样进这一层。
    • 计划:可以清空(plan/clear)。
    • 设置:session/config/get 给出的各行(功能位 settings 为假时没有这个标签),顶部写「改动从下一轮起生效」;composer 与 common 在前,advanced 收进默认折叠的「高级」,按声明它的插件实例分组,组名取插件显示名,后缀实例写「显示名 · 实例名」。每行写名称、生效值与「来自:<来源>」,来源按层表写:全局写「全局(插件配置)」;平台档写「平台档「<平台>」」,平台取回包的 platform(会话服务这次按哪个平台取的平台档,WebUI 与终端的会话是入口平台,如「来自:平台档「webui」」);平台类型档写「平台类型档「<平台> · <类型的显示名>」」(没声明显示名时写类型名);场所写层的列表里场所的称呼加「(场所)」,如「来自:群「读书会」(场所)」「来自:工作区「论文」(场所)」;全局层的值缺失或不合法、落到作者默认时写「来自:默认」;本会话这一层设的不写来源。值按可选项的标签显示(动态可选项一并取来,如工具分组的 * 写成它在可选项里的标签,列表里有代表其余全部的一项(all)时只写它;不在可选项里的按可选项接口另给的 labels 写名字,取不到时写原样),空列表、空映射与空文字写「无」;哪一层都没设时写字段的默认值,没有默认值时写行的 actual(声明字段的插件给出的此刻实际在用的值,如「跟随服务偏好(此刻:<模型名>)」),再没有写字段的 unset(开关写在开关下面)。顶部选改动写到哪一层:「只这个会话」(缺省,session/config/update)、根会话的场所(「这个工作区」,场所是窗口的按多方标记写「这个群」「这个私聊」;会话摘要里没有场所的,即没有场所层的空记录,没有这一项)与「全局(插件配置)」,后两种经 session/config/layer/update 写,之后随 session/config/changed 重取;子会话只有「只这个会话」(各项在建立时写在它自己那一层,改到场所或全局对它不起作用);平台档与平台类型档不在这里选,只在设置的「会话默认」里改。生效值出自比选中的更窄的层时,那一行写「这里还被<来源>覆盖」。层表没取到时设置行照常列出,来源按层名写,另写一句层表没取到。点行就地修改、改完即存,开关直接拨;生效值出自选中的那一层时行尾有「恢复继承」(全局层叫「恢复默认」);选「全局(插件配置)」时,生效值出自全局层(或按作者默认)又等于作者默认的行名字旁标「默认」、不出「恢复默认」,与「会话默认」的全局层一致;写不进去时原因写在行下,写进了运行态而没落盘时写回包的 unsaved 那一句。按字段类型渲染,前端不认识任何设置项;可选项取自字段的 dynamicOptions(/api/models/:service),模型字段(llm-ref)取模型服务。字段声明了 dependsOn 的,取可选项时带上同一实例里那一行的生效值(没设时带它的 actual;模型写成条目 id,放在 dependency 查询参数里),那一行变了就重取;那一行眼下给不出可选项时这一行不能改、写「当前<那一行的名称>不能调」,生效值不在可选项里时写「当前<那一行的名称>没有这一档,按默认(<默认那一项的名字>)」。行尾按钮与名字所在的那一行竖直居中:行的上下内边距让只有一行字的行正好撑满单行的行高,名字在单行与写着值的两行里都在同一个高度。输入框旁的快捷切换(composer 的项)只写本会话这一层,同样按 dependsOn 取可选项,没设的模型写此刻的模型。插件启停、别处改了设置或重连后重取。
    • 用量:头部的百分比是上下文已用占窗口的比例,弱化色,达到压缩线(compressAt)换警告色。拆分表写上下文窗口、已用、条目里的各分项(行名与次序照条目的 parts,前端不认分项)、为回复预留(maxTokens)与剩余,为 0 的不显示;带注入者明细的分项可以展开,按注入者的插件实例合并到插件,行名取插件显示名,对不上的归「其他」。表上方一条按上下文窗口画的堆叠进度条:分项按占用从大到小堆叠、取图表色(--color-chart-1 到 -6,多出的与为回复预留用弱化色),表里每行前一个同色的点作图例,剩余与条的底色相同;有压缩线时在条上标出位置,表下写明「到 N% 自动压缩」。不知道窗口时没有进度条与剩余。表下另写会话累计的输入与输出 token(摘要的 tokens,有子会话时写明「含子会话」),没有时不写。手机上从头部的百分比点开,从底部弹出。
    • 后台(src/background/):子会话与这个会话转到后台的工具调用两组,组头带数目,在跑的在前,结束的收进默认折叠的「已结束」(刚建、还没跑完一轮的子会话不算结束);有在跑的后台工作(摘要的 background 大于 0)时顶上有「全部停止」(background/stopAll:停各级子会话的回合,以及本会话与各级子会话的后台任务,不停本会话的回合)。子会话按缩进列这个会话的各级子会话(sessions/list 带 parentId 按层取,有子会话的行再往下取),每行写状态(它或它下面的子会话在等审批时写「等你批准」,其余是转动、完成、出错写原因、已停止)、标题、耗时(建会话到最近一轮结束,在跑的走到此刻)与 token,点行打开子会话,在跑的有「停止」(停它的回合并对它全部停止)。工具调用一行:display 是命令的写等宽单行的命令(放不下时中间省略,悬停或点开看全)与工作目录(最后两级),其余写工具名;另有状态与耗时、输出的最后一行(弱化色,实时更新)。最后一行里的本机网址只在页面经回环地址打开时给「打开」链接(src/background/loopback.ts,客户端唯一写回环地址的地方),否则只显示地址。动作「输出」与「停止」:「输出」在标签里打开日志(background/output,先取缓冲里的全部,最后输出时间或状态变了就取新增的接在后面,只留末尾 64 KiB;最新的在底部,停在底部时跟着走、往上翻看时不被拽走;可复制;早于缓冲的已丢弃时写明),桌面嫌窄就拖宽面板,手机上从底部全屏弹出,不另开地址;「停止」不弹确认。界面只画登记表与会话树推来的状态:按停止之后等 background/changed 推来,不先自行标成已停止。后台任务整页取一份(src/background/tasks.tsx):握手与重连后取 background/list,之后按 background/changed 的增量更新,没有后台任务登记(unsupported)时按没有任务。
    • 插件面板:位置为 session 的页面,默认不在表里,从「+」第一次打开时追加在末尾,在所有适用的会话里出现。哪些适用由 GET /api/session-pages?sessionId= 按会话取,换会话、插件登记表变了、会话面板页面推来不带会话或带本会话的 page.invalidate、管理连接重连后重取。面板里的页面只有手动刷新,放在标签栏里「+」的前面,不另占一行;取数与动作都带上当前会话。插件前端的面板按下面「插件前端」一节挂载,标签内容顶上写出处。适用的页面、页面表或布局记录没取到时「+」前面多一个写明原因的按钮,点它重试,「+」照列已知的。
    • 插件的显示名取自 GET /api/plugins,插件登记表变了(plugins.changed)或管理连接重连后重取;没取到时写明,按插件实例 id 显示。
  • Markdown:她的回复、思考与插件给的文字都不可信。原始 HTML 当文字显示;图片只认媒体地址(/api/media 与存储地址),外站图片显示成链接、不发请求;链接只许 http、https 与 mailto,外链在新标签页打开;公式(KaTeX)不信任输入;单个 ~ 不是删除线(~~删除~~ 照常);行内公式是 \(...\)、单行的 $$...$$,以及按成文规则认出的单美元(开头的 $ 右边紧跟非空白;结尾的 $ 左边紧跟非空白、右边不紧跟数字或 $,以免和相邻的 $ 连成 $$;同一行,中间没有未转义的 $),认不出的单美元按字面显示,「$5 和 $10」不会成公式;代码(包括流式输出时还没闭合的围栏)与链接地址里的不当公式;表格分隔行的列数对不上表头时按表头修好:模型常多打或少打一根竖线,渲染器遇到列数不等会把整张表退成纯文字,只改显示,存下的原文不动;代码块按语法色令牌高亮,上方一条细的标题行写语言名与复制按钮(剪贴板可用、即安全上下文时才有按钮),桌面与触屏一样常显、不占代码区,按钮看得见的与这一行同高,触屏上点按范围由伪元素补足;没写语言、又不能复制时没有这一行;图片取不到时显示替代;Mermaid 用到时才加载,用严格安全级别,颜色取自当前主题的令牌,换主题后重画;所在的消息还在输出时只显示源码,输出完才画。
  • 附件:图片、音频、视频按媒体地址经 /api/media 取,外站图片经 /api/proxy/image;存储里的文件按句柄经 /api/session-file 下载,网址的文件链到网址,没有句柄的只写名字。

声明式页面 ​

插件页面默认是声明式:只交声明(api-webui 的「声明式页面」一节),由这里统一渲染,前端不认识任何插件的页面键与动作名;需要自绘时用插件前端,见下面「插件前端」一节。页面表取自 GET /api/pages,布局记录(固定的页面、会话面板的标签)取自 GET /api/layout,整页各取一份:页面表在 plugins.changed、布局记录在 layout.changed 后重取,管理连接重连后两样都重取;重取失败时留着上一次的,打开着的页面与对话框不受影响,原因写在固定区。

  • 标题栏:标题,带实例后缀的实例另写实例名(弱化色),手动刷新;位置为插件详情的页面另有「固定」(按下态,再按一次取消),按操作写进布局记录(POST /api/layout),写不进去时出错 Toast;布局记录没取到时「固定」换成一个写明原因的按钮,点它重试。会话面板里的页面只有刷新,标签就是标题,刷新放在标签栏里。
  • 页面不在:地址里的页面不在页面表里(所属插件停用或卸载)时显示「这个页面所属的插件已停用」与「去插件详情」,页面重新登记后自动回到原页;页面表没取到时写明原因,可以重试。设置里的一节不在 /p/ 下显示。会话面板里同样:地址上的插件面板所属的插件停用时,标签栏末尾留着「已停用」这一个标签,内容是同样的占位,关掉它回到会话;插件重新启用后它回到标签栏里原来的位置。
  • 取数:每个组件打开时按 GET /api/pages/:instance/:pageKey/data/:componentId 取一次(会话面板带 sessionId,详情里的带选中的 rowKey),page.invalidate 指到它时重取:带组件 id 的只重取这个组件,带会话的只让打开着这个会话的面板重取。手动刷新与管理连接重连后整页重取。一块取数失败只影响这一块:原地写明原因,可以重试。
  • 排法:竖排的组件,或者标签页(选中的标签写进地址,切换用替换,不多留历史;标签放不下时横向滚动)。表格与关系图带详情时是主从:选中一行(点行或点行首的链接)写进地址,从没选中到选中是新的一条历史,后退回到列表;宽时详情在右边、占得比列表多(列表窄了就按表格的规则只留标了 primary 的列),详情贴住滚动区的顶边,长了在详情里自己滚,滚到头不带动列表,标题与关闭按钮贴着详情的顶边,列表照旧随页面滚;窄时详情代替这一屏(同一屏的其他组件收起),头部是「返回列表」。地址里选中的行已经不在时不显示详情。
  • 组件:
    • 表格:复用房间表的表格,排序、搜索与分页在客户端做,行数超过一页才出现搜索框与分页;纯数字的列右对齐、等宽数字,数字按外壳实际用的语言(外壳的句子选中的那份字典的语言,即 <html lang>)分千位,同一列按这一列最多的小数位补齐(最多 3 位);窄时只留标了 primary 的列,都没标时留第一列,带详情的表格另留选中用的那一列,长的状态折行。行动作按 visibleWhen 显示;宽时第一个不是 destructive 的动作是文字按钮,其余进「更多」,窄时全部进「更多」;这一行有动作还没回来时写「进行中」。没有行时写声明的空状态。
    • 关系图:力导向排一次就定住,节点与边按类别的 tone 取 --color-chart-1 到 -6,没有类别的用中性色;上方的图例只列这张图里有的类别,节点类别画成圆点、边的类别画成短线;滚轮、双指与拖动缩放平移,角上有放大、缩小与复位;带详情时节点是按钮,选中的节点在详情里看;鼠标点选不留浏览器给 SVG 元素的默认焦点框,键盘聚焦照常有焦点环。
    • 键值:名称一列、值一列,数字用等宽数字、分千位。文字:按 Markdown 渲染,规则同下面「会话工作台」的 Markdown。按钮组:次要按钮,destructive 是危险色字的文字按钮;详情里的按钮组作用于选中的那一行。
  • 单元格:文本、时间(今天写时分,别的日子带日期,悬停看完整时刻)、倒计时(每秒走,到零写「已到时间」,不重取)、链接(只认 http、https,新标签页打开,别的按文字)、状态(需要处理警告色、出错危险色、进行中带旋转图标、停用弱化色,正常不上色;宽时不在中间折行,窄时折行而不撑出横向滚动)、代码、长文本(收起时只占一行、末尾省略,按可折行的文字算宽,窄时不按全文撑宽所在的列;点开看全文)、图片(只认宿主换好的 /api/media 地址,懒加载,别的按文字)、文件(有句柄时是 /api/page-file 的下载链接,没有的只写名字)。字符串数组按界面的语言连成一句(代码等宽,长文本可展开,别的种类按文字;空数组按空值)。
  • 动作:destructive 由宿主弹确认框,标题是按钮上的字加问号,正文是插件给的确认文案(没给时写「这个操作不能撤销。」),关闭键写「返回」(免得与「取消运行」一类以取消开头的动作名点反),焦点落在它上面;带 input 的弹一个单列的小表单(手机上从底部弹出),控件按字段类型(文字、数字、多行、列表一行一项、映射一行一个「键=值」、选择与动态可选项、多选、开关是复选框),字段一行的写法与插件配置相同(FieldRow:标签在控件上面,说明与问题在控件下面),必填的写「必填」,行动作里与行字段同名的输入项用这一行的值预填(「编辑」;多选与列表字段取数组,其余取标量,类型对不上的与逐行写不出的列表不预填),其余取字段的默认值;destructive 的表单开头同样写确认文案(没给时写「这个操作不能撤销。」)。提交时空着的可选项不送,读不出的(不是数)写在字段下,服务端逐项的问题写回对应的字段、表单不关。其余直接调。发出后到回来,这个动作在这一行上算进行中(按钮或行上写「进行中」);确认框与表单在回来之前可以关掉,请求照常进行,回来时只关发起它的那个。成功时处理函数给的一句结果用 Toast 显示,失败时出错 Toast 写原因(表单还开着时写在表单上)。结果变了由插件 invalidate,界面不自己重取。
  • 图标:只按 lucide 图标名查,没写或不认识的用通用图标;图标名的对照表单独成块,用到页面图标时才加载,每个图标也是用到时才加载。
  • 宽度:组件按容器宽度变形(容器查询),会话面板与手机共用一套规则:表格窄于 36em 时只留标了 primary 的列、行动作全部进「更多」,写字的格子折行(没有断行机会的长串也在格子里断开,数字列不折),不把后面的列挤出视野;一屏窄于 36em 时关系图矮一些,窄于 56em 时主从的详情代替这一屏。

插件区 ​

插件区在 src/plugins/。插件列表取自 GET /api/plugins,整页取一份(list.tsx),plugins.changed 与管理连接重连后重取;会话设置「高级」的分组名、用量里按插件合并的行名与插件前端的出处也读它。

  • 实例树(左栏):按插件声明的 subsystem 分组,组名与顺序取 api-webui 的 DEFAULT_SUBSYSTEM_METADATA,目录外的按 id 原样排在后面,没声明的归「其他」;组里按插件分、按显示名排,多实例插件的其余实例挂在主实例下面。状态写成短的一句:正常运行不写,启动中、缺服务、等待启动、待配置、失败、已停用写在行尾,缺服务与待配置另有行首的需要处理标记,失败另有出错标记;只在等别的插件启动的写「等待启动」,不带标记;在等自己旧实例停下的(列表项带 lingering)也写「等待启动」,详情头部的原因写「等待 <插件> 的旧实例停下」。顶上是「市场」与「只看有问题的」(只留失败、待配置与缺服务的实例);头部的「重新扫描插件目录」(POST /api/plugins/scan)加载手动放进插件目录的包,扫完重取列表,回执写在 Toast 里。列表没取到时写明原因,可以重试。桌面上 /plugins 的主区写「选一个插件」,旁边的「打开插件列表」在左栏是抽屉或收起着时把它打开。管理动作(启停、重载、实例增删、装卸)做完等最新的一次列表取回才跳转,这次取数被插件登记表的变化作废时接着等后来的那次。
  • 详情(/plugins/:instance)由宿主拼装,没有内容的块不出现:
    • 头部:包名与版本,状态与原因(缺服务写每条链与根,待配置写没填的键,等待启动写等谁启动,失败写失败的原因);停用、重载(POST /api/plugins/:name/reload),已停用的是启用;「查看日志」进运行状况的日志标签、按这个实例筛好;主实例另有卸载(经市场,先确认,列出依赖它的插件,写明代码删掉、数据目录里的数据留着、配置默认留着;配置文件里有它的配置段(configSection,空段也算)时另有「同时删除配置(含 N 个密钥)」,默认不勾,N 是只有它的各实例引用的密钥数),其余实例另有「移除实例」(先确认,移除后回到主实例;手机上实例块的行没有「更多」,靠它移除)。插件列表带 self 的是提供这个界面的插件,停用与卸载不可点,写明原因。写进去了就看头部的状态,出错用 Toast 报;无论成败都重取列表。
    • 需要处理:这个插件的提示,与首页摘要同一种行,不写出处;故障原因写在头部、缺的服务写在依赖块,这里不重复。没有提示时不出现。
    • 配置:按 schema 画的整张表(ConfigForm.tsx),插件停用或失败时照样能改。宽时标签一列、控件一列,窄时标签在控件上面;必填写「必填」,改了没保存的行左边一条强调色竖线、行尾有「撤销」,数组整块算一项。每一项写作者默认:当前值等于作者默认时名字旁标「默认」,不等时控件下面一行写「默认:<值>」(值按选项的标签写,动态可选项一并算,与控件里的值同一份称呼,开关写开或关,空的写「无」,长文字截到一行、悬停看全文,对象数组写「默认:N 项」),行尾一个「恢复默认」;没有作者默认的项没设时标「默认」并写「不设时:<unset>」,设了时写「默认:不设」;密钥字段不写默认值;规则在 src/settings/field.ts 的 authorDefault 一处,设置行也用它。可按会话覆盖的字段(功能位 settings 为真时)名字旁加「可按会话设置」,全局之外另外设过的再加「另有 N 处设置」(子会话逐个算进 N),点开在这一项下面列出每一处写在哪(层写它的标题,如「平台类型档「onebot · 群」」「群「读书会」(场所)」;会话写「会话「<标题>」」,子会话另写「(父会话「<标题>」)」)与设成了什么,每处链到设置的那一层或那个会话的「设置」标签;数据取自 session/config/overrides,session/config/changed 后重取,没取到时名字旁写「别处的设置没取到:<原因>(点按重试)」(src/plugins/session-marks.tsx)。写了 preference 的字段(对话代理的「模型」)不在表里,它的全局层是下面「服务偏好」块里那个服务的一行,同样带两个标记。底部一条写几项未保存,有「放弃」与「保存并重载」;只改了可按会话覆盖的字段、或插件停用时是「保存」(不重载插件);保存的结果也写在这条上。只送改了的顶层键(分组、数组整块),清空的顶层键送 null,由服务端回到默认值;写法读不出的(数字框里不是数、映射里有一行没写等号)拦下不送;服务端逐项的问题写在对应的行下。「恢复默认」把草稿换成默认值,顶层与分组里已存的密钥照旧保留。这个实例的配置在别处改了(plugins.changed 后重取到的与草稿的基准不同):草稿没有改动时静默换上新值;有未保存的改动时不丢草稿,底条写「配置已在别处改动」,附「载入新值」;自己的保存在路上时收到的推送不处理,保存完成后统一重取。
    • 密钥(secret: true)只写不回显:文档里是引用,行上只写「已设置」与键名,「更换」后输入的新值替换、留空保持原值,「清除」送空值,撤销在行尾;顶层与分组里的替换写进原来的键,被别处共用时写明会一并改到几处,数组元素里的另建新键。没有引用(没设置过,或宿主没有密钥存储、文档里是明文)时用密码框照常编辑;没声明 secret: true 的字段一律明文显示,不按名字猜。模型字段(llm-ref)可以选「跟随默认」,旁边的「刷新模型列表」让会自己发现模型的服务重新探测一遍。
    • 依赖与贡献:依赖的服务由谁提供(宿主自己提供的不列);缺的服务每条写「<服务>:缺」与链和根:根已停用的一键「启用 <根>」(没启用成时 Toast 的「查看日志」去这个根插件),根失败与在运行却没在提供的「去处理 <根>」进它的详情(启用失败的插件只是重试,配置错了还会失败),根待配置的「去配置 <根>」,没有提供者与成环只写说明;在等的写「<服务>:等待 <根> 启动」。多实例的按钮上写实例名。另写可选依赖、提供的服务、工具、指令、扩展的事件与钩子。
    • 服务偏好:它声明的服务里有几个提供者(或给它设过本插件偏好)的各一行(服务表取自 GET /api/services,本插件偏好取自插件列表那一项的 servicePreferences,此刻的选择取自 serviceSelections),设了的写所选与「本插件偏好」,没设的写「跟随全局偏好」;每行另写它此刻用的那一家与依据(「现在用:<提供者>(本插件偏好 / 全局偏好 / 没有偏好,按优先级)」);改选送本插件偏好(POST /api/plugins/:instanceId/services/:name/prefer),「跟随全局」清除(DELETE 同一路径),之后重取插件列表;所选的那一家不在时句首另写「这一家现在不在」。一个可按会话覆盖的字段写了 preference 指向这个服务时(对话代理的 llm),这一行就是那个字段的全局层:会话设置在时总列出,带「可按会话设置」与「另有 N 处设置」。顶上一句提示:不需要让这个插件改用别家、也不需要它固定用某一家时别设,跟随全局偏好。没有这样的服务时整块不出现。
    • 页面:这个插件登记的全部页面与位置;位置为插件详情的点开到 /p/:instance/:pageKey,设置里的一节链到 /settings/p/:instance/:pageKey,会话面板写明在会话右侧面板的「+」里打开;插件前端另写它申请的宿主数据能力,能力的显示文字由宿主按 api-webui 的能力词表给(读会话树与状态、停止会话的回合)。
    • 实例:可多开的插件列出全部实例,新建(写错了的原因写在框下,建好跳到新实例)与移除(先确认);主实例不能移除。
  • 市场(/market):npm 上带 Aalis 类型关键词的包(GET /api/marketplace),加上本地已装的系统组件(GET /api/system-components)。类型(默认只看插件,另有前端界面、系统组件与全部)、状态与搜索在本页筛,搜索匹配名称、说明、作者与包声明提供的服务,搜不到时写明「按服务搜只找得到在 package.json 里声明了提供服务的包」;条目是图标加文字,写版本(装了的写本地版本)、官方或社区、来源、说明与「提供:<服务>」,只认 http、https 的主页链接。插件与前端界面可以装卸,先确认:装之前列出一并装上的包、要用的服务由谁提供,卸之前列出依赖它的,配置与详情里的卸载同一写法;装上插件后跳到它详情的配置区(安装的回执带 plugin);安装的确认框(InstallDialog.tsx)与「开始使用」共用。可更新的勾上后用「更新所选」一起提交,选了内核或宿主时写明会整个重启,没过预检时逐条列出冲突。服务端的说明(检索源不可达、查不到最新版)写在头部下面。

设置 ​

设置在 src/settings/(SettingsArea.tsx)。宿主的分节与插件的分节(位置为设置的页面)是同一张分节表,宿主先占好位置:「会话默认」在最前,插件的分节按页面的先后排在它后面,「外观」「系统」在最后。左栏是这张表的目录,主区按地址打开一节;「会话默认」只在会话服务在(插件列表里有提供会话协议、正在运行的插件)时出现,插件的分节随它的页面登记与撤回,「外观」「系统」总在。插件列表或页面表没取到时只少依赖它的那几节,目录写明原因、可以重试;直接打开「会话默认」时同样写明插件列表没取到,不当成会话服务没启用。每一节的标题栏在手机上有回到目录的按钮。

  • 插件的分节:用声明式页面的渲染器画,标题栏有手动刷新(整页重取);页面不在页面表里(所属插件停用)时显示「这个页面所属的插件已停用」,链到插件详情,重新登记后回来。
  • 设置行(SettingRow.tsx):会话的「设置」标签、会话默认、房间的「场所设置」与系统共用。每行写名称、来源(徽标,会话的「设置」标签写成值下面的一句「来自:…」)、生效时机(要重启的写「重启后生效」)与生效值,生效值按可选项的标签显示,没有值时写可选项接口标了默认的那一项,再没有写登记方给的「没设时」;点行就地修改,编辑控件就是表单用的 FieldInput,没有值时从此刻的状态开始(选择框停在「不设」那一项上,开关、勾选框与输入框从行的 actual 开始),可选项由这一行取一次交给它:选择与多选选了即存(有序的多选,如语言列表,选中的按先后排在前面,各带「上移」「下移」,读屏名是「上移 <选项名>」这样的整句,勾选、取消与移动之后焦点留在那一项上(存的时候焦点去了别处就不拉回),写回的就是新的顺序;无序的多选按可选项的先后排、勾选不挪位,存完焦点同样回到刚勾的那一项;可选项带成员时下面有「包含 N 项」的展开(读屏名带这一项的名字,如「网页:包含 2 项」),默认收起,点开列出成员、不改勾选,有序的在上移、下移那一行的下面;来源另给不属于任何可选项的成员时,末尾一行写它的名字、能展开;可选项里有代表其余全部的一项(all,如工具分组的 *)时,勾它只存它、取消它即全不选,选着它时各项都显示勾上、不分先后,取值里另写的名字不列出、没有添加框,取消某一项存其余各项,各项都勾上的固定名单不算选着它),其余回车或离开时存,Esc 放弃,写法读不出的写在控件下、不存;数字清空、选择选空的一项写 null,与行尾的「恢复继承」「恢复默认」一样回到下一层,文字与列表清空写的是空值;存的时候编辑控件不可用;编辑控件组里的按钮(模型旁的刷新、多选的「完成」)按下时不动焦点,免得有的浏览器先失焦把编辑控件收起;开关直接拨;写不进去的原因写在行下。
  • 会话默认(SessionDefaults.tsx,功能位 settings 为真才有这一节):先选层、再改这一层的项。层的列表取自 session/config/layers,按层表分组(组名是层表里的称呼:全局(插件配置)、平台档、平台类型档、场所):全局一行;平台档每个平台一行,写平台名;平台类型档写「<平台> · <类型的显示名>」(没声明时写类型名);场所写场所的称呼(默认工作区、工作区「<名字>」、群「<群名>」),窗口在称呼后补平台。顶上一个搜索框,按层的称呼、平台名、类型名与类型的显示名、场所名与场所键过滤。所选的层记进地址(?layer=<层名>[:<平台>[:<类型>]],场所写 place:<场所键>),刷新与分享链接都回到这一层,地址里的层不在了时回到选层。宽屏左右并排,没选时右边写「先在左边选一层」;手机上是两屏,第二屏顶上「换一层」。选中后标题写这一层的标题(「平台类型档「onebot · 群」」「群「读书会」(场所)」,不带平台以外的说明),下面一句先后说明按层表写(「会话设置按本会话、场所、平台类型档、平台档、全局(插件配置)的先后……」),再下面是这一层的行(session/config/layer/get,LayerRows.tsx,与房间的「场所设置」共用),与会话的「设置」标签同一套:这一层没写的项写它从哪一层继承来(窗口场所的继承来源写明平台与类型,如「来自:平台类型档「onebot · 群」」)。全局层的每行按作者默认的规则写,行尾按钮叫「恢复默认」,其余层叫「恢复继承」;全局以外的层存着不合法的值(生效值不出自这一层)时,那一行写「这一层存的值不合法,生效的是<来源>的值」(生效的是作者默认时写「这一层存的值不合法,生效的是默认值」)并给「恢复继承」清掉它;全局层的「模型」是对话代理的本插件偏好,没设时写「跟随全局偏好(此刻:<模型名>)」。改动经 session/config/layer/update 写;写进运行态而没落盘时行下写回包的 unsaved 那一句。session/config/changed、plugins.changed 与重连后重取层与行。
  • 外观:
    • 主题:「跟随系统」开时选深色、浅色各用哪个,关时选一个;可选的有内置的 8 个、导入的(写「(导入)」)与插件提供的(写提供它的插件)。打开跟随时正在用的主题留在它那一种深浅上,关掉时固定成正在用的。这些存在本浏览器。
    • 主题文件:列出全部主题,每个都能导出成主题文件(经下载链接,文件名取主题名);导入一个文件:超过 16 KB、不是 JSON、过不了 parseTheme(不在令牌表里的名字、不是颜色的值)的拒收并逐条写明原因,缺的令牌按同种类的偏中性补齐,同名同深浅的替换旧的;导入后按 themeContrastIssues 列出对比度不够的组合,仍可使用。导入的主题可以移除。
    • 快捷栏:图标栏上固定的页面,按记录的顺序,可以上移、下移、取消固定;所属插件没启用的写「<插件> · <页面 key>」并标「未启用」。
    • 会话面板标签:宿主标签与插件标签按记录的顺序,可以上移、下移、显示、隐藏,插件标签另可「移出」(remove-tab,从记录里删掉,下次从「+」打开时排在最后);所属插件没启用的标「未启用」,隐藏着的不能再显示。
  • 系统:
    • 宿主配置:GET /api/config 给的核心键,来源是「配置文件」(文档里写了)或「默认」,restartKeys 里的键写「重启后生效」;改一项只送这一个键(PUT /api/config),「恢复默认」送 null。保存了要重启的键之后,这一块顶上出「改动重启以生效」与「重启」按钮(POST /api/restart)。
    • 服务偏好:GET /api/services 里有几个提供者(或写过偏好)的服务,写正在用的提供者,来源是「服务偏好」或「默认」;改选送偏好,「恢复默认」清除。llm 在这里是普通一行,改的是全局偏好(标题生成、摘要等没设本插件偏好的插件用它);对话代理的模型是它的本插件偏好,在插件详情的服务偏好块与「会话默认」的全局层改。界面前端(webui-client)也在其中,换了以后页面重新加载,新的前端用不了时打开 /__clients 切回。plugins.changed 与重连后重取。
    • 数据目录:宿主路径的 data,宿主不提供路径时没有这一块。

插件前端 ​

位置为插件详情或会话面板的页面可以是插件前端(api-webui 的「插件前端」一节):插件包里的一份静态网页,在这里画的隔离框架里运行,经消息通道请宿主代办。这部分都在 src/frame/:channel.ts 是握手与转发,FramePage.tsx 画出处、框架、占位与宿主对话框。

  • 外围:框架上面一行写「来自 <插件显示名>」(插件页面在标题栏下,会话面板在标签内容顶上),框架画出的东西总在这一行下面。标题栏、标签栏、占位、确认框与 Toast 都由宿主画。
  • 框架:iframe 的 sandbox 只取 FRAME_SANDBOX,referrerpolicy 是 no-referrer,不加 allow,title 取页面标题;地址是页面的 frame.src 加片段里每次挂载新生成的随机数(nonce)。握手完成前框架不可见,区域里是加载态。以 IPv6 字面地址访问时不挂载,写明「用主机名或 IPv4 地址访问才能显示插件前端」。
  • 握手:只认来源窗口是这个框架、来源是 null、nonce 对得上、过了 parseFrameHello 的第一个 hello,其余的窗口消息都不理;主版本不同时不交端口,写明两边的版本。之后建一个 MessageChannel,把 init(协议版本、experimental、会话面板的 sessionId、当前主题,界面的语言链与服务进程的时区,以及挂载前从 GET /api/pages/:instance/:pageKey/texts 取到的这一页所属插件的字典与语言包的补充)连同端口交给框架,此后只走这个端口。每次挂载(含「重新加载」、手动刷新与页面换了地址)都重取字典,用这一次取到的;字典没取到时不挂框架,原地写明、可以重试;全局语言变了整个界面重挂,框架随之按新的语言重新握手。第一次 load 之后 10 秒内没有合格的 hello 时显示「页面没有响应」。
  • 卸载:离开页面、切换标签或会话、手动刷新、页面不在页面表里(插件停用)、收到 leaving、第二次 load、每秒超过 200 条消息、宿主对话框连续被取消 3 次,都走同一个卸载:关端口,撤窗口消息监听与两条连接上的处理函数,在途的结果不再转回,关掉它弹出的对话框,清计时器。leaving 与第二次 load 显示「这个页面离开了宿主给它的地址,已停止显示」,超频显示「这个页面发了太多消息,已停止显示」,连续取消显示「这个页面反复弹出确认,已停止」,这三种在浏览器控制台记一条告警。占位上的「重新加载」与手动刷新都是重新挂载;不做隐藏时保活。页面表重取回同一个框架地址时不重新挂载,页面重新登记换了地址就重新挂载。
  • 转发:每条消息先过 parseFrameMessage,不合格而认得出 id 的回 invalid;实例、页面与会话面板的会话取挂载时的值,消息里带这些键就是多余的键。要宿主数据能力的请求按页面的 frame.host 与 FRAME_REQUIRED_CAPABILITY 放行,没申请的回 denied;在途最多 8 个,超了回 busy。输入要能写成 JSON,否则回 invalid。
    • data:调 GET /api/pages/:instance/:pageKey/data/:source,带会话面板的 sessionId 与写成 JSON 的 input,数据源名与输入由服务端核。
    • action:按页面声明的 actions 找(只认自有的键),找不到回 not-found、不发请求。destructive 先弹宿主确认框:标题是按钮上的字加问号,写出确认文案(没给时写「这个操作不能撤销。」),再按输入字段的名字以纯文本列出这次的值(没声明的键照写键名,过长截断);取消回 cancelled。然后调 POST …/actions/:action,请求体只有 sessionId 与 input。经过确认框的动作另用 Toast 显示结果与失败,其余动作的结果只回给框架。
    • open:{ session } 在应用内打开这个会话。{ url } 与宿主同源的回 invalid,其余弹宿主对话框,以纯文本写出完整地址,「打开」是在新标签页打开的真实链接(rel="noopener noreferrer"),点了回 null,取消回 cancelled;不调 window.open。
    • sessions.list、turn.abort:转发会话协议的 sessions/list(只透传 parentId、archived、cursor、limit,回包去掉 groups)与 turn/abort;会话连接没连上时回 unavailable。申请了 sessions:read 的框架从握手起收到 sessions.changed(转发 sessions/changed,去掉 groups)。
    • 错误码:HTTP 400、414、431 对到 invalid(414、431 写「输入太长」),404 对到 not-found,503 与连不上对到 unavailable,其余对到 failed;会话协议的 not-found 对到 not-found,参数不合对到 invalid,断线对到 unavailable,其余对到 failed。
  • 宿主对话框:外链与 destructive 动作的对话框,同一个框架同一时刻只一个,开着时再请求的回 denied;焦点落在「取消」上,对话框显示后 500 毫秒内点确认无效(按点按事件发生的时刻算),免得用户在框架里的连点落到确认上;连续被取消 3 次就卸载框架,中间确认过一次重新计数。
  • 推送:插件推来的 page.invalidate 按实例、页面与会话过滤后转成 invalidate(带组件 id 的写成 source);会话或管理连接重连后各推一次不带 source 的。<html data-theme> 或根元素内联样式上的令牌变了(换主题、跟随系统时深浅变了、插件提供的主题改了颜色)时按当前主题的深浅与令牌推 theme,没变不推。

连接 ​

  • 两条 WebSocket:/session 是会话协议(api-session-protocol),/admin 是管理面推送(api-webui 的「管理面推送」一节)。地址由页面所在的源推出,https 的页面连 wss。
  • 断开后按 1、2、5、10、30 秒退避重连,连上一次就从头算;网络恢复或页面回到前台时立即重试。一次也没连上就断开时查一次 GET /api/auth/status,登录失效就重新加载页面,由服务端给出登录页。
  • 会话连接先握手才算连上;协议版本不兼容时停止重连并给出服务端的原因。断开时还没回应的请求以「连接断开」失败,不重发;还没握手时发起的请求等握手完再发。
  • 订阅一律先取快照,之后的推送按 epoch 与 seq 去重;epoch 变了或收到 session/resync 时重新订阅。重连后重新握手、sessions/watch 与 background/watch(有人关心才发)并重订打开着的会话。管理面的事件断线期间不补发,重连上以后通知界面重取。
  • 在场:页面可见且最近 10 分钟有键盘或指针活动算在场。有活动时报在场(持续活动时每分钟最多一次),转到后台报不在场;重连后若仍算在场就立即重报。服务端从上报的时刻算 10 分钟,所以报在场之后,到最近一次活动满 10 分钟时还没有新活动就报不在场。打开页面本身不算活动。

兼容与写法 ​

  • 构建目标是近两年的 Chrome、Edge、Firefox(取 2024 年 10 月时的版本:Chrome 129、Edge 129、Firefox 131)与 Safari 16 起。页面只在源的根上提供,不支持挂在反向代理的子路径下。静态资源一律是同源的文件,不内联成 data: 地址:webui-server 给的文档级 CSP 里字体只许同源。
  • 写法由 test/architecture/webui-client-guards.test.ts 守住:颜色只来自主题令牌,组件样式不写 px(线宽 1px、2px 除外),内联 style 只设 CSS 变量,不用原生 alert、confirm、prompt 与 window.open,不用原生的选择框(选择一律用 Select),浏览器存储只经存储模块,不写死主机与端口,dangerouslySetInnerHTML 按文件白名单;回环地址只出现在认本机网址的模块里;源码不出现插件包名,字符串字面量不等于任何插件登记的页面键、动作名、数据源名、设置键或工具名(与会话协议、管理面事件同名的字段除外)。
  • 端到端测试在 test/e2e-webui/:构建前端,起 create-aalis minimal 档加 webui-server 的运行时(假模型、内存记忆,plugin-i18n 的配置里界面语言是中文,日志记到 info、写进数据目录的 logs/latest.log),在无头 Chromium 里按桌面(1280 宽)与手机(390 宽、触屏)两种宽度走打开、发消息与流式回复、代码块的标题行(语言名与复制按钮,不压住第一行代码,触屏上按钮点按范围不小于 44)、审批的批准与拒绝、切会话、断线重连、刷新与主题,并核对最少插件组合下计划、回滚与压缩的入口、图标栏里的页面与会话面板的「+」都不出现,「需要处理」为空、首页摘要不出现、标签页标题只有应用名、运行状况没有角标;待配置的插件与插件的提示出现在首页摘要(手机上在第一层顶部)与发出它的插件详情里,标签页标题与运行状况的角标(手机上是第一层入口的数)带上数目,运行状况页列出同样的几条,「去配置」进插件详情的配置区。上手与故障另在 onboard.test.ts:没有模型时首页的「开始使用」列出待配置的测试模型插件与假检索源里可装的提供者,首页开着时在另一个标签页的配置区填上 key,首页的它随之消失、发消息有回复,市场按服务搜得到提供者;「需要处理」里链(A 要 B 的服务、B 要 C 的、C 失败:A、B 两行写链、按钮去 C)、没有提供者、成环各有写法,只在等慢启动插件的不计数、它启动完转为运行;根已停用时在详情的依赖块里一键启用;记忆后端坏了时「会话服务未启用」写链、去根插件。运行状况在最少插件组合下核对「需要处理」写明没有、平台连接只有 WebUI 一行且在线,日志的「审批」预设筛出审批结算的那一行,筛选写进地址,刷新后筛选与那一行都在。声明式页面用一个探针插件登记,核对表格与详情按宽度变形(窄时长的字折行、表格不用横向滚动;宽时详情贴住、自己滚,滚到头不带动页面,键盘能滚,头部贴着详情的顶边、画在详情内容之上,详情里的焦点环往里画)、长文本与状态单元格、关系图的图例与缩放按钮、插件推来 page.invalidate 后页面更新、从标题栏固定与取消(手机上看第一层的「固定的页面」)、插件停用时的占位与重新启用后回到原页、会话面板的标签从「+」打开、关闭后进「+」并回到原位(宿主标签与插件标签一样,刷新在标签栏里)、两台设备同时固定;另装迁移过来的计划任务与工作流,走带输入的动作(新建、按行预填的编辑)、destructive 的确认与表格详情。设置在最少插件组合下核对目录(会话默认、权限、外观、系统)、会话默认先选层(全局、webui 平台档与默认工作区,宽屏左右并排、手机两屏)与全局层的行(正是各插件声明的可按会话设的项,假模型不声明思考档位,「深度思考」写「当前模型不能调」)、外观里只有内置主题与宿主标签,权限一节刷新后仍在;另在外观里导入一个主题、选中后核对整页与插件前端都换上它的令牌,刷新时第一帧就是它。控件的尺寸另有两条:桌面上按钮、图标按钮、输入框与选择框都是 28 像素,左栏头部、输入框区、审批卡与改名框里同一处同高,带开关的设置行不被开关撑高,有底色的按钮字重常规、文字按钮中等,插件前端的按钮与输入框和宿主的次要按钮、输入框一样;触屏上会话页、会话设置(另开着带成员的多选)、会话默认的一层(手机上的第二屏)、房间表、计划任务、插件前端与它的两种确认框(删除、打开外部链接)以及平板的首页里,每个看得见的可点元素点按范围不小于 44×44,补出的部分贴在元素上、不被别的元素压住,有底色的按钮看得见的比点按范围矮(插件前端的按钮悬停时也一样),插件自己整个重置掉(all: unset)的按钮不挡住框架里别处,多选里的一项只有字那一段能点(这一行右端的空白处点了不勾选),末尾不属于任何一项的那一行与复选框左对齐;触屏上设置行的「恢复继承」与插件配置的「撤销」和名字竖直居中对齐。经一个改掉升级请求 Host 的中转(像没转发原始 Host 的反向代理)核对连接横条的提示。后台工作用一个测试插件登记的可转后台命令工具(会写输出、响应中止)与子任务插件核对:前台等满让出时限转到后台后,工具卡保持活的,头部与左栏写「后台 N」(手机上「更多」带点),「后台」标签与运行状况的「正在运行」列出它,日志贴着底部跟随输出、往上翻看时不被拽走,正常结束后收到结束通知、她回复,标签里移进「已结束」,时间线里的结束通知是一行状态、点开是这个任务的日志,刷新后一样;带 runInBackground 的立即转到后台,在「后台」标签与运行状况各停一个,停后标「已停止」,没有结束通知;子会话在「后台」标签里带状态、耗时与 token,停掉父会话在等的那个后等待随即返回,「全部停止」停掉余下的,子会话页的路径逐层退回,父会话用量的累计含子会话;归档有后台任务在跑的会话,任务不停,运行状况照样列出、能停;关掉宿主的「后台」标签(两组一起)后由插件前端的会话面板替代;最少插件组合下模型拿到的工具里没有后台任务工具,没有「后台」标签,头部、左栏与运行状况都没有后台工作,会话行没有子任务展开。指令的回复是一张卡,/help clear 的标题、用法各是一段,取值是选项下面的子列表。standard 档另在 standard.test.ts:照脚手架 standard 档算出的插件集与它写的配置文档组装(假模型、内存记忆,OpenAI Embedding 不填 apiKey),两种宽度走首页不出「开始使用」、发消息与流式回复、模型经工具搜索找到工具再调用、写文件的审批与回滚、计划、压缩线、转后台的命令在「后台」里停掉、会话设置与会话默认(webui 平台档没设工具分组,写「没设时」那一句)、插件区、运行状况与「需要处理」(向量记忆的链写到 OpenAI Embedding,CLI 没有终端时写「没有连接」),各插件页与设置页打开都有内容、没有取数失败;「后台」开着、命令在跑时页面停 5 秒,不发请求、会话连接上不发帧(推送替代轮询);服务端除了 OpenAI Embedding 起不来的那一行没有告警与错误。英文界面另在 english.test.ts:plugin-i18n 的配置里界面语言是英文,在桌面宽度核对登录页(<html lang> 与说明)、图标栏、插件详情的配置标签、/help clear 回复卡片的标题与脚手架生成的插件前端(框架的 lang、按钮与列表空了的文字)都是英文。页面里的未捕获异常、CSP 拦下的加载与回 4xx、5xx 的请求都算失败。无障碍检查(test/e2e-webui/a11y.test.ts)经 DevTools 协议把 axe-core 注入页面,深色主题下按桌面与手机两种宽度走首页、会话页(审批卡等着)与「后台」标签、房间表、插件区与插件详情、设置各节、运行状况、一个声明式页面与一个插件前端页面的宿主外围(框架里是插件自己的页面,不查),浅色在桌面走首页与会话页,「严重」「关键」两级的问题一条也不许有。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布