# 部署与运行说明

更新日期：2026-09-09。对应实施：`交付文档/环境调查与实施计划.md` 第 4 节步骤 1—9。

## 1. 基本信息

| 项 | 值 |
| --- | --- |
| 访问入口 | `https://sosp.zhuxincn.com/`（HTTPS，泛域名证书，手机浏览器直接访问） |
| 演示访问码 | 必须由 `SOSP_ACCESS_CODE` 显式配置；无源码默认值，值只由运维方安全提供 |
| 应用目录 | `/var/www/demo/sosp-assistant/` |
| 业务端口 | 127.0.0.1:8460（nginx 反代 443 → 8460，不直接对公网） |
| AI 隔离实例 | 127.0.0.1:8461（按需自动拉起，空闲 60 秒自动停止） |
| 数据模式 | `mock`（演示数据；保存只写本地 Demo 库，不写任何 Salesforce） |
| 服务管理 | `systemctl {start|stop|restart|status} sosp-assistant` |

## 2. 目录结构

```text
/var/www/demo/sosp-assistant/
├── server.js                 # Express 入口（端口 8460）
├── package.json              # 运行依赖 express、ws；无构建链
├── lib/
│   ├── config.js             # 端口/模式/阈值/演示基准日期等全部配置
│   ├── db.js                 # node:sqlite 建表（含 owner/auth session、pending audio、草稿与幂等状态）
│   ├── engine/               # 业务引擎（程序规则，不依赖模型）
│   │   ├── fields.js         #   六项信息口径 + 完整度 + 缺口追问
│   │   ├── stage.js          #   阶段建议规则引擎（退出条件配置化）
│   │   ├── stagnation.js     #   停滞双口径 + 提醒去重 + 动作
│   │   └── dates.js          #   模糊日期/补录判定
│   ├── ai/                   # AI 层（隔离 opencode serve）
│   │   ├── client.js         #   实例管理（独立端口/数据目录/Basic 认证/空闲自停）
│   │   ├── prompt.js         #   系统提示词 + 每轮上下文
│   │   ├── validate.js       #   模型输出 JSON 信封校验（工具白名单、参数净化）
│   │   ├── tools.js          #   工具注册表 + 卡片构建
│   │   └── orchestrator.js   #   对话编排（会话→模型→校验→执行→卡片流）
│   ├── asr/
│   │   └── xfyun.js          # 讯飞签名（APISecret 只在服务端使用）
│   └── adapters/
│       ├── store.js          # 唯一写路径（仅本地 SQLite；uat 模式拒绝写）
│       └── sf-readonly.js    # 生产只读查询（默认禁用；白名单 SOQL）
├── public/                   # 前端（原生 ES Module，四页 SPA）
│   ├── index.html / app.js / style.css
│   ├── xfyun-asr.js          # 浏览器 PCM 采集、重采样和流式结果合并
│   └── brand/sosp-logo-icon.svg   # 品牌素材（用户提供，未改动）
├── scripts/init-db.js        # 建表 + 合成数据初始化（--force 还需显式重置开关）
├── deploy/sosp-assistant.service  # systemd 单元（已安装至 /etc/systemd/system/）
└── data/                     # 运行数据（不对外暴露）
    ├── sosp.db               # SQLite（WAL）
    ├── audio/                # 录音私有目录（仅经鉴权接口读取）
    ├── ai/config|data/       # AI 隔离实例的 XDG_CONFIG_HOME / XDG_DATA_HOME
    ├── ai/token              # AI 实例 Basic 认证 token（0600，自动生成）
    └── secret.key            # 会话 Cookie 签名密钥（0600，自动生成）
```

私密运行配置位于源码目录之外：访问码在 `/etc/sosp-assistant/access.env`，讯飞凭据在 `/etc/sosp-assistant/xfyun.env`。目录权限 0700，文件权限 0600；systemd 强制加载两者，任一缺失时拒绝启动。

## 3. 启动 / 停止 / 日志

```bash
systemctl start sosp-assistant        # 启动
systemctl stop sosp-assistant         # 停止
systemctl restart sosp-assistant      # 重启
systemctl status sosp-assistant       # 状态
journalctl -u sosp-assistant -f       # 实时日志（含 AI 实例拉起/停止记录）
```

- 服务异常自动重启（`Restart=on-failure`）。
- 服务以独立非 root 用户 `sosp-assistant` 运行，并启用 `ProtectSystem=strict`、`ProtectHome=true`、`NoNewPrivileges=true` 等 systemd 沙箱。
- 内存约束：`MemoryHigh=600M / MemoryMax=900M`（应用本体约 30—64MB；AI 实例按需拉起后总量实测约 480—536MB，空闲自动停止）。
- nginx 配置块位于 `/etc/nginx/sites-available/zhuxincn`（sosp.zhuxincn.com server 块，proxy → 8460）。改动前备份在 `/root/backups/zhuxincn.bak-20260909-sosp-switch`。修改后必须 `nginx -t && systemctl reload nginx`。

## 4. 数据初始化与重置

```bash
cd /var/www/demo/sosp-assistant
node --experimental-sqlite scripts/init-db.js          # 首次初始化（已有数据则跳过）
SOSP_SEED_ALLOW_RESET=1 node --experimental-sqlite scripts/init-db.js --force  # 清空并重建演示数据
systemctl restart sosp-assistant
```

合成数据内容（全部标注为演示数据）：
- 东莞供电局 · SVG 静止无功发生器改造（主机会，初步方案 40%，预置 3 条带来源证据的历史日志）
- 东莞供电局 · 配电网智能巡检终端（同客户干扰机会，用于演示纠正关联）
- 佛山供电局 · 变电站二次保护装置改造（停滞机会：最后有效活动距今 187 天，含 1 条补录日志）

所有相对日期基于演示基准日期（默认当天）计算；更换演示日期用 `SOSP_DEMO_BASE_DATE=YYYY-MM-DD` 后重新 `--force` 初始化。

## 5. 配置项（环境变量，均可在 unit 文件修改）

| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `SOSP_MODE` | `mock` | 数据模式：mock / production-readonly / uat（uat 未核验前写入一律拒绝） |
| `SOSP_ACCESS_CODE` | 无，必填 | 演示访问码（登录用）；只存私密 EnvironmentFile |
| `SOSP_DEMO_BASE_DATE` | 当天 | 演示基准日期 YYYY-MM-DD |
| `SOSP_AI_PORT` | `8461` | 隔离 AI 实例端口 |
| `SOSP_AI_MODEL` | `zai-coding-plan/glm-5.3-flash` | 主模型（备用 `SOSP_AI_FALLBACK_MODEL`=deepseek/deepseek-v4-flash） |
| `SOSP_AI_IDLE_STOP_MS` | `60000` | AI 实例空闲自动停止（内存保护） |
| `SOSP_AI_MAX_CONCURRENT` / `SOSP_AI_MAX_QUEUED` | `2` / `20` | AI 推理并发与等待队列上限 |
| `XFYUN_APP_ID` | 无 | 讯飞应用 ID；配置在私密 EnvironmentFile |
| `XFYUN_API_KEY` | 无 | 讯飞语音听写 APIKey；不得进入源码或前端 |
| `XFYUN_API_SECRET` | 无 | 讯飞语音听写 APISecret；不得进入源码或前端 |
| `SOSP_ASR_MAX_SECONDS` | `55` | 单轮录音上限；讯飞接口上限 60 秒，预留收尾时间 |
| `SOSP_PENDING_AUDIO_TTL_MS` / `SOSP_BOUND_AUDIO_TTL_MS` | `1800000` / `86400000` | 未绑定录音/已绑定草稿录音的恢复窗口；草稿更新会续期 |
| `SOSP_STORED_AUDIO_OWNER_BYTES` / `SOSP_STORED_AUDIO_GLOBAL_BYTES` | 250MiB / 1GiB | 已存与待存录音的设备级/全局容量边界 |
| `SOSP_STAGNATION_ACTIVITY_DAYS` | `60` | 停滞口径一阈值（演示配置） |
| `SOSP_STAGNATION_STAGE_DAYS` | `90` | 停滞口径二阈值（演示配置） |
| `SOSP_ENABLE_SF_READONLY` | `false` | 生产只读查询开关（仅查询，白名单 SOQL） |
| `SOSP_UAT_WRITE` | `false` | UAT 写入开关（开启也需先完成身份核验，见《UAT接入说明》） |

改完环境变量：编辑 `/etc/systemd/system/sosp-assistant.service` → `systemctl daemon-reload && systemctl restart sosp-assistant`。

讯飞凭据更新后无需改源码，只需修改 `/etc/sosp-assistant/xfyun.env`，保持 0600 权限并重启独立服务。可用 16k/16bit/单声道 PCM 样例验证：

```bash
cd /var/www/demo/sosp-assistant
npm run test:asr -- /path/to/sample.pcm
```

语音链路：浏览器立即并行运行 MediaRecorder（原音保存）和 Web Audio（连接期间先缓冲 PCM，再流式转写）；PCM 经 `wss://sosp.zhuxincn.com/api/asr/stream` 的登录会话、同源 Origin 和全局并发校验后代理到讯飞。浏览器不会取得 APIKey/APISecret；每个设备同时只保留一个连接，单轮自动限制为 55 秒。

录音先以客户端生成 UUID 幂等上传，并同时暂存到浏览器 IndexedDB。服务端以 SQLite `pending_audio` 状态机管理，文件写入/重命名在数据库提交前执行 `fsync`；进程重启会恢复 `pending` 和中断在 `committing` 的录音。确认保存后记录文件大小并受设备级、全局配额及磁盘最低余量保护；回放支持 HTTP Range，兼容手机播放器。

## 6. 自动化验证

涉及运行中 Demo 的脚本必须显式提供 `SOSP_TEST_ALLOW_LIVE=1`、测试访问码和数据库路径，并会核对目标服务与清理库是否为同一实例。主要命令：

```bash
npm run test:store
npm run test:uat
set -a; . /etc/sosp-assistant/access.env; . /etc/sosp-assistant/xfyun.env; set +a
SOSP_TEST_ALLOW_LIVE=1 SOSP_TEST_ACCESS_CODE="$SOSP_ACCESS_CODE" SOSP_TEST_DB_PATH="$PWD/data/sosp.db" npm run test:security
SOSP_TEST_ALLOW_LIVE=1 SOSP_TEST_ACCESS_CODE="$SOSP_ACCESS_CODE" SOSP_TEST_DB_PATH="$PWD/data/sosp.db" npm run test:asr-proxy -- /path/to/sample.pcm
SOSP_TEST_ALLOW_LIVE=1 SOSP_TEST_ACCESS_CODE="$SOSP_ACCESS_CODE" npm run test:recording-flow -- /path/to/sample.pcm
```

`test:pending-recovery` 分 `prepare` / 重启服务 / `verify` 两阶段，可验证普通待存状态和 `prepare-committing` 文件提交中断状态。AI 孤儿会话维护必须先停止服务，再显式设置 `SOSP_MAINTENANCE_ALLOW_DELETE=1` 运行 `npm run maintenance:prune-ai`。

## 7. 回退方式

- **应用回退**：`systemctl stop sosp-assistant` 即可；nginx 的 sosp server 块原本指向已失效的 LobeChat（8458），不存在"恢复原状"需求；如需彻底下线入口，把该 server 块注释后 reload。
- **数据回退**：显式设置 `SOSP_SEED_ALLOW_RESET=1` 后运行 `scripts/init-db.js --force`；该操作会永久清空 Demo 数据与录音。
- **nginx 配置回退**：`cp /root/backups/zhuxincn.bak-20260909-sosp-switch /etc/nginx/sites-available/zhuxincn && nginx -t && systemctl reload nginx`。
- AI 实例完全独立（独立端口/数据目录/token），停止后不影响 VPS 上任何现有服务。

## 8. 与现有服务的隔离

- 独立 systemd 单元、独立端口（8460/8461，实施前已实测空闲）、独立数据目录。
- AI 隔离实例使用独立 `XDG_CONFIG_HOME`/`XDG_DATA_HOME` + `OPENCODE_SERVER_PASSWORD` Basic 认证；与正在运行的 openchamber/其他 opencode serve 互不影响。
- 未修改 `/root/sfcrmapps-sync/repo`、未修改 Salesforce 任何状态、未改动其他 nginx server 块。
