面向中国药科大学学生的校园服务聚合站点,由在校学生自主研发。
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗药大拾间是一个面向中国药科大学学生的校园信息聚合站点。当前仓库已经不是单纯的 Web MVP,而是一个完整的多端项目,包含:
web/:Vue 3 前端站点与 /schedule PWA 课表页server/:Express + Prisma + PostgreSQL 后端voicehub/:药苑之声 Nuxt 全栈子应用(完整保留 VoiceHub 点歌系统)android/:Android WebView 壳与桌面课表小组件harmony/:HarmonyOS WebView 壳与 JS Bridgedesktop/:Windows / Apple Silicon macOS 桌面端(Electron 单窗口标签页,OAuth2 登录 + 校园网自动连接 + 学习通标签页)仓库地址:https://github.com/sx120609/CPU-web
想先用不那么专业的方式了解整个项目,可以阅读: 《药大拾间完整技术路线》,也可以直接下载排版好的 PDF 版本。
说明
- 本项目为学生自发聚合站,非中国药科大学官方平台。
- 学校统一认证只用于用户授权后的教务数据获取;项目不保存学校密码和验证码。
- 二手功能是论坛信息交流板块;本站不提供站内下单、支付、担保、退款或结算。
| 模块 | 典型路径 | 访问要求 | 说明 |
|---|---|---|---|
| 首页 / 搜索 / 公告 | /home /search /announcements | 公开 | 展示站内聚合内容与公告源 |
| 论坛入口 / 板块 / 帖子 | /forum /forum/b/:slug /forum/topic/:id | 公开或受社区权限控制 | 公开入口可访问,具体社区内容仍会受服务端权限与功能开关拦截 |
| 热榜 / 最新 / 二手交流 / 课程点评 | /forum/hot /forum/latest /market /coursereview | 浏览按社区权限控制,发布需站内登录 | 受 forum / market / coursereview 功能开关与社区权限控制 |
| 发帖 / 编辑 / 回复 / 点赞 / 消息 | /post /post/:id/edit /messages | 站内登录 | 帖子正文支持 Markdown、匿名、图片上传与审核 |
| 教务页壳 | /jwxt /schedule | 页面可公开访问 | 真正拉取课表、成绩、考试等数据时需要学校 SSO 授权得到 jwxtToken |
| 校园服务导航 | /services | 公开 | 聚合校内外常用入口与说明 |
| 校园小工具入口 | /services/tools /services/tools/:slug | 公开 | 具体工具是否要求登录由工具配置决定 |
| 三维校园 | /services/tools/yaoda-can-fly/campus-3d | 公开 | 联机时游客显示随机名,登录后显示站内昵称 |
| 药苑之声 | /voicehub/ | 排期与歌曲公开,投稿/投票/后台需登录 | 复用本站会话,无需在 VoiceHub 二次注册或登录 |
| 问卷填写 / 文件提交 | /services/tools/questionnaires/:slug /services/tools/filestore/submit/:slug | 通常公开 | 支持按工具设置切换是否要求登录 |
| 成绩核对查询 | /services/tools/grade-checks/:slug | 默认需登录 | 登录后只看自己的学号记录 |
| 个人中心 / 赞助 / QQBot 绑定 | /profile /u/:id /sponsor-wall | 部分公开、部分登录 | 鸣谢墙公开;个人资料、赞助订单、QQBot 绑定需登录 |
| 管理后台 | /admin | mod / admin | 含用户、板块、站务、AI 审核、支付、QQBot、数据库等后台 |
| 层级 | 技术 |
|---|---|
| 前端 | Vue 3、Vite、TypeScript、Vue Router、Pinia、Element Plus、Axios、ECharts |
| 后端 | Node.js、Express、TypeScript、Prisma 5、PostgreSQL |
| 内容处理 | marked、DOMPurify、Cheerio、Turndown、iconv-lite、@resvg/resvg-js |
| 文件与表格 | multer、xlsx、viewerjs、html-to-image |
| 鉴权与安全 | JWT、bcryptjs、学校统一认证会话、Zod |
| 辅助子系统 | Filestore 文件收集、Android WebView、HarmonyOS ArkUI Web 容器 |
| 药苑之声 | Nuxt 4、Nitro、Drizzle ORM、独立 PostgreSQL、WebSocket 音乐状态同步 |
| 三维校园 | three.js 0.160(WebGL2、EffectComposer 后处理)、Shadow DOM 隔离 |
CPU-web/
├── android/ # Android WebView 壳 + 课表桌面小组件
├── harmony/ # HarmonyOS Stage 工程 + JS Bridge
├── ios/ # 旧版 iOS WebView 客户端(独立保留)
├── ios_next/ # 当前 SwiftUI iOS 客户端 + Apple Watch + WidgetKit
├── desktop/ # Electron 桌面端(不参与主站部署链路,单独构建)
├── server/
│ ├── prisma/ # Prisma schema、迁移、种子数据
│ ├── scripts/ # 调试脚本与数据修复脚本
│ └── src/
│ ├── routes/ # auth / forum / jwxt / payments / tools / admin 等接口
│ ├── services/ # 公告抓取、教务、QQBot、赞助、AI 审核、Filestore 等服务
│ ├── middleware/ # 鉴权、参数校验、错误处理
│ └── utils/ # JWT、密码、响应格式、客户端识别等工具
├── voicehub/ # 药苑之声:原 VoiceHub 全量源码 + CPU-web 用户桥接
├── web/
│ ├── public/ # PWA manifest、图标、离线缓存脚本、静态资源
│ └── src/
│ ├── api/ # 前端 API 封装
│ ├── components/ # 通用组件、论坛组件、教务组件等
│ ├── data/ # 服务工具元数据
│ ├── layouts/ # 主布局
│ ├── router/ # 路由与守卫
│ ├── stores/ # Pinia 状态
│ ├── styles/ # 全局样式
│ ├── utils/ # 客户端桥接、Markdown、缓存与格式化工具
│ └── views/ # 页面视图
├── deploy.sh # Debian / Ubuntu 一键部署脚本(主站 + 教务代理)
├── deploy-agent.ps1 # Windows 出站教务 Agent 部署脚本
├── deploy-agent.cmd # Windows 命令行入口(自动绕过脚本执行策略)
├── package.json # 根目录脚本入口
└── README.md
>= 22>= 9>= 14npm run install:all
该命令按各目录已提交的 lockfile 依次对根目录、server/、web/ 和 voicehub/ 执行 npm ci,不会改写 lockfile。根目录的 npm install 只安装根目录工具,不再自动安装子项目。
复制示例配置并按部署环境修改:
cp server/.env.example server/.env
核心配置示例:
PORT=3000
NODE_ENV=development
DATABASE_URL="postgresql://user:[email protected]:5432/cpu_web?schema=public"
VOICEHUB_DATABASE_URL="postgresql://user:[email protected]:5432/cpu_web_voicehub"
VOICEHUB_ORIGIN="http://127.0.0.1:3001"
VOICEHUB_PORT=3001
JWT_SECRET="please-change-this-in-production"
JWT_EXPIRES_IN="7d"
JWXT_PROXY_URL=""
JWXT_PROXY_AUTH=""
# 首次保存“管理后台 -> 教务节点”后,节点配置改由数据库接管
# SSO_LOGIN_LOCAL_ENABLED=false
SSO_LOGIN_LOCAL_WEIGHT=1
SSO_LOGIN_TIMEOUT_MS=15000
PROXY_AUTH=""
PROXY_PORT=23334
MEDIA_STORAGE_PROVIDER="local"
MEDIA_STORAGE_IMAGE_PROVIDER="local"
MEDIA_STORAGE_VIDEO_PROVIDER="local"
MEDIA_STORAGE_REMOTE_PREFIXES="forum"
ONEDRIVE_CN_TENANT_ID=""
ONEDRIVE_CN_CLIENT_ID=""
ONEDRIVE_CN_CLIENT_SECRET=""
ONEDRIVE_CN_DRIVE_ID=""
ONEDRIVE_CN_ROOT_PATH="cpu-web-media"
npm run db:setup
该命令会:
prisma db pushVOICEHUB_DATABASE_URL 指向的独立数据库中执行 VoiceHub 原始 Drizzle 迁移VOICEHUB_DATABASE_URL 不能和 DATABASE_URL 指向同一个数据库。VoiceHub 保留了自己的业务表;用户进入药苑之声时,服务端会用 CPU-web 的 HttpOnly 会话确认身份,再在独立库中自动建立/更新不可登录的映射资料。原 VoiceHub 注册、密码登录和 OAuth 接口已停用。
如需清空并重建:
npm run db:reset
npm run dev
默认地址:
3001)Vite 已代理以下路径到后端:
/api/uploads/filestore| 账号 | 密码 | 说明 |
|---|---|---|
alice | 123456 | 普通测试用户 |
bob | 123456 | 普通测试用户 |
carol | 123456 | 普通测试用户 |
admin | admin123 | 管理员 |
补充说明:
school-bot 为种子机器人账号,不用于手动登录。/api/auth/register 仅在开发模式开放。/api/auth/sso-begin 与 /api/auth/sso-login。主站为 Electron 等公开客户端提供 Authorization Code + PKCE(S256)认证流程。Electron 不需要保存 client_secret,应使用系统浏览器打开授权地址,并使用本机回调地址接收授权结果。
OAuth 客户端配置位于服务端环境变量:
OAUTH_CLIENT_ID=cpu-electron
OAUTH_ALLOWED_REDIRECT_URIS=http://127.0.0.1,http://localhost
OAUTH_ALLOWED_REDIRECT_URIS 使用逗号分隔。对于 http://127.0.0.1 和 http://localhost,服务端允许 Electron 使用随机本机端口,例如 http://127.0.0.1:43127/callback。其他地址应配置完整且固定的 origin,生产环境应只配置实际需要的回调 origin。
AI 接口复用站点设置中的 AI 服务池和场景路由,不新增单独的 OAuth AI 密钥配置。AI API 密钥始终只保存在服务端,不能下发给 Electron。
管理后台“AI 管理 → 服务线路”里可以一次配置多个厂家/兼容接口(包括 Ollama),再给拾间 AI、网课解题、智慧发帖、文字审核、歌曲审核、QQ群广告、图片审核和视频审核各排一条线路:第一环是主服务和它的模型,后面依次是回退。某个服务出故障时在这一页把它停用,由它承载的场景立即改走各自线路里的下一环,恢复后自动回到原线路;页面上能看到每个服务最近请求的成败,也能对线路上的服务和模型发一条真实请求做实测。Ollama 地址可填写 http://127.0.0.1:11434,服务端会使用 Ollama 的 OpenAI 兼容 Chat Completions 接口,API Key 可以留空,模型填写本机已安装的模型名,例如 qwen3:8b。Ollama 地址必须能从运行 CPU-web 服务端的机器访问。
state 和 code_verifier。code_challenge。/api/oauth/authorize。code 和原始 state 重定向到 Electron 回调地址。state,然后调用 /api/oauth/token 并提交 code_verifier。Authorization: Bearer <access_token> 调用其他接口。授权码有效期为 60 秒且只能使用一次。access token 当前有效期为 30 天。Electron 应使用系统安全存储,例如 Electron safeStorage,不要将 token 写入渲染进程的 localStorage。
GET /api/oauth/authorize授权端点要求用户已经登录。请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
response_type | 是 | 固定为 code |
client_id | 是 | 默认 cpu-electron |
redirect_uri | 是 | 已配置的回调 origin 下的地址 |
scope | 是 | 空格分隔,支持 openid profile ai |
state | 是 | Electron 生成的随机状态值 |
code_challenge | 是 | PKCE S256 challenge |
code_challenge_method | 是 | 固定为 S256 |
示例:
GET /api/oauth/authorize?response_type=code&client_id=cpu-electron&redirect_uri=http%3A%2F%2F127.0.0.1%3A43127%2Fcallback&scope=openid%20profile%20ai&state=...&code_challenge=...&code_challenge_method=S256
授权成功后回调:
http://127.0.0.1:43127/callback?code=...&state=...
失败时回调会携带 error 和 state 参数。Electron 必须先校验 state,再处理 code。
POST /api/oauth/token使用 application/x-www-form-urlencoded 或 JSON 提交:
{
"grant_type": "authorization_code",
"code": "授权回调中的 code",
"redirect_uri": "http://127.0.0.1:43127/callback",
"client_id": "cpu-electron",
"code_verifier": "发起授权时生成的原始 verifier"
}
成功响应遵循 OAuth2 标准格式,不使用站内通用响应包装:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 2592000,
"scope": "openid profile ai"
}
GET /api/oauth/userinfo请求头:
Authorization: Bearer <access_token>
需要 profile scope。返回用户基础信息、用户等级、AI 每日额度、今日已用次数和剩余次数。aiBalance 表示当前自然日剩余的校园 AI 请求次数,不是金额余额,也不是 Token 余额。
POST /api/oauth/revokeElectron 可以主动撤销 access token:
{
"token": "需要撤销的 access_token",
"client_id": "cpu-electron"
}
接口始终返回 HTTP 200,避免通过响应差异泄露 token 是否存在。撤销后的 token 不能继续调用 OAuth 接口。
POST /api/oauth/v1/responses这是面向外部客户端新增的 OpenAI Responses 兼容代理接口,不是 /api/search/assistant 的直接复用。它复用站点 AI 上游配置和校园 AI 每日额度系统,但只接受 user 和 assistant 输入,不允许客户端提交 system、developer 或未知扩展字段;服务端仍会执行敏感话题拦截。
当站点 AI URL 配置为 /v1/responses 时,服务端使用 Responses API 的 input 格式;配置为 /v1/chat/completions 时,会将 Responses 请求转换为 Chat Completions 的 messages 格式。服务端始终使用后台配置的模型和 API Key,并检查 ai.review.enabled。
流式与非流式调用都会按 OAuth 客户端和服务端模型生成稳定的 prompt_cache_key,并在上游支持时请求 24 小时提示缓存保留。若兼容上游拒绝缓存保留参数或缓存键,服务端会自动降级并重试;接入本代理的聊天与后续 AI 解答功能无需在客户端保存或传递上游缓存参数。
请求头:
Authorization: Bearer <access_token>
Content-Type: application/json
请求体接受常用 Responses 字段,仅支持非流式请求,stream 只能为 false 或省略:
{
"model": "客户端可传入,但服务端始终使用站点配置中的模型",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "请描述这张图片" },
{
"type": "input_image",
"image_url": "data:image/png;base64,...",
"detail": "auto"
}
]
}
],
"temperature": 0.7,
"stream": false
}
纯文本也支持简写形式:"content": "你好",服务端会在发送到 Responses API 前自动转换为 input_text 内容项。图片支持 data:image/jpeg|png|webp|gif;base64,...、http:// 和 https:// URL;HTTP URL 会在转发前自动规范化编码。
需要 ai scope。每次请求最多消耗 1 次当日额度,当前按请求次数计费,不按 Token 数量、字符数或响应长度计费:
stream: true 不受支持。非流式成功响应使用 Responses 格式,并只保留模型文本:
{
"output_text": "回答内容",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{ "type": "output_text", "text": "回答内容", "annotations": [] }
]
}
]
}
每日额度由站点设置中的 assistant.dailyQuotas 根据用户等级决定。上游失败状态码和错误内容会转发给 Electron;成功响应会裁剪为上述格式,服务端不会把上游 API Key 返回给客户端。
| Scope | 权限 |
|---|---|
openid | 表示使用本站 OAuth2 身份 |
profile | 调用 /api/oauth/userinfo 获取用户等级、额度等信息 |
ai | 调用 /api/oauth/v1/responses |
建议 Electron 默认申请 openid profile ai,并在本地明确保存授权 scope。
| 命令 | 说明 |
|---|---|
npm run install:all | 按 lockfile(npm ci)安装根目录、Server、Web 与 VoiceHub 依赖 |
npm test | 运行 server/tests 与 web/tests 的 Node.js 测试(tools/run-node-tests.mjs,可追加文件名过滤;与 CI 一致使用 Node.js 24) |
npm run harmony:generate | 重新生成 HarmonyOS 课表桥接与原生主题配色文件 |
npm run dev | 同时启动前后端 |
npm run dev:server | 只启动后端 |
npm run dev:web | 只启动前端 |
npm run build | 构建 Server、Web 与 VoiceHub |
npm run typecheck | 后端构建 + 前端类型检查 |
npm run db:setup | 推送 schema 并写入种子数据 |
npm run db:reset | 重建数据库并重新写入种子数据 |
npm run start | 启动构建后的后端服务 |
server/| 命令 | 说明 |
|---|---|
npm run dev --prefix server | 后端热重载 |
npm run build --prefix server | prisma generate + TypeScript 编译 |
npm run start --prefix server | 启动主服务 |
npm run proxy:dev --prefix server | 教务代理开发模式 |
npm run proxy --prefix server | 教务代理生产运行 |
npm run db:studio --prefix server | Prisma Studio |
npm run prisma:generate --prefix server | 手动生成 Prisma Client |
npm run forum:recount-stats --prefix server | 重新统计论坛数据 |
web/| 命令 | 说明 |
|---|---|
npm run dev --prefix web | 启动前端开发服务器 |
npm run build --prefix web | 前端类型检查 + 生产构建 |
npm run preview --prefix web | 预览构建结果 |
npm run type-check --prefix web | 仅做前端类型检查 |
后端主要读取 server/.env:
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT | 3000 | 主服务端口 |
NODE_ENV | development | 生产环境请设为 production |
DATABASE_URL | 无 | PostgreSQL 连接串 |
JWT_SECRET | cpu-web-dev-secret | 站内 JWT 签名密钥;生产环境强制至少 32 位,部署脚本会自动生成 |
JWT_EXPIRES_IN | 7d | 站内登录 token 有效期 |
MARKET_ENCRYPTION_SECRET | 从 JWT_SECRET 派生 | 仅用于兼容保留的旧商城数据;当前二手论坛不读取收款资料或电子交付内容 |
BROWSER_SESSION_IDLE_MS | 1800000 | 未勾选“保持登录”时的服务端会话空闲期,默认 30 分钟 |
BROWSER_SESSION_ABSOLUTE_MS | 31536000000 | 勾选“保持登录”时的滑动有效期,默认 365 天;活跃使用会自动续期 |
JWXT_SESSION_IDLE_MS | 31536000000 | 加密教务会话与跨 Agent 密文副本的空闲有效期,默认 365 天 |
CORS_ALLOWED_ORIGINS | 空 | 额外允许的同源 Web 地址,逗号分隔;不要填写通配符 * |
DORM_ELECTRIC_CAMPUS_BASE | http://10.200.13.18:8899 | 仅 Agent 使用的校内宿舍电费接口地址;主服务不读取 |
JWXT_PROXY_URL | 空 | 配置后主服务通过教务代理访问教务/CMS |
JWXT_PROXY_AUTH | 空 | 主服务访问代理时使用的共享密钥 |
JWXT_PROXY_TIMEOUT_MS | 15000 | 主服务调用代理的超时(毫秒) |
JWXT_AGENTS | 空 | 首次启动用的 Agent JSON 配置;后台保存后由数据库配置接管 |
JWXT_CRAWL_AGENT_ID | 空 | 初始公告抓取 Agent;公告抓取不参与负载均衡 |
JWXT_AGENT_PATH | /api/internal/jwxt-agent/connect | Agent 主动连接主服务的 WebSocket 路由 |
JWXT_AGENT_SERVER | 空 | Agent 节点连接的主服务 ws(s) 地址 |
JWXT_AGENT_ID |
补充说明:
site_settings 中,通过管理后台维护。https://你的站点域名/api/storage/onedrive-cn/callback;如果你在后台配置了“网站域名”,系统会优先用它生成回调地址。offline_access、User.Read、Files.ReadWrite.All、Sites.ReadWrite.All,并完成管理员同意。MEDIA_STORAGE_PROVIDER、MEDIA_STORAGE_IMAGE_PROVIDER、MEDIA_STORAGE_VIDEO_PROVIDER 与 ONEDRIVE_CN_* 这些环境变量仍可作为后备方式使用,但新的后台授权流程优先面向管理后台配置。推荐使用出站 Agent 替代 FRP。主服务只暴露一个 WebSocket 路由,校内机器主动连接主服务,因此 Agent 机器不需要公网 IP 或入站端口。
浏览器 ──HTTPS──► 主服务
▲
│ WSS(Agent 主动发起)
┌────────┼────────┐
校内 Agent A 校内 Agent B 其它 Agent
│ │
└──────► 统一认证 / 教务系统
在“管理后台 → 教务节点”中完成配置:
server/logs/agent-remote-update.log。cpu-jwxt-proxy,宿舍电费与教务访问均不再需要该入站代理。Agent 机器使用与主服务相同版本的 server 代码,构建后配置:
JWXT_AGENT_SERVER=wss://your-main-site.example.com/api/internal/jwxt-agent/connect
JWXT_AGENT_ID=campus-a
JWXT_AGENT_TOKEN=后台生成的密钥
JWXT_AGENT_KEY_FILE=.jwxt-agent-identity.json
NODE_ENV=production
REDIS_ENABLED=false
# 校园侧地址不是默认值时才需要设置
DORM_ELECTRIC_CAMPUS_BASE=http://10.200.13.18:8899
然后运行:
./deploy.sh agent-init
# 后续更新
./deploy.sh agent-update
# 查看连接日志
./deploy.sh agent-logs
# 开启、查看或关闭 Linux 开机自启
./deploy.sh agent-autostart
./deploy.sh agent-autostart-status
./deploy.sh agent-autostart-off
Windows Agent 使用仓库根目录的脚本:
# 首次部署
.\deploy-agent.cmd init
# 后续更新
.\deploy-agent.cmd update
# 查看连接日志
.\deploy-agent.cmd logs -Lines 200
# 查看状态
.\deploy-agent.cmd status
# 开启当前用户登录后的自动启动
.\deploy-agent.cmd autostart
# 查看自动启动状态
.\deploy-agent.cmd autostart-status
# 关闭自动启动
.\deploy-agent.cmd autostart-off
Windows 脚本会检查并安装 Node.js 22+ 与 PM2;更新前会停止 Agent,避免运行中的 Prisma DLL 阻止构建,失败时会尝试恢复原进程。执行 autostart 后,脚本会为当前 Windows 用户注册登录启动项,并以隐藏窗口启动 Agent,无需管理员权限;启动失败原因会写入 server\logs\jwxt-agent-autostart.log。仓库路径变化后重新执行一次 autostart 即可更新启动项。
只配置了 JWXT_AGENT_* 且没有 DATABASE_URL 的 Agent 机器,也可以继续执行 ./deploy.sh update,脚本会自动识别并切换到 Agent 更新流程。旧部署使用 proxy-update 时,如果检测到 JWXT_AGENT_*,也会自动迁移到 Agent 流程。
如果主服务前面有 Nginx,需要允许 WebSocket Upgrade:
location /api/internal/jwxt-agent/connect {
proxy_pass http://127.0.0.1:23333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 90s;
}
JWXT_AGENTS、JWXT_CRAWL_AGENT_ID、SSO_LOGIN_LOCAL_ENABLED 和 SSO_LOGIN_LOCAL_WEIGHT 仍可用于首次启动。管理后台首次保存后,数据库中的配置成为权威配置。旧 LOGIN_AGENT_* 变量、login-agent 命令、JWXT_PROXY_URL 和 SSO_LOGIN_NODES 路径保留用于平滑迁移,但新部署无需 FRP,也无需运行旧 proxy 服务。
安全与部署注意:
JWXT_AGENT_KEY_FILE,再在后台“解除身份固定”,随后启动 Agent。Linux 部署脚本会设置 0600,Windows 脚本会收紧 ACL。JWXT_SESSION_IDLE_MS 控制,默认 365 天。id.cpu.edu.cn 统一认证;服务端复用统一认证会话自动向 jwxt.cpu.edu.cn/jsxsd/sso.jsp 换取教务会话,不使用教务独立账号密码或验证码登录。课表、成绩、考试、校历和培养方案全部从 jwxt.cpu.edu.cn/jsxsd 读取;登录入口沿用学校在统一认证中登记的 jsxsd.cpu.edu.cn 服务地址,不从该域名读取任何教务数据。各域 Cookie 都只保存在加密 CookieJar 中。JWXT_SESSION_SYNC_KEYS=新密钥,旧密钥;轮换时先把新密钥放到首位,等待超过最长会话有效期后再删除旧密钥。多主服务实例必须共享同一密钥环。JWT_SECRET。目前 Agent WebSocket 会话属于接收连接的主服务实例,网关层需保证相关教务请求到达同一主实例。浏览器侧防护:
HttpOnly、Secure、SameSite=Strict 的不透明会话 Cookie,旧版 Local Storage token 会在首次迁移后删除。Cache-Control: no-store。前端启用强制 CSP、Trusted Types 与 DOMPurify;生产响应同时启用 HSTS、nosniff、点击劫持和权限策略防护。仓库自带 deploy.sh,面向 Debian / Ubuntu:
chmod +x deploy.sh
./deploy.sh
./deploy.sh update
./deploy.sh update-all
./deploy.sh swap-init 6
./deploy.sh build-status
./deploy.sh artifact-check
./deploy.sh nginx-http-fix
./deploy.sh restart
./deploy.sh logs
./deploy.sh status
# 开启、查看或关闭主服务 Linux 开机自启
./deploy.sh autostart
./deploy.sh autostart-status
./deploy.sh autostart-off
./deploy.sh reset-db
脚本可以直接帮你创建数据库与账号:
./deploy.sh postgres-init
./deploy.sh
如果你已有现成 PostgreSQL:
./deploy.sh postgres-config "postgresql://user:[email protected]:5432/cpu_web?schema=public"
# 远程数据库需显式提供第二个独立数据库;本机 PostgreSQL 会自动创建 cpu_web_voicehub
./deploy.sh voicehub-postgres-config "postgresql://user:[email protected]:5432/cpu_web_voicehub"
./deploy.sh update
部署脚本会同时构建并由 PM2 管理 cpu-web 与 cpu-voicehub。./deploy.sh autostart 会为这两个主服务生成独立的 systemd 启动单元;Agent 机器使用 ./deploy.sh agent-autostart,两套单元互不代替,也不会通过全局 pm2 resurrect 拉起无关进程。请用实际运行对应 PM2 进程的同一个 Linux 账号执行自启命令,脚本会在需要写入 systemd 时自动调用 sudo。关闭自启不会停止当前正在运行的进程。
./deploy.sh update 会根据上次成功部署以来的变更路径,只安装、构建和重启受影响的子项目:只改主站不会重装、迁移、构建或重启药苑之声;只改药苑之声也不会重建主站。部署基线记录在 Git 元数据中,因此即使先手动执行 git pull,随后运行 update 也不会漏掉尚未部署的改动;首次启用基线记录时会安全地完整更新一次。依赖目录变更才会执行对应项目的 npm ci,Prisma 目录变更才会同步主站数据库。需要完整重建时,使用 ./deploy.sh update-all。主站把 /voicehub/(含 WebSocket)反向代理到仅监听 127.0.0.1:23335 的 Nuxt/Nitro 进程;可用 ./deploy.sh voicehub-logs 单独查看日志。
低配生产机应先执行一次 ./deploy.sh swap-init 6。该命令只使用固定路径 /swapfile-cpu-web,创建并持久启用 6 GiB Swap,同时设置 vm.swappiness=10;已启用时可重复执行。所有本机 npm ci 与生产构建都会经过资源护栏:低 CPU/IO 优先级、默认 2304 MiB Node heap、2 GiB 磁盘余量检查,并在低内存机器没有至少 2 GiB Swap 时拒绝冒险编译。可用 ./deploy.sh build-status 查看实时资源状态。
GitHub Actions 是正式生产制品的权威构建来源,本地构建只用于按需开发验证,不是强制推送门禁或正式制品;详细门禁与完成判定见 docs/production-requirements.md。main 的每次推送(包括仅修改文档或工作流)都会触发 Linux deployment artifact GitHub Actions。工作流在 Ubuntu/Node 24 上构建 server/dist、web/dist 与 voicehub/.output,生成绑定完整提交 SHA 的清单和三个 SHA-256 校验值,并上传短期 Actions 审计制品 cpu-web-linux-<SHA>;每个 SHA 的构建都会完整运行,不会因后续推送被取消。随后的发布任务只在该 SHA 仍是 main 最新提交时,才强制更新仅含当前制品的 deploy-artifacts Git 传输分支和同名预发布资产(用于审计和手动取回),较旧的运行不会覆盖较新的制品。./deploy.sh update(包括 DEPLOY_UPDATE_MODE=maintenance)只经仓库当前 origin(可为已配置的 GitHub 镜像)取得制品,等待并验证它与当前 HEAD 完全匹配后再暂存、原子发布和健康检查。只有显式设置 DEPLOY_ARTIFACT_URL 时才会启用额外的 HTTP 下载备用源,默认不会绕过 origin 直连 GitHub。正式生产发布应使用 DEPLOY_BUILD_MODE=ci;默认的 auto 与 ci 相同,精确 SHA 的 GitHub 制品缺失时失败关闭,不会回退到生产机编译。只有在明确授权的应急维护更新中显式设置 DEPLOY_BUILD_MODE=local 才会在本机编译,蓝绿更新始终拒绝 local。./deploy.sh artifact-check [完整提交 SHA] 只下载、校验并缓存制品,不发布文件。解压后的制品缓存在 .git/cpu-web-deploy-artifacts/,默认只保留最近使用的 3 个(可用 DEPLOY_ARTIFACT_CACHE_KEEP 调整),本次使用的制品和上次成功部署的制品不会被清理。
./deploy.sh redis-init 会优先启用发行版自带的 redis-server systemd 服务。若日志明确出现 libjemalloc.so.2: failed to map segment,且原服务启用了 MemoryDenyWriteExecute,脚本只为该服务写入最小 drop-in 兼容覆盖并重新验证;切换失败时会恢复临时 daemon,避免部署期间直接丢失 Redis。
若宝塔把 HTTP 与 HTTPS 合并在同一个 Nginx server 中,可执行一次 ./deploy.sh nginx-http-fix。脚本会备份 /www/server/panel/vhost/nginx/cpu.lizmt.cn.conf,把 80 端口拆成只负责跳转的入口,保留宝塔 ACME 验证 include,并把 cputime.cn、www.cputime.cn、cpu.lizmt.cn 统一 301 到 https://cputime.cn;只有 nginx -t、reload 和本机响应头回读全部通过才保留新配置,否则自动恢复备份。前端还包含同域 HTTP 页面兜底跳转,用于处理内置 WebView 未正确遵循代理跳转的情况。
生产环境的站点超级管理员也可以在“管理后台 → 更新部署”中触发同一套增量更新。后台入口只执行固定的 bash deploy.sh update,使用独立 runner 和 Git 元数据目录中的文件锁、状态及脱敏日志,因此主服务被 PM2 重载后仍能继续查询结果。首次加入该功能时仍需按原方式部署一次;如需紧急关闭后台部署,可在 server/.env 设置 ADMIN_DEPLOY_ENABLED=false 并重启主服务。
./deploy.sh proxy-init
./deploy.sh proxy-update
./deploy.sh proxy-restart
./deploy.sh proxy-logs
部署脚本默认行为:
233332333423335(仅本机)pm2当前多端能力概览:
/schedule 已配置 PWA,可离线打开最近一次课表缓存。CPUAndroid Bridge,并提供课表桌面小组件。CPUHarmony 与 CPUAndroid 兼容桥接。桥接脚本 NativeWebCompatibility.js 和原生配色 SchedulePalettes.ets 由 harmony/bridge/ 及其引用的 web/src 课表与主题代码生成:修改相关代码后在根目录运行 npm run harmony:generate 并一起提交,否则 CI 的“Verify HarmonyOS schedule bridges”步骤会失败。/api/site/downloads/android-app,会跳转到 server/src/releases/android.json 指定的企业盘 APK;web/public/downloads/ 只保留当前版本,供 CI 校验签名与版本。web/public/sw.js 当前只重点缓存 /schedule 相关静态资源,不是整站离线。web/src/views/services/campus3d/ 下的场景代码、界面、样式、图片和 OSM 数据由独立的建模工程导出生成,不要手改;联机位置只存在内存里,接口见 三维校园。prisma generate 或 server 构建可能失败;先停止正在运行的 Node 后端进程再试。npm test 运行 server/tests 与 web/tests 中除需要 PostgreSQL 或 web/dist 的少数文件外的全部 Node.js 测试,CI 在 Node.js 24 上执行同一命令(部分引用 server/src 的 Web 测试在 Node 22 上会因 tsx 的 CommonJS/ESM 互操作失败);此外日常校验依赖后端构建、前端类型检查与前端构建。药大拾间由学生自主开发和维护,仅供个人学习、技术研究、校园公益及非商业社区使用。严禁未经项目维护方书面授权,将药大拾间品牌、官方构建、在线服务、生产资源或运营成果用于收费服务、商业推广、转售、付费部署及其他营利活动。
站点名称、Logo、域名、官方安装包、生产配置、密钥、用户数据、学校标识、第三方素材和部署数据不随代码许可授权。不得以“药大拾间官方”“学校官方”或其他可能造成混淆的名义运营衍生项目。
仓库中可识别的开源代码与第三方组件仍按各自许可证执行;本说明不会改变 OCS、Electron 等第三方项目的既有许可,也不追溯撤销已经按既有许可证取得的权利。
本项目建议采用 AGPL-3.0-or-later 作为代码开源协议;正式发布时请以仓库顶层 LICENSE 文件为准。
选择 AGPL 的原因是本项目主要以 Web 服务形式运行。社区可以自由学习、部署、修改和贡献代码;如果修改后的版本通过网络向用户提供服务,也应向相应用户开放对应源码。药大拾间品牌、官方构建、在线服务和生产资源不包含在该代码许可中,严禁未经授权用于商业用途。
除代码外,站点名称、视觉标识、域名、学校相关标识、用户数据、生产环境配置、密钥、第三方素材和部署数据均不随本协议授权。正式部署前请自行评估合规、隐私、安全和运维风险。
网页端提供游戏、工具与站点扩展的上传、私有预览、站长审核、公开使用和版本管理。扩展通过受控 SDK 使用公开信息、界面提示等能力;开屏与弹窗由站长单独启用确切版本。开发者可从 /workshop/develop 下载三类模板并生成中文 Agent 开发提示词;运行协议、SDK、打包方式与持久存储配置见创意工坊开发与维护。
| 空 |
| Agent ID,必须与后台一致 |
JWXT_AGENT_TOKEN | 空 | Agent 密钥,必须与后台生成值一致 |
JWXT_AGENT_KEY_FILE | .jwxt-agent-identity.json | Agent 的 RSA 加密身份文件;必须持久化并限制为运行账户可读 |
JWXT_LOCAL_AGENT_KEY_FILE | .jwxt-local-agent-identity.json | 本机参与教务池时使用的 RSA 加密身份文件 |
JWXT_SESSION_SYNC_KEYS | 空 | 本地敏感会话缓存的轮换密钥环,按“新密钥,旧密钥”排列,每项至少 32 位 |
JWXT_SESSION_SYNC_KEY | 从 JWT_SECRET 派生 | 单密钥兼容配置;新部署优先使用 JWXT_SESSION_SYNC_KEYS |
SSO_LOGIN_NODES | 空 | 统一认证登录远端节点 JSON 数组;节点字段为 id、可选 name、url、可选 auth、enabled、weight |
SSO_LOGIN_LOCAL_ENABLED | false | 兼容环境变量:本机是否参与完整教务服务池(登录与查询绑定) |
SSO_LOGIN_LOCAL_WEIGHT | 1 | 本机教务服务节点权重,范围 1..100 |
SSO_LOGIN_TIMEOUT_MS | JWXT_PROXY_TIMEOUT_MS | 登录池单节点请求超时(毫秒) |
PROXY_AUTH | 空 | 教务代理端校验密钥 |
PROXY_PORT | 23334 | 教务代理监听端口 |
MEDIA_STORAGE_PROVIDER | local | 媒体资源默认存储后端;可设为 local 或 onedrive-cn,未单独指定图片/视频时作为回退值 |
MEDIA_STORAGE_IMAGE_PROVIDER | 空 | 图片资源存储后端;可单独设为 local 或 onedrive-cn |
MEDIA_STORAGE_VIDEO_PROVIDER | 空 | 视频资源存储后端;可单独设为 local 或 onedrive-cn |
MEDIA_STORAGE_REMOTE_PREFIXES | forum | 哪些 /uploads/... 前缀走远端存储,逗号分隔 |
ONEDRIVE_CN_TENANT_ID | 空 | 世纪互联版 Microsoft 365 / Entra 租户 ID |
ONEDRIVE_CN_CLIENT_ID | 空 | 世纪互联应用注册的客户端 ID |
ONEDRIVE_CN_CLIENT_SECRET | 空 | 世纪互联应用注册的客户端密钥 |
ONEDRIVE_CN_DRIVE_ID | 空 | SharePoint 文档库或 OneDrive 对应的 Drive ID |
ONEDRIVE_CN_ROOT_PATH | 空 | 远端根目录下的存储子路径,例如 cpu-web-media |
DESKTOP_APP_DOWNLOAD_URL | 空 | PDS 解析失败时的可选备用下载页,必须是 https。留空时不会再回落到旧蓝奏云链接 |
DESKTOP_APP_DOWNLOAD_PASSWORD | 空 | 备用网盘分享页的提取码;PDS 直链模式下不显示 |
DESKTOP_PDS_SHARE_URL | 桌面端固定文件夹分享 | 阿里云盘企业版(PDS)文件夹分享链接。建议内含 Windows / macOS 子目录;服务端递归选择最后更新的 .exe 与 Apple Silicon .dmg,每次请求临时换取直链并由本站稳定地址 302 跳转 |
DESKTOP_PDS_SHARE_PASSWORD | 空 | PDS 分享提取码;无提取码时留空 |
ANDROID_APP_PDS_SHARE_URL | 复用桌面端 PDS 分享 | 可选的安卓专用 PDS 文件夹分享链接;未配置时复用 DESKTOP_PDS_SHARE_URL,选择 server/src/releases/android.json 指定的 APK,不因上传了更高版本而自动发布 |
ANDROID_APP_PDS_SHARE_PASSWORD | 复用桌面端 PDS 提取码 | 安卓专用 PDS 分享提取码;仅在配置安卓专用分享链接时使用 |
ASSESSMENT_TOOL_PDS_SHARE_URL | 复用桌面端 PDS 分享 | 综测填表工具的可选独立分享链接;默认在桌面端分享中选择版本号最高的 药大拾间-综测填表工具-v*.zip |
ASSESSMENT_TOOL_PDS_SHARE_PASSWORD | 复用桌面端 PDS 提取码 | 综测填表工具独立分享的提取码;仅在配置独立分享链接时使用 |
DESKTOP_APP_VERSION | 空 | 桌面端安装包版本号的可选显式覆盖;Windows 与 macOS 的 PDS 标准文件名会自动提取版本 |
CAMPUS_MAP_PDS_SHARE_URL | 校园地图原图固定分享 | 校园地图原图的阿里云盘企业版分享链接;服务端选择其中体积最大的图片,为页面查看与“下载原图”分别换取内联/附件直链并 302 跳转,原图不进入前端包,也不经过本站带宽 |
CAMPUS_MAP_PDS_SHARE_PASSWORD | 空 | 校园地图原图 PDS 分享提取码;无提取码时留空 |
PG_DUMP_BIN | pg_dump | 后台数据库备份使用的命令路径 |