merge: 合入远程 oms + 四作业包布局
保留本机 .env 启动、审计字段和 deal_result GBK 截断。 选品目录按远程迁到 rpa_selection。
This commit is contained in:
commit
fa4df0f27b
|
|
@ -2,6 +2,8 @@
|
|||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
!rpa_selection/.vscode/
|
||||
!rpa_selection/.vscode/**
|
||||
*.swp
|
||||
|
||||
# Python
|
||||
|
|
@ -17,11 +19,10 @@ __pycache__/
|
|||
|
||||
# Node
|
||||
node_modules/
|
||||
state_center_web/dist/
|
||||
|
||||
# Local runtime / agent
|
||||
.runtime/
|
||||
rpa_yisaiqi/screenshots/
|
||||
rpa_contract/screenshots/
|
||||
rpa-chrome-profile/
|
||||
**/chrome-profile/
|
||||
.tmp/
|
||||
|
|
@ -30,17 +31,15 @@ rpa-chrome-profile/
|
|||
.claude/
|
||||
.superpowers/
|
||||
|
||||
# Duplicate / scratch
|
||||
state_center_副本/
|
||||
|
||||
# Local M5 credentials
|
||||
state_center/fixtures/m5/connection.local.json
|
||||
.env
|
||||
.env.*
|
||||
!.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)
|
||||
*.mp4
|
||||
|
|
|
|||
55
AGENTS.md
55
AGENTS.md
|
|
@ -1,6 +1,6 @@
|
|||
# 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 靠下面这张地图定点打开,禁止用扫仓代替思考。改对应了,反工和消耗都会少。
|
||||
|
||||
|
|
@ -21,27 +21,25 @@
|
|||
| 接口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 入口 / 鉴权 | `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` |
|
||||
| 选品班次落表 | `选品流程/job.py`、`persist.py`、`repo.py` | `选品流程/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_yisaiqi/contract_draft.py` | `state_center/tests/test_yisaiqi_contract_draft.py::点名` |
|
||||
| 节点入口、回调 complete | `rpa_yisaiqi/contract_create_node.py`、`callback_client.py` | 回调 JSON 含 `contractSystemNo` |
|
||||
| 浏览器适配 | `rpa_yisaiqi/apa_browser_adapter.py` | `rpa_yisaiqi/tests/test_apa_browser_adapter.py::点名` |
|
||||
| 文字验收 / 截图策略 | `rpa_yisaiqi/runtime/accept.py` | 只读 `ACCEPT.txt`,禁止读 png |
|
||||
| 艺赛旗画布代码块 | `rpa_yisaiqi/build_multinode_seq.py`(源)→ `studio/`(编译产物) | 未说画布不要改这两处 |
|
||||
| 跨节点共享状态 | `rpa_yisaiqi/_flow_state.py` | — |
|
||||
| 状态机 / API | `state_center/app/main.py`、`web_state_machine.py`、`match_engine.py` | `state_center/tests/test_web_state_machine.py::点名` 等 |
|
||||
| 前端看板 | `state_center_web/src/stateMachine.ts`、`App.tsx` | 浏览器点一遍相关页 |
|
||||
| 一键启动(状态中心) | `scripts/dev.py start` | 状态中心 :8000,前端 :5173 |
|
||||
| 节点业务规则(点名一节) | `资料/采购主链RPA一期实施与验收规范.md` | 禁止通读 `资料/`、`docs/` |
|
||||
| 选品规则 / 拆量 | `rpa_selection/rules.py`、`pipeline.py` | `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` |
|
||||
| 选品班次落表 | `rpa_selection/job.py`、`persist.py`、`repo.py` | `rpa_selection/tests/test_job.py::test_job_searches_barcode_not_approval_no` |
|
||||
| 合同页面步骤 | `rpa_contract/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt` |
|
||||
| 合同入参 / 客商草稿 | `rpa_contract/contract_draft.py` | `rpa_contract` 点名用例 |
|
||||
| 节点入口 | `rpa_contract/contract_create_node.py`、`main.py` | 读到正式合同号 |
|
||||
| 浏览器适配 | `rpa_contract/apa_browser_adapter.py` | `rpa_contract/tests/test_apa_browser_adapter.py::点名` |
|
||||
| 文字验收 / 截图策略 | `rpa_contract/accept.py` | 只读 `ACCEPT.txt`,禁止读 png |
|
||||
| 加购 / 支付回传 | `rpa_cart/README.md` | 占位,未实现 |
|
||||
| 返利班 | `rpa_rebate/README.md` | 占位,未实现 |
|
||||
| 四个口地图 | `rpa_oms/README.md`、根 `README.md` | — |
|
||||
| 节点业务规则(点名一节) | `docs/RPA订单中心接口开发口径_20260930.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:销售商品对码数据接收。`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`)。
|
||||
|
||||
修接口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`。
|
||||
|
||||
|
|
@ -76,11 +74,11 @@
|
|||
|
||||
湖南壹号、甘肃壹号云不在自营集合。不加购,不推接口 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 落点
|
||||
|
||||
`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_SUBMIT` | RPA | `submitted` | 点提交送审 |
|
||||
| `M5_CREATE_ORDER` | RPA 桌面端 | `m5OrderNo` | 录单后**停、不保存**。`M5_QC_MISSING` 必须识别准(直接转人工、不重试) |
|
||||
| `M5_MANUAL_CONFIRM` | 采购 | — | 人在 M5 里保存签订;前端任务推进 |
|
||||
| `M5_MANUAL_CONFIRM` | 采购 | — | 人在 M5 里保存签订 |
|
||||
| `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`;禁止为「显得全面」跑整包再去连真库。
|
||||
- 选品:点名 `选品流程/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
||||
- 合同页:只读控制台 `ACCEPT` 或 `screenshots/<节点>/ACCEPT.txt`。
|
||||
- 单测只跑点名用例,例如 `state_center/tests/test_yisaiqi_contract_flow_steps.py::test_read_contract_number_falls_back_to_my_contracts_list`。禁止跑整份该文件。
|
||||
- 该文件已打掉 `time.sleep`;不要再拿全量带睡的套件当回归。
|
||||
- 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
||||
- 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
|
||||
- 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
|
||||
|
||||
## 不要
|
||||
|
||||
- 不要扫根目录,不要通读 `资料/`、`docs/`。需要背景就点名打开一节。
|
||||
- 不要顺手改 `studio/`、`build_multinode_seq.py`(画布任务除外)。
|
||||
- 不要扫根目录,不要通读 `docs/`。需要背景就点名打开一节。
|
||||
- 不要把 Oracle 密码写进仓库、README、示例 JSON。
|
||||
- 不要在接口1 请求路径里跑选品。
|
||||
- 不要为「显得全面」多开文件、重跑、看图。落点外的改动会制造反工。
|
||||
|
|
|
|||
|
|
@ -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 天一起联调。
|
||||
|
||||
---
|
||||
|
||||
要我把这份文档发给艺赛旗吗?或者你先看看有没有要调整的。
|
||||
|
|
@ -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 日的四流程稿也是归档。作业语义以口径为准。
|
||||
Binary file not shown.
|
|
@ -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 月设计稿,同样视为归档。
|
||||
|
|
@ -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 的鉴权
|
||||
- 只凑到一部分数量时,多平台报文怎么组
|
||||
- 返利测试环境确认:点送审后协议号是否立即可见
|
||||
- 加购业务失败不重推:口头跟对方确认一句
|
||||
|
||||
开发以口径文档为准。本文只记为什么这么定。
|
||||
|
|
@ -5,9 +5,9 @@
|
|||
依据:
|
||||
|
||||
- `Downloads/RPA与订单中心接口设计_20260922.xlsx`(以当时最新一份为准)
|
||||
- `资料/API鉴权.doc`
|
||||
- `资料/合同返利系统字段映射_20260911.xlsx`(返利总单默认值)
|
||||
- `选品流程/` 既有规则
|
||||
- `docs/API鉴权.doc`
|
||||
- `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`)
|
||||
|
||||
|
|
@ -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 节提供接口说明书,并确认客户类型、账期天数、计划员对照、货源细单号、对码回写、尽调来源。未齐备前,合同与返利不得按缺省猜测值提交。
|
||||
|
||||
上述事项确认后,按《福建洁达四流程技术实现说明》实施。实施顺序为:先完成订单中心取数入库与状态中心可见,再接入选品、合同与返利。
|
||||
|
|
@ -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 '货主编码。';
|
||||
|
|
@ -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_*`、审计字段。
|
||||
|
|
@ -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. 确认根上没有 `选品流程/`、`资料/`
|
||||
Binary file not shown.
|
|
@ -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 的列对照(缺了什么、多了什么)。不要展开下一阶段。
|
||||
|
|
@ -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.
|
|
@ -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 推送。
|
||||
|
|
@ -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
|
||||
|
|
@ -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 回写。
|
||||
|
|
@ -11,9 +11,9 @@ from dataclasses import dataclass, field
|
|||
from pathlib import Path
|
||||
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"
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
APA (艺赛旗) Selenium Chrome BrowserAdapter 实现
|
||||
通过 Selenium WebDriver 直接操控 Chrome,不依赖 APA 浏览器模块
|
||||
Selenium Chrome 浏览器适配。通过 WebDriver 操控 Chrome。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -495,7 +494,7 @@ class ApaBrowserAdapter:
|
|||
return ""
|
||||
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -1,40 +1,29 @@
|
|||
"""CONTRACT_CREATE node: input building + state-center callback (runtime.shell) + Selenium drafting."""
|
||||
"""CONTRACT_CREATE: 入参组装 + Selenium 起草。结果返回合同号,不回调旧状态中心。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from .apa_browser_adapter import ApaBrowserAdapter
|
||||
from .callback_client import StateCenterClient
|
||||
from .contract_draft import (
|
||||
from apa_browser_adapter import ApaBrowserAdapter
|
||||
from contract_draft import (
|
||||
ContractDraft,
|
||||
contract_draft_from_dict,
|
||||
is_placeholder_customer_code,
|
||||
load_contract_draft,
|
||||
)
|
||||
from .contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||||
from .pdf_utils import ensure_merged_pdf
|
||||
from .runtime.errors import NodeError, PdfMergeUnsupported, UnknownRpaError
|
||||
from .runtime.shell import execute_node
|
||||
from contract_flow_steps import ContractFlowContext, run_contract_create_flow
|
||||
from errors import NodeError, PdfMergeUnsupported, UnknownRpaError
|
||||
from pdf_utils import ensure_merged_pdf
|
||||
|
||||
NODE_CODE = "CONTRACT_CREATE"
|
||||
CONTRACT_URL = "http://contract-test.gzmpc.com:8080/"
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -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)
|
||||
class ContractCreateInput:
|
||||
job_id: str
|
||||
|
|
@ -87,18 +56,15 @@ ContractRunner = Callable[[ContractCreateInput], str]
|
|||
|
||||
def execute_contract_create(
|
||||
spec: ContractCreateInput,
|
||||
client: StateCenterClient,
|
||||
*,
|
||||
contract_runner: ContractRunner | None = None,
|
||||
) -> dict[str, Any]:
|
||||
runner = contract_runner or create_contract_in_contract_system
|
||||
|
||||
# dry_run:只跑到提交前,不回调状态中心(不 start/complete/fail)
|
||||
if spec.submit_mode == "dry_run":
|
||||
runner(spec)
|
||||
return {"status": "dry_run", "submitted": False}
|
||||
|
||||
def _run() -> dict[str, Any]:
|
||||
try:
|
||||
contract_no = runner(spec)
|
||||
except NodeError:
|
||||
|
|
@ -106,22 +72,13 @@ def execute_contract_create(
|
|||
except Exception as exc:
|
||||
raise UnknownRpaError(str(exc)) from exc
|
||||
if not contract_no:
|
||||
raise ContractCreateError("submit 模式未读取到真实合同编号,无法 complete")
|
||||
raise ContractCreateError("submit 模式未读取到真实合同编号")
|
||||
return {
|
||||
"businessRefs": {"contractSystemNo": contract_no},
|
||||
"status": "done",
|
||||
"contractSystemNo": contract_no,
|
||||
"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:
|
||||
"""在合同系统中起草合同并返回合同编号。"""
|
||||
|
|
@ -321,21 +278,13 @@ def load_input_data(raw: str) -> dict[str, Any]:
|
|||
raise ContractCreateError(f"无法解析入参:既不是合法 JSON,也不是存在的文件路径:{raw}")
|
||||
|
||||
|
||||
def run_contract_create(
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
state_center_url: str,
|
||||
agent_token: str,
|
||||
) -> dict[str, Any]:
|
||||
def run_contract_create(data: dict[str, Any]) -> dict[str, Any]:
|
||||
spec = build_spec_from_data(data)
|
||||
client = StateCenterClient(state_center_url, agent_token)
|
||||
return execute_contract_create(spec, client)
|
||||
return execute_contract_create(spec)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run YiSaiQi CONTRACT_CREATE node.")
|
||||
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 = argparse.ArgumentParser(description="合同系统页面起草。")
|
||||
parser.add_argument("--input-json", default=os.getenv("CONTRACT_INPUT_JSON"))
|
||||
parser.add_argument("--job-id")
|
||||
parser.add_argument("--supplier-name")
|
||||
|
|
@ -353,7 +302,6 @@ def parse_args() -> argparse.Namespace:
|
|||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
client = StateCenterClient(args.state_center_url, args.token)
|
||||
|
||||
if 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,
|
||||
)
|
||||
|
||||
result = execute_contract_create(spec, client)
|
||||
result = execute_contract_create(spec)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .contract_flow_steps import ContractGoodsLine
|
||||
from contract_flow_steps import ContractGoodsLine
|
||||
|
||||
_PLACEHOLDER_CUSTOMER_PREFIXES = ("CUST-", "TEST-", "MOCK-")
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
import time
|
||||
|
|
@ -10,8 +6,8 @@ from dataclasses import dataclass, field
|
|||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from .runtime.accept import AcceptanceReport, accept_log_path, evidence_shot
|
||||
from .runtime.errors import (
|
||||
from accept import AcceptanceReport, accept_log_path, evidence_shot
|
||||
from errors import (
|
||||
LoginFailed,
|
||||
NodeError,
|
||||
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)
|
||||
return ""
|
||||
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(
|
||||
"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:
|
||||
_callback_start()
|
||||
browser.open_url(ctx.contract_url)
|
||||
_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:
|
||||
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):
|
||||
raise SupplierContractDisabled(
|
||||
|
|
@ -1120,10 +1068,6 @@ def submit_contract_for_approval(browser: BrowserAdapter, ctx: ContractFlowConte
|
|||
evidence_shot(browser, "contract-submitted")
|
||||
print("提交成功,回我的合同取编号。", flush=True)
|
||||
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)
|
||||
return True
|
||||
_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:
|
||||
if ctx is None:
|
||||
state = _flow_runtime()
|
||||
if state is not None:
|
||||
ctx = getattr(state, "ctx", None)
|
||||
if ctx is None:
|
||||
ctx = ContractFlowContext(
|
||||
contract_url="http://contract-test.gzmpc.com:8080/",
|
||||
|
|
@ -1232,13 +1172,7 @@ def read_contract_number(browser: BrowserAdapter, ctx: ContractFlowContext | Non
|
|||
order_date="",
|
||||
source_type="",
|
||||
)
|
||||
number = _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
|
||||
return _read_contract_number_from_my_list(browser, ctx)
|
||||
|
||||
|
||||
def _read_contract_number_on_page(browser: BrowserAdapter) -> str:
|
||||
|
|
@ -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):
|
||||
|
|
@ -38,7 +38,5 @@
|
|||
"mdmCode": "",
|
||||
"unitPrice": "50.00"
|
||||
}
|
||||
],
|
||||
"stateCenterUrl": "http://127.0.0.1:8000",
|
||||
"agentToken": "dev-agent-token"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from contract_create_node import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""PDF utilities for YiSaiQi contract drafting."""
|
||||
"""合同 PDF 合并。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
|
@ -7,8 +7,7 @@ from pathlib import Path
|
|||
def ensure_merged_pdf(source_paths: list[str], output_path: str) -> str:
|
||||
"""Return one PDF path, merging multiple source PDFs when needed.
|
||||
|
||||
YiSaiQi must upload only one YYC contract document for the same supplier
|
||||
and same order date. A single source PDF can be uploaded directly.
|
||||
同供应商同日只上传一份 PDF。单文件直接返回路径。
|
||||
"""
|
||||
if not source_paths:
|
||||
raise ValueError("YYC contract creation requires at least one platform PDF")
|
||||
|
|
@ -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
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
from rpa_yisaiqi.runtime.accept import (
|
||||
from accept import (
|
||||
AcceptanceReport,
|
||||
accept_log_path,
|
||||
artifact_dir,
|
||||
evidence_shot,
|
||||
field_match,
|
||||
)
|
||||
from rpa_yisaiqi.runtime.errors import NodeError
|
||||
from errors import NodeError
|
||||
import pytest
|
||||
|
||||
|
||||
def test_each_flow_writes_its_own_accept_file(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"rpa_yisaiqi.runtime.accept.SCREENSHOT_ROOT",
|
||||
"accept.SCREENSHOT_ROOT",
|
||||
tmp_path,
|
||||
)
|
||||
report = AcceptanceReport(flow="ORDER_FETCH")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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():
|
||||
|
|
@ -11,7 +11,7 @@ def test_default_chrome_profile_is_outside_repo():
|
|||
|
||||
|
||||
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)
|
||||
profile = resolve_chrome_profile_base(requested=str(inside))
|
||||
assert profile == (REPO_ROOT.parent / "rpa-chrome-profile").resolve()
|
||||
|
|
@ -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"
|
||||
|
|
@ -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" },
|
||||
]
|
||||
|
|
@ -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/")
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,38 +1,33 @@
|
|||
# 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`
|
||||
- 鉴权:header `X-Oms-Token`,环境变量 `OMS_INBOUND_TOKEN`
|
||||
- 落表:`rpa_sales_goods_mapping_data`(`deal_flag=0`)
|
||||
- 请求里不跑选品。选品班次在仓库根目录 `选品流程/`,按条形码搜两平台、四级仓序拆量后写 `rpa_cart_settlement_goods_data`
|
||||
- 请求里不跑选品。选品班次在 `rpa_selection/`,按条形码搜两平台、四级仓序拆量后写 `rpa_cart_settlement_goods_data`
|
||||
|
||||
## 本地(内存仓库)
|
||||
|
||||
```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。单测用内存替身,不连库:`uv run pytest -v`。
|
||||
|
||||
## 真库
|
||||
|
||||
Oracle 11.2 需要 Instant Client(thick)。密码只放本机 `rpa_oms/.env`(已 gitignore),不要写进仓库、README、命令行历史。
|
||||
|
||||
```bash
|
||||
export OMS_REPO=oracle
|
||||
export OMS_INBOUND_TOKEN=...
|
||||
cd rpa_oms
|
||||
uv sync --python 3.12
|
||||
export OMS_INBOUND_TOKEN=dev-oms-token
|
||||
export RPA_ORACLE_HOST=192.168.12.244
|
||||
export RPA_ORACLE_PORT=1521
|
||||
export RPA_ORACLE_SERVICE=rpa
|
||||
|
|
@ -41,6 +36,7 @@ export RPA_ORACLE_PASSWORD=...
|
|||
export RPA_ORACLE_THICK=1
|
||||
# 如需指定 Instant Client 目录:
|
||||
# export RPA_ORACLE_CLIENT_LIB=/path/to/instantclient
|
||||
uv run uvicorn app.main:app --reload --port 8001
|
||||
```
|
||||
|
||||
缺 `RPA_ORACLE_HOST` / 用户 / 密码时,收件接口回 503。
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""内存仓库。单测和 OMS_REPO=memory 用,进程退出即丢。
|
||||
"""内存仓库。只给单测用,进程退出即丢。运行时走 Oracle。
|
||||
|
||||
按 oms_id 判重,已存在就 DuplicateOmsId,不覆盖。
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import Depends, FastAPI, Header
|
||||
|
|
@ -18,21 +17,14 @@ from .local_env import load_local_env
|
|||
|
||||
load_local_env()
|
||||
from .inbound.envelope import EnvelopeError, parse_envelope
|
||||
from .inbound.memory_repo import MemorySalesGoodsRepo
|
||||
from .inbound.repo import RepoUnavailable
|
||||
from .inbound.sales_goods import receive_sales_goods
|
||||
|
||||
app = FastAPI(title="洁达 RPA × 订单中心", version="0.1")
|
||||
|
||||
# 内存仓库做成进程内单例,同一进程多次 POST 才能测到 DUPLICATE。
|
||||
_memory_repo = MemorySalesGoodsRepo()
|
||||
|
||||
|
||||
def get_repo():
|
||||
# 默认走 Oracle。本机连不上库时 OMS_REPO=memory。
|
||||
if os.getenv("OMS_REPO", "oracle") == "memory":
|
||||
return _memory_repo
|
||||
# thick 客户端初始化有副作用,用到才 import。
|
||||
# 运行时只走 Oracle。单测用 dependency_overrides 注入内存替身。
|
||||
from .inbound.oracle_repo import OracleSalesGoodsRepo
|
||||
|
||||
return OracleSalesGoodsRepo.from_env()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# rpa_rebate
|
||||
|
||||
返利班。本包尚未实现。
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| 读 | 表 3 / 表 4:已有合同号尚未回合同成功的单,以及只录协议的单 |
|
||||
| 写 | 表 4:搜到合同号则 `contractStatus=2`;录协议后 `actionCode=2`、`agreementStatus=1` |
|
||||
| 口 | 口 4 |
|
||||
|
||||
与合同同一天两班,建议合同班跑完再跑。查号挂在本班,不单开查状态任务。搜不到不重录合同,留给下一班再搜。
|
||||
|
||||
## 本期不做
|
||||
|
||||
返利中心查号、录协议、送审、接口 4 回写。
|
||||
|
|
@ -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
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
班次作业:从表 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
|
||||
cd 选品流程
|
||||
cd rpa_selection
|
||||
uv sync --python 3.12
|
||||
uv run playwright install chromium
|
||||
```
|
||||
|
|
@ -50,7 +50,7 @@ Oracle 与 `rpa_oms` 同一套变量:`RPA_ORACLE_HOST` / `PORT` / `SERVICE` /
|
|||
## 运行
|
||||
|
||||
```bash
|
||||
cd 选品流程
|
||||
cd rpa_selection
|
||||
uv run pytest -v
|
||||
uv run python main.py --limit 5
|
||||
uv run python main.py --platform yaoex
|
||||
|
|
@ -59,7 +59,7 @@ uv run python main.py --platform yaoex
|
|||
Windows:
|
||||
|
||||
```powershell
|
||||
cd 选品流程
|
||||
cd rpa_selection
|
||||
uv sync --python 3.12
|
||||
Copy-Item -LiteralPath .env.example -Destination .env
|
||||
# 商城账号写本机 .env;Oracle 也可只留在 rpa_oms/.env
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "jieda-procure"
|
||||
name = "rpa-selection"
|
||||
version = "0.1.0"
|
||||
description = "洁达选品班次:表1 deal_flag=0 → 双平台搜品 → 写表2"
|
||||
description = "洁达选品班次(rpa_selection):表1 deal_flag=0 → 双平台搜品 → 写表2"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
|
|
@ -53,7 +53,7 @@ if (Test-Path -LiteralPath $venvPy) {
|
|||
|
||||
$uv = Get-Command uv -ErrorAction SilentlyContinue
|
||||
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
|
||||
|
|
@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "oracledb"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
|
|
@ -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 |
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
"""YiSaiQi RPA integration helpers."""
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pypdf==5.1.0
|
||||
selenium>=4.15.0
|
||||
webdriver-manager>=4.0.0
|
||||
requests>=2.31.0
|
||||
|
|
@ -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
Loading…
Reference in New Issue