merge: 合入远程 oms + 四作业包布局

保留本机 .env 启动、审计字段和 deal_result GBK 截断。
选品目录按远程迁到 rpa_selection。
This commit is contained in:
34BFF76918A9449ABAB323149DDCEA73 2026-10-03 10:29:44 +08:00
commit fa4df0f27b
170 changed files with 2119 additions and 16438 deletions

17
.gitignore vendored
View File

@ -2,6 +2,8 @@
.DS_Store .DS_Store
.idea/ .idea/
.vscode/ .vscode/
!rpa_selection/.vscode/
!rpa_selection/.vscode/**
*.swp *.swp
# Python # Python
@ -17,11 +19,10 @@ __pycache__/
# Node # Node
node_modules/ node_modules/
state_center_web/dist/
# Local runtime / agent # Local runtime / agent
.runtime/ .runtime/
rpa_yisaiqi/screenshots/ rpa_contract/screenshots/
rpa-chrome-profile/ rpa-chrome-profile/
**/chrome-profile/ **/chrome-profile/
.tmp/ .tmp/
@ -30,17 +31,15 @@ rpa-chrome-profile/
.claude/ .claude/
.superpowers/ .superpowers/
# Duplicate / scratch
state_center_副本/
# Local M5 credentials
state_center/fixtures/m5/connection.local.json
.env .env
.env.* .env.*
!.env.example !.env.example
# Mall login cache (tokens / cookies) # 选品凭据与运行结果
**/data/.auth/ rpa_selection/secrets.json
rpa_selection/data/.auth/
rpa_selection/output/结果/*.xlsx
rpa_selection/output/结果/*.txt
# Media (keep docs, drop recordings and extracted frames) # Media (keep docs, drop recordings and extracted frames)
*.mp4 *.mp4

View File

@ -1,6 +1,6 @@
# AutoRpa # AutoRpa
洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)和**选品班次**(`选品流程/`):收件落 Oracle,请求里不跑选品;选品作业另起进程捞 `deal_flag=0`。采购主链(状态中心编排、艺赛旗/Selenium 点合同系统)仍在仓库里,按问题打开对应行。 洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)和**选品班次**(`rpa_selection/`):收件落 Oracle,请求里不跑选品;选品作业另起进程捞 `deal_flag=0`。合同页面在 `rpa_contract/`;加购 `rpa_cart/`、返利 `rpa_rebate/` 仍是占位。根上不再有 `选品流程/`、`state_center/`、`rpa_yisaiqi/`。
智力用在**定落点、想清楚对错**;速度和 token 靠下面这张地图定点打开,禁止用扫仓代替思考。改对应了,反工和消耗都会少。 智力用在**定落点、想清楚对错**;速度和 token 靠下面这张地图定点打开,禁止用扫仓代替思考。改对应了,反工和消耗都会少。
@ -21,27 +21,25 @@
| 接口1 收件契约 | `rpa_oms/app/inbound/sales_goods.py`、`envelope.py` | `rpa_oms/tests/test_sales_goods_receive.py::点名` | | 接口1 收件契约 | `rpa_oms/app/inbound/sales_goods.py`、`envelope.py` | `rpa_oms/tests/test_sales_goods_receive.py::点名` |
| 接口1 Oracle 落表 | `rpa_oms/app/inbound/oracle_repo.py` | `rpa_oms/tests/test_oracle_repo.py::点名` | | 接口1 Oracle 落表 | `rpa_oms/app/inbound/oracle_repo.py` | `rpa_oms/tests/test_oracle_repo.py::点名` |
| 接口1 入口 / 鉴权 | `rpa_oms/app/main.py`、`auth.py` | 同上;真库见 `rpa_oms/README.md` Windows 段 | | 接口1 入口 / 鉴权 | `rpa_oms/app/main.py`、`auth.py` | 同上;真库见 `rpa_oms/README.md` Windows 段 |
| 选品规则 / 拆量 | `选品流程/rules.py`、`pipeline.py` | `选品流程/tests/test_pipeline.py::test_split_across_malls_is_success` | | 选品规则 / 拆量 | `rpa_selection/rules.py`、`pipeline.py` | `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` |
| 选品班次落表 | `选品流程/job.py`、`persist.py`、`repo.py` | `选品流程/tests/test_job.py::test_job_searches_barcode_not_approval_no` | | 选品班次落表 | `rpa_selection/job.py`、`persist.py`、`repo.py` | `rpa_selection/tests/test_job.py::test_job_searches_barcode_not_approval_no` |
| 合同页面步骤 S02–S13 | `rpa_yisaiqi/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_yisaiqi/screenshots/CONTRACT_CREATE/ACCEPT.txt` | | 合同页面步骤 | `rpa_contract/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt` |
| 合同入参 / 客商草稿 | `rpa_yisaiqi/contract_draft.py` | `state_center/tests/test_yisaiqi_contract_draft.py::点名` | | 合同入参 / 客商草稿 | `rpa_contract/contract_draft.py` | `rpa_contract` 点名用例 |
| 节点入口、回调 complete | `rpa_yisaiqi/contract_create_node.py`、`callback_client.py` | 回调 JSON 含 `contractSystemNo` | | 节点入口 | `rpa_contract/contract_create_node.py`、`main.py` | 读到正式合同号 |
| 浏览器适配 | `rpa_yisaiqi/apa_browser_adapter.py` | `rpa_yisaiqi/tests/test_apa_browser_adapter.py::点名` | | 浏览器适配 | `rpa_contract/apa_browser_adapter.py` | `rpa_contract/tests/test_apa_browser_adapter.py::点名` |
| 文字验收 / 截图策略 | `rpa_yisaiqi/runtime/accept.py` | 只读 `ACCEPT.txt`,禁止读 png | | 文字验收 / 截图策略 | `rpa_contract/accept.py` | 只读 `ACCEPT.txt`,禁止读 png |
| 艺赛旗画布代码块 | `rpa_yisaiqi/build_multinode_seq.py`(源)→ `studio/`(编译产物) | 未说画布不要改这两处 | | 加购 / 支付回传 | `rpa_cart/README.md` | 占位,未实现 |
| 跨节点共享状态 | `rpa_yisaiqi/_flow_state.py` | — | | 返利班 | `rpa_rebate/README.md` | 占位,未实现 |
| 状态机 / API | `state_center/app/main.py`、`web_state_machine.py`、`match_engine.py` | `state_center/tests/test_web_state_machine.py::点名` 等 | | 四个口地图 | `rpa_oms/README.md`、根 `README.md` | — |
| 前端看板 | `state_center_web/src/stateMachine.ts`、`App.tsx` | 浏览器点一遍相关页 | | 节点业务规则(点名一节) | `docs/RPA订单中心接口开发口径_20260930.md` | 禁止通读 `docs/` |
| 一键启动(状态中心) | `scripts/dev.py start` | 状态中心 :8000,前端 :5173 |
| 节点业务规则(点名一节) | `资料/采购主链RPA一期实施与验收规范.md` | 禁止通读 `资料/`、`docs/` |
禁止打开:`node_modules/`、`chrome-profile/`、任何 png。Chrome 用户目录只在仓库外 `D:\work\rpa-chrome-profile`。Oracle 密码放本机 `rpa_oms/.env`(选品空缺时复用);商城账号放 `选品流程/.env`。都已 gitignore,不要写进仓库、README、示例 JSON,不要 `git add` / commit / push。 禁止打开:`node_modules/`、`chrome-profile/`、任何 png。Chrome 用户目录只在仓库外 `D:\work\rpa-chrome-profile`。Oracle 密码放本机 `rpa_oms/.env`(选品空缺时复用);商城账号放 `rpa_selection/.env`。都已 gitignore,不要写进仓库、README、示例 JSON,不要 `git add` / commit / push。
## 接口1 落点(订单中心) ## 接口1 落点(订单中心)
订单中心四接口,本期只开放接口 1:销售商品对码数据接收。`POST /api/oms/v1/sales-goods`,鉴权 header `X-Oms-Token`(`OMS_INBOUND_TOKEN`)。落表 `rpa_sales_goods_mapping_data`,`deal_flag=0`,等选品作业捞;**请求里不跑选品**。 订单中心四接口,本期只开放接口 1:销售商品对码数据接收。`POST /api/oms/v1/sales-goods`,鉴权 header `X-Oms-Token`(`OMS_INBOUND_TOKEN`)。落表 `rpa_sales_goods_mapping_data`,`deal_flag=0`,等选品作业捞;**请求里不跑选品**。
默认 `OMS_REPO=oracle`,本机连不上库用 `OMS_REPO=memory`。服务端口 **8001**,不走 `scripts/dev.py`。 运行时只落 Oracle。单测用内存替身,不连库。服务端口 **8001**。
| 步 | 函数 | 要点 | | 步 | 函数 | 要点 |
| --- | --- | --- | | --- | --- | --- |
@ -58,11 +56,11 @@
验收(内存、不连库):`cd rpa_oms` → `uv run pytest tests/test_sales_goods_receive.py::点名 -q`。真库:`GET /health` 后把 `examples/sales_goods_batch.json` POST 两次(第一次两条 `ACCEPTED`,第二次两条 `DUPLICATE`)。 验收(内存、不连库):`cd rpa_oms` → `uv run pytest tests/test_sales_goods_receive.py::点名 -q`。真库:`GET /health` 后把 `examples/sales_goods_batch.json` POST 两次(第一次两条 `ACCEPTED`,第二次两条 `DUPLICATE`)。
修接口1不要顺手改 `state_center/`、`rpa_yisaiqi/`、`选品流程/`。接口 2–4 未开放,不要先写。 修接口1不要顺手改 `rpa_selection/`、`rpa_cart/`、`rpa_contract/`、`rpa_rebate/`。接口 2–4 未开放,不要先写。
## 选品落点 ## 选品落点
班次作业在 `选品流程/`,不在接口 1 请求里跑。从表 1 领 `deal_flag=0`,按**条形码**搜 1 号药城 / 药帮忙,两平台合并后按四级仓序贪心拆量,写入表 2。凑齐才落成功行;凑不齐不落部分成功。 班次作业在 `rpa_selection/`,不在接口 1 请求里跑。从表 1 领 `deal_flag=0`,按**条形码**搜 1 号药城 / 药帮忙,两平台合并后按四级仓序贪心拆量,写入表 2。凑齐才落成功行;凑不齐不落部分成功。
口径:`docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。 口径:`docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。
@ -76,11 +74,11 @@
湖南壹号、甘肃壹号云不在自营集合。不加购,不推接口 2。 湖南壹号、甘肃壹号云不在自营集合。不加购,不推接口 2。
验收:`cd 选品流程` → `uv run pytest tests/test_pipeline.py::test_split_across_malls_is_success -q`(杭州 3 + 广东 7 = 计划 10)。 验收:`cd rpa_selection` → `uv run pytest tests/test_pipeline.py::test_split_across_malls_is_success -q`(杭州 3 + 广东 7 = 计划 10)。
## CONTRACT_CREATE 落点 ## CONTRACT_CREATE 落点
`run_contract_create_flow` 串起来;修一步只打开对应函数。 页面脚本在 `rpa_contract/`。`run_contract_create_flow` 串起来;修一步只打开对应函数。从表 3 合单、写表 4 尚未接。
| 步 | 函数 | 要点 | | 步 | 函数 | 要点 |
| --- | --- | --- | | --- | --- | --- |
@ -100,7 +98,7 @@
## 返利中心及之后 ## 返利中心及之后
合同审批通过后才进这一段。三把钥匙:`contractSystemNo` → `rebateOrderNo` → `receiptNoticeNo`。页面步骤未建,联调 `rpa_yisaiqi/runners/mock.py`。开做时按合同同款:一个节点一份 `*_flow_steps.py`,验收只看 ACCEPT / 点名用例。 `rpa_rebate/` 仍是占位。合同审批通过后才进这一段。三把钥匙:`contractSystemNo` → `rebateOrderNo` → `receiptNoticeNo`。开做时按合同同款:一个节点一份 `*_flow_steps.py`,验收只看 ACCEPT / 点名用例。
| 节点 | 谁做 | 要回传 | 页面要做什么(尚未拆函数) | | 节点 | 谁做 | 要回传 | 页面要做什么(尚未拆函数) |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -108,24 +106,21 @@
| `REBATE_IMPORT` | RPA | 成功/失败行 | 下模板 → 按字段填 Excel → 上传 → 读导入结果 | | `REBATE_IMPORT` | RPA | 成功/失败行 | 下模板 → 按字段填 Excel → 上传 → 读导入结果 |
| `REBATE_SUBMIT` | RPA | `submitted` | 点提交送审 | | `REBATE_SUBMIT` | RPA | `submitted` | 点提交送审 |
| `M5_CREATE_ORDER` | RPA 桌面端 | `m5OrderNo` | 录单后**停、不保存**。`M5_QC_MISSING` 必须识别准(直接转人工、不重试) | | `M5_CREATE_ORDER` | RPA 桌面端 | `m5OrderNo` | 录单后**停、不保存**。`M5_QC_MISSING` 必须识别准(直接转人工、不重试) |
| `M5_MANUAL_CONFIRM` | 采购 | — | 人在 M5 里保存签订;前端任务推进 | | `M5_MANUAL_CONFIRM` | 采购 | — | 人在 M5 里保存签订 |
| `M5_RECEIPT_NOTICE` | 采购 | `receiptNoticeNo` | 人填收货通知单号,全链终点钥匙 | | `M5_RECEIPT_NOTICE` | 采购 | `receiptNoticeNo` | 人填收货通知单号,全链终点钥匙 |
| `ARCHIVE_JOB` | 状态中心 | — | 无页面 |
调度:`rpa_yisaiqi/agent.py`。人节点在 `HUMAN_NODES`。不要连续轮询审批;返利段开始前的 `CONTRACT_APPROVE_WAIT` 约 30 分钟查一次。 不要连续轮询审批;返利段开始前大约 30 分钟查一次。
## 验收 ## 验收
- 接口1:点名 `rpa_oms/tests/test_sales_goods_receive.py` 或 `test_oracle_repo.py`;禁止为「显得全面」跑整包再去连真库。 - 接口1:点名 `rpa_oms/tests/test_sales_goods_receive.py` 或 `test_oracle_repo.py`;禁止为「显得全面」跑整包再去连真库。
- 选品:点名 `选品流程/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。 - 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
- 合同页:只读控制台 `ACCEPT` 或 `screenshots/<节点>/ACCEPT.txt`。 - 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
- 单测只跑点名用例,例如 `state_center/tests/test_yisaiqi_contract_flow_steps.py::test_read_contract_number_falls_back_to_my_contracts_list`。禁止跑整份该文件。 - 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
- 该文件已打掉 `time.sleep`;不要再拿全量带睡的套件当回归。
## 不要 ## 不要
- 不要扫根目录,不要通读 `资料/`、`docs/`。需要背景就点名打开一节。 - 不要扫根目录,不要通读 `docs/`。需要背景就点名打开一节。
- 不要顺手改 `studio/`、`build_multinode_seq.py`(画布任务除外)。
- 不要把 Oracle 密码写进仓库、README、示例 JSON。 - 不要把 Oracle 密码写进仓库、README、示例 JSON。
- 不要在接口1 请求路径里跑选品。 - 不要在接口1 请求路径里跑选品。
- 不要为「显得全面」多开文件、重跑、看图。落点外的改动会制造反工。 - 不要为「显得全面」多开文件、重跑、看图。落点外的改动会制造反工。

View File

@ -1,367 +0,0 @@
# 状态中心 ↔ 艺赛旗并行开发方案
## 核心原则:契约驱动,各自独立,最后对接
双方只通过 **HTTP 回调契约** 交互,这个契约在交接文档里已经明确定义。在对接前,双方各自用假数据/假回调验证自己的逻辑,互不阻塞。
---
## 对接契约(双方共同遵守)
### 基础信息
- **状态中心地址**:联调时给艺赛旗实际 IP,如 `http://192.168.x.x:8000`
- **鉴权**:所有回调必须带 header `X-Agent-Token: <token>`(默认 `dev-agent-token`,上线换正式值)
- **幂等键**:`complete` 回调可带 `idempotencyKey`,重发同键不会二次推进
### 三个回调接口(艺赛旗调状态中心)
#### 1. 节点开始
```bash
POST /api/jobs/{jobId}/nodes/{nodeCode}/start
Content-Type: application/json
X-Agent-Token: dev-agent-token
{
"attemptNo": 1,
"executorPlatform": "iS-RPA",
"operator": "agent"
}
```
#### 2. 节点成功
```bash
POST /api/jobs/{jobId}/nodes/{nodeCode}/complete
Content-Type: application/json
X-Agent-Token: dev-agent-token
{
"attemptNo": 1,
"executorRunId": "YD-RUN-001",
"idempotencyKey": "uuid-xxx",
"businessRefs": {
"contractSystemNo": "HT2026...", // 三把钥匙,对应节点必传
"rebateOrderNo": "FL2026...",
"m5OrderNo": "M5-xxx",
"receiptNoticeNo": "SH-xxx"
},
"summary": { "orderCount": 12 },
"artifacts": [
{
"artifactType": "SOURCE_EXCEL",
"storagePath": "/data/jobs/xxx/orders.xlsx",
"fileHash": "sha256:abc",
"visibleRoles": ["PURCHASER", "PLANNER"],
"isMasked": false
}
]
}
```
**返回**:`{"ok": true, "next_node": "OCR_EXTRACT"}` ← 艺赛旗拿到 `next_node` 继续跑下一节点
#### 3. 节点失败
```bash
POST /api/jobs/{jobId}/nodes/{nodeCode}/fail
Content-Type: application/json
X-Agent-Token: dev-agent-token
{
"attemptNo": 1,
"errorCode": "SYS_LOGIN_FAIL", // 见交接文档错误码表
"errorSummary": "验证码识别失败",
"retryable": true, // 系统类填 true,业务类填 false
"routeRole": "OPS", // OPS/PLANNER/PURCHASER
"artifacts": []
}
```
**返回**:`{"ok": true, "job_status": "FAILED_RETRYABLE"}`
### 必须回传的业务编号(三把钥匙)
| 节点 | 必传字段 | 说明 |
| --- | --- | --- |
| CONTRACT_CREATE | `contractSystemNo` | 合同编号(后续返利、M5靠它串) |
| REBATE_HEADER | `rebateOrderNo` | 返利中心总单号 |
| M5_RECEIPT_NOTICE | `receiptNoticeNo` | 收货通知单号(全链终点) |
不传或传空,状态中心返回 **400 拒绝**。
### 错误码规范(部分,全表见交接文档)
| 错误码 | 含义 | retryable | routeRole |
| --- | --- | --- | --- |
| SYS_LOGIN_FAIL | 登录失败 | true | OPS |
| SYS_PAGE_TIMEOUT | 页面超时 | true | OPS |
| UI_ELEMENT_NOT_FOUND | 页面改版找不到元素 | false | OPS |
| BIZ_CONTRACT_DISABLED | 供应商合同被禁 | false | PURCHASER |
| M5_QC_MISSING | M5质管资料缺失 | - | - |
| REBATE_DUP_CODE | 返利重复编码 | false | PURCHASER |
**特殊规则**:`M5_QC_MISSING` 无条件转 `FAILED_MANUAL`,`retryable` 填什么都没用。
---
## 状态中心这边(你的任务)
### 阶段1:独立验证(不依赖艺赛旗)
**时间**:1-2天
**目标**:后端在真 MySQL 上用假回调跑通全链
#### 任务
1. **环境准备**
- Docker 起 MySQL 8,执行 `schema.sql` 建库。
- 配 `MYSQL_DSN`,起后端服务。
- 前端 `npm run dev` 起来。
2. **用假回调跑通一单**
- 方式1:用 curl 按顺序调回调接口。
- 方式2:用前端"开发:节点回调模拟"区域的按钮。
- **路径**:建单 → ORDER_FETCH start/complete → PRODUCT_MATCH run(有 MULTI) → 人工确认 → SOURCE_ADD 人工提交 → CONTRACT_CREATE complete(带合同编号) → ... → ARCHIVE_JOB。
- **验证点**:
- job 状态正确推进(PENDING → RUNNING → WAITING_USER → ...)。
- 业务编号正确落库(合同编号、返利总单号、M5单号、收货通知单号)。
- job_node 每个节点有历史记录,attempt_no 正确。
- user_task 在该创建时创建,提交后关闭。
- job_artifact 证据落库。
- 条件节点(OCR_EXTRACT/SOURCE_ADD)正确跳过或执行。
3. **发现并修复真实问题**
- SQL 语法问题(如 MySQL 8 的保留字冲突)。
- JSON 列序列化问题(Python dict → MySQL JSON → 前端拿到的是对象还是字符串)。
- 事务回滚是否正确。
- datetime 字段格式(MySQL 存的 `YYYY-MM-DD HH:MM:SS`,前端拿到的格式)。
- 幂等逻辑是否真的防重(重复 complete 同一节点,状态不会乱)。
4. **记录真实 API 响应**
- 把 `GET /api/jobs/{id}` 的完整响应存下来,给 UI 优化做参考。
#### 产出
- ✅ 一个能在真 MySQL 上跑通全链的后端(用假回调验证)。
- ✅ 一份真实响应示例 + 发现的问题清单。
- ✅ 前端能正常展示真实数据(不再依赖 `demoJobDetailMap`)。
### 阶段2:优化与补全(不依赖艺赛旗)
**时间**:2-3天
**目标**:UI 优化 + 补缺失接口 + 加调度重试
#### 任务
1. **UI 优化**
- 按刚才的方案,分区 + 可折叠 + 调试开关。
- 基于阶段1 的真实响应,确保所有字段正确展示。
2. **补缺失接口**
- `GET /api/jobs/{id}/match/unmatched`:返回该 job 未命中的订单行列表。
- 其他小接口(如果阶段1 发现有遗漏)。
3. **调度与重试(任务6)**
- 定时建单:每天早上扫前一天订单。
- 系统类错误自动重试(`FAILED_RETRYABLE` 状态的 job,重试 1-2 次)。
- 超时升级:合同审批超时、人工待办超时转 OPS。
#### 产出
- ✅ 优化后的前端(信息分层清晰,调试模式可开关)。
- ✅ 完整的接口(验收点名的都实现)。
- ✅ 调度器能定时触发建单和重试。
### 阶段3:准备对接(等艺赛旗阶段2 完成)
**时间**:半天
**目标**:给艺赛旗提供联调环境
#### 任务
1. **部署联调环境**
- 把后端部署到艺赛旗能访问的机器(或你本机开端口映射)。
- 给艺赛旗提供:基础地址(如 `http://192.168.x.x:8000`) + 正式 `X-Agent-Token`。
2. **准备测试数据**
- 在库里建几个不同类型的 job:
- 医药城线上单(会跳过 OCR_EXTRACT)。
- 线下单(会走 OCR_EXTRACT)。
- 有对码 MULTI 的单。
- 缺货源的单(会走 SOURCE_ADD)。
- 每个 job 停在 ORDER_FETCH 节点,等艺赛旗来调 start/complete 推进。
3. **写联调检查表**
- 逐节点列验收点,双方对着检查表勾。
#### 产出
- ✅ 一个可供艺赛旗调用的联调环境。
- ✅ 测试数据 + 联调检查表。
---
## 艺赛旗那边(你要转述给他们)
### 阶段1:独立开发(不依赖状态中心)
**时间**:2-3周(他们的活重)
**目标**:每个节点的页面自动化都能独立跑通
#### 任务
1. **理解回调契约**
- 仔细读 `给艺赛旗的开发交接.md`。
- 确认三个回调接口的字段、错误码表。
2. **逐节点开发页面自动化**
- **节点顺序**(他们的优先级):
1. ORDER_FETCH(订单获取):登录医药城/小药药 → 导出 Excel → 存证据。
2. CONTRACT_CREATE(合同建单):登录合同系统 → 填表 → 上传 PDF(医药城) → 提交送审 → 拿到合同编号。
3. REBATE_HEADER(返利总单):登录返利中心 → 按合同编号查总单 → 填必填项 → 保存 → 拿到返利总单号。
4. REBATE_IMPORT(返利细单):下载模板 → 填字段 → 上传 → 读成功/失败行。
5. REBATE_SUBMIT(返利送审):点"提交送审"按钮。
6. M5_CREATE_ORDER(M5建单):桌面自动化 → 录单 → **停,不保存**。
7. CONTRACT_APPROVE_WAIT(合同审批):定时查审批状态(到点被状态中心扫到时才查一次)。
- **每个节点独立验证**:
- 不调状态中心,在自己环境里跑(比如用测试账号登录真实系统,手工给初始数据)。
- 验证能拿到业务编号(合同编号、返利总单号等)。
- 验证错误情况能识别出正确的错误码(如登录失败是 `SYS_LOGIN_FAIL`,合同被禁是 `BIZ_CONTRACT_DISABLED`)。
3. **本地模拟回调**
- 自己起个假的状态中心(或者直接用 curl 打印 JSON),验证回调 payload 格式对不对。
- 示例(他们自己写个小脚本验证):
```python
# 假的状态中心 mock.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/jobs/<job_id>/nodes/<node_code>/complete', methods=['POST'])
def complete(job_id, node_code):
data = request.json
print(f"收到 {node_code} complete:", data)
# 检查必传字段
if node_code == 'CONTRACT_CREATE' and not data.get('businessRefs', {}).get('contractSystemNo'):
return jsonify({"error": "缺少合同编号"}), 400
return jsonify({"ok": True, "next_node": "下一节点"})
app.run(port=8000)
```
#### 产出
- ✅ 每个节点的页面自动化脚本(iS-RPA 流程)。
- ✅ 能正确识别各种错误并返回对应错误码。
- ✅ 能拿到三把钥匙(合同编号、返利总单号、收货通知单号)。
### 阶段2:准备对接(等状态中心阶段3 准备好)
**时间**:1天
**目标**:改成调真实状态中心接口
#### 任务
1. **获取联调环境信息**
- 从状态中心拿到:基础地址 + `X-Agent-Token`。
2. **改造回调逻辑**
- 把阶段1 的假回调改成调真实状态中心接口。
- **关键**:拿到 `complete` 返回的 `next_node`,继续跑下一节点(不要自己写死顺序)。
3. **小范围试跑**
- 先跑一个最简单的节点(如 ORDER_FETCH),验证回调能调通、状态中心能正确推进。
#### 产出
- ✅ 能调通状态中心回调接口的 RPA 脚本。
---
## 联调阶段(双方一起)
**时间**:3-5天
**目标**:端到端跑通一单真实流程
### 任务
1. **按节点逐个验证**
- 顺序:ORDER_FETCH → (OCR_EXTRACT) → PRODUCT_MATCH(状态中心内部) → (SOURCE_ADD 人工) → CONTRACT_CREATE → ...
- 每个节点验证:
- RPA 能正确调 start/complete/fail。
- 状态中心正确推进到下一节点或转人工。
- 业务编号正确落库。
- 证据正确存储。
2. **跑完整链**
- 医药城线上单(跳过 OCR)。
- 线下单(走 OCR)。
- 有对码 MULTI 的单(走人工确认)。
- 缺货源的单(走 SOURCE_ADD 人工)。
- 合同审批等待(定时查几次,验证不会死等)。
3. **异常场景**
- 登录失败 → 重试。
- 页面改版找不到元素 → 转 OPS。
- M5 质管缺失 → 直接 FAILED_MANUAL,不重试。
- 返利重复编码 → 该单挂起,整批继续。
4. **对照验收规范逐条勾**
- 打开 `实施与验收规范.md`,每个节点的代码验收 + 业务验收两栏逐条过。
### 产出
- ✅ 一个能端到端跑通的完整流程(从订单获取到归档)。
- ✅ 验收报告(哪些通过,哪些有问题)。
---
## 时间线(并行开发)
```
第1周 第2周 第3周
状态中心 │────阶段1────│───阶段2───│──阶段3──│────联调────│
真MySQL跑通 UI优化+补全 准备环境 与艺赛旗对接
(1-2天) (2-3天) (半天) (3-5天)
艺赛旗 │────────────阶段1────────────│─阶段2─│────联调────│
独立开发各节点页面自动化 改真回调 与状态中心对接
(2-3周) (1天) (3-5天)
```
**关键点**:
- 前两周双方完全独立,互不阻塞。
- 状态中心先做完(1周内),等艺赛旗追上。
- 艺赛旗开发周期长(页面自动化细节多),但不影响状态中心进度。
- 第3周开始联调,双方都已在各自环境验证过,对接就是走形式。
---
## 给艺赛旗的检查清单(转述)
在开始联调前,艺赛旗必须自证能做到:
### 功能检查
- [ ] 能登录医药城/小药药/合同系统/返利中心/M5,并正确处理验证码/超时。
- [ ] 能导出订单 Excel 并存到指定路径。
- [ ] 能建合同、上传 PDF(医药城)、拿到合同编号。
- [ ] 能在返利中心按合同编号查总单、填表、拿到返利总单号。
- [ ] 能下载返利细单模板、填字段、上传、读成功/失败行。
- [ ] 能点返利送审按钮。
- [ ] 能用桌面自动化在 M5 录单(但不保存)。
- [ ] 能定时查合同审批状态(不是连续轮询,是到点查一次)。
### 回调检查
- [ ] 知道何时调 start、何时调 complete、何时调 fail。
- [ ] complete 时能正确带 `businessRefs`(三把钥匙对应节点必传)。
- [ ] fail 时能正确识别错误码(`SYS_*` vs `BIZ_*`,`retryable` 填对)。
- [ ] 拿到 `next_node` 后能继续跑下一节点(不写死顺序)。
- [ ] 所有回调都带 `X-Agent-Token` header。
### 异常检查
- [ ] 登录失败时返回 `SYS_LOGIN_FAIL`,`retryable=true`。
- [ ] 页面改版找不到元素时返回 `UI_ELEMENT_NOT_FOUND`,`retryable=false`,`routeRole=OPS`。
- [ ] M5 质管缺失时返回 `M5_QC_MISSING`(状态中心会直接转 FAILED_MANUAL,不重试)。
- [ ] 供应商合同被禁时返回 `BIZ_CONTRACT_DISABLED`,`retryable=false`,`routeRole=PURCHASER`。
---
## 总结
**你现在要做的**:
1. 马上开始阶段1(真 MySQL 跑通),1-2天搞定。
2. 接着做阶段2(UI 优化 + 补全),2-3天搞定。
3. 然后做阶段3(准备联调环境),半天搞定。
4. **这时你这边已经全部做完**,等艺赛旗追上(他们要 2-3 周)。
**艺赛旗要做的**(你转述给他们):
1. 阶段1:独立开发各节点页面自动化,2-3周。期间不需要状态中心配合,自己用假数据验证。
2. 阶段2:拿到状态中心联调环境信息后,改成调真实接口,1天。
3. 联调阶段:双方一起按检查表逐条验收,3-5天。
**不会互相阻塞**:你 1 周内做完你的,艺赛旗 2-3 周做完他们的,最后 3-5 天一起联调。
---
要我把这份文档发给艺赛旗吗?或者你先看看有没有要调整的。

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# AutoRpa
福建洁达采购 RPA。仓库里叠过两套产品,以本文「现行」为准。
## 现行工作
订单中心四接口 + 四条独立作业。对码在订单中心做完,本方收件、选品、录合同/返利、回写结果。
| 口 | 方向 | 底表 | 状态 |
| --- | --- | --- | --- |
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | 进行中(`rpa_oms/`) |
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 选品已落表,push_status=0,路径仍空 |
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | 复用接口 1 接收壳 |
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 路径空,先造数 |
作业互不串联:选品读表 1,合同/返利读表 3。HTTP 收件不同步跑 RPA。
## 代码入口
| 目录 | 身份 |
| --- | --- |
| `rpa_oms/` | 四个 HTTP 口,一个 FastAPI。先做接口 1 |
| `rpa_selection/` | 选品班。读表 1 `deal_flag=0`,双平台选品,写表 2 |
| `rpa_cart/` | 加购 + 支付回传(占位)。表 2 → 口 2 |
| `rpa_contract/` | 合同班。页面录入在本包;表 3 合单、接口 4 尚未接 |
| `rpa_rebate/` | 返利班(占位)。查号、录协议 → 表 4 |
| 包 | 读 | 写 | 口 |
| --- | --- | --- | --- |
| `rpa_selection` | 表 1 `deal_flag=0` | 表 2 `operate_type=1`,表 1 处理标志 | 不直接调口;口 2 由 `rpa_cart` 推 |
| `rpa_cart` | 表 2 | 表 2 `push_status`;支付行 | 口 2 |
| `rpa_contract` | 表 3 `process_status=0` | 表 4 `actionCode=1` | 口 3 收件在 oms;口 4 回写 |
| `rpa_rebate` | 表 3 / 表 4 待查号与待录协议 | 表 4 `contractStatus=2`、`actionCode=2` | 口 4 |
合同怎么录、合单和回写口径见 `docs/合同系统录入流程.md`。页面控件以 `rpa_contract/contract_flow_steps.py` 为准。
新接口只进 `rpa_oms/`。起服务见 [`rpa_oms/README.md`](rpa_oms/README.md)。
## 文档入口
现行口径和表:[`docs/README.md`](docs/README.md)。
开发对照只看:
- `docs/RPA订单中心接口开发口径_20260930.md`
- `docs/ddl/rpa_oms_staging_tables.sql`
一期主链需求/设计在 `docs/archive/`;`docs/superpowers/specs/` 里 9 月 12–13 日的四流程稿也是归档。作业语义以口径为准。

BIN
docs/API鉴权.doc Normal file

Binary file not shown.

35
docs/README.md Normal file
View File

@ -0,0 +1,35 @@
# docs
按现行 / 作业资产 / 历史查阅。
## 现行(订单中心四接口)
| 文件 | 用途 |
| --- | --- |
| `RPA订单中心接口开发口径_20260930.md` | 开发对照,作业语义以这份为准 |
| `RPA订单中心接口口径讨论纪要_20260929.md` | 为什么这么定 |
| `ddl/rpa_oms_staging_tables.sql` | Oracle 11.2 四张暂存表 |
| `ddl/列对照.md` | 与接口 Excel 列对照 |
| `API鉴权.doc` | 调订单中心时的 Token;接口 1、3 入站鉴权由本方定 |
| `下一会话任务_建表.md` | 建表任务说明(已完成) |
| `合同系统录入流程.md` | 合同班合单、尽调、提交、回写;页面步骤指向 `rpa_contract` |
接口字段、示例报文仍以 Excel 为准(当前 `Downloads/RPA与订单中心接口设计_20260922.xlsx`)。Excel 与口径冲突时,作业语义听口径。
`ddl 2/`、`ddl.zip` 与 `ddl/` 重复,不要用。
## 作业资产
| 文件 | 用途 |
| --- | --- |
| `合同返利系统字段映射_20260911.xlsx` | 返利总单默认值 |
| `采购平台选品条件优先项_20260902.txt` | 选品规则原文 |
| `【项目方案】福建洁达采购合同RPA自动化录入需求_v20260924.docx` | 页面操作参考 |
## 历史(一期 13 节点主链)
见 `archive/`:`PLAN.md`、一期需求/设计/开发规划/验收规范、`一期开发任务清单_表结构_API草案.md`、会议整理版、业务思路 docx、界面截图记录。
那套模型是「一单合同走状态机」。不要用它解释接口 1 的收件箱。
`superpowers/` 是 8–9 月设计稿,同样视为归档。

View File

@ -0,0 +1,86 @@
# 纪要:RPA 与订单中心接口口径
- 时间:2026-09-29~30
- 材料:`RPA与订单中心接口设计_20260922.xlsx`、`API鉴权.doc`、`docs/合同返利系统字段映射_20260911.xlsx`、方案 v20260924
- 开发对照:`docs/RPA订单中心接口开发口径_20260930.md`
---
## 一、接口怎么理解
四个口不是四条流程一对一。1 是选品收件,2 是加购/支付回传,3 是合同和返利共用的行级下发,4 是合同/协议结果回写。
对码不在本方。接口 1 的 `goodsId` 进来就已经有了。
表 1 是收件箱:订单中心推 → 本方接口写入表 1 → 选品捞待处理。不是手填。
---
## 二、已锁定论
### 合单
福建确认一天两班(中午、下午)。Albert:未处理行按供应商汇总,一个供应商一份合同、一份返利。
锁定:
- 按 `supplierId` 合,不用 `omsId`,不用名称
- 任务启动冻结快照,新进数据下一班
- 头字段取该班该供应商第一行(同一班账期不一致也照录,算对方发数)
- 下午再来的另开一份,不改上午已录合同
- 带合同号的协议重录不进按供应商合
### 选品 / 加购 / 支付
- 搜商城用条形码
- 规则不过:表 1 失败 + 表 2 `status=2` + 原因;默认对方**不重推**该 `omsId`(口头再跟对方确认,开发按已锁)
- `status` 只用于加购。支付:有单号就推 `operateType=2`,没付不推,没有支付失败
- 入站一单一个品种;多平台凑数量时同一 `omsId` 多行、`multi_platform_flag=1`、必须同包回(已进表 2)
- 付款仍是采购员两班;支付回传另排,选品不等
### 合同 / 返利回写(业务确认)
合同选 A:提交后回起草;成功=返利中心能搜到合同号。搜不到下一班再搜,不重录合同。
返利选 A 的字面、按现场做:据说无审批流,点送审即成功,点完就回成功并带协议号。以后若开审批流再改。
查号挂在返利班,不单开查状态任务。合同班只录+提交+回起草。
这期不做「合同审完订单中心再推返利包」。二次推只用接口已写的「协议失败带合同号重录」。
暂存号不用于返利。返利用正式合同号去搜。
### 这期范围
- 只做账期。`customerType` 预留,对方固定传 1。预付项目组不做。
- 平台字典:66 = 1 药城,48 = 药帮忙
- 返利总单默认值跟映射表,不跟方案 5.5
- MDM 码对方说默认必填,按必填做;示例没带先不管
- 失败行数量/单价必填、总价舍入:先不管,真数据出问题打回订单中心
### 联调
- Token 测试地址已有;测试用户名 `rpa`(密码不入库)
- 接口 2、4 路径暂时空着,先造数
---
## 三、讨论过程中澄清过的误解
1. 接口 3 一次可以很多行,那是批量收件,不是「已经合成一份合同」。合单是本方按供应商做的。
2. 供应商编码不是小药药商家名。这期用对方给的 MDM/供应商字段填页面。
3. 接口 4 示例是 RPA 把结果推出去,不是「合同审批完通知订单中心再推返利」。
4. `contractStatus=2` 不是让本方重复录合同,是提交后按班次去返利**查号**。
5. 审批/驳回不是本方作业;但合同成功码按接口仍要查号,不能理解成「填完报成功」。
6. 加购「跳过」最后不单开状态码,用失败+原因;和多平台同包回不是一件事。
---
## 四、后置(不挡造数)
- 接口 2、4 真实 URL;`orgSecret` 是否要带;订单中心调本方 1、3 的鉴权
- 只凑到一部分数量时,多平台报文怎么组
- 返利测试环境确认:点送审后协议号是否立即可见
- 加购业务失败不重推:口头跟对方确认一句
开发以口径文档为准。本文只记为什么这么定。

View File

@ -5,9 +5,9 @@
依据: 依据:
- `Downloads/RPA与订单中心接口设计_20260922.xlsx`(以当时最新一份为准) - `Downloads/RPA与订单中心接口设计_20260922.xlsx`(以当时最新一份为准)
- `资料/API鉴权.doc` - `docs/API鉴权.doc`
- `资料/合同返利系统字段映射_20260911.xlsx`(返利总单默认值) - `docs/合同返利系统字段映射_20260911.xlsx`(返利总单默认值)
- `选品流程/` 既有规则 - `rpa_selection/` 既有规则
--- ---
@ -47,7 +47,7 @@
商城查询键:**条形码**。批准文号照收,不拿去搜。 商城查询键:**条形码**。批准文号照收,不拿去搜。
规则沿用 `选品流程/`(自营仓、毛利 ≥ 4%、效期 ≥ 12 个月;两平台合并后按仓序拆量,凑齐计划量才算成功)。毛利用 `salePrice`,拆量目标用 `planPurchaseQty`。详见 `docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。 规则沿用 `rpa_selection/`(自营仓、毛利 ≥ 4%、效期 ≥ 12 个月;两平台合并后按仓序拆量,凑齐计划量才算成功)。毛利用 `salePrice`,拆量目标用 `planPurchaseQty`。详见 `docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。
### 3.2 加购结果(接口 2,`operateType=1`) ### 3.2 加购结果(接口 2,`operateType=1`)

View File

@ -0,0 +1,305 @@
# 福建洁达采购合同全链条自动化设计说明
| 项目 | 内容 |
| --- | --- |
| 文档名称 | 福建洁达采购合同全链条自动化设计说明 |
| 版本 | V1.1 |
| 日期 | 2026-09-12 |
| 适用范围 | 福建广药洁达医药有限公司采购合同、返利中心录入及相关选品作业 |
| 依据 | 《福建洁达采购合同全链条 RPA 自动化录入流程》《采购平台选品条件优先项》(2026-09-02)《合同返利系统字段映射》(2026-09-11) |
| 配套文档 | 《福建洁达四流程技术实现说明》 |
---
## 1. 概述
本方案在现有状态中心与 RPA 执行体系上,将采购作业拆为四条可独立启动的流程:选品采购、订单对码、合同录入、返利录入。
页面操作仍由 RPA 完成。**四条流程启动后的第一步,均由状态中心调用订单中心接口取数(或回写),写入 RPA 数据库并标记处理状态;接口未成功返回前,RPA 不得打开下游系统页面。** 操作人员可在状态中心启动作业、查看进度与业务数据,亦可按约定时间自动触发。
四条流程独立启动、独立失败、独立重跑,互不占用同一执行会话。同一笔业务单据的数据仍按「选品 → 对码 → 合同 → 返利」顺序衔接;前置数据未就绪时,后续流程不予执行,并记录原因。
---
## 2. 建设目标
1. 提供统一操作入口:状态中心可分别启动四条流程,或按计划定时运行。
2. 保证过程可见:每条作业的执行状态、当前环节、责任角色、业务数据及失败原因可在状态中心查询。
3. 保证数据可追溯:订单中心接口数据先落入 RPA 库,记录待处理、成功、失败等状态,支持失败重拉与按单重跑。
4. 保证规则可落地:选品按已确认的优先项执行;合同、返利按字段映射表填写,不自行推断未提供的字段。
5. 控制操作风险:商城付款、未尽调客户的合同填报,均不由机器人自动完成。
---
## 3. 总体方案
```
状态中心(启动 / 监控 / 待办)
│
│ 每条流程启动后,先调订单中心接口
▼
订单中心 ──读取──► 销售总单 / 客户主数据 / 货品 / 计划员 / 账期
◄──回写── 对码结果
│
▼
RPA 数据库(带处理状态:待处理 / 成功 / 失败)
│
│ 仅 STATUS=就绪 时,RPA 才点页面
▼
1号药城 / 药帮忙 / 合同系统 / 返利中心
```
| 流程 | 业务内容 | 建议运行时点 |
| --- | --- | --- |
| 选品采购 | 读取订单中心销售总单,在 1 号药城、药帮忙按规则选品,生成待复核清单 | 销售订单产生之后 |
| 订单对码 | 将选品结果或手工单字段写入订单中心对码表 | 选品完成,或手工单到达之后 |
| 合同录入 | 按字段映射填写合同系统 | 对码完成,且客户已尽调 |
| 返利录入 | 按合同号填写返利中心总单与细单 | 合同编号已生成 |
启动某一流程仅处理该流程范围内的任务,不强制四条连续一次跑完。某流程缺少必要数据或处于人工待办时,系统停留在该流程并写明原因,不向下游系统提交空数据。
---
## 4. 订单中心接口
订单中心是本方案的**唯一业务数据源**。合同系统、返利中心页面上由 RPA 填写、且映射表标注「从订单中心获取」的字段,一律经本接口取得,禁止从商城页面抄写,禁止在 RPA 脚本中写死。
调用原则:
1. **只由状态中心调用。** RPA 不直连订单中心。状态中心将结果写入 RPA 库后,机器人只读本地就绪数据。
2. **先接口、后页面。** 接口失败、超时、缺必填字段,本流程记失败并停止,不打开 1 号药城、合同系统或返利中心。
3. **原文留存。** 每次响应写入 RPA 库(含原始报文),状态中心可查,便于对账与重拉。
4. **按单幂等。** 同一业务键重复启动不重复插入成功数据;失败允许按同一键重拉。
正式 URL、鉴权、报文格式由订单中心提供后锁定。本节先按业务能力列出必须具备的接口;能力未齐备前,对应流程不得对生产系统写入。
### 4.1 接口清单与调用时点
| 序号 | 接口能力 | 方向 | 调用流程 | 调用时点 | 失败后果 |
| --- | --- | --- | --- | --- | --- |
| A | 销售总单查询 | 读 | 选品 | 选品启动后、上商城之前 | 选品停止,不上 1 号药城 / 药帮忙 |
| B | 销售明细查询 | 读 | 选品 | 紧接 A,按总单取行 | 同 A;无批准文号的行单独失败,不阻断其他行 |
| C | 对码结果回写 | 写 | 对码 | 选品完成或手工单识别之后 | 对码失败;合同不得启动 |
| D | 对码商家与货品查询 | 读 | 对码 / 合同 | 回写成功后,取客户名称、货品编码等 | 合同不得启动 |
| E | 客户主数据查询 | 读 | 合同 | 合同启动后、打开合同系统之前 | 合同停止;不填报 |
| F | 合同货品明细查询 | 读 | 合同 | 与 E 一并发起 | 同 E;缺货品行则本单失败 |
| G | 计划员与采购员对照查询 | 读 | 返利 | 返利启动后、打开返利中心之前 | 返利停止;不猜测计划员 |
| H | 返利源数据查询 | 读 | 返利 | 与 G 一并发起 | 缺供应商编码、订单号或货源细单号则本单失败 |
### 4.2 各接口必须返回的字段
字段名称以订单中心正式文档为准。下表为业务含义,来源为流程图及《合同返利系统字段映射》。
**A 销售总单查询**
入参:订单日期(默认前一日),可选供应商名称。
出参(每张总单):销售总单号、订单日期、客户(商家)名称、客户编码、来源、总金额。
**B 销售明细查询**
入参:销售总单号。
出参(每行):订单号、批准文号、通用名、规格、生产厂家、采购数量、**销售价格**(选品计算 5% 毛利的基准,不得用采购价替代)、平台商品编码(如有)。
无批准文号、无销售价格的行标记失败,不进入商城查询。
**C 对码结果回写**
入参:订单号或销售明细行标识,以及待写入对码表的字段(品名、规格、厂家、批准文号、数量、选品后的货品信息等)。
出参:写入是否成功。
说明:对应流程图「提取需要字段数据,传到订单中心对码功能数据表」。此为写接口,与查询分开。
**D 对码商家与货品查询**
入参:客户(商家)标识、货品匹配条件。
出参:客户名称、客户编码;货品编码、货品名称、注册证号、规格、单位。
说明:合同系统「客户名称」对照此处填写,不以商城店铺名为准。
**E 客户主数据查询**
入参:客户编码或客户名称。
出参:
| 字段 | 用途 |
| --- | --- |
| 客户名称、客户编码 | 合同「客户名称」 |
| 客户类型(预付 / 账期) | 预付:合同版本「我司定制」、预付款「是」、付款期限 1 天;账期:合同版本「非格式合同」、预付款「否」、付款期限取账期天数 |
| 账期天数 | 仅账期客户必填;映射表要求订单中心具备该维护功能 |
| 合同签订日期、开始日期、结束日期 | 合同对应日期字段 |
| 尽调状态(如订单中心有维护) | 未尽调则本份合同跳过并邮件通知采购员。若尽调仅在合同系统回显,则本字段改为页面读取,须在接口说明书中明确 |
**F 合同货品明细查询**
入参:销售总单号或订单号。
出参(每行):货品编码、货品名称、注册证号、规格、单位、数量、单价。
说明:用于合同明细导入模板。数量、单价取订单中心,不取商城实时价。
**G 计划员与采购员对照查询**
入参:计划员或采购员。可按全量或按供应商查询。
出参:计划员、对应采购员。
说明:映射表注明「订单中心需要有计划员和采购员的对应关系」。无对照则返利总单「计划员」无法填写。
**H 返利源数据查询**
入参:合同号或销售总单号。
出参:供应商编码、返利单位编码、计划员、订单号、货源细单号;以及细单行的货品编码、数量、含税协议单价。
说明:返利细单「细单备注」填订单号,「货源细单 ID」填货源细单号,二者均须订单中心给出。
### 4.3 调用顺序(按一笔业务)
```
选品启动
① 调 A、B → 写入销售总单/明细(就绪) → 再上商城查价
A/B 失败 → 选品失败,不上商城
对码启动
② 调 C 回写对码表 → 调 D 取客户与货品编码 → 就绪后方可合同
C/D 失败 → 对码失败,合同入口拒绝启动
合同启动
③ 调 E、F → 客户类型/账期/日期/货品行就绪 → 再打开合同系统
必填缺失 → 合同失败,不填报
未尽调 → 本单跳过,邮件通知,处理下一单
返利启动
④ 调 G、H → 计划员对照与细单源数据就绪 → 再打开返利中心
必填缺失 → 返利失败,不猜测字段
```
四条流程仍可分时、分别启动;上述顺序约束的是**同一业务键上的数据依赖**,不是强制一次连续跑完四条。
### 4.4 失败与重试
| 情形 | 处理 |
| --- | --- |
| 网络超时、服务不可用 | 本流程失败,可重拉接口;不启动 RPA |
| 返回成功但缺必填字段 | 本单失败,记录缺失字段名称;不启动 RPA |
| 部分明细行缺批准文号或销售价格 | 仅该行失败,其余行继续 |
| 对码回写失败 | 对码失败;合同、返利不得启动 |
| 客户未尽调 | 本合同跳过(不算接口失败),发邮件,继续下一客户 |
| 计划员对照不存在 | 返利失败,不使用默认计划员 |
重拉只覆盖失败数据,已成功入库的记录不得覆盖。
### 4.5 需订单中心确认的事项
1. 上表 A–H 的正式地址、鉴权方式、测试与生产环境。
2. 客户类型、账期天数、计划员与采购员对照、货源细单号是否已有维护功能与查询接口(映射表已提出,当前文档未提供接口说明)。
3. 尽调状态以订单中心为准,或以合同系统选择客户后的回显为准。
4. 对码功能数据表的写入接口是否已经开放;未开放则对码流程无法闭环。
未确认前,状态中心可用测试数据验证「调用 → 入库 → 状态中心可见」链路,不对生产订单中心与生产合同系统写入。
---
## 5. 流程说明
### 5.1 选品采购
调用接口 A、B,取得销售总单及明细后,按批准文号检索 1 号药城、药帮忙商品,依据第 6 节规则筛选。筛选结果形成待复核清单,由采购人员确认后再付款。机器人不执行支付操作。
已完成的商城登录与按文号查询能力纳入本流程复用,查询结果改为写入 RPA 库及状态中心,不再以桌面表格作为唯一产出。
### 5.2 订单对码
调用接口 C,将选品结果或 DocAgent 识别的手工单字段写入订单中心对码功能数据表;再调用接口 D,取得客户名称与货品编码。合同系统中的「客户名称」以接口 D 为准。
### 5.3 合同录入
调用接口 E、F 取得客户主数据与货品明细后,依据《合同返利系统字段映射》填写合同系统。录入方式分为系统自动生成、RPA 填写、无需填写三类;RPA 仅处理标注为「RPA」的字段。
客户未完成主体尽调时:向采购员发送邮件,本份合同跳过,继续处理后续合同,不中断整批作业。
客户类型为预付时:合同版本类型取「我司定制」,是否预付款为「是」,付款期限为 1 天。
客户类型为账期时:合同版本类型取「非格式合同」,是否预付款为「否」,付款期限取接口 E 返回的账期天数。
### 5.4 返利录入
调用接口 G、H 取得计划员对照与返利源数据后,按合同号查询采购协议,填写总单必填项并导入细单。提交失败转为人工处理,不循环强行提交。
---
## 6. 选品规则
规则依据《采购平台选品条件优先项》(2026-09-02)。下列第 1 至 4 项须同时满足,再比较仓库距离。销售价格以订单中心接口 B 为准。
1. **自营仓**:仅在已勾选的自营仓范围内选择。
2. **价格**:与销售价格比较,毛利不低于 5%。
3. **有效期**:大于等于 12 个月。
4. **库存**:库存数量不低于本单采购数量。
5. **仓库距离**:在满足以上条件后,按下列自营仓顺序优先选择距离较近的仓库:
1. 广东壹号药业有限公司
2. 昆山亿方药业有限公司
3. 湖北亿昊药业有限公司
4. 天津壹号药业有限公司
5. 重庆亿昊药业有限公司
6. 重庆壹号医药有限公司
7. 湖南壹号药业股份有限公司
8. 辽宁亿昊药业有限公司
9. 河南壹号医药有限公司
10. 甘肃壹号云医药有限责任公司企业店
11. 陕西壹号药业有限公司
现有商城查价程序实现了双平台登录与按批准文号取价,可作为选品流程的查询能力复用。该程序现行过滤逻辑与上述规则存在差异,上线口径以本节为准:
| 现有查价程序 | 本方案采用规则 |
| --- | --- |
| 按采购价格上下浮动 10% 过滤 | 相对销售价格,毛利不低于 5% |
| 未限定自营仓范围 | 仅在上述 11 家自营仓内选择 |
| 未校验库存是否覆盖采购数量 | 库存须大于等于采购数量 |
| 结果输出至本地表格 | 结果写入 RPA 库,并在状态中心展示 |
---
## 7. 合同与返利录入口径
字段来源、录入方式以《合同返利系统字段映射》(2026-09-11)为准。凡标注「从订单中心获取」的字段,取值路径见第 4 节,缺项不得提交。
合同、返利中标注为默认值的字段(如结算类型「收支双向」、付款方式「电汇」、税率 0.13、承付模式及参数等)按映射表执行,不在运行时另行解释。
---
## 8. 运行监控
状态中心提供以下可见信息:
- 当日四条流程的执行数量,以及成功、失败、等待人工处理的分布
- 单笔作业的当前环节与责任角色
- 订单中心各接口最近一次调用时间、成功或失败、缺失字段
- 选品候选、毛利是否达标、选用自营仓
- 合同关键字段(客户名称、金额、日期、尽调结果)
- 返利总单编号、细单导入结果
- 失败原因及是否允许按本流程重跑
接口数据写入 RPA 测试库,并标记处理状态(待处理 / 处理中 / 成功 / 失败 / 跳过)。状态中心同步展示,作为业务查询与问题定位的依据。
---
## 9. 本期范围
**本期包含**
- 四条流程的独立启动、定时启动与状态查询
- 订单中心接口 A–H 的调用、入库与失败停机
- 选品查询能力与优先项规则落地
- 合同、返利按映射表自动填写
**本期不包含**
- 机器人自动完成商城支付
- 对未尽调客户自动填写并提交合同
- 将四条流程拆成互不共享状态的独立工具
- 在订单中心正式接口就绪前对生产系统写入
流程图未包含 M5 采购订单。若现场仍需 M5 作业,另行立项,不纳入本四条流程。
---
## 10. 待确认事项
1. 四条流程采用统一状态中心入口、分别启动;允许只运行其中一条。
2. 选品正式规则以第 6 节为准;现有查价程序仅复用登录与查询能力。
3. 订单中心按第 4.5 节提供接口说明书,并确认客户类型、账期天数、计划员对照、货源细单号、对码回写、尽调来源。未齐备前,合同与返利不得按缺省猜测值提交。
上述事项确认后,按《福建洁达四流程技术实现说明》实施。实施顺序为:先完成订单中心取数入库与状态中心可见,再接入选品、合同与返利。

View File

@ -0,0 +1,318 @@
-- RPA 库暂存表 DDL(Oracle 11.2)
-- 依据:/Users/yuuuu/Downloads/RPA与订单中心接口设计_20260922.xlsx(2026-09-30 16:24)
-- 四张「数据表」sheet。表1 已去掉 oms_orderno;表1 fid 为 number(19);
-- 表2 push_time/create_time/update_time 为 date(create_time 写作 dateNOT NULL,按 date NOT NULL)。
-- 库版本:11.2.0.4,无 IDENTITY;表1/2/4 自增用 SEQUENCE + BEFORE INSERT 触发器(fid 为空才取号,等同 BY DEFAULT)。
-- 标识符不超过 30 字节。
-- 范围:仅本方 RPA 库四张暂存表。不是订单中心对码功能表。本方不建对码引擎。
-- oms_id 一律 number(20)。
--
-- 待确认(Excel 现稿保留,未删列):
-- 表1 oms_id:需确认字段属性是否与订单中心一致。
-- 表2 bar_code:确认同一条形码是否可出现在多个订单;普通索引已建,不作为主键。
-- 表3 fid:确认重录协议时是否生成新的 fid。
-- 表3 purchase_tax_rate:确认税率是百分数还是小数比例;Excel 备注为 0.13 表示 13%。
-- 表3 contract_entry_status:确认是否保留 need_contract 旧字段名;现稿用本列名。
--
-- 主键 / 唯一(Excel 未写主键时按任务口径):
-- 表1 fid 自增主键;oms_id 幂等唯一(因此补 NOT NULL)。
-- 表2、表4 fid 本方序列主键。
-- 表3 fid 订单中心来源唯一序号,作主键。
-- ---------------------------------------------------------------------------
-- 1. 销售商品对码数据(Excel 名;实为选品收件箱)
-- ---------------------------------------------------------------------------
CREATE TABLE rpa_sales_goods_mapping_data (
oms_id NUMBER(20) NOT NULL,
bar_code VARCHAR2(40),
order_date DATE,
sale_price NUMBER(16,4),
sale_qty NUMBER(10),
plan_purchase_qty NUMBER(10),
goods_id VARCHAR2(20),
goods_name VARCHAR2(200),
goods_spec VARCHAR2(100),
manufacturer VARCHAR2(100),
approval_no VARCHAR2(100),
unit VARCHAR2(20),
customer_id VARCHAR2(20),
customer_name VARCHAR2(200),
create_time DATE,
create_user_id NUMBER(10),
create_user_name VARCHAR2(20),
update_time DATE,
update_user_id NUMBER(10),
update_user_name VARCHAR2(20),
deal_flag NUMBER(2) DEFAULT 0 NOT NULL,
deal_result VARCHAR2(200),
deal_time DATE,
deal_user_id NUMBER(10),
deal_user_name VARCHAR2(20),
fid NUMBER(19) NOT NULL,
org_code VARCHAR2(10),
goods_mdm_code VARCHAR2(100),
customer_mdm_code VARCHAR2(100),
CONSTRAINT pk_rpa_sgmd PRIMARY KEY (fid),
CONSTRAINT uk_rpa_sgmd_oms_id UNIQUE (oms_id)
);
CREATE SEQUENCE seq_rpa_sgmd_fid
START WITH 1
INCREMENT BY 1
NOMAXVALUE
NOCYCLE
NOCACHE;
CREATE OR REPLACE TRIGGER trg_rpa_sgmd_bi
BEFORE INSERT ON rpa_sales_goods_mapping_data
FOR EACH ROW
WHEN (new.fid IS NULL)
BEGIN
SELECT seq_rpa_sgmd_fid.NEXTVAL INTO :NEW.fid FROM dual;
END;
/
COMMENT ON TABLE rpa_sales_goods_mapping_data IS '销售商品对码数据(选品收件箱)。订单中心推入,按 oms_id 幂等。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.oms_id IS '订单中心序号。已存在则不入库、不重跑。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.bar_code IS '条形码。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.order_date IS '订单日期。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.sale_price IS '销售单价。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.sale_qty IS '销售数量。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.plan_purchase_qty IS '计划采购量。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.goods_id IS '货品ID。入站时已有,本方不做对码。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.goods_name IS '品名。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.goods_spec IS '货品规格。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.manufacturer IS '厂家。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.approval_no IS '国药准字/注册证号。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.unit IS '单位。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.customer_id IS '客户ID。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.customer_name IS '客户名称。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.create_time IS '创建时间。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.create_user_id IS '创建人ID。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.create_user_name IS '创建人名称。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.update_time IS '更新时间。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.update_user_id IS '更新人ID。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.update_user_name IS '更新人名称。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.deal_flag IS '处理标志。0-待处理、1-处理成功、2-处理失败、3-处理中。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.deal_result IS '处理结果。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.deal_time IS '处理时间。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.deal_user_id IS '处理人ID。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.deal_user_name IS '处理人名称。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.fid IS '序号,自增序列。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.org_code IS '货主编码。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.goods_mdm_code IS '货品MDM码。';
COMMENT ON COLUMN rpa_sales_goods_mapping_data.customer_mdm_code IS '客户MDM码。';
-- ---------------------------------------------------------------------------
-- 2. 加购及结算商品数据(加购/支付回传缓冲)
-- ---------------------------------------------------------------------------
CREATE TABLE rpa_cart_settlement_goods_data (
fid NUMBER(19,0) NOT NULL,
operate_type NUMBER(2,0) NOT NULL,
oms_id NUMBER(20) NOT NULL,
platform_type NUMBER(4,0) NOT NULL,
supplier_name VARCHAR2(200 CHAR) NOT NULL,
bar_code VARCHAR2(40 CHAR) NOT NULL,
purchase_price NUMBER(16,4) NOT NULL,
purchase_qty NUMBER(10,0) NOT NULL,
purchase_total NUMBER(16,4) NOT NULL,
status NUMBER(2,0),
fail_reason VARCHAR2(500 CHAR),
pay_order_no VARCHAR2(40 CHAR),
pay_order_dtl_id VARCHAR2(40 CHAR),
push_status NUMBER(1,0) DEFAULT 0 NOT NULL,
push_result VARCHAR2(500 CHAR),
push_time DATE,
create_time DATE NOT NULL,
update_time DATE,
create_user_id NUMBER(19,0),
create_user_name VARCHAR2(100 CHAR),
update_user_id NUMBER(19,0),
update_user_name VARCHAR2(100 CHAR),
org_code VARCHAR2(10),
multi_platform_flag NUMBER(1,0) DEFAULT 0 NOT NULL,
CONSTRAINT pk_rpa_csgd PRIMARY KEY (fid)
);
CREATE SEQUENCE seq_rpa_csgd_fid
START WITH 1
INCREMENT BY 1
NOMAXVALUE
NOCYCLE
NOCACHE;
CREATE OR REPLACE TRIGGER trg_rpa_csgd_bi
BEFORE INSERT ON rpa_cart_settlement_goods_data
FOR EACH ROW
WHEN (new.fid IS NULL)
BEGIN
SELECT seq_rpa_csgd_fid.NEXTVAL INTO :NEW.fid FROM dual;
END;
/
CREATE INDEX idx_rpa_csgd_bar_code
ON rpa_cart_settlement_goods_data (bar_code);
COMMENT ON TABLE rpa_cart_settlement_goods_data IS '加购及结算商品数据。RPA 回传缓冲;同一 oms_id 多平台多行。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.fid IS 'RPA记录技术主键;接口推送时作为业务回执关联键。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.operate_type IS '操作类型。1-加购,2-支付。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.oms_id IS '订单中心单据编号。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.platform_type IS '平台类型。48 药帮忙,66 1药城。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.supplier_name IS '供应商名称。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.bar_code IS '条形码。保留前导零,不作为单独主键。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.purchase_price IS '采购单价。金额精度4位小数。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.purchase_qty IS '采购数量。大于0。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.purchase_total IS '采购总价。金额字段统一 NUMBER(16,4)。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.status IS '整单采购状态。仅加购:1-成功,2-失败。支付填空。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.fail_reason IS '整单采购失败原因。status=2时填写。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.pay_order_no IS '支付订单号。支付场景填写。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.pay_order_dtl_id IS '支付订单明细ID。支付场景填写。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.push_status IS '推送状态。0-未推送,1-推送成功,2-推送失败,3-推送中。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.push_result IS '推送结果。保存订单中心回执或失败原因。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.push_time IS '推送时间。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.create_time IS '创建时间。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.update_time IS '更新时间。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.create_user_id IS '创建人ID。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.create_user_name IS '创建人名称。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.update_user_id IS '更新人ID。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.update_user_name IS '更新人名称。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.org_code IS '货主编码。';
COMMENT ON COLUMN rpa_cart_settlement_goods_data.multi_platform_flag IS '多平台满足标志。同一 OMS ID 多行时为1,须同报文返回。';
-- ---------------------------------------------------------------------------
-- 3. 采购合同数据
-- ---------------------------------------------------------------------------
CREATE TABLE rpa_purchase_contract_data (
fid NUMBER(19,0) NOT NULL,
oms_id NUMBER(20) NOT NULL,
supplier_id VARCHAR2(20 CHAR) NOT NULL,
supplier_name VARCHAR2(200 CHAR) NOT NULL,
contract_sign_date DATE NOT NULL,
contract_start_date DATE NOT NULL,
contract_end_date DATE NOT NULL,
payment_term NUMBER(5,0) NOT NULL,
goods_id VARCHAR2(20 CHAR) NOT NULL,
goods_name VARCHAR2(200 CHAR) NOT NULL,
reg_no VARCHAR2(100 CHAR) NOT NULL,
spec VARCHAR2(100 CHAR) NOT NULL,
unit VARCHAR2(20 CHAR) NOT NULL,
qty NUMBER(16,4) NOT NULL,
purchase_tax_rate NUMBER(8,4) NOT NULL,
unit_price NUMBER(16,4) NOT NULL,
goods_source_id VARCHAR2(20 CHAR) NOT NULL,
purchaser_id VARCHAR2(20 CHAR) NOT NULL,
purchaser_name VARCHAR2(100 CHAR) NOT NULL,
planner_id VARCHAR2(20 CHAR) NOT NULL,
planner_name VARCHAR2(100 CHAR) NOT NULL,
contract_entry_status NUMBER(1,0),
contract_no VARCHAR2(40 CHAR),
process_status NUMBER(1,0) DEFAULT 0 NOT NULL,
process_result VARCHAR2(1000 CHAR),
process_time TIMESTAMP(6),
create_time TIMESTAMP(6) NOT NULL,
update_time TIMESTAMP(6),
org_code VARCHAR2(10),
customer_type NUMBER(2,0) NOT NULL,
goods_mdm_code VARCHAR2(100),
supplier_mdm_code VARCHAR2(100),
CONSTRAINT pk_rpa_pcd PRIMARY KEY (fid)
);
COMMENT ON TABLE rpa_purchase_contract_data IS '采购合同数据。订单中心行级下发;按来源 fid 幂等。';
COMMENT ON COLUMN rpa_purchase_contract_data.fid IS '订单中心来源稳定唯一序号。';
COMMENT ON COLUMN rpa_purchase_contract_data.oms_id IS '订单中心单据编号。';
COMMENT ON COLUMN rpa_purchase_contract_data.supplier_id IS '供应商ID。';
COMMENT ON COLUMN rpa_purchase_contract_data.supplier_name IS '供应商名称。';
COMMENT ON COLUMN rpa_purchase_contract_data.contract_sign_date IS '合同签订日期。';
COMMENT ON COLUMN rpa_purchase_contract_data.contract_start_date IS '合同开始日期。';
COMMENT ON COLUMN rpa_purchase_contract_data.contract_end_date IS '合同结束日期。';
COMMENT ON COLUMN rpa_purchase_contract_data.payment_term IS '付款期限(账期),单位:天。';
COMMENT ON COLUMN rpa_purchase_contract_data.goods_id IS '货品ID。';
COMMENT ON COLUMN rpa_purchase_contract_data.goods_name IS '货品名称。';
COMMENT ON COLUMN rpa_purchase_contract_data.reg_no IS '注册证号。';
COMMENT ON COLUMN rpa_purchase_contract_data.spec IS '规格。';
COMMENT ON COLUMN rpa_purchase_contract_data.unit IS '单位。';
COMMENT ON COLUMN rpa_purchase_contract_data.qty IS '数量。支持非整数采购数量。';
COMMENT ON COLUMN rpa_purchase_contract_data.purchase_tax_rate IS '进货税率。0.13 表示 13%。';
COMMENT ON COLUMN rpa_purchase_contract_data.unit_price IS '采购单价。';
COMMENT ON COLUMN rpa_purchase_contract_data.goods_source_id IS '货源细单ID。';
COMMENT ON COLUMN rpa_purchase_contract_data.purchaser_id IS '采购员ID。';
COMMENT ON COLUMN rpa_purchase_contract_data.purchaser_name IS '采购员。';
COMMENT ON COLUMN rpa_purchase_contract_data.planner_id IS '计划员ID。';
COMMENT ON COLUMN rpa_purchase_contract_data.planner_name IS '计划员。';
COMMENT ON COLUMN rpa_purchase_contract_data.contract_entry_status IS '录入合同状态。null-首次下发,0-失败,1-成功。';
COMMENT ON COLUMN rpa_purchase_contract_data.contract_no IS '采购合同号。失败重录协议时填写。';
COMMENT ON COLUMN rpa_purchase_contract_data.process_status IS '处理状态。0-待处理,1-成功,2-失败,3-处理中。';
COMMENT ON COLUMN rpa_purchase_contract_data.process_result IS '处理结果。保存RPA处理结果或失败原因。';
COMMENT ON COLUMN rpa_purchase_contract_data.process_time IS '处理时间。';
COMMENT ON COLUMN rpa_purchase_contract_data.create_time IS '创建时间。';
COMMENT ON COLUMN rpa_purchase_contract_data.update_time IS '更新时间。';
COMMENT ON COLUMN rpa_purchase_contract_data.org_code IS '货主编码。';
COMMENT ON COLUMN rpa_purchase_contract_data.customer_type IS '客户类型。口径本期固定 1 账期。';
COMMENT ON COLUMN rpa_purchase_contract_data.goods_mdm_code IS '货品MDM码。';
COMMENT ON COLUMN rpa_purchase_contract_data.supplier_mdm_code IS '供应商MDM码。';
-- ---------------------------------------------------------------------------
-- 4. 合同协议录入结果
-- ---------------------------------------------------------------------------
CREATE TABLE rpa_contract_agreement_result (
fid NUMBER(19,0) NOT NULL,
oms_id NUMBER(20) NOT NULL,
action_code NUMBER(2,0) NOT NULL,
contract_status NUMBER(2,0),
contract_fail_reason VARCHAR2(500 CHAR),
contract_no VARCHAR2(40 CHAR),
agreement_status NUMBER(2,0),
agreement_fail_reason VARCHAR2(500 CHAR),
agreement_id VARCHAR2(40 CHAR),
agreement_dtl_id VARCHAR2(40 CHAR),
push_status NUMBER(1,0) DEFAULT 0 NOT NULL,
push_result VARCHAR2(500 CHAR),
push_time TIMESTAMP(6),
create_time TIMESTAMP(6) NOT NULL,
update_time TIMESTAMP(6),
create_user_id NUMBER(19,0),
create_user_name VARCHAR2(100 CHAR),
update_user_id NUMBER(19,0),
update_user_name VARCHAR2(100 CHAR),
org_code VARCHAR2(10),
CONSTRAINT pk_rpa_car PRIMARY KEY (fid)
);
CREATE SEQUENCE seq_rpa_car_fid
START WITH 1
INCREMENT BY 1
NOMAXVALUE
NOCYCLE
NOCACHE;
CREATE OR REPLACE TRIGGER trg_rpa_car_bi
BEFORE INSERT ON rpa_contract_agreement_result
FOR EACH ROW
WHEN (new.fid IS NULL)
BEGIN
SELECT seq_rpa_car_fid.NEXTVAL INTO :NEW.fid FROM dual;
END;
/
COMMENT ON TABLE rpa_contract_agreement_result IS '合同协议录入结果。本方回写缓冲。';
COMMENT ON COLUMN rpa_contract_agreement_result.fid IS 'RPA记录技术主键;接口推送时作为回执关联键。';
COMMENT ON COLUMN rpa_contract_agreement_result.oms_id IS '订单中心单据编号。';
COMMENT ON COLUMN rpa_contract_agreement_result.action_code IS '回传动作。1-合同,2-返利协议。';
COMMENT ON COLUMN rpa_contract_agreement_result.contract_status IS '合同状态。1-起草,2-成功,3-失败。';
COMMENT ON COLUMN rpa_contract_agreement_result.contract_fail_reason IS '合同录入失败原因。';
COMMENT ON COLUMN rpa_contract_agreement_result.contract_no IS '合同编号。';
COMMENT ON COLUMN rpa_contract_agreement_result.agreement_status IS '返利协议状态。1-成功,2-失败。';
COMMENT ON COLUMN rpa_contract_agreement_result.agreement_fail_reason IS '协议录入失败原因。';
COMMENT ON COLUMN rpa_contract_agreement_result.agreement_id IS '采购协议总单ID。';
COMMENT ON COLUMN rpa_contract_agreement_result.agreement_dtl_id IS '采购协议细单ID。';
COMMENT ON COLUMN rpa_contract_agreement_result.push_status IS '推送状态。0-未推送,1-推送成功,2-推送失败,3-推送中。';
COMMENT ON COLUMN rpa_contract_agreement_result.push_result IS '推送结果。';
COMMENT ON COLUMN rpa_contract_agreement_result.push_time IS '推送时间。';
COMMENT ON COLUMN rpa_contract_agreement_result.create_time IS '创建时间。';
COMMENT ON COLUMN rpa_contract_agreement_result.update_time IS '更新时间。';
COMMENT ON COLUMN rpa_contract_agreement_result.create_user_id IS '创建人ID。';
COMMENT ON COLUMN rpa_contract_agreement_result.create_user_name IS '创建人名称。';
COMMENT ON COLUMN rpa_contract_agreement_result.update_user_id IS '更新人ID。';
COMMENT ON COLUMN rpa_contract_agreement_result.update_user_name IS '更新人名称。';
COMMENT ON COLUMN rpa_contract_agreement_result.org_code IS '货主编码。';

169
docs/ddl/列对照.md Normal file
View File

@ -0,0 +1,169 @@
# 四张暂存表 ↔ Excel 列对照
依据:`/Users/yuuuu/Downloads/RPA与订单中心接口设计_20260922.xlsx`(2026-09-30 16:24)四张「数据表」sheet。
DDL:`docs/ddl/rpa_oms_staging_tables.sql`(Oracle 11.2)。
Excel 无表英文名;表名按中文意译,未用拼音。
库是 11.2.0.4:表1/2/4 自增不用 IDENTITY,改 SEQUENCE + 触发器;约束/索引名压到 30 字节内。列集合未改。
约束(非列):表1 `oms_id` 唯一、`fid` 主键;表2/4 `fid` 主键;表3 `fid` 主键(来源唯一);表2 `bar_code` 普通索引。
## 总览
| Excel 数据表 | DDL 表名 | 列数 | 缺列 | 多列 |
| --- | --- | ---: | --- | --- |
| 销售商品对码数据 | `rpa_sales_goods_mapping_data` | 29 | 无 | 无 |
| 加购及结算商品数据 | `rpa_cart_settlement_goods_data` | 24 | 无 | 无 |
| 采购合同数据 | `rpa_purchase_contract_data` | 32 | 无 | 无 |
| 合同协议录入结果 | `rpa_contract_agreement_result` | 20 | 无 | 无 |
口径点名且 Excel 已有的列,按所在表落地:`org_code`(四张都有);MDM 在表1(货品/客户)、表3(货品/供应商);`customer_type` 仅表3;`multi_platform_flag` 仅表2;`push_status` 在表2、表4。
---
## 1. 销售商品对码数据
Excel 待确认:`oms_id` 属性是否与订单中心一致。
| Excel 序号 | 中文名 | 英文名 | Excel 属性 | DDL | 差异 |
| ---: | --- | --- | --- | --- | --- |
| 1 | 订单中心序号 | oms_id | number(20) | NUMBER(20) NOT NULL | 幂等唯一,补 NOT NULL + UNIQUE |
| 2 | 条形码 | bar_code | varchar2(40) | VARCHAR2(40) | 同 |
| 3 | 订单日期 | order_date | date | DATE | 同 |
| 4 | 销售单价 | sale_price | number(16,4) | NUMBER(16,4) | 同 |
| 5 | 销售数量 | sale_qty | number(10) | NUMBER(10) | 同 |
| 6 | 计划采购量 | plan_purchase_qty | number(10) | NUMBER(10) | 同 |
| 7 | 货品ID | goods_id | varchar2(20) | VARCHAR2(20) | 同 |
| 8 | 品名 | goods_name | varchar2(200) | VARCHAR2(200) | 同 |
| 9 | 货品规格 | goods_spec | varchar2(100) | VARCHAR2(100) | 同 |
| 10 | 厂家 | manufacturer | varchar2(100) | VARCHAR2(100) | 同 |
| 11 | 国药准字/注册证号 | approval_no | varchar2(100) | VARCHAR2(100) | 同 |
| 12 | 单位 | unit | varchar2(20) | VARCHAR2(20) | 同 |
| 13 | 客户ID | customer_id | varchar2(20) | VARCHAR2(20) | 同 |
| 14 | 客户名称 | customer_name | varchar2(200) | VARCHAR2(200) | 同 |
| 15 | 创建时间 | create_time | date | DATE | 同 |
| 16 | 创建人ID | create_user_id | number(10) | NUMBER(10) | 同 |
| 17 | 创建人名称 | create_user_name | varchar2(20) | VARCHAR2(20) | 同 |
| 18 | 更新时间 | update_time | date | DATE | 同 |
| 19 | 更新人ID | update_user_id | number(10) | NUMBER(10) | 同 |
| 20 | 更新人名称 | update_user_name | varchar2(20) | VARCHAR2(20) | 同 |
| 21 | 处理标志 | deal_flag | number(2) DEFAULT 0 NOT NULL | NUMBER(2) DEFAULT 0 NOT NULL | 同 |
| 22 | 处理结果 | deal_result | varchar2(200) | VARCHAR2(200) | 同 |
| 23 | 处理时间 | deal_time | date | DATE | 同 |
| 24 | 处理人ID | deal_user_id | number(10) | NUMBER(10) | 同 |
| 25 | 处理人名称 | deal_user_name | varchar2(20) | VARCHAR2(20) | 同 |
| 26 | 序号 | fid | number(19) 自增序列 | NUMBER(19) 主键 + SEQUENCE/触发器 | 11g 无 IDENTITY |
| 27 | 货主编码 | org_code | varchar2(10) | VARCHAR2(10) | 同 |
| 28 | 货品MDM码 | goods_mdm_code | varchar2(100) | VARCHAR2(100) | 同 |
| 29 | 客户MDM码 | customer_mdm_code | varchar2(100) | VARCHAR2(100) | 同 |
相对接口 sheet:数据表多审计字段、`deal_*`、`fid`。接口业务字段均在表中。`oms_orderno` 已删。
---
## 2. 加购及结算商品数据
Excel 待确认:同一条形码是否可出现在多个订单。平台字典已写入备注:48 药帮忙,66 1药城。
| Excel 序号 | 中文名 | 英文名 | Excel 属性 | DDL | 差异 |
| ---: | --- | --- | --- | --- | --- |
| 1 | 序号 | fid | number(19,0) IDENTITY | NUMBER(19,0) 主键 + SEQUENCE/触发器 | 11g 无 IDENTITY,主键补 NOT NULL |
| 2 | 操作类型 | operate_type | number(2,0) NOT NULL | 同 | 无 |
| 3 | 订单中心单据编号 | oms_id | number(20) NOT NULL | 同 | 无 |
| 4 | 平台类型 | platform_type | number(4,0) NOT NULL | 同 | 无 |
| 5 | 供应商名称 | supplier_name | varchar2(200 char) NOT NULL | 同 | 无 |
| 6 | 条形码 | bar_code | varchar2(40 char) NOT NULL | 同;普通索引 | 无 |
| 7 | 采购单价 | purchase_price | number(16,4) NOT NULL | 同 | 无 |
| 8 | 采购数量 | purchase_qty | number(10,0) NOT NULL | 同 | 无 |
| 9 | 采购总价 | purchase_total | number(16,4) NOT NULL | 同 | 无 |
| 10 | 整单采购状态 | status | number(2,0) | 同 | 无 |
| 11 | 整单采购失败原因 | fail_reason | varchar2(500 char) | 同 | 无 |
| 12 | 支付订单号 | pay_order_no | varchar2(40 char) | 同 | 无 |
| 13 | 支付订单明细ID | pay_order_dtl_id | varchar2(40 char) | 同 | 无 |
| 14 | 推送状态 | push_status | number(1,0) DEFAULT 0 NOT NULL | 同 | 无 |
| 15 | 推送结果 | push_result | varchar2(500 char) | 同 | 无 |
| 16 | 推送时间 | push_time | date | DATE | 无 |
| 17 | 创建时间 | create_time | date NOT NULL | DATE NOT NULL | Excel 写作 dateNOT NULL |
| 18 | 更新时间 | update_time | date | DATE | 无 |
| 19 | 创建人ID | create_user_id | number(19,0) | 同 | 无 |
| 20 | 创建人名称 | create_user_name | varchar2(100 char) | 同 | 无 |
| 21 | 更新人ID | update_user_id | number(19,0) | 同 | 无 |
| 22 | 更新人名称 | update_user_name | varchar2(100 char) | 同 | 无 |
| 23 | 货主编码 | org_code | varchar2(10) | 同 | 无 |
| 24 | 多平台满足标志 | multi_platform_flag | number(1,0) DEFAULT 0 NOT NULL | 同 | 无 |
相对接口 sheet:数据表多 `push_*`、审计、`multi_platform_flag`。
---
## 3. 采购合同数据
Excel 待确认:重录协议是否新 `fid`;税率百分数还是小数;是否保留 `need_contract` 旧名。
| Excel 序号 | 中文名 | 英文名 | Excel 属性 | DDL | 差异 |
| ---: | --- | --- | --- | --- | --- |
| 1 | 序号 | fid | number(19,0) NOT NULL | 同,主键(来源唯一) | 无 |
| 2 | 订单中心单据编号 | oms_id | number(20) NOT NULL | 同 | 无 |
| 3 | 供应商ID | supplier_id | varchar2(20 char) NOT NULL | 同 | 无 |
| 4 | 供应商名称 | supplier_name | varchar2(200 char) NOT NULL | 同 | 无 |
| 5 | 合同签订日期 | contract_sign_date | date NOT NULL | 同 | 无 |
| 6 | 合同开始日期 | contract_start_date | date NOT NULL | 同 | 无 |
| 7 | 合同结束日期 | contract_end_date | date NOT NULL | 同 | 无 |
| 8 | 付款期限(账期) | payment_term | number(5,0) NOT NULL | 同 | 无 |
| 9 | 货品ID | goods_id | varchar2(20 char) NOT NULL | 同 | 无 |
| 10 | 货品名称 | goods_name | varchar2(200 char) NOT NULL | 同 | 无 |
| 11 | 注册证号 | reg_no | varchar2(100 char) NOT NULL | 同 | 无 |
| 12 | 规格 | spec | varchar2(100 char) NOT NULL | 同 | 无 |
| 13 | 单位 | unit | varchar2(20 char) NOT NULL | 同 | 无 |
| 14 | 数量 | qty | number(16,4) NOT NULL | 同 | 无 |
| 15 | 进货税率 | purchase_tax_rate | number(8,4) NOT NULL | 同 | 无 |
| 16 | 采购单价 | unit_price | number(16,4) NOT NULL | 同 | 无 |
| 17 | 货源细单ID | goods_source_id | varchar2(20 char) NOT NULL | 同 | 无 |
| 18 | 采购员ID | purchaser_id | varchar2(20 char) NOT NULL | 同 | 无 |
| 19 | 采购员 | purchaser_name | varchar2(100 char) NOT NULL | 同 | 无 |
| 20 | 计划员ID | planner_id | varchar2(20 char) NOT NULL | 同 | 无 |
| 21 | 计划员 | planner_name | varchar2(100 char) NOT NULL | 同 | 无 |
| 22 | 录入合同状态 | contract_entry_status | number(1,0) | 同 | 无 |
| 23 | 采购合同号 | contract_no | varchar2(40 char) | 同 | 无 |
| 24 | 处理状态 | process_status | number(1,0) DEFAULT 0 NOT NULL | 同 | 无 |
| 25 | 处理结果 | process_result | varchar2(1000 char) | 同 | 无 |
| 26 | 处理时间 | process_time | timestamp(6) | 同 | 无 |
| 27 | 创建时间 | create_time | timestamp(6) NOT NULL | 同 | 无 |
| 28 | 更新时间 | update_time | timestamp(6) | 同 | 无 |
| 29 | 货主编码 | org_code | varchar2(10) | 同 | 无 |
| 30 | 客户类型 | customer_type | number(2,0) NOT NULL | 同 | 无 |
| 31 | 货品MDM码 | goods_mdm_code | varchar2(100) | 同 | 无 |
| 32 | 供应商MDM码 | supplier_mdm_code | varchar2(100) | 同 | 无 |
相对接口 sheet:数据表多 `process_*`、`create_time`、`update_time`。未建 `need_contract`。
---
## 4. 合同协议录入结果
无 Excel「待确认」列。
| Excel 序号 | 中文名 | 英文名 | Excel 属性 | DDL | 差异 |
| ---: | --- | --- | --- | --- | --- |
| 1 | 序号 | fid | number(19,0) IDENTITY | NUMBER(19,0) 主键 + SEQUENCE/触发器 | 11g 无 IDENTITY,主键补 NOT NULL |
| 2 | 订单中心单据编号 | oms_id | number(20) NOT NULL | 同 | 无 |
| 3 | 回传动作 | action_code | number(2,0) NOT NULL | 同 | 无 |
| 4 | 合同状态 | contract_status | number(2,0) | 同 | 无 |
| 5 | 合同录入失败原因 | contract_fail_reason | varchar2(500 char) | 同 | 无 |
| 6 | 合同编号 | contract_no | varchar2(40 char) | 同 | 无 |
| 7 | 返利协议状态 | agreement_status | number(2,0) | 同 | 无 |
| 8 | 协议录入失败原因 | agreement_fail_reason | varchar2(500 char) | 同 | 无 |
| 9 | 采购协议总单ID | agreement_id | varchar2(40 char) | 同 | 无 |
| 10 | 采购协议细单ID | agreement_dtl_id | varchar2(40 char) | 同 | 无 |
| 11 | 推送状态 | push_status | number(1,0) DEFAULT 0 NOT NULL | 同 | 无 |
| 12 | 推送结果 | push_result | varchar2(500 char) | 同 | 无 |
| 13 | 推送时间 | push_time | timestamp(6) | 同 | 无 |
| 14 | 创建时间 | create_time | timestamp(6) NOT NULL | 同 | 无 |
| 15 | 更新时间 | update_time | timestamp(6) | 同 | 无 |
| 16 | 创建人ID | create_user_id | number(19,0) | 同 | 无 |
| 17 | 创建人名称 | create_user_name | varchar2(100 char) | 同 | 无 |
| 18 | 更新人ID | update_user_id | number(19,0) | 同 | 无 |
| 19 | 更新人名称 | update_user_name | varchar2(100 char) | 同 | 无 |
| 20 | 货主编码 | org_code | varchar2(10) | 同 | 无 |
相对接口 sheet:数据表多 `push_*`、审计字段。

View File

@ -0,0 +1,268 @@
# 仓库布局:四接口一服务 + 四个英文作业包
| 文档属性 | 内容 |
| --- | --- |
| 日期 | 2026-10-02 |
| 状态 | 已锁定。随后 `rpa_yisaiqi/` 已删,页面脚本迁入 `rpa_contract/` |
| 范围 | 根目录命名与分家:`选品流程/` 整包迁到 `rpa_selection/`;占位 `rpa_cart/`、`rpa_contract/`、`rpa_rebate/`;`资料/` 并入 `docs/`。路径与文档引用。 |
| 替代 | 根 `README.md` 代码入口表;`资料/README.md`(并入后成为 `docs/README.md`) |
| 不变 | 四张表、四个口的作业语义、接口 1 行为、选品规则与拆量、`rpa_yisaiqi` 页面步骤 |
四个 HTTP 口不是四个服务。四条作业不是一个包。代码目录名全英文。
---
## 1. 要做成什么
根目录变成:
```
AutoRpa/
README.md
rpa_oms/ # 四个 HTTP 口,一个 FastAPI
rpa_selection/ # 选品班(现 选品流程/ 整包搬来)
rpa_cart/ # 加购 + 支付回传(占位)
rpa_contract/ # 合同班(占位)
rpa_rebate/ # 返利班(占位)
docs/ # 现行口径 + 归档(现 资料/ 并入)
rpa_yisaiqi/ # 过渡:合同班能跑之后整包删除
```
完成标准:
1. 仓库根上不再有 `选品流程/`、`资料/`。
2. `cd rpa_selection && uv run pytest -v` 与搬前同等通过。
3. `cd rpa_oms && uv run pytest -v` 通过。
4. 根 README、`docs/README.md`、`.gitignore`、现行口径里的路径指向新目录。
5. `rpa_cart/`、`rpa_contract/`、`rpa_rebate/` 各有 README 和空 `pyproject.toml`,能看出职责,没有可运行作业代码。
---
## 2. 不做什么
- 不加购、不支付回传、不录合同、不录返利
- 不实现接口 2 / 3 / 4,不在 `rpa_oms` 里建空路由模块
- 不改选品规则、班次判定、表结构、接口 1 报文
- 不整理、不改名、不删除 `rpa_yisaiqi/`(本轮只改指向它的现行文档路径,若文档从 `资料/` 搬走)
- 不把作业改成 `app/` 包;作业保持扁平脚本(`main.py` + `job.py`)
- 不把四个作业收进 `rpa_jobs/`
- 不改 `docs/superpowers/` 里历史 spec / plan 的死链(视为归档)
- 不提交 `state_center/` 等本轮无关的已删文件(那些删除不在本 spec 范围)
---
## 3. 已锁定决策
| 项 | 决定 |
| --- | --- |
| 四个口 | 继续住在 `rpa_oms/`。入站 1、3 在 `app/inbound/`,出站 2、4 做到时再加 `app/outbound/`。本轮只把这张地图写进 `rpa_oms/README.md` |
| 四个作业 | 根上并列:`rpa_selection`、`rpa_cart`、`rpa_contract`、`rpa_rebate` |
| `rpa_cart` | 加购(接口 2 `operateType=1`)和支付回传(`operateType=2`)同一个包。两个班次,共享表 2 |
| 选品 | `git mv 选品流程 rpa_selection`。业务代码、扁平 import、`pythonpath = ["."]` 不动 |
| 占位包 | 只有 `README.md` + `pyproject.toml`。无 `main.py`、无 `tests/` |
| 中文 | **代码目录**全英文。文档文件名可以中文。`docs/` 是目录名,里面的 `.md` 文件名不改 |
| `rpa_yisaiqi` | 过渡资产。合同页面步骤仍以它为准。等 `rpa_contract` 能跑再整包删 |
| `docs/superpowers/` | 原地不动,归档 |
---
## 4. `rpa_oms`:四个口的地图
本轮不改 Python 文件(README 除外)。运行时仍只开放接口 1。
| 口 | 方向 | 底表 | 代码落点(做到再写) |
| --- | --- | --- | --- |
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | 已有 `app/inbound/sales_goods.py`,`POST /api/oms/v1/sales-goods` |
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 未来 `app/outbound/`,由 `rpa_cart` 班次调用,本服务也可提供 push 客户端 |
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | 未来 `app/inbound/`,复用接口 1 信封/鉴权 |
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 未来 `app/outbound/`,由合同/返利班次调用 |
`rpa_oms/README.md` 用上表替换「本期只开放接口 1」的单一叙述,并写明:HTTP 收件不同步跑作业。
---
## 5. 四个作业包
作业语义仍以 `docs/RPA订单中心接口开发口径_20260930.md`(搬后路径)为准。包只是落点。
### 5.1 `rpa_selection/`(整包搬)
来源:`选品流程/`。
| 动作 | 细节 |
| --- | --- |
| 移动 | `git mv 选品流程 rpa_selection`。未跟踪残留(`.explore/`、`.magic/`、`output/结果` 下 html/png、本地 `.venv` / `.env`)不会跟着走,目录会留下。把需要的本地文件普通 `mv` 进 `rpa_selection/`,探测垃圾直接删,然后去掉空的 `选品流程/`。不要 `git add` `.explore` / `.magic` / 截图 |
| `pyproject.toml` | `name = "rpa-selection"`;description 改成带新目录名。依赖、`requires-python`、pytest 配置不动 |
| `README.md` | 所有 `cd 选品流程`、`` `选品流程/` `` 改成 `rpa_selection`;口径路径改 `docs/...` |
| 代码 | 不改 import、不改 `PROJECT_ROOT = Path(__file__).resolve().parent` |
| `output/结果` | 不改名(gitignored 产物)。`.gitignore` 路径跟着父目录改 |
搬完后包内仍是扁平脚本:`main.py`、`job.py`、`persist.py`、`repo.py`、`rules.py`、`pipeline.py`、`search_*.py`、`auth_login.py`、`tests/`。
### 5.2 占位包共同形态
`rpa_cart/`、`rpa_contract/`、`rpa_rebate/` 各两个文件,模板一致。
`pyproject.toml`:
```toml
[project]
name = "rpa-<slot>"
version = "0.1.0"
description = "<一句话职责>"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
package = false
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true
```
三个 `name` / `description`:
| 目录 | name | description |
| --- | --- | --- |
| `rpa_cart` | `rpa-cart` | 洁达加购与支付回传。表 2 → 接口 2。本包尚未实现。 |
| `rpa_contract` | `rpa-contract` | 洁达合同班。表 3 合单录入 → 表 4。本包尚未实现。 |
| `rpa_rebate` | `rpa-rebate` | 洁达返利班。按合同号查号、录协议 → 表 4。本包尚未实现。 |
每个 README 必须写清:
1. 读哪张表、写哪张表、对应哪个口
2. 班次(选品随接口 1;加购在选品之后另班;支付另班对准 12:30 / 17:30;合同/返利一天两班,返利建议合同班之后)
3. 本期不做(占位)
4. 合同包额外一句:页面步骤目前仍在 `rpa_yisaiqi/`,本包能跑之后再删艺赛旗
`rpa_cart` README 须写明两个入口将来都在本包:`operateType=1` 加购、`operateType=2` 支付回传。不要暗示会拆成第五个包。
### 5.3 作业与表的对照(写入根 README)
| 包 | 读 | 写 | 口 |
| --- | --- | --- | --- |
| `rpa_selection` | 表 1 `deal_flag=0` | 表 2 `operate_type=1`,表 1 处理标志 | 不直接调口;口 2 由 `rpa_cart` 推 |
| `rpa_cart` | 表 2 | 表 2 `push_status`;支付行 | 口 2 |
| `rpa_contract` | 表 3 `process_status=0` | 表 4 `actionCode=1` | 口 3 收件在 oms;口 4 回写 |
| `rpa_rebate` | 表 3 / 表 4 待查号与待录协议 | 表 4 `contractStatus=2`、`actionCode=2` | 口 4 |
---
## 6. `资料/` → `docs/`
`docs/` 已有 `superpowers/`。不要新建第二套文档根。`资料/` 拆开并入后删除空目录。
已跟踪的用 `git mv`;未跟踪的用普通 `mv`,改完再 `git add`。不要对未跟踪文件跑 `git mv`。
仓库根 `README.md` 当前未跟踪。本轮按第 5.3 节写好后 `git add README.md`。
`选品流程/output/结果/.gitkeep` 已跟踪,随父目录 `git mv` 进入 `rpa_selection/output/结果/.gitkeep`。目录名保持 `结果`。
### 6.1 现行(`docs/` 根下)
已跟踪(`git mv`):
| 来源 | 去向 |
| --- | --- |
| `资料/RPA订单中心接口开发口径_20260930.md` | `docs/` 同名 |
| `资料/采购平台选品条件优先项_20260902.txt` | `docs/` 同名 |
未跟踪(`mv` + `git add`):
| 来源 | 去向 |
| --- | --- |
| `资料/README.md` | `docs/README.md`(重写路径,去掉「路径先不动」) |
| `资料/RPA订单中心接口口径讨论纪要_20260929.md` | `docs/` 同名 |
| `资料/ddl/` | `docs/ddl/` |
| `资料/API鉴权.doc` | `docs/` |
| `资料/下一会话任务_建表.md` | `docs/` |
| `资料/合同系统录入流程.md` | `docs/` |
| `资料/合同返利系统字段映射_20260911.xlsx` | `docs/` |
| `资料/【项目方案】福建洁达采购合同RPA自动化录入需求_v20260924.docx` | `docs/` |
口径正文里的 `` `资料/...` `` 改成 `` `docs/...` ``;`` `选品流程/` `` 改成 `` `rpa_selection/` ``。`合同系统录入流程.md` 里指向 `rpa_yisaiqi` 的句子保留。
### 6.2 归档(`docs/archive/`)
一期 13 节点主链,以及未列入现行表的旧设计说明。
已跟踪(`git mv` 进 `docs/archive/`):
- `PLAN.md`
- `一期开发任务清单_表结构_API草案.md`
- `一期需求简版说明_业务版.md`
- `界面截图证据记录.md`
- `采购业务RPA思路20260625.docx`
- `采购主链RPA一期实施与验收规范.md`
- `采购主链RPA一期开发规划.md`
- `采购主链RPA一期设计文档.md`
- `采购主链RPA一期需求文档_三方会审版.md`
- `采购主链RPA一期需求文档_会议整理版.md`
未跟踪(`mv` + `git add` 进 `docs/archive/`):
- `福建洁达采购合同自动化设计说明.md`
这些文件内部的 `资料/` 链接不改。`docs/README.md` 的历史节指向 `docs/archive/`,并保留原警告:那套模型是「一单合同走状态机」,不要用来解释接口 1 收件箱。
### 6.3 必须改路径的现行文件
只改还在用的入口,不扫历史 spec。
| 文件 | 改什么 |
| --- | --- |
| `README.md`(仓库根) | 代码入口表换成五包;文档入口改 `docs/` |
| `docs/README.md` | 见 6.1 |
| `rpa_oms/README.md` | 四个口地图;文档链接 `docs/` |
| `rpa_selection/README.md` | 目录名与口径路径 |
| `rpa_yisaiqi/README.md` | `` `资料/合同系统录入流程.md` `` → `` `docs/合同系统录入流程.md` `` |
| `.gitignore` | `选品流程/` → `rpa_selection/`(`.vscode` 例外规则、secrets、`data/.auth`、`output/结果`) |
| `docs/RPA订单中心接口开发口径_20260930.md` | `选品流程/`、`资料/` 路径 |
| `docs/RPA订单中心接口口径讨论纪要_20260929.md` | `资料/` 路径 |
| `docs/ddl/列对照.md` | `资料/ddl/` → `docs/ddl/` |
| `docs/下一会话任务_建表.md` | `资料/` 路径 |
| `docs/合同系统录入流程.md` | 若正文有 `资料/` 自引用则改 |
`docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md` 里的 `选品流程/`、`资料/` 是当时实施记录,不改。
---
## 7. `.gitignore`
`选品流程` 出现在两处,按前缀替换,不要当成一整段覆盖:
1. 顶部 `.vscode` 例外:`!选品流程/.vscode/` → `!rpa_selection/.vscode/`(两行)
2. 底部凭据与产物:`选品流程/secrets.json` 等四行,前缀改 `rpa_selection/`
其余规则不动。根 README 的代码入口表仍保留 `rpa_yisaiqi` 一行,标明过渡;「五包」指 oms + 四个作业,不是把艺赛旗从入口拿掉。
---
## 8. 错误与回归
这是搬家,没有新的运行时错误分支。失败模式只有路径漏改:
- 测试或文档还写 `选品流程`、`资料/`(现行入口)
- `cd 选品流程` 还出现在现行 README
- 占位包被误写成可运行作业(出现 `main.py` 却无实现)
回归:
```bash
cd rpa_oms && uv run pytest -v
cd rpa_selection && uv run pytest -v
```
选品单测不得改断言、不得为搬家放宽。若有测试读仓库根路径或写死 `选品流程`,改为相对包根(`Path(__file__).resolve().parents[1]` 已是包根的保持不动)。
---
## 9. 实施顺序
1. `git mv 选品流程 rpa_selection`;按 5.1 处理未跟踪残留,改该包 `pyproject.toml` 与 README
2. 建三个占位包并 `git add`
3. 按第 6 节:已跟踪文件 `git mv`,未跟踪文件 `mv` 后 `git add`;重写 `docs/README.md`
4. 写根 `README.md` 并 `git add`(该文件当前未跟踪);改 `rpa_oms/README.md`、`rpa_yisaiqi/README.md`、`.gitignore`、搬过来的现行口径
5. 跑第 8 节两条 pytest
6. 确认根上没有 `选品流程/`、`资料/`

View File

@ -0,0 +1,45 @@
# 下一会话任务:四张接口底表 DDL
先读再写,不要发挥。
必读:
1. `docs/RPA订单中心接口开发口径_20260930.md`(作业口径,列不能和口径打架)
2. `docs/RPA订单中心接口口径讨论纪要_20260929.md`
3. 接口 Excel 四张「数据表」sheet(列名、类型、备注以 Excel 为准)
当前文件:`/Users/yuuuu/Downloads/RPA与订单中心接口设计_20260922.xlsx`
若 Downloads 有更新,用最新一份。
4. 不要凭方案 5.5 加列。返利默认值是填页面用的,不是表结构。
## 交付
给出 Oracle 建表语句,四张都是 **RPA 库暂存**,不是订单中心的对码功能表。对码在订单中心做完,本方不建对码引擎。
选品这一步本方只有两张:收件 + 回传。另外两张是合同收件、合同/协议回写。
- 销售商品对码数据(接口 1):选品收件箱。Excel 沿用对方名字,实为待选品行,不是对码表。
- 加购及结算商品数据(接口 2):选品结果回传缓冲。
- 采购合同数据(接口 3)
- 合同协议录入结果(接口 4)
建议落盘:`docs/ddl/` 或仓库内现有 SQL 目录(若已有 Oracle 脚本目录,跟现有走)。
每张表包含:列、默认值、主键/唯一约束、口径里点名的索引(如条形码普通索引、接口 1 的 `oms_id` 幂等唯一、接口 3 的来源 `fid` 唯一)。表名、列名跟 Excel 英文名,不要自创拼音。
必须带上 Excel 已有、口径也锁了的列:`org_code`、MDM 码、`customer_type`、`multi_platform_flag`、`push_status` 等。`oms_id` 用 `number(20)`。
## 不要做
- 不要写选品/合同/返利页面流程
- 不要写接收 API、推送客户端
- 不要改口径文档
- 不要把测试密码写入仓库
- 接口 2、4 路径空着,DDL 不管网关
## 不确定时
Excel 与口径冲突:口径赢在作业语义,列集合以 Excel 表 sheet 为准。
Excel「待确认」列:按现稿建,不要删列。在 DDL 文件头用注释列出待确认项。
缺主键:接口 1 业务幂等是 `oms_id`;接口 2、4 的 `fid` 是本方自增主键;接口 3 的 `fid` 是订单中心来源唯一序号。
做完只交 DDL 和与 Excel 的列对照(缺了什么、多了什么)。不要展开下一阶段。

View File

@ -0,0 +1,75 @@
# 合同系统录入流程(现行)
对照:`RPA订单中心接口开发口径_20260930.md` 第 4 节。
页面动作对照:`rpa_contract/contract_flow_steps.py`、`rpa_contract/selectors/contract_create_selectors.json`、`rpa_contract/examples/`。
状态中心 job / 13 节点 / start-complete-fail **不再使用**。合单、班次、回写以口径为准;点哪些控件以 `rpa_contract` 为准。
---
## 1. 数据从哪来
订单中心推接口 3,落入 `rpa_purchase_contract_data`。合同班读 `process_status=0`。
一天两班(中午、下午)。启动瞬间冻结本班名单,跑的过程中新进的下一班再处理。
按 `supplierId` 合成一份合同:
- 不用 `omsId`,不用供应商名称
- 头字段(账期、签订日、起止日、采购员、计划员)取该班该供应商**第一行**
- 下午同一供应商另开一份,不改上午已录合同
- `contractEntryStatus=0` 且带 `contractNo`:只录返利协议,不进合单、不重开合同
旧脚本按「供应商 + 订单日期」建单,并且把签订日/开始日写成订单日期。那是上一套模型,不要抄。
---
## 2. 页面步骤(合同测试系统)
地址:`http://contract-test.gzmpc.com:8080/`
测试账号见 `rpa_contract/README.md`。客商必须是系统里真实存在的操作码,占位符搜不到。
脚本现用直达起草/模板 URL,绕过左侧菜单和「点模板」的前端委托(Selenium 点不中):
- 起草:`DRAFT_PAGE_URL`
- 非格式采购协议(2025 集团版)模板 ID:`198d10b04325279b8ffeb8b4c6493b5a` → `FORM_PAGE_URL`
顺序:
1. 打开合同系统并登录
2. 进起草,套非格式采购协议模板
3. 签约方:客户编码、客户名称(用接口 3 的供应商 MDM / 名称,以页面能搜到的为准)
4. 基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质=主合同
5. 金额与日期:签订日 / 开始日 / 结束日用接口 3 第一行的 `contractSignDate`、`contractStartDate`、`contractEndDate`
6. 货品明细:该班该供应商合进来的行(货品、规格、数量、单价、MDM)
7. 付款与标志:这期一律账期,**不勾预付款**;付款期限用 `paymentTerm`。页面「账期支付」对应选项「电汇」(见脚本 `_PAYMENT_ALIASES`)
8. **尽调**:看合同系统页面回显。未尽调 → 本单失败,原因「客户未尽调」,不往下填。旧脚本没有这一步,口径有
9. 点**提交**,不要点暂存。暂存号不拿去录返利
10. 从「我的合同」读正式合同号
医药城同供应商同日 PDF 合并上传,是旧主链(平台导出扫描件)的步骤。接口 3 这条链有没有附件,录合同时再核对,不要默认还去拼 PDF。
---
## 3. 提交之后
提交成功:
- 写入表 4,接口 4 `actionCode=1`,`contractStatus=1`(起草),带合同号
- 同一份合同下多个 `fid` 回同一套号和状态
- **合同任务结束。不要重录合同。**
「合同成功」不是提交成功,而是返利中心能搜到这个合同号。查号挂在返利班,搜到再回 `contractStatus=2`;搜不到不重录,下一班再搜。
---
## 4. 代码要从哪抄
| 要 | 文件 |
| --- | --- |
| 打开/登录/填表/提交/读号 | `rpa_contract/contract_flow_steps.py` |
| 字段 name / 选择器 | 同文件里的 `F`、`X`;`selectors/contract_create_selectors.json` |
| 浏览器适配 | `rpa_contract/apa_browser_adapter.py` |
| 干跑样例 | `rpa_contract/examples/contract_create_input_dry_run.json` |
旧状态中心(`callback_client.py` / `agent.py` / start-complete-fail)已从仓库删除。不要再接。

Binary file not shown.

16
rpa_cart/README.md Normal file
View File

@ -0,0 +1,16 @@
# rpa_cart
加购与支付回传。两个班次,同一个包,不拆第五个目录。本包尚未实现。
| 班次 | 读 | 写 | 口 |
| --- | --- | --- | --- |
| 加购 `operateType=1` | 表 2(选品已落、`push_status=0`) | 表 2 加购结果,再推口 2 | 口 2 |
| 支付回传 `operateType=2` | 商城已支付订单 | 表 2 支付行(`payOrderNo` 等) | 口 2 |
加购在选品班之后另排。支付对准采购员两班付完后(例如 12:30、17:30),有单号才推,没付不推。
选品落表在 `rpa_selection/`。HTTP 推送路径做到时进 `rpa_oms` 的 `app/outbound/`。
## 本期不做
真加购、清空购物车、支付回传、接口 2 推送。

13
rpa_cart/pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[project]
name = "rpa-cart"
version = "0.1.0"
description = "洁达加购与支付回传。表 2 → 接口 2。本包尚未实现。"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
package = false
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

49
rpa_contract/README.md Normal file
View File

@ -0,0 +1,49 @@
# rpa_contract
合同班。页面录入脚本在本包;从表 3 合单、写表 4、推接口 4 尚未接。
| | |
| --- | --- |
| 读 | 表 3 `rpa_purchase_contract_data`(`process_status=0`) |
| 写 | 表 4 `rpa_contract_agreement_result`(接口 4 `actionCode=1`,`contractStatus=1` 起草) |
| 口 | 口 3 收件在 `rpa_oms`;口 4 回写做到时由本班调用 |
一天两班(中午、下午)。启动瞬间冻结本班名单。按 `supplierId` 合单;头字段取该班该供应商第一行。下午同一供应商另开一份。
口径:`docs/RPA订单中心接口开发口径_20260930.md` 第 4 节;合单细节见 `docs/合同系统录入流程.md`。
## 测试账号
地址:`http://contract-test.gzmpc.com:8080/`
账号:`test002 / 1`
客商必须是合同系统里真实存在的操作码,占位符搜不到。例如 `DW0100046361` / `重庆亿昊药业有限公司`。
## 页面步骤
脚本直达起草/模板 URL:
1. 打开合同系统并登录
2. 进起草,套非格式采购协议模板
3. 签约方:客户编码、客户名称
4. 基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质=主合同
5. 金额与日期:签订日 / 开始日 / 结束日
6. 货品明细
7. 付款与标志:这期一律账期,不勾预付款;页面「账期支付」对应选项「电汇」
8. 尽调:口径有,脚本尚未加这一闸
9. 点提交,不要点暂存
10. 从「我的合同」读正式合同号
## 运行
```bash
cd rpa_contract
uv sync --python 3.12
uv run pytest -v
uv run python main.py --input-json examples/contract_create_input_dry_run.json
```
`submitMode=dry_run` 跑到提交前,不点提交。真起草用 `examples/contract_draft_yyc.json` / `examples/contract_draft_xyy.json`。
## 本期不做
从表 3 按班次合单、尽调闸、接口 4 回写。

View File

@ -11,9 +11,9 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Protocol from typing import Protocol
from .errors import NodeError from errors import NodeError
PACKAGE_ROOT = Path(__file__).resolve().parents[1] PACKAGE_ROOT = Path(__file__).resolve().parent
SCREENSHOT_ROOT = PACKAGE_ROOT / "screenshots" SCREENSHOT_ROOT = PACKAGE_ROOT / "screenshots"

View File

@ -1,7 +1,6 @@
# coding=utf-8 # coding=utf-8
""" """
APA (艺赛旗) Selenium Chrome BrowserAdapter 实现 Selenium Chrome 浏览器适配。通过 WebDriver 操控 Chrome。
通过 Selenium WebDriver 直接操控 Chrome,不依赖 APA 浏览器模块
""" """
from __future__ import annotations from __future__ import annotations
@ -495,7 +494,7 @@ class ApaBrowserAdapter:
return "" return ""
def screenshot(self, name: str) -> str: def screenshot(self, name: str) -> str:
from .runtime.accept import artifact_dir from accept import artifact_dir
save_dir = artifact_dir(self._flow) save_dir = artifact_dir(self._flow)
save_dir.mkdir(parents=True, exist_ok=True) save_dir.mkdir(parents=True, exist_ok=True)

View File

@ -1,40 +1,29 @@
"""CONTRACT_CREATE node: input building + state-center callback (runtime.shell) + Selenium drafting.""" """CONTRACT_CREATE: 入参组装 + Selenium 起草。结果返回合同号,不回调旧状态中心。"""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json import json
import os import os
import sys
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from pathlib import Path from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
from .apa_browser_adapter import ApaBrowserAdapter from apa_browser_adapter import ApaBrowserAdapter
from .callback_client import StateCenterClient from contract_draft import (
from .contract_draft import (
ContractDraft, ContractDraft,
contract_draft_from_dict, contract_draft_from_dict,
is_placeholder_customer_code, is_placeholder_customer_code,
load_contract_draft, load_contract_draft,
) )
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow from contract_flow_steps import ContractFlowContext, run_contract_create_flow
from .pdf_utils import ensure_merged_pdf from errors import NodeError, PdfMergeUnsupported, UnknownRpaError
from .runtime.errors import NodeError, PdfMergeUnsupported, UnknownRpaError from pdf_utils import ensure_merged_pdf
from .runtime.shell import execute_node
NODE_CODE = "CONTRACT_CREATE"
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/" CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
class ContractCreateError(NodeError): class ContractCreateError(NodeError):
"""入参缺失 / 页面元素找不到 / 页面改版,均映射 UI_ELEMENT_NOT_FOUND。""" """入参缺失 / 页面元素找不到 / 页面改版。"""
def __init__(self, message: str = "") -> None:
# Studio 编译出来的旧 S01 在 input_arg 为空时会直接 raise。
# 这里拦下来改走默认联调 JSON,避免每次改 Main.py 都被覆盖。
if _should_bootstrap_default_input(message):
_bootstrap_default_input_and_exit()
super().__init__(message)
def default_contract_input_path() -> str: def default_contract_input_path() -> str:
@ -43,26 +32,6 @@ def default_contract_input_path() -> str:
) )
def _should_bootstrap_default_input(message: str) -> bool:
if "缺少业务入参" not in str(message):
return False
script = os.path.basename(str(sys.argv[0] if sys.argv else "")).lower()
return script == "main.py"
def _bootstrap_default_input_and_exit() -> None:
path = default_contract_input_path()
if not Path(path).is_file():
return
print("未传入 -i/--input,使用默认联调入参:", path, flush=True)
codes_dir = Path(__file__).resolve().parent / "studio" / "codes"
if str(codes_dir) not in sys.path:
sys.path.insert(0, str(codes_dir))
from run_contract import main
raise SystemExit(main())
@dataclass(frozen=True) @dataclass(frozen=True)
class ContractCreateInput: class ContractCreateInput:
job_id: str job_id: str
@ -87,40 +56,28 @@ ContractRunner = Callable[[ContractCreateInput], str]
def execute_contract_create( def execute_contract_create(
spec: ContractCreateInput, spec: ContractCreateInput,
client: StateCenterClient,
*, *,
contract_runner: ContractRunner | None = None, contract_runner: ContractRunner | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
runner = contract_runner or create_contract_in_contract_system runner = contract_runner or create_contract_in_contract_system
# dry_run:只跑到提交前,不回调状态中心(不 start/complete/fail)
if spec.submit_mode == "dry_run": if spec.submit_mode == "dry_run":
runner(spec) runner(spec)
return {"status": "dry_run", "submitted": False} return {"status": "dry_run", "submitted": False}
def _run() -> dict[str, Any]: try:
try: contract_no = runner(spec)
contract_no = runner(spec) except NodeError:
except NodeError: raise
raise except Exception as exc:
except Exception as exc: raise UnknownRpaError(str(exc)) from exc
raise UnknownRpaError(str(exc)) from exc if not contract_no:
if not contract_no: raise ContractCreateError("submit 模式未读取到真实合同编号")
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete") return {
return { "status": "done",
"businessRefs": {"contractSystemNo": contract_no}, "contractSystemNo": contract_no,
"summary": _build_summary(spec), "summary": _build_summary(spec),
"artifacts": [], }
}
return execute_node(
client,
spec.job_id,
NODE_CODE,
_run,
attempt_no=spec.attempt_no,
operator="agent",
)
def create_contract_in_contract_system(spec: ContractCreateInput) -> str: def create_contract_in_contract_system(spec: ContractCreateInput) -> str:
@ -321,21 +278,13 @@ def load_input_data(raw: str) -> dict[str, Any]:
raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}") raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}")
def run_contract_create( def run_contract_create(data: dict[str, Any]) -> dict[str, Any]:
data: dict[str, Any],
*,
state_center_url: str,
agent_token: str,
) -> dict[str, Any]:
spec = build_spec_from_data(data) spec = build_spec_from_data(data)
client = StateCenterClient(state_center_url, agent_token) return execute_contract_create(spec)
return execute_contract_create(spec, client)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE node.") parser = argparse.ArgumentParser(description="合同系统页面起草。")
parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000"))
parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON")) parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON"))
parser.add_argument("--job-id") parser.add_argument("--job-id")
parser.add_argument("--supplier-name") parser.add_argument("--supplier-name")
@ -353,7 +302,6 @@ def parse_args() -> argparse.Namespace:
def main() -> int: def main() -> int:
args = parse_args() args = parse_args()
client = StateCenterClient(args.state_center_url, args.token)
if args.input_json: if args.input_json:
spec = build_spec_from_data(load_input_data(args.input_json)) spec = build_spec_from_data(load_input_data(args.input_json))
@ -374,7 +322,7 @@ def main() -> int:
merged_pdf_output=args.merged_pdf_output, merged_pdf_output=args.merged_pdf_output,
) )
result = execute_contract_create(spec, client) result = execute_contract_create(spec)
print(json.dumps(result, ensure_ascii=False)) print(json.dumps(result, ensure_ascii=False))
return 0 return 0

View File

@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from .contract_flow_steps import ContractGoodsLine from contract_flow_steps import ContractGoodsLine
_PLACEHOLDER_CUSTOMER_PREFIXES = ("CUST-", "TEST-", "MOCK-") _PLACEHOLDER_CUSTOMER_PREFIXES = ("CUST-", "TEST-", "MOCK-")

View File

@ -1,8 +1,4 @@
"""Flowchart-sized CONTRACT_CREATE browser steps for YiSaiQi. """合同系统页面步骤:登录、起草、填表、提交、读合同号。"""
Each function is intentionally small enough to map to one YiSaiQi flowchart
node or one script block behind a flowchart node.
"""
from __future__ import annotations from __future__ import annotations
import time import time
@ -10,8 +6,8 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Protocol from typing import Protocol
from .runtime.accept import AcceptanceReport, accept_log_path, evidence_shot from accept import AcceptanceReport, accept_log_path, evidence_shot
from .runtime.errors import ( from errors import (
LoginFailed, LoginFailed,
NodeError, NodeError,
PdfMergeUnsupported, PdfMergeUnsupported,
@ -120,10 +116,6 @@ def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext)
_ACCEPT.note("S13", "读合同号", {"contractNo": "(none)", "mode": ctx.submit_mode}, ok=False) _ACCEPT.note("S13", "读合同号", {"contractNo": "(none)", "mode": ctx.submit_mode}, ok=False)
return "" return ""
contract_no = read_contract_number(browser, ctx) contract_no = read_contract_number(browser, ctx)
state = _flow_runtime()
if state is not None:
state.contract_no = contract_no
_callback_complete(contract_no)
_ACCEPT.note( _ACCEPT.note(
"S13", "S13",
"读合同号", "读合同号",
@ -294,51 +286,7 @@ CUSTOMER_PICKER = {
} }
def _flow_runtime():
try:
from . import _flow_state as state
except Exception:
return None
return state
def _callback_start() -> None:
state = _flow_runtime()
if state is None or getattr(state, "is_dry_run", False):
return
client = getattr(state, "client", None)
spec = getattr(state, "spec", None)
if not client or not spec:
return
print("回调状态中心 start", spec.job_id, flush=True)
client.start_node(spec.job_id, "CONTRACT_CREATE")
def _callback_complete(contract_no: str) -> None:
state = _flow_runtime()
if state is None or getattr(state, "is_dry_run", False):
return
if getattr(state, "completed", False):
print("状态中心 complete 已发送,跳过重复回调", flush=True)
return
client = getattr(state, "client", None)
spec = getattr(state, "spec", None)
number = (contract_no or "").strip()
if not client or not spec or not number:
print("跳过状态中心 complete:合同编号为空", flush=True)
return
print("回调状态中心 complete", spec.job_id, number, flush=True)
client.complete_node(
spec.job_id,
"CONTRACT_CREATE",
{"businessRefs": {"contractSystemNo": number}},
)
state.completed = True
state.contract_no = number
def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None: def open_contract_system(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
_callback_start()
browser.open_url(ctx.contract_url) browser.open_url(ctx.contract_url)
_ACCEPT.note("S02", "打开合同系统", {"url": ctx.contract_url}, ok=True) _ACCEPT.note("S02", "打开合同系统", {"url": ctx.contract_url}, ok=True)
@ -411,7 +359,7 @@ def fill_contract_signatory_info(browser: BrowserAdapter, ctx: ContractFlowConte
if not ctx.customer_code: if not ctx.customer_code:
raise SupplierContractDisabled("客商操作码(customerCode)为空,无法加载客商") raise SupplierContractDisabled("客商操作码(customerCode)为空,无法加载客商")
from .contract_draft import is_placeholder_customer_code from contract_draft import is_placeholder_customer_code
if is_placeholder_customer_code(ctx.customer_code): if is_placeholder_customer_code(ctx.customer_code):
raise SupplierContractDisabled( raise SupplierContractDisabled(
@ -1120,10 +1068,6 @@ def submit_contract_for_approval(browser: BrowserAdapter, ctx: ContractFlowConte
evidence_shot(browser, "contract-submitted") evidence_shot(browser, "contract-submitted")
print("提交成功,回我的合同取编号。", flush=True) print("提交成功,回我的合同取编号。", flush=True)
fd_id = fd_id or _read_document_id(browser) fd_id = fd_id or _read_document_id(browser)
state = _flow_runtime()
if state is not None:
state.fd_id = fd_id
state.submitted = True
_ACCEPT.note("S12", "提交", {"submitted": "yes"}, ok=True) _ACCEPT.note("S12", "提交", {"submitted": "yes"}, ok=True)
return True return True
_confirm_submit_dialogs(browser) _confirm_submit_dialogs(browser)
@ -1219,10 +1163,6 @@ def _confirm_submit_dialogs(browser: BrowserAdapter) -> None:
def read_contract_number(browser: BrowserAdapter, ctx: ContractFlowContext | None = None) -> str: def read_contract_number(browser: BrowserAdapter, ctx: ContractFlowContext | None = None) -> str:
if ctx is None:
state = _flow_runtime()
if state is not None:
ctx = getattr(state, "ctx", None)
if ctx is None: if ctx is None:
ctx = ContractFlowContext( ctx = ContractFlowContext(
contract_url="http://contract-test.gzmpc.com:8080/", contract_url="http://contract-test.gzmpc.com:8080/",
@ -1232,13 +1172,7 @@ def read_contract_number(browser: BrowserAdapter, ctx: ContractFlowContext | Non
order_date="", order_date="",
source_type="", source_type="",
) )
number = _read_contract_number_from_my_list(browser, ctx) return _read_contract_number_from_my_list(browser, ctx)
if _usable_contract_no(number):
state = _flow_runtime()
if state is not None:
state.contract_no = number
_callback_complete(number)
return number
def _read_contract_number_on_page(browser: BrowserAdapter) -> str: def _read_contract_number_on_page(browser: BrowserAdapter) -> str:

View File

@ -1,4 +1,4 @@
"""Node errors that map onto state-center fail callbacks.""" """Page-flow errors. Codes stay for logs; they no longer map to a state-center fail callback."""
class NodeError(RuntimeError): class NodeError(RuntimeError):

View File

@ -38,7 +38,5 @@
"mdmCode": "", "mdmCode": "",
"unitPrice": "50.00" "unitPrice": "50.00"
} }
], ]
"stateCenterUrl": "http://127.0.0.1:8000",
"agentToken": "dev-agent-token"
} }

4
rpa_contract/main.py Normal file
View File

@ -0,0 +1,4 @@
from contract_create_node import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -1,4 +1,4 @@
"""PDF utilities for YiSaiQi contract drafting.""" """合同 PDF 合并。"""
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
@ -7,8 +7,7 @@ from pathlib import Path
def ensure_merged_pdf(source_paths: list[str], output_path: str) -> str: def ensure_merged_pdf(source_paths: list[str], output_path: str) -> str:
"""Return one PDF path, merging multiple source PDFs when needed. """Return one PDF path, merging multiple source PDFs when needed.
YiSaiQi must upload only one YYC contract document for the same supplier 同供应商同日只上传一份 PDF。单文件直接返回路径。
and same order date. A single source PDF can be uploaded directly.
""" """
if not source_paths: if not source_paths:
raise ValueError("YYC contract creation requires at least one platform PDF") raise ValueError("YYC contract creation requires at least one platform PDF")

View File

@ -0,0 +1,29 @@
[project]
name = "rpa-contract"
version = "0.1.0"
description = "洁达合同班。表 3 合单录入 → 表 4。页面脚本已在本包;合单与接口 4 尚未接。"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pypdf>=5.1.0",
"selenium>=4.15.0",
"webdriver-manager>=4.0.0",
"requests>=2.31.0",
]
[dependency-groups]
dev = [
"pytest>=8.0.0",
]
[tool.uv]
package = false
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
addopts = "-q"
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

View File

@ -1,18 +1,18 @@
from rpa_yisaiqi.runtime.accept import ( from accept import (
AcceptanceReport, AcceptanceReport,
accept_log_path, accept_log_path,
artifact_dir, artifact_dir,
evidence_shot, evidence_shot,
field_match, field_match,
) )
from rpa_yisaiqi.runtime.errors import NodeError from errors import NodeError
import pytest import pytest
def test_each_flow_writes_its_own_accept_file(tmp_path, monkeypatch): def test_each_flow_writes_its_own_accept_file(tmp_path, monkeypatch):
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False) monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
monkeypatch.setattr( monkeypatch.setattr(
"rpa_yisaiqi.runtime.accept.SCREENSHOT_ROOT", "accept.SCREENSHOT_ROOT",
tmp_path, tmp_path,
) )
report = AcceptanceReport(flow="ORDER_FETCH") report = AcceptanceReport(flow="ORDER_FETCH")

View File

@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
from rpa_yisaiqi.apa_browser_adapter import REPO_ROOT, resolve_chrome_profile_base from apa_browser_adapter import REPO_ROOT, resolve_chrome_profile_base
def test_default_chrome_profile_is_outside_repo(): def test_default_chrome_profile_is_outside_repo():
@ -11,7 +11,7 @@ def test_default_chrome_profile_is_outside_repo():
def test_env_path_inside_repo_is_rejected(tmp_path, monkeypatch): def test_env_path_inside_repo_is_rejected(tmp_path, monkeypatch):
inside = REPO_ROOT / "rpa_yisaiqi" / "runtime" / "chrome-profile" inside = REPO_ROOT / "rpa_contract" / "screenshots" / "chrome-profile"
monkeypatch.delenv("RPA_CHROME_PROFILE", raising=False) monkeypatch.delenv("RPA_CHROME_PROFILE", raising=False)
profile = resolve_chrome_profile_base(requested=str(inside)) profile = resolve_chrome_profile_base(requested=str(inside))
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve() assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()

View File

@ -0,0 +1,15 @@
from errors import LoginFailed, NodeError
def test_node_error_carries_default_ui_code():
exc = NodeError("元素找不到")
assert exc.error_code == "UI_ELEMENT_NOT_FOUND"
assert exc.retryable is False
assert exc.route_role == "OPS"
def test_login_failed_is_retryable():
exc = LoginFailed("登录失败")
assert exc.error_code == "SYS_LOGIN_FAIL"
assert exc.retryable is True
assert exc.route_role == "OPS"

480
rpa_contract/uv.lock Normal file
View File

@ -0,0 +1,480 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "cffi"
version = "2.1.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.5.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "idna"
version = "3.20"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "outcome"
version = "1.3.0.post0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "attrs" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pypdf"
version = "6.19.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" },
]
[[package]]
name = "pysocks"
version = "1.7.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.4"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "rpa-contract"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pypdf" },
{ name = "requests" },
{ name = "selenium" },
{ name = "webdriver-manager" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "pypdf", specifier = ">=5.1.0" },
{ name = "requests", specifier = ">=2.31.0" },
{ name = "selenium", specifier = ">=4.15.0" },
{ name = "webdriver-manager", specifier = ">=4.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0.0" }]
[[package]]
name = "selenium"
version = "4.50.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "certifi" },
{ name = "trio" },
{ name = "trio-websocket" },
{ name = "typing-extensions" },
{ name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/6c/09a5bdf96a5f850a2bf69ee670a246ae30a132dca3975c7fb081aae09338/selenium-4.50.0.tar.gz", hash = "sha256:80402fb6f48e0ee2183ec1d58b257a593dcd5c8130c1e6419ee7509f2128be5d", size = 1051762, upload-time = "2026-09-30T19:49:33.506Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/d3/fedaee179dcd47a7033bb5a74b7d5742da18e46d4ef3b279277c887de64a/selenium-4.50.0-py3-none-any.whl", hash = "sha256:f724a38a7fd8b0561c6767c620bd9b2c7bd8012d4cb664b2ad23064e5d67d0df", size = 12080288, upload-time = "2026-09-30T19:49:30.734Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "trio"
version = "0.34.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "attrs" },
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
{ name = "idna" },
{ name = "outcome" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/dc/a2d25ed73ad49cfd79bf18d262577c3731c98e382284e28d522f49a0df35/trio-0.34.0.tar.gz", hash = "sha256:63b9485408bdfdde544fced107045a8c0086cdc4bd0ef2f797b9e0dd111b964b", size = 607457, upload-time = "2026-08-11T00:33:42.198Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1f/555f1364bed52a92a864181962b77f1b15adadeacf23b86105324363e461/trio-0.34.0-py3-none-any.whl", hash = "sha256:6c7c9f49917694dcdcd5f67abd168df5599eca480d61f29854d17a61a75c2f05", size = 511840, upload-time = "2026-08-11T00:33:40.552Z" },
]
[[package]]
name = "trio-websocket"
version = "0.12.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "outcome" },
{ name = "trio" },
{ name = "wsproto" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
[[package]]
name = "urllib3"
version = "2.8.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[package.optional-dependencies]
socks = [
{ name = "pysocks" },
]
[[package]]
name = "webdriver-manager"
version = "4.1.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "packaging" },
{ name = "python-dotenv" },
{ name = "requests" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/e5/78d362c3507473d27708068fa66c2439b55127571351a096c7ec8eb85e9c/webdriver_manager-4.1.2.tar.gz", hash = "sha256:230515ff4dc4c2feff1add25306ca262421b31016d77ded305e8dd921266445f", size = 38948, upload-time = "2026-06-04T06:00:00.895Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/2c/7414ab135e99df7d9cc4a4afbd21b46906a9397aa2519beac50f6517138e/webdriver_manager-4.1.2-py3-none-any.whl", hash = "sha256:552bbb4e6f95e6e8b772565d2023e7c0ec5ff1736db0b186922d0466fc78c3de", size = 32512, upload-time = "2026-06-04T05:59:59.565Z" },
]
[[package]]
name = "websocket-client"
version = "1.9.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110, upload-time = "2026-08-31T14:08:40.964Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786, upload-time = "2026-08-31T14:08:39.899Z" },
]
[[package]]
name = "wsproto"
version = "1.3.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
]

View File

@ -1,89 +0,0 @@
#!/usr/bin/env python3
"""
Mock 医药城/小药药订单数据生成器
用于在没有真实医药平台访问权限时,模拟 ORDER_FETCH 节点的输出
"""
import pandas as pd
from datetime import datetime, timedelta
import random
import os
# 假订单数据模板
MOCK_SUPPLIERS = [
"重庆亿昊药业",
"华润医药",
"国药控股",
"上海医药"
]
MOCK_PRODUCTS = [
{"name": "阿卡波糖片", "spec": "50mg*30片", "manufacturer": "示例制药", "approval_no": "国药准字H00000001"},
{"name": "二甲双胍片", "spec": "0.5g*60片", "manufacturer": "示例制药", "approval_no": "国药准字H00000002"},
{"name": "格列美脲片", "spec": "2mg*10片", "manufacturer": "示例制药", "approval_no": "国药准字H00000003"},
{"name": "阿托伐他汀钙片", "spec": "10mg*7片", "manufacturer": "示例制药", "approval_no": "国药准字H00000004"},
]
def generate_mock_orders(supplier_name: str, order_date: str, source_type: str = "YYC", item_count: int = 5):
"""
生成模拟订单Excel
Args:
supplier_name: 供应商名称
order_date: 订单日期 YYYY-MM-DD
source_type: 来源类型 YYC/XYY/OFFLINE
item_count: 订单明细行数
Returns:
生成的Excel文件路径
"""
rows = []
for i in range(item_count):
product = random.choice(MOCK_PRODUCTS)
rows.append({
"订单号": f"DD-{order_date.replace('-', '')}-{i+1:03d}",
"订单日期": order_date,
"供应商名称": supplier_name,
"药品通用名": product["name"],
"规格": product["spec"],
"单位": "盒",
"数量": random.randint(10, 500),
"单价": round(random.uniform(5.0, 200.0), 2),
"生产厂家": product["manufacturer"],
"批准文号": product["approval_no"],
})
df = pd.DataFrame(rows)
# 生成文件路径
output_dir = "/tmp/mock_rpa_data"
os.makedirs(output_dir, exist_ok=True)
filename = f"orders_{supplier_name}_{order_date}_{source_type}.xlsx"
filepath = os.path.join(output_dir, filename)
df.to_excel(filepath, index=False, sheet_name="订单明细")
print(f"✓ 生成模拟订单: {filepath}")
print(f" - 供应商: {supplier_name}")
print(f" - 订单日期: {order_date}")
print(f" - 明细行数: {len(rows)}")
return filepath
if __name__ == "__main__":
# 生成几个测试订单
test_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
print("生成模拟订单数据...\n")
# 医药城订单(线上)
generate_mock_orders("重庆亿昊药业", test_date, "YYC", item_count=8)
# 小药药订单(线上)
generate_mock_orders("华润医药", test_date, "XYY", item_count=5)
# 线下手工单
generate_mock_orders("国药控股", test_date, "OFFLINE", item_count=3)
print("\n✓ 全部完成,文件位于: /tmp/mock_rpa_data/")

View File

@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""
ORDER_FETCH 节点 Mock 实现
模拟从医药城/小药药导出订单,实际返回本地生成的假数据
"""
import requests
import hashlib
from pathlib import Path
from mock_orders import generate_mock_orders
from datetime import datetime, timedelta
STATE_CENTER_URL = "http://localhost:8000" # 状态中心地址
AGENT_TOKEN = "dev-agent-token-12345" # 开发令牌(和state_center的配置一致)
def calculate_file_hash(filepath: str) -> str:
"""计算文件SHA256哈希"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return f"sha256:{sha256.hexdigest()}"
def mock_order_fetch(job_id: str, order_date: str, source_type: str = "YYC"):
"""
模拟 ORDER_FETCH 节点执行
Args:
job_id: 单据ID
order_date: 订单日期
source_type: 来源类型 YYC/XYY/OFFLINE
"""
print(f"\n{'='*60}")
print(f"🤖 [Mock RPA] ORDER_FETCH 开始执行")
print(f" Job ID: {job_id}")
print(f" 订单日期: {order_date}")
print(f" 来源: {source_type}")
print(f"{'='*60}\n")
# 1. 调用 start 回调
print("📞 调用状态中心 /start 回调...")
start_resp = requests.post(
f"{STATE_CENTER_URL}/api/jobs/{job_id}/nodes/ORDER_FETCH/start",
headers={"X-Agent-Token": AGENT_TOKEN},
json={
"attemptNo": 1,
"executorRunId": f"MOCK-RUN-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"executorPlatform": "mock-rpa",
"operator": "mock-script"
}
)
print(f" 响应: {start_resp.status_code} - {start_resp.json()}\n")
# 2. 生成假订单数据
print("📦 生成模拟订单数据...")
supplier_name = "重庆亿昊药业" if source_type == "YYC" else "华润医药"
excel_path = generate_mock_orders(supplier_name, order_date, source_type, item_count=8)
print()
# 3. 计算文件哈希
file_hash = calculate_file_hash(excel_path)
# 4. 调用 complete 回调
print("📞 调用状态中心 /complete 回调...")
order_nos = [f"DD-{order_date.replace('-', '')}-{i+1:03d}" for i in range(8)]
complete_resp = requests.post(
f"{STATE_CENTER_URL}/api/jobs/{job_id}/nodes/ORDER_FETCH/complete",
headers={"X-Agent-Token": AGENT_TOKEN},
json={
"attemptNo": 1,
"executorRunId": f"MOCK-RUN-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"executorPlatform": "mock-rpa",
"operator": "mock-script",
"summary": {
"orderCount": 8,
"supplierList": [supplier_name],
"orderNoList": order_nos
},
"artifacts": [
{
"artifactType": "SOURCE_EXCEL",
"storagePath": excel_path,
"fileHash": file_hash
}
]
}
)
print(f" 响应: {complete_resp.status_code}")
result = complete_resp.json()
print(f" next_node: {result.get('next_node')}")
print(f" job_status: {result.get('job_status')}\n")
print(f"✅ ORDER_FETCH 完成!下一节点: {result.get('next_node')}\n")
return result
if __name__ == "__main__":
# 示例:模拟昨天的医药城订单
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
job_id = f"YYC-重庆亿昊药业-{yesterday.replace('-', '')}-001"
mock_order_fetch(job_id, yesterday, "YYC")

View File

@ -1,38 +1,33 @@
# rpa_oms # rpa_oms
订单中心四接口。本期只开放接口 1:销售商品对码数据接收。 订单中心四个 HTTP 口,一个 FastAPI。HTTP 收件不同步跑作业。作业在 `rpa_selection` / `rpa_cart` / `rpa_contract` / `rpa_rebate`。
口径:`docs/RPA订单中心接口开发口径_20260930.md`。DDL:`docs/ddl/rpa_oms_staging_tables.sql`。
| 口 | 方向 | 底表 | 代码 |
| --- | --- | --- | --- |
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | 已有 `app/inbound/sales_goods.py`,`POST /api/oms/v1/sales-goods` |
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 未做。将来 `app/outbound/`,由 `rpa_cart` 调用 |
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | 未做。将来 `app/inbound/`,复用接口 1 信封/鉴权 |
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 未做。将来 `app/outbound/`,由合同/返利班调用 |
本期只开放接口 1:
- 路径:`POST /api/oms/v1/sales-goods` - 路径:`POST /api/oms/v1/sales-goods`
- 鉴权:header `X-Oms-Token`,环境变量 `OMS_INBOUND_TOKEN` - 鉴权:header `X-Oms-Token`,环境变量 `OMS_INBOUND_TOKEN`
- 落表:`rpa_sales_goods_mapping_data`(`deal_flag=0`) - 落表:`rpa_sales_goods_mapping_data`(`deal_flag=0`)
- 请求里不跑选品。选品班次在仓库根目录 `选品流程/`,按条形码搜两平台、四级仓序拆量后写 `rpa_cart_settlement_goods_data` - 请求里不跑选品。选品班次在 `rpa_selection/`,按条形码搜两平台、四级仓序拆量后写 `rpa_cart_settlement_goods_data`
## 本地(内存仓库) 运行时只落 Oracle。单测用内存替身,不连库:`uv run pytest -v`。
```bash
cd rpa_oms
uv sync --python 3.12
export OMS_INBOUND_TOKEN=dev-oms-token
export OMS_REPO=memory
uv run uvicorn app.main:app --reload --port 8001
```
```bash
curl -sS http://127.0.0.1:8001/api/oms/v1/sales-goods \
-H 'Content-Type: application/json' \
-H 'X-Oms-Token: dev-oms-token' \
-d @examples/sales_goods_batch.json
```
单测:`uv run pytest -v`
## 真库 ## 真库
Oracle 11.2 需要 Instant Client(thick)。密码只放本机 `rpa_oms/.env`(已 gitignore),不要写进仓库、README、命令行历史。 Oracle 11.2 需要 Instant Client(thick)。密码只放本机 `rpa_oms/.env`(已 gitignore),不要写进仓库、README、命令行历史。
```bash ```bash
export OMS_REPO=oracle cd rpa_oms
export OMS_INBOUND_TOKEN=... uv sync --python 3.12
export OMS_INBOUND_TOKEN=dev-oms-token
export RPA_ORACLE_HOST=192.168.12.244 export RPA_ORACLE_HOST=192.168.12.244
export RPA_ORACLE_PORT=1521 export RPA_ORACLE_PORT=1521
export RPA_ORACLE_SERVICE=rpa export RPA_ORACLE_SERVICE=rpa
@ -41,6 +36,7 @@ export RPA_ORACLE_PASSWORD=...
export RPA_ORACLE_THICK=1 export RPA_ORACLE_THICK=1
# 如需指定 Instant Client 目录: # 如需指定 Instant Client 目录:
# export RPA_ORACLE_CLIENT_LIB=/path/to/instantclient # export RPA_ORACLE_CLIENT_LIB=/path/to/instantclient
uv run uvicorn app.main:app --reload --port 8001
``` ```
缺 `RPA_ORACLE_HOST` / 用户 / 密码时,收件接口回 503。 缺 `RPA_ORACLE_HOST` / 用户 / 密码时,收件接口回 503。

View File

@ -1,4 +1,4 @@
"""内存仓库。单测和 OMS_REPO=memory 用,进程退出即丢。 """内存仓库。只给单测用,进程退出即丢。运行时走 Oracle。
按 oms_id 判重,已存在就 DuplicateOmsId,不覆盖。 按 oms_id 判重,已存在就 DuplicateOmsId,不覆盖。
""" """

View File

@ -7,7 +7,6 @@
""" """
from __future__ import annotations from __future__ import annotations
import os
from typing import Annotated, Any from typing import Annotated, Any
from fastapi import Depends, FastAPI, Header from fastapi import Depends, FastAPI, Header
@ -18,21 +17,14 @@ from .local_env import load_local_env
load_local_env() load_local_env()
from .inbound.envelope import EnvelopeError, parse_envelope from .inbound.envelope import EnvelopeError, parse_envelope
from .inbound.memory_repo import MemorySalesGoodsRepo
from .inbound.repo import RepoUnavailable from .inbound.repo import RepoUnavailable
from .inbound.sales_goods import receive_sales_goods from .inbound.sales_goods import receive_sales_goods
app = FastAPI(title="洁达 RPA × 订单中心", version="0.1") app = FastAPI(title="洁达 RPA × 订单中心", version="0.1")
# 内存仓库做成进程内单例,同一进程多次 POST 才能测到 DUPLICATE。
_memory_repo = MemorySalesGoodsRepo()
def get_repo(): def get_repo():
# 默认走 Oracle。本机连不上库时 OMS_REPO=memory。 # 运行时只走 Oracle。单测用 dependency_overrides 注入内存替身。
if os.getenv("OMS_REPO", "oracle") == "memory":
return _memory_repo
# thick 客户端初始化有副作用,用到才 import。
from .inbound.oracle_repo import OracleSalesGoodsRepo from .inbound.oracle_repo import OracleSalesGoodsRepo
return OracleSalesGoodsRepo.from_env() return OracleSalesGoodsRepo.from_env()

15
rpa_rebate/README.md Normal file
View File

@ -0,0 +1,15 @@
# rpa_rebate
返利班。本包尚未实现。
| | |
| --- | --- |
| 读 | 表 3 / 表 4:已有合同号尚未回合同成功的单,以及只录协议的单 |
| 写 | 表 4:搜到合同号则 `contractStatus=2`;录协议后 `actionCode=2`、`agreementStatus=1` |
| 口 | 口 4 |
与合同同一天两班,建议合同班跑完再跑。查号挂在本班,不单开查状态任务。搜不到不重录合同,留给下一班再搜。
## 本期不做
返利中心查号、录协议、送审、接口 4 回写。

13
rpa_rebate/pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[project]
name = "rpa-rebate"
version = "0.1.0"
description = "洁达返利班。按合同号查号、录协议 → 表 4。本包尚未实现。"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
package = false
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

View File

@ -2,7 +2,7 @@
班次作业:从表 1 捞 `deal_flag=0`,按**条形码**搜 1 号药城 / 药帮忙,规则选出仓,结果写入表 2。不加购,不推订单中心。 班次作业:从表 1 捞 `deal_flag=0`,按**条形码**搜 1 号药城 / 药帮忙,规则选出仓,结果写入表 2。不加购,不推订单中心。
口径:`资料/RPA订单中心接口开发口径_20260930.md` 第 3 节。 口径:`docs/RPA订单中心接口开发口径_20260930.md` 第 3 节。
## 数据流 ## 数据流
@ -33,7 +33,7 @@ rpa_sales_goods_mapping_data (deal_flag=0)
## 环境 ## 环境
```bash ```bash
cd 选品流程 cd rpa_selection
uv sync --python 3.12 uv sync --python 3.12
uv run playwright install chromium uv run playwright install chromium
``` ```
@ -50,7 +50,7 @@ Oracle 与 `rpa_oms` 同一套变量:`RPA_ORACLE_HOST` / `PORT` / `SERVICE` /
## 运行 ## 运行
```bash ```bash
cd 选品流程 cd rpa_selection
uv run pytest -v uv run pytest -v
uv run python main.py --limit 5 uv run python main.py --limit 5
uv run python main.py --platform yaoex uv run python main.py --platform yaoex
@ -59,7 +59,7 @@ uv run python main.py --platform yaoex
Windows: Windows:
```powershell ```powershell
cd 选品流程 cd rpa_selection
uv sync --python 3.12 uv sync --python 3.12
Copy-Item -LiteralPath .env.example -Destination .env Copy-Item -LiteralPath .env.example -Destination .env
# 商城账号写本机 .env;Oracle 也可只留在 rpa_oms/.env # 商城账号写本机 .env;Oracle 也可只留在 rpa_oms/.env

View File

@ -1,7 +1,7 @@
[project] [project]
name = "jieda-procure" name = "rpa-selection"
version = "0.1.0" version = "0.1.0"
description = "洁达选品班次:表1 deal_flag=0 → 双平台搜品 → 写表2" description = "洁达选品班次(rpa_selection):表1 deal_flag=0 → 双平台搜品 → 写表2"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [

View File

@ -53,7 +53,7 @@ if (Test-Path -LiteralPath $venvPy) {
$uv = Get-Command uv -ErrorAction SilentlyContinue $uv = Get-Command uv -ErrorAction SilentlyContinue
if ($null -eq $uv) { if ($null -eq $uv) {
throw "找不到 uv,也没有 选品流程\.venv\Scripts\python.exe" throw "找不到 uv,也没有 rpa_selection\.venv\Scripts\python.exe"
} }
$uvArgs = @('run', '--python', '3.12', 'python') + $pyArgs $uvArgs = @('run', '--python', '3.12', 'python') + $pyArgs

View File

@ -384,35 +384,6 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "jieda-procure"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "oracledb" },
{ name = "playwright" },
{ name = "pycryptodome" },
{ name = "requests" },
{ name = "urllib3" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "oracledb", specifier = ">=2.4.1" },
{ name = "playwright", specifier = ">=1.49.0" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "requests", specifier = ">=2.34.2" },
{ name = "urllib3", specifier = ">=2.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0.0" }]
[[package]] [[package]]
name = "oracledb" name = "oracledb"
version = "26.0.1" version = "26.0.1"
@ -585,6 +556,35 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
] ]
[[package]]
name = "rpa-selection"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "oracledb" },
{ name = "playwright" },
{ name = "pycryptodome" },
{ name = "requests" },
{ name = "urllib3" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "oracledb", specifier = ">=2.4.1" },
{ name = "playwright", specifier = ">=1.49.0" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "requests", specifier = ">=2.34.2" },
{ name = "urllib3", specifier = ">=2.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0.0" }]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.16.0" version = "4.16.0"

View File

@ -1,191 +0,0 @@
# 艺赛旗联调包
这个目录是 Windows 和 macOS 共用的 RPA 包。状态机在 `state_center`,这里只负责回调外壳和节点 runner。
Mac 上先用 mock runner 把契约跑通:
```bash
# 状态中心已启动、DEV_MODE=true
python -m rpa_yisaiqi --create --auto-human
```
Agent 只听状态中心的当前节点和 `next_node`,不写死顺序。人工节点默认停下;加 `--auto-human` 才会替你提交待办。
## CONTRACT_CREATE 联调骨架
第一版目标是先验证:
1. Windows 虚拟机能访问状态中心。
2. `CONTRACT_CREATE` 能按约定调用 `start`。
3. 合同系统动作完成后能调用 `complete`,并带 `businessRefs.contractSystemNo`。
4. 登录失败、页面改版、合同禁用等异常能调用 `fail`。
## 合同节点步骤点
来自 `state_center/给艺赛旗的开发交接.md`:
1. 打开合同测试系统:`http://contract-test.gzmpc.com:8080/`
2. 登录:`test002 / 1`
3. 左侧菜单进入 `合同管理 > 合同起草`
4. 在最近使用模板中选择 `templateName`,非格式默认 `非格式--采购协议`
5. 填签约方信息:客户编码、客户名称
6. 填基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质
7. 填金额和日期,合同签订日期和合同开始日期必须等于订单日期
8. 填货品明细
9. 填付款信息与特殊标志:是否重大合同、是否预付款、归属年份、付款方式、付款期限、合同状态、是否包含血液制品
10. 医药城 `YYC`:同供应商同日 PDF 先合并成一个再上传
11. 小药药 `XYY`:格式合同,跳过合同证文上传
12. 提交并读取合同编号
13. 调状态中心 `complete`,必传 `businessRefs.contractSystemNo`
## 先做联通验证
在项目根目录执行:
```powershell
python -m rpa_yisaiqi.contract_create_node `
--state-center-url http://127.0.0.1:8000 `
--token dev-agent-token `
--job-id YYC-联调测试供应商-20260811-a6e5df `
--supplier-name 联调测试供应商 `
--order-date 2026-08-11 `
--source-type YYC `
--contract-system-no HT-20260811-001
```
这条只验证回调链路,合同号由命令行传入。
## 真实合同起草运行
客商必须用合同测试系统里**真实存在**的操作码。`CUST-YYC-001` 这类占位符会在选客商时搜不到,入参校验会直接拒绝。
测试环境当前可用:
| 字段 | 值 |
| --- | --- |
| customerCode | `DW0100046361` |
| customerName | `重庆亿昊药业有限公司` |
联调入参用:
- `examples/contract_create_input_dry_run.json`:真客商 + `dry_run`,先校准页面
- `examples/contract_draft_yyc.json`:医药城,同上客商
- `examples/contract_draft_xyy.json`:小药药分支(不上传 PDF),客商仍用合同系统里能搜到的同一条
YYC 有多份平台 PDF 时需要合并,先安装:
```powershell
pip install -r rpa_yisaiqi/requirements.txt
```
然后运行:
```powershell
python -m rpa_yisaiqi.contract_create_node `
--state-center-url http://192.168.x.x:8000 `
--token dev-agent-token `
--job-id <状态中心job_id> `
--supplier-name 重庆亿昊药业有限公司 `
--order-date 2026-08-11 `
--source-type YYC `
--draft-path rpa_yisaiqi/examples/contract_draft_yyc.json `
--merged-pdf-output C:/rpa/jobs/<状态中心job_id>/contract-merged.pdf
```
这条会走真实起草流程:
1. 状态中心 `/start`
2. 打开合同系统并登录
3. 进入 `合同管理 > 合同起草`
4. 选择最近使用模板
5. 填签约方、基础信息、金额日期、货品明细、付款和特殊标志
6. YYC 合并并上传 PDF,XYY 跳过上传
7. 提交
8. 读取合同编号
9. 状态中心 `/complete`
### 先做 dry-run 预览
如果你只想先验证页面,不想真的提交,把输入里的 `submitMode` 设成 `dry_run`。这时流程会跑到提交前并输出 `contract-dry-run-ready` 截图,不会点提交,也不会读取真实合同号。
适合先校准 selector 的情况:
1. 进入 `合同管理 > 合同起草`
2. 选择最近使用模板
3. 填签约方、基础信息、金额日期、明细、付款和特殊标志
4. 停在提交前
如果状态中心部署在 macOS 主机,`--state-center-url` 换成 Windows 虚拟机能访问到的地址,例如:
```powershell
--state-center-url http://192.168.x.x:8000
```
## 艺赛旗画布怎么看
Studio 里「插入代码块」标题改不了,会一直显示「插入代码块#1」。步骤名写在节点**备注**里,画布上出现在代码块下方,例如 `S06 填写签约方`。
流程按四段换行,不要再排成一条长龙:
```
准备 开始 → S01 初始化 → S02 打开系统 → S03 登录
进入起草 S04 进入起草 → S05 选择模板
填写表单 S06 签约方 → S07 基础信息 → S08 金额日期 → S09 货品 → S10 付款
提交回写 S11 上传PDF → S12 提交 → S13 读合同号 → S14 回调
```
改步骤后先关掉 Studio,再生成画布:
```powershell
python rpa_yisaiqi/build_multinode_seq.py
```
然后重新打开 `rpa_yisaiqi/studio` 项目并编译。
## 接入真实页面自动化
真实艺赛旗页面操作按流程图拆在:
- `flows/contract_create_flow.json`:流程图节点清单
- `contract_flow_steps.py`:每个流程图节点对应的动作函数
- `yisaiqi_adapter_template.py`:艺赛旗浏览器活动适配模板
艺赛旗设计器里建议按这个顺序拖流程图:
1. `S01_CALLBACK_START`:HTTP 请求状态中心 `/start`
2. `S02_OPEN_CONTRACT_SYSTEM`:打开合同系统
3. `S03_LOGIN`:输入 `j_username`、`j_password`,点击 `btn_submit`
4. `S04_ENTER_CONTRACT_DRAFT`:进入 `合同管理 > 合同起草`
5. `S05_SELECT_RECENT_TEMPLATE`:选择最近使用模板
6. `S06_FILL_SIGNATORY_INFO`:填客户编码、客户名称
7. `S07_FILL_CONTRACT_BASIC_FORM`:填合同类别、结算类型、背景概述、合同性质
8. `S08_FILL_AMOUNT_AND_DATES`:填金额、签订日期、开始日期、结束日期
9. `S09_FILL_GOODS_LINES`:循环填货品明细
10. `S10_FILL_PAYMENT_AND_FLAGS`:填付款方式、付款期限、合同状态、是否包含血液制品等
11. `S11_MERGE_PDF_IF_YYC`:`sourceType=YYC` 时先合并同供应商同日 PDF;`XYY` 跳过
12. `S12_UPLOAD_PDF_IF_YYC`:`sourceType=YYC` 时上传合并 PDF;`XYY` 跳过
13. `S13_SUBMIT`:提交
14. `S14_READ_CONTRACT_NO`:读取合同编号到变量 `contractSystemNo`
15. `S15_CALLBACK_COMPLETE`:HTTP 请求状态中心 `/complete`
如果只做预览,把这条流程里的 `submitMode` 设成 `dry_run`,或者直接导入 `flows/contract_create_preview_flow.json`。
如果艺赛旗支持 Python 脚本块,直接把每个 `contract_flow_steps.py` 函数挂到对应流程图节点。
如果艺赛旗只支持原生活动,就按 `flows/contract_create_flow.json` 的节点顺序拖活动,用 `yisaiqi_adapter_template.py` 里的方法名做映射。
外层回调骨架仍然在 `contract_create_node.py`:
```python
create_contract_in_contract_system(spec)
```
这个函数最后返回合同编号字符串即可。状态中心回调、幂等键、错误码映射已经封在外层。后续接真页面时,可以在这里调用艺赛旗流程图,或让流程图最后把合同编号回传给这个函数。
## 异常映射
| 场景 | 异常类 | 回调错误码 | retryable | routeRole |
| --- | --- | --- | --- | --- |
| 登录失败 | `LoginFailed` | `SYS_LOGIN_FAIL` | true | OPS |
| 供应商合同被禁/失效 | `SupplierContractDisabled` | `BIZ_CONTRACT_DISABLED` | false | PURCHASER |
| PDF 合并能力不支持 | `PdfMergeUnsupported` | `CAP_PDF_MERGE_UNSUPPORTED` | false | OPS |
| 页面改版/元素找不到 | `ContractCreateError` | `UI_ELEMENT_NOT_FOUND` | false | OPS |

View File

@ -1,2 +0,0 @@
"""YiSaiQi RPA integration helpers."""

View File

@ -1,59 +0,0 @@
"""Run the portable agent against a state center.
Examples:
python -m rpa_yisaiqi --create --auto-human
python -m rpa_yisaiqi --job-id YYC-xxx --auto-human
"""
from __future__ import annotations
import argparse
import os
from datetime import datetime, timedelta
from uuid import uuid4
from .agent import JobAgent
from .callback_client import StateCenterClient
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Drive one job through the state center.")
parser.add_argument("--state-center-url", default=os.getenv("STATE_CENTER_URL", "http://127.0.0.1:8000"))
parser.add_argument("--token", default=os.getenv("AGENT_TOKEN", "dev-agent-token"))
parser.add_argument("--job-id")
parser.add_argument("--create", action="store_true", help="create a new job before running")
parser.add_argument("--source-type", choices=("YYC", "XYY", "OFFLINE"), default="YYC")
parser.add_argument("--supplier-name", default=None)
parser.add_argument("--order-date", default=None)
parser.add_argument("--auto-human", action="store_true")
parser.add_argument("--auto-retry", action="store_true")
parser.add_argument("--load-fixture-goods", action="store_true", default=True)
parser.add_argument("--no-load-fixture-goods", action="store_false", dest="load_fixture_goods")
return parser.parse_args()
def main() -> None:
args = parse_args()
client = StateCenterClient(args.state_center_url, args.token)
job_id = args.job_id
if args.create or not job_id:
order_date = args.order_date or (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
supplier = args.supplier_name or f"重庆亿昊药业-{uuid4().hex[:4]}"
created = client.create_job(args.source_type, order_date, supplier, created_by="rpa-agent")
job_id = created["job_id"]
print(f"created job {job_id}")
if args.load_fixture_goods:
loaded = client.load_fixture_goods()
print(f"fixture goods loaded: {loaded.get('loaded')}")
history = JobAgent(
client,
job_id,
runner_mode="mock",
auto_human=args.auto_human,
auto_retry=args.auto_retry,
).run()
last = history[-1]
print(f"job {job_id} stopped={last.get('stopped')} next_node={last.get('next_node')} steps={len(history)}")
if __name__ == "__main__":
main()

View File

@ -1,17 +0,0 @@
# coding=utf-8
"""多节点流程图(Main.seq)的跨节点共享状态容器。
艺赛旗编译 Main.seq 后,各代码节点的 PBCode 会在同一 Python 进程中执行。
本模块以「模块级全局变量」承载 browser / ctx / client / spec 等对象,
避免依赖艺赛旗对局部变量的拼接方式,让每个节点都能可靠地取到上一步的产物。
"""
spec = None
client = None
ctx = None
browser = None
submitted = False
contract_no = ""
fd_id = ""
is_dry_run = False
completed = False

View File

@ -1,125 +0,0 @@
"""Drive one job by asking the state center where it is, then acting."""
from __future__ import annotations
import uuid
from typing import Any
from .runners import mock as mock_runners
from .runtime.shell import execute_node
HUMAN_NODES = {"SOURCE_ADD", "M5_MANUAL_CONFIRM", "M5_RECEIPT_NOTICE"}
class JobAgent:
def __init__(
self,
client: Any,
job_id: str,
*,
runner_mode: str = "mock",
auto_human: bool = False,
auto_retry: bool = False,
operator: str = "mock-agent",
):
self.client = client
self.job_id = job_id
self.runner_mode = runner_mode
self.auto_human = auto_human
self.auto_retry = auto_retry
self.operator = operator
def step(self) -> dict[str, Any]:
detail = self.client.get_job(self.job_id)
job = detail["job"]
tasks = detail.get("tasks") or []
status = job.get("status")
node = job.get("current_node_code")
if status == "SUCCESS":
return {"stopped": "SUCCESS", "next_node": None}
if status == "FAILED_RETRYABLE":
if not self.auto_retry:
return {"stopped": "FAILED_RETRYABLE"}
return self.client.retry_job(self.job_id)
if status == "WAITING_USER" or node in HUMAN_NODES:
if not self.auto_human:
return {"stopped": "WAITING_USER"}
return self._submit_human(job, tasks)
if node == "PRODUCT_MATCH":
return self.client.match_run(self.job_id)
if node == "CONTRACT_APPROVE_WAIT":
return self.client.check_contract_result(self.job_id, True, operator=self.operator)
if node == "ARCHIVE_JOB":
return {"stopped": "SUCCESS", "next_node": None}
return execute_node(
self.client,
self.job_id,
node,
lambda: self._run(node, job),
attempt_no=int(job.get("attempt_no") or 1),
operator=self.operator,
)
def _run(self, node_code: str, job: dict[str, Any]) -> dict[str, Any]:
if self.runner_mode != "mock":
raise RuntimeError(f"unsupported runner_mode {self.runner_mode}")
return mock_runners.run(node_code, job, self.client)
def _submit_human(self, job: dict[str, Any], tasks: list[dict[str, Any]]) -> dict[str, Any]:
node = job["current_node_code"]
task = _open_task(tasks, node)
if task is None and hasattr(self.client, "list_tasks"):
listed = self.client.list_tasks(status="OPEN")
task = _open_task(listed.get("items") or [], node, self.job_id)
if task is None:
return {"stopped": "WAITING_USER", "reason": "no_open_task"}
action = "CONFIRM_AND_CONTINUE"
payload: dict[str, Any] = {}
if node == "PRODUCT_MATCH":
action = "SELECT_CANDIDATE"
payload = {
"productMatchId": task.get("product_match_id"),
"goodsId": "G-001",
"goodsCode": "YP-001",
"baseUnit": "盒",
"saveAsAlias": True,
}
elif node == "M5_RECEIPT_NOTICE":
payload = {"receiptNoticeNo": f"SH{uuid.uuid4().hex[:8].upper()}"}
return self.client.submit_task(
task["task_id"],
action,
payload,
submit_token=uuid.uuid4().hex,
operator=self.operator,
)
def run(self, *, max_steps: int = 40) -> list[dict[str, Any]]:
history: list[dict[str, Any]] = []
for _ in range(max_steps):
result = self.step()
history.append(result)
if result.get("stopped") in {"SUCCESS", "WAITING_USER", "FAILED_RETRYABLE", "FAILED_MANUAL"}:
return history
if result.get("waiting_user"):
continue
if result.get("next_node") is None and result.get("ok"):
detail = self.client.get_job(self.job_id)
if detail["job"].get("status") == "SUCCESS":
history.append({"stopped": "SUCCESS", "next_node": None})
return history
raise RuntimeError(f"job {self.job_id} exceeded {max_steps} agent steps")
def _open_task(tasks: list[dict[str, Any]], node_code: str, job_id: str | None = None):
for task in tasks:
if task.get("node_code") != node_code:
continue
if task.get("status") not in (None, "OPEN"):
continue
if job_id and task.get("job_id") not in (None, job_id):
continue
return task
return None

View File

@ -1,358 +0,0 @@
# coding=utf-8
"""
构建合同建单 Main.seq —— 按业务段换行,画布上能直接读出步骤。
艺赛旗「插入代码块」的标题固定为「插入代码块#1」,「业务流程描述」节点
打开后再保存标题会被清空。画布上真正能看见的是代码块的备注(NOTE)。
因此本脚本:
1. 不再插入空的业务流程描述节点
2. 把「S06 填写签约方」写进 NOTE,作为画布副标题
3. 按 准备 / 进入起草 / 填写表单 / 提交回写 四段换行
关闭 Studio 后运行本脚本,再重新打开项目编译。
"""
from __future__ import annotations
import base64
import json
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT = HERE / "studio" / "Main.seq"
COL_W = 280
ROW_H = 200
ORIGIN_X = 360
ORIGIN_Y = 220
BEGIN_TAG = "202608141000000001"
def b64_apa(val):
# 艺赛旗 EP_CONTROL / EP_Gen 字段值末尾带换行,缺失会导致字段解析失败
return json.dumps({"val": base64.b64encode(str(val).encode()).decode()}, indent=4) + "\n"
def b64_raw(val):
return base64.b64encode(str(val).encode()).decode()
STEP_INIT = '''import sys, os, json
from pathlib import Path
_REPO_ROOT = next(
p for p in Path(__file__).resolve().parents
if (p / "rpa_yisaiqi" / "__init__.py").exists()
)
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_create_node import ContractCreateError, load_input_data, build_spec_from_data
from rpa_yisaiqi.callback_client import StateCenterClient
from rpa_yisaiqi.apa_browser_adapter import ApaBrowserAdapter
def _first(*values):
for value in values:
if value is not None and str(value).strip():
return str(value).strip()
return ""
raw = (self.input_arg or "").strip()
if not raw:
raw = os.getenv("CONTRACT_INPUT_JSON", "").strip()
if not raw:
raw = str(_REPO_ROOT / "rpa_yisaiqi" / "examples" / "contract_create_input_dry_run.json")
print("未传入 -i/--input,使用默认联调入参:", raw)
if not raw:
raise ContractCreateError("缺少业务入参:请通过 -i/--input 传入 JSON 字符串或文件路径")
data = load_input_data(raw)
state_center_url = _first(
data.get("stateCenterUrl"), data.get("stateCenterURL"),
os.getenv("STATE_CENTER_URL"), "http://192.168.105.69:8000",
)
agent_token = _first(data.get("agentToken"), os.getenv("AGENT_TOKEN"), "dev-agent-token")
_st.spec = build_spec_from_data(data)
_st.client = StateCenterClient(state_center_url, agent_token)
_st.ctx = _st.spec.flow_context
_st.browser = ApaBrowserAdapter(url=_st.ctx.contract_url, flow="CONTRACT_CREATE")
_st.is_dry_run = (_st.spec.submit_mode == "dry_run")
if not _st.is_dry_run:
print("回调状态中心 start", _st.spec.job_id)
_st.client.start_node(_st.spec.job_id, "CONTRACT_CREATE")'''
STEP_OPEN = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import open_contract_system
open_contract_system(_st.browser, _st.ctx)'''
STEP_LOGIN = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import login_contract_system
login_contract_system(_st.browser, _st.ctx)'''
STEP_ENTER = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import enter_contract_create
enter_contract_create(_st.browser)'''
STEP_TEMPLATE = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import select_contract_template
select_contract_template(_st.browser, _st.ctx)'''
STEP_SIGNATORY = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import fill_contract_signatory_info
fill_contract_signatory_info(_st.browser, _st.ctx)'''
STEP_BASIC = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import fill_contract_basic_info
fill_contract_basic_info(_st.browser, _st.ctx)'''
STEP_AMOUNT = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import fill_contract_amount_and_dates
fill_contract_amount_and_dates(_st.browser, _st.ctx)'''
STEP_GOODS = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import fill_contract_goods_lines
fill_contract_goods_lines(_st.browser, _st.ctx)'''
STEP_PAYMENT = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import fill_contract_payment_info
fill_contract_payment_info(_st.browser, _st.ctx)'''
STEP_UPLOAD = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import upload_contract_document_if_needed
upload_contract_document_if_needed(_st.browser, _st.ctx)'''
STEP_SUBMIT = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import submit_contract_for_approval
_st.submitted = submit_contract_for_approval(_st.browser, _st.ctx)'''
STEP_READ_NO = '''import rpa_yisaiqi._flow_state as _st
from rpa_yisaiqi.contract_flow_steps import read_contract_number
if _st.submitted:
_st.contract_no = read_contract_number(_st.browser, _st.ctx)'''
STEP_COMPLETE = '''import json
import rpa_yisaiqi._flow_state as _st
if not _st.is_dry_run and _st.contract_no:
print("回调状态中心 complete", _st.spec.job_id, _st.contract_no)
_st.client.complete_node(_st.spec.job_id, "CONTRACT_CREATE", {
"businessRefs": {"contractSystemNo": _st.contract_no},
})
print(json.dumps({"status": "done", "contractSystemNo": _st.contract_no}, ensure_ascii=False))'''
# 每段一行。NOTE 会显示在画布「插入代码块#1」下方,务必写短步骤名。
PHASES = [
(
"准备",
[
("S01 初始化并回调 start", STEP_INIT),
("S02 打开合同系统", STEP_OPEN),
("S03 登录合同系统", STEP_LOGIN),
],
),
(
"进入起草",
[
("S04 进入合同起草", STEP_ENTER),
("S05 选择模板", STEP_TEMPLATE),
],
),
(
"填写表单",
[
("S06 填写签约方", STEP_SIGNATORY),
("S07 填写基础信息", STEP_BASIC),
("S08 填写金额日期", STEP_AMOUNT),
("S09 填写货品明细", STEP_GOODS),
("S10 填写付款信息", STEP_PAYMENT),
],
),
(
"提交回写",
[
("S11 上传合同PDF", STEP_UPLOAD),
("S12 提交送审", STEP_SUBMIT),
("S13 读取合同编号", STEP_READ_NO),
("S14 回调 complete", STEP_COMPLETE),
],
),
]
def code_tag(i: int) -> str:
return f"202608143{i:02d}0000000"
def line_tag(i: int) -> str:
return f"202608149{i:02d}0000000"
def _iter_steps():
index = 1
for phase_name, items in PHASES:
for title, code in items:
yield index, phase_name, title, code
index += 1
def build_begin_node(px: int, py: int) -> dict:
return {
"NodePanelData": {
"ExpandedGroups": [],
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
"PubProperty": "",
"RetPins": [],
},
"NodeProperty": "",
"NodeUI": {
"NodeElementID": "",
"NodeModel": "2",
"NodePropErr": False,
"NodePx": str(px),
"NodePy": str(py),
"NodeSubType": "EnumWidget_Begin",
"NodeTag": BEGIN_TAG,
"NodeTitle": "开始",
},
}
def build_code_node(tag: str, title: str, code: str, px: int, py: int) -> dict:
body = f'print("[{title}]")\n' + code
return {
"NodePanelData": {
"ExpandedGroups": [],
"InputProperty": {"mouse_x": 0, "mouse_y": 0},
"PubProperty": "",
"RetPins": [],
},
"NodeProperty": {
"EP_CONTROL": {
"EP_CONTROL_IMPORT": json.dumps(
{"PImport": "", "PBCode": b64_raw(body)}, indent=4
) + "\n"
},
"EP_Gen": {
"EP_Gen_AFTERDELAY": b64_apa("0"),
"EP_Gen_BEFOREDELAY": b64_apa("0"),
"EP_Gen_NOTE": b64_apa(title),
},
},
"NodeUI": {
"NodeElementID": "",
"NodeModel": "2",
"NodePropErr": False,
"NodePx": str(px),
"NodePy": str(py),
"NodeSubType": "EnumWidget_Code_Custom",
"NodeTag": tag,
"NodeTitle": title,
},
}
def build_nodes():
nodes = []
placed = [] # (tag, x, y)
begin_x, begin_y = ORIGIN_X, ORIGIN_Y
nodes.append(build_begin_node(begin_x, begin_y))
placed.append((BEGIN_TAG, begin_x, begin_y))
# 第一段从「开始」右侧接着排;后续每段单独一行
col = 1
row = 0
current_phase = None
for index, phase_name, title, code in _iter_steps():
if current_phase is None:
current_phase = phase_name
elif phase_name != current_phase:
current_phase = phase_name
row += 1
col = 0
x = ORIGIN_X + col * COL_W
y = ORIGIN_Y + row * ROW_H
tag = code_tag(index)
nodes.append(build_code_node(tag, title, code, x, y))
placed.append((tag, x, y))
col += 1
return nodes, placed
def build_lines(placed: list) -> list:
lines = []
for k in range(len(placed) - 1):
prev_tag, prev_x, prev_y = placed[k]
next_tag, next_x, next_y = placed[k + 1]
same_row = prev_y == next_y
if same_row:
start_x, start_y = prev_x + 150, prev_y + 40
end_x, end_y = next_x - 4, next_y + 40
else:
start_x, start_y = prev_x + 70, prev_y + 90
end_x, end_y = next_x + 70, next_y - 8
lines.append({
"LineColor": "255, 255, 255",
"LineCommented": 0,
"LineEnd": {
"Pidx": f"{next_tag}/EPin_Flow_In",
"Px": str(end_x),
"Py": str(end_y),
},
"LineNote": "",
"LineSplit": {"Px": "", "Py": ""},
"LineStart": {
"Pidx": f"{prev_tag}/EPin_Flow_Out",
"Px": str(start_x),
"Py": str(start_y),
},
"LineTag": line_tag(k + 1),
"LineType": 0,
})
return lines
def main() -> None:
nodes, placed = build_nodes()
xs = [x for _, x, _ in placed]
ys = [y for _, _, y in placed]
main_seq = {
"LineList": build_lines(placed),
"NodeList": nodes,
"SeqTag": "Main",
"ViewCenterPx": str((min(xs) + max(xs)) // 2),
"ViewCenterPy": str((min(ys) + max(ys)) // 2),
"ViewScaling": "0.7",
}
OUT.write_text(json.dumps(main_seq, ensure_ascii=False, indent=4), encoding="utf-8")
print(f"多节点 Main.seq 已生成:{OUT}")
print(f"节点数:{len(nodes)}(1 开始 + {len(placed) - 1} 代码块,已去掉空业务标识)")
print(f"连线数:{len(main_seq['LineList'])}")
current_phase = None
for index, phase_name, title, _ in _iter_steps():
if phase_name != current_phase:
current_phase = phase_name
print(f" [{phase_name}]")
print(f" {title}")
if __name__ == "__main__":
main()

View File

@ -1,166 +0,0 @@
"""HTTP client for YiSaiQi callbacks into the state center."""
from __future__ import annotations
import json
from typing import Any, Callable, Dict
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
JsonDict = Dict[str, Any]
Opener = Callable[[Request, float], Any]
class StateCenterError(RuntimeError):
def __init__(self, status: int, detail: Any):
super().__init__(f"state center returned {status}: {detail}")
self.status = status
self.detail = detail
class StateCenterClient:
def __init__(
self,
base_url: str,
token: str,
*,
timeout: float = 15,
opener: Opener | None = None,
):
self.base_url = base_url.rstrip("/")
self.token = token
self.timeout = timeout
self._opener = opener or _urlopen_with_timeout
def start_node(
self,
job_id: str,
node_code: str,
*,
attempt_no: int = 1,
operator: str = "agent",
) -> JsonDict:
return self._post(
f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/start",
{
"attemptNo": attempt_no,
"executorPlatform": "iS-RPA",
"operator": operator,
},
)
def complete_node(
self,
job_id: str,
node_code: str,
payload: JsonDict,
) -> JsonDict:
return self._post(f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/complete", payload)
def fail_node(
self,
job_id: str,
node_code: str,
payload: JsonDict,
) -> JsonDict:
return self._post(f"/api/jobs/{_path_part(job_id)}/nodes/{_path_part(node_code)}/fail", payload)
def get_job(self, job_id: str) -> JsonDict:
return self._request("GET", f"/api/jobs/{_path_part(job_id)}")
def ingest_items(self, job_id: str, rows: list[JsonDict], replace: bool = True) -> JsonDict:
return self._post(
f"/api/jobs/{_path_part(job_id)}/items",
{"replace": replace, "rows": rows},
)
def match_run(self, job_id: str) -> JsonDict:
return self._post(f"/api/jobs/{_path_part(job_id)}/match/run", {})
def load_fixture_goods(self) -> JsonDict:
return self._post("/api/dev/match/load-fixture-goods", {})
def retry_job(self, job_id: str) -> JsonDict:
return self._post(f"/api/jobs/{_path_part(job_id)}/retry", {})
def list_tasks(self, *, role_code: str | None = None, status: str | None = None) -> JsonDict:
query = []
if role_code:
query.append(f"roleCode={quote(role_code, safe='')}")
if status:
query.append(f"status={quote(status, safe='')}")
suffix = f"?{'&'.join(query)}" if query else ""
return self._request("GET", f"/api/tasks/my{suffix}")
def submit_task(
self,
task_id: str,
action: str,
payload: JsonDict,
submit_token: str,
operator: str = "agent",
) -> JsonDict:
return self._post(
f"/api/tasks/{_path_part(task_id)}/submit",
{
"action": action,
"payload": payload,
"submitToken": submit_token,
"operator": operator,
},
)
def check_contract_result(
self,
job_id: str,
approved: bool,
*,
operator: str = "agent",
detail: str | None = None,
) -> JsonDict:
return self._post(
f"/api/jobs/{_path_part(job_id)}/nodes/CONTRACT_APPROVE_WAIT/check-result",
{"approved": approved, "operator": operator, "detail": detail},
)
def create_job(self, source_type: str, order_date: str, supplier_name: str, created_by: str = "agent") -> JsonDict:
return self._post(
"/api/jobs",
{
"source_type": source_type,
"order_date": order_date,
"supplier_name": supplier_name,
"created_by": created_by,
},
)
def _post(self, path: str, payload: JsonDict) -> JsonDict:
return self._request("POST", path, payload)
def _request(self, method: str, path: str, payload: JsonDict | None = None) -> JsonDict:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
headers = {"X-Agent-Token": self.token}
if data is not None:
headers["Content-Type"] = "application/json"
request = Request(
f"{self.base_url}{path}",
data=data,
headers=headers,
method=method,
)
try:
with self._opener(request, self.timeout) as response:
raw = response.read().decode("utf-8")
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise StateCenterError(exc.code, detail) from exc
return json.loads(raw) if raw else {}
def _path_part(value: str) -> str:
return quote(value, safe="")
def _urlopen_with_timeout(request: Request, timeout: float) -> Any:
return urlopen(request, timeout=timeout)

View File

@ -1,151 +0,0 @@
{
"flowName": "CONTRACT_CREATE_合同建单",
"platform": "YiSaiQi iS-RPA",
"nodeCode": "CONTRACT_CREATE",
"inputs": [
"stateCenterUrl",
"agentToken",
"jobId",
"supplierName",
"orderDate",
"sourceType",
"templateType",
"templateName",
"customerCode",
"customerName",
"contractCategory",
"settlementType",
"contractBackground",
"amount",
"contractEndDate",
"paymentMethod",
"paymentDays",
"submitMode",
"goodsLines",
"mergedPdfPath"
],
"outputs": [
"contractSystemNo",
"nextNode"
],
"steps": [
{
"id": "S01_CALLBACK_START",
"name": "回调状态中心-start",
"script": "StateCenterClient.start_node(jobId, 'CONTRACT_CREATE')"
},
{
"id": "S02_OPEN_CONTRACT_SYSTEM",
"name": "打开合同系统",
"script": "open_contract_system(browser, ctx)"
},
{
"id": "S03_LOGIN",
"name": "登录合同系统",
"script": "login_contract_system(browser, ctx)",
"selectors": {
"username": "input[name='j_username']",
"password": "input[name='j_password']",
"submit": "input[name='btn_submit']"
}
},
{
"id": "S04_ENTER_CONTRACT_DRAFT",
"name": "进入合同起草",
"script": "enter_contract_create(browser)"
},
{
"id": "S05_SELECT_RECENT_TEMPLATE",
"name": "选择最近使用模板",
"script": "select_contract_template(browser, ctx)",
"rule": "优先点击最近使用模板中的 templateName;非格式默认非格式--采购协议"
},
{
"id": "S06_FILL_SIGNATORY_INFO",
"name": "填写签约方信息",
"script": "fill_contract_signatory_info(browser, ctx)",
"fields": [
"客户编码",
"客户名称"
]
},
{
"id": "S07_FILL_CONTRACT_BASIC_FORM",
"name": "填写合同基础表单",
"script": "fill_contract_basic_info(browser, ctx)",
"fields": [
"合同版本类型",
"合同类别",
"结算类型",
"合同背景概述",
"合同性质"
]
},
{
"id": "S08_FILL_AMOUNT_AND_DATES",
"name": "填写金额与日期",
"script": "fill_contract_amount_and_dates(browser, ctx)",
"hardRules": [
"合同签订日期=orderDate",
"合同开始日期=orderDate"
]
},
{
"id": "S09_FILL_GOODS_LINES",
"name": "填写货品明细",
"script": "fill_contract_goods_lines(browser, ctx)"
},
{
"id": "S10_FILL_PAYMENT_AND_FLAGS",
"name": "填写付款信息与特殊标志",
"script": "fill_contract_payment_info(browser, ctx)"
},
{
"id": "S11_MERGE_PDF_IF_YYC",
"name": "医药城合并同供应商同日PDF",
"script": "ensure_merged_pdf(platformPdfPaths, mergedPdfOutput)",
"branch": "sourceType=YYC 且 platformPdfPaths 多于一份时执行;单份直接沿用;sourceType=XYY 跳过"
},
{
"id": "S12_UPLOAD_PDF_IF_YYC",
"name": "医药城上传合并合同PDF",
"script": "upload_contract_document_if_needed(browser, ctx)",
"branch": "sourceType=YYC 才执行;sourceType=XYY 跳过"
},
{
"id": "S13_SUBMIT",
"name": "提交",
"script": "submit_contract_for_approval(browser)"
},
{
"id": "S14_READ_CONTRACT_NO",
"name": "读取合同编号",
"script": "read_contract_number(browser)"
},
{
"id": "S15_CALLBACK_COMPLETE",
"name": "回调状态中心-complete",
"script": "StateCenterClient.complete_node(jobId, 'CONTRACT_CREATE', businessRefs.contractSystemNo)"
},
{
"id": "E_LOGIN_FAILED",
"name": "异常-登录失败",
"script": "StateCenterClient.fail_node(..., errorCode='SYS_LOGIN_FAIL', retryable=true, routeRole='OPS')"
},
{
"id": "E_CONTRACT_DISABLED",
"name": "异常-供应商合同禁用",
"script": "StateCenterClient.fail_node(..., errorCode='BIZ_CONTRACT_DISABLED', retryable=false, routeRole='PURCHASER')"
},
{
"id": "E_PDF_MERGE_UNSUPPORTED",
"name": "异常-PDF合并能力不支持",
"script": "StateCenterClient.fail_node(..., errorCode='CAP_PDF_MERGE_UNSUPPORTED', retryable=false, routeRole='OPS')"
},
{
"id": "E_PAGE_CHANGED",
"name": "异常-页面改版",
"script": "StateCenterClient.fail_node(..., errorCode='UI_ELEMENT_NOT_FOUND', retryable=false, routeRole='OPS')"
}
]
}

View File

@ -1,42 +0,0 @@
{
"flowName": "CONTRACT_CREATE_预览",
"platform": "YiSaiQi iS-RPA",
"nodeCode": "CONTRACT_CREATE",
"inputs": [
"stateCenterUrl",
"agentToken",
"jobId",
"supplierName",
"orderDate",
"sourceType",
"templateType",
"templateName",
"customerCode",
"customerName",
"contractCategory",
"settlementType",
"contractBackground",
"amount",
"contractEndDate",
"paymentMethod",
"paymentDays",
"submitMode",
"goodsLines",
"mergedPdfPath"
],
"outputs": [
"previewDone"
],
"steps": [
{ "id": "P01", "name": "打开合同系统", "script": "open_contract_system(browser, ctx)" },
{ "id": "P02", "name": "登录合同系统", "script": "login_contract_system(browser, ctx)" },
{ "id": "P03", "name": "进入合同起草", "script": "enter_contract_create(browser)" },
{ "id": "P04", "name": "选择模板", "script": "select_contract_template(browser, ctx)" },
{ "id": "P05", "name": "填写签约方", "script": "fill_contract_signatory_info(browser, ctx)" },
{ "id": "P06", "name": "填写基础表单", "script": "fill_contract_basic_info(browser, ctx)" },
{ "id": "P07", "name": "填写金额日期", "script": "fill_contract_amount_and_dates(browser, ctx)" },
{ "id": "P08", "name": "填写明细", "script": "fill_contract_goods_lines(browser, ctx)" },
{ "id": "P09", "name": "填写付款与标志", "script": "fill_contract_payment_info(browser, ctx)" },
{ "id": "P10", "name": "预览结束", "script": "submit_contract_for_approval(browser, ctx) with submitMode=dry_run" }
]
}

View File

@ -1,4 +0,0 @@
pypdf==5.1.0
selenium>=4.15.0
webdriver-manager>=4.0.0
requests>=2.31.0

View File

@ -1 +0,0 @@
"""Node runners. Mock now, Windows page runners later."""

Some files were not shown because too many files have changed in this diff Show More