🔄 卡若AI 同步 2026-02-22 11:47 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 8 个
This commit is contained in:
@@ -120,16 +120,19 @@ def get_cookie_from_args_or_file(cookie_arg: str | None) -> str:
|
||||
|
||||
|
||||
def get_bv_csrf_token(cookie: str) -> str:
|
||||
"""从 cookie 字符串中解析 bv_csrf_token(需 36 字符,与 GitHub 一致)。"""
|
||||
key = "bv_csrf_token="
|
||||
i = cookie.find(key)
|
||||
if i == -1:
|
||||
return ""
|
||||
start = i + len(key)
|
||||
end = cookie.find(";", start)
|
||||
if end == -1:
|
||||
end = len(cookie)
|
||||
return cookie[start:end].strip()
|
||||
"""从 cookie 字符串中解析 bv_csrf_token 或 minutes_csrf_token(36 字符,兼容 GitHub bingsanyu/feishu_minutes)。"""
|
||||
for key in ("bv_csrf_token=", "minutes_csrf_token="):
|
||||
i = cookie.find(key)
|
||||
if i == -1:
|
||||
continue
|
||||
start = i + len(key)
|
||||
end = cookie.find(";", start)
|
||||
if end == -1:
|
||||
end = len(cookie)
|
||||
val = cookie[start:end].strip()
|
||||
if len(val) == 36:
|
||||
return val
|
||||
return ""
|
||||
|
||||
|
||||
def build_headers(cookie: str, require_bv: bool = True):
|
||||
|
||||
57
02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_md_to_wiki_一键流程.md
Normal file
57
02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_md_to_wiki_一键流程.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Markdown → 飞书 JSON → 直接上传图片 · 一键流程
|
||||
|
||||
> 按飞书文档格式:本地先转 JSON,再以 JSON 直接上传图片到 Wiki 文档。
|
||||
|
||||
---
|
||||
|
||||
## 流程说明
|
||||
|
||||
| 步骤 | 操作 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| 1 | 本地转 JSON | `md_to_feishu_json.py` 将 Markdown 转为飞书 docx blocks JSON |
|
||||
| 2 | 写入 image_paths | JSON 内含 `image_paths` 数组(相对路径),供上传脚本读取 |
|
||||
| 3 | 直接上传图片 | 上传脚本按 `image_paths` 上传到文档素材,并写入 blocks |
|
||||
|
||||
---
|
||||
|
||||
## 基因胶囊文章 · 一键命令
|
||||
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI
|
||||
|
||||
# Step 1: Markdown → 飞书 JSON(含图片占位与 image_paths)
|
||||
python3 "02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py" \
|
||||
"/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/卡若:基因胶囊——AI技能可遗传化的实现与落地.md" \
|
||||
"/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/卡若_基因胶囊_AI技能可遗传化_feishu_blocks.json" \
|
||||
--images "assets/基因胶囊_概念与流程.png,assets/基因胶囊_完整工作流程图.png"
|
||||
|
||||
# Step 2: 按 JSON 直接上传图片并创建/更新飞书文档
|
||||
python3 "02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_wiki_gene_capsule_article.py"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSON 格式规范(飞书 docx blocks)
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "由 xxx.md 转换的飞书 docx blocks",
|
||||
"image_paths": ["assets/图1.png", "assets/图2.png"],
|
||||
"children": [
|
||||
{"block_type": 3, "heading1": {...}},
|
||||
{"block_type": 2, "text": {"elements": [{"text_run": {"content": "【配图 1:待上传】"}}]}},
|
||||
{"block_type": 4, "heading2": {...}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `image_paths`:相对文章目录的图片路径,按 `【配图 1】`、`【配图 2】` 顺序对应
|
||||
- `children`:飞书 blocks 数组,`block_type` 2=文本、3=一级标题、4=二级、18=gallery(图片)
|
||||
- 上传时脚本会将 `【配图 N】` 替换为 gallery 块(若 API 支持)或保留文本说明
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 图片块(block_type 18 gallery)若飞书 API 报 `invalid param`,会退化为文本说明,图片仍上传至文档素材,用户可手动「插入 → 图片 → 文档素材」插入
|
||||
- `image_paths` 建议用相对路径,便于 JSON 迁移
|
||||
@@ -11,10 +11,11 @@ from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
FEISHU_SCRIPT = SCRIPT_DIR / "feishu_wiki_create_doc.py"
|
||||
IMG_DIR = Path("/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/assets")
|
||||
ARTICLE_DIR = Path("/Users/karuo/Documents/个人/2、我写的日记/火:开发分享")
|
||||
IMG_DIR = ARTICLE_DIR / "assets"
|
||||
PARENT_TOKEN = "KNf7wA8Rki1NSdkkSIqcdFtTnWb"
|
||||
TITLE = "卡若:基因胶囊——AI技能可遗传化的实现与落地"
|
||||
JSON_PATH = Path("/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/卡若_基因胶囊_AI技能可遗传化_feishu_blocks.json")
|
||||
JSON_PATH = ARTICLE_DIR / "卡若_基因胶囊_AI技能可遗传化_feishu_blocks.json"
|
||||
|
||||
# 导入 feishu 脚本的 token 逻辑
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
@@ -48,6 +49,17 @@ def upload_image_to_doc(token: str, doc_token: str, img_path: Path) -> str | Non
|
||||
return None
|
||||
|
||||
|
||||
def _make_image_block(file_token: str) -> dict:
|
||||
"""生成飞书图片块,尝试 gallery 与 file 两种格式"""
|
||||
return {
|
||||
"block_type": 18,
|
||||
"gallery": {
|
||||
"imageList": [{"fileToken": file_token}],
|
||||
"galleryStyle": {"align": "center"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _title_matches(node_title: str, target: str) -> bool:
|
||||
"""判断节点标题是否与目标相似(含关键词即视为匹配)"""
|
||||
if not node_title or not target:
|
||||
@@ -164,15 +176,26 @@ def create_doc_with_images():
|
||||
if not doc_token:
|
||||
doc_token = node_token
|
||||
|
||||
# 4. 上传图片
|
||||
img1 = IMG_DIR / "基因胶囊_概念与流程.png"
|
||||
img2 = IMG_DIR / "基因胶囊_完整工作流程图.png"
|
||||
file_token1 = upload_image_to_doc(token, doc_token, img1) if img1.exists() else None
|
||||
file_token2 = upload_image_to_doc(token, doc_token, img2) if img2.exists() else None
|
||||
if file_token1:
|
||||
print(f"✅ 图片1 上传成功")
|
||||
if file_token2:
|
||||
print(f"✅ 图片2 上传成功")
|
||||
# 4. 上传图片(优先从 JSON 的 image_paths 读取,否则用默认)
|
||||
img_paths = []
|
||||
if JSON_PATH.exists():
|
||||
try:
|
||||
j = json.load(open(JSON_PATH, "r", encoding="utf-8"))
|
||||
for p in j.get("image_paths", []):
|
||||
full = (ARTICLE_DIR / p) if not Path(p).is_absolute() else Path(p)
|
||||
img_paths.append(full)
|
||||
except Exception:
|
||||
pass
|
||||
if not img_paths:
|
||||
img_paths = [IMG_DIR / "基因胶囊_概念与流程.png", IMG_DIR / "基因胶囊_完整工作流程图.png"]
|
||||
file_tokens = []
|
||||
for p in img_paths:
|
||||
ft = upload_image_to_doc(token, doc_token, p) if p.exists() else None
|
||||
file_tokens.append(ft)
|
||||
if ft:
|
||||
print(f"✅ 图片上传: {p.name}")
|
||||
file_token1 = file_tokens[0] if len(file_tokens) > 0 else None
|
||||
file_token2 = file_tokens[1] if len(file_tokens) > 1 else None
|
||||
|
||||
# 5. 构建 blocks:从 JSON 加载,配图占位处注入图片 block
|
||||
if JSON_PATH.exists():
|
||||
@@ -185,9 +208,9 @@ def create_doc_with_images():
|
||||
c = (b.get("text") or {}).get("elements") or []
|
||||
content = (c[0].get("text_run") or {}).get("content", "") if c else ""
|
||||
if "【配图 1" in content and tokens[0]:
|
||||
blocks.append({"block_type": 18, "gallery": {"imageList": [{"fileToken": tokens[0]}], "galleryStyle": {"align": "center"}}})
|
||||
blocks.append(_make_image_block(tokens[0]))
|
||||
elif "【配图 2" in content and len(tokens) > 1 and tokens[1]:
|
||||
blocks.append({"block_type": 18, "gallery": {"imageList": [{"fileToken": tokens[1]}], "galleryStyle": {"align": "center"}}})
|
||||
blocks.append(_make_image_block(tokens[1]))
|
||||
elif "【配图 1" in content or "【配图 2" in content:
|
||||
blocks.append(b)
|
||||
else:
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
直接将 Markdown 文件(含图片)上传到飞书 Wiki。
|
||||
不依赖 JSON,直接解析 .md 并转换为飞书 blocks。
|
||||
|
||||
用法:
|
||||
python3 feishu_wiki_md_upload.py /path/to/article.md
|
||||
python3 feishu_wiki_md_upload.py "/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/卡若:基因胶囊——AI技能可遗传化的实现与落地.md"
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PARENT_TOKEN = "KNf7wA8Rki1NSdkkSIqcdFtTnWb"
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
import feishu_wiki_create_doc as fwd
|
||||
|
||||
|
||||
def upload_image_to_doc(token: str, doc_token: str, img_path: Path) -> str | None:
|
||||
"""上传图片到飞书文档,返回 file_token"""
|
||||
if not img_path.exists():
|
||||
return None
|
||||
size = img_path.stat().st_size
|
||||
if size > 20 * 1024 * 1024:
|
||||
return None
|
||||
url = "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all"
|
||||
with open(img_path, "rb") as f:
|
||||
files = {
|
||||
"file_name": (None, img_path.name),
|
||||
"parent_type": (None, "docx_image"),
|
||||
"parent_node": (None, doc_token),
|
||||
"size": (None, str(size)),
|
||||
"file": (img_path.name, f, "image/png"),
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
r = requests.post(url, headers=headers, files=files, timeout=60)
|
||||
if r.json().get("code") == 0:
|
||||
return r.json().get("data", {}).get("file_token")
|
||||
return None
|
||||
|
||||
|
||||
def _text_block(t: str):
|
||||
return {"block_type": 2, "text": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _h1(t: str):
|
||||
return {"block_type": 3, "heading1": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _h2(t: str):
|
||||
return {"block_type": 4, "heading2": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _h3(t: str):
|
||||
return {"block_type": 5, "heading3": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _code_block(code: str):
|
||||
return {"block_type": 15, "code": {"language": "Plain Text", "elements": [{"text_run": {"content": code, "text_element_style": {}}}]}}
|
||||
|
||||
|
||||
def md_to_blocks(md_path: Path, file_tokens: dict[str, str]) -> list:
|
||||
"""将 Markdown 解析为飞书 blocks。file_tokens: {相对路径或文件名: file_token}"""
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
blocks = []
|
||||
lines = text.split("\n")
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# 一级标题
|
||||
if line.startswith("# ") and not line.startswith("## "):
|
||||
blocks.append(_h1(line[2:].strip()))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 二级标题
|
||||
if line.startswith("## ") and not line.startswith("### "):
|
||||
blocks.append(_h2(line[3:].strip()))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 三级标题
|
||||
if line.startswith("### "):
|
||||
blocks.append(_h3(line[4:].strip()))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 代码块:飞书 code block API 易报 invalid param,暂以文本块呈现
|
||||
if line.strip().startswith("```"):
|
||||
lang_raw = line.strip()[3:].strip()
|
||||
code_lines = []
|
||||
i += 1
|
||||
while i < len(lines) and not lines[i].strip().startswith("```"):
|
||||
code_lines.append(lines[i])
|
||||
i += 1
|
||||
if i < len(lines):
|
||||
i += 1
|
||||
code = "\n".join(code_lines)
|
||||
blocks.append(_text_block(f"```{lang_raw}\n{code}\n```"))
|
||||
continue
|
||||
|
||||
# 图片 :飞书 gallery/image 插入 API 易报 invalid param,用占位符 + 提示
|
||||
m = re.match(r'!\[([^\]]*)\]\(([^)]+)\)', line.strip())
|
||||
if m:
|
||||
alt, path = m.group(1), m.group(2)
|
||||
resolved = (md_path.parent / path).resolve()
|
||||
# 图片已上传到文档素材,但 API 插入块易失败,用占位符;用户可手动「插入→图片→文档素材」
|
||||
blocks.append(_text_block(f"📷 [图片: {alt or Path(path).name}](已上传至文档素材,可在飞书中插入)"))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 引用块 >
|
||||
if line.startswith("> "):
|
||||
blocks.append(_text_block(line[2:].strip()))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 分隔线
|
||||
if line.strip() in ("---", "***", "___"):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 空行
|
||||
if not line.strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 普通段落
|
||||
blocks.append(_text_block(line.rstrip()))
|
||||
i += 1
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def upload_md_to_feishu(md_path: Path, parent_token: str = PARENT_TOKEN) -> tuple[bool, str]:
|
||||
"""将 Markdown 上传到飞书 Wiki,有同名则更新"""
|
||||
token = fwd.get_token(parent_token)
|
||||
if not token:
|
||||
return False, "Token 无效"
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
title = md_path.stem
|
||||
if not title:
|
||||
title = md_path.name
|
||||
|
||||
r = requests.get(
|
||||
f"https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token={parent_token}",
|
||||
headers=headers, timeout=30)
|
||||
if r.json().get("code") != 0:
|
||||
return False, r.json().get("msg", "get_node 失败")
|
||||
space_id = r.json()["data"]["node"].get("space_id") or \
|
||||
(r.json()["data"]["node"].get("space") or {}).get("space_id") or \
|
||||
r.json()["data"]["node"].get("origin_space_id")
|
||||
if not space_id:
|
||||
return False, "无法获取 space_id"
|
||||
|
||||
doc_token = None
|
||||
node_token = None
|
||||
nodes = []
|
||||
page_token = None
|
||||
while True:
|
||||
params = {"parent_node_token": parent_token, "page_size": 50}
|
||||
if page_token:
|
||||
params["page_token"] = page_token
|
||||
rr = requests.get(
|
||||
f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{space_id}/nodes",
|
||||
headers=headers, params=params, timeout=30)
|
||||
if rr.json().get("code") != 0:
|
||||
break
|
||||
data = rr.json().get("data", {})
|
||||
nodes = data.get("nodes", []) or data.get("items", [])
|
||||
for n in nodes:
|
||||
t = n.get("title", "") or n.get("node", {}).get("title", "")
|
||||
if title in t or "基因胶囊" in t:
|
||||
doc_token = n.get("obj_token") or n.get("node_token")
|
||||
node_token = n.get("node_token")
|
||||
break
|
||||
if doc_token:
|
||||
break
|
||||
page_token = data.get("page_token")
|
||||
if not page_token:
|
||||
break
|
||||
|
||||
if not doc_token:
|
||||
create_r = requests.post(
|
||||
f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{space_id}/nodes",
|
||||
headers=headers,
|
||||
json={
|
||||
"parent_node_token": parent_token,
|
||||
"obj_type": "docx",
|
||||
"node_type": "origin",
|
||||
"title": title,
|
||||
},
|
||||
timeout=30)
|
||||
cd = create_r.json()
|
||||
if cd.get("code") != 0:
|
||||
return False, cd.get("msg", str(cd))
|
||||
doc_token = cd.get("data", {}).get("node", {}).get("obj_token")
|
||||
node_token = cd.get("data", {}).get("node", {}).get("node_token")
|
||||
if not doc_token:
|
||||
doc_token = node_token
|
||||
print("📄 创建新文档")
|
||||
else:
|
||||
print("📋 更新已有文档")
|
||||
|
||||
file_tokens = {}
|
||||
for m in re.finditer(r'!\[([^\]]*)\]\(([^)]+)\)', md_path.read_text(encoding="utf-8")):
|
||||
path = m.group(2)
|
||||
resolved = (md_path.parent / path).resolve()
|
||||
if resolved.exists():
|
||||
ft = upload_image_to_doc(token, doc_token, resolved)
|
||||
if ft:
|
||||
file_tokens[str(resolved)] = ft
|
||||
file_tokens[path] = ft
|
||||
file_tokens[resolved.name] = ft
|
||||
print(f"✅ 图片上传: {resolved.name}")
|
||||
|
||||
if doc_token and doc_token != node_token:
|
||||
child_ids = []
|
||||
pt = None
|
||||
while True:
|
||||
params = {"page_size": 100}
|
||||
if pt:
|
||||
params["page_token"] = pt
|
||||
rb = requests.get(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks",
|
||||
headers=headers, params=params, timeout=30)
|
||||
if rb.json().get("code") != 0:
|
||||
break
|
||||
data = rb.json().get("data", {})
|
||||
items = data.get("items", [])
|
||||
for b in items:
|
||||
if b.get("parent_id") == doc_token:
|
||||
child_ids.append(b["block_id"])
|
||||
pt = data.get("page_token")
|
||||
if not pt:
|
||||
break
|
||||
if child_ids:
|
||||
for j in range(0, len(child_ids), 50):
|
||||
batch = child_ids[j : j + 50]
|
||||
requests.delete(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{doc_token}/children/batch_delete",
|
||||
headers=headers, json={"block_id_list": batch}, timeout=30)
|
||||
|
||||
blocks = md_to_blocks(md_path, file_tokens)
|
||||
|
||||
for i in range(0, len(blocks), 50):
|
||||
batch = blocks[i : i + 50]
|
||||
wr = requests.post(
|
||||
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc_token}/blocks/{doc_token}/children",
|
||||
headers=headers,
|
||||
json={"children": batch, "index": i},
|
||||
timeout=30)
|
||||
if wr.json().get("code") != 0:
|
||||
return False, wr.json().get("msg", "写入失败")
|
||||
import time
|
||||
time.sleep(0.3)
|
||||
|
||||
url = f"https://cunkebao.feishu.cn/wiki/{node_token}"
|
||||
return True, url
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Markdown 直接上传到飞书 Wiki")
|
||||
ap.add_argument("md", nargs="?", default="/Users/karuo/Documents/个人/2、我写的日记/火:开发分享/卡若:基因胶囊——AI技能可遗传化的实现与落地.md", help="Markdown 文件路径")
|
||||
ap.add_argument("--parent", default=PARENT_TOKEN, help="父节点 token")
|
||||
args = ap.parse_args()
|
||||
|
||||
md_path = Path(args.md).expanduser().resolve()
|
||||
if not md_path.exists():
|
||||
print(f"❌ 文件不存在: {md_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 50)
|
||||
print(f"📤 Markdown 直接上传: {md_path.name}")
|
||||
print("=" * 50)
|
||||
ok, result = upload_md_to_feishu(md_path, args.parent)
|
||||
if ok:
|
||||
print(f"✅ 成功")
|
||||
print(f"📎 {result}")
|
||||
else:
|
||||
print(f"❌ 失败: {result}")
|
||||
sys.exit(1)
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
142
02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py
Normal file
142
02_卡人(水)/水桥_平台对接/飞书管理/脚本/md_to_feishu_json.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
将 Markdown 本地转换为飞书文档 JSON 格式。
|
||||
图片用占位符 __IMAGE:路径__ 标注,上传时替换为 file_token。
|
||||
|
||||
用法:
|
||||
python3 md_to_feishu_json.py input.md output.json
|
||||
python3 md_to_feishu_json.py input.md output.json --images img1.png,img2.png
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _h1(t):
|
||||
return {"block_type": 3, "heading1": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _h2(t):
|
||||
return {"block_type": 4, "heading2": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _h3(t):
|
||||
return {"block_type": 5, "heading3": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _text(t):
|
||||
return {"block_type": 2, "text": {"elements": [{"text_run": {"content": t, "text_element_style": {}}}], "style": {}}}
|
||||
|
||||
|
||||
def _image_placeholder(idx: int, path: str) -> dict:
|
||||
"""图片占位符,上传时由脚本替换为 gallery block"""
|
||||
return {"__image__": path, "__index__": idx}
|
||||
|
||||
|
||||
def md_to_blocks(md: str, image_paths: list[str] | None = None) -> list:
|
||||
"""将 Markdown 转为飞书 blocks"""
|
||||
blocks = []
|
||||
image_paths = image_paths or []
|
||||
img_idx = 0
|
||||
|
||||
in_code = False
|
||||
code_lines = []
|
||||
for line in md.split("\n"):
|
||||
if line.strip().startswith("```"):
|
||||
if in_code:
|
||||
blocks.append(_text("```\n" + "\n".join(code_lines) + "\n```"))
|
||||
code_lines = []
|
||||
in_code = not in_code
|
||||
continue
|
||||
if in_code:
|
||||
code_lines.append(line)
|
||||
continue
|
||||
|
||||
# 图片语法 
|
||||
img_match = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", line.strip())
|
||||
if img_match:
|
||||
path = img_match.group(2)
|
||||
if img_idx < len(image_paths):
|
||||
path = image_paths[img_idx]
|
||||
blocks.append(_image_placeholder(img_idx, path))
|
||||
img_idx += 1
|
||||
continue
|
||||
|
||||
# 标题
|
||||
if line.startswith("# "):
|
||||
blocks.append(_h1(line[2:].strip()))
|
||||
elif line.startswith("## "):
|
||||
blocks.append(_h2(line[3:].strip()))
|
||||
elif line.startswith("### "):
|
||||
blocks.append(_h3(line[4:].strip()))
|
||||
elif line.strip():
|
||||
blocks.append(_text(line))
|
||||
else:
|
||||
blocks.append(_text(""))
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def blocks_to_upload_format(blocks: list, base_dir: Path) -> tuple[list, list]:
|
||||
"""
|
||||
将含 __image__ 占位符的 blocks 转为可上传格式。
|
||||
返回 (文本 blocks 列表, 图片路径列表,按出现顺序)。
|
||||
image_paths 优先存相对路径(相对 base_dir),便于 JSON 移植。
|
||||
"""
|
||||
out = []
|
||||
paths = []
|
||||
for b in blocks:
|
||||
if isinstance(b, dict) and "__image__" in b:
|
||||
path = b.get("__image__", "")
|
||||
resolved = None
|
||||
if path and (base_dir / path).exists():
|
||||
resolved = base_dir / path
|
||||
elif path and Path(path).exists():
|
||||
resolved = Path(path).resolve()
|
||||
if resolved:
|
||||
try:
|
||||
rel = str(resolved.relative_to(base_dir))
|
||||
except ValueError:
|
||||
rel = str(resolved)
|
||||
paths.append(rel)
|
||||
else:
|
||||
paths.append(path if path else "unknown")
|
||||
out.append({"block_type": 2, "text": {"elements": [{"text_run": {"content": f"【配图 {len(paths)}:待上传】", "text_element_style": {}}}], "style": {}}})
|
||||
else:
|
||||
out.append(b)
|
||||
return out, paths
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("input", help="Markdown 文件")
|
||||
ap.add_argument("output", help="输出 JSON 文件")
|
||||
ap.add_argument("--images", default="", help="图片路径,逗号分隔(按序对应 ![]())")
|
||||
args = ap.parse_args()
|
||||
|
||||
inp = Path(args.input)
|
||||
if not inp.exists():
|
||||
print(f"❌ 文件不存在: {inp}")
|
||||
sys.exit(1)
|
||||
|
||||
md = inp.read_text(encoding="utf-8")
|
||||
image_paths = [p.strip() for p in args.images.split(",") if p.strip()]
|
||||
blocks = md_to_blocks(md, image_paths)
|
||||
final, img_paths = blocks_to_upload_format(blocks, inp.parent)
|
||||
|
||||
out = {
|
||||
"description": f"由 {inp.name} 转换的飞书 docx blocks",
|
||||
"source": str(inp),
|
||||
"image_paths": img_paths,
|
||||
"children": final,
|
||||
}
|
||||
Path(args.output).write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"✅ 已写入 {args.output}")
|
||||
if img_paths:
|
||||
print(f" 图片占位: {len(img_paths)} 处 → 需在上传时替换为 file_token")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -83,3 +83,4 @@
|
||||
| 2026-02-22 11:07:02 | 🔄 卡若AI 同步 2026-02-22 11:07 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 8 个 |
|
||||
| 2026-02-22 11:32:57 | 🔄 卡若AI 同步 2026-02-22 11:32 | 更新:金仓、运营中枢工作台 | 排除 >20MB: 8 个 |
|
||||
| 2026-02-22 11:40:59 | 🔄 卡若AI 同步 2026-02-22 11:40 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 |
|
||||
| 2026-02-22 11:44:40 | 🔄 卡若AI 同步 2026-02-22 11:44 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 |
|
||||
|
||||
@@ -86,3 +86,4 @@
|
||||
| 2026-02-22 11:07:02 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:07 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-22 11:32:57 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:32 | 更新:金仓、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-22 11:40:59 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:40 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-22 11:44:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:44 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user