CLI 工具参考
本页面汇总 ClassIntra 常用命令行工具,覆盖 pnpm、PM2、构建脚本、版本管理与数据库迁移。
pnpm 命令
ClassIntra 使用 pnpm workspace 管理 monorepo,根目录 package.json 中定义了快捷脚本。
主要命令
| 命令 | 实际执行 | 说明 |
|---|---|---|
pnpm dev | pnpm --parallel --filter server --filter client run dev | 并行启动前后端开发服务器 |
pnpm dev:server | pnpm --filter server run dev | 仅启动后端 |
pnpm dev:client | pnpm --filter client run dev | 仅启动前端(Vite dev) |
pnpm build | 更新 version.json + cd client && pnpm run build | 构建前端生产产物 |
pnpm start | pnpm --filter server run start | 启动后端生产服务(node src/app.js) |
pnpm install | 安装全部 workspace 依赖 | 含 server / client / apps/* |
各 workspace 内部脚本
| Workspace | 命令 | 说明 |
|---|---|---|
server | pnpm dev / pnpm start | node src/app.js(同义) |
client | pnpm dev | vite dev server,端口 5001 |
client | pnpm prebuild | node scripts/prebuild.js(版本号递增) |
client | pnpm build | vite build(构建产物到 dist/) |
client | pnpm preview | vite preview(预览构建产物) |
用法示例
# 1. 全新部署
pnpm install
cp server/.env.example server/.env
# 编辑 .env 至少设置 JWT_SECRET
# 2. 开发
pnpm dev
# → 前端 http://localhost:5001
# → 后端 http://localhost:9001
# → WebSocket ws://localhost:10001
# 3. 生产构建
pnpm build
# 4. 启动生产服务
pnpm start
# → http://localhost:9001
# 5. 仅启动后端调试
pnpm dev:server
# 6. 仅启动前端调试(需后端已运行)
pnpm dev:client
# 7. 单独安装某 workspace 依赖
pnpm --filter server add <pkg>
pnpm --filter client add <pkg>pnpm 加速
国内网络可在 .npmrc 配置淘宝镜像:
registry=https://registry.npmmirror.comPM2 命令
PM2 用于生产环境进程守护,项目自带 ecosystem.config.js 配置。
进程管理
| 命令 | 说明 |
|---|---|
pm2 start ecosystem.config.js | 启动 ClassIntra 服务 |
pm2 start ecosystem.config.js --update-env | 重启并刷新环境变量 |
pm2 stop classintra-server | 停止进程 |
pm2 restart classintra-server | 重启进程 |
pm2 reload classintra-server | 零停机重启(cluster 模式) |
pm2 delete classintra-server | 从 PM2 列表移除 |
pm2 save | 保存进程列表(用于 resurrect) |
pm2 resurrect | 恢复上次保存的进程列表 |
pm2 startup | 生成开机自启命令 |
查看状态
| 命令 | 说明 |
|---|---|
pm2 status / pm2 list / pm2 ls | 进程列表 |
pm2 describe classintra-server | 详细信息(路径、env、重启次数等) |
pm2 monit | 实时监控面板(CPU/内存/日志) |
pm2 info classintra-server | 进程元信息 |
日志
| 命令 | 说明 |
|---|---|
pm2 logs classintra-server | 实时查看 stdout + stderr |
pm2 logs classintra-server --lines 200 | 最近 200 行 |
pm2 logs classintra-server --err | 仅错误日志 |
pm2 logs classintra-server --out | 仅标准输出 |
pm2 flush classintra-server | 清空日志文件 |
pm2 reloadLogs | 重新加载日志 |
完整流程示例
# 1. 启动
pm2 start ecosystem.config.js
# 2. 验证
pm2 status
pm2 logs classintra-server --lines 30
# 3. 保存(用于开机自启)
pm2 save
# 4. 配置开机自启
pm2 startup
# 按 PM2 返回的命令执行:
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>
# 5. 重启并刷新环境变量(修改 .env 后)
pm2 restart ecosystem.config.js --update-env
# 6. 停止
pm2 stop classintra-server
# 7. 完全移除
pm2 delete classintra-serverecosystem.config.js 与 .env
ecosystem.config.js 会自动读取 server/.env 并注入到 PM2 进程环境变量。修改 .env 后必须 --update-env 重启,否则旧值仍生效。
构建脚本
项目根目录提供多个一键脚本,简化部署操作。
build.bat(Windows 一键构建)
build.bat执行流程:
- 安装依赖(server 用
--prod,client 正常安装) - 更新
server/version.json(写入buildHash与buildTime) - 在
client/目录执行npx vite build - 产物输出到
client/dist/
build.bat 用途
适合无 pnpm 的 Windows 服务器,自动降级到 npm。
start.sh(Linux 开发启动)
./start.sh执行流程:
- 初始化数据库(
node src/utils/init-db.js) - 后台启动后端(
node src/app.js,端口 9001) - 后台启动前端(
npx vite --host,端口 5001) Ctrl+C优雅停止两个进程
start.sh 仅开发
start.sh 使用 Vite dev server,仅用于开发。生产环境请使用 pnpm build && pnpm start 或 PM2。
start.bat(Windows 开发启动)
start.bat在新窗口分别启动后端与前端 dev server。
start-prod.bat(Windows 生产启动)
start-prod.bat生产模式一键启动(仅后端,无 Vite dev server)。
CI.bat(CI 构建)
CI.batCI 环境使用,自动安装依赖、构建、跑测试。
build-better-sqlite3.bat(原生模块编译)
build-better-sqlite3.bat仅在预编译包不可用时手动编译 better-sqlite3,正常情况下 .npmrc 的 build_from_source=false 已自动处理。
构建产物回收与耗时
为什么 dist/assets 会越滚越大
client/vite.config.mjs 关掉了 emptyOutDir(本机构建环境禁止构建前批量删目录, 否则 Vite 清空 outDir 会被安全删除守卫拦下,构建中途失败且线上产物已被删残)。 副作用是旧的带哈希 chunk 永不回收 —— 实测曾堆到 11920 个文件 / 786MB(index-*.js 就有 320 份)。
client/scripts/prune-dist.js 负责在构建后精确回收,由 pruneDistPlugin 自动调用:
cd client
node scripts/prune-dist.js # 回收(默认只删 24h 之前的残留)
node scripts/prune-dist.js --dry # 只报告不删
node scripts/prune-dist.js --keep-hours 0 # 水位放宽到 0 小时(激进)
SKIP_PRUNE=1 npx vite build # 本次构建跳过回收存活判定有四道(任意一道判「还在用」就保留):
- 本轮构建的
dist/.vite/manifest.json(file/css/assets) dist根目录所有 html 及其正文引用- 不动点扫描:存活文件正文里的资源名迭代入队(覆盖
./chunk-xxx.js这类相对引用、 html 内联脚本、css 的url()字体),保证 legacy chunk 图与字体整条链路不被误删 - 24 小时水位:兜住「用户已打开但尚未刷新的旧页面」——它仍可能去拉上一代的懒加载 chunk
首次回收实测
--dry 报告可回收 10490 个 / 662.4 MB,执行后 dist 从 786MB 降到 104MB(余量为当天的历史代际,次日构建自动清掉)。
构建耗时构成(2026-09-29 实测,2663 个模块)
| 阶段 | 耗时 |
|---|---|
| 模块转换(Rollup transform) | ~18s |
| 打包 + 压缩 + legacy 转译 | ~160s |
| 收尾(体积统计/写盘) | ~20s |
| 合计 | ~3 分 20 秒 |
插件级归因:vite:legacy-post-process 累计 1,271,129 ms(208 次调用、单次最高 86s,并行执行故累计远大于墙钟), 即 @vitejs/plugin-legacy 用 Babel 把所有 chunk(含 1MB 的 mermaid / video.js)再转译一遍,占构建时间约 85%。
LEGACY_CHUNKS=0 快速构建
cd client && LEGACY_CHUNKS=0 npx vite build # 3m20s → 35s(5.6 倍)代价:不再产出 nomodule 的 legacy bundle,旧浏览器没有兜底(现代 bundle 与 polyfills 不受影响)。 校园平板基线是 Chrome 80,原生支持 ESM、加载的是 type="module" 的现代 bundle, 所以理论上可关;但是否切换应由真机实测后决定,因此默认仍为开启。
产物冒烟检查
回收会删掉上千个旧文件,「删了不该删的」只有真实页面才能验出来:
cd client
node scripts/smoke-dist.mjs # 断言启动屏退场 + #app 挂载 + 无产物 404脚本用 CDP 驱动无头 Chrome 打开首页,区分「产物失败」(必须为 0,影响退出码) 与「接口 4xx」(未登录时的 401,预期内)。截图输出到 logs/smoke/。
版本管理
ClassIntra 使用 server/version.json 管理版本,由 client/scripts/prebuild.js 在每次 pnpm build 时自动维护。
version.json 结构
{
"version": "1.0.5",
"lastBuiltVersion": "1.0.4",
"buildHash": "l1a2b3c4d5e6f7g8",
"buildTime": "2025-08-21T08:00:00.000Z",
"changelog": "fix: 修复 WebSocket 断线重连\nfeat: 添加主题切换快捷键",
"minClientVersion": "1.0.0",
"forceUpdate": false,
"updateUrl": ""
}| 字段 | 说明 |
|---|---|
version | 当前版本号(MAJOR.MINOR.PATCH) |
lastBuiltVersion | 上次构建版本号 |
buildHash | 构建哈希(8 字符随机串) |
buildTime | 构建时间(ISO 8601) |
changelog | 自动从 git log 生成 |
minClientVersion | 客户端最低兼容版本 |
forceUpdate | 是否强制更新(前端检测后自动刷新) |
updateUrl | 更新下载地址(可选) |
自动递增规则
prebuild.js 在每次 pnpm build 时执行:
- 读取
version.json当前版本 - PATCH 自动 +1(如
1.0.4→1.0.5) - 若 MAJOR 或 MINOR 变化(手动修改),PATCH 归零
- 从
git log提取自上次构建以来的 commit message 生成 changelog - 写入
buildHash、buildTime、changelog - 同步更新根目录
CHANGELOG.md
手动升版本
要发布大版本时,手动编辑 version.json 改 MAJOR/MINOR:
{ "version": "2.0.0", ... }下次 pnpm build 时,prebuild 会识别到 MAJOR/MINOR 已变化,PATCH 归零并保留新版本号。
查询当前版本
# 命令行
cat server/version.json | python -m json.tool
# API
curl http://localhost:9001/api/system/version
# 前端
# 桌面 → 设置 → 关于强制更新
将 version.json 的 forceUpdate 设为 true,前端通过 /api/system/heartbeat 检测后强制刷新:
# 通过 API 设置(仅班管)
curl -X POST http://localhost:9001/api/system/set-version \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.5",
"forceUpdate": true,
"changelog": "紧急修复:修复 JWT 泄露漏洞,请立即更新",
"minClientVersion": "1.0.5"
}'强制更新影响
设为 true 后,所有客户端会在下一次心跳检测时自动刷新页面。建议仅在紧急修复时使用,避免上课期间强制刷新。
数据库迁移
自动迁移
服务启动时,server/src/utils/migration-runner.js 自动扫描 server/src/migrations/ 目录,按文件名顺序执行未应用的迁移。
# 启动服务即触发迁移
pnpm start
# 或
pm2 restart classintra-server
# 日志会输出:
# [migration-runner] 当前版本: 0
# [migration-runner] 执行迁移 001_baseline (version 1)
# [migration-runner] 执行迁移 002_add_index (version 2)
# [migration-runner] 迁移完成,当前版本: 2手动执行
# 单独运行迁移脚本
cd server
node -e "
var runner = require('./src/utils/migration-runner');
runner.runAll().then(function() {
console.log('迁移完成');
process.exit(0);
}).catch(function(e) {
console.error('迁移失败:', e);
process.exit(1);
});
"查看迁移状态
# 已执行的迁移
sqlite3 server/database/classintra.db "SELECT * FROM schema_version ORDER BY version;"
# 输出示例:
# 1|baseline|2025-08-01 08:00:00
# 2|add-posts-index|2025-08-15 14:30:00重置数据库
危险操作
以下操作会清除所有数据,仅在开发环境或确认数据已备份时使用。
# 方式一:删除数据库文件后重启(自动重建)
cd server
rm -f database/classintra.db database/classintra.db-wal database/classintra.db-shm
node src/app.js
# 自动执行 init-db.js 与所有迁移
# 方式二:使用 reset-db.js(如存在)
node src/utils/reset-db.js
# 方式三:通过迁移 down 回滚(如迁移文件提供 down)
sqlite3 server/database/classintra.db "DELETE FROM schema_version WHERE version >= 2;"
# 删除迁移 2 创建的表/索引
# 再启动服务,会重新执行迁移 2编写迁移文件
// server/src/migrations/003_add_reminders.js
module.exports = {
version: 3,
name: 'add-reminders-table',
up: function(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
remind_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
done INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_reminders_user ON reminders(user_id);
CREATE INDEX IF NOT EXISTS idx_reminders_time ON reminders(remind_at);
`);
},
down: function(db) {
db.exec('DROP TABLE IF EXISTS reminders;');
}
};迁移规范
- 必须幂等:使用
CREATE TABLE IF NOT EXISTS、CREATE INDEX IF NOT EXISTS - 必须可回滚:提供
down函数(虽然目前 migration-runner 不会自动调用,但保留备用) - 小步前进:一个迁移只做一件事
- 不修改现有列:使用新迁移加列(
ALTER TABLE ... ADD COLUMN),避免数据迁移
质量门与生态工具
ClassIntra 提供一组验证脚本,覆盖模块化、动效规范、manifest schema 与市场应用审查。
质量门
| 命令 | 脚本 | 检查内容 |
|---|---|---|
pnpm verify:modules | scripts/modularity-verify.js | 删除式自测:无插件 / 无可选业务模块时仍能 build + boot + 冒烟 |
pnpm verify:motion | scripts/motion-verify.js | 动效与视觉规范(E1–E8,扫描 client/src + apps/) |
pnpm verify:schema | scripts/schema-verify.mjs | manifest schema 前后端一致性 + 42 项断言 |
pnpm verify:all | — | 依次跑上述三项 |
# 一次跑完全部质量门(提交前建议执行)
pnpm verify:all市场应用审查
# 审查单个市场应用
pnpm review:market market-apps/gomoku
# 机器可读输出(CI 集成用)
node scripts/market-review.mjs market-apps/gomoku --json按五组检查:A manifest 合规 · B Chrome 80 语法 · C CSS 兼容 · D 危险 API · E 资源回收。 存在错误时退出码为 1,可直接用于 CI 门禁。
应用脚手架
# 生成市场应用(纯 JS,第三方分发)
pnpm create:app my-tool --label "我的工具"
# 带后端路由与建表 SQL
pnpm create:app my-tool --label "我的工具" --with-backend
# 生成官方内置应用(.vue,进 apps/)
pnpm create:app my-app --label "我的应用" --kind official --dir apps| 选项 | 说明 |
|---|---|
--label <名称> | 显示名称(默认由应用名推导) |
--dir <路径> | 输出目录(默认 market-apps/) |
--kind <类型> | market(默认)或 official |
--with-backend | 生成后端路由、建表 SQL 与后端 README |
--color <hex> | 主题色(默认 #007AFF) |
--force | 目标目录已存在时覆盖 |
生成的模板本身就是「正确示例」——已预置 ES5 语法、--ci-* 令牌消费、context.app.onDestroy 四类资源回收,可直接通过 review:market 审查(0 错误 0 警告)。
应用 / 主题 / 小组件脚手架(三合一)
create:app 面向市场应用;官方内置应用(apps/)、主题(themes/)与桌面小组件用 scripts/scaffold.js 三合一脚手架:
# 官方内置应用(自动附满铺图标模板 icon.svg)
node scripts/scaffold.js app my-app --label "我的应用" [--route /my-app]
# 主题(type 必须为 light|dark,theme-loader 硬性契约)
node scripts/scaffold.js theme my-theme --name "我的主题" --type dark
# 桌面小组件(自动把声明合并进应用 manifest 的 frontend.widgets[],无需手动编辑)
node scripts/scaffold.js widget my-app my-clock --name "时钟"生成的 icon.svg 是直角满铺底(根 rect 无 rx/ry)——圆角由 AppIcon 容器 CSS 统一裁切,资产内烘焙圆角会在四角露出透明缝隙(详见 第三方应用开发 § 图标满铺规范)。主题 tokens 与小组件契约详见 主题开发 与 小组件。
校验器 diag
与运行时同一套 manifest schema 校验器,覆盖三类模块 + 兼容性 lint:
| 命令 | 检查内容 |
|---|---|
node scripts/diag.js app [name] | 应用 manifest 规范 + 文件完整性 + 路由冲突(含 widgets[].component 存在性) |
node scripts/diag.js plugin [name] | 插件校验(backend 必有 + mountPath 跨类冲突检测) |
node scripts/diag.js theme [id] | 主题硬性契约(type = light/dark + TOKENS 导出)+ tokens 冒烟 |
node scripts/diag.js compat [name] | Chrome 80 兼容性 lint(扫描 apps/* + market-apps/* 前端) |
node scripts/diag.js all | 全部(自动附带 compat) |
compat 分级规则(apps/ 经 vite 构建可转译语法,market-apps/ 直出无任何兜底):
| 级别 | 规则 |
|---|---|
| FAIL(直出型专属) | ?. ?? &&= ||= ??=(语法类,Chrome 80 无法解析) |
| FAIL(一律) | structuredClone replaceChildren .at() .findLast(运行时 API,构建也不可转译) |
| FAIL(一律) | CSS aspect-ratio / inset / dvh / :is() / :where() |
| FAIL(直出)/ WARN(构建型) | CSS gap(构建型 flex gap 有 polyfill,grid 需 grid-gap) |
| WARN | backdrop-filter 缺 -webkit- 前缀 |
扫描自动跳过块注释与 // 行注释(防文档误报),输出 文件:行号 + 违规内容,FAIL 影响退出码(pre-commit 钩子同标准)。背景与替代方案详见 Chrome 80 兼容约束。
按需构建 build-app
node scripts/build-app.js <name> # 兼容 lint +(apps/)vite 构建 /(market-apps/)直出检查
node scripts/build-app.js <name> --watch # market-apps/ 专用:文件变化自动 lint + 提示刷新自动识别应用类型(依次查 apps/ 与 market-apps/ 的 manifest.json)。两类应用的「改动生效」机制完全不同:
- 先跑
diag.js compat <name>,FAIL 即终止,避免把带伤产物构建出去 apps/(构建型):vite 全量构建(单页架构无法单应用构建,vendor chunk 共享)。开发期迭代用cd client && npx vite devHMR 免全量构建market-apps/(直出型):检查 entry/style/icon 文件存在性;改完刷新浏览器即生效(no-cache 直出,无需构建)。--watch防抖 300ms 自动重跑 lint,backend 变更单独提示需重启服务器
后端热重载(开发机可选)
CLASSINTRA_HOT_RELOAD=1 启动服务器时,apps/*/backend 与 plugins/*/backend 改动自动热重载免重启(fs.watch + 防抖 300ms,语法错误时保留旧代码服务不中断)。只覆盖各模块 backend/ 目录内文件,宿主层(server/src/)与前端不适用。
图标满铺体检 icon-trim
项目硬性规范:图标资产不留白——内容顶格铺满 100% 画布,圆角由 AppIcon 容器 CSS 统一裁切(72px + radius 20px + object-fit cover)。
.\scripts\icon-trim.ps1 # 处理 Resources/public/icons(默认)
.\scripts\icon-trim.ps1 -Dir <path> # 指定其他图标目录检测 alpha 包围盒 → 裁剪 → 高质量重采样回满画布;支持 PNG 与内嵌 base64 位图的 SVG。全透明、已满铺、>480KB 的文件自动跳过(pre-commit 有 500KB 单文件上限,历史超大资产应先降采样重嵌而非重编码)。
市场双仓同步 sync-market(市场维护者)
主仓 .gitignore 忽略 plugins/ 与 market-apps/,插件与市场应用文件只进 market 仓。在主仓工作区开发后必须立即同步,防双仓漂移:
.\scripts\sync-market.ps1 # 同步 plugins/ → market 仓 + 显示变更
.\scripts\sync-market.ps1 -Commit "feat: xxx" # 同步 + 自动提交 market 仓
.\scripts\sync-market.ps1 -Health # 附带主仓 git health(b→o 字符损坏体检)
.\scripts\sync-market.ps1 -App gomoku # 额外镜像 market-apps/gomoku → market/apps/gomoku-App 用 robocopy /MIR 镜像(退出码 0–7 视为成功);-Commit 时自动 git add apps/。插件 manifest 的 version 独立语义化递增,与主仓 server/version.json 无关。
常用组合命令
一键开发环境
# Linux / macOS
git pull && pnpm install && pnpm dev
# Windows
git pull & pnpm install & pnpm dev一键生产部署
# 拉取最新代码
git pull
# 安装依赖
pnpm install
# 构建前端
pnpm build
# 重启 PM2
pm2 restart ecosystem.config.js --update-env
# 验证
sleep 3
curl http://localhost:9001/api/system/health升级后回滚
# 查看历史版本
git log --oneline -10
# 回滚到某 commit
git reset --hard <commit-hash>
# 重装依赖(依赖可能变化)
pnpm install
# 重建前端
pnpm build
# 重启
pm2 restart classintra-server数据库备份脚本
#!/bin/bash
# backup-db.sh
set -e
cd /opt/ClassIntra
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR=/backup/classintra
mkdir -p $BACKUP_DIR
# 在线备份(无需停服)
sqlite3 server/database/classintra.db ".backup '$BACKUP_DIR/classintra-$DATE.db'"
# 保留最近 30 天
find $BACKUP_DIR -name "classintra-*.db" -mtime +30 -delete
echo "[$DATE] 备份完成: $BACKUP_DIR/classintra-$DATE.db"加入 crontab:
# 每日凌晨 3 点备份
0 3 * * * /opt/ClassIntra/scripts/backup-db.sh >> /var/log/classintra-backup.log 2>&1
