给静态博客接入 AI,不是把 API Key 写进主题,而是在自己的服务器上放一个受保护的中间服务。本篇按 “准备 → 部署 → 接入 → 验证 → 日常维护” 的顺序,说明服务器和 Shoka 两边分别要做什么。
#前言
本文沿用《Shoka 主题:美化统计页面并新增多维图表》的教程组织方式:先说明效果和文件结构,再分步操作,每个阶段都有检查方法。所有域名、账号、路径和额度都是通用示例,不是某个站点的生产配置。
基础问答可以先做,联网搜索、宇宙导游、音乐控制和文字工坊随后再加。不要一次复制几十处修改后,才第一次检查页面能不能打开。
下面列的是完整方案的能力清单,不是 “原版 Shoka 一次复制就会拥有的所有模块”。建议先按基础路线验收,再增加可选能力:
| 路线 | 可以实现什么 | 额外前提 |
|---|---|---|
| 基础路线 | AI 问答、文章导读、选区解释、Markdown 回答、原文联动、七种文字工坊、服务管理 | 一个已经正常运行的 Shoka 博客;完成本篇的主题合并 |
| 联网路线 | 通过 SearXNG 获取实时搜索摘要 | 搜索容器与上游引擎实测可用 |
| 控制与漫游路线 | 页面导航、日夜切换;进一步控制音乐、阅读设置、查询统计 | 导航目录;其他操作分别需要对应的真实组件与接口 |
| 宇宙导游路线 | 推荐文章并在星系中高亮路线 | 先有博客宇宙页面及路线高亮接口 |
完整功能包括:
- 导航栏 “问问本站”:普通搜索保留,AI 搜索作为另一种入口;
- 右下角工具栏 AI 助手:点击打开对话框,再次点击关闭;
- 文章开头的 AI 导读,以及选中文字附近的解释窗口;
- AI 回答支持 Markdown、参考来源和跳回原文章节;
- 博客资料优先,资料不足时用通用知识补充;
- 可选的实时联网检索,不把 DeepSeek 网页端的搜索功能误认为 API 自带能力;
- 博客宇宙 AI 导游、自然语言导航和心情漫游;
- 自然语言控制音乐、页面、阅读设置和日夜模式,查询公开统计;
- 七种文字工坊:文字分岔剧场、措辞试衣间、观点圆桌、线索工作台、回忆编织机、平行世界档案馆、思想考古;
- 中文
ai管理命令、额度与限流、异常冷却、本地用量告警、知识同步、开机自启和证书续期。
本文不包含球形悬浮助手、唱片顾问、AI 配色实验室,也不把 AI 当成可以随意执行命令的管理员。
#一、先理解:到底部署了什么
#1. 不是在小服务器上安装一个大模型
这里部署的是 “AI 中间服务”,不是 DeepSeek 模型本体。
访客浏览器 | |
│ HTTPS:问题、必要上下文 | |
▼ | |
ai.example.com / Nginx | |
│ 只转发到本机 | |
▼ | |
127.0.0.1:3001 / Node.js 服务 | |
├─ 检查输入、额度、并发与来源 | |
├─ 检索博客公开知识索引 | |
├─ 可选:查询本机 SearXNG | |
└─ 使用服务器上的 API Key 请求 DeepSeek | |
│ | |
▼ | |
流式回答 → 浏览器安全渲染 |
一台 2 核、2 GB 内存的服务器可以作为这个方案的起点,因为主要推理在模型服务商那里完成;加入搜索容器后,仍需要观察内存和负载。这不是本地运行大模型的硬件建议。
#2. 博客、AI、评论是三个不同的部分
| 部分 | 负责什么 | 是否需要这次迁移 |
|---|---|---|
| Hexo / Shoka | 生成文章、按钮、对话框和公开索引 | 修改主题与构建脚本 |
| 自己的 AI 服务 | 保护密钥、调用模型、限流、同步知识 | 本篇部署的主要对象 |
| 已有 Waline | 评论、浏览量等已有数据 | 不因接入 AI 而必须迁移 |
AI 查询浏览量、评论数时,浏览器读取已有的公开接口;AI 服务不需要你的 Waline 管理员密码、数据库连接串或后台令牌。
#3. 静态网页里不能藏住一个可用的密钥
_config.yml 可以保存公开地址和开关,不能保存准备在浏览器使用的真实 API Key。把密钥编码、混淆或拆成几段,也不能阻止访客从网页和网络请求中恢复它。
本方案的密钥放在服务器的 /etc/blog-ai/config.json ,不进入 source/ 、 public/ 、Git 仓库或 CDN。
#二、开始前准备
#1. 需要准备的东西
- 一个可以正常生成和访问的 Hexo + Shoka 博客;
- 一台 Ubuntu Server 24.04 LTS 服务器,并能进入终端;
- 一个可管理解析记录的域名;
- 一个 DeepSeek API Key,以及可用的 API 账户;
- 本机的 Node.js、终端、文件编辑器和博客备份;
- 本文的通用配套源码。
国内服务器正式对外提供网站或接口服务前,还应向服务商确认备案、接入及服务类型要求。不要只因为某个端口能访问,就认为其他要求已经满足。
#2. 先约定示例名称
| 本文写法 | 你需要替换成什么 |
|---|---|
blog.example.com | 你真正的博客域名 |
ai.example.com | 你准备给 AI 使用的子域名 |
203.0.113.10 | 你的服务器公网 IP;本文这个是文档示例地址 |
ubuntu | 服务器实际登录用户名 |
/opt/blog-ai | AI 程序安装目录,可直接采用此通用目录 |
blog-ai.service | 系统服务名称,可直接采用 |
ai | 服务器上的中文管理命令 |
example.com 和 203.0.113.10 只是占位示例,不能直接作为真实部署地址使用。下面出现的 “在本机执行” 和 “在服务器执行” 也不要混淆。
#3. 下载配套源码
本版 ZIP 的 SHA-256 是:
bb744e25a2f65657db59a6476ef6fb894ec3444ba257f8ad5fa39fbc626f5f2c |
Windows PowerShell 可用 Get-FileHash .\shoka-ai-tutorial-v4.zip -Algorithm SHA256 检查;Linux 可用 sha256sum shoka-ai-tutorial-v4.zip 。大小写不影响比较。不同则先重新下载,不要继续运行包里的程序。
配套包只包含本教程需要的通用 AI 源码、测试和合并片段,不包含整个博客、真实文章、用户信息、密钥、数据库配置、生产额度文件和历史更新包。
解压后,在配套包根目录的本机终端执行以下命令。先把两个地址换成自己的真实域名:
node configure-examples.mjs https://blog.example.com https://ai.example.com |
命令会拒绝未替换的 example.com 占位地址。它只修改配套包清单里的示例文件,不修改你已有的博客,也不要求输入 API Key。换域名时,从原压缩包重新解压,再执行一次即可。
这个步骤很重要:博客索引中的 site 、服务端允许的博客来源、知识下载地址和浏览器 API 地址必须一致。只改 Nginx 的域名是不够的。
#4. 配套文件结构
shoka-ai-tutorial/ | |
├── README.md | |
├── manifest.json | |
├── configure-examples.mjs | |
├── server/ 上传到服务器的后端源码 | |
│ ├── src/ 模型请求、知识检索、安全与各项功能 | |
│ ├── bin/ 管理命令、知识校验、安全检查 | |
│ ├── deploy/ systemd、Nginx、续期与同步模板 | |
│ ├── data/blog-knowledge.json 空的合法示例索引,不是真实文章 | |
│ ├── test/ 不调用真实模型的自动化测试 | |
│ └── package.json | |
├── blog/ 按对应目录新增到博客 | |
│ ├── lib/ | |
│ ├── scripts/ | |
│ ├── source/text-workshop/ | |
│ └── themes/shoka/ | |
│ ├── layout/text-workshop.njk | |
│ └── source/{js,css}/ | |
└── integration/ 需要合并的片段,不是独立安装脚本 | |
├── config.js | |
├── theme-config.yml | |
├── ai-loader.js | |
├── ai-entry.js | |
├── site-control-bridge.js | |
└── workshop-shell.css |
#5. 哪些文件新增,哪些需要合并
| 位置 | 操作 | 注意 |
|---|---|---|
配套包 server/ | 新安装到服务器 | 不上传博客根配置 |
博客 lib/ai-*.cjs 、工坊目录脚本 | 新增或比较后合并 | 同名文件不能盲目覆盖 |
博客 scripts/ai-*.js | 新增构建钩子 | scripts/ 只放可执行的构建脚本 |
| 主题独立 AI JS / Stylus | 新增 | 不修改原 Markdown 扩展语法 |
主题 _app/ 、脚本生成器、模板 | 手动合并 | 保留原搜索、音乐、评论和 PJAX |
public/ | 构建后生成 | 不是编辑源文件的地方 |
本篇以站点放在域名根目录 / 、文章采用 /posts/.../ 路径的改造版 Shoka 为参照。原版 Shoka 或其他修改分支的函数名、模板位置可能不同。基础问答不依赖统计和音乐;进阶功能必须先接好实际组件接口,不能假定复制一个文件就拥有原项目的全部非 AI 模块。
这里的 “从零” 指从空 AI 服务器开始接入,不包含从零安装整个 Hexo 主题。 如果还没有能正常生成的博客,先按 原版 Shoka 的安装说明完成主题、Markdown 渲染器及其原有插件。已有博客不要为了本文更改永久链接,避免原文章地址失效。
后文同时给出原版 Shoka 0.2.5 的具体合并位置;其他分支先比较差异。主题模板和音乐、统计接口可能不同,这一部分仍是代码集成,不是纯粹复制几行服务器命令。不能保证任意分支、任意网络环境都无需适配。
本文配套代码已做通用域名替换、隔离单元测试,以及原版主题模板内的基础 AI 接入测试;文章在电脑、平板、手机尺寸下检查显示与跳转。浏览器使用测试响应验证流程,不代表已经在每位读者的空服务器上申请过证书、调用过真实模型。DNS、HTTPS、模型账户、搜索上游和已有主题差异,仍按每一节的检查结果验收,不作 “一次轻松完美完成” 的承诺。
#三、服务器部署:从一个空系统开始
#第 1 步:登录服务器并更新系统
使用服务商网页终端,或在本机通过 SSH 密钥登录。下面的操作都在服务器终端完成。
sudo apt update | |
sudo apt upgrade | |
sudo apt install nginx certbot python3 curl xz-utils unzip openssl dnsutils ca-certificates unattended-upgrades |
升级过程中若询问是否替换已有配置,先确认文件用途,不要盲目选 “全部覆盖”。已有业务的服务器应先做快照或备份,并安排维护时间。
检查磁盘和内存:
free -m | |
df -h / | |
sudo dpkg --audit |
dpkg --audit 没有输出通常表示没有发现未完成的包安装。若系统提示需要重启,先结束安装、保存文件,再选择合适时间重启,随后重新进入终端。
Ubuntu 支持自动安装配置范围内的更新,不能仅看到定时器处于 active 就认为所有安全更新已经配置好;可按 Ubuntu 自动更新文档检查允许来源和日志。
#第 2 步:配置域名和云防火墙
在域名解析管理中新增记录:
| 类型 | 主机记录 | 记录值 |
|---|---|---|
| A | ai | 你的服务器公网 IPv4 地址 |
不要修改博客、图床或 CDN 已有的记录。没有配置 IPv6 的服务器,也不要顺手添加一个错误的 AAAA 记录。
在服务器的云防火墙中允许 TCP 80、443。它们用于网站访问、HTTP 跳转和证书验证。
不要向公网开放 3001 和 8088。 后端和搜索服务只给本机使用。
SSH 22 端口属于管理通道:需要 SSH 时只允许自己的来源 IP,并使用密钥;只用云厂商终端时,先确认该终端不依赖 SSH、关掉 22 后仍能重新登录,再调整规则。不要照着教程直接把唯一的管理入口封掉。
可以在本机或服务器查询解析:
nslookup ai.example.com |
预期结果是自己的公网 IP。解析修改可能需要等待缓存到期。
#第 3 步:安装适合服务的 Node.js
后端配套代码要求 Node.js 24.19.0 及以上的 24.x 版本。本文核对时 Node.js 官方下载页提供的 24.x LTS 是 v24.21.0 ;未来安装时应重新确认同一主版本的安全补丁,不要直接切换到未经测试的新主版本。
先查看 CPU 架构:
uname -m |
x86_64:下面使用linux-x64;aarch64:把下面的node_arch=linux-x64改成node_arch=linux-arm64。
逐段执行,不要省略校验:
node_version=v24.21.0 | |
node_arch=linux-x64 | |
node_archive="node-${node_version}-${node_arch}.tar.xz" | |
sudo install -d -m 0755 /opt/blog-ai /opt/blog-ai/runtimes /opt/blog-ai/releases | |
sudo install -d -m 0700 /opt/blog-ai/downloads /etc/blog-ai |
sudo curl --fail --show-error --location --connect-timeout 10 --max-time 180 \ | |
"https://nodejs.org/dist/${node_version}/${node_archive}" \ | |
-o "/opt/blog-ai/downloads/${node_archive}" | |
sudo curl --fail --show-error --location --connect-timeout 10 --max-time 60 \ | |
"https://nodejs.org/dist/${node_version}/SHASUMS256.txt" \ | |
-o /opt/blog-ai/downloads/SHASUMS256.txt |
sudo sh -c 'cd /opt/blog-ai/downloads && grep -F " $1" SHASUMS256.txt | sha256sum -c -' sh "$node_archive" |
下载目录是 root 所有、 700 ,所以这里把进入目录和校验都交给 sudo 执行,不要自行把目录改成 777 。必须看到对应文件 OK 。失败就停下,不要解压一个未通过校验的包。这里验证下载完整性;需要更强的来源验证时,按官方说明验证签名后的校验文件。
sudo tar -xJf "/opt/blog-ai/downloads/${node_archive}" \ | |
-C /opt/blog-ai/runtimes --no-same-owner | |
sudo ln -s "/opt/blog-ai/runtimes/node-${node_version}-${node_arch}" /opt/blog-ai/runtime | |
/opt/blog-ai/runtime/bin/node --version |
此处 ln -s 用于首次安装;如果 /opt/blog-ai/runtime 已存在,先确认其实际指向,不要直接覆盖别人的运行环境。
如果服务器下载很慢,可以从本机下载同一个官方安装包与校验文件,上传后再在服务器校验。不要为了快,执行不明网站给出的 root 安装脚本。
#第 4 步:创建低权限运行用户
sudo useradd --system --home-dir /var/lib/blog-ai --shell /usr/sbin/nologin blog-ai |
如果提示用户已经存在,先确认它确实属于这套服务,不要删除用户重建。
这个用户只负责运行服务,不是用来登录服务器的管理员。程序、管理脚本和密钥文件由 root 管理;运行用户不能随意修改它们。
#第 5 步:上传后端源码
先确认配套包已经在本机完成域名替换。
在配套包根目录的本机终端打包:
tar -czf blog-ai-server.tar.gz server |
只打包 server/ ,不要把整个博客、 node_modules 、 .env 或自己的其他文件一起压进去。
可通过云厂商终端的上传功能,将它放到服务器的用户目录;使用 SSH 的读者也可从本机执行:
scp blog-ai-server.tar.gz ubuntu@203.0.113.10:/home/ubuntu/ |
这行需要替换用户名和 IP。不使用 SSH 的读者不用为上传临时开放 22。
回到服务器终端,先检查压缩包目录:
tar -tzf /home/ubuntu/blog-ai-server.tar.gz | head -20 |
应以 server/ 为顶层,没有 /etc/... 这样的绝对路径或 ../ 路径。只解压自己刚刚打包、确认来源的文件。
tutorial_stage=$(mktemp -d /tmp/blog-ai-tutorial.XXXXXX) | |
tar -xzf /home/ubuntu/blog-ai-server.tar.gz -C "$tutorial_stage" --no-same-owner | |
sudo install -d -m 0755 /opt/blog-ai/releases/tutorial-v1 | |
sudo cp -a "$tutorial_stage/server/." /opt/blog-ai/releases/tutorial-v1/ | |
sudo chown -R root:root /opt/blog-ai/releases/tutorial-v1 | |
sudo find /opt/blog-ai/releases/tutorial-v1 -type d -exec chmod 0755 {} + | |
sudo find /opt/blog-ai/releases/tutorial-v1 -type f -exec chmod 0644 {} + | |
sudo ln -s /opt/blog-ai/releases/tutorial-v1 /opt/blog-ai/current |
这也是首次安装流程。已经存在 current 时,不要继续覆盖,转到后面的更新章节。
后端运行时使用 Node.js 内置模块,不需要在服务器额外安装 OpenAI SDK、Redis、数据库或完整博客的依赖。
#第 6 步:先跑不收费的代码测试
cd /opt/blog-ai/current | |
/opt/blog-ai/runtime/bin/node --test test/*.test.mjs | |
python3 -m unittest discover -s test -p 'test_*.py' |
这批配套测试使用隔离的本机服务和模拟模型响应,不读取真实 API Key,也不发送真实模型请求。任何测试失败时先处理,不要只因为最后一行看起来像正常输出就忽略错误。
#第 7 步:初始化私有配置与系统服务
初始化配置:
sudo python3 /opt/blog-ai/current/bin/ai-admin.py init |
初始化会生成管理凭据,默认不会对访客开放收费调用。不要把生成的配置内容复制到文章、聊天或截图里。
安装服务和管理命令:
sudo install -m 0644 /opt/blog-ai/current/deploy/blog-ai.service \ | |
/etc/systemd/system/blog-ai.service | |
sudo install -m 0755 /opt/blog-ai/current/deploy/ai /usr/local/bin/ai | |
sudo systemctl daemon-reload | |
sudo systemctl enable --now blog-ai |
systemd 配置的重要部分是:
[Service] | |
User=blog-ai | |
Group=blog-ai | |
WorkingDirectory=/opt/blog-ai/current | |
ExecStart=/opt/blog-ai/runtime/bin/node --max-old-space-size=256 src/main.mjs | |
LoadCredential=ai-config:/etc/blog-ai/config.json | |
StateDirectory=blog-ai | |
StateDirectoryMode=0700 | |
UMask=0077 | |
NoNewPrivileges=yes | |
ProtectSystem=strict | |
ProtectHome=yes | |
PrivateTmp=yes | |
PrivateDevices=yes | |
MemoryMax=512M |
这是说明用的节选,安装时用配套包中的完整文件,不要只拿这几行替换整个服务。
LoadCredential 让运行进程通过 systemd 获得只读凭据,而不是把 API Key 放在进程命令行里。真实配置目录应为 root 所有、 700 ;配置文件应为 root 所有、 600 。
检查:
systemctl is-active blog-ai | |
curl --fail --silent --show-error --max-time 10 http://127.0.0.1:3001/healthz | |
ai status |
预期是 active 、 {"ok":true} ,以及尚未对访客开放的状态。不要在这一步期待 AI 已经会回答。
#第 8 步:私下填写 DeepSeek API Key
sudo python3 /opt/blog-ai/current/bin/ai-admin.py set-key |
在提示处粘贴自己的 Key 并回车。输入不会回显,终端里看不到星号也正常。
不要使用 ai ... sk-真实密钥 这样的命令,也不要用 echo 把密钥写到文件:它可能进入终端历史、日志或截图。
配套服务使用 DeepSeek 的 Chat Completions 接口并流式输出。模型名称和接口形式应以 DeepSeek 官方接入文档为准,后面会通过 ai models 查询可用名称,不把模型名写死在文章操作里。
填写 Key 不会自动对访客开放,也不会为了 “检测连接” 自动请求模型。
#第 9 步:先配置 HTTP,再申请 HTTPS
先安装只处理证书验证的 HTTP 模板:
sudo install -d -m 0755 /var/lib/letsencrypt | |
sudo install -m 0644 /opt/blog-ai/current/deploy/nginx-http.conf \ | |
/etc/nginx/sites-available/blog-ai | |
sudo ln -s /etc/nginx/sites-available/blog-ai /etc/nginx/sites-enabled/blog-ai | |
sudo nginx -t | |
sudo systemctl reload nginx |
检查模板中的 server_name 已经是自己的 AI 域名。不要同时在两个 Nginx 配置里为同一域名放置冲突的 server。
确认域名解析和 TCP 80 可用后执行:
sudo certbot certonly --webroot -w /var/lib/letsencrypt -d ai.example.com |
按终端提示填写自己的邮箱、阅读并确认服务条款。证书申请会产生公开的域名证书记录;不要给不属于自己的域名申请证书。
成功后,再切换到 HTTPS 模板:
先执行 sudo certbot certificates ,确认实际的 Certificate Path 与 Private Key Path。首次安装通常是 /etc/letsencrypt/live/你的AI域名/ ;如果已经有同名证书,Certbot 可能使用带 -0001 的目录。此时先在配套 deploy/nginx-https.conf 中把 ssl_certificate 、 ssl_certificate_key 改成实际路径,再安装模板。证书申请失败就保留 HTTP 配置,不要提前启用引用不存在证书的 HTTPS 配置。
sudo install -m 0644 /opt/blog-ai/current/deploy/nginx-https.conf \ | |
/etc/nginx/sites-available/blog-ai | |
sudo nginx -t | |
sudo systemctl reload nginx |
模板把 443 的请求转发到 127.0.0.1:3001 ,限制请求体和连接数,并关闭流式回答的响应缓存与缓冲。HTTP 的 /api/ 不接受 API 请求。
对外检查:
curl --fail --silent --show-error --max-time 15 https://ai.example.com/healthz | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/api/public/status |
域名根路径 / 返回 404 是可以的:这里不是管理网页,真正的健康地址是 /healthz 。
#第 10 步:设置证书自动续期
安装续期后重新加载 Nginx 的钩子:
sudo install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy | |
sudo install -m 0755 /opt/blog-ai/current/deploy/renew-nginx.sh \ | |
/etc/letsencrypt/renewal-hooks/deploy/blog-ai-nginx | |
sudo systemctl enable --now certbot.timer | |
sudo certbot renew --dry-run | |
systemctl list-timers certbot.timer |
钩子先执行 nginx -t ,配置正确才 reload。 --dry-run 是续期演练;若要专门检查钩子,可在确认配置正常后手动执行它,而不是把模拟续期成功当作所有钩子都已验证。
实际证书有效期以 sudo certbot certificates 为准,不用记一个固定的 “180 天”。保留证书验证路径、正确 DNS 和 TCP 80,定期检查自动续期日志。Certbot 官方说明
#四、博客接入:不要动密钥,只加入界面和公开索引
#第 1 步:确认博客能正常构建
以下操作回到本机博客根目录。这个目录通常包含 _config.yml 、 package.json 、 source 和 themes 。
先备份主题、站点配置和文章,或提交一次自己的 Git。然后执行:
npm install | |
npx hexo generate --bail |
先保证原博客可以生成,再引入 AI。不要在已有模板报错时一边升级整套依赖、一边修改 AI,排查会很困难。
配套构建脚本需要以下额外依赖;已经安装的项目先比较版本,不必重复升级全部依赖:
npm install cheerio moment markdown-it --save | |
npm install esbuild --save-dev |
本教程的 AI Markdown 渲染器使用 markdown-it 分词后创建允许的 DOM,不直接把模型内容当 HTML 插入页面。它与文章原来的 Markdown 渲染器是两个用途,不要求替换原渲染器或删除已有插件。
本机也建议使用经过测试的 Node.js 24.x,避免新的 cheerio 、 esbuild 与旧 Node 不兼容。不要仅凭服务器已经装好 Node,就忽略本机的 node --version 。
如果是全新博客,可在站点根配置使用 url: https://自己的博客域名 、 root: / 、 permalink: posts/:title/ ,不需要为了路径另外安装短链接插件。已有站点只在确认兼容后调整路径检查,不随意改变原永久链接。
#第 2 步:复制构建模块和独立前端文件
把配套包 blog/ 里的内容,按相同相对目录新增到自己的博客根目录。
主要文件的作用:
| 文件 | 作用 |
|---|---|
lib/ai-knowledge.cjs + scripts/ai-knowledge.js | 生成公开知识索引 |
lib/ai-markdown.cjs + scripts/ai-markdown.js | 把安全 Markdown 渲染器打包成本地资源 |
lib/text-workshop-catalog.cjs + 对应脚本 | 生成工坊可选择的公开文章摘录 |
lib/ai-control-catalog.cjs + 对应脚本 | 生成可导航页面、公开友链和订阅目录 |
lib/pageview-catalog.cjs | 提取已接入浏览量计数的公开页面信息 |
themes/shoka/source/js/blog-ai.js | 对话、流式回答、连接状态、引用与续答 |
ai-actions.js / ai-playground.js | 确认后执行控制;导航与心情路线 |
text-workshop.js | 七种文字玩法、草稿和记录 |
对应 .styl 文件 | Shoka 配色、布局、移动端与减弱动效适配 |
如果已有 scripts/pageview-catalog.js 正在生成 ai-control-catalog.json ,不要再同时安装配套的同名功能钩子。一个目录文件由一个构建入口生成即可。
#第 3 步:加入主题公开配置
修改主题配置 themes/shoka/_config.yml ,合并:
blog_ai: | |
enable: true |
这里的开关控制博客入口是否展示,不是服务器收费开关。
然后找到主题脚本生成器,当前参考结构是:
themes/shoka/scripts/generaters/script.js |
在它生成前端 CONFIG 的对象中合并配套 integration/config.js 的字段:
blogAI: { | |
enabled: theme.blog_ai && theme.blog_ai.enable === true, | |
endpoint: 'https://ai.example.com', | |
siteUrl: config.url | |
} |
注意逗号:这是对象的一项,不是可以单独运行的完整 JS 文件。 endpoint 应使用前面已替换好的真实 AI 地址, config.url 应是站点根配置中自己的正式博客 URL。
这里不能出现 apiKey 、 adminToken 、数据库密码或验证码服务密钥。生成后的 CONFIG 对所有访客可见。
#第 4 步:接入按需加载和页面入口
配套的 integration/ 文件属于主 app 同一作用域里的合并片段,不能各自作为独立 <script> 加进页面。它们需要访问主题已有的 CONFIG 、主题切换函数、工具栏和 PJAX 状态。改造版如果用 IIFE 包裹 app,它们也必须放在同一个 IIFE 中。
将文件复制到主题的 _app/ 目录,并起对应名称:
integration/ai-loader.js → themes/shoka/source/js/_app/ai-loader.js | |
integration/ai-entry.js → themes/shoka/source/js/_app/ai.js |
在 app 的构建文件列表中,把它们放到已有工具函数、主题切换、播放器和搜索代码之后,PJAX 初始化之前。例如保留原列表,仅增加:
'themes/shoka/source/js/_app/ai-loader.js', | |
'themes/shoka/source/js/_app/ai.js', |
上面是使用完整文件路径的构建列表写法。原版 Shoka 不采用这种列表:它在 themes/shoka/scripts/generaters/script.js 中遍历模块名,再自行拼接 _app/ 路径。原版应把这一行的数组改成:
['utils', 'dom', 'player', 'global', 'sidebar', 'page', | |
'ai-loader', 'ai', 'pjax'].forEach(function(item) { | |
text += fs.readFileSync('themes/shoka/source/js/_app/' + item + '.js').toString(); | |
}); |
不要把完整路径填进这个模块名数组,否则会生成重复路径并报找不到文件。
参考顺序是:
工具函数与 DOM → 音乐、主题、搜索 → AI 加载器 | |
→ 可选的站点控制桥接 → AI 入口 → PJAX 初始化 |
在已有的首次页面初始化入口调用:
initBlogAI(); | |
mountAIWorkshop(); |
initBlogAI() 内部防止重复注册。配套工坊加载器已有 PJAX 监听,不要再到处重复添加一份监听。
原版的具体位置:在 themes/shoka/source/js/_app/pjax.js 的 siteRefresh() 中、 lazyload.observe() 后加入这两句。此时 siteInit() 已经先执行 domInit() ,右下角 toolBtn 、 toolPlayer 才存在。不要直接在 app 文件末尾立即调用,也不要提前放到 domInit() 之前。
如果你的主题已经有 ShokaFeatures 和按需模块注册表,优先把五项 AI 模块合并到原加载器,避免维护两个加载系统:
| 模块 ID | JS / CSS 文件名 | 配套包导出的全局对象 |
|---|---|---|
blog_ai | blog-ai | ShokaBlogAI |
ai_markdown | ai-markdown | ShokaAIMarkdown |
ai_actions | ai-actions | BlogAIActions |
ai_playground | ai-playground | BlogAIPlayground |
text_workshop | text-workshop | ShokaTextWorkshop |
名称需要成套对应,不能把另一个主题分支的对象名只改一处。配套兼容加载器只处理 AI 模块;阅读设置等原有功能仍交给原主题加载器。
本篇参考的工具栏变量是 toolBtn 、 toolPlayer ,提示函数是 showtip ,主题切换是 changeTheme 与 store.set 。原版或其他分支没有这些名称时,先找到对应组件做映射,不能在报 “未定义” 后随便创建一个同名空函数。
原版 0.2.5 具备以上基础变量与函数,但没有改造版的音乐、阅读和统计扩展接口。先只接通基础对话、导读、选区和工坊;没有接好控制桥接时不要把所有控制选项当成可用功能。
#第 5 步:先加载小样式,再延迟加载大模块
AI 按钮、对话框外壳和选区浮钮需要首屏就有尺寸,不能等用户点击后才获得基础样式。
在主题的全站样式引入处新增入口样式链接,模板示意如下:
{% if theme.blog_ai.enable %} | |
<link rel="stylesheet" href="{{ url_for(theme.statics + 'css/ai-tutorial-entry.css') }}"> | |
{% endif %} |
ai-tutorial-entry.styl 由 Hexo 生成对应 CSS。完整对话组件、工坊和 Markdown 渲染器仍在需要时加载。
如果站点已有内容指纹资源映射,沿用原来的资源 helper,不要把带指纹文件名写死。基础按钮不能使用一个没有宽高的 SVG,否则慢网速下可能短暂出现浏览器默认的巨大图标。
原版可以在 themes/shoka/layout/_partials/head/head.njk 原全站样式附近,使用已有 _css('ai-tutorial-entry.css') helper。模板写法如下;它会沿用原主题的 statics 和 css 配置,不要另外拼一个可能重复斜杠的地址:
{% if theme.blog_ai.enable %} | |
{{ _css('ai-tutorial-entry.css') }} | |
{% endif %} |
若启用了 CSP,在原来的 connect-src 中加入自己的 AI HTTPS 地址;保留原评论和其他必要来源。不要为接入 AI 把整个 CSP 改成通配符,也不需要允许浏览器直接连接模型 API。
#第 6 步:导航栏加入 “问问本站”
找到导航栏右侧的模板,当前参考位置是:
themes/shoka/layout/_partials/header.njk |
在普通搜索按钮旁新增 AI 入口,不删除原搜索:
{% if theme.blog_ai.enable %} | |
<li class="item blog-ai-nav"> | |
<button class="blog-ai-nav__button" type="button" | |
data-blog-ai="ask" aria-expanded="false" aria-label="AI 搜索 · 问问本站"> | |
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" | |
stroke="currentColor" stroke-width="1.9" aria-hidden="true"> | |
<path d="m12 3 2.6 6.4L21 12l-6.4 2.6L12 21l-2.6-6.4L3 12l6.4-2.6Z"/> | |
</svg> | |
<span class="blog-ai-nav__label">问问本站</span> | |
</button> | |
</li> | |
{% endif %} |
普通搜索适合精确找标题、关键词,不调用模型;AI 搜索适合 “我想了解某种主题,应该先读什么”。两者不要混成同一个必须联网才能使用的搜索框。
保留上面的 SVG 与明确宽高:小屏幕会隐藏文字标签、保留图标;如果只复制文字 span,小屏幕可能出现一个没有内容的按钮。
右下角工具栏入口由 ai-entry.js 创建:点击打开聊天面板,再次点击关闭。关闭、切页和停止生成会清理相关请求;不要额外保留一套旧球形动画组件。
#第 7 步:接入文章导读与选区解释
找到文章外层 article ,只对允许参与 AI 的内容加入属性:
原版的位置是 themes/shoka/layout/_partials/post/post.njk ,该模板接收到的变量是 post ;不要在另一个只提供 page 的模板里照抄 post.ai 。
<article class="post block" | |
{% if theme.blog_ai.enable and post.ai !== false %}data-blog-ai-content{% endif %}> |
这只是示意属性,需要合并到原来的 article 开始标签,不要再嵌套一层重复的文章容器。
文章正文仍使用主题原来的结构,例如 .body[itemprop="articleBody"] 。在文章开头的工具区加入按钮,在正文前放导读宿主:
{% if theme.blog_ai.enable and post.ai !== false %} | |
<div class="sr-article-tools"> | |
<div class="sr-article-tools__actions"> | |
<button type="button" data-blog-ai="guide" aria-expanded="false">文章导读</button> | |
<button type="button" data-blog-ai="explain" aria-expanded="false">选区解释</button> | |
</div> | |
</div> | |
<aside class="blog-ai-host blog-ai-host--guide" | |
data-blog-ai-host="guide" aria-label="本文 AI 导读" hidden></aside> | |
{% endif %} |
如果已有 “本文阅读” 工具区,就把两个按钮并进去,不要为了 AI 重新制作一套文章工具栏。
配套入口样式也包含独立按钮的基础布局,不依赖先安装阅读设置模块。请保留 sr-article-tools__actions 这一层,才能得到对应间距、圆角与渐变。
文章导读发给服务器的是当前文章路径,由服务器查已同步索引;不会因为点了按钮就读取本地文件。选区解释只取允许正文里的选区,不取评论框、表单和后台信息,并在选区附近显示可放大的窗口。
不想让某篇文章参与索引,在 frontmatter 中设置:
ai: false |
它是索引和 AI 入口的排除开关,不是给已公开文章加密。真正不能公开的信息必须从文章中删除或使用可靠的访问保护。
#五、让 AI 认识博客:生成并同步公开知识
#1. 索引不是把整个项目交给 AI
本方案在 hexo generate 阶段,根据已经渲染的公开文章和符合条件的独立页面生成:
public/ai-knowledge.json | |
public/text-workshop-catalog.json | |
public/ai-control-catalog.json | |
public/js/ai-markdown.js |
知识索引包含标题、路径、脱敏正文片段、章节和合规的公开外链。它不扫描服务器磁盘,也不导出站点 _config.yml 、源码 frontmatter、草稿目录、评论账户和环境变量。
索引默认排除未发布、未来发布、私有、隐藏、受密码保护、禁用 AI 等内容,并去掉代码块、表单、隐藏节点和明显凭据。它是有限的公开检索材料,不是向量数据库,也不需要另开一个数据库服务。
正文索引有长度和总大小边界;超长文章的完整源码不会全放进去。因此 “AI 没有提到某段代码” 不一定是连接失败,可能是那段内容本来就没有被索引。
#2. 在本机生成并自查
npx hexo generate --bail |
打开 public/ai-knowledge.json 检查:
site是自己的正式博客地址;articles里能找到一篇普通公开文章;- 没有草稿、API Key、邮箱凭据、私有配置和整段管理代码;
- 标题、文章 URL 和章节锚点对应真实页面。
示意结构:
{ | |
"version": 1, | |
"generatedAt": "2026-09-28T00:00:00.000Z", | |
"site": "https://blog.example.com", | |
"articles": [], | |
"playlists": [] | |
} |
配套包里的 articles 是空数组,这是正常的。自己的索引由博客构建产生,不要把示例空文件当成真实知识库发布。
新博客至少先写一篇没有隐私的公开测试文章,再生成索引。完全没有公开文章或页面时, ai sync 会拒绝空知识库;这不是 Key 出错。可以先用 npx hexo new ai-demo 新建文章,填写标题、非未来日期和几段正文,不要设置 draft 、 private 、 ai: false 等排除项。
索引本身会放在公开网站上,访客也能下载。脱敏规则是辅助保护,不是 “无论写进什么都能自动保密” 的保证。发布前必须自己检查内容。
#3. 先发布博客静态文件,再到服务器同步
确认本地构建正常后,按自己原来的发布流程部署。使用 Git 部署的读者可以在确认目标后执行原有 hexo deploy ;CDN 使用者应把同一次构建产生的资源和页面一起发布,避免旧页面引用未上传的新文件。
然后在服务器执行:
curl --fail --silent --show-error --max-time 30 https://blog.example.com/ai-knowledge.json -o /tmp/blog-ai-knowledge-check.json | |
ai sync |
第一条只是检查公开索引能下载,第二条才负责校验、原子替换和必要的服务重载。
同步成功会显示类似:
Public knowledge updated and reloaded; configuration and quotas preserved. |
没有内容变化时则显示 unchanged ,不会为了构建时间变了就重载。
下载超时、格式错误、站点域名不一致或校验失败时,应保留旧索引。不要为了让命令成功,关闭校验或删掉配置和额度文件。
#4. 可选:定时自动同步
先手动成功一次,再安装同步任务:
sudo install -m 0644 /opt/blog-ai/current/deploy/blog-ai-knowledge-sync.service \ | |
/etc/systemd/system/blog-ai-knowledge-sync.service | |
sudo install -m 0644 /opt/blog-ai/current/deploy/blog-ai-knowledge-sync.timer \ | |
/etc/systemd/system/blog-ai-knowledge-sync.timer | |
sudo systemctl daemon-reload | |
sudo systemctl enable --now blog-ai-knowledge-sync.timer | |
systemctl list-timers blog-ai-knowledge-sync.timer |
配套定时器每小时检查,并带随机错峰。需要立即更新时还是用 ai sync 。
sudo journalctl -u blog-ai-knowledge-sync.service -n 30 --no-pager |
自动同步不调用模型,不消耗模型问答次数,但会产生正常的服务器下载流量。内容改变时服务可能短暂重载,更新应避开重要的正在生成的会话。
#5. 开放前设定适合自己的额度
下面只是教程示例,不是推荐所有站点采用相同额度:
ai limit 100 10 | |
ai tokens 4096 | |
ai timeout 120 | |
ai status |
确认知识同步、密钥、HTTPS、额度都正确后,才执行:
ai on |
这一步允许访客产生模型费用。要暂停调用,执行 ai off ;隐藏博客按钮不等于后端停用。
#六、基础对话的体验与正确性
#1. 一打开就显示完整外壳
点击 AI 时,应立即显示最终对话框的尺寸、输入区和操作区,先显示 “正在检查连接”。不能先出现一个简陋的加载框,再突然跳成另一种布局。
公开状态接口是:
GET https://ai.example.com/api/public/status |
只有对应功能已开启、服务可用时才启用调用按钮:
- 绿色状态点:已连接且可以使用当前功能;
- 红色状态点:关闭、额度耗尽或连接失败,显示明确原因;
- 检查中:生成按钮暂不可用;
- 网络失败:提供重试,不缓存 “失败” 作为永久状态。
enabled 不等于 available 。例如服务开着但今日额度用完,应显示不可用;文章导读也要检查 features.guide ,不能只看到问答开启就启用所有按钮。
本地确定性的站点控制是后面说明的例外:AI 后端关闭时仍可能执行已接入的本地操作,但不能把它标成 “AI 已连接”。
#2. SSE 流式输出和停止按钮
浏览器使用 fetch 读取流式 POST 响应。服务以 meta 、 delta 、 done 、 error 等事件传递来源、文字和结束状态。
流式读取要处理网络分块:一段 JSON 可能被拆到两次读取中,一次读取也可能包含多个事件,不能把 “读取一次” 当成 “收到完整一句”。
关闭窗口、点击停止、切换页面和超时,都应通过 AbortController 取消当前请求,并防止旧回答写进新页面。失败不自动重复收费调用;让用户明确重试。
#3. Markdown 要渲染,但不能变成任意 HTML
AI 的 Markdown 由独立渲染器处理:标题、列表、强调、引用、表格、代码块可以正常显示,代码会作为代码展示。
不能直接使用:
answer.innerHTML = modelOutput; // 不要这样处理不可信模型输出 |
配套渲染器解析 token 后创建允许的节点,链接仅接受安全的 HTTP (S) 地址,不执行脚本、事件属性或模型生成的样式,也不加载模型指定的追踪图片。
模型回答不是文章源文件:它不会执行 Hexo 标签、GitHub 卡片标签或文章里的 HTML 扩展。不能为了 “显示得一模一样” 把 AI 的文本交给浏览器任意执行。
#4. 多轮问答与上下文
当前方案保留最近三组完成的问答作为上下文,并按长度边界裁剪。页面上的历史可显示更多,但不代表每一轮都重新发送整个聊天记录。
没有相关博客资料时,可以正常回答一般问题;有相关资料时优先引用本站,通用补充不能伪装成文章原文。
#5. 长回答与截断
回答上限是最多允许生成的 Token 数,不是强制回答长度。模型遇到自然结束仍会提前停止,Token 也不等于汉字数。
前后端需同时协调:
- 模型本身支持的输出上限;
ai tokens的输出设置;ai timeout的服务超时;- Nginx 的
proxy_read_timeout; - 浏览器读取超时与回答大小边界。
本配套版本的服务超时最多 120 秒,Nginx 模板给流式读取留有余量。不能只把 Nginx 调到十分钟,就期待服务突破自己的两分钟边界。
当模型因长度停止或连接中断,应显示原因、保留已收到的文字,允许手动继续。续答是一次新的请求,可能再次计费和计次;不要悄悄无限续写。
#七、回答与原文联动
知识索引保留文章 URL 和真实章节锚点。回答中的来源编号由服务提供的来源清单建立,不由模型自由生成一个 “看起来像真的” 链接。
点击本站来源后,可以进入对应文章,并定位、高亮实际章节;同页引用直接滚动定位。若锚点失效,应回到正文或显示提示,不能在其他 DOM 节点上随意高亮。
参考文章中的外部链接也可以作为资料线索,但知道链接存在,不等于服务已经抓取该网站全文。本方案不根据模型任意生成的 URL 自动下载页面,避免变成开放代理或请求到内网地址。
为了减少无依据引用,可在测试时提出:
这篇文章的部署步骤是什么?请把依据标到对应章节。 |
再点击来源,检查是否确实到达对应内容。普通知识答案不应硬凑一个本站来源。
#八、可选:加入实时联网检索
#1. DeepSeek API 和网页端不是同一个产品界面
本篇使用的普通 Chat Completions 调用,不会自动附带 DeepSeek 网页端的搜索工具。需要实时资料时,本方案在服务端查询自己部署的 SearXNG,再把有限的结果摘要交给模型。
“自由提问” 也不等于 “自由访问所有网址”。搜索查询可以自由表达,实际联网入口、大小、超时和返回资料仍受控。
#2. 安装 Docker,仅给搜索容器使用
先确认有没有 Docker:
command -v docker |
没有时,可以使用 Ubuntu 提供的软件包:
sudo apt install docker.io | |
sudo systemctl enable --now docker |
不必把普通登录用户加入 Docker 组:Docker 控制权限很高,这里使用 sudo docker 即可。
#3. 拉取官方镜像并固定摘要
SearXNG 官方发布 Docker 镜像,可参考 官方容器安装文档。
sudo docker pull docker.io/searxng/searxng:latest | |
sudo docker image inspect docker.io/searxng/searxng:latest --format '{{json .RepoDigests}}' |
记录这次镜像的 @sha256:... 摘要,实际运行时使用自己拉取到的完整名称,不复制别人的历史摘要。
中国大陆访问 Docker Hub 或 GHCR 可能慢、超时。可使用自己信任的镜像同步服务,或在可访问官方仓库的机器下载后 docker save 、上传、 docker load 。第三方镜像源存在信任和供应链风险;一个摘要只能标识镜像内容,不能单独证明第三方内容与官方一致。
不要为了拉镜像购买本教程不需要的付费云产品,也不能保证某个免费镜像源永远可用。
#4. 创建最小私有搜索配置
sudo install -d -m 0755 /opt/blog-search/settings /opt/blog-search/cache | |
openssl rand -hex 32 | |
sudo nano /opt/blog-search/settings/settings.yml |
将刚生成的随机值填到下面的 secret_key ;这不是 DeepSeek Key,也不要把自己的模型密钥放在这里。
use_default_settings: | |
engines: | |
keep_only: | |
- bing | |
- sogou | |
server: | |
secret_key: "替换为刚生成的随机字符串" | |
limiter: false | |
image_proxy: false | |
search: | |
safe_search: 1 | |
formats: | |
- json | |
outgoing: | |
request_timeout: 5.0 | |
engines: | |
- name: bing | |
disabled: false | |
- name: sogou | |
disabled: false |
这是小型私有实例的起点,不是推荐公开搜索网站关闭 limiter。这里搜索只绑定本机,外层 AI 服务另有限流。若要公开 SearXNG,需按官方方案配置额外的访问保护,不能沿用这份设置。
formats 需允许 JSON,否则 API 可能返回 403。 server 与 search 配置项说明见 SearXNG server 文档、search 文档。搜索引擎可用性随地区和上游策略变化,Bing、搜狗这里只是待实测的示例。
在 nano 中保存: Ctrl+O 、回车;退出: Ctrl+X 。
#5. 启动时只绑定回环地址
把下面变量换成自己上一阶段记录的镜像名称和摘要:
search_image='docker.io/searxng/searxng@sha256:替换为自己的镜像摘要' | |
sudo docker run -d --name blog-search --restart unless-stopped \ | |
--memory 384m --cpus 0.5 --pids-limit 128 \ | |
-p 127.0.0.1:8088:8080 \ | |
-v /opt/blog-search/settings:/etc/searxng \ | |
-v /opt/blog-search/cache:/var/cache/searxng \ | |
"$search_image" |
检查:
sudo docker port blog-search | |
sudo docker logs --tail 30 blog-search | |
curl --fail --silent --show-error --max-time 15 \ | |
http://127.0.0.1:8088/search \ | |
--data-urlencode 'q=Hexo 静态博客' \ | |
--data 'format=json&language=zh-CN&categories=general' |
端口应显示 127.0.0.1:8088 ,而不是 0.0.0.0:8088 。不要在 Nginx 再给它增加公网转发,也不要添加 8088 的云防火墙允许规则。
某个引擎返回 CAPTCHA 不代表整个 AI 服务坏了;可以暂时停用异常引擎,保留能用的结果。不要让模型伪造 “检索成功”。内存不足导致容器重启时,要调整规模或停用可选搜索,不能只把内存保护全部解除。
#6. 接到 AI 服务上
sudo systemctl edit blog-ai |
输入:
[Service] | |
Environment=AI_SEARCH_ENDPOINT=http://127.0.0.1:8088/search |
保存后:
sudo systemctl daemon-reload | |
sudo systemctl restart blog-ai | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/api/public/status |
前端根据 features.webSearch 展示联网选项。状态中出现这个功能标志,仅代表后端接入了搜索入口,不证明所有上游引擎都正常;还需实际搜索测试。
搜索失败时应退回博客资料和一般知识,并明确不能实时确认。搜索结果只当不可信参考资料,不能改变系统规则,也不能成为自动执行的操作。
#九、博客宇宙 AI 导游、导航与心情漫游
#1. 宇宙导游需要已有的宇宙页面
AI 服务不会凭空生成原项目的 Three.js 星系。先完成博客宇宙模块,再在该页面加入导游按钮与宿主:
<button type="button" data-blog-ai="tour" aria-expanded="false">AI 导游</button> | |
<aside class="blog-ai-host blog-ai-host--tour" | |
data-blog-ai-host="tour" aria-label="宇宙 AI 导游" hidden></aside> |
导游使用已有问答能力和真实来源清单,不需要一个虚构的 /api/public/tour 接口。返回路线后通过:
window.BlogUniverse.setAITour(sources); |
让宇宙模块按真实文章匹配星球,并高亮路线。如果自己的宇宙模块没有这个接口,就在模块内部增加一个接收来源列表的适配函数,而不是在 AI 代码里操纵任意 Three.js 对象。
“发现航线” 按钮应在展开后可见,来源和路线可以展开查看;不要把关键按钮藏在一大段说明的底部。
#2. 自然语言导航
在对话组件的玩法入口里,可说:
想看看博客里能玩的东西。 | |
打开图集,切到夜间并减弱动效。 |
模型只选择预先登记的页面 ID 和设置 ID。浏览器将它们变成可点击的确认按钮,不让模型生成任意地址或 JS。
配套 ai-playground.js 中的 destinations 就是入口清单。请按自己真正已有的页面增删:没有图集、随笔或写作室,就不要保留对应推荐。
#3. 心情漫游
例如:
忙了一天,想花十分钟放空一下。 | |
有点无聊,想逛逛音乐、图片和新奇页面。 |
生成两到四个站内角落串起来的路线,附上推荐理由和参考停留时间。路线链接手动打开;音乐也由访客手动播放,不在用户没有授权时自动发声。
这两种玩法共用现有问答服务的限流、停止按钮和连接检查,不能另开一个不计额度的匿名收费入口。
#十、让 AI 控制博客,但不让 AI 接管浏览器
#1. 支持哪些操作
| 类型 | 例子 | 实际执行者 |
|---|---|---|
| 音乐 | 播放、暂停、停止、上一首、下一首、指定歌曲、音量、播放模式 | 当前播放器公开方法 |
| 页面 | 打开文章、打开页面、滚到顶部/底部/评论区 | 已登记的站内路径与导航方法 |
| 显示 | 白天/夜间、动效、背景、纹理、首页排列、自定义配色 | 原显示设置组件 |
| 阅读 | 字号、行高、段落间距、正文宽度、阅读模式 | 当前文章阅读组件 |
| 搜索 | 搜索某个关键词 | 原本的普通搜索组件 |
| 数据 | 阅读量、评论数、公开评论者、文章与访问统计、友链 | 公开目录和公开只读接口 |
| 订阅 | 打开 RSS、Atom、JSON Feed | 真实存在的订阅文件 |
不支持发评论、删动态、改数据库、执行 shell、访问管理员后台或读取浏览器登录凭据。
#2. 自然语言不等于固定口令
简单、确定的命令先在本地解析,例如 “下一首”“音乐轻一点”“切到夜间”。复杂表达再交给模型生成受限的操作方案。
当前控制请求复用问答接口,例如:
POST /api/public/chat |
{ | |
"question": "把音乐调到百分之三十,然后去文章归档", | |
"control": true | |
} |
它不是一个可以提交任意脚本的执行接口,也没有必要创建 /api/public/control 后直接执行模型内容。控制模式只生成方案,由前端核对后给用户确认。
相近措辞可以由模型理解;但识别结果涉及多篇文章、多首歌曲时,应先给选择,不要猜一条就跳转。
#3. 接入窄接口,而不是只改按钮图标
配套 site-control-bridge.js 是参考桥接片段。只有先确认自己的播放器与导航 API 匹配后,才把它加入 app 构建列表,放在 AI 入口之前。
它暴露的核心对象是:
window.BlogSiteControl = { | |
root: CONFIG.root || '/', | |
waline: CONFIG.waline && CONFIG.waline.serverURL || '', | |
navigate: function(path) { /* 校验站内路径,使用原 PJAX 或 location */ }, | |
music: function() { /* 返回当前播放器的窄接口,没有播放器就返回 null */ } | |
}; |
原版播放器不一定提供参考项目的完整方法。音乐适配需要落实下列契约:
| 方法 | 应该做什么 |
|---|---|
prepare() | 加载已有歌单,失败可重试 |
tracks() | 返回当前真实曲目,至少包含可识别的标题和歌手 |
state() | 返回实际播放、音量、模式等状态 |
play() / pause() / stop() | 调用原播放器,不伪造播放状态 |
next() / previous() / track(index) | 走原曲目切换逻辑 |
volume(percent) / mute(boolean) | 同步音频对象、滑块和图标 |
mode(value) | 对应列表、随机、单曲循环的原状态逻辑 |
例如原播放器内部增加 state() 时,应从真实的 <audio> 、当前曲目和模式字段读取,而不是另外维护一份 “AI 音乐状态”。浏览器阻止播放、音源失败时,也不能显示 “已播放成功”。
模型并不能解除音乐版权或音源的 30 秒试听限制,这属于原音源返回内容。
#4. 日夜与阅读设置同步
配套加载器通过 ShokaFeatures.setTheme() 调用主题原来的 changeTheme() ,保存原状态。不能只设置一个 CSS 属性,却漏掉日夜按钮图标和保存逻辑。
阅读、背景、配色控制则需要现有组件的 update 、 apply 、 open 等接口。没有这些组件时,相关操作应提示未接入;不要让一个空函数返回成功。
默认阅读调整只针对当前文章。访客明确说 “所有文章都这样” 时,才进入全局保存的路径;不要一次临时阅读请求永久改变全站偏好。
#5. 公开数据查询的边界
控制目录只应导出已经公开的页面标题、路径、友链与订阅文件,不能导出账户或后台配置。
浏览量查询需要页面已经有正确的计数键。配套页面目录通过 .waline-pageview-count[data-path] 识别已接入计数的页面,不会自动替你为所有页面启用计数。原版使用其他计数结构时,需要按自己的结构适配提取器。
导航与浏览量不绑定:本版配套生成器会从合规公开知识中登记真实文章和独立页面,再单独标记是否有浏览量计数。没接 Waline 的普通文章也能成为导航目标;没有计数接口时不把访问量猜成零。音乐、访问日历和友链等仍各自依赖真实模块。
参考版本可读取 Waline 的公开评论和累计阅读接口;访问日历、整日访问量等增强数据依赖已有后端扩展,例如 /api/pageview-history 。普通 Waline 未提供这些扩展时,跳过该项或显示 “尚未接入”,不能把接口 404 当成零访问。
“评论者” 只指已公开显示的昵称等信息,不查询邮箱、IP、用户令牌和后台账户列表。统计结果由接口返回,不凭模型猜测。
#6. AI 服务关了,还能控制吗
可以保留已经接好、能够在浏览器确定执行的操作,例如日夜切换、页面滚动和简单音乐操作。它们不需要模型理解,也不消耗模型额度。
复杂自然语言推断、导读、解释和创作则需要后端。这里的 “本地控制” 也不代表完全离线:音乐、评论统计、搜索目录等仍可能需要网络。
#十一、加入七种文字工坊
#1. 页面与用途
配套包包含入口页和七个工作台:
| 页面标识 | 名称 | 适合做什么 |
|---|---|---|
story | 文字分岔剧场 | 写故事、选方向、回溯分支 |
phrasing | 措辞试衣间 | 同一段话换表达、语气和结构 |
roundtable | 观点圆桌 | 对一个问题呈现不同立场 |
clues | 线索工作台 | 从材料中整理线索与待核对关系 |
memoir | 回忆编织机 | 将自己提供的片段组织为文字 |
world | 平行世界档案馆 | 创作另一个可能世界的设定与档案 |
archaeology | 思想考古 | 对自己的文字材料做主题关联 |
它们是创作辅助,不是后台发布系统。生成内容不会自动成为博客文章,也不会因为放进 “回忆” 功能就自动读取个人经历。
#2. 页面 frontmatter
例如 source/text-workshop/story/index.md :
--- | |
title: 文字分岔剧场 | |
layout: text-workshop | |
text_workshop: story | |
comment: false | |
reading: false | |
copyright: false | |
fancybox: false | |
--- |
layout 指向配套 themes/shoka/layout/text-workshop.njk 。入口页的 text_workshop 是 home ,其他六页使用表里的对应标识。
把 “文字工坊” 链接加入自己想放的导航二级菜单即可,路径是 /text-workshop/ 。不要覆盖整个导航配置,只添加一个条目。
#3. 避免头图和巨大图标一闪而过
工坊使用应用式页面布局,不需要普通文章的大横幅、随机文章和最新评论。
必须在主布局输出 header 之前,设置会随 PJAX 交换的页面标记,并引入 integration/workshop-shell.css 的关键规则:
<div id="ai-page-shell" class="pjax" hidden> | |
{% if page.text_workshop %} | |
<span data-page-shell="workshop"></span> | |
<!-- 在这里放配套 workshop-shell.css 的完整关键样式 --> | |
{% endif %} | |
</div> |
这里的注释不是实际 CSS。把完整规则放在已有关键样式区,或把配套 CSS 新增为主题 source/css/ai-workshop-shell.css ,在 <head> 提前引入。这份小样式应全站加载,因为普通文章经 PJAX 进入工坊时不会重新执行整个 head 的非交换链接;规则只在工坊标记存在时生效,不影响其他页面。启用严格 CSP 时应沿用原站点的 hash/nonce 或允许的样式地址,不能全局取消保护。
参考改造版已有 page-feature-assets 这样的 PJAX 槽位,就合并进去,不再创建第二个同用途容器。同时确认 PJAX 选中并替换这个槽位,否则从普通文章切入工坊后,旧页面的头部样式可能残留。
在模板生成阶段排除头部图片,而不只是用 JS 事后隐藏。原版是在 themes/shoka/layout/_partials/layout.njk 的 #imgs 中,把原有 _cover 调用及整个图片分支包在条件里面:
<div id="imgs" class="pjax"> | |
{% if not page.text_workshop %} | |
<!-- 保留原来的 _cover、ul / img 完整分支 --> | |
{% endif %} | |
</div> |
不要仅将 covers 改为空数组却继续输出 <img src=""> 。没有 _cover helper 的主题,在自己的横幅生成逻辑外加 not page.text_workshop 条件即可。
#ai-page-shell 放在主布局的 #container 内、 <header id="header"> 之前,每一种页面都保留这个 .pjax 容器,普通页让它为空。原版 PJAX 已交换 .pjax ;其他分支应在选择器中明确加入 #ai-page-shell ,并保持两类页面槽位数量、顺序一致。
保留 #footer 与导航的稳定结构,由预先加载的工坊规则隐藏 footer,而不是仅在工坊首次访问时从模板删除 footer。否则从工坊返回文章时,原版不交换的 footer 无法凭空恢复。确实要省掉页脚内部的数据加载,需要把内部内容另做一个两类页面都存在的 PJAX 交换槽位,再按条件渲染;不要直接删除外层。
原版 footer 中还有随机文章与评论小工具,但不包含在本包内;基础路线先按 CSS 隐藏整个 footer,后续优化它们的加载时机。导航沿用原主题的日夜配色。
关键 CSS 预先给 .tw-icon 明确宽高,避免浏览器把 SVG 暂时显示成 300×150 的默认大小。
#4. 文字输出不要被过度校验丢弃
工坊通过:
POST /api/public/workshop |
调用后端。服务尽量要求模型给出规定结构,前端据此展示卡片、分支或不同版本;但创作文字与博客引用回答不是同一种内容。
本方案采用 “优先符合结构,格式异常时保留安全的文字结果” 的策略:不因少了一个字段、引用不适用于虚构故事或多写了 Markdown,就把整个回答丢掉。
输入大小、返回体大小、危险链接和执行边界仍要检查;这些是传输与浏览器安全措施,不是重复替模型做一套内容审查。
增加输出上限可以减少长内容截断,但不能强迫模型写满。想要更长的文字,也需要在玩法提示里明确段落、展开程度和保留的材料;不使用自动无限续答。
#5. 草稿、材料与隐私
工坊草稿和历史保存在当前浏览器。刷新可以恢复,但无痕模式、清理站点数据、更换设备后不保证还在;重要内容应主动复制备份。
草稿保存不等于 “永不离开本机”:点击生成时,当前输入及选择的必要材料会通过自己的 AI 服务发送给模型服务商。不要输入密码、身份证、私密聊天和他人敏感信息。
思想考古里的 “选择博客材料” 读取公开文章摘录目录;只是打开目录不会调用模型,选中后也应允许检查、删减,再决定发送。
#十二、服务器管理:用 ai 命令就能调整
#1. 常用命令
下面均在服务器终端执行。数字是示例,根据自己的成本和规模调整。
| 命令 | 作用 |
|---|---|
ai | 打开中文管理菜单 |
ai status | 查看服务、模型、额度和安全设置 |
ai on / ai off | 持续开放 / 关闭访客模型调用 |
ai limit 100 10 | 全站每日 100 次、每 IP 每日 10 次 |
ai tokens 4096 | 设置每次最大输出 Token |
ai timeout 120 | 设置请求超时秒数 |
ai rate 12 | 每 IP 每分钟上限 |
ai cooldown 6 | 短时异常冷却触发阈值 |
ai global-rate 20 | 跨 IP 共享的每分钟调用上限 |
ai global-hour 120 | 跨 IP 共享的每小时调用上限 |
ai parallel 2 | 全站同时生成数 |
ai input 8000 | 通用输入字符上限 |
ai models | 查询官方可用模型,不生成回答 |
ai model 完整模型ID | 切换官方目录中的兼容模型 |
ai usage | 查看当天计次和本地告警 |
ai sync | 同步已发布的公开索引 |
ai help | 查看全部帮助 |
设置命令会校验、保存并按需要重启服务,正常成功后不需要再手动重启一次。额度不会因为改设置而清零;重启可能中断正在输出的回答。
#2. 参数不是都能无限放大
| 参数 | 配套版本边界 | 说明 |
|---|---|---|
| 全站 / IP 每日额度 | 正整数,受安全整数边界约束 | 不表示有无限余额 |
| 请求超时 | 1~120 秒 | 更长超时需要前后端一起改造 |
| 全站并发 | 1~2 | 小服务器与资源保护的设计边界 |
| 单 IP 同时请求 | 1 | 避免一个人占满生成资源 |
| 单 IP 分钟上限 | 4~60 次 | 冷却阈值需小于该值 |
| 冷却阈值 | 2~30 次 | 与分钟上限配套调整 |
| 通用输入 | 1~16000 字符 | 各功能还有更小的独立边界 |
| 输出 Token | 受服务技术边界和所选模型上限约束 | 不能超出模型实际能力 |
例如问答问题长度、选区长度和工坊结构材料各有上限,调高 ai input 不会绕过所有独立限制。
#3. 模型更新后怎么换
ai models | |
ai model deepseek-flash | |
ai status |
模型 ID 请以自己当时查询到的官方列表为准。 flash 、 pro 是现有快捷别名,也可填写目录中的完整名称。
如果只是新增一个兼容的模型名称,通常无需重新部署;如果请求协议、模型目录字段、模态或支持的参数发生变化,则仍可能需要代码适配。不能承诺 “未来任何模型都只改名称就能用”。
#4. 额度、费用和告警
每日额度按北京时间更新,失败或取消的已预扣模型请求也计次,用量不是精确账单。80%、95%、100% 阈值会生成本地告警。
ai usage | |
sudo journalctl -u blog-ai --since today --no-pager |
公开站点设上限很重要:CORS、来源检查和每 IP 限流都不能保证匿名接口不被脚本或多 IP 消耗。发现异常,先 ai off ,查看脱敏日志和模型账户用量,再按情况降低全站限流、暂停搜索或调整开放方式。
本教程不默认开通验证码、WAF、短信、付费日志等云产品;可选验证码需要另外评估申请、权限和费用,不属于基础部署必选项。服务器、带宽和模型调用仍可能按已有服务计费,开源软件免费不等于整体没有费用。
#十三、安全检查:不能只看 healthz
#1. 只暴露必要端口
sudo ss -ltnp |
应重点看到:
- Nginx 的 80、443 对外监听;
- AI 3001 仅监听
127.0.0.1; - 可选搜索 8088 只绑定本机;
- SSH 管理入口遵循自己已经验证可用的策略。
#2. 检查私有文件权限
sudo stat -c '%U %a %n' /etc/blog-ai /etc/blog-ai/config.json | |
sudo python3 /opt/blog-ai/current/bin/security-audit.py |
审核程序只输出检查结果,不应打印密钥。配置应 root 所有、目录 700 、文件 600 。不能为了修复 “权限错误” 把它们改成 777 。
审核里的默认 SSH 策略和缓存中的待更新包数,并不能替代对云防火墙、Match 规则和实时更新状态的检查。
#3. 来源和真实 IP
服务器只允许配置的正式博客来源,以及明确开启的本地测试来源。Nginx 用真实连接来源覆盖 X-Real-IP ,后端只在可信本机代理链下采信它。
不能把访客自己传来的 X-Forwarded-For 直接当真实 IP,否则限流会被随意绕过。后端 3001 不公开,也是这层信任边界的一部分。
Origin 检查保护正常浏览器的跨站调用,不是一个不可伪造的身份凭据。攻击脚本仍可能伪装来源,多 IP 也可能共同耗尽额度;全站分钟、小时和每日上限必须保留。
#4. 日志和模型输入
日志不记录 API Key、Authorization、请求正文、模型全文和查询字符串。Nginx 的最小访问日志仍可能包含来源 IP 和访问时间,需要合理保留、限制读取权限。
模型看到的是本次问题、必要选区或工坊材料、有限历史,以及检索到的公开资料,不是整个浏览器或服务器文件系统。普通回答和搜索资料都不能成为新的系统指令。
原文联动、搜索摘要与控制方案分别校验;文本再自由,也不能因此开放任意 URL、CSS、选择器、脚本或数据库操作。
#十四、上线前回归:逐项确认,别一次试完就算成功
#1. 无费用的服务器检查
readlink -f /opt/blog-ai/current | |
systemctl is-active blog-ai nginx certbot.timer | |
ai status | |
ai usage | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/healthz | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/api/public/status |
健康检查只证明服务能响应;知识是否就绪、访客是否开放、具体功能是否支持,还要看公开状态和实际界面。
#2. 本地预览来源要明确开启
本篇配套版本只允许配置中的本地地址,不接受任意 localhost 端口。准备在 http://localhost:4000 测试时,在服务器执行:
sudo python3 /opt/blog-ai/current/bin/ai-admin.py local-preview on |
在本机启动博客:
npx hexo server --port 4000 |
若 4000 被占用,先确认已有进程是不是自己的博客。不要随便结束不认识的进程,也不要换到 4001 后误以为后端全部坏了——它可能只是没有允许这个来源。
测试完回服务器关闭:
sudo python3 /opt/blog-ai/current/bin/ai-admin.py local-preview off |
本地预览调用的仍可能是正式模型服务,会产生真实费用;“在 localhost 打开” 不代表免费。
#3. PC、平板和手机都要检查
建议逐项勾选:
#4. 最后再做少量真实模型测试
开启额度后,用一条简单问题、一篇短文章导读、一段选区解释,分别确认真实回答正常。工坊可以用不含隐私的虚构材料测试。
比如文字分岔剧场输入:
主角没有碰旧唱盘,而是检查店门、柜台和后间的帘子,寻找失踪店主留下的线索。请续写这一段,并给出几个可以继续探索的方向。 |
检查模型费用账户和 ai usage 是否有预期变化,避免把自动化页面检查误当成已经完成了真实模型验证。
#十五、日常维护、更新与回退
#1. 发新文章后做什么
本机修改文章 → 重新生成 → 按原流程发布静态文件 → 服务器 ai sync ,或等待同步定时器。
只修改本地 source/ 而没有发布,服务器当然看不到新文章;发布到 CDN 后还要确认 JSON 内容已经更新,不能同步到缓存中的旧版本。
#2. 修改额度或模型要不要重启
正常使用 ai 命令即可,它会校验并按需要加载新配置,不用再重复手动重启。配置文件手工编辑更容易出错,不建议新手从一大段 JSON 中改数字。
设置成功不代表正在输出的旧请求可以无缝继续,所以最好在低使用时段调整。
#3. 后端更新与博客发布是两件事
| 改了什么 | 发布到哪里 |
|---|---|
| 按钮、样式、模板、知识生成逻辑 | 博客静态页面 / CDN |
| 请求协议、安全逻辑、工坊或控制能力 | AI 服务器的新代码版本 |
| 已有评论接口 | 按原 Waline 部署流程,非本篇默认步骤 |
| 额度、模型、超时 | 在服务器用 ai 命令修改 |
如果前端新增了后端没有的能力,应通过公开状态提示未支持,不能只改前端按钮就认为功能已上线。
#4. 使用新版本目录,保留旧版本
后续更新不要直接覆盖正在运行的 src/ 。先把新包安装到一个新的 /opt/blog-ai/releases/版本名 目录,核对来源、权限、代码测试和接口兼容性,再切换 current 。
继续保留这些共享数据:
/etc/blog-ai/config.json 密钥与设置 | |
/var/lib/blog-ai/ 用量和告警状态 | |
旧 release/data/blog-knowledge.json 当前公开知识索引 |
候选新版本需要继承旧的合法知识索引,不能把教程的空索引再次覆盖进去。Node 运行环境也要单独确认,不随一次功能更新偷偷升级主版本。
手动更新至少应包括 “检查候选版本 → 保存旧指向 → 切换 → 重启 → 检查健康和公开状态 → 失败切回” 的完整过程。自动安装器则应保留以上校验和回退,而不是出错后删掉配置、清空额度重装。
如果更新来源仍是自己核对、打包的 server/ ,可按下面的手动流程操作。先上传新包;以下命令只适用于本教程安装目录,不是任意服务器的通用覆盖命令。
先保存当前版本,并检查它确实在自己的 release 目录下:
previous_release=$(readlink -f /opt/blog-ai/current) | |
case "$previous_release" in | |
/opt/blog-ai/releases/*) printf '当前版本:%s\n' "$previous_release" ;; | |
*) echo '目录异常,停止更新'; return 1 2>/dev/null || exit 1 ;; | |
esac | |
test -L /opt/blog-ai/current |
不要关闭终端;后面的回退需要这个 previous_release 变量。若 test 失败或目录异常,先检查,不再继续。
检查新包的顶层仍为 server/ ,然后解压到新的临时目录,使用新的版本名:
update_stage=$(mktemp -d /tmp/blog-ai-update.XXXXXX) | |
tar -xzf /home/ubuntu/blog-ai-server.tar.gz -C "$update_stage" --no-same-owner | |
new_name="release-$(date +%Y%m%d-%H%M%S)" | |
candidate_release="/opt/blog-ai/releases/$new_name" | |
sudo install -d -m 0755 "$candidate_release" | |
sudo cp -a "$update_stage/server/." "$candidate_release/" | |
sudo chown -R root:root "$candidate_release" | |
sudo find "$candidate_release" -type d -exec chmod 0755 {} + | |
sudo find "$candidate_release" -type f -exec chmod 0644 {} + |
继承当前知识,验证候选代码,而不是切换后才测试:
sudo install -m 0644 "$previous_release/data/blog-knowledge.json" \ | |
"$candidate_release/data/blog-knowledge.json" | |
/opt/blog-ai/runtime/bin/node "$candidate_release/bin/validate-knowledge.mjs" \ | |
"$candidate_release/data/blog-knowledge.json" | |
cd "$candidate_release" | |
/opt/blog-ai/runtime/bin/node --test test/*.test.mjs |
任何一步失败就停下,旧服务保持运行。全部通过后,创建新软链接再替换 current :
sudo ln -s "$candidate_release" /opt/blog-ai/current.next | |
sudo mv -T /opt/blog-ai/current.next /opt/blog-ai/current | |
sudo systemctl restart blog-ai | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/healthz | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/api/public/status | |
ai status |
如果 current.next 已经存在,停止核对,不要删除未知内容后继续。检查中确认模型、额度、功能开关与原先一致;出现问题可以切回刚才保存的旧目录:
sudo ln -s "$previous_release" /opt/blog-ai/current.rollback | |
sudo mv -T /opt/blog-ai/current.rollback /opt/blog-ai/current | |
sudo systemctl restart blog-ai | |
curl --fail --silent --show-error --max-time 15 https://ai.example.com/healthz | |
ai status |
这套过程不改私有配置、用量目录和在线 Nginx 配置。需要变更配置格式、管理命令位置或请求协议的升级,必须另写相应迁移步骤,不能套用 “只换源码” 的流程。
#5. 备份时注意密钥
需要备份私有配置和用量时,在服务器受保护的目录里加密保存,并限制读取权限。不要把服务器备份包放进博客 source/downloads/ 或公开对象存储。
可公开分发的是通用程序源码,不是 “从生产服务器把整个目录压缩下来” 的备份。
#十六、常见问题
#1. 为什么服务器健康正常,但按钮不能发送
查看公开状态中的 enabled 、 available 、额度和当前功能标志,再检查浏览器 Origin、HTTPS、CSP 和网络。
有 Key、服务 active、 healthz 正常,只满足了部分条件;后端默认关闭访客收费能力是正常保护。
#2. 为什么生成了索引,AI 还不知道新文章
检查是否已发布、正式地址下载到的是否是新 JSON、服务器是否执行过 ai sync 、文章是否被 ai:false 或私有条件排除。
超长内容、代码块和被脱敏的信息也不一定进入索引。AI 不是全文浏览器,不能要求它准确复述从未收到的内容。
#3. sync 超时了怎么办
先在服务器用 curl 测试固定的公开索引地址,检查 DNS、首字节、整体下载时间、Content-Type 和 CDN 缓存。
保留旧索引后稍后手动重试;不通过增加任意 URL 参数、允许跳转到未知站点或取消大小边界解决。索引有 4 MiB 大小限制,大站点应调整收录策略,而不是无限放大请求。
#4. 为什么 AI 不能 “查看所有网页”
默认是公开博客资料和通用知识。可选联网只查询受控的搜索服务,并使用有限摘要;文章已有外链不会自动触发全文抓取。
这样做减少内网请求、恶意页面和无限流量风险。需要新增抓取能力时应单独设计安全边界,不能让模型给一个 URL 就直接下载。
#5. 国内网络慢,是否换更多 CDN 就行
先区分哪个请求慢:博客 HTML、静态 JS、Waline、模型生成还是搜索上游。关键 JS/CSS 和 AI Markdown 依赖由本地构建输出,可随自己的 CDN 发布,避免首次点击临时访问多个境外 npm CDN。
国内服务器通常能改善本机 AI 中间服务的连接,但不能保证所有运营商、搜索引擎和模型请求永远不超时。请求要有边界、重试入口和失败降级,而不是无限自动重试。
#6. 为什么显示设置改了,图标没变
多半是绕开原组件,只改变了一份 AI 内部状态。应调用原 changeTheme 、播放器或设置组件的正式方法,并从真实组件读取结果。
#7. 为什么没有统计、音乐或宇宙功能
这些是可选的已有博客模块,不是 AI 服务顺带安装的软件。先安装或实现对应模块,再按前面的接口表接入;不支持的能力不要登记为可执行操作。
#8. 我需要部署一个 AI 管理网页吗
不需要。 ai 命令在服务器管理员终端使用,没有公开的配置写入页面,也不把管理令牌放到前端。不要为了省一步输入命令,开放一个未经保护的 “改额度” 接口。
#9. 匿名接口能绝对防止恶意消耗吗
不能。限流和每日额度用于把损失与负载限制在可控范围,不是实名身份验证。多人共享网络可能共用 IP 额度,攻击者也可能使用多个 IP。
如果持续受到攻击,先暂停服务,再考虑更严格的开放范围、登录权限或可选验证。不能因为模型有内容安全检查,就删掉请求验证、限流和执行白名单。
#十七、配套代码对应关系
完整源码在文章开头的配套包中。下面列的是查找入口,不需要把所有代码重复粘贴到一篇文章里。
| 需求 | 主要代码位置 |
|---|---|
| 服务启动与凭据加载 | server/src/main.mjs 、 config.mjs |
| 公开接口、模型流式请求、计次 | server/src/service.mjs |
| 博客检索、上下文、导读、选区与来源 | server/src/knowledge.mjs |
| 联网搜索与外链边界 | server/src/web-search.mjs |
| 七种玩法和格式兼容 | server/src/workshop.mjs |
| 自然语言操作方案 | server/src/control.mjs |
| IP 限流、冷却与本地告警 | server/src/abuse.mjs 、 usage-alerts.mjs |
| 私有管理、模型与索引同步 | server/bin/ai-admin.py 、 ai-control.py |
| HTTPS、凭据隔离、自启与同步 | server/deploy/ |
| Hexo 公开索引与本地渲染资源 | blog/lib/ 、 blog/scripts/ |
| 对话与工坊完整界面 | blog/themes/shoka/source/ 、 layout/text-workshop.njk |
| 主题内的加载和控制桥接 | integration/ |
模型回答尽可能自由,程序执行边界保持清晰;公开资料可检索,私有配置始终留在服务器。这两个原则比 “把一个聊天框放进页面” 更重要。
#参考资料与致谢
- 本站教程:Shoka 主题:美化统计页面并新增多维图表:本文的教程结构参考;
- DeepSeek 官方 API 接入文档:模型调用方式与名称;
- Node.js 官方下载:运行环境和校验说明;
- Ubuntu Server 自动更新:系统维护;
- Certbot 官方使用说明:证书申请和续期;
- SearXNG 容器安装、配置文档:可选搜索服务。
原 Shoka 主题;本文在其风格和页面结构上接入 AI。
可选的开源元搜索服务,部署为仅本机访问的检索入口。
AI Markdown 的分词基础,不执行模型生成的 HTML。
完成基础部署后,先把 “状态正确、能够问答、引用真实、可以停止、密钥不外泄” 验证清楚,再逐步开启联网、导游、控制和文字工坊。每一步都能单独检查和回退,比一次堆满所有功能更适合第一次接入 AI 的读者。