系統管理員指南

這份指南寫給架設和維護 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地區(任何分區的任何地區)、儲存桶、存取金鑰和密鑰s3.<地區>.amazonaws.com(中國為 .com.cn,其他分區用其網域)
r2Cloudflare R2帳戶 ID、儲存桶、R2 API 權杖的存取金鑰和密鑰<帳戶>.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,而且是英文或中文時,用那個語言;否則一律同時用英文和繁體中文,不會用簡體中文。

用完的是哪一項額度,決定貼出哪一句。代理自己的每日預算(budgets.per_agent_day 和 budgets.per_asker_day,或網站上的「代理每日預算」),以及一個人在學校金鑰上的額度(網站上的「每人額度」)用完時,貼出代理的提示 on_quota_text。學校方案的額度(school: 下的 per_owner_day、per_asker_day 和 per_day,或網站上方案的「每日額度」)用完時,貼出方案自己的提示,由 school: 下的 on_quota_text 設定。學校金鑰上的額度用完時,如擁有者在方案後面加上了自己的金鑰,就由那把金鑰回答,不貼提示。繁體中文的提示是:

情況設定提示
預算用完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只在堆疊的網絡內