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

230 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术栈(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。** 同一职责不并存两套方案。
## 五、构建与运行命令
> `init.ps1` 已由 T-003 在 Windows PowerShell 实际验证:它优先复用合规的既有采购工具虚拟环境
> (本机实际为 Python 3.12);仅在虚拟环境不存在时,才从 Python Launcher 的已安装版本中确定性选择
> 最高的 Python 3.11+ 创建它,以 editable 方式安装采购工具,并跑两端
> 离线门禁。桌面 GUI 和真机流程不属于该入口的验收范围。
| 用途 | 采购服务(`admin/`) | 采购工具(`client/`) |
| --- | --- | --- |
| 安装依赖 | `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` |
| 构建 | `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 已完成人工双通道验收)
`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;上述命令保留用于可审计的复现与排障。
### 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 方法。`panel-opened-target-preselected`、
`alternate-all-dimensions-selected`、`target-selection-restored` 三种值写入 manifest 的字段名是
`human_declared_state`,明确表示人工声明,不得将其当作自动识别结果。旧的“未选 / 单维度 / 全选”
状态假设已被真机事实推翻,旧枚举会被采集器和脱敏器明确拒绝。运行拼多多版本必须精确为 `8.17.0`,
前台 package 必须是拼多多,否则 fail closed;目标目录已存在、截图/XML 无效或超时均不得覆盖已有内容
或发布半成品。
T-103 已证明当前衣服商品没有独立「规格/已选」入口。T-110 只批准拼多多 `8.17.0`、goods_id
`937122477375` 上经真机确认的精确唯一 `快要抢光` 作为可逆的受控规格面板入口;“免拼购买 / 单独购买 /
直接拼成”等其他文案不能凭人工经验复用,必须分别重新取证。项目所有者已确认面板刚打开时目标颜色
“黑色CHA(纯棉)”和尺码“M(建议100-115)”均已自动选中;取证不强行取消选择,而是记录刚打开状态、
人工把两个维度都改成非目标值、再恢复目标值三个真实状态。只读取证 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 panel-opened-target-preselected --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-opened-target-<GOODS_ID>-v2\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 alternate-all-dimensions-selected --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-alternate-<GOODS_ID>-v2\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 target-selection-restored --output-dir "$env:LOCALAPPDATA\cmbuyer\artifacts\T-103\sku-panel-target-restored-<GOODS_ID>-v2\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。第一组新 raw 的 PNG 头部由人确认实际为
1080×2376,推翻了 v1 将截图和 XML 坐标空间都写成 1080×2400 的假设;v1 正确拒绝且原始证据未重采。
随后 v2 在同一份 raw 上安全报告 XML `observed 1080x2376` 并拒绝发布,证明其独立配置的
1080×2400 假设同样不成立。v3 用同一 raw 成功发布后,人已确认截图隐私带完整遮挡、派生 XML
不含地址或手机号且目标颜色和尺码仍为预选;但顶部当前价节点横跨隐私带边界,v3 只留下断片,
而下方完整的“提交订单 ¥12.88”属于第一趟硬拒绝区,不能作为 SKU 单价证据。
当前 `t103-privacy-v4` 继续精确绑定 PKG110 / Android 16 / 拼多多 8.17.0 / goods_id
`937122477375`;screenshot space 与 XML coordinate space 仍分别建模、分别校验,当前都精确为
1080×2376,且都把
`[0,0,1080,540)` 作为整宽隐私带。截图覆盖该区域;XML 递归移除区域内节点,跨界容器只清空自身
敏感属性并保留下方子节点。v4 **不改变或缩小截图遮罩**,只允许交界带两个已取证精确 bounds 中,
属于拼多多包、不可点击、可见且启用的 `TextView` 叶节点投影最小价格属性;当前价必须唯一匹配
可选“快卖光”前缀加人民币符号和两位小数,原价同格式且最多一个。包外、可点击、结构漂移、重复、
零值、混杂文本或坐标变化一律拒绝。地址依靠已确认的整块几何隔离而不是易漏的关键词表;完整、
掩码、带分隔符或跨节点手机号残留会被自动复检拒绝。v4 离线实现已通过合成测试,仍须由人用同一
第一态 raw 重跑并确认派生结果,才构成真实价格证据。
```powershell
.\client\.venv\Scripts\python.exe client\scripts\sanitize_sku_panel_evidence.py --raw-dir "<证据目录>\raw" --output-dir "<证据目录>\derived"
```
脱敏器校验源 manifest 与文件哈希、设备/App/商品/人工状态、截图分辨率和 XML 隐私结构;XML 所有
节点观察到的最大 right/bottom 必须精确为 1080×2376,否则只报告非敏感 observed 尺寸并拒绝。它只允许发布到
同级且尚不存在的 `derived`;失败或发布竞态不覆盖已有目录、不留下 staging。派生 manifest 记录
`privacy_tier=SANITIZED`、sanitizer 版本、两个坐标空间、清理计数及本机 source/derived 哈希,不记录
原始路径、serial 或页面正文;v4 另记录 `preserved_crossing_price_nodes`,用于核对严格价格投影
数量。只有自动复检通过并经人确认状态对应性的派生物,agent 才能读取并提取最小 fixture、编写判据。
2026-08-04 的首轮旧证据只用于确认上述入口事实;其中旧 `initial` 不是规格面板,另两张原始截图含隐私
区域,因此 XML 未读取、fixture 与选择器未生成。T-110 完成受控入口与脱敏契约后,T-103 重新取证;
该边界调整不授权第一趟调整数量、进入确认页、点击“提交订单”或触碰任何支付控件。
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 已声明全部这些依赖。
当前 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"
```
## 六、验证矩阵与构建产物
| 层级 | 触发条件 | 命令 / 操作 | 通过证据 |
| --- | --- | --- | --- |
| 任务相关验证 | 每个任务必跑 | 改 `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;**版本号不能单独证明部署的是本次构建**。
## 七、依赖纪律
- 新增第三方依赖前,先说明用途、替代方案和维护成本,并同步本文。
- 依赖随任务按需引入,不为「将来可能用到」提前添加。
- 不确定的技术选型先更新本文,再进入代码。
- 不允许同一职责并存两套框架。