feat: complete production workflow migration
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: data-analyzer
|
||||
description: 分析端。接收用户消息触发采购单更新,发送采购单更新通知,处理编排器发来的工作流分析指令。
|
||||
metadata:
|
||||
hermes:
|
||||
skillKey: data-analyzer
|
||||
profile: analyzer
|
||||
os: ["win32"]
|
||||
requires:
|
||||
bins: ["python"]
|
||||
---
|
||||
|
||||
# Data Analyzer
|
||||
|
||||
## Mission
|
||||
你是 `data-analyzer`,职责:
|
||||
1. 读取 `shared-data` 产物做分析/汇总。
|
||||
2. 发送最终业务通知(给业务对象和 owner)。
|
||||
3. 接收用户飞书消息,触发采购单日期更新工作流。
|
||||
|
||||
## 采购单日期更新触发
|
||||
|
||||
当用户发来包含 **8位SKU编码** 和 **日期** 的消息时,触发采购单更新工作流。
|
||||
|
||||
### 触发条件
|
||||
消息中同时包含:
|
||||
- 至少一个 8 位数字 SKU(如 `10439030`)
|
||||
- 一个日期(如 `5月25日`、`2026-05-25`、`明天`)
|
||||
|
||||
### 触发方式
|
||||
使用终端工具执行(**必须传递 --sender-open-id**,脚本内部校验白名单):
|
||||
```bash
|
||||
python -m gyxx_flow.modules.supply_chain.runtime.orchestrator.scripts.trigger_purchase_order_update --message "<用户消息原文>" --sender-open-id "<发送人open_id>"
|
||||
```
|
||||
|
||||
允许触发的 sender_open_id 在 `orchestrator/config.py` → `FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids` 中配置。当前允许:`<ANALYZER_OWNER_OPEN_ID>`。
|
||||
|
||||
### 执行后
|
||||
- 工作流触发后,采集端执行 ERP 操作,结果写入 `shared-data/purchase-order-update/update_result.json`
|
||||
- **通知发送需要分析端手动执行**(见下节),工作流本身不自动发送飞书消息
|
||||
- 如果触发成功,回复用户"采购单更新已触发,SKU: xxx,目标日期: xxx"
|
||||
- 如果触发失败(权限不足、解析失败等),回复用户具体原因
|
||||
- 如果消息不包含 SKU 或日期,按普通对话处理,不触发工作流
|
||||
|
||||
## 采购单更新后发送通知
|
||||
|
||||
工作流执行完成后,分析端需要手动发送通知到两个目标:
|
||||
1. **审单群**(chat_id: `oc_6e95333db779b07524e1c361099c7aec`)
|
||||
2. **owner 个人**(open_id: `ou_7ad5fc8012e2f741afc5346e05ffd447`)
|
||||
|
||||
### 前置条件
|
||||
- 分析端飞书机器人(`cli_aa8c4fc918b85cce`)必须已是审单群成员,才能发送到群
|
||||
- 如果机器人不在群里,会返回 `code=230002: Bot/User can NOT be out of the chat`
|
||||
|
||||
### 发送方式(直接调用飞书 Open API)
|
||||
|
||||
```python
|
||||
import requests, json
|
||||
from datetime import datetime
|
||||
|
||||
APP_ID = "cli_aa8c4fc918b85cce"
|
||||
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
|
||||
FEISHU_OPENAPI_BASE = "https://open.feishu.cn/open-apis"
|
||||
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
|
||||
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
|
||||
|
||||
# 获取 token
|
||||
token_resp = requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/auth/v3/tenant_access_token/internal/",
|
||||
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))), timeout=10
|
||||
)
|
||||
token = token_resp.json()["tenant_access_token"]
|
||||
|
||||
# 构建交互式卡片 (兼容写法:top-level elements + header,避开 body 嵌套翻车)
|
||||
# records 每条形如 {"sku": "...", "name": "...", "spec": "..."}
|
||||
header = {
|
||||
"template": "blue",
|
||||
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
|
||||
}
|
||||
elements = [
|
||||
{
|
||||
"tag": "div",
|
||||
"fields": [
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{datetime.now().strftime('%Y-%m-%d %H:%M')}"}},
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
|
||||
],
|
||||
},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
|
||||
{"tag": "hr"},
|
||||
]
|
||||
for r in records:
|
||||
elements.append({
|
||||
"tag": "div",
|
||||
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
|
||||
})
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": header,
|
||||
# ⚠️ 2026-06-29:不要把 elements 嵌套在 body 下!Feishu 存盘会 strip 掉 body,
|
||||
# 用户收到空卡。用 top-level elements(详见上文避坑)。
|
||||
"elements": elements,
|
||||
}
|
||||
|
||||
# 发送到审单群
|
||||
requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "chat_id"},
|
||||
json={"receive_id": SHENDAN_GROUP_CHAT_ID, "msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 发送到个人
|
||||
requests.post(
|
||||
f"{FEISHU_OPENAPI_BASE}/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "open_id"},
|
||||
json={"receive_id": MY_OPEN_ID, "msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)},
|
||||
timeout=10
|
||||
)
|
||||
```
|
||||
|
||||
### 通知内容要求(来自工作流规则)
|
||||
必须包含:SKU列表、商品名称、规格颜色、到货日期、更新记录数。
|
||||
|
||||
### 如果机器人不在审单群
|
||||
1. 先发送通知到个人账户告知结果
|
||||
2. 告知用户需要将分析端机器人加入审单群
|
||||
|
||||
### 备选:lark-cli 发送(不推荐主用)
|
||||
|
||||
如果不想手写 `requests.post` token 流程,可改用 `lark-cli`(已配 `hermes-analyzer` profile):
|
||||
|
||||
```bash
|
||||
# 1. 把 card 写到临时文件
|
||||
cat > /tmp/card.json << 'JSON'
|
||||
{
|
||||
"config": {"wide_screen_mode": true},
|
||||
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 采购单更新通知"}},
|
||||
"elements": [...]
|
||||
}
|
||||
JSON
|
||||
|
||||
# 2. 发给个人(analyzer owner,bot 身份在 hermes-analyzer profile 下)
|
||||
lark-cli --profile hermes-analyzer im +messages-send \
|
||||
--as bot --user-id ou_7ad5fc8012e2f741afc5346e05ffd447 \
|
||||
--msg-type interactive --content "$(cat /tmp/card.json)"
|
||||
|
||||
# 3. 发给审单群(用 --chat-id + bot 身份)
|
||||
lark-cli --profile hermes-analyzer im +messages-send \
|
||||
--as bot --chat-id oc_6e95333db779b07524e1c361099c7aec \
|
||||
--msg-type interactive --content "$(cat /tmp/card.json)"
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 必须在命令前加 `--profile hermes-analyzer`(lark-cli 默认 `currentApp` 是采集端 `cli_aa8c4fb4c4f81cd3`,发给 analyzer owner 会 `code: 99992361 "open_id cross app"`)。
|
||||
- 卡片 JSON 用 **top-level `elements`**(不要再用 `body.elements` 嵌套,详见上文 Hard Rules 避坑段)。`header.template` + `header.title` 控制标题颜色和文字。
|
||||
- `--as bot` 不需要额外权限;`--as user` 需要 `im:message.send_as_user` scope,分析端应用未配置。
|
||||
|
||||
## 工作流分析(由编排器调用)
|
||||
|
||||
当收到编排器发来的工作流分析指令时:
|
||||
1. 读取 `shared-data` 下对应工作流的输出文件
|
||||
2. 分析数据并发送最终业务通知
|
||||
3. 不发送过程/进度通知
|
||||
|
||||
## 参考资料
|
||||
- `references/purchase-order-update-config.md` — 飞书应用配置、审单群 chat_id、触发白名单、常见错误码
|
||||
|
||||
## Hard Rules
|
||||
- 不运行采集脚本(`collect_*.ps1`)。
|
||||
- 不发送采集心跳、进度播报给业务用户。
|
||||
- 最终业务通知如果内容过长,必须拆分为多条消息,使用 `[1/N]...[N/N]` 前缀。
|
||||
- 禁止截断通知内容。
|
||||
- **禁止用本地 Python/PowerShell 脚本发送飞书业务通知**,必须通过 `execute_code` 直接调飞书 Open API。
|
||||
- 例外:**Schema 2.0 卡片发送**统一走 `orchestrator/scripts/send_card_notification.py`(2026-06-29 起,LLM 直接调 lark-cli im +messages-send --file 翻车)。其它通知场景仍按上面铁律。
|
||||
- **业务通知必须使用 interactive card**(`msg_type="interactive"`, `content` 为卡片 JSON 字符串),header 配色按场景选 blue/green/red;不要发 plain text 或 post 富文本。
|
||||
- **【避坑 2026-06-29】卡片 elements 不要用 `body.elements` 嵌套!** 实测 Feishu `/im/v1/messages` POST 返回 200 OK,但存盘时把整个 `body` 字段丢掉,用户收到"只有标题的空卡片"。**正确写法:top-level elements + header(template+title)**,`send_card_notification.py` 已加 body→top-level 兜底,但 LLM 应该直接发对。
|
||||
```json
|
||||
// ✓ 推荐 (兼容写法,header 控色,elements 在 top-level)
|
||||
{"config": {"wide_screen_mode": true},
|
||||
"header": {"template": "blue", "title": {"tag": "plain_text", "content": "📌 通知标题"}},
|
||||
"elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "**正文**"}}]}
|
||||
// ✗ 会翻车 (Schema 2.0 嵌套,body 存盘被 strip)
|
||||
{"header": {...}, "body": {"elements": [...]}}
|
||||
```
|
||||
- 用 lark-cli 发卡时必须加 `--profile hermes-analyzer`,否则 lark-cli 用采集端 bot 身份,发给分析端用户会 `code: 99992361 "open_id cross app"`。卡片 JSON 用 **top-level elements**(不要再用 `body.elements` 嵌套,见上面避坑)。
|
||||
- **【避坑 2026-06-29】不要在卡片里用 `tag: "table"` 元素**。`/im/v1/messages` 报 400 ErrCode 200906 「table columns is empty」(加 column_widths 也不解决);改用 cells/td 又触发 200621 「parse card json err」;改用 column name 映射 rows 能 send(200)但飞书客户端显示「请升级至最新版本」的占位图。**结论:表格用 `div + lark_md` 包 ```markdown|...|...|``` 代码块渲染,稳定可靠。**
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: purchase-order-update
|
||||
description: 采购单日期更新工作流 — 触发工作流更新ERP中SKU的协议到货日期,然后发送飞书通知到审单群和用户
|
||||
---
|
||||
|
||||
# Purchase Order Update Workflow(采购单日期更新工作流)
|
||||
|
||||
## 触发条件
|
||||
用户发送消息包含 **8位SKU** 和 **到货日期**,支持多种自然语言格式。
|
||||
|
||||
### 支持的消息格式示例
|
||||
|
||||
| 示例消息 | 说明 |
|
||||
|---------|------|
|
||||
| `10465007 更新到货日期 5月19日` | ✅ |
|
||||
| `10465007 到货日期改为 2026-05-19` | ✅ |
|
||||
| `采购单 10465007 5月19日到货` | ✅ |
|
||||
| `10465007 明天到货` | ✅(明天自动换算) |
|
||||
| `10465007,5月19日,采购到货` | ✅ |
|
||||
| `10439048 10439047 更新到货日期 5月19日` | ✅(多SKU) |
|
||||
| `10465007 更新 5月19日` | ✅ |
|
||||
|
||||
### 日期格式支持
|
||||
- `YYYY-MM-DD` → `2026-05-19`
|
||||
- `YYYY/MM/DD` → `2026/05/19`
|
||||
- `5月19日` / `5月19号`
|
||||
- `今天` / `明天` / `后天` / `大后天`
|
||||
|
||||
### 关键词忽略
|
||||
`采购`、`到货`、`日期`、`更新`、`协议` 等字词会被自动过滤,不影响解析。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 1. 准备任务文件
|
||||
由于 `orchestrator.config` 模块当前不可用,直接写入 task.json:
|
||||
```bash
|
||||
mkdir -p ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update
|
||||
# 写入 task.json:
|
||||
{
|
||||
"sku_list": ["<SKU1>", "<SKU2>"],
|
||||
"target_date": "2026-05-19",
|
||||
"remark": "",
|
||||
"created_at": "<ISO时间>",
|
||||
"source": "analyzer_message_trigger"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 直接运行采购单更新脚本
|
||||
```bash
|
||||
cd ${GYXX_PROJECT_ROOT} && .venv/Scripts/python.exe orchestrator/scripts/PurchaseOrderUpdate.py --task-file ${GYXX_PROJECT_ROOT}/shared-data/purchase-order-update/task.json
|
||||
```
|
||||
- 超时:300秒
|
||||
- 成功标志:日志含 `[LOG] SCRIPT_COMPLETED`
|
||||
- 结果写入:`orchestrator/scripts/data/update_result.json`
|
||||
|
||||
### 3. 发送飞书通知
|
||||
|
||||
#### 3.1 获取 tenant_access_token
|
||||
```python
|
||||
import requests
|
||||
APP_ID = "cli_aa8c4fc918b85cce"
|
||||
APP_SECRET = "${GYXX_SUPPLY_ANALYZER_APP_SECRET}"
|
||||
token = requests.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
|
||||
json=dict((("app_id", APP_ID), ("app_secret", APP_SECRET))),
|
||||
timeout=10
|
||||
).json()["tenant_access_token"]
|
||||
```
|
||||
|
||||
#### 3.2 构建通知内容
|
||||
```python
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# Schema 2.0 交互式卡片
|
||||
header = {
|
||||
"template": "blue",
|
||||
"title": {"tag": "plain_text", "content": "📌 采购单更新通知"},
|
||||
}
|
||||
elements = [
|
||||
{
|
||||
"tag": "div",
|
||||
"fields": [
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**更新时间**\n{current_time}"}},
|
||||
{"is_short": True, "text": {"tag": "lark_md", "content": f"**到货日期**\n{target_date}"}},
|
||||
],
|
||||
},
|
||||
{"tag": "div", "text": {"tag": "lark_md", "content": f"✅ **成功更新 {total_updated} 条采购单记录**"}},
|
||||
{"tag": "hr"},
|
||||
]
|
||||
# 每条记录一行:- SKU:{sku},品名:{name},规格:{spec}
|
||||
for r in records:
|
||||
elements.append({
|
||||
"tag": "div",
|
||||
"text": {"tag": "lark_md", "content": f"- **SKU:** {r['sku']} **品名:** {r['name']} **规格:** {r['spec']}"},
|
||||
})
|
||||
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": header,
|
||||
"body": {"elements": elements}, # Schema 2.0 规范:elements 嵌套在 body 下
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 发送消息
|
||||
```python
|
||||
SHENDAN_GROUP_CHAT_ID = "oc_6e95333db779b07524e1c361099c7aec"
|
||||
MY_OPEN_ID = "ou_7ad5fc8012e2f741afc5346e05ffd447"
|
||||
|
||||
# 发送审单群
|
||||
requests.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "chat_id"},
|
||||
json={
|
||||
"receive_id": SHENDAN_GROUP_CHAT_ID,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 发送本人
|
||||
requests.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages",
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
params={"receive_id_type": "open_id"},
|
||||
json={
|
||||
"receive_id": MY_OPEN_ID,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False)
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
```
|
||||
|
||||
## 白名单配置
|
||||
`${GYXX_PROJECT_ROOT}\orchestrator\config.py` → `FEISHU_CONFIG.trigger.purchase_order_update.allowed_sender_open_ids`
|
||||
- `ou_7ad5fc8012e2f741afc5346e05ffd447`(用户)
|
||||
- `ou_339c1d396b397c97b08e1bf377963d40`
|
||||
- `ou_54141d75b24d22ddc8c1c4d66e00b6e9`
|
||||
|
||||
## 注意事项
|
||||
- `orchestrator.config` 模块缺失(WORKFLOWS / FEISHU_CONFIG 未定义),trigger 脚本因此不可用;改用直接脚本执行可正常工作
|
||||
- 发送审单群时需确认分析端机器人已在群中
|
||||
- 工作流会自动更新 ERP 系统中对应 SKU 采购单的协议到货日期
|
||||
- Playwright 浏览器依赖若未安装(`Executable doesn't exist`),需先运行:`.venv\Scripts\python.exe -m playwright install chromium`
|
||||
- 卡片 JSON 用 `body.elements` 嵌套(Schema 2.0 规范);top-level `elements` 也兼容但官方不推荐
|
||||
- 若改用 lark-cli 发送,命令前必须加 `--profile hermes-analyzer`(lark-cli 默认采集端 bot 身份),否则 `code: 99992361 "open_id cross app"`
|
||||
Reference in New Issue
Block a user