# 环境调查与实施计划

调查日期：2026-09-09（与交接包同日）。调查人均为只读操作；生产 Salesforce 未发送任何写请求；`/root/sfcrmapps-sync/repo` 未做任何修改。

本文是《语音销售助手-Demo方案.md》第 4 节要求的调查结论与实施计划。实施在新 chat 中按本文执行。

---

## 1. 已确认事实

### 1.1 VPS 与网络

| 项 | 结果 |
| --- | --- |
| 系统 | Ubuntu 24.04.4 LTS，x86_64，kernel 6.8，主机名 racknerd-6b9913a |
| 资源 | 2 核；内存 1.9Gi；磁盘 34G（已用 64%，可用 12G） |
| 内存现状 | 本次调查中应用户要求删除了 `grafana`、`lobechat` 两个容器，可用内存 **373MB → 770MB**；swap 已用约 880MB，仍有压力 |
| Node | v22.23.2（`node:sqlite` 加 `--experimental-sqlite` 可用，已实测通过） |
| Docker | 运行中。现存容器：homepage、filebrowser、gitea、beszel(-agent)、freshrss、loki、victoriametrics、gitea-postgres（grafana、lobechat 已删，镜像未 prune，磁盘不紧张，暂留） |
| nginx | 运行中，监听 80/443/8443/8444；`sites-enabled`：demo（80 端口 default_server，root 指向 `/var/www/demo`，即交接包上传位置）、gitea、git-https、zhuxincn |
| 域名/证书 | `zhuxincn.com` + 泛域名 `*.zhuxincn.com`，通配符证书在 `/root/cert/zhuxincn-wildcard/`（fullchain.pem/key.pem），HTTPS 手机入口条件具备 |
| `sosp.zhuxincn.com` | 原为 LobeChat 反代入口（2026-09-09 配置，basic auth + proxy 127.0.0.1:8458）。LobeChat 容器已按用户指示删除，该入口已失效；**nginx 配置块、域名、证书可复用给本 Demo** |
| 空闲端口 | 8460、8461、8462 均空闲（已实测），选 8460 为 Demo 业务端口、8461 为隔离 AI 实例端口 |
| 其他 | Tailscale、fail2ban、ttyd（127.0.0.1:8457 的 root Web 终端，建议后续处置）在运行 |

### 1.2 参考项目 `/root/sfcrmapps-sync/repo`

- **纯 Salesforce DX 元数据仓库**（`force-app/main/default`，101 种元数据类型、46,671 个成员；branch `production`，工作树干净，最新提交 `0d075a89 Sync production metadata changes 2026-09-09`）。
- **没有 Web 应用代码**（无 package.json、无前端、无后端服务），没有可直接复用的应用栈；复用价值在元数据即业务口径。
- AGENTS.md 关键约束（与主 Plan 一致且更具体）：
  - 生产组织 ID `00DC80000071iuEMAQ`（用户 `sospdomain@sf-auto.com`，实例 CHN1）；UAT 组织 ID `00DC5000000kicLMAQ`（用户 `sospdomain@sf-auto.com.uat`，实例 CHN5S）；API 版本 67.0。
  - 每条 sf 命令必须显式 `--target-org`，不得依赖默认组织，不得凭别名判断环境。
  - 仓库配置了每日生产元数据自动同步定时任务（已获用户授权，推送到 `vps/production`），看到 `Sync production metadata changes YYYY-MM-DD` 提交属正常。
- 对象结构（321 个对象）与本 Demo 相关的参考：
  - `Visit_Plan__c`：字段非常丰富（`ActivityDateTime__c`、`Competitor__c`、`Contact__c`、`contractorDecisionMaker__c`、`CheckinLocation__c`、`Business_Unit__c` 等），可作为六项信息口径的现实参照。
  - `log__c` / `logNew__c`：旧日志对象（会议明确弃用旧录入方式，但字段口径可参考）。
  - `Opportunity` 自定义金额字段多个（`expectAmount__c`、`expectAmount_CNY__c`、`closeReason__c` 等）。
  - **阶段取值已在元数据中确认**（`OpportunityStageHistory__c.Stage__c`）：线索、需求确认、初步方案、方案验证、解决方案、招标准备、投标、商务谈判、合同谈判、合同签署、项目推进、项目暂停、中标、赢单、丢单、已关闭。会议截图中的“初步方案 40% → 方案验证 60%”是**真实存在的阶段名**；概率映射在 Demo 中按演示配置标注（组织真实概率配置需 UAT 后核对）。
  - 已有语音录入相关 REST 服务：`VoiceInputRest`、`VoiceInputQueryEventRest`（钉钉入口，Event 增删改查 + 商机/客户/合同查询），证明“语音录入→Event”业务口径在组织里已有雏形，Demo 字段设计对齐它。
- 现有改动保护：每日同步是自动提交，实施期间**不得**在该仓库执行任何写/推送/清理操作。

### 1.3 Salesforce 连接状态

- sf CLI 2.147.7（有 2.150.6 更新，暂不升级，避免引入变量）。
- `sf org list`：**仅 `sf-auto-prod`（生产，00DC80000071iuEMAQ）已连接**。UAT 未出现在组织列表 → 确认 UAT 尚未认证。
- `sf org display --target-org sf-auto-prod` 核对组织 ID 与 AGENTS.md 一致。
- 结论：**当前只有 mock 模式可做**；`production-readonly` 可选实现（仅查询）；`uat` 需用户完成认证后按主 Plan §3.3 流程开启。

### 1.4 OpenCode

- 版本 1.18.30，路径 `/root/.opencode/bin/opencode`。
- 已配置的 AI provider（读 auth.json 仅取 key 名，未输出密钥）：`zhipuai-coding-plan`、`zai-coding-plan`、`deepseek`、`openai`。
- 可用模型（`opencode models` 摘选）：`zhipuai-coding-plan/glm-4.6v、glm-4.7、glm-5-turbo…`、`zai-coding-plan/glm-5.3-flash 等`、`deepseek/deepseek-v4-flash 等`。**首选用 `zai-coding-plan/glm-5.3-flash` 或 `deepseek/deepseek-v4-flash` 做结构化提取（快、省），复杂摘要可换高档模型，全部走配置可换。**
- `opencode serve --port X --hostname 127.0.0.1` 可正常启动（已实测临时实例并已清理）。
- **重要发现：serve 的 HTTP API 无凭据访问返回 401**（新起实例同样）。实施第一步需解决认证：优先查 `@openchamber/web`（1.22.2，全局安装于 `/usr/lib/node_modules`）如何取得 token，或查 opencode 文档/`opencode.db` 中的 server key 机制；仍不通则改用 `opencode run` 子进程方式（每次调用独立进程，隔离性更好但慢）。此为技术风险项，已列入实施步骤第 1 步。
- 当前正在运行的两个进程**不能动**：`opencode serve`（127.0.0.1:39925，RSS ~596MB）与 `openchamber`（127.0.0.1:8456）——**本次云端 AI 会话正运行在其中**。Demo 需要自起隔离实例（独立端口 8461 + 独立 `XDG_CONFIG_HOME`/`XDG_DATA_HOME` 目录），不共享会话与数据。

### 1.5 语音（ASR/TTS）

- 初始调查时 VPS 上**没有**任何已配置的 ASR/TTS 服务或密钥（已查 lobechat compose 等位置，无语音类 key）。
- 浏览器侧 Web Speech API 在国内 Android/iOS 上不可靠。
- 初始结论：Demo 内先完成真实录音、存储、回放，未取得云端凭据前不伪装转写。
- **实施更新（2026-09-09）：**用户提供测试凭据后，已接通讯飞“语音听写（流式版）”。浏览器把麦克风数据转换为 16k/16bit/单声道 PCM，经本站鉴权 WebSocket 代理流式发送到讯飞；停止后自动取得最终文字并生成草稿。原始 MediaRecorder 音频仍独立保存和回放。
- 讯飞密钥仅保存在 `/etc/sosp-assistant/xfyun.env`（目录 0700、文件 0600），不在源码、前端或交接包中；测试结束后需轮换。
- VPS 不安装 Whisper 或 ffmpeg；ASR 代理实测使应用服务内存维持约 22—29MB。讯飞官方 16k PCM 样例已通过直接 API、本站登录 WebSocket 代理和公网 nginx WSS 三段真实验证。

### 1.6 交接包资料

- 逐字稿与 5 张截图已完整阅读，结论与主 Plan §5 一致；补充细节：截图 4 的停滞提醒有“置休眠/仍在跟进/稍后”三动作且“处理前一直保留”；截图 5 的日志流有“派生”来源徽章与“补录不计入周期口径”规则——两者纳入 Demo 数据模型（提醒去重、补录标记）。
- 品牌素材已读：`sosp-logo-icon.svg`（内嵌原图裁切视口）复制到 Demo 静态目录 `public/brand/` 使用，页面配文字“SOSP移动办公 · 销售助手”，不重绘 Logo。

## 2. 隔离与复用决策

| 项 | 决策 |
| --- | --- |
| 复用 | 仅复用参考仓库的**业务口径**（阶段取值、Visit_Plan 字段、语音录入接口语义）与 SOSP 品牌素材；**不复用/不修改**参考仓库任何文件 |
| Demo 目录 | `/var/www/demo/sosp-assistant/`（独立；`/var/www/demo` 恰好是 nginx 80 端口 default root，但 Demo 不走 80，走独立端口 + HTTPS 域名反代） |
| 数据 | SQLite（`node:sqlite`，`--experimental-sqlite`）单文件库 + 音频私有目录 `data/audio/`（nginx 不直接暴露，经鉴权接口读取） |
| AI 隔离 | 后端 spawn `opencode serve --port 8461`，`XDG_CONFIG_HOME`/`XDG_DATA_HOME` 指向 Demo 专用目录；会话按对话 ID 隔离；模型无 shell、无 sf、无文件系统工具权限 |
| SF 隔离 | `production-readonly` 适配器仅允许白名单 SOQL（固定模板 + 参数转义），显式 `--target-org sf-auto-prod`；写路径只存在 mock 库；`uat` 适配器默认关闭 |
| 端口/服务 | 8460（业务）+ 8461（AI），systemd 单元 `sosp-assistant.service` 独立管理，不动任何现有服务 |

## 3. 技术栈及理由

| 层 | 选择 | 理由 |
| --- | --- | --- |
| 后端 | Node 22 + Express（依赖极少） | 环境已有 Node 22.23；2 核/1.9G 内存量入最小化；无构建链 |
| 数据库 | `node:sqlite`（实测可用） | 零原生编译（VPS 内存紧张，避免 node-gyp）；单实例足够 |
| AI | 隔离 `opencode serve` + HTTP/SDK 调用 | 主 Plan 指定方向；provider 密钥已在环境中；失败兜底方案为 `opencode run` 子进程 |
| 前端 | 原生 ES Module SPA（无构建） | 4 页 + 卡片流规模可控；省内存省时间；手机浏览器直接适配 |
| 语音 | MediaRecorder 保存/回放 + Web Audio 转 16k PCM + 本站 WSS 代理讯飞流式听写 | 手机格式不影响 ASR；密钥仅在服务端；不在 VPS 跑模型或转码 |
| 部署 | systemd + nginx 反代 `sosp.zhuxincn.com` → 8460（复用泛域名证书） | 手机 HTTPS 入口零新增成本 |

模式支持状态：`mock` ✅ 本期交付；`production-readonly` ◑ 接口留好，实施可选开启（仅查询）；`uat` ❌ 等待认证，按主 Plan §3.3 清单执行。

## 4. 实施步骤（新 chat 按此执行）

1. **打通 opencode 认证**（先做，唯一技术风险项）：隔离实例 + token 机制调查；不通则降级 `opencode run`。验证：后端能创建会话并拿到结构化 JSON 回复。
2. **骨架与数据**：目录、`package.json`、`server.js`、SQLite 建表（opportunities / visit_logs / log_fields(含状态与来源证据) / drafts / reminders / sessions / idempotency）、合成数据初始化脚本（东莞供电局·SVG 主机会 + 同客户干扰机会 + 佛山局·二次保护装置停滞 187 天；阶段用已核实的组织阶段名；演示基准日期可配置）。启动 `--experimental-sqlite` 的 systemd 单元。
3. **业务引擎**（程序规则，不依赖模型）：六项信息口径配置（参照 Visit_Plan__c/会议口径，标注为演示配置）、完整度计算、阶段建议规则引擎（退出条件配置化，程序校验）、停滞双口径（最后有效活动距今/阶段停留，阈值配置）、提醒去重、补录/模糊日期（保留原文与范围）。
4. **AI 层**：工具注册表（`search_opportunities`、`get_opportunity_context`、`prepare_visit_log`、`save_visit_log`、`generate_briefing`、`propose_next_action`，均声明读写性质与允许模式）+ 意图路由提示词 + JSON 结果校验；模型输出仅能选择工具/卡片，不能注入 HTML/命令；同会话切意图保留草稿。
5. **API**：`/api/chat`（消息→卡片流）、`/api/draft`（草稿确认/修正/否定）、`/api/opportunities`、`/api/logs`（幂等保存，重复点击只一条）、`/api/audio/*`（会话校验后回放）、`/api/reminders`、`/api/mode`（只读返回当前模式）。前端不直连 AI/SF/DB。
6. **前端四页**：首页（录音入口、最近日志、待办建议）、日志对话（录音计时→文字/语音输入→草稿卡：关联机会候选、活动类型/时间、六项信息带状态徽章、缺口追问、确认保存显示“已保存到 Demo”）、机会详情（时间线+来源展开+录音回放+完整度+风险）、简报（可追溯：每条结论挂日志来源）。顶部固定显示当前数据模式徽章。品牌素材接入。
7. **nginx 切换**：把 `sosp.zhuxincn.com` 现有 server 块的 proxy 目标从已死的 8458 改为 8460，去掉 basic auth（改由应用会话 cookie 保护音频/日志接口）。改动仅此一处，旧目标已删除无影响；改前备份配置，`nginx -t` 通过后 reload，不动其他 server 块。
8. **验证**：主 Plan §9 的 12 条逐项核对（mock 拦截写操作用假请求验证；多机会纠正、臆造姓名/预算拒绝、幂等保存、刷新保草稿、补录/模糊日期、会话隔离、构建检查、手机布局）；记录真实“停止录音→草稿卡”耗时；真机项留给用户的明确列出。
9. **交付文档**：`部署与运行说明.md`、`验收与演示说明.md`、`UAT接入说明.md`（认证动作 + 目标组织 00DC5000000kicLMAQ 核验 + 允许列表配置 + 生产永不写入声明）。

## 5. 验收方式

- 演示脚本按主 Plan §7 五分钟流程在 `https://sosp.zhuxincn.com/` 手机实测（录音、讯飞转写、草稿、保存与回放均走真实链路）。
- 后端写路径测试证明仅落 SQLite；`production-readonly` 下保存仍仅本地；伪造 `mode=uat`/org 参数请求被服务端拒绝。
- 每条验收结论标注：实测通过 / 模拟验证 / 待用户真机确认。

## 6. 尚未确认项与需要用户完成的动作

| 项 | 状态 | 需要的动作 |
| --- | --- | --- |
| UAT 认证 | ❌ 未连接（org list 无 UAT） | 用户在 VPS 执行 `sf org login web`（UAT 沙箱 Connected App），完成后按 UAT接入说明核验组织 ID `00DC5000000kicLMAQ` |
| ASR 测试凭据 | 已配置讯飞 | 测试结束后在讯飞控制台轮换，并只更新 `/etc/sosp-assistant/xfyun.env`，不要把新值发到对话或提交源码 |
| opencode server 认证机制 | ⚠️ 实施第 1 步解决 | 无需用户操作；失败自动降级 |
| 生产组织真实阶段概率映射 | ⚠️ 需查询核对 | production-readonly 开启时只读核对；mock 期用演示配置 |
| ttyd（root Web 终端 8457） | 建议处置 | 用户决定是否关闭（安全面） |
| grafana/lobechat 镜像 | 磁盘不紧张暂留 | 需要磁盘时 `docker image prune` |

## 7. 本次会话已执行的环境变更（留存备查）

1. 删除容器 `grafana`、`lobechat`（用户明确指示），内存可用 373MB→770MB；镜像未删。
2. 在 `/tmp/opencode/oc-test` 起过一次 opencode serve 测试实例，已停止并删除目录。
3. 未改动 `/root/sfcrmapps-sync/repo`、nginx、systemd、Salesforce 任何状态。

---

## 8. 实施状态（2026-09-09 实施会话补记）

第 4 节 9 个步骤已全部执行完毕。交付物：

| 项 | 结果 |
| --- | --- |
| 应用 | `/var/www/demo/sosp-assistant/`（Express + node:sqlite + 原生 ES Module 前端；运行依赖 express、ws） |
| 入口 | `https://sosp.zhuxincn.com/`（nginx 8460 反代，应用会话 Cookie 保护日志/音频接口），访问码必须由私密环境显式提供 |
| 服务 | 非 root systemd `sosp-assistant.service`（8460），AI 隔离实例 8461 按需拉起、空闲 60 秒自停 |
| 第 1 步认证 | **已解决**：opencode serve 原生支持 `OPENCODE_SERVER_PASSWORD` → HTTP Basic（实测 401 拦截 + 会话创建 + 结构化 JSON 回复），未降级 `opencode run` |
| 语音转写 | **已接通**：讯飞语音听写流式版；官方样例经直接 API、本地代理和公网 WSS 均返回正确文字；手机麦克风采集待用户真机确认 |
| 模式 | mock 交付；production-readonly 适配器就绪（默认禁用）；uat 写入安全默认为拒绝（实测双场景拒绝、伪造参数忽略） |
| 验证 | 主 Plan §9 十二条逐项核对，10 条实测通过、2 条部分（真机项待用户），见《验收与演示说明》 |

实施中发现并修正的问题（记录供后续维护）：

1. opencode 隔离实例与业务会话 ID 是两套体系：音频归属按聊天会话 ID 校验（设备登录 Cookie 另行签名）。
2. 幂等重放必须先于"草稿是否存在"判断，否则重复点击会误报 404。
3. AI 会话首轮必须注入系统提示词（信封契约），否则模型输出格式不稳定且重试放大延迟；热请求实测 12—23s/轮，未达 5—10s 目标，可换模型调整。
4. systemd 单元以 `sosp-assistant` 运行，显式使用隔离 HOME `/var/lib/sosp-assistant`；`MemoryMax` 覆盖 AI 子进程峰值（600M high / 900M max）。
5. nginx 配置备份不能留在 `sites-enabled/`（会被当作配置加载导致 duplicate default server）。
6. 讯飞 ASR 使用浏览器 PCM + 服务端 WebSocket 代理，避免低内存 VPS 安装模型或 ffmpeg；凭据必须放在 Web 根目录之外。
7. 录音采用浏览器 IndexedDB + 服务端 `pending_audio` 双层暂存，客户端 UUID 幂等上传；文件提交和数据库提交可在重启后恢复，已保存录音受容量与磁盘余量限制。

本次实施会话环境变更：新建 `/var/www/demo/sosp-assistant/`；nginx `zhuxincn` 文件 sosp server 块（8458→8460、去 basic auth，备份 `/root/backups/zhuxincn.bak-20260909-sosp-switch`）；新增 systemd 单元；后续新增 `/etc/sosp-assistant/xfyun.env` 私密运行配置并接通讯飞；未触碰参考仓库、Salesforce、其他 nginx server 块与运行中服务。
