🔄 卡若AI 同步 2026-02-25 12:27 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 13 个

This commit is contained in:
2026-02-25 12:27:46 +08:00
parent 0fc1e550fe
commit 668afa27cf
5 changed files with 163 additions and 16 deletions

View File

@@ -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
# 文档标题
## 一、背景
一句话说明。
## 二、配图示例
![配图1](../../图片/你的图片1.png)
![配图2](../../图片/你的图片2.png)
## 三、代码示例
```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 表格自动转飞书表格块并回填**

View 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
# 你的文档标题
## 一、背景
一句话说明背景。
## 二、配图
![配图1](../../图片/图1.png)
![配图2](../../图片/图2.png)
## 三、代码
```bash
python3 your_script.py --dry-run
```
## 四、表格
| 模块 | 作用 | 备注 |
| --- | --- | --- |
| manifest | 元信息 | name/version/owner |
| skill_content | 技能正文 | 规则与步骤 |
````
## 映射说明
- `# / ## / ###` -> 飞书标题块
- 普通文本 -> 飞书文本块
- `![...](...)` -> 上传素材并替换为图片/文件块(失败保底文字)
- 代码块 -> 转为正文代码说明行
- Markdown 表格 -> 飞书文档内电子表格块(自动回填单元格)

View File

@@ -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:

View File

@@ -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 个 |

View File

@@ -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) |