Files
cmbuyer/docs/03-tech-stack.md
T

212 lines
17 KiB
Markdown
Raw Normal View History

# 技术栈(Tech Stack)
> 「用什么」的统一速查表。选型与理由在此集中维护;「怎么把它们搭起来」见
> [架构设计](04-architecture.md)。未定项必须标为待定,不要让 agent 在代码里自行决定。
本项目是**双产品**结构:采购服务位于 `admin/`,采购工具位于 `client/`;两套技术栈独立,
通过 HTTP 契约耦合。
## 一、采购服务(网页端,`admin/`,Go)
| 维度 | 选型 | 状态 | 理由 / 说明 |
| --- | --- | --- | --- |
| 语言 | Go 1.23+ | 已定 | 单二进制部署,前序项目同栈,无运行时依赖 |
| HTTP 框架 | `gin` | 已定 | 前序项目已验证;路由、中间件、绑定够用 |
| 页面渲染 | Go `html/template`(服务端渲染) | 已定 | 管理后台交互密度低,不引入前端构建链 |
| UI 样式 | 手写 CSS,单文件 | 已定 | 无构建步骤;原型见 `docs/design/` |
| 数据库 | SQLite(`mattn/go-sqlite3`) | 已定 | 单机部署,单写入者;并发压力低 |
| 迁移 | `goose` | 已定 | 前序项目已验证 |
| 管理端鉴权 | Session Cookie + CSRF Token | 已定 | 服务端渲染的标准做法 |
| 设备端鉴权 | Bearer Token + 设备绑定 | 已定 | 桌面端是机器身份,不用 Cookie |
| 对象存储 | 本地文件系统 + SHA-256 寻址 | 已定 | MVP 单机;接口留抽象以便后续换 S3 |
| 测试 | `go test` | 已定 | 标准库足够 |
| 部署 | 单二进制 + 数据目录 | 已定 | 运营电脑本机运行 |
## 二、采购工具(Windows 桌面端,`client/`,Python)
| 维度 | 选型 | 状态 | 理由 / 说明 |
| --- | --- | --- | --- |
| 语言 | Python 3.11+ | 已定 | AI 生态原生;`uiautomator2` 官方语言 |
| 真机控制 | `uiautomator2` | 已定 | 前序项目真机验证过选规格与订单确认页读取 |
| 传输 | ADB(USB 或 WiFi) | 已定 | `uiautomator2` 3.x 走 adb 通道,`ip:port` 与 USB serial 同等对待 |
| 桌面 GUI | `PySide6` | 已定 | 前序项目已验证;执行员需要看设备状态和批次进度 |
| 截图处理 | `Pillow` | 已定 | 判断页面是否渲染完成,避免保存白屏壳层 |
| HTTP 客户端 | 标准库 `urllib` 或 `httpx` | **待定** | 先用标准库;确有重试/连接池需求再评估 |
| Excel | 不引入 | 已定 | Excel 解析移到采购服务;采购工具不再直接读表 |
| 测试 | `unittest`(标准库) | 已定 | 前序项目 171 项测试均用标准库,无需 pytest |
| 打包 | `pyinstaller` | 已定 | 交付给运营电脑;开发期依赖 |
## 三、AI 辅助(P1,MVP 不启用)
| 维度 | 选型 | 状态 | 说明 |
| --- | --- | --- | --- |
| 调用位置 | 采购工具 | 已定 | PC 有算力;改 prompt 不需要重新打包 |
| provider | 待定 | **待定** | 需先确认预算与合规;不得由 agent 自行选定 |
| 凭据存储 | 采购工具本机配置文件,不入库、不上传 | 已定 | 采购服务不保存、不代理、不下发任何模型凭据 |
| 输入 | 自动复检通过的脱敏派生 XML + 页面截图 | 已定 | 原始证据只允许本机确定性脱敏器消费,AI/agent 不读取原始地址或手机号 |
## 四、决策记录与演进
- **不做前端框架。** 管理后台的交互密度不值得引入构建链和状态管理。若将来出现复杂
实时视图再评估,届时以整页替换为单位迁移,不做半 SPA。
- **SQLite 而不是 Postgres。** MVP 单机、单写入者。出现多实例或跨机访问需求时再迁移;
数据访问层不得写死 SQLite 方言。
- **Excel 解析放采购服务而不是采购工具。** 建单入口集中在一处才能统一审计。代价是要在
Go 侧重写表头校验和行级报错,不能直接复用前序项目的 Python 实现。
- **采购工具不持有业务权威。** 金额上限、授权有效性、任务状态流转的判定权在采购服务;
采购工具本地校验只作为第二道防线,两边不一致时一律转人工。
- **不引入 pytest / 不引入 ORM。** 同一职责不并存两套方案。
## 五、构建与运行命令
2026-08-03 18:55:32 +08:00
> `init.ps1` 已由 T-003 在 Windows PowerShell 实际验证:它优先复用合规的既有采购工具虚拟环境
> (本机实际为 Python 3.12);仅在虚拟环境不存在时,才从 Python Launcher 的已安装版本中确定性选择
> 最高的 Python 3.11+ 创建它,以 editable 方式安装采购工具,并跑两端
> 离线门禁。桌面 GUI 和真机流程不属于该入口的验收范围。
| 用途 | 采购服务(`admin/`) | 采购工具(`client/`) |
| --- | --- | --- |
2026-08-03 18:55:32 +08:00
| 安装依赖 | `go mod download` | Windows 运行 `./init.ps1`;它复用合规 venv,或选择 Python Launcher 中最高的 Python 3.11+ 创建 venv 后执行 `pip install -e .` |
| 本地开发 | `go run ./cmd/server` | `.\.venv\Scripts\python.exe -m cmbuyer_client` |
2026-08-03 18:55:32 +08:00
| 构建 | `go build ./...` | `.\.venv\Scripts\python.exe -m pip wheel --no-deps . --wheel-dir <输出目录>`,再运行 `.\.venv\Scripts\python.exe scripts/verify_wheel_metadata.py <wheel 路径>` |
| 测试 | `go test ./...` | `.\.venv\Scripts\python.exe -m unittest discover -s tests -t .` |
| 静态检查 | `go vet ./...` | `.\.venv\Scripts\python.exe -m compileall -q src tests scripts` |
### T-101 设备基线取证(2026-08-04 已完成人工双通道验收)
2026-08-03 19:28:39 +08:00
`client/scripts/capture_device_baseline.py` 只允许对**手工明确填写**的 ADB serial 做连接前核验、
设备型号 / Android / 拼多多版本读取、截图和 `dump_hierarchy(compressed=False)`。它不打开商品、
不读取页面判据,也不执行采购、下单或付款动作。多个在线通道必须完成 `getprop` 物理身份比对:
同一手机 USB + WiFi 同时在线,或任一在线通道的身份读取失败,都会 fail closed,不能随机继续。
人工验收前,先由人把手机切换到不含收货地址、手机号、支付信息或其他无关隐私的安全页面,再在
`adb devices -l` 中**手工复制**一个在线 serial;USB 和 WiFi 分别验收,且每次只保留一个通道在线。
WiFi 通道必须由人先行建立;脚本禁止 `adb connect`、`adb disconnect` 或自动重连。以下命令中的尖括号
必须替换为该次人工确认的实际 serial,不能省略或改成自动选择:
```powershell
# 仓库根目录;先手工确认设备状态,命令本身只读 ADB 清单
D:\Portable\adb\adb.exe devices -l
# USB:粘贴该次 devices -l 显示的 USB serial
.\client\.venv\Scripts\python.exe client\scripts\capture_device_baseline.py --serial <USB_SERIAL> --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-101\usb-baseline" --timeout 10 --adb D:\Portable\adb\adb.exe
# WiFi:由人先建立 WiFi ADB 通道、断开 USB 后,粘贴该次 devices -l 显示的 WiFi serial
.\client\.venv\Scripts\python.exe client\scripts\capture_device_baseline.py --serial <WIFI_SERIAL> --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-101\wifi-baseline" --timeout 10 --adb D:\Portable\adb\adb.exe
```
成功时输出目录仅包含截图、完整 XML 与不含页面正文的 `manifest.json`(设备元数据、通道、时间、
文件 SHA-256 和 serial 哈希)。`--timeout` 约束 ADB 命令、ADB socket 及 `takeScreenshot` /
`dumpWindowHierarchy(compressed=False, max_depth=50)` 的公开 JSON-RPC 调用;uiautomator2 初始化仍有
上游固定启动上限。截图 Base64 仅兼容 RPC 返回值中的空格、TAB、CR、LF,其余字符
仍严格拒绝。XML 仅留在本机明确指定的证据目录;人工必须先在本地检查截图/XML,再只记录路径
和哈希,不得把原始证据提交 Git。USB 与 WiFi 已分别由人完成取证和隐私检查,设备型号、Android、
拼多多版本、产物路径及 SHA-256 已记录到 T-101;上述命令保留用于可审计的复现与排障。
2026-08-03 19:28:39 +08:00
### T-102 按链接打开商品取证(2026-08-04 已完成人工真机验收)
`client/scripts/capture_product_open.py` 只接受
`https://mobile.yangkeduo.com/goods.html?goods_id=<纯数字>` 的唯一 canonical 表示。解析后由
`goods_id` 再次重建 URL,并以参数数组执行显式限定 `com.xunmeng.pinduoduo` 的 Android `VIEW`
intent;没有任意 URL、任意 shell 或其他 App 控件操作入口。运行拼多多版本必须精确等于 T-101 已
取证的 `8.17.0`,版本失配会在 intent 前停止。
```powershell
# 仓库根目录;USB 或 WiFi 每次只保留一个通道在线,再手工复制该次 serial
D:\Portable\adb\adb.exe devices -l
.\client\.venv\Scripts\python.exe client\scripts\capture_product_open.py --serial <SERIAL> --url "https://mobile.yangkeduo.com/goods.html?goods_id=<GOODS_ID>" --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-102\product-open-<GOODS_ID>" --timeout 10 --adb D:\Portable\adb\adb.exe
```
脚本在 intent 成功后,以 `--timeout` 为明确上限只读轮询前台 package;只有观察到拼多多才采集截图与
完整 XML,超时仍 fail closed。这一等待只解决 App 异步切换,不根据 Activity、节点文本或旧项目常量
声称已到详情页。成功目录以原子方式发布,manifest 仅记录 `goods_id`、canonical URL、
设备/App 非敏感元数据、受限命令摘要、文件路径和 SHA-256,不含原始 serial、Activity 或页面正文。
必须由人本地确认截图对应目标商品并检查截图/XML 无地址、手机号、支付信息或其他无关隐私;原始
证据不得提交 Git。T-102 已由人确认 goods_id `958756616606` 对应目标商品并完成截图/XML 隐私检查,
设备、版本、证据路径与 SHA-256 已记录到任务执行记录。
### T-103 规格面板三状态只读取证(T-110 边界调整后待重新验证)
`client/scripts/capture_sku_panel_spike.py` 只采集人已在手机上准备好的规格面板截图和完整 XML。
它不打开链接或规格面板,不识别面板,不点击、滑动、输入或选择规格,也不读取价格;接口只暴露
`app_info`、`app_current` 和两个只读 JSON-RPC 方法。`initial`、`one-dimension-selected`、
`all-dimensions-selected` 三种值写入 manifest 的字段名是 `human_declared_state`,明确表示人工声明,
不得将其当作自动识别结果。运行拼多多版本必须精确为 `8.17.0`,前台 package 必须是拼多多,否则
fail closed;目标目录已存在、截图/XML 无效或超时均不得覆盖已有内容或发布半成品。
T-103 已证明当前衣服商品没有独立「规格/已选」入口。T-110 只批准拼多多 `8.17.0`、goods_id
`937122477375` 上经真机确认的精确唯一 `快要抢光` 作为可逆的受控规格面板入口;“免拼购买 / 单独购买 /
直接拼成”等其他文案不能凭人工经验复用,必须分别重新取证。只读取证 CLI 本身仍不执行点击;三种状态
分别使用一个全新原始证据目录:
```powershell
.\client\.venv\Scripts\python.exe client\scripts\capture_sku_panel_spike.py --serial <SERIAL> --url "https://mobile.yangkeduo.com/goods.html?goods_id=<GOODS_ID>" --state initial --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-initial-<GOODS_ID>\raw" --timeout 10 --adb D:\Portable\adb\adb.exe
.\client\.venv\Scripts\python.exe client\scripts\capture_sku_panel_spike.py --serial <SERIAL> --url "https://mobile.yangkeduo.com/goods.html?goods_id=<GOODS_ID>" --state one-dimension-selected --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-one-dimension-<GOODS_ID>\raw" --timeout 10 --adb D:\Portable\adb\adb.exe
.\client\.venv\Scripts\python.exe client\scripts\capture_sku_panel_spike.py --serial <SERIAL> --url "https://mobile.yangkeduo.com/goods.html?goods_id=<GOODS_ID>" --state all-dimensions-selected --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-all-dimensions-<GOODS_ID>\raw" --timeout 10 --adb D:\Portable\adb\adb.exe
```
PDD 规格面板不可避免显示收货区域和掩码手机号。原始截图/XML 只能留在
`%LOCALAPPDATA%\cmbuyer\artifacts\...\raw` 隔离目录,不供 agent、fixture、业务或 HTTP 上传读取;
T-103 已在 `client/src/cmbuyer_client/device/sku_evidence_sanitizer.py` 实现本机确定性脱敏器,并由
`client/scripts/sanitize_sku_panel_evidence.py` 提供离线 CLI。当前 `t103-privacy-v1` 配置精确绑定
PKG110 / Android 16 / 拼多多 8.17.0 / goods_id `937122477375` / 1080×2400,把截图的
`[0,0,1080,540)` 整宽隐私带遮罩,并按相同几何边界递归移除 XML 节点;跨界容器只清空自身敏感属性,
保留下方子节点。地址依靠已确认的整块几何隔离而不是易漏的关键词表;完整、掩码、带分隔符或跨节点
手机号残留会被自动复检拒绝。
```powershell
.\client\.venv\Scripts\python.exe client\scripts\sanitize_sku_panel_evidence.py --raw-dir "<证据目录>\raw" --output-dir "<证据目录>\derived"
```
脱敏器校验源 manifest 与文件哈希、设备/App/商品/人工状态、分辨率和 XML 隐私结构,只允许发布到
同级且尚不存在的 `derived`;失败或发布竞态不覆盖已有目录、不留下 staging。派生 manifest 记录
`privacy_tier=SANITIZED`、sanitizer 版本、清理计数及本机 source/derived 哈希,不记录原始路径、serial
或页面正文。只有自动复检通过并经人确认状态对应性的派生物,agent 才能读取并提取最小 fixture、编写判据。
2026-08-04 的首轮证据只用于确认上述入口事实;其中 `initial` 不是规格面板,另两张原始截图含隐私
区域,因此 XML 未读取、fixture 与选择器未生成。T-110 完成受控入口与脱敏契约后,T-103 重新取证;
该边界调整不授权第一趟调整数量、进入确认页、点击“提交订单”或触碰任何支付控件。
2026-08-03 18:55:32 +08:00
Windows 的标准入口是仓库根 `./init.ps1`。它要求 Go、两端目录及其哨兵文件存在;已有合规
`client/.venv` 时,所有采购工具检查与 validator 都使用该解释器。只有 venv 不存在时,才从 `py -0p`
枚举的版本中确定性选择最高的 Python 3.11+ 创建它;没有合规版本时明确失败,绝不回退默认 `python`。
它执行 admin 的 `go mod download` / test / vet / build、client 的 editable install / 包导入 / unittest /
compileall。既有 `client/.venv` 若不是 Python 3.11+ 会明确失败,不会自动覆盖用户环境。`init.sh` 保持等价门禁语义;WSL 或 Unix 环境缺少所需 Go、Python 3.11+
或项目文件时必须非零退出,语法通过或 Unix 成功不构成 Windows / 真机验收。
采购工具的运行时依赖只维护在 `client/requirements.txt`。`client/pyproject.toml` 通过 setuptools
动态读取该文件生成 wheel 的 `Requires-Dist`,避免两份依赖列表漂移;
`scripts/verify_wheel_metadata.py` 会验证生成 wheel 已声明全部这些依赖。
2026-08-03 18:55:32 +08:00
当前 Windows 默认 `python` 仍可能指向 Python 3.10,不满足采购工具的 Python 3.11+ 下限;
不得把未加版本选择器的 `python` 当作采购工具命令。统一入口优先使用既有合规 venv,仅在需要创建时
自动选择 Launcher 中最高的合规版本;本机 Python 3.12 与 3.14 均已验证,主工作区当前选择 Python
3.14。桌面 GUI 与真机流程不属于 T-003 验收范围。
Windows PowerShell 差异:
```powershell
# Go 工具链固定用本机版本,避免自动下载
$env:GOTOOLCHAIN = "local"
```
## 六、验证矩阵与构建产物
| 层级 | 触发条件 | 命令 / 操作 | 通过证据 |
| --- | --- | --- | --- |
2026-08-03 18:55:32 +08:00
| 任务相关验证 | 每个任务必跑 | 改 `admin/` 跑 `go test ./...` + `go vet ./...`;改 `client/` 跑 `.venv\Scripts\python.exe -m unittest discover -s tests -t .` + `.venv\Scripts\python.exe -m compileall -q src tests scripts` | 退出码 0、测试数 |
| 完整门禁 | 发布前;修改 HTTP 契约、数据库 schema、依赖或构建配置时;跨端改动时 | 两端全部测试 + 静态检查 + 两端构建 | 退出码 0、测试数、产物路径 |
| 人工 / 设备验收 | 任何涉及真机页面判据、下单动作或付款路径的任务 | 连接真机执行,记录设备型号、Android 版本、拼多多版本、goods_id、截图与页面 XML 路径 | 人工结论 + 证据文件路径 |
- 任务相关验证不能省略。跨端契约改动必跑完整门禁——两端会同时坏。
- **真机验收只能由人完成。** agent 不得据自身判断把需要真机的任务标为 `DONE`。
- 真机验收必须记录**拼多多 App 版本**。页面判据与版本绑定,换版本即失效。
- 交付采购工具安装包时记录产物路径与 SHA-256;**版本号不能单独证明部署的是本次构建**。
## 七、依赖纪律
- 新增第三方依赖前,先说明用途、替代方案和维护成本,并同步本文。
- 依赖随任务按需引入,不为「将来可能用到」提前添加。
- 不确定的技术选型先更新本文,再进入代码。
- 不允许同一职责并存两套框架。