背景:用户报「下发锁屏等等指令不生效」。先把服务端那条链路单独验了一遍
(模拟客户端:登记 → 下发 → 心跳带回 → 设备列表可见 → 回执 → 清除),**整条是通的**;
真正的问题在两处。
客户端:执行失败会谎报成功
- `spawn` 找不到可执行文件 / 没权限时**不抛异常**,而是在下一个 tick 发一个异步 `error` 事件 ——
只包 `try/catch` 抓不到,代码一路走到 `return { ok: true }`,回执写着「已下达系统命令」,
Web 端看起来一切正常而机器纹丝不动
- 现在等一个 200ms 的窗口确认没有立刻报错,失败如实回执(`client-agent.ts` 的 `executeCommand`)
Web 端:点了没生效,界面上什么都不说
- 一条命令被**取消 / 过期**之后就从 `pendingCommand` 里消失,页面留白 ——
而最常见的两种"没反应"恰恰是「本机没人确认(客户端 60 秒自动按取消)」与「下达时机器离线」
- `GET /clients/devices` 增加 `lastCommand`(最近一条,不论状态);
「客户端」页的设备表多一列「上次指令」,由 `commandStateText()` 翻成一句人话
(已执行 / 本机用户取消 / 已过期 —— 下达时机器不在线 / 待执行)
- 下发成功的提示补上「60 秒内没人点确认就按取消处理」
文档
- AGENTS.md §5 第 69 条记下这两条,并写明排查同类反馈的顺序:**先看那台机器的客户端版本是否 ≥ 1.3.0**
(远控命令的处理 `client-agent.ts` 是 1.3.0 才有的,旧客户端连心跳都不发)
- docs/reference.md 4.15 的表格补上「二次确认」的准确口径与新增的「上次指令」列
验收:`lint` / `typecheck` / `check:icons` / `docs:check` 全绿;
后端端到端 **247/247**、联动链路 **44/44**、插件静态契约 **76/76**、
Web 真实点击 **34/34**、客户端冒烟 **90/90**;
远控链路另用模拟客户端跑通,并确认新增的「上次指令」在待执行与已回执两种状态下都显示正确。
57 lines
2.3 KiB
YAML
57 lines
2.3 KiB
YAML
# ClassHelper · Docker 单容器部署(SQLite + 数据卷)
|
||
#
|
||
# 适用:云服务器 / 与别的服务共用一台机器。相比 deploy/docker-compose.yml(MySQL 变体)
|
||
# 少跑一个数据库容器,内存占用小很多;数据落在 Docker 卷 classhelper-data 里。
|
||
#
|
||
# 使用:
|
||
# 1) cp deploy/.env.sqlite.example deploy/.env.sqlite 并填 JWT_SECRET / 管理员密码
|
||
# (生成密钥:node -e "console.log(require('crypto').randomBytes(48).toString('hex'))")
|
||
# 2) docker compose -f deploy/docker-compose.sqlite.yml --env-file deploy/.env.sqlite up -d --build
|
||
# 3) 访问:http://<服务器IP>:${SERVER_PORT}/
|
||
#
|
||
# 首次启动(AUTO_MIGRATE=true)会自动执行 prisma/migrations 里的建表语句并创建管理员账号,
|
||
# 见 packages/server/src/lib/db-bootstrap.ts("Docker 挂载空数据卷"就是它的目标场景之一)。
|
||
#
|
||
# 备份:docker run --rm -v classhelper-sqlite_classhelper-data:/data -v "$PWD":/backup alpine \
|
||
# tar czf /backup/classhelper-db.tar.gz -C /data .
|
||
|
||
name: classhelper-sqlite
|
||
|
||
services:
|
||
server:
|
||
build:
|
||
context: ..
|
||
dockerfile: deploy/Dockerfile
|
||
target: runtime
|
||
image: classhelper-server:${APP_VERSION:-1.3.2}
|
||
restart: unless-stopped
|
||
environment:
|
||
NODE_ENV: production
|
||
HOST: 0.0.0.0
|
||
PORT: 4000
|
||
DATABASE_PROVIDER: sqlite
|
||
# 必须写绝对路径:容器里 serverRoot 是 /app/server,相对路径会解析到别处
|
||
DATABASE_URL: file:/app/data/classhelper.db
|
||
# 首启动自动建表 + 建管理员;生产自检会拒绝 dev.db 与过短的 JWT_SECRET
|
||
AUTO_MIGRATE: 'true'
|
||
STARTUP_DB_CHECK: 'true'
|
||
JWT_SECRET: ${JWT_SECRET:?请在 deploy/.env.sqlite 中设置(≥32 位随机串)}
|
||
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
|
||
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
||
TRUST_PROXY: '1'
|
||
RATE_LIMIT_ENABLED: 'true'
|
||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||
TERM_START_DATE: ${TERM_START_DATE:-}
|
||
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
|
||
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-admin123}
|
||
|
||
DEFAULT_CLASS_PASSWORD: ${DEFAULT_CLASS_PASSWORD:-123456}
|
||
DEFAULT_TEACHER_PASSWORD: ${DEFAULT_TEACHER_PASSWORD:-123456}
|
||
ports:
|
||
- '${SERVER_PORT:-4000}:4000'
|
||
volumes:
|
||
- classhelper-data:/app/data
|
||
|
||
volumes:
|
||
classhelper-data:
|