feat: expand module workflows, dynamic config, notifications and console API

This commit is contained in:
wangyunlong committed 2026-09-04 09:49:37 +08:00
1 parent 8df5266abb
commit b124d757b0
309 files changed
+89357 -6231

No files matched your search

@@ -0,0 +1,165 @@
"""One-time exporter for the retired Feishu workflow configuration Base.
This module is deliberately outside runtime adapters. Production workflows
must read :mod:`gyxx_flow.adapters.workflow_config`; this exporter only creates
the auditable seed used to bootstrap an empty project database.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from gyxx_flow.adapters.workflow_config import DEFAULT_SEED_PATH, normalize_config
SOURCE_URL = (
"https://bu0zgpibak.feishu.cn/base/"
"TtoCb1NuQaDy3NsZWTpc0GIvnph"
"?table=tblKCjplVAFrRwMC&view=vewvy33xEk"
)
BASE_TOKEN = "TtoCb1NuQaDy3NsZWTpc0GIvnph"
TABLE_ID = "tblKCjplVAFrRwMC"
VIEW_ID = "vewvy33xEk"
FIELD_MAP = {
"平台": "platform",
"ERP款式编码": "erp_codes",
"款式": "style_name",
"商品链接ID( 填多个ID 用英文逗号隔开)": "item_ids",
"注释": "note",
"飞书多维表格地址": "sales_bitable_url",
"主图飞书多维表格地址": "main_image_bitable_url",
"合作达人多维表格地址": "creator_bitable_url",
"品牌": "brand",
"人群画像(天猫)": "tm_persona_bitable_url",
"人群画像(京东)": "jd_persona_bitable_url",
"人群画像(抖音)": "dy_persona_bitable_url",
"自营合作达人表格地址": "self_creator_bitable_url",
"每周笔记分析": "weekly_note_analysis_url",
"平台单品分析": "style_analysis_bitable_url",
"款式内容": "style_content",
}
class FeishuSeedExportError(RuntimeError):
pass
def _parse_json(text: str) -> dict[str, Any]:
decoder = json.JSONDecoder()
position = 0
while position < len(text):
start = text.find("{", position)
if start < 0:
break
try:
payload, end = decoder.raw_decode(text, start)
except json.JSONDecodeError:
position = start + 1
continue
if isinstance(payload, dict):
return payload
position = end
raise FeishuSeedExportError("lark-cli 没有返回可解析的 JSON")
def _lark_cli() -> str:
executable = (
os.getenv("LARK_CLI_PATH")
or shutil.which("lark-cli")
or shutil.which("lark-cli.cmd")
)
if not executable:
raise FeishuSeedExportError("找不到 lark-cli")
return executable
def read_source_records() -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
offset = 0
while True:
command = [
_lark_cli(),
"base",
"+record-list",
"--base-token",
BASE_TOKEN,
"--table-id",
TABLE_ID,
"--view-id",
VIEW_ID,
"--offset",
str(offset),
"--limit",
"200",
"--format",
"json",
"--as",
"user",
]
result = subprocess.run(
command,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
if result.returncode:
raise FeishuSeedExportError(
f"lark-cli 读取第 {offset} 行失败: {(result.stderr or result.stdout)[:500]}"
)
envelope = _parse_json(result.stdout)
if not envelope.get("ok"):
raise FeishuSeedExportError(f"飞书返回错误: {envelope.get('error')}")
data = envelope.get("data") or {}
fields = data.get("fields") or []
row_values = data.get("data") or []
record_ids = data.get("record_id_list") or []
if len(row_values) != len(record_ids):
raise FeishuSeedExportError("飞书行数据与 record_id 数量不一致")
for index, values in enumerate(row_values):
raw: dict[str, Any] = {}
for name, value in zip(fields, values):
key = FIELD_MAP.get(str(name))
if key:
if key == "platform" and isinstance(value, list):
value = value[0] if value else ""
raw[key] = value
raw["enabled"] = True
records.append(
{
"source_record_id": record_ids[index],
"config": normalize_config(raw, require_identity=False),
}
)
offset += len(row_values)
if not data.get("has_more"):
break
if not row_values:
raise FeishuSeedExportError("飞书分页标记异常:has_more=true 但当前页为空")
return records
def export_seed(destination: Path = DEFAULT_SEED_PATH) -> dict[str, Any]:
records = read_source_records()
payload = {
"schema_version": 1,
"source_url": SOURCE_URL,
"captured_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"record_count": len(records),
"records": records,
}
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".tmp")
temporary.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
temporary.replace(destination)
return payload