Files
gyxx-flow/docs/history/supply-chain/skills/analyzer/SKILL.md
T

191 lines
9.0 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 ownerbot 身份在 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|...|...|``` 代码块渲染,稳定可靠。**