系统管理员指南

这份指南写给部署和维护 AIshie 服务器的人:安装、配置、更新、备份和排查问题。所有步骤都用 AIShie-Deploy 完成。

目录

关于这份指南

本指南假设你会在 Linux 上使用 shell、能管理域名的 DNS,并能登录云服务商的控制台;内容针对的是新安装的服务器。

如何阅读这份指南

  • 每条命令都在服务器上以 root 身份运行(sudo -i)。
  • aishie.example.edu 代表你的服务器名称。例子中的学校“示範大學”、它的 AD FS(adfs.example.edu)、学习管理系统(lms.example.edu)和存储桶(aishie-files)都是虚构的。
  • 代码块中的英文信息是命令本身的输出。标为“示例”的代码块是根据命令的输出整理的,实际内容(版本、摘要、时间)会有所不同。

网站界面中标示“由服务器运维者设置”的项目,都在这里设置。网站给管理员看的说明不写设置的名称:把鼠标移到旁边的 ⓘ,会显示“供服务器运维方参考:…”和那一项设置,管理员可以把它转告你。

在网站“管理”页面工作的平台管理员和部门管理员,请看日常管理员指南:登录方式、AI 与文件、人员与智能体、导出对话等,都在那里说明。学生、教师和智能体拥有者,请看用户指南。

依据的版本

三份指南(用户指南、日常管理员指南和本指南)依据 2026 年 10 月 10 日的 AIshie 编写:最新的正式版本 0.2.0,加上之后已合并到各仓库 main、但尚未发布正式版本的功能(下面第二个表)。新增和重新拍摄的截图取自 main 上的这些版本;其余的截图取自 0.2.0,画面没有改变。

最新的正式版本 0.2.0,2026 年 10 月 3 日为三个镜像同时发布:

组件正式版本commit
Core(AIShie-Core)0.2.081ad1fe
智能体运行环境(AIShie-Agent-Runtime)0.2.01514bdf
前端(AIShie-Frontend)0.2.097ef815

0.2.0 加入了要求修改、回答依据的教材、全班成绩册、手机上的 PDF 预览,以及用“返回”关闭盖在页面上的窗口,也改了网站的外观和部分用词:登录页先列出单点登录、课程的栏目只排一行、账号菜单多了“关于 AIshie”、对话窗口说明谁会阅读对话。它带来 Core 的迁移 0028 和 0029,以及运行环境的迁移 0015,三者都不需要你做任何事;更新的顺序是 Core、运行环境、前端(见升级到 0.2.0)。

0.2.0 之后合并到 main 的版本,跟随 edge 的服务器已在运行;跟随 stable 的服务器,要等下一个正式版本:

组件正式版本main 的 commit(edge)
Core(AIShie-Core)尚未发布311a597
智能体运行环境(AIShie-Agent-Runtime)尚未发布f090bec
前端(AIShie-Frontend)尚未发布69d216b
AIShie-Deploy—fe78636

它们加入了小组作业与组员互评、永久删除作业、学校方案中 OpenRouter 模型的上游路由,以及回答用尽预算时的处理和提示的语言;AIShie-Deploy 则可以让服务器放在 Cloudflare 后面。数据库方面,有 Core 的迁移 0030、0031、0032 和运行环境的迁移 0016,都不需要你做任何事(见Core 的迁移 0030、0031、0032)。edge 照常按 Core、运行环境、前端的顺序更新。

之前的正式版本,是 2026 年 10 月 2 日为三个镜像同时发布的第一个正式版本 0.1.0;功能列表最初也依据它编写:

组件正式版本commit
Core(AIShie-Core)0.1.02ba8ac7
智能体运行环境(AIShie-Agent-Runtime)0.1.0a3308a8
前端(AIShie-Frontend)0.1.03d02414
AIShie-Deploy—d9d3821

AIShie-Deploy 没有正式版本:上表是它当天 main 的 commit,本指南中服务器上的步骤依据它编写。

跟随 edge 的服务器,运行的可能是比这些更新的版本。你的服务器正在运行的版本和 commit,见 Core 和智能体运行环境的 /healthz,以及前端的 /version.json(见健康检查);网站上账号菜单的“关于 AIshie”也会显示。

内容

  1. 架构概览:五个容器、路由、文件放在哪里
  2. 安装新服务器:准备工作、setup-server.sh、第一位管理员、放在 Cloudflare 后面
  3. 配置文件:aishie.env、core.env、runtime.env、postgres.env
  4. 上传的文件存放在哪里:磁盘或 S3 存储桶、CORS、迁移
  5. 智能体运行环境:它的凭证、管理员、上限、每个回答的预算、检查
  6. 模型供应商与密钥:学校 AI 方案
  7. 单点登录(服务器级):OIDC_*、SECRETS_KEY、私有地址
  8. 让其他网站嵌入 AIshie
  9. 更新:edge 与 stable:aishie-update、升级到 0.2.0
  10. 数据库迁移与回滚:包括 Core 的迁移 0027 至 0032,以及运行环境的迁移 0015 和 0016
  11. 备份与恢复
  12. 轮换密钥
  13. 日志与监控
  14. 故障排除
  15. 附录:命令与端口

架构概览

每台服务器运行一套 Docker Compose 堆栈,项目名称是 aishie。服务器自己从 GitHub 的镜像仓库(ghcr.io)拉取新版本,没有任何东西从外部推送到服务器。

五个容器

服务镜像监听
caddycaddy:2所有网络接口的 80/tcp、443/tcp 和 443/udp(HTTP/3):为服务器名称提供 HTTPS,自动获取证书
coreghcr.io/aishie-education/aishie-core8080,只发布在 127.0.0.1:8080
runtimeghcr.io/aishie-education/aishie-agent-runtime9090(/healthz、/metrics、/status),只发布在 127.0.0.1:9090;9091 是给前端的 API,只能经 Caddy 到达
webghcr.io/aishie-education/aishie-frontend8080,只发布在 127.0.0.1:8081
postgrespostgres:185432,只在堆栈的网络内;两个数据库 aishie_core 和 aishie_runtime

Caddy 按路径转发请求:

  • /v1/*、/mcp、/mcp/*、/healthz,以及 /.well-known/ 下 OAuth 和 OpenID 的发现路径:转发到 Core。这些响应不压缩,因为 MCP 是流式传输。
  • /runtime/api/*:转发到智能体运行环境的 API(9091),并先删除浏览器的 Cookie 请求头。这个 API 只接受 Core 为已登录用户签发的断言,不接受登录 Cookie。前端的 AI 与文件 页和站内托管的对话框都经这里使用运行环境。
  • 其他一切:转发到前端(web)。

运行环境的 9090(健康检查、指标和 /status)从不经 Caddy 对外开放。

智能体运行环境托管站内的智能体、把 Office 文件转换为 PDF、读取扫描文件,并在管理员开启时把文件转写成文字版(见智能体运行环境)。

网络与 HOST

  • 堆栈的网络有自己的子网 AISHIE_SUBNET(172.30.83.0/24),Caddy 固定在 AISHIE_CADDY_IP(172.30.83.10)。这是 Core 和运行环境的 API 唯一信任的代理服务器:它们只从 Caddy 送来的 X-Forwarded-For 获取用户的地址。Caddy 在那里只写一个地址:连接本身的地址,或放在 Cloudflare 后面时访客的地址(见放在 Cloudflare 后面)。
  • Caddy 在网络上另有一个别名,就是服务器的名称 HOST。运行环境调用 https://aishie.example.edu 时,直接连到 Caddy,得到与浏览器相同的证书,不用绕到服务器的公网地址再回来(有些服务商不会这样转发)。
  • 子网与服务器上的其他网络冲突时,在 /etc/aishie/aishie.env 同时修改这两项,再运行 aishie compose down && aishie compose up -d。镜像和数据都不变。
  • 更新由服务器自己拉取:aishie-update 每 5 分钟检查一次(见更新)。GitHub 的工作流从不连接服务器。

文件放在哪里

服务器上的路径内容
/opt/aishie/堆栈:compose.yaml、stack.yaml、caddy/Caddyfile、postgres/initdb/、逐项说明设置的 env/*.env.example、AIShie-Deploy 的 README.md 和 docs/
/etc/aishie/aishie.env运维者的设置:HOST、ENVIRONMENT、三个通道、网络、FRAME_ANCESTORS
/etc/aishie/core.env、runtime.env、postgres.env各服务的设置和机密,只有 root 可读(0600)
/etc/aishie/runtime/agents/运维者自己的智能体的 YAML,以及 runtime: 文档(学校 AI 方案);在运行环境中以只读方式挂载为 /config
/etc/aishie/runtime/secrets/运行环境的密钥 kek/v1、它在 Core 的凭证 core/agent_runtime、模型密钥(例如 school/keys/…);以只读方式挂载为 /secrets
/var/lib/aishie/images.env每个服务正在运行的镜像(以摘要记录),由 aishie-update 写入
/srv/aishie/core/Core 把文件存放在磁盘时,上传的文件在 blobs/ 下
/var/backups/aishie/数据库备份
/var/log/aishie-update.log每次更新的结果一行
/usr/local/bin/aishie-update、aishie、aishie-storage、aishie-front-proxy四个命令
/etc/systemd/system/aishie-update.timer、aishie-backup.timer、aishie-front-proxy.timer每 5 分钟检查更新;每晚备份;每周更新 Cloudflare 的地址列表(只在设置了 FRONT_PROXY 时起作用)
/opt/aishie/caddy/front-proxy/、/var/lib/aishie/cloudflare-ipsaishie front-proxy 为 Caddy 写的配置,以及最后获取的 Cloudflare 地址列表(见放在 Cloudflare 后面)

安装新服务器

一条命令完成大部分工作:setup-server.sh。之后只剩 DNS、第一位管理员和备份 /etc/aishie。

准备工作

  • 服务器:全新的 Ubuntu 24.04 或更新版本,起步配置为 2 个 CPU、4 GB 内存和 40 GB 磁盘。服务器会保存成绩和学生的作业,请选择学校允许的服务商和区域。
  • 防火墙:服务商的防火墙要开放 80/tcp、443/tcp 和 443/udp。服务器上的 ufw 开启时,setup-server.sh 会自行放行;Docker 发布的端口本来就不受 ufw 限制。
  • DNS 名称:一个指向服务器的名称(A 记录;服务器有 IPv6 的话再加 AAAA)。名称一解析到服务器,Caddy 就会获取证书。
  • Docker:不必预先安装。setup-server.sh 会安装 Docker Engine 和 compose 插件(需要 compose 2.24 或以上):在 Ubuntu 24.04 上用 Ubuntu 的 docker.io 和 docker-compose-v2;Ubuntu 的软件包太旧、或已经装了 Docker 的 docker-ce 时,改用 Docker 的 apt 软件源。它会说明用了哪一种。
  • 账号:不需要。AIShie-Deploy 仓库和三个镜像都是公开的,服务器不用任何密钥、令牌或登录就能读取和拉取。

先决定四件事

  1. edge 还是 stable(见两个通道):edge 跟随每个仓库 main 通过检查后的最新版本(:edge),会自动更新;stable 只跟随你亲手设置的正式版本(X.Y.Z)。选 stable 之前,先确认三个镜像都已发布你要用的正式版本。第一个正式版本 0.1.0 已于 2026 年 10 月 2 日为三个镜像同时发布,三者搭配同一个 Core:选 stable 时,下面的 X.Y.Z 填 0.1.0 或之后的正式版本。
  2. 上传的文件放在哪里(见上传的文件存放在哪里):这台服务器的磁盘,或 S3 类的存储桶。设置好之后,只能用 aishie storage migrate 迁移。
  3. 单点登录(见单点登录):不用;在 core.env 设置运维者的提供者;以及/或者让平台管理员在网站上添加提供者。
  4. 模型的密钥(见模型供应商与密钥):只用智能体拥有者自己的密钥,还是另外提供学校 AI 方案。

运行 setup-server.sh

  1. 以 root 身份在 root 的主目录获取 AIShie-Deploy:
    git clone https://github.com/AIShie-Education/AIShie-Deploy.git
  2. 运行设置脚本,加上服务器的 DNS 名称和环境(edge 或 stable):
    sh AIShie-Deploy/setup-server.sh aishie.example.edu edge
    有人在终端前时,它会先问上传的文件放在哪里:直接按 Enter 就是这台服务器的磁盘;也可以选 Amazon S3、Cloudflare R2、Backblaze B2 或其他 S3 兼容的服务,再输入存储桶和密钥。无人应答时,用选项指定(见设置存储桶)。
  3. 等它完成。最后它会列出还要做的事(见下面的示例)。

它依次做这些事,输出中每一步以 == 开头的标题标示:

输出中的标题做什么
Packages安装缺少的软件包,以及 Docker
The bucket(选了存储桶时)用密钥以只读方式检查存储桶;被拒绝就停止,什么都不写入
Settings and secrets in /etc/aishie写入四个 env 文件;生成数据库密码、SIGNING_KEY、SECRETS_KEY 和运行环境的密钥 kek/v1;创建目录
The bucket's CORS rule(选了存储桶时)设置或打印存储桶的 CORS 规则
The stack, the scripts and the timers安装 /opt/aishie、三个命令和两个 systemd 定时器,并启用定时器
Firewallufw 开启时放行 80/tcp、443/tcp、443/udp
PostgreSQL and Caddy启动数据库和 Caddy
The images检查能否拉取三个镜像
The first update (aishie-update)第一次更新:依次部署 Core、运行环境和前端;Core 在这一步建立数据库结构
The runtime's credential for CoreCore 完成迁移后,为运行环境签发它在 Core 的凭证,直接写进文件,不在任何地方显示,然后重建运行环境
Done. What is left列出还要做的事

生成的机密:两个数据库角色和 PostgreSQL 超级用户的密码(各 24 个随机字节,hex)、SIGNING_KEY(32 字节,hex)、SECRETS_KEY(32 字节,base64)和 kek/v1(32 字节,base64)。它们只写进 /etc/aishie 下的文件,不会打印出来。

示例:一台用磁盘存放文件的 edge 新服务器,setup-server.sh 最后打印的内容:

== Done. What is left
1. Point aishie.example.edu at this server in DNS (A, and AAAA if it has IPv6). Caddy gets
   its certificate once the name resolves here:
     curl https://aishie.example.edu/healthz
2. The first administrator, once Core runs (aishie-update --status): it asks
   for a name, an email and a password, then restarts Core, and the
   administrator signs in at https://aishie.example.edu with that email and password:
     aishie admin
3. Agents (README.md, Hosting agents): each is hosted one way for good, chosen
   when it is made. A runtime agent's owner has this server's runtime host it
   from the site, and the runtime is issued its token by its id: nobody pastes
   one. An operator's own runtime agent is a YAML in /etc/aishie/runtime/agents,
   readable by group 65532, that names its agent_id and holds no token:
     install -g 65532 -m 640 tutor.yaml /etc/aishie/runtime/agents/
     aishie runtime check --live && aishie compose kill -s HUP runtime
4. Keep a copy of /etc/aishie somewhere else, encrypted, and apart from the
   database's backups: it holds SIGNING_KEY, SECRETS_KEY and the runtime's key
   (kek/v1), which no backup of the database can bring back, and the
   runtime's credential for Core (README.md, What to keep off the server).

Updates come by themselves from now on: aishie-update --status says what runs
and how the last check went; journalctl -u aishie-update has every run.
中途停止了?

修正它指出的问题,再运行同一条命令。setup-server.sh 从不覆盖已有的设置、机密或数据。

选了 stable

stable 的三个通道一开始是空的,所以第一次更新不会执行。setup-server.sh 最后会列出这一步:在 /etc/aishie/aishie.env 中把 CORE_IMAGE、RUNTIME_IMAGE 和 WEB_IMAGE 设为正式版本,运行 aishie-update,然后运行 aishie runtime-credential 为运行环境签发凭证。

CORE_IMAGE=ghcr.io/aishie-education/aishie-core:X.Y.Z
RUNTIME_IMAGE=ghcr.io/aishie-education/aishie-agent-runtime:X.Y.Z
WEB_IMAGE=ghcr.io/aishie-education/aishie-frontend:X.Y.Z

把 X.Y.Z 换成你要用的版本号。

DNS 与第一位管理员

  1. 把服务器名称指向服务器(A 记录,有 IPv6 的话再加 AAAA)。名称一解析到服务器,Caddy 就获取证书。确认 Core 有响应:
    curl https://aishie.example.edu/healthz
    它返回一段 JSON,包括 status、version 和 commit。
  2. 创建第一位管理员,也就是你用来登录网站的账号:
    aishie admin
    它会询问姓名、邮箱和密码(输入两次,不会显示;至少 10 个字符),创建 Root 账号和系统账号,再重启 Core,让后台任务以系统账号开始运行。
  3. 在 https://aishie.example.edu 用这个邮箱和密码登录。

示例(节选):

Name: 陈大文
Email (to sign in with): admin@example.edu
Password (10 characters or more):
The password again:
aishie: restarting Core, so that its background jobs start
aishie: done. Sign in at https://aishie.example.edu with admin@example.edu and that password.
  • 人没有 API 令牌,管理员也一样:只有智能体才有。
  • 用脚本创建时:把密码经标准输入交给 aishie core bootstrap --name … --email … --password-stdin >/dev/null,再运行 aishie compose restart core。密码不会出现在命令行上。
  • 之后的设置(学期、部门、课程、登录方式)由平台管理员在网站上完成:请他看日常管理员指南的“首次设置”。

设置完成之后

  1. 把 /etc/aishie 加密后复制到服务器之外,与数据库备份分开存放(见服务器之外要保存什么)。
  2. 第二天确认夜间备份已执行:
    ls -l /var/backups/aishie/*-daily-*

更新 AIShie-Deploy 本身的文件

镜像会自己更新,但堆栈的文件(compose.yaml、aishie-update、aishie、systemd 单元)不会。要安装较新的版本:

git -C AIShie-Deploy pull
sh AIShie-Deploy/setup-server.sh aishie.example.edu edge

它安装新文件,设置、机密和数据保持不变。只有两种情况它会补上东西:core.env 没有 SECRETS_KEY 时加上一个;运行环境的凭证文件不见了或是空的,就签发一个新的。它补上东西时会说明,届时请再复制一次 /etc/aishie。

更改服务器名称

  1. 在 /etc/aishie/aishie.env 中修改 HOST,然后运行 aishie compose up -d:Core、运行环境和 Caddy 会以新名称重建,Caddy 为它获取证书。
  2. 用存储桶的话,它的 CORS 规则写的是旧网址,要改成新网址(见CORS 规则)。
  3. 每个单点登录提供者登记的重定向 URI 也是旧网址,要在提供者那边改成 https://新名称/v1/auth/sso/callback。

放在 Cloudflare 后面(让中国内地的访客连得上)

服务器自己的 IP 地址可能在中国内地被封锁:那里的人不论用什么名称都连不上网站。例如一台位于新加坡的服务器,从中国电信、中国联通和中国移动完全连不上,香港、新加坡、日本和美国则正常。把服务器的名称交给 Cloudflare 代理(控制台中橙色的云),访客连到的是 Cloudflare 的地址,再由 Cloudflare 连到服务器。

先知道两件事
  • Cloudflare 免费方案在中国内地有时会慢。那里的访客连到的是中国内地以外的 Cloudflare 数据中心,经过大家共用的国际线路,繁忙时段较慢,也可能中断一段时间。这是绕过被封锁地址的方法,不是加速的方法。Cloudflare 在中国内地的网络属于企业方案,要与当地伙伴合作,并需要 ICP 备案。
  • 在中国内地架设服务器需要 ICP 备案,要经当地的服务商办理。在此之前,放在中国内地以外、经 Cloudflare 代理的服务器是可行的做法。

在服务器上

  1. 先更新 AIShie-Deploy 本身的文件(见设置完成之后):git -C AIShie-Deploy pull,再运行 setup-server.sh。没有设置 FRONT_PROXY 时,这不会改变访客看到的任何东西。
  2. 在 /etc/aishie/aishie.env 中加上:
    FRONT_PROXY=cloudflare
  3. 运行(再运行一次 setup-server.sh 也一样):
    aishie front-proxy

之后:

  • Caddy 只相信来自 Cloudflare 地址的连接所说的访客地址:访客是 Cloudflare 对每个请求都会设置的 CF-Connecting-IP。来自其他地址的连接,不论带着什么请求头,都以连接本身的地址作为访客地址。
  • Core 和运行环境得到的是访客的地址,不是 Cloudflare 的:Core 按它计算登录尝试次数,运行环境的 API 按它记录和限流。
  • Cloudflare 的地址列表:每次运行 aishie front-proxy 时,以及 aishie-front-proxy.timer 每周一次,从 https://www.cloudflare.com/ips-v4 和 /ips-v6 获取。列表只在完整时才采用;获取不到或内容不对时,沿用上次获取的列表(/var/lib/aishie/cloudflare-ips),从未获取过的服务器用 AIShie-Deploy 附带的列表,并说明用了哪一份。列表或配置有变化时才重新加载 Caddy;Caddy 拒绝新配置时,文件会还原,Caddy 照旧运行。
  • aishie front-proxy 写的是 /opt/aishie/caddy/front-proxy/ 中的两个文件:要改设置,请改 aishie.env,不要改这两个文件。最后它会像 Let's Encrypt 一样请求 http://HOST/.well-known/acme-challenge/,并说明是谁响应的(见下面的“证书”)。

记录经过 Cloudflare 代理之后,可以选择再加上:

FRONT_PROXY_ONLY=yes

再运行 aishie front-proxy:不是来自 Cloudflare、也不是来自私有地址(堆栈自己的网络,以及 Docker 的代理)的请求,一律返回 403,没有人能绕过 Cloudflare 直接访问网站。记录还没有经过代理就设置的话,所有直接连到服务器的人都会被拒绝,aishie front-proxy 会提醒你。它不会隐藏服务器:服务器的地址仍会响应(以拒绝响应)。

在 Cloudflare 的控制台

由有权更改这个域名设置的人:

  1. SSL/TLS › Overview:Full (strict)。不要用 Flexible:Caddy 会把 HTTP 重定向到 HTTPS,Flexible 会造成无限重定向。这是整个域名的设置:域名中其他经代理、有自己服务器的名称,也必须提供有效的证书。
  2. DNS › Records:服务器的名称,一条指向服务器 IPv4 地址的 A 记录,设为 Proxied(经代理),而且不要有指向服务器的 AAAA 记录。访客仍然可以经 IPv6 连到 Cloudflare。
  3. 让 /.well-known/acme-challenge/ 的普通 HTTP 请求到达服务器,服务器才能续期证书:SSL/TLS › Edge Certificates › Always Use HTTPS:Off。Caddy 自己会把 HTTP 重定向到 HTTPS,所以访客最终仍然用 HTTPS。域名中其他名称需要它时,仍然把它关掉,改在 Rules › Redirect Rules 中加一条作用相同、但不包括这个路径的规则(AIShie-Deploy 的 README“Behind Cloudflare”中有完整的条件)。
  4. 保持不变,或检查一下:不要用缓存所有内容的规则(真的有的话,要排除 /v1/*、/mcp*、/runtime/api/* 和 /healthz);Under Attack 模式、Bot Fight Mode 和任何会出验证题的规则都不能涵盖这个名称,因为智能体的 MCP 客户端、脚本和 Let's Encrypt 都无法作答;Pseudo IPv4 和 Rocket Loader 保持关闭(默认)。

然后在服务器上运行 aishie front-proxy。

证书

Caddy 继续自己向 Let's Encrypt 申请证书,经 HTTP 验证:Let's Encrypt 请求 http://HOST/.well-known/acme-challenge/…,Full (strict) 下 Cloudflare 把普通 HTTP 请求原样交给服务器的 80 端口,由 Caddy 响应。Caddy 的日志中可能先有一次 tls-alpn-01 的验证失败,再有 http-01 成功,这是正常的。

Always Use HTTPS 开着时,证书仍然有效的服务器可以续期,但已过期的证书,或新服务器的第一张证书,就无法获取:Full (strict) 会先拒绝没有有效证书的服务器(Cloudflare 的 526)。所以要做上面的第 3 步,否则一次续期失败就会让网站停止服务。aishie front-proxy 会说明:goes through Cloudflare (cf-ray …) to Caddy, which answers it (308) 是正确的;否则会警告 Cloudflare 做了什么(301 就是 Always Use HTTPS)。它按公共 DNS 给这个名称的地址发出请求,就像 Let's Encrypt 一样。查看服务器现在的证书和到期日:

echo | openssl s_client -connect 127.0.0.1:443 -servername aishie.example.edu 2>/dev/null | openssl x509 -noout -issuer -enddate

不要用 Cloudflare 的 Origin CA 证书:运行环境在服务器内部经 Caddy 以 https://HOST 连到 Core,会像浏览器一样检查证书,而 Origin CA 证书只有 Cloudflare 信任。

检查

curl -sI https://aishie.example.edu/healthz | grep -i -e '^HTTP' -e '^server' -e '^cf-ray'

返回 200、server: cloudflare 和一行 cf-ray,表示请求经过了 Cloudflare。它只证明这一点:中国内地的访客能否连上,要从当地的网络测试。aishie front-proxy 会说明 Cloudflare 的地址列表从哪里来、Caddy 是否已采用,以及证书的验证路径由谁响应。

Cloudflare 免费方案的限制

  • 每个请求的内容最多 100 MB,更大的由 Cloudflare 拒绝(413)。AIshie 每个文件一个请求,默认最多 50 MiB(MAX_UPLOAD_BYTES),所以不会碰到;只有把 MAX_UPLOAD_BYTES 调高到超过约 95 MiB 时,介于两者之间的文件才会在浏览器中被 Cloudflare 拒绝。要上传更大的文件,请把它保持在这个数值以下,或改用存储桶(浏览器直接上传到存储桶,不经 Cloudflare)。
  • 等待响应约两分钟:服务器要在约 125 秒内开始响应,否则 Cloudflare 返回 524。AIshie 的请求都在这之内响应;唯一的例外是大型的对话导出,Core 最多等 5 分钟才响应。超过两分钟的导出会得到 524,请缩小导出的范围(一门课程、一个部门、一位参与者或一段时间)。

堆栈的默认设置没有超过这两项限制,所以不用为 Cloudflare 改任何设置。

改回直接连接

  1. 设置了 FRONT_PROXY_ONLY 的话,先从 aishie.env 删去它,运行 aishie front-proxy。
  2. 在 Cloudflare 的控制台把记录改为 DNS only(灰色的云)。先做这一步的话,直接连到服务器的人会被拒绝,直到你运行 aishie front-proxy。
  3. FRONT_PROXY 至少再保留五分钟:解析器可能仍缓存着 Cloudflare 的地址(经代理的记录 TTL 是 300 秒),那些访客仍会经过 Cloudflare。之后可以删去它再运行 aishie front-proxy,也可以保留:Caddy 只相信来自 Cloudflare 地址的连接,直接连来的访客不受影响。

配置文件

所有设置都在 /etc/aishie 下的四个 env 文件中。/opt/aishie/env/*.env.example 逐项说明每一项设置。

规则

  • 一行一个 NAME=value,不要 export,值后面不要加注释。
  • 文件由 Docker Compose 读取。值按原样使用(包括空格),但 $ 会被当作变量的开头。含有 $ 的值(例如别人给你的密钥)要用单引号:'like$this'。aishie.env 的 FRAME_ANCESTORS 是唯一用双引号的值(见让其他网站嵌入)。
  • 改完后重建该服务:aishie compose up -d core(或 runtime、web)。restart 不会重新读取文件。
  • stack.yaml 按 aishie.env 设置以下各项,文件里写什么都会被它取代:
    • Core:HTTP_ADDR=:8080、PUBLIC_URL=https://HOST、TRUSTED_PROXIES(Caddy 的地址)、RUNTIME_AUDIENCES=https://HOST/runtime。
    • 运行环境:HTTP_ADDR=:9090、CONFIG=/config、SECRETS_DIR=/secrets、CORE_BASE_URL_ALLOWLIST、API_ADDR=:9091、API_AUDIENCE、CORE_BASE_URL、API_TRUSTED_PROXIES。
  • aishie core help 和 aishie runtime help 列出正在运行的镜像读取的每一项设置及其默认值。

aishie.env

setup-server.sh 按它的参数写一次,之后由你手动修改。里面没有机密。

设置项说明
HOST服务器的 DNS 名称。修改后运行 aishie compose up -d(见更改服务器名称)
ENVIRONMENTedge 或 stable
CORE_IMAGE、RUNTIME_IMAGE、WEB_IMAGE每个镜像跟随的通道。edge 是 :edge;stable 是正式版本 X.Y.Z 或摘要(见两个通道)
AISHIE_SUBNET、AISHIE_CADDY_IP堆栈的子网和 Caddy 的地址(172.30.83.0/24、172.30.83.10)
RUNTIME_STOP_GRACE停止运行环境时,Docker 等它多久才强制结束(30s):运行环境的 SHUTDOWN_GRACE 再加 15 秒
FRAME_ANCESTORS可选:哪些网站可以在框架中显示 AIshie(见让其他网站嵌入)
FRONT_PROXY可选:cloudflare 表示服务器放在 Cloudflare 的代理后面,Caddy 从 Cloudflare 获取访客的地址。修改后运行 aishie front-proxy(见放在 Cloudflare 后面)
FRONT_PROXY_ONLY可选:yes 拒绝(403)不是经 Cloudflare、也不是来自私有地址的请求。只在 DNS 记录已经经过 Cloudflare 代理之后才设置

core.env

setup-server.sh 写入 DATABASE_URL、SIGNING_KEY、SECRETS_KEY 和文件存放的设置(BLOB_STORE、BLOB_FS_ROOT,用存储桶时还有 S3_*)。其余各项都是可选的,不设置就用 Core 的默认值。

密钥

设置项默认值说明
SIGNING_KEY由设置脚本生成为上传和下载链接以及单点登录的状态签名。至少 32 个字符。不要更改(见其他密钥)
SECRETS_KEY由设置脚本生成加密平台管理员在网站上添加的单点登录提供者的客户端密钥。不能丢失,只能以轮换方式更改(见轮换 SECRETS_KEY)
SECRETS_KEY_PREVIOUS无只在轮换期间使用:之前的密钥,以逗号分隔
ASSERTION_KEY由 SIGNING_KEY 派生Core 为运行环境签发断言用的密钥(32 字节,base64)。如果设置了,要像 SIGNING_KEY 一样妥善保存
ASSERTION_TTL5m断言的有效时间,1 至 15 分钟

文件

设置项默认值说明
BLOB_STORE、BLOB_FS_ROOT、S3_*由设置脚本写入文件存放在哪里(见上传的文件存放在哪里)
MAX_UPLOAD_BYTES52428800(50 MiB)单个文件的上限
DOCUMENT_MAX_FILES_PER_VERSION20教材的一个版本最多几个文件,最多 100
DOCUMENT_MAX_VERSION_BYTES209715200(200 MiB)一个版本所有文件的总大小
ATTACHMENT_MAX_BYTES52428800(50 MiB)对话中一条消息附加的单个文件上限,不会超过 MAX_UPLOAD_BYTES
ATTACHMENT_MAX_PER_MESSAGE10一条消息最多附加几个文件,最多 100
ATTACHMENT_MAX_CONVERSATION_BYTES524288000(500 MiB)一段对话所有附件的总大小
RENDITION_MAX_BYTES104857600(100 MiB)Office 文件转换成的 PDF 最大可以多大;更大的不收,文件只能下载

导出对话

设置项默认值说明
EXPORT_MAX_MESSAGES100000一次导出最多几条消息
EXPORT_MAX_BYTES268435456(256 MiB)一次导出的文字总量
EXPORT_TTL24h导出的文件保留多久(15 分钟至 168 小时),之后由清理任务删除。文件含个人数据,时间宜短

登录与加入课程

设置项默认值说明
SESSION_TTL12h登录会话的时长
SIGN_IN_ATTEMPTS_PER_MINUTE10每个地址(IPv6 以 /64 计)及每个邮箱或学号每分钟的登录尝试次数;经邀请链接注册也计算在内
JOIN_LINK_REGISTRATIONon没有账号的人能否经课程的邀请链接自行注册。单点登录覆盖所有人后可设为 off:大家先登录,再加入课程
JOIN_REGISTRATIONS_PER_MINUTE60每条邀请链接每分钟的注册数;0 为不限
COOKIE_SAMESITElax其他网站在框架中显示 AIshie 时设为 none(见让其他网站嵌入)
TRUSTED_ORIGINS无前端在另一个源时才需要;这套堆栈的前端与 Core 同在 HOST,不用设置
OIDC_*、SSO_ALLOW_PRIVATE_ISSUERS无单点登录(见单点登录)

负载与后台任务

设置项默认值说明
RATE_LIMIT_PER_MINUTE600每个账号(人或智能体)每分钟的调用次数;0 为不限。智能体搜索大量教材时可能要调高(见课程教材搜索)
RATE_LIMIT_BURST100短时间内可以超出的数量
LONG_POLL_WAITERS1000同时等待新消息的调用数;0 为不让任何调用等待
LONG_POLL_WAITERS_PER_ACTOR16其中一个账号最多占几个
PROPOSAL_TTL336h(14 天)提议多久没有决定就过期;0 为永不过期
JOBS、JOBS_INTERVALtrue、1m后台清理任务及其间隔
SHUTDOWN_GRACE15s停止时留给进行中请求的时间;要少于堆栈给 Core 的 20 秒

智能体

设置项默认值说明
AGENT_SELF_SERVICEonoff:只有管理员可以注册智能体
AGENT_MAX_PER_OWNER5一个人最多可以拥有几个未停用的智能体

不要设置 INSECURE_COOKIES:它只用于经 http://localhost 访问的开发环境。

runtime.env

setup-server.sh 写入 DATABASE_URL(运行环境自己的数据库,不是 Core 的)、LOG_FORMAT=json 和 KMS_KEY_ID=local:/secrets/kek/v1。以下各项都是可选的。

设置项默认值说明
CORE_SERVICE_CREDENTIALsecret://core/agent_runtime运行环境在 Core 的凭证放在哪里。保持不设置(见运行环境在 Core 的凭证)
ADMIN_ACTOR_IDS无谁可以管理运行环境(见谁可以管理运行环境)
EGRESS_PROXY无所有对外连接经过的代理服务器(http://、https://、socks5://);它本身要拒绝私有地址(见对外连接)
LOG_LEVELinfodebug、info、warn 或 error
LOG_REDACT_EXTRA无以逗号分隔的正则表达式,从每行日志中删去匹配的内容
SHUTDOWN_GRACE15s停止时让进行中的回答完成的时间;修改时要同时修改 aishie.env 的 RUNTIME_STOP_GRACE(多 15 秒)
WORKER_ID主机名加进程号这个工作进程在租约中的名称
PRICES无价目表的路径,例如 /config/prices/prices.yaml(见学校 AI 方案)

以下各项控制扫描文件、Office 文件和文件转写。/opt/aishie/env/runtime.env.example 没有列出它们,aishie runtime help 有完整说明。它们也是网站管理员设置的上限(见运维者的上限)。

设置项默认值说明
OCRauto读取扫描 PDF 和图片中的文字。auto:镜像中有程序就开启;on:没有程序就不启动;off:关闭,网站无法开启
OCR_LANGUAGESchi_sim+chi_tra+engtesseract 的语言。镜像只有简体中文、繁体中文和英文
OCR_MAX_PAGES、OCR_DPI40、300每个 PDF 读几页;页面的分辨率(72 至 600)
OCR_PAGE_TIMEOUT、OCR_TIMEOUT、OCR_MEMORY_MB90s、15m、1024每页和每个文件的时限;每个程序的内存
OCR_CONCURRENCY、OCR_QUEUE、OCR_WAIT1、8、5s同时读几个文件(1 至 8)、可排队几个、提问时等待文字多久
OFFICE_PDFauto用 LibreOffice 把演示文稿和文档转换为 PDF。off 同时关闭 Office 文件的 PDF 预览
OFFICE_PDF_TIMEOUT、OFFICE_PDF_MAX_PAGES、OFFICE_PDF_MEMORY_MB2m、300、2048每个文件的时限、页数上限、LibreOffice 的内存
PDF_PART_PAGES10较长的 PDF 以几页为一份交给模型
RENDITIONSauto把 Core 保存的每个 Office 文件转换为网站预览用的 PDF。auto:LibreOffice 可用就进行
RENDITIONS_CONCURRENCY、RENDITIONS_TIMEOUT、RENDITIONS_LEASE1、5m、10m同时转换几个(1 至 8)、每个的时限、Core 为它保留文件多久
TRANSCRIBEauto文件转写(文字版)。auto:管理员在网站上开启时执行;off:永不执行

postgres.env

POSTGRES_PASSWORD、AISHIE_CORE_DB_PASSWORD 和 AISHIE_RUNTIME_DB_PASSWORD。PostgreSQL 只在第一次创建数据卷时读取它们;之后修改这个文件,数据库里的密码不会改变(见更换数据库密码)。

上传的文件存放在哪里

教材、作业、对话中的文件和 Office 文件的 PDF,存放在这台服务器的磁盘,或一个 S3 类的存储桶中。在设置服务器时就选好。

磁盘还是存储桶

  • 这台服务器的磁盘(BLOB_STORE=fs,默认):在 /srv/aishie/core/blobs。每次上传和下载都经过服务器,占用它的带宽;磁盘要随文件增多而扩容。
  • 存储桶(BLOB_STORE=s3):Core 给浏览器一条由它签名、几分钟内有效的链接,浏览器直接上传到存储桶并从那里下载。文件不经过服务器,服务器的磁盘也不存放它们。
--storage服务setup-server.sh 会问Core 的 S3_ENDPOINT
fs这台服务器的磁盘不问无
awsAmazon S3区域(任何分区的任何区域)、存储桶、访问密钥 ID 和秘密访问密钥s3.<区域>.amazonaws.com(中国区为 .com.cn,其他分区用各自的域名)
r2Cloudflare R2账户 ID、存储桶、R2 API 令牌的访问密钥 ID 和秘密访问密钥<账户>.r2.cloudflarestorage.com,区域 auto
b2Backblaze B2区域(取自存储桶的端点 s3.<区域>.backblazeb2.com)、存储桶、keyID 和 applicationKeys3.<区域>.backblazeb2.com
s3其他 S3 兼容服务端点(HOST[:PORT],HTTPS)、区域(默认 us-east-1)、存储桶、路径式寻址、密钥你提供的端点

选择时要考虑的

  • 靠近用户:浏览器直接连接存储桶所在的区域。香港的学校可选 AWS 的 ap-east-1(香港;要先在 AWS 账户中启用这个区域)或 ap-southeast-1(新加坡),或以“Asia-Pacific”位置提示创建的 R2 存储桶。B2 在开户时决定区域,请确认靠近用户。Core 只在附加文件(复制和删除)和清理时连接存储桶,所以存储桶离服务器远,只会让附加文件稍慢。
  • 费用:磁盘除了服务器本身不另收费,但每个字节都占用服务器的带宽,扩容磁盘和备份它也要付费。Amazon S3 按存储量、每个请求和传出到互联网的数据(也就是每次下载)收费。Cloudflare R2 按存储量和每项操作收费,传出数据免费。Backblaze B2 按存储量收费,下载量在存储量的一定倍数内免费。具体数字请看各服务商该区域的价格页面。
  • 端点必须是 HTTPS:网站的页面是 https://,浏览器不会上传到 http://。
  • 名称含点号的存储桶(例如 files.example.edu)只能用路径式寻址,因为没有证书能覆盖 https://名.称.端点。setup-server.sh 拒绝它与 --s3-path-style no 一起使用,也拒绝在 Core 的 S3 客户端不认识的较新 AWS 区域使用它。名称不含点号的存储桶在任何区域都可以。

S3_BUCKET_LOOKUP 和 S3_REGION

  • S3_BUCKET_LOOKUP 决定请求如何指定存储桶:path 放在端点之后(https://端点/存储桶/键);dns 放在主机名中(https://存储桶.端点/键),用于只接受这种方式的服务;auto(默认)在 AWS、Google 和阿里云上用 dns,其他用 path。
  • 选 --storage s3 时,--s3-path-style yes 写成 path,no 写成 dns,不指定则为 auto。AWS、R2 和 B2 一律是 auto。
  • S3_REGION(默认 us-east-1;R2 是 auto)是请求签名所用的区域;在 AWS 上也是请求发往的区域,包括 Core 的 S3 客户端不认识的较新区域。
  • 较旧的 Core 镜像不读取 S3_BUCKET_LOOKUP。setup-server.sh 和 aishie storage migrate 在需要时会先确认服务器上的 Core 认识它(aishie core help 列出这一项)。

设置存储桶

  1. 先在服务商处创建一个私有存储桶(Core 签名的链接不需要公开访问)。
  2. 为它创建只能做 Core 所做之事的密钥:
    • AWS:IAM 用户或角色,允许存储桶上的 s3:ListBucket,以及存储桶内对象的 s3:GetObject、s3:PutObject 和 s3:DeleteObject。再加上存储桶的 s3:GetBucketCORS 和 s3:PutBucketCORS,setup-server.sh 就能自行设置 CORS 规则。aws 以外的分区用该分区的 ARN,例如中国区的 arn:aws-cn:s3:::。
    • R2:只限这个存储桶、具有 Object Read & Write 权限的 R2 API 令牌。账户 ID 在存储桶的 S3 API 地址中:https://<账户>.r2.cloudflarestorage.com/<存储桶>。
    • B2:只限这个存储桶、可读写的应用程序密钥。存储桶的页面显示它的端点 s3.<区域>.backblazeb2.com。
  3. 运行 setup-server.sh,在提示时选择存储桶;或者用选项,密钥放在环境变量中(见下文)。

示例:存储桶 aishie-files 的 IAM 策略。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketCORS", "s3:PutBucketCORS"],
      "Resource": "arn:aws:s3:::aishie-files"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::aishie-files/*"
    }
  ]
}

无人值守时

密钥只放在环境变量 AISHIE_S3_ACCESS_KEY 和 AISHIE_S3_SECRET_KEY 中,永远不写在命令行上(命令行会留在 shell 的历史记录里)。在 bash 中:

read -r AISHIE_S3_ACCESS_KEY; read -rs AISHIE_S3_SECRET_KEY; export AISHIE_S3_ACCESS_KEY AISHIE_S3_SECRET_KEY
sh AIShie-Deploy/setup-server.sh aishie.example.edu edge --storage aws --s3-region ap-east-1 --s3-bucket aishie-files
sh AIShie-Deploy/setup-server.sh aishie.example.edu edge --storage r2 --r2-account-id <32 位十六进制数字> --s3-bucket aishie-files
sh AIShie-Deploy/setup-server.sh aishie.example.edu edge --storage b2 --s3-region us-west-004 --s3-bucket aishie-files
sh AIShie-Deploy/setup-server.sh aishie.example.edu edge --storage s3 --s3-endpoint s3.example.com --s3-bucket aishie-files --s3-path-style no

(以上是四种服务各一个示例,只运行其中一行。)也可以用变量代替选项:AISHIE_STORAGE、AISHIE_S3_BUCKET、AISHIE_S3_REGION、AISHIE_S3_ENDPOINT、AISHIE_S3_PATH_STYLE、AISHIE_R2_ACCOUNT_ID 和 AISHIE_R2_JURISDICTION(eu 或 fedramp)。都没有设置、又无人应答时,就用磁盘。

检查

  • 写入任何东西之前,setup-server.sh 用密钥以只读方式检查存储桶:列出最多一个键,再请求一个不存在的对象(Core 启动时也这样做)。密钥被拒、区域错误或存储桶不存在,它就停止,显示服务返回的错误码和信息,什么都不写入。
  • 它不写入,所以不会留下任何东西;第一次上传(或 aishie storage migrate)才能证明密钥可以写入。
  • 之后随时可以再检查:aishie storage check。
  • 密钥不会出现在命令行或输出中:core.env(0600)为 Core 保存它们。

CORS 规则

网站的页面直接上传到存储桶,所以存储桶要允许网站的源:

[
  {
    "AllowedOrigins": ["https://aishie.example.edu"],
    "AllowedMethods": ["GET", "HEAD", "PUT"],
    "AllowedHeaders": ["content-type"],
    "MaxAgeSeconds": 3600
  }
]
  • 存储桶没有任何 CORS 规则、而密钥有权设置时,setup-server.sh(和 aishie storage migrate)会自行设置。已有规则的存储桶不会被改动,因为那个操作会替换全部规则;这时它会打印规则和设置的位置。
  • AWS 控制台:S3 › Buckets › 存储桶 › Permissions › Cross-origin resource sharing (CORS) › Edit,粘贴(与已有规则并列),Save changes。
  • Cloudflare 控制台:R2 Object Storage › 存储桶 › Settings › CORS Policy › Add CORS policy,在 JSON 标签页粘贴,Save。
  • Backblaze:网页控制台的 CORS 设置只有预设选项,这条规则要经 S3 API 设置,使用一个可以修改存储桶(writeBuckets)的密钥。
  • 任何服务:aishie storage cors --apply --ask-keys 用你在提示时输入的密钥设置(密钥不会保存)。aishie storage cors 检查存储桶是否已有规则。

没有这条规则,网站上的上传会在浏览器中失败(浏览器的控制台会提到 CORS)。下载是链接,不需要规则。

查看与迁移:aishie storage

命令做什么
aishie storage文件现在存放在哪里,以及磁盘上有多少
aishie storage check用 core.env 中的密钥检查存储桶,只读
aishie storage cors [--apply] [--ask-keys]存储桶需要的 CORS 规则;--apply 在没有规则的存储桶上设置
aishie storage migrate --to s3 [--dry-run] [--size-only] …把文件从磁盘迁移到存储桶

从磁盘迁移到存储桶

aishie storage migrate --to s3 --dry-run --storage aws --s3-region ap-east-1 --s3-bucket aishie-files
aishie storage migrate --to s3 --storage aws --s3-region ap-east-1 --s3-bucket aishie-files

密钥放在 AISHIE_S3_ACCESS_KEY 和 AISHIE_S3_SECRET_KEY 中,或在提示时输入。--dry-run 检查存储桶,说明共有多少文件、存储桶中已有多少、要复制多少,什么都不改。正式迁移时:

  1. 检查存储桶,设置或检查 CORS 规则(没有规则又无法设置,就在复制之前停止);
  2. 在 Core 运行期间复制每个文件,以 Core 读取它所用的键存放,并带上正确的内容类型;
  3. 以大小和校验和(MD5 对比对象的 ETag)核对两边的每个文件;
  4. 停止 Core,复制期间新上传的文件,再核对一次:网站会停止服务几分钟;
  5. 把 BLOB_STORE=s3 和存储桶的设置写入 core.env(旧文件保留为 core.env.before-storage-<时间>),启动 Core。Core 一分钟内没有报告正常,就放回旧的 core.env 再启动。
  • 它用 rclone 的容器复制(版本固定在 aishie-storage 中),服务器上不安装任何东西。中途停止或核对失败时不会切换,再次运行会从停下的地方继续。
  • ETag 不是 MD5 的存储桶(例如以 SSE-KMS 加密的),用 --size-only 只核对大小。
  • 磁盘上的副本会保留,Core 不再读取它。确定一切正常后,用 rm -r /srv/aishie/core/blobs 删除。
从存储桶迁回磁盘

aishie storage migrate --to fs 目前只从存储桶取回 courses/ 下的文件(较早的上传),不会取回 documents/、conversations/ 和 renditions/ 下的文件(多文件的教材版本、对话中的文件、Office 文件的 PDF)。在这一点修复之前,请把迁回磁盘视为不支持,在设置服务器时就选好存放位置。从磁盘迁移到存储桶则会复制磁盘上的所有目录。

Core 存放什么

  • documents/:教材和作业各版本的文件(一个版本可以有多个文件)。
  • courses/:较早时一个版本只有一个文件时的上传。
  • conversations/:对话中附加的文件。
  • renditions/:Office 文件转换成的 PDF。
  • exports/:导出的对话,EXPORT_TTL 后删除。

用存储桶时,附加后的文件在 attached/ 下(例如 attached/documents/)。Core 不理会目录或存储桶中的其他东西。

两套 AIshie 绝不能共用一个目录或存储桶:例如从学校网站复制数据库建立的测试站,会把对方附加的文件当作孤立文件删除。

智能体运行环境

堆栈中的 runtime 容器。每个网站只有一个运行环境:只有它持有 Core 的 agent_runtime 凭证,所以只有它可以在网站上托管智能体。在别处部署的运行环境无法连接网站来托管智能体;在别处使用的智能体,是通过 MCP 访问的智能体。

它做什么

  • 托管站内的智能体:智能体拥有者在网站上把智能体交给它,选择模型和密钥;运行环境按智能体的 ID 向 Core 获取智能体唯一的令牌并加密保存,没有人需要粘贴令牌(见用户指南的“站内托管”)。
  • 把 Office 文件转换为 PDF:Core 保存的每个 Word、Excel、PowerPoint、OpenDocument 和 RTF 文件,都转换一次成网站预览用的 PDF(见用户指南的“Office 文件以 PDF 显示”)。这不用 AI,不产生费用,网站上也没有开关。升级后,已有的文件从最新的开始转换,新上传的文件优先。
  • 读取扫描文件(OCR):模型自己无法读取文件时,用 tesseract 识别扫描 PDF 和图片中的文字(简体中文、繁体中文、英文)。
  • LibreOffice:把演示文稿和文档转换为 PDF 交给能读文件的模型,让模型看到幻灯片的样子。
  • 文件转写(文字版):管理员开启后,用学校 AI 方案的模型把课程文件转写成文字版(见用户指南的“文件的文字版”)。它用自己的另一个凭证,由平台管理员在网站上签发,不是你的工作。
  • 课程教材搜索:智能体回答时可以搜索课程的教材(见课程教材搜索)。

运行环境在 Core 的凭证

在 Core 里,运行环境是网站服务 agent_runtime,用自己的凭证(aissvc_…)调用 Core。只有凭它,运行环境才能核查智能体的拥有者、获取和撤销智能体的令牌,以及领取要转换的 Office 文件。

  • 凭证在 /etc/aishie/runtime/secrets/core/agent_runtime(0600,属于运行环境的用户 65532),运行环境以 secret://core/agent_runtime 读取。setup-server.sh 在 Core 完成迁移后签发它。
  • 没有有效的凭证:没有智能体会运行(状态 error,原因 runtime_misconfigured),也没有 Office 文件会被转换。智能体自己的令牌不受影响。

签发新的凭证

aishie runtime-credential

它请 Core 签发新的凭证并撤销这个服务的其他凭证,把文件整个替换(不会只写一半;被拒绝时文件保持原样),再以新凭证重建运行环境。运行环境托管的智能体保留它们的令牌。成功时最后两行是:

aishie: kept in /etc/aishie/runtime/secrets/core/agent_runtime, which the runtime reads as secret://core/agent_runtime; Core revoked any other the service held
aishie: the runtime recreated, with it

以下情况运行它:凭证可能泄露了;管理员在网站上撤销了它;你从轮换之前的副本恢复了 /etc/aishie;或者运行环境的日志说 Core 拒绝了它的凭证(401)。

管理 › AI 与文件 › 智能体托管:运行环境现在的凭证,标示签发者、签发时间和最后使用时间。截图中的凭证是从网站签发的(签发者“平台管理员”),下面说明不要这样做;新服务器上看到的是配置服务器时签发的凭证,名称是 runtime,签发者是“服务器(设置时)”。
不要手动放置凭证

网站的“智能体托管”卡片也有 签发凭证,签发的凭证要自行放进服务器。请不要这样做,也不要手动编辑或清空这个文件:一律用 aishie runtime-credential,它会完成所有步骤,而且凭证不会出现在屏幕上。

谁可以管理运行环境

网站的 AI 与文件 页(学校 AI 方案、计价与额度、今日用量、文件、智能体托管)只供运行环境的管理员使用。默认是 Core 中所有的 Root 和平台管理员。要只限其中几位,在 runtime.env 中列出他们的参与者 ID(在 管理 › 人员与智能体 中打开此人的页面可以看到),以逗号分隔:

ADMIN_ACTOR_IDS=<参与者 ID>,<参与者 ID>

然后运行 aishie compose up -d runtime。不是 Root 或平台管理员的账号,列在这里也不算。不在名单中的管理员打开这一页,会看到“你不是这个运行环境的管理员”。

不在 ADMIN_ACTOR_IDS 中的平台管理员打开 AI 与文件。

运维者的上限

运行环境的管理员在网站上可以开关 OCR、选择 OCR 的语言、开关文件转写、添加学校 AI 方案的模型、设置额度和价格(见日常管理员指南的“AI 与文件”)。改动在几秒内生效,不用重启。但以下各项由你在服务器上决定,网站不能越过:

  • OCR=off:无论网站如何设置,OCR 都不会运行。其他 OCR_* 设置只在 runtime.env 中。
  • TRANSCRIBE=off:文件转写永不执行。
  • OFFICE_PDF=off:不用 LibreOffice,所以也不会转换 Office 文件的 PDF 预览。RENDITIONS=off 只停止 PDF 预览的转换。两者之下,之前已转换的 PDF 照常预览;新上传的 Office 文件会一直显示“正在转换为 PDF…”,读者仍可下载原文件。
  • runtime.yaml 的 school: 模型及其密钥文件:网站只能查看,不能修改,也不能添加同一 ID 的模型。
  • allowed_models 和 denied_models:网站添加的模型也受它们限制。
  • KMS_KEY_ID:加密网站保存的学校密钥和拥有者的密钥。

网站上的额度和价格设置会取代 runtime.yaml 中的设置,直到管理员把它重置为服务器默认值。

AI 与文件 › 文件:运维者设置了 OCR=off 和 TRANSCRIBE=off,页面说明服务器已停用这两项功能,无论这里如何设置都不会执行;ⓘ 写明是哪一项设置。

资源

  • OCR:读一个文件时用一个 CPU(优先级低于回答问题),tesseract 每页约用 200 MB 内存。默认 300 dpi 下,40 页的扫描文件要几分钟;OCR_DPI=200 约快一倍,但小字较难识别。
  • LibreOffice:转换时用一个 CPU 和约 400 MB 内存(上限 2 GiB)。一份 38 页的演示文稿约 5 秒。
  • CPU 有余量的服务器,可以把 OCR_CONCURRENCY 或 RENDITIONS_CONCURRENCY 调到 2。

每个回答的预算

运行环境为每一个回答设置上限,避免一个问题用掉过多的时间和金钱。上限在 runtime.yaml 的 budgets.per_answer 中:写在 runtime.defaults 下的,适用于每个智能体,包括拥有者在网站上托管的智能体;写在运维者自己的智能体 YAML 中的,只适用于那个智能体。

设置项默认说明
turns8模型最多几轮(每轮可以调用工具)
tool_calls12最多调用几次工具
max_writes10其中最多几次更改课程(只在拥有者自己发起的对话中)
input_tokens150000一个回答所有调用的输入 token 合计
output_tokens12000一个回答所有调用的输出 token 合计,包括模型的思考
wall_clock_s180一个回答最多用多少秒

每一次调用模型的输出上限是另一项设置:模型的 params.max_output_tokens,默认 4000;拥有者可以在网站的“高级”中为自己托管的智能体设置(256 至 32000)。例子:让所有智能体每个回答可以输出更多、用更长时间:

runtime:
  defaults:
    budgets:
      per_answer: {output_tokens: 24000, wall_clock_s: 240}

修改后运行 aishie runtime check --live,再运行 aishie compose kill -s HUP runtime。

预算用完时:强制的最后一轮

  • 预算快用完时,运行环境会强制最后一轮:模型不能再调用工具,要按已读到的内容作答。之前的每一轮都会为它留下一些输出 token:2000、一次调用的上限,以及 output_tokens 的四分之一,三者中最少的一个。
  • 某一轮还没写出任何内容就碰到输出上限(会思考的模型可能整轮都在思考),也会强制最后一轮,而不是直接放弃。
  • 强制的一轮,以及接续被截断的回答时,会请模型用它所接受的最少推理:能关掉思考的就关掉(例如 DeepSeek、GPT-5.5 及之后的模型、Gemini 2.5 Flash),不能的就用最低的推理强度;本来就思考最少的模型不另作要求;运行环境不认识的模型系列(例如 Kimi、GLM、Qwen 自己的端点),最多只用设置的推理强度。供应商的 API 以 400 拒绝这个要求时,会按原来的设置再调用一次,并记录在日志中。这同样适用于经 OpenRouter、Azure 或 Bedrock 使用的模型。
  • 强制的一轮什么都没写出时,会再请模型一次,用几十到几百字简短作答(回答的日志行写 asked_again)。
  • 仍然没有答案时,提问者会看到预算用完的提示(见下文)。

会思考的模型把思考也算进输出 token。回答经常以提示结束、或被截断时,请提高 output_tokens 和模型的 params.max_output_tokens,或在设置了推理强度的地方把 reasoning.effort 调低。指标 budget_exhausted_total{budget} 记录回答碰到的是哪一项预算;output_tokens 也包括还没写出内容就被截断的那一轮(见指标)。

提示的语言

智能体无法回答时,会贴出一句提示代替答案:拒绝回答(on_refusal_text)、预算用完或所有供应商都连不上(on_budget_text),以及每日额度用完(on_quota_text,或学校方案自己的提示)。这些默认都不设置,运行环境贴出自己的提示:

  • 拒绝回答、预算用完:以提问者的语言写成:智能体设置了固定的 answer_language 时用那个语言,否则按问题的文字判断(英文、繁体中文或简体中文);判断不了时,同时用英文和繁体中文。
  • 额度用完:运行环境在读取对话之前就检查额度,所以这句提示无法跟随问题的语言。智能体设置了固定的 answer_language,而且是英文或中文时,用那个语言;否则一律同时用英文和繁体中文,不会用简体中文。要让它用简体中文,可以把 answer_language 固定为简体中文(如 fixed:zh-Hans,所有回答也都会用简体中文),或自己写这句提示(见下文)。

用完的是哪一项额度,决定贴出哪一句。智能体自己的每日预算(budgets.per_agent_day 和 budgets.per_asker_day,或网站上的“智能体每日预算”),以及一个人在学校密钥上的额度(网站上的“每人额度”)用完时,贴出智能体的提示 on_quota_text。学校方案的额度(school: 下的 per_owner_day、per_asker_day 和 per_day,或网站上方案的“每日额度”)用完时,贴出方案自己的提示,由 school: 下的 on_quota_text 设置。学校密钥上的额度用完时,如果拥有者在方案后面加上了自己的密钥,就由那把密钥回答,不贴提示。简体中文的提示是(两句额度提示只在 answer_language 固定为简体中文时才这样出现):

情况设置提示
预算用完on_budget_text这题我没能完成,请试着问得具体一点。
拒绝回答on_refusal_text这个我无法在这里帮忙,请向你的老师询问。
智能体的每日预算或每人额度用完on_quota_text今天我能回答的问题已达上限。请明天再试,或向你的老师询问。
学校方案的额度用完school.on_quota_text今天的学校 AI 额度已用完,请明天再试。

所以,answer_language 没有固定时,学校方案的额度用完后,提问者不论用什么语言提问,看到的都是:

Today's school AI allowance is used up. Please try again tomorrow.

今天的學校 AI 額度已用完,請明天再試。

你设置的文字(在智能体的设置、runtime.defaults 或 school: 中)会照写的贴出,不论提问者用什么语言;不设置,就用运行环境自己的提示,语言如上。AIShie-Agent-Runtime 仓库的 examples/runtime.yaml 在 runtime.defaults 中设置了 on_quota_text:从那里复制了的话,智能体的额度提示会一直是那句话,直到你把它删去。提示下面不会列出任何“依据”,网站也不会在它下面写“未引用课程教材”。

检查运行环境

命令做什么
aishie runtime check --live以运行时的方式加载设置,把每个智能体连接到 Core,并以一次调用试用每个模型密钥;列出 OCR、LibreOffice、PDF 预览和文件转写的状态,以及学校方案的模型和额度
aishie runtime-status运行环境的 /status:每个智能体的状态、席位、今天的回答和花费
aishie compose kill -s HUP runtime让运行环境重新读取 /etc/aishie/runtime/agents/ 中的设置
curl -s 127.0.0.1:9090/healthz是否在运行、哪个版本

示例:aishie runtime check 输出中的几行。

ocr: tesseract 5.5.0 chi_sim+chi_tra+eng 300dpi, 1 at once, 40 pages a file at most
office: LibreOffice 25.2.3.2, 1 at once, 300 pages a file at most, 2m0s a file
renditions: on, 1 at once, each in 5m0s at most, with LibreOffice 25.2.3.2, with the runtime's own credential in Core (CORE_SERVICE_CREDENTIAL)
transcriber: off in the site's settings; …

/status 中值得留意的智能体状态:

  • error,原因 runtime_misconfigured:运行环境没有有效的凭证,运行 aishie runtime-credential。
  • error,原因 mcp_agent:这是 MCP 访问的智能体,运行环境不托管它。
  • error,原因 core_too_old:要升级 Core。
  • unauthorized:智能体的令牌在 Core 被撤销了。运维者的智能体在重新读取设置(HUP)时就会获取新令牌;站内托管的智能体由拥有者在网站上请求新令牌。
  • owner_changed:智能体在 Core 的拥有者不是把它交给运行环境的人;它停止运行,直到拥有者再次托管它。

运维者自己的智能体(可选)

由你在服务器上设置、而不是由拥有者在网站上托管的智能体,是 /etc/aishie/runtime/agents/ 中的一个 YAML 文件。

  1. 先在网站上创建一个站内托管的智能体(或由管理员注册),记下它的 ID。
  2. 编写 YAML,以 agent_id 指定这个智能体。它不含令牌:运行环境会用自己的凭证获取。运行环境的 docs/deploying.md 和 examples/ 说明每一项。
  3. 它引用的每个机密(例如模型密钥 secret://keys/tutor)是 /etc/aishie/runtime/secrets/ 下的一个文件(这里是 keys/tutor)。YAML 和机密都属于 root,用户组 65532 可读;运行环境不能写入它们。
  4. 安装、检查、生效:
    install -g 65532 -m 640 tutor.yaml /etc/aishie/runtime/agents/
    aishie runtime check --live
    aishie compose kill -s HUP runtime

这个目录中最多只能有一份 runtime: 文档(见学校 AI 方案)。不是智能体设置的 YAML 文件(例如价目表 prices.yaml)要放在子目录中:与智能体并列的 .yaml 会被当作智能体的设置读取。

对外连接

  • 站内托管的智能体,模型只会在供应商的官方端点被调用,没有人可以给运行环境一个网址。运行环境连接时拒绝任何不在公共互联网上的地址(本机、私有网络、链路本地和云端的元数据地址、运营商级 NAT、含 IPv4 地址的 IPv6 形式和其他保留范围),也不跟随重定向。
  • 设置了 EGRESS_PROXY 时,运行环境只连接代理服务器,由它解析名称和建立连接,所以代理服务器本身要拒绝这些地址。
  • 学校自己的模型服务器(网关、vLLM、Ollama)只能经 runtime.yaml 的 school: 提供(见学校 AI 方案)。
  • 运行环境以长轮询等候 Core 的新消息,每个请求最长约 40 秒。运行环境与 Core 之间的任何代理服务器,都要允许请求持续至少 40 秒。
  • 你在网站前面另加的代理服务器或负载均衡器,同样要允许请求持续至少 40 秒(nginx 默认的 60 秒就够)。大型的导出对话,Core 会让请求最长等 5 分钟:代理服务器提前切断,只会丢失这次的响应,导出本身仍会完成。堆栈中的 Caddy 不会切断任何请求。

模型供应商与密钥

站内托管的智能体用哪一把密钥调用模型,有三种来源。只有第二种要你在服务器上设置。

三种密钥

  1. 拥有者自己的密钥:在网站上为智能体输入,加密保存在运行环境的数据库中,用 kek/v1 加密(见轮换运行环境的密钥)。
  2. 学校 AI 方案(服务器):在 runtime.yaml 的 school: 中设置,每把密钥是服务器上的一个文件,不存入数据库,也从不显示(见学校 AI 方案)。
  3. 学校 AI 方案(网站):运行环境的管理员在 AI 与文件 › 学校 AI 方案 中添加,使用供应商的官方端点和学校的密钥,先试用才保存,像拥有者的密钥一样加密,之后只显示提示(例如 sk-…3f9a)。

网站上可选的供应商:OpenAI、Azure OpenAI、Anthropic、Google Gemini、Amazon Bedrock、DeepSeek、Qwen(阿里云百炼)、Moonshot(Kimi)、智谱 GLM 和 OpenRouter。

学校 AI 方案

学校可以用自己的密钥提供模型,让在这里托管智能体的人不用自备密钥。不设置的话,站内托管的智能体只用拥有者自己的密钥。

  1. 创建密钥的目录,从标准输入写入密钥(粘贴密钥,按 Enter,再按 Ctrl-D),并安装 runtime.yaml:
    install -d -m 750 -g 65532 /etc/aishie/runtime/secrets/school /etc/aishie/runtime/secrets/school/keys
    install -m 640 -g 65532 /dev/stdin /etc/aishie/runtime/secrets/school/keys/anthropic-main
    install -g 65532 -m 640 runtime.yaml /etc/aishie/runtime/agents/
    aishie runtime check --live
    aishie compose kill -s HUP runtime
  2. check 列出方案的模型和额度;HUP 让它们生效。运行环境报告有学校密钥后,网站会在站内托管的模型选项中提供“学校方案”。

/opt/aishie 中没有示例文件;AIShie-Deploy 仓库的 examples/runtime/runtime.yaml 可以作为起点。示例(节选):

runtime:
  # prices_ref: prices/prices.yaml     # 可选;只有以美元计的额度才需要
  school:
    offers:
      - id: standard
        label: "School AI (Claude Haiku)"
        adapter: anthropic
        model: claude-haiku-4-5
        key_ref: secret://school/keys/anthropic-main
        params: {max_output_tokens: 1500}
    per_owner_day: {answers: 100}     # 每人(所有智能体合计)
    per_asker_day: {answers: 20}      # 每位提问者(每个智能体、每门课)
    # per_day: {answers: 5000}        # 可选:全校上限
  • 密钥文件 0640 root:65532,目录 school 和 school/keys 都是 0750 root:65532。密钥从不离开服务器:运行环境在调用模型时才读取它。
  • 额度:每人每天 per_owner_day(未设置时为 100 次回答)、每位提问者 per_asker_day(未设置时为 20 次),以及可选的全校 per_day。一天按 UTC 计算,在 00:00 UTC 重新开始。拥有者可以在方案后面加上自己的密钥,方案额度用完后由它回答;没有的话,提问者会被告知今天的学校额度已用完(on_quota_text 可以改写这句话)。
  • 以美元计的额度(usd:)需要一个为每个模型定价的价目表,否则运行环境不会启动。价目表要放在子目录中,例如 /etc/aishie/runtime/agents/prices/prices.yaml,由 prices_ref: prices/prices.yaml 或 runtime.env 的 PRICES=/config/prices/prices.yaml 指定。示例 examples/runtime/prices/prices.yaml 中的数字只是演示格式,请按供应商的价格页面填写。
  • allowed_models 和 denied_models 限制可以用学校密钥的模型,网站添加的模型也受它们限制。
  • 文件转写的模型调用计入方案的全校上限,不计入任何人的额度。
  • 今天每人的用量:见网站“AI 与文件”的 今日用量 标签页。
  • 更换学校密钥:替换文件(运行环境每次调用模型时才读取它),再运行 aishie runtime check --live。
  • 从 school: 删去一个模型后,使用它的智能体改用拥有者在它后面加上的模型和密钥;没有的话,智能体停止并说明学校撤回了这个模型。
AI 与文件 › 学校 AI 方案:runtime.yaml 中的模型标示为由服务器运维方设置,只能查看;旁边是管理员在网站上添加的模型。

单点登录(服务器级)

AIshie 通过 OpenID Connect 身份提供者(例如 AD FS、Microsoft Entra ID、Google、Keycloak)让用户登录。你可以在服务器上设置一个,平台管理员也可以在网站上添加。

两种提供者

  • 运维者的提供者:在 core.env 的 OIDC_* 中设置,最多一个。网站的 登录方式 页把它排在最前,标示“由服务器运维者设置”,只能查看。
  • 网站的提供者:Root 和平台管理员在 管理 › 登录方式 中添加、测试和启用(见日常管理员指南的“单点登录”)。改动在下一次登录时生效,不用重启。它们需要 SECRETS_KEY。

两种都在提供者处登记同一个重定向 URI:

https://aishie.example.edu/v1/auth/sso/callback

单点登录不会创建账号:先由管理员注册此人,然后关联他的账号(运维者的提供者,以及没有开启“以邮箱关联”的网站提供者),或依靠网站提供者的“以邮箱关联”(见日常管理员指南的“关联账号”)。单点登录覆盖所有人之后,可以在 core.env 中设置 JOIN_LINK_REGISTRATION=off,关闭经邀请链接自行注册。

管理 › 登录方式:运维者的提供者排在最前,标示“由服务器运维者设置”,只能查看。

设置运维者的提供者

  1. 在身份提供者处登记一个客户端,重定向 URI 填上面的网址。
  2. 把它给你的信息写进 /etc/aishie/core.env:
    OIDC_ISSUER=https://adfs.example.edu/adfs
    OIDC_CLIENT_ID=<客户端 ID>
    OIDC_CLIENT_SECRET=<客户端密钥;含有 $ 的话加单引号>
    OIDC_PROVIDER_NAME=school-adfs
    OIDC_DISPLAY_NAME=示範大學 NetID
  3. 运行 aishie compose up -d core。Core 启动时读取提供者的发现文档。
  4. 一分钟内,登录页会出现 以示範大學 NetID登录。
  • OIDC_ISSUER 有值,运维者的提供者就开启。AD FS 的颁发者是 AD FS 服务器的网址加上 /adfs。
  • OIDC_PROVIDER_NAME 是提供者的 ID,每个关联到它的账号都记在这个 ID 下。在任何人关联之前设置,之后永不更改,否则已关联的人都无法登录。不设置的话,它是占位用的 example-adfs:一律设置你自己的 ID。网站的提供者如果用了同一个 ID,运维者的提供者会取代它。
  • OIDC_DISPLAY_NAME 是登录按钮上的名称,最多 64 个可打印字符,否则 Core 不会启动(aishie logs core 会说明原因)。不设置的话,按钮显示“以单点登录登录”。
  • OIDC_SUBJECT_CLAIM(默认 upn)决定账号以哪个声明识别;OIDC_SCOPES 默认 openid profile email。默认值适用于 AD FS。
  • 运维者的提供者可以位于任何地址,不受下面的私有地址限制。
登录页:单点登录按钮排在最前,按钮上的名称取自提供者的显示名称;密码登录收在下面的链接后面。

SECRETS_KEY

网站的提供者的客户端密钥,以 core.env 中的 SECRETS_KEY(32 个随机字节的 base64)加密保存在 Core 的数据库中。setup-server.sh 在新服务器上已写入它。

  • 没有它,网站无法添加提供者(secrets_key_missing),页面会说明需由运维者加入;运维者的提供者不受影响。
  • 它加密的东西,只有它能解开。像 SIGNING_KEY 一样,把它复制到服务器之外(见服务器之外要保存什么),不要丢失,只以轮换方式更改(见轮换 SECRETS_KEY)。
  • 它丢失或没有轮换好时,用它加密的提供者会显示“无法打开密钥”(secret_unavailable),不再提供,直到管理员重新输入密钥。
Core 没有 SECRETS_KEY 时的登录方式页:说明需由服务器运维者设置一把加密客户端密钥的密钥;旁边的 ⓘ 写明是 SECRETS_KEY。

位于私有地址的提供者

对于网站的提供者,Core 只会连接公共地址:读取发现文档和密钥、登录时调用令牌端点,都是如此。它在每次连接时检查实际连到的地址,并拒绝:

  • 本机和 localhost;
  • 私有网络:10/8、172.16/12、192.168/16、fc00::/7;
  • 链路本地地址:169.254/16、fe80::/10(云端的元数据地址就在这里);
  • 共享地址 100.64/10、组播地址,以及 IANA 列为非全局可达的其他地址,包括以 IPv6 形式写出的这些地址。

它也不经 HTTPS_PROXY 连接这些提供者。症状:

  • 管理员按 测试,报告问题的原因是 issuer_address_not_allowed;
  • 经这个提供者登录失败(sso_provider_unavailable),aishie logs core 中有原因。

以下情况要在 core.env 中加上 SSO_ALLOW_PRIVATE_ISSUERS=true,再运行 aishie compose up -d core:

  • 学校的 AD FS 在校内网络,名称解析到以 10. 开头的地址;
  • 服务器只能经代理服务器连接互联网;
  • 服务器的 DNS 以 198.18. 开头的地址回答所有名称(fake-IP)。

开启后,Core 启动时会在日志中说明;只有位于同一台机器上的颁发者可以用 http。

之后再移除这项设置的话,已启用、而颁发者写成 localhost 或非公开地址的网站提供者,就不会再出现在登录页;在“管理 › 登录方式”中,它的状态是“颁发者地址非公开”(issuer_address_not_allowed)。

添加提供者时的测试报告:颁发者的名称解析到私有地址,原因是 issuer_address_not_allowed。

确认登录页显示什么

前端的镜像对所有服务器都一样,单点登录的按钮由 Core 决定。登录页向 Core 的 GET /v1/auth/methods 查询:

curl -s 127.0.0.1:8080/v1/auth/methods

没有单点登录时:

{"password": true, "password_accepts": ["login_id", "email"], "sso": null, "sso_providers": []}

有提供者时,sso_providers 列出现在可以登录的每一个(运维者的排在最前),各有 id、label(按钮上的名称)和 start。浏览器可能把答案缓存一分钟,所以改动会在一分钟内出现在登录页。有可以登录的提供者时,登录页先列出它们的按钮,学号和密码的表单收在按钮下面的链接后面。

让其他网站嵌入 AIshie

例如学习管理系统(LMS)在 iframe 中打开 AIshie。默认只有 AIshie 自己的页面可以这样做。

在学习管理系统的框架中显示

  1. 在 /etc/aishie/aishie.env 中设置 FRAME_ANCESTORS,整个值用双引号,因为 CSP 的关键字本身带单引号:
    FRAME_ANCESTORS="'self' https://lms.example.edu"
  2. 运行 aishie compose up -d web。
  3. 确认浏览器收到的响应头:
    curl -sI https://aishie.example.edu/ | grep -i content-security-policy
  4. 要在框架中保持登录,在 core.env 中设置 COOKIE_SAMESITE=none,再运行 aishie compose up -d core:在别的网站的框架中,Core 的登录 Cookie 属于第三方 Cookie。
  • FRAME_ANCESTORS="'none'" 不让任何页面嵌入。不设置或留空,就是 'self'。
  • 值写成多行会让前端无法启动(aishie logs web 中有原因),之后每个新的前端镜像也会因健康检查失败而回滚。
  • Safari 和 iOS 上的所有浏览器都拒绝第三方 Cookie,关闭了第三方 Cookie 的浏览器也一样:在这些浏览器中,AIshie 只能在自己的标签页中使用。
  • 在框架中用单点登录,会把框架带到提供者的登录页,而大多数提供者不允许被嵌入。
  • 反方向(AIshie 在框架中显示别的网站,例如相似度检测服务的查看器)不受限制。

更新:edge 与 stable

服务器自己更新:aishie-update 每 5 分钟检查三个镜像的通道,有新版本就按安全的顺序部署,失败就回滚。

两个通道

  • edge:CORE_IMAGE、RUNTIME_IMAGE、WEB_IMAGE 跟随 :edge,即每个仓库 main 通过检查后的最新版本。
  • stable:每个通道必须是正式版本 X.Y.Z 或摘要(sha256:…)。aishie-update 在 stable 上拒绝 :edge、:1.3、:latest 和 :stable 这类会自己移动的标签,所以每次升级和随之而来的迁移,都是有人决定的。
  • 每个通道都必须是它自己仓库的镜像:镜像的来源标签不符就会被拒绝。

aishie-update

aishie-update.timer 每 5 分钟运行一次(最多随机延迟一分钟;服务器关机时错过的,开机时补上)。每次先获取锁,再依次处理 Core、运行环境和前端:

  1. 拉取通道的标签,读出它现在指向的摘要。
  2. 与正在运行的相同:什么都不用做。之前失败过的摘要:也不做,日志记录一次,直到通道指向另一个摘要或有人运行 --retry。
  3. 其他摘要就部署:
    • Core:检查镜像名称和来源标签;读取版本;询问它的迁移到第几号(见迁移 0027);把 aishie_core 备份到 /var/backups/aishie/core-deploy-<UTC 时间>.dump(保留最近十个;备份失败就停止,什么都不改);以新镜像运行 migrate up 和 seed;重建 Core;一分钟内 /healthz 必须报告 ok 以及新的版本和提交。
    • 运行环境:同样,另外在备份之前用新镜像 check 智能体的设置;数据库是 aishie_runtime;只有 migrate up,没有 seed。
    • 前端:检查名称和标签;重建;/version.json 必须报告镜像标签上的提交。
  4. 新版本没有报告正常,就换回之前的版本(并等它报告正常),新摘要记为失败。迁移在切换之前已完成,而每个迁移都让之前的版本仍能运行(Core 的迁移 0027 例外)。
  5. 部署后,删除三个仓库中没有容器使用的镜像,不动其他镜像。

每次运行遇到第一个错误就停止;在重建之前,原来的版本照常运行。正在运行的镜像以摘要记录在 /var/lib/aishie/images.env:重启或 aishie compose up -d 启动的是同样的镜像,不会跟随标签。

命令做什么
aishie-update --status每个服务:正在运行的镜像、通道、上次检查及结果、失败的摘要、固定版本
aishie-update --dry-run现在运行会做什么,不作任何改动
aishie-update立即运行一次,不用等 5 分钟
aishie-update --retry SERVICE忘记这个服务失败的摘要,再运行一次
aishie-update --pin SERVICE REF部署指定的镜像(标签或摘要),并固定在它上面
aishie-update --unpin SERVICE恢复跟随通道,再运行一次
journalctl -u aishie-update每次运行的完整输出

示例:aishie-update --status 的格式(只列出 Core;运行环境和前端相同)。

aishie.example.edu (edge)

core
  running:     ghcr.io/aishie-education/aishie-core@sha256:…
  channel:     ghcr.io/aishie-education/aishie-core:edge
  last check:  2026-10-02T03:05:41Z up to date
  last result: 2026-10-02T02:40:12Z core sha256:… -> sha256:…: deployed … (…)

升级 stable

  1. 先在 edge 服务器上试用这个版本(每个正式版本的提交都已在 edge 上运行过)。
  2. 在 stable 服务器的 /etc/aishie/aishie.env 中设置新版本,例如 CORE_IMAGE=ghcr.io/aishie-education/aishie-core:X.Y.Z。
  3. 运行 aishie-update(或等 5 分钟)。它先部署 Core,再部署运行环境和前端。
  4. 查看 aishie runtime-status:运行环境启动时读取 Core 的工具目录,拒绝它所依赖的工具已不存在或已改变的 Core。只升级了 Core 时,重启运行环境(aishie compose restart runtime),它才会用上新 Core 在工具目录中新增的功能。

要回到之前的版本,设回之前的版本再运行 aishie-update,或用 aishie-update --pin(见回到之前的版本)。

升级到 0.2.0

0.2.0 要按这个顺序更新:Core,然后智能体运行环境,最后前端,三者都升到 0.2.0。

  1. 先更新 Core。运行环境和前端都是按 Core 0.2.0 检查的。较旧的 Core 会拒绝前端的“要求修改”,回答也不会显示依据的教材。
  2. 再更新运行环境。0.2.0 之前的运行环境,回答被退回修改后就不会再回答那个问题,要等提问的人再发消息。
  3. 最后更新前端,永远不要比运行环境早。前端 0.2.0 在每项提议上都提供“要求修改”,包括站内托管的智能体在对话中的回答。

在 stable 服务器上,把 /etc/aishie/aishie.env 中的 CORE_IMAGE、RUNTIME_IMAGE 和 WEB_IMAGE 一起设为三个 :0.2.0 镜像,再运行 aishie-update:它按 Core、运行环境、前端的顺序部署,遇到第一个失败就停止。下一次运行会跳过记为失败的镜像,继续部署之后的服务,所以 Core 或运行环境失败时,请把排在它之后的通道设回 0.1.0,直到问题解决(用 aishie-update --retry SERVICE 重试)。跟随 edge 的服务器也是同一个顺序。

  • Core 的迁移 0028、0029 和运行环境的迁移 0015 都不需要你做任何事,也不改动任何现有的数据;aishie-update 在切换之前运行 migrate up。没有新的设置。
  • 回到 0.1.0 就是部署 0.1.0 的镜像,数据库结构保持不变(见回到之前的版本)。把运行环境换回 0.1.0 时,前端也要一并换回 0.1.0;在此之前,请教师拒绝智能体的回答并写明原因,而不要退回修改。不要运行 migrate down(见Core 的迁移 0028 和 0029)。

PostgreSQL

stack.yaml 固定使用 PostgreSQL 18,aishie-update 不会更新它。18 的次版本更新:

aishie compose pull postgres && aishie compose up -d postgres

新的主版本无法读取旧版本的文件,要在所有服务停止时手动转储和还原。等 AIShie-Deploy 改用新的主版本时,按服务器上 /opt/aishie/README.md 中“PostgreSQL's major version”一节进行。

数据库迁移与回滚

Core 和运行环境各有自己的数据库结构。迁移在切换到新版本之前执行,而且(除一个例外)让之前的版本仍能运行,所以回滚通常只是换回之前的镜像。0.2.0 之后的迁移,见Core 的迁移 0030、0031、0032。

回到之前的版本

edge 会自动回滚:新版本没有报告正常,就换回之前的版本。要手动回滚,把服务固定在一个镜像上,它会以同样的步骤部署(备份、migrate up、切换、健康检查):

aishie-update --pin core sha256:<摘要>      # 摘要取自日志或 --status
aishie-update --pin core ghcr.io/aishie-education/aishie-core:X.Y.Z
  • 固定了的服务不再跟随通道,直到运行 aishie-update --unpin core。
  • 新的数据库结构会保留,之前的版本可以在它上面运行(Core 的迁移 0027 例外)。要回到比上一个版本更早的版本,就要恢复备份(见恢复备份)。
  • 不要运行 migrate down:它会删除数据。唯一的例外是回到 Core 的迁移 0027 之前(下一节)。

查看数据库结构的版本:

aishie core migrate version       # schema version N (embedded latest M)
aishie runtime migrate version

Core 的迁移 0027

迁移 0027 删除了之前的版本仍在读写的字段,是唯一一个让之前的版本无法运行的迁移。迁移停在 0027 之前的 Core,在有 0027 的数据库上会报告正常,却会让每个读取账号或文档版本的已登录调用失败。新服务器从第一次部署起就有 0027,所以只有在有人固定或设置了一个比 0027 更旧的 Core 镜像时,这一节才重要。

请让 Core 和运行环境跟随同一个通道,使用同一时期的版本:有 0027 的 Core 要求运行环境在试用文件转写的凭证时指明文件,较旧的运行环境会被拒绝(credential_rejected)。

aishie-update 在每个 Core 镜像接触数据库之前,先询问它的迁移到第几号,并在三种情况下保护数据库:

  • 新的 Core(有 0027)没有报告正常:不会换回 0027 之前的 Core。它继续运行,不记为失败,日志显示 not rolled back: sha256:…'s migrations stop at 26, before Core's migration 0027。查看 aishie logs core 找原因(通常是 env 文件),修正后运行 aishie compose up -d core。
  • 指定了 0027 之前的 Core(通过 --pin 或通道):在备份之前就拒绝,什么都不改,原来的 Core 继续运行,日志显示 refused: sha256:…'s migrations stop at 26, before Core's migration 0027, which the schema has。通道指向它时,每次运行都会拒绝(只记录一次),运行环境和前端照常更新。
  • 新的 Core(有 0027)seed 失败:之前的 Core 继续运行,但数据库此时已有 0027,它无法正常工作。日志会说明,新摘要记为失败。修正 seed 指出的问题后运行 aishie-update --retry core,或按下面的方法手动回滚。

手动回到 0027 之前(少见)

停止 Core,用有 0027 的镜像(正在运行的那个,aishie core 用的就是它)把数据库结构回退一级,再部署之前的版本。期间网站停止服务,包括 aishie-update 先做的备份在内:

aishie compose stop core
aishie core migrate down --yes      # says: schema version 26 (embedded latest 27)
aishie-update --pin core ghcr.io/aishie-education/aishie-core:<之前的版本>

在 stable 上,最后一步可改为把 CORE_IMAGE 设回之前的版本再运行 aishie-update。只运行这一次 migrate down:再多一次就会删除数据。回退时会从每个版本的文件和每个站内托管智能体的令牌补回之前版本读取的数据,但已清除的版本的文件类型和大小无法补回。

seed 失败之后,正在运行的已是 0027 之前的 Core,它的 migrate down 无法移除 0027(no migration found for version 27)。这时以日志中的摘要指定有 0027 的镜像:

aishie compose stop core
CORE_REF=ghcr.io/aishie-education/aishie-core@sha256:<有 0027 的摘要> \
  docker compose --project-directory /opt/aishie -f /opt/aishie/compose.yaml \
  run --rm --no-deps -T core migrate down --yes
aishie compose up -d core

Core 的迁移 0028 和 0029

这两个迁移不需要你做任何事,也不改动任何现有的数据,aishie-update 照常部署即可。它们在同一个 Core 版本中,按顺序运行。之前的版本可以在新的数据库结构上运行,所以回滚仍然只是换回之前的镜像(见回到之前的版本)。

  • 0028,要求修改:审批的人可以把提议退回,写明要修改什么;提出者再次提出时,可以指明修订的是哪一项(见用户指南的要求修改)。回滚到之前的版本后,网站不会再要求修改,已退回修改的提议显示为已结束。
  • 0029,回答依据的教材:智能体的回答可以列出它依据的课程教材,Core 把它们与回答一起保存,并按每位读者的权限显示(见用户指南的回答依据的教材)。在运行环境开始提供依据之前,回答没有任何依据,和以前一样。回滚到之前的版本后,网站不读写依据;这时仍在等待批准、而带有依据的回答提议,批准时会被取消,智能体会重新回答。

请让 Core、智能体运行环境和前端跟随同一个通道,使用同一时期的版本。前端 0.2.0 也在智能体的回答上提供“要求修改”,而不认识“要求修改”的运行环境(0.2.0 之前的),回答被退回修改后就不会再回答那个问题;所以运行环境要先于前端更新(见升级到 0.2.0)。运行环境自己的迁移 0015(记录被退回修改的回答)同样不需要你做任何事,也不改动任何现有的数据;不要为它运行 migrate down:运行环境的 migrate down 会把所有迁移都回退,删除它保存的一切。

不要为这两个迁移运行 migrate down:回退 0029 会删除所有回答的依据,回退 0028 会把所有退回修改的提议改为拒绝。

Core 的迁移 0030、0031、0032,以及运行环境的迁移 0016

这四个迁移在 0.2.0 之后的 main 上(见依据的版本),都不需要你做任何事,aishie-update 照常部署即可:Core 的三个按顺序运行,然后是运行环境的。之前的版本可以在新的数据库结构上运行,所以回滚仍然只是换回之前的镜像(见回到之前的版本)。

  • 0030,永久删除作业:有“管理作业”权限的人可以永久删除作业,连同它的提交、文件和成绩(见用户指南的永久删除作业)。迁移加上删除的记录、待删除文件的队列,以及被删除清空的操作的标记,不改动任何现有的数据。数据库的保护只为删除作业而放开:提交、成绩、提交和反馈的文件,只有删除作业时才会被删除;教材、作业说明和评分标准永远不会被删除。
  • 0031,小组作业:分组、小组和谁在何时属于哪个小组,小组的作业和小组成绩(见用户指南的小组作业与组员互评)。它为每一份现有的提交写一行“作业属于谁”的记录(就是那位学生),并把成绩指向它。
  • 0032,组员互评:互评的设置、每份互评和它的内容,以及成绩上由互评而来的调整。它不改动任何现有的数据。
  • 运行环境的 0016,OpenRouter 的上游路由:学校方案的模型可以带上 OpenRouter 的上游路由(见日常管理员指南的OpenRouter 模型的上游路由)。之前版本的运行环境不读这一列,调用 OpenRouter 时不会发送学校设置的路由(例如不会强制只用不保留数据的上游供应商),所以要等运行环境更新之后,管理员才设置上游路由。这套堆栈只有一个运行环境容器,aishie-update 会一次换掉它。

大型网站:索引

0030 创建九个索引,0031 重建一个成绩的索引。创建每个索引时,会短暂阻塞对那张表的写入,在学校的网站上只是一瞬间。数据非常多的网站,可以在部署之前以不阻塞任何操作的方式先创建它们,迁移便会跳过:Core 的 docs/deploying.md(Migration 0030、Migration 0031)列出要以数据库所有者身份逐条运行的 CREATE INDEX CONCURRENTLY 语句,以及之后要确认没有留下无效的索引。

删除的作业的文件

作业删除后,它的提交和反馈文件会在 Core 的后台任务下一次运行时(JOBS,每 JOBS_INTERVAL)从文件存放处(磁盘或存储桶)删除。存放处拒绝删除的,稍后再试(最多相隔一天),并记录在日志中(queued file)。所有 Core 都关闭了后台任务时,它们会留在队列中。注意:在删除之前做的数据库备份仍有那些记录,但恢复它不会找回已从存放处删除的文件(除非存储桶保留旧版本)。

回到之前的版本,以及 migrate down

  • 换回 0.2.0 的 Core 时,前端也要一并换回 0.2.0。0.2.0 没有删除作业、小组作业和组员互评的功能;已创建的小组作业,它读取时会失败,学生自己的作业和成绩则照常运行。再次更新后,一切照旧。
  • 不要为这些迁移运行 migrate down。0031 的回退在仍有任何小组作业时会拒绝运行,什么都不改,并说明 group work exists: delete the group assignments (assignment.delete) or restore from a backup taken before 0031;这时 migrate version 会停在 30 并标记为 dirty,运行 aishie core migrate force 31 恢复。即使能回退,也会失去分组、小组和分组记录,以及哪些作业是小组作业。0032 的回退会把每项互评调整改为评分者的加减分(分数不变),但失去互评的设置、每份互评、评语和系数。0030 的回退会失去删除的记录和待删除文件的队列。

迁移失败

日志显示 migrate up failed,journal 中有迁移的错误。原来的版本继续运行,新摘要记为失败。失败之前刚做了备份(core-deploy-*.dump 或 runtime-deploy-*.dump)。

每个迁移在一个事务中执行,失败通常不留下任何东西,但数据库结构会在失败的编号 N 处标记为 dirty:migrate version 会显示 DIRTY。Core 的 /healthz 在 dirty 时返回 503(原来的版本仍在处理请求);运行环境在 dirty 的结构上,任何版本都无法启动,但正在运行的会继续运行。

  1. 修正错误指出的原因。多数是迁移没有预料到的数据,那就要等新的镜像,通道会自己切换过去。
  2. 把失败之前的那个迁移记为最后完成的:
    aishie core migrate force <N - 1>
    aishie compose exec postgres psql -U postgres -d aishie_runtime \
      -c 'UPDATE schema_migrations SET version = <N - 1>, dirty = false'   # 运行环境没有 migrate force
  3. 再试:aishie-update --retry core(或 runtime)。

不确定失败的迁移留下了什么,就改为恢复它之前的备份。

备份与恢复

数据库每晚和每次部署前自动备份,但备份与数据库在同一块磁盘上。服务器之外的副本,要由你安排。

自动备份

  • 每次部署 Core 或运行环境之前:/var/backups/aishie/core-deploy-<时间>.dump 和 runtime-deploy-<时间>.dump,各保留最近十个。
  • 每晚:服务器时间 03:00(最多随机延迟 15 分钟),aishie-backup.timer 运行 aishie backup,写成 core-daily-N.dump 和 runtime-daily-N.dump,N 是星期几(1 至 7),所以各保留七个。
  • 立即备份:aishie backup。

每个备份都是在 PostgreSQL 容器中运行的 pg_dump -Fc,先写成 .part,完整后才改名,只有 root 可读。备份失败时,上星期同一天的那份仍会保留;journalctl -u aishie-backup 中有原因,通常是磁盘满了。

恢复备份

备份之后写入的一切都会丢失。停止服务,以备份重建数据库,再固定使用备份时正在运行的镜像(日志中有记录):

aishie compose stop core
aishie compose exec postgres dropdb -U postgres aishie_core
aishie compose exec postgres createdb -U postgres -O aishie_core aishie_core
aishie compose exec -T postgres pg_restore -U postgres --exit-on-error --no-owner --role=aishie_core -d aishie_core < /var/backups/aishie/<文件>.dump
aishie-update --pin core sha256:<当时的摘要>
aishie compose up -d core      # 那个摘要已是正在部署的镜像时,用它再启动

运行环境的步骤相同,把 core 换成 runtime、aishie_core 换成 aishie_runtime。运行环境已经发到 Core 的回答会留在 Core。

备份之后有人永久删除了作业的话,恢复的数据库会有那份作业和它的提交,但它们的文件可能已从文件存放处删除(见删除的作业的文件)。

恢复的 Core 数据库如果来自 SECRETS_KEY 轮换之前,要把当时的密钥放回 SECRETS_KEY_PREVIOUS 再运行 aishie core secrets rewrap(见轮换 SECRETS_KEY)。运行环境的数据库同样需要当时的 kek 密钥。

服务器之外要保存什么

定期把以下各项复制到别处:

  1. /etc/aishie/,加密:SIGNING_KEY、SECRETS_KEY、运行环境的密钥 runtime/secrets/kek/v1、数据库密码、运行环境的凭证,以及学校方案和运维者智能体的模型密钥。数据库的备份无法找回 SIGNING_KEY、SECRETS_KEY 和 kek:没有 SECRETS_KEY,网站的单点登录提供者的密钥打不开;没有 kek,运行环境保存的密钥读不到。运行环境的凭证丢失了,用 aishie runtime-credential 重新签发即可。
  2. /var/backups/aishie/:数据库。与 /etc/aishie 的副本分开存放:两者放在一起,就是全部机密。
  3. /srv/aishie/core/(文件存放在磁盘时):上传的文件。不要包括 /srv/aishie/core/blobs/exports/:导出的对话含个人数据,服务器在 EXPORT_TTL 后就会删除,备份不应保存得更久。
  4. 存储桶(用存储桶时):由服务商保存,aishie backup 不包括它。开启它的版本控制(或复制),再加一条生命周期规则,一段时间后删除旧版本,因为 Core 在正常运行中也会删除对象。

以下时候要再复制一次 /etc/aishie:每次轮换之后;setup-server.sh 说它加上了 SECRETS_KEY 或签发了运行环境的凭证之后。

轮换密钥

每把密钥的更换方法不同。有些可以随时更换,有些要新旧并存一段时间。每次更换之后,再复制一次 /etc/aishie。

一览

密钥放在哪里如何更换
SECRETS_KEYcore.env新旧并存,aishie core secrets rewrap(见下文)
kek/v1runtime/secrets/kek/加上 v2,aishie runtime keys rewrap(见下文)
运行环境的凭证runtime/secrets/core/agent_runtimeaishie runtime-credential,随时
数据库密码postgres.env 和 DATABASE_URL在数据库和两个文件中更改
SIGNING_KEYcore.env只在泄露时更改
存储桶密钥core.env 的 S3_ACCESS_KEY、S3_SECRET_KEY创建新密钥,换上,再删除旧密钥
学校方案的密钥runtime/secrets/school/keys/替换文件

轮换 SECRETS_KEY

SECRETS_KEY 加密网站的单点登录提供者的客户端密钥,而这些密钥只有它能解开,所以它从不直接替换。怀疑它泄露时:

  1. 在 core.env 中把新密钥放进 SECRETS_KEY,旧密钥放进 SECRETS_KEY_PREVIOUS:
    SECRETS_KEY=<新的:openssl rand -base64 32>
    SECRETS_KEY_PREVIOUS=<原来 SECRETS_KEY 的值>
  2. 以新密钥重建 Core,再把所有客户端密钥以新密钥重新加密:
    aishie compose up -d core      # Core 以新密钥加密,两把密钥都能解开
    aishie core secrets rewrap     # 把每个客户端密钥以新密钥重新加密
    secrets rewrap 会说明重新加密了多少个。
  3. 成功后,删去 SECRETS_KEY_PREVIOUS 一行,再运行 aishie compose up -d core,并再复制一次 /etc/aishie。
  • 在 rewrap 完成之前,网站的 登录方式 页把仍以旧密钥加密的提供者标示为“旧密钥”。页面上的提示写的是 aishie-core secrets rewrap;在这套堆栈中,命令是 aishie core secrets rewrap。
  • rewrap 失败、指出有提供者的密钥两把密钥都解不开:把当初加密它们的密钥也放进 SECRETS_KEY_PREVIOUS(多把以逗号分隔),重建 Core 再运行;或由管理员在网站上重新输入那些提供者的密钥。
  • 轮换之前的数据库备份仍以旧密钥加密。只要还保留这些备份,就要另外保存旧密钥,因为恢复时要把它放回 SECRETS_KEY_PREVIOUS 再 rewrap。
  • 不是 32 字节 base64 的密钥(例如粘贴时漏了一截,或用了 hex)会让 Core 无法启动,aishie logs core 会指出是哪一项。只有 SECRETS_KEY_PREVIOUS 而没有 SECRETS_KEY 也一样。
轮换期间的登录方式页:仍以旧密钥加密的提供者标示“旧密钥”。

轮换运行环境的密钥(kek)

运行环境把拥有者的模型密钥、网站添加的学校密钥和智能体的令牌加密保存在数据库中,每个机密有自己的数据密钥,而 kek/ 目录中的密钥用来包装这些数据密钥。KMS_KEY_ID 指定的那把用来加密新的机密;目录中其他密钥仍可解开它们当初加密的东西(名称以 . 开头的文件不算)。

检查每个机密都能解开:

aishie runtime keys check

更换密钥(例如能读取它的人离职,或按计划定期更换):加上新密钥,把 KMS_KEY_ID 指向它,重建运行环境,以新密钥重新包装所有机密,检查,然后才移除旧密钥:

(umask 077 && openssl rand -base64 32 > /etc/aishie/runtime/secrets/kek/.v2.new)
chown root:65532 /etc/aishie/runtime/secrets/kek/.v2.new && chmod 640 /etc/aishie/runtime/secrets/kek/.v2.new
mv /etc/aishie/runtime/secrets/kek/.v2.new /etc/aishie/runtime/secrets/kek/v2
sed -i 's|^KMS_KEY_ID=.*|KMS_KEY_ID=local:/secrets/kek/v2|' /etc/aishie/runtime.env
aishie compose up -d runtime
aishie runtime keys rewrap
aishie runtime keys check      # 每个机密都能解开,而且全部由 v2 包装
  • 保留 v1(和它的副本),直到比 rewrap 更早的数据库备份都轮替掉(一星期的夜间备份和最近十次部署的备份):它们的机密仍由 v1 包装。之后才运行 rm /etc/aishie/runtime/secrets/kek/v1。
  • 这些步骤依据运行环境本身的文档,换成了这套堆栈的路径;AIShie-Deploy 的 README 还没有收录。第一次操作之前,请先在可以丢弃的服务器上试一次。

其他密钥

运行环境的凭证

aishie runtime-credential,随时都可以(见运行环境在 Core 的凭证)。

数据库密码

PostgreSQL 只在创建数据卷时读取 postgres.env,所以新密码要同时在数据库和两个文件中设置:

aishie compose exec postgres psql -U postgres -c '\password aishie_core'   # 输入两次,不会被记录

再把它写进 postgres.env 的 AISHIE_CORE_DB_PASSWORD 和 core.env 的 DATABASE_URL,然后运行 aishie compose up -d core。运行环境相同(aishie_runtime、AISHIE_RUNTIME_DB_PASSWORD、runtime.env)。用 hex(openssl rand -hex 24)就不用在网址中转义。

SIGNING_KEY

正常情况下不要更改:Core 发出的每条上传和下载链接,以及进行中的单点登录,都会失效。ASSERTION_KEY 没有设置时由它派生,也会随之改变。泄露了就照样更改(在 core.env 中写入 openssl rand -hex 32 的结果,再运行 aishie compose up -d core),接受这些后果。

存储桶的密钥

  1. 在服务商处创建新密钥,写进 core.env 的 S3_ACCESS_KEY 和 S3_SECRET_KEY(含 $ 的密钥加单引号)。
  2. 运行 aishie compose up -d core,再运行 aishie storage check。
  3. 删除旧密钥。以旧密钥签名、已发出的链接随即失效;它们本来也只有几分钟有效。

学校方案的密钥

替换 /etc/aishie/runtime/secrets/school/keys/ 下的文件,再运行 aishie runtime check --live。

日志与监控

每个服务的日志、健康检查,以及运行环境的 Prometheus 指标。

日志

  • aishie logs core(或 runtime、web、caddy、postgres):显示最后 100 行并持续跟踪。加上其他选项时,它们直接传给 docker compose logs,例如 aishie logs --since 1h core。
  • Docker 为每个容器保留最多 5 个 20 MB 的日志文件。
  • Core:每个请求一行 JSON。
  • 运行环境:每个事件一行 JSON,只有 ID、数量、结果和时间,从不记录任何人写的内容、令牌或密钥。每次 OCR、转换、PDF 预览和转写各记一行。LOG_REDACT_EXTRA 可以再删去匹配的内容。
  • 更新:/var/log/aishie-update.log 每个结果一行(时间、服务、前后的摘要、结果);journalctl -u aishie-update 有每一步的输出,健康检查失败时还有新版本最后 30 行日志。
  • 备份:journalctl -u aishie-backup。

健康检查

命令响应
curl -s 127.0.0.1:8080/healthzCore:status、version、commit。数据库连不上、或数据库结构落后或处于 dirty 状态时返回 503
curl -s 127.0.0.1:9090/healthz运行环境:同样的字段;数据库连不上时返回 503
curl -s 127.0.0.1:8081/version.json前端:version 和 commit
curl -s https://aishie.example.edu/healthz从外部经 Caddy 到 Core:同时确认 DNS 和证书
aishie ps堆栈的容器及其状态
df -h /var/lib/docker /var/backups /srv/aishie磁盘空间:数据库、备份和上传的文件

在网站上,账号菜单的 关于 AIshie 也显示网页应用(前端的 /version.json)和服务器(Core 的 /healthz)的版本;读不到的写“未知”。登录页不再显示服务器的版本。

指标

Core 没有指标端点。运行环境在 127.0.0.1:9090/metrics 提供 Prometheus 格式的指标,供同一台机器上的 Prometheus 读取,或经 SSH 隧道读取;Caddy 从不对外转发它。

curl -s 127.0.0.1:9090/metrics
类别指标
回答answers_total、answer_latency_seconds、inbox_polls_total、presence_gap_seconds
模型llm_calls_total、llm_tokens_total、llm_cost_usd_total{key_source}、budget_exhausted_total
调用 Corecore_calls_total、long_polls、long_poll_fallbacks_total{why}、draft_writes_total
托管agents{state}、agent_tokens_issued_total、lease_takeovers_total
OCRocr_jobs_total、ocr_pages_total、ocr_jobs_running、ocr_jobs_waiting、ocr_requests_total 等
LibreOfficeoffice_conversions_total、office_requests_total、pdf_cuts_total 等
PDF 预览rendition_jobs_total{outcome,reason}、rendition_seconds、rendition_inflight、rendition_claim_errors_total
文件转写transcribe_jobs_total、transcribe_pages_total、transcribe_inflight、transcribe_claim_errors_total
教材搜索search_requests_total{result}、search_files_total{outcome}
  • 最值得留意的是 presence_gap_seconds:超过 60,Core 就把智能体显示为离开。
  • long_poll_fallbacks_total{why="cut"} 持续增加,表示运行环境与 Core 之间有东西提前切断了长轮询(见对外连接)。
  • budget_exhausted_total 数值大,表示回答经常碰到单次回答的预算上限;{budget="output_tokens"} 多,通常是会思考的模型用完了输出 token(见每个回答的预算)。

故障排除

先看三个地方:aishie-update --status、/var/log/aishie-update.log 和 journalctl -u aishie-update;再用 aishie logs SERVICE 查看服务本身的日志。无论哪一步出错,旧版本在重建之前都照常运行;每次运行都停在出错的那一步。

更新

  • migrate up failed:见迁移失败。
  • not healthy within 60 seconds,后面接着以下四种之一:
    • rolled back: sha256:… runs again:之前的版本已恢复并报告正常,新摘要记为失败。原因在 journal 中新版本的最后 30 行日志里。
    • not rolled back: …:迁移 0027 的情况,见Core 的迁移 0027。
    • rolled back to sha256:…, which does not report healthy either:两个版本都起不来,最可能是共用的设置有问题:env 文件,或运行环境的智能体设置。aishie logs core 会指出是哪一项。修正后运行 aishie compose up -d core,再运行 aishie-update --retry core 试用新版本。
    • nothing ran before it, and nothing runs now:服务的第一次部署没有起来,容器已移除。查看 journal,修正后运行 aishie-update --retry SERVICE。
  • refused: …'s migrations stop at 26:指定了 0027 之前的 Core,见Core 的迁移 0027。
  • skipped: it failed before:这是有意的,失败过的摘要不会每 5 分钟重试。通道指向新摘要时会照常部署;或者在服务器上修正原因后运行 aishie-update --retry SERVICE。固定了的服务(--status 显示 pinned:)在 --unpin 之前不会更新。
  • could not pull …:
    • denied 或 unauthorized:服务器还保留着一个过期的 ghcr.io 登录;运行 docker logout ghcr.io。
    • not found 或 manifest unknown:aishie.env 中的通道指向一个没有发布(或尚未发布)的镜像或标签,例如还没推出的正式版本,或拼写错误。
    • 其他(超时、connection refused、TLS 错误):服务器连不上 ghcr.io(DNS、防火墙、代理服务器),或 GitHub 的服务有问题。
    修正后运行 aishie-update。
  • another aishie-update, or aishie backup, has held the lock:正在部署或备份,或之前的运行被中断而进程还在:journalctl -u aishie-update -u aishie-backup、ps -ef | grep aishie。
  • CORE_IMAGE refused:通道不是它自己仓库的镜像;或者在 stable 上不是正式版本或摘要。
  • PostgreSQL is not up and healthy:aishie logs postgres。通常是磁盘满了:df -h /var/lib/docker /var/backups。
  • the backup of the database … failed:什么都没改,下次运行时再试。通常是磁盘满了。
  • could not pull the newest postgres:18 and caddy:2(setup-server.sh):Docker Hub 暂时限制了拉取(429 Too Many Requests)。设置脚本会用服务器上已有的镜像继续,稍后再运行可获取最新的镜像。

网站打不开或嵌入失败

  • Caddy 没有证书:aishie logs caddy。DNS 名称必须解析到这台服务器,服务商的防火墙必须开放 80 和 443(ufw 不影响 Docker 发布的端口)。
  • 学习管理系统无法在框架中显示 AIshie:浏览器控制台说页面拒绝被嵌入。用 curl -sI https://aishie.example.edu/ | grep -i content-security-policy 查看前端发出的响应头,学习管理系统的源要在其中。FRAME_ANCESTORS 没有用双引号时,Docker 会在第一个单引号处截断,响应头变成 frame-ancestors self,任何网站都无法嵌入。修正后运行 aishie compose up -d web。
  • 在框架中登录后无法保持登录:在 core.env 中设置 COOKIE_SAMESITE=none,再运行 aishie compose up -d core。Safari 和 iOS 上的浏览器无论如何都会拒绝。
  • 改了 FRAME_ANCESTORS 后前端无法启动:aishie logs web 中有 Caddy 的错误,值写成了多行。修正 aishie.env 中那一行,再运行 aishie compose up -d web。

单点登录

  • 登录页没有单点登录按钮:curl -s 127.0.0.1:8080/v1/auth/methods。"sso": null 表示没有提供者可用:Core 运行时的 core.env 没有 OIDC_ISSUER(改完要运行 aishie compose up -d core),网站的提供者也没有一个已启用。登录页可能会把旧答案缓存一分钟。
  • 网站无法添加提供者:secrets_key_missing:core.env 没有 SECRETS_KEY 或它没有值。没有那一行时,再运行 setup-server.sh(见更新 AIShie-Deploy 本身的文件),它会加上并重建 Core。有那一行但没有值,它不会改动:自己写入 openssl rand -base64 32 的结果,再运行 aishie compose up -d core。之后都要再复制一次 /etc/aishie。
  • 提供者显示“无法打开密钥”(secret_unavailable):它的密钥以 Core 已不再持有的密钥加密:rewrap 完成前就删了旧密钥、恢复了轮换前的数据库,或放回了比 SECRETS_KEY 更旧的 core.env。从 /etc/aishie 的副本把当时的密钥放进 SECRETS_KEY_PREVIOUS,再运行 aishie compose up -d core 和 aishie core secrets rewrap。没有副本的话,由管理员在网站上重新输入密钥。
  • Core 无法启动,日志提到 SECRETS_KEY 或 SECRETS_KEY_PREVIOUS:密钥不是 32 字节的 base64,或者只有 SECRETS_KEY_PREVIOUS。修正 core.env,再运行 aishie compose up -d core。
  • Core 无法启动,日志提到 OIDC_DISPLAY_NAME:超过 64 个字符或含有不可打印的字符。
  • 测试报告 issuer_address_not_allowed,或登录失败 sso_provider_unavailable:提供者位于私有地址,或服务器经代理服务器上网。见位于私有地址的提供者。
  • 按了按钮,却说还没有注册:单点登录不会创建账号。由管理员注册此人,再关联他的学校账号,或使用开启了“以邮箱关联”的网站提供者。

文件与存储桶

  • 上传在浏览器中失败,控制台提到 CORS:aishie storage cors 检查存储桶是否有网站需要的规则,并打印规则和设置的位置;aishie storage cors --apply --ask-keys 以有权限的密钥设置。规则写的是 https://HOST:改了服务器名称后要更新。
  • Core 无法启动:the S3 bucket is not reachable:Core 启动时会向存储桶请求一个对象。aishie storage check 用 core.env 中的密钥连接并说明被拒的原因:密钥被删或输错、区域错误、存储桶不存在。修正 core.env,再运行 aishie compose up -d core。
  • needs a Core whose help names S3_BUCKET_LOOKUP(setup-server.sh 或 aishie storage migrate):服务器上的 Core 太旧,无法以 --s3-path-style no 或在较新的 AWS 区域连接存储桶;什么都没改。先更新 Core,再运行一次。
  • aishie storage migrate 停止了:没有切换,core.env 不变,停止了的 Core 已重新启动。修正它指出的问题再运行,它只复制还没有的文件。如果它对每个文件都报告 the two sides differ,而存储桶以 SSE-KMS 加密,就加上 --size-only。could not pull rclone/rclone:Docker Hub 暂时限制,稍后再试。
  • Core did not report healthy on the other side:core.env 已放回,Core 以它启动,没有丢失任何东西,文件在两边都有。aishie logs core 说明 Core 为何无法在另一边启动。

智能体运行环境

  • AI 与文件页显示“这个服务器没有智能体运行环境”或“无法连接智能体运行环境”:用 aishie ps 查看 runtime 是否在运行,aishie logs runtime 查看原因,curl -s https://aishie.example.edu/runtime/api/v1/info 确认 API 经 Caddy 可以到达。运行环境重启时会短暂无法连接。
  • “你不是这个运行环境的管理员”:runtime.env 的 ADMIN_ACTOR_IDS 没有列出这位管理员(见谁可以管理运行环境)。
  • 运行环境没有托管任何智能体,日志说 Core 拒绝了它的凭证(401):凭证被撤销了(管理员在网站上撤销,或放回了轮换前的 /etc/aishie)。运行 aishie runtime-credential。不要手动编辑或清空那个文件。
  • the runtime was not given its credential for Core(setup-server.sh)或 Core issued no credential(aishie runtime-credential):Core 还没有部署。aishie-update --status 显示 Core 在运行后,运行 aishie runtime-credential。
  • 运行环境的 check 拒绝了设置:新版本不接受现在的智能体 YAML。aishie runtime check 以正在运行的版本说明原因,journal 中有新版本给出的原因。修正 YAML(两个版本都要接受),运行 aishie compose kill -s HUP runtime,再运行 aishie-update --retry runtime。
  • 文件转写显示“受阻”:“没有凭证”或“AIshie 拒绝了它的凭证”,由平台管理员在网站的 文件 标签页重新签发转写的凭证;“未选择模型”“所选模型无法使用”“今日页数已用完”也在那里处理。页面说服务器已停用文件转写或 OCR(ⓘ 写明“运行环境的 TRANSCRIBE=off”或“运行环境的 OCR=off”),是因为 runtime.env 中有 TRANSCRIBE=off 或 OCR=off。
  • 智能体在对话窗口显示为“暂停”,或在它的页面上只有“最后连接”:presence_gap_seconds 超过 60。查看 aishie runtime-status 和 aishie logs runtime。
  • 智能体经常回答“这题我没能完成,请试着问得具体一点。”:回答用完了预算,多数是会思考的模型用完了输出 token。查看 budget_exhausted_total{budget},再提高 budgets.per_answer.output_tokens 和 params.max_output_tokens,或调低推理强度(见每个回答的预算)。

Office 文件一直显示“正在转换为 PDF…”

用户会被告知通知管理员(见用户指南)。转换由运行环境进行,所以:

  1. aishie ps:运行环境在运行吗?
  2. aishie runtime check 的 renditions: 一行:是 on,还是说明了为何不能(例如没有凭证、OFFICE_PDF=off 或 RENDITIONS=off)?
  3. 没有有效的凭证就不会转换:aishie runtime-credential。
  4. 指标:rendition_jobs_total 有没有增加,rendition_claim_errors_total 有没有错误。
  5. 刚升级 Core 时,已有的 Office 文件会从最新的开始逐一转换(默认一次一个),新上传的优先,所以旧文件要等一会儿。

转换失败或被跳过时,文件只能下载。原因:

原因含义
password_protected受密码保护的文件(跳过)
unsupported根本不是 Office 文件(跳过)
too_large文件超过 100 MiB,或转换成的 PDF 超过 Core 的 RENDITION_MAX_BYTES(跳过)。调高这个上限要考虑磁盘或存储桶的空间
conversion_failedLibreOffice 打不开:文件损坏,或是受密码保护的旧式二进制文件
timeout超过 RENDITIONS_TIMEOUT
attempts_exhausted被领取了五次都没有完成

失败的文件可以由教学人员在网站上重新提交转换。

上限

  • 上传因大小被拒:单个文件受 MAX_UPLOAD_BYTES 限制;教材的一个版本受 DOCUMENT_MAX_FILES_PER_VERSION 和 DOCUMENT_MAX_VERSION_BYTES 限制;对话中的文件受 ATTACHMENT_MAX_BYTES、ATTACHMENT_MAX_PER_MESSAGE 和 ATTACHMENT_MAX_CONVERSATION_BYTES 限制(见core.env)。
  • 导出对话被拒:export_too_large:请管理员缩小范围,或调高 EXPORT_MAX_MESSAGES 或 EXPORT_MAX_BYTES。
  • 大型导出没有响应:网站前面你自己的代理服务器在 5 分钟内切断了请求。导出本身仍会完成,只是这次的响应丢失了。把代理服务器的时限设为 5 分钟以上。
  • 智能体回答很慢,core_calls_total 中有很多 rate_limited:智能体搜索大量教材时用尽了 Core 的调用上限。见课程教材搜索。

附录:命令与端口

命令一览

全部在服务器上以 root 身份运行。aishie core … 和 aishie runtime … 以正在运行的镜像、它的 env 文件、挂载和网络,启动一个一次性的容器来执行。

命令做什么
sh AIShie-Deploy/setup-server.sh HOST edge|stable设置新服务器,或安装较新的 AIShie-Deploy 文件
aishie-update立即检查并部署更新;--dry-run、--status、--retry、--pin、--unpin
aishie admin创建第一位管理员
aishie ps堆栈的容器
aishie logs SERVICE服务的日志(core、runtime、web、caddy、postgres)
aishie backup立即备份两个数据库
aishie compose …这个堆栈的 docker compose,例如 aishie compose up -d core
aishie runtime-credential为运行环境签发新的凭证,撤销之前的,重建运行环境
aishie runtime-status运行环境的 /status
aishie storage …文件存放位置;check、cors、migrate
aishie front-proxy按 aishie.env 的 FRONT_PROXY 和 FRONT_PROXY_ONLY 写 Caddy 的配置、获取 Cloudflare 的地址列表,并检查证书的验证路径(见放在 Cloudflare 后面)
aishie core migrate versionCore 数据库结构的版本
aishie core migrate force N把版本记为 N,不执行任何迁移(迁移失败后用)
aishie core token issue --actor ID --label L --days 90为 MCP 访问的智能体签发 API 令牌
aishie core secrets rewrapSECRETS_KEY 轮换的最后一步
aishie core helpCore 的命令和所有设置
aishie runtime check [--live]检查运行环境的设置;--live 连接 Core 并试用模型密钥
aishie runtime migrate version运行环境数据库结构的版本
aishie runtime keys check、keys rewrap检查和重新包装运行环境保存的机密
aishie runtime help运行环境的命令和所有设置

端口

端口谁在监听从哪里可以到达
80/tcp、443/tcp、443/udpCaddy互联网
127.0.0.1:8080Core只有服务器本身
127.0.0.1:8081前端只有服务器本身
127.0.0.1:9090运行环境的 /healthz、/metrics;/status 只响应运行环境容器自己的本机地址(用 aishie runtime-status)只有服务器本身
9091运行环境的 API只经 Caddy 的 /runtime/api/*
5432PostgreSQL只在堆栈的网络内