🔄 卡若AI 同步 2026-02-25 12:27 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个
This commit is contained in:
@@ -328,6 +328,61 @@ JSON 格式:与 `团队入职流程与新人登记表_feishu_blocks.json` 相
|
||||
|
||||
---
|
||||
|
||||
## 统一文章上传(强制入口)
|
||||
|
||||
用于“本地 Markdown → 飞书 Wiki 文档”的统一发布。
|
||||
**规则**:同名/近似名优先更新;命中近似名时优先改名后更新,不再重复新建。
|
||||
|
||||
```bash
|
||||
python3 /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_article_unified_publish.py \
|
||||
--parent MyvRwCVNSiTg5ok6e3fc6uA5nHg \
|
||||
--title "文档标题" \
|
||||
--md "/绝对路径/文章.md" \
|
||||
--json "/Users/karuo/Documents/卡若Ai的文件夹/导出/文章_feishu_blocks.json"
|
||||
```
|
||||
|
||||
### 本地写作模板(推荐直接复用)
|
||||
|
||||
````markdown
|
||||
# 文档标题
|
||||
|
||||
## 一、背景
|
||||
一句话说明。
|
||||
|
||||
## 二、配图示例
|
||||

|
||||

|
||||
|
||||
## 三、代码示例
|
||||
```bash
|
||||
python3 script.py --arg value
|
||||
```
|
||||
|
||||
## 四、表格示例
|
||||
| 模块 | 作用 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| manifest | 元数据 | name/owner/version |
|
||||
| skill_content | 技能正文 | 规则与流程 |
|
||||
````
|
||||
|
||||
### Markdown 到飞书 Block 映射(已固化)
|
||||
|
||||
| 本地写法 | 飞书块 |
|
||||
|:---|:---|
|
||||
| `# / ## / ###` | 标题块(3/4/5) |
|
||||
| 普通段落 | 文本块(2) |
|
||||
| `` | 图片上传 + 图片/文件块(27/12,失败保底文字) |
|
||||
| `````代码块````` | 文本块(前缀 `代码:`) |
|
||||
| Markdown 表格 | 文档内电子表格块(30)+ 自动回填单元格 |
|
||||
|
||||
### 图片路径匹配规则(已固化)
|
||||
|
||||
1. 先按 JSON 所在目录解析相对路径
|
||||
2. 若不存在,再按 `source`(原 Markdown 文件目录)解析
|
||||
3. 两者都不存在则提示缺图,不中断正文发布
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
@@ -383,5 +438,5 @@ python3 /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台
|
||||
|
||||
---
|
||||
|
||||
**版本**: v3.4 | **更新**: 2026-02-25
|
||||
**特性**: 静默授权、倒序插入、TNTWF规范、四象限分类、**按月份自动路由写入(防串月)**、**写前标题校验+写后双文档校验**、**运营报表子技能(截图→填表→发群竖状格式、会议纪要图片上传、月度统计)**
|
||||
**版本**: v3.5 | **更新**: 2026-02-25
|
||||
**特性**: 静默授权、倒序插入、TNTWF规范、四象限分类、**按月份自动路由写入(防串月)**、**写前标题校验+写后双文档校验**、**运营报表子技能(截图→填表→发群竖状格式、会议纪要图片上传、月度统计)**、**统一文章上传(同名/近似名改名更新)**、**Markdown 表格自动转飞书表格块并回填**
|
||||
|
||||
46
02_卡人(水)/水桥_平台对接/飞书管理/references/飞书文章本地模板_配图代码表格.md
Normal file
46
02_卡人(水)/水桥_平台对接/飞书管理/references/飞书文章本地模板_配图代码表格.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 飞书文章本地模板(配图/代码/表格)
|
||||
|
||||
> 用这份模板写本地 Markdown,再走统一上传脚本即可自动匹配飞书块格式。
|
||||
|
||||
## 使用方式
|
||||
|
||||
```bash
|
||||
python3 /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_article_unified_publish.py \
|
||||
--parent MyvRwCVNSiTg5ok6e3fc6uA5nHg \
|
||||
--title "你的文档标题" \
|
||||
--md "/绝对路径/你的文章.md" \
|
||||
--json "/Users/karuo/Documents/卡若Ai的文件夹/导出/你的文章_feishu_blocks.json"
|
||||
```
|
||||
|
||||
## 文章模板
|
||||
|
||||
````markdown
|
||||
# 你的文档标题
|
||||
|
||||
## 一、背景
|
||||
一句话说明背景。
|
||||
|
||||
## 二、配图
|
||||

|
||||

|
||||
|
||||
## 三、代码
|
||||
```bash
|
||||
python3 your_script.py --dry-run
|
||||
```
|
||||
|
||||
## 四、表格
|
||||
| 模块 | 作用 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| manifest | 元信息 | name/version/owner |
|
||||
| skill_content | 技能正文 | 规则与步骤 |
|
||||
````
|
||||
|
||||
## 映射说明
|
||||
|
||||
- `# / ## / ###` -> 飞书标题块
|
||||
- 普通文本 -> 飞书文本块
|
||||
- `` -> 上传素材并替换为图片/文件块(失败保底文字)
|
||||
- 代码块 -> 转为正文代码说明行
|
||||
- Markdown 表格 -> 飞书文档内电子表格块(自动回填单元格)
|
||||
|
||||
@@ -135,20 +135,24 @@ def _is_similar_title(a: str, b: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def find_existing_node_by_title(parent_token: str, title: str, headers: dict) -> tuple[str | None, str | None, str | None]:
|
||||
"""在父节点下查找同名/相似标题文档,返回(doc_token,node_token,node_title)"""
|
||||
def find_existing_node_by_title(
|
||||
parent_token: str, title: str, headers: dict
|
||||
) -> tuple[str | None, str | None, str | None, str | None]:
|
||||
"""在父节点下查找同名/相似标题文档,返回(doc_token,node_token,node_title,space_id)"""
|
||||
r = requests.get(
|
||||
f"https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token={parent_token}",
|
||||
headers=headers, timeout=30)
|
||||
j = r.json()
|
||||
if j.get("code") != 0:
|
||||
return None, None, None
|
||||
return None, None, None, None
|
||||
node = j["data"]["node"]
|
||||
space_id = node.get("space_id") or (node.get("space") or {}).get("space_id") or node.get("origin_space_id")
|
||||
if not space_id:
|
||||
return None, None, None
|
||||
return None, None, None, None
|
||||
|
||||
page_token = None
|
||||
best = None
|
||||
best_score = -1
|
||||
while True:
|
||||
params = {"parent_node_token": parent_token, "page_size": 50}
|
||||
if page_token:
|
||||
@@ -158,19 +162,54 @@ def find_existing_node_by_title(parent_token: str, title: str, headers: dict) ->
|
||||
headers=headers, params=params, timeout=30)
|
||||
nj = nr.json()
|
||||
if nj.get("code") != 0:
|
||||
return None, None, None
|
||||
return None, None, None, None
|
||||
data = nj.get("data", {}) or {}
|
||||
nodes = data.get("nodes", []) or data.get("items", []) or []
|
||||
for n in nodes:
|
||||
node_title = n.get("title", "") or n.get("node", {}).get("title", "")
|
||||
if _is_similar_title(node_title, title):
|
||||
obj = n.get("obj_token")
|
||||
node_token = n.get("node_token")
|
||||
return (obj or node_token), node_token, node_title
|
||||
if not _is_similar_title(node_title, title):
|
||||
continue
|
||||
obj = n.get("obj_token")
|
||||
node_token = n.get("node_token")
|
||||
na, nb = _normalize_title(node_title), _normalize_title(title)
|
||||
score = 100 if na == nb else 60 + min(len(na), len(nb))
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = ((obj or node_token), node_token, node_title, space_id)
|
||||
page_token = data.get("page_token")
|
||||
if not page_token:
|
||||
break
|
||||
return None, None, None
|
||||
return best or (None, None, None, space_id)
|
||||
|
||||
|
||||
def rename_node_title(space_id: str, node_token: str, new_title: str, headers: dict) -> bool:
|
||||
"""命中相似标题后,优先把节点标题改成目标标题。"""
|
||||
if not space_id or not node_token or not new_title:
|
||||
return False
|
||||
r = requests.patch(
|
||||
f"https://open.feishu.cn/open-apis/wiki/v2/spaces/{space_id}/nodes/{node_token}",
|
||||
headers=headers,
|
||||
json={"title": new_title},
|
||||
timeout=30,
|
||||
)
|
||||
try:
|
||||
j = r.json()
|
||||
except Exception:
|
||||
return False
|
||||
return j.get("code") == 0
|
||||
|
||||
|
||||
def resolve_image_full_path(raw_path: str, json_base_dir: Path, source_md_dir: Path | None) -> Path:
|
||||
p = Path(raw_path)
|
||||
if p.is_absolute() and p.exists():
|
||||
return p.resolve()
|
||||
candidates = [json_base_dir / p]
|
||||
if source_md_dir:
|
||||
candidates.append(source_md_dir / p)
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return c.resolve()
|
||||
return (json_base_dir / p).resolve()
|
||||
|
||||
|
||||
def resolve_doc_token(node_token: str, headers: dict) -> str:
|
||||
@@ -461,6 +500,8 @@ def main():
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
blocks = data.get("children", [])
|
||||
image_paths = data.get("image_paths", []) or []
|
||||
source_md = (data.get("source") or "").strip()
|
||||
source_md_dir = Path(source_md).expanduser().resolve().parent if source_md else None
|
||||
|
||||
token = fwd.get_token(args.target or args.parent)
|
||||
if not token:
|
||||
@@ -484,10 +525,15 @@ def main():
|
||||
print("⚠️ 清空失败,将以追加方式更新(仍不会新建重复文档)")
|
||||
else:
|
||||
# 默认:先查同名/相似标题,命中则更新,不再新建
|
||||
found_doc, found_node, found_title = find_existing_node_by_title(args.parent, args.title, headers)
|
||||
found_doc, found_node, found_title, found_space = find_existing_node_by_title(args.parent, args.title, headers)
|
||||
if found_doc and found_node:
|
||||
doc_token, node_token = found_doc, found_node
|
||||
print(f"📋 命中相似标题,改为更新: {found_title}")
|
||||
if found_title != args.title and found_space:
|
||||
if rename_node_title(found_space, node_token, args.title, headers):
|
||||
print(f"✅ 已将文档重命名为:{args.title}")
|
||||
else:
|
||||
print("⚠️ 文档重命名失败,继续按原文档更新内容")
|
||||
if clear_doc_blocks(doc_token, headers):
|
||||
print("✅ 已清空原内容")
|
||||
else:
|
||||
@@ -499,9 +545,7 @@ def main():
|
||||
# 上传图片
|
||||
file_tokens = []
|
||||
for p in image_paths:
|
||||
pth = Path(p)
|
||||
full = (base_dir / pth) if not pth.is_absolute() else pth
|
||||
full = full.resolve()
|
||||
full = resolve_image_full_path(p, base_dir, source_md_dir)
|
||||
ft = upload_image_to_doc(token, doc_token, full)
|
||||
file_tokens.append(ft)
|
||||
if ft:
|
||||
|
||||
@@ -143,3 +143,4 @@
|
||||
| 2026-02-25 12:07:57 | 🔄 卡若AI 同步 2026-02-25 12:07 | 更新:Cursor规则、水桥平台对接、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 12:09:29 | 🔄 卡若AI 同步 2026-02-25 12:09 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 12:11:44 | 🔄 卡若AI 同步 2026-02-25 12:11 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
| 2026-02-25 12:13:11 | 🔄 卡若AI 同步 2026-02-25 12:13 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 |
|
||||
|
||||
@@ -146,3 +146,4 @@
|
||||
| 2026-02-25 12:07:57 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 12:07 | 更新:Cursor规则、水桥平台对接、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 12:09:29 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 12:09 | 更新:运营中枢、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 12:11:44 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 12:11 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-02-25 12:13:11 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-25 12:13 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user