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

This commit is contained in:
2026-03-02 03:05:35 +08:00
parent 66ce62298f
commit e9978c1669
8 changed files with 138 additions and 27 deletions

View File

@@ -383,6 +383,9 @@ python3 脚本/batch_upload_json_to_feishu_wiki.py /path/to/本地目录 --wiki-
```
- 目录结构会原样还原为 Wiki 子节点;多维表格仍依赖用户身份权限,失败项会列在最终汇总中。
- **内容保证**文档写入若遇「invalid param / block not support」会自动用「标题 + 全文」回退建文档,保证每个 JSON 都有对应文档非多维表格类失败会再试一次回退。iframe/思维笔记等不支持块会转为正文或链接。
- **多维表格权限与重新授权**后台开通「用户身份权限」bitable:app、base:app:create 后,**必须重新授权**才能拿到带新权限的 Token。操作运行 `python3 脚本/feishu_force_reauth.py`(会删除旧 Token 并打开授权页);在浏览器完成飞书扫码授权。若本机未启动回调服务,先运行 `python3 脚本/feishu_api.py``bash start.sh`,再完成授权。授权后再执行批量上传即可。
- **上传后校验**:脚本结束会打印「成功 X/总数 Y」可打开 Wiki 链接逐层核对子目录与文档数量是否与本地一致。
---
@@ -463,6 +466,7 @@ python3 script.py --arg value
├── feishu_wiki_create_doc.py # Wiki 子文档创建(日记/研究)
├── upload_json_to_feishu_doc.py # 飞书导出 JSON 按原格式上传(文档/多维表格/问卷等)
├── batch_upload_json_to_feishu_wiki.py # 目录下全部 JSON 按目录结构批量上传到指定 Wiki 节点
├── feishu_force_reauth.py # 强制重新授权(删旧 Token、打开带多维表格权限的授权页
└── .feishu_tokens.json # Token 存储
```

View File

@@ -34,10 +34,12 @@
## 三、用户重新授权(必须)
- 权限开通并发布后,**已授权用户不会自动获得新权限**,必须**重新走一遍授权**,拿到新的 access_token / refresh_token 才会带上述权限。
- 操作任选其一
- **方式 A**:本机运行一次 `python3 脚本/auto_log.py`,在需要时按提示用飞书扫码/授权(会用新 scope 拉授权页)。
- **方式 B**:用浏览器打开本地服务 `http://localhost:5050` 的授权入口,重新授权一次。
- 授权成功后,再执行上传脚本即可创建多维表格。
- **推荐**:本机执行一次 **强制重新授权**(会删除旧 Token 并打开带「多维表格」权限的授权页)
```bash
python3 脚本/feishu_force_reauth.py
```
完成飞书扫码/授权后,再运行上传或批量上传脚本即可。
- 或:运行 `python3 脚本/auto_log.py` 在需要时按提示授权;或打开 `http://localhost:5050` 的授权入口重新授权。
---

View File

@@ -1,6 +0,0 @@
{
"access_token": "u-7UdzAmYi576o0FmONFJQh4l5mqoBk1ipO8aaFBM00BO2",
"refresh_token": "ur-40tvc.eGNfRbWU4UWQvvUWl5kUMBk1WVhoaaUMw00wOi",
"name": "飞书用户",
"auth_time": "2026-03-02T02:30:21.403787"
}

View File

@@ -78,17 +78,30 @@ def upload_one_json(
json_path: Path,
parent_token: str,
access_token: str,
fallback_only: bool = False,
) -> tuple[bool, str]:
"""上传单个 JSON 到指定父节点下。返回 (成功, url或信息)。"""
"""上传单个 JSON 到指定父节点下。fallback_only=True 时仅用「标题+全文」建文档。返回 (成功, url或信息)。"""
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
export_type, name = detect_export_type(data)
title = (data.get("content") or name or "未命名").split("\n")[0].strip() or name or "未命名"
def fallback_doc():
raw = (data.get("content") or "").strip() or title
return [
{"block_type": 3, "heading1": {"elements": [{"text_run": {"content": title, "text_element_style": {}}}], "style": {}}},
{"block_type": 2, "text": {"elements": [{"text_run": {"content": raw[:50000], "text_element_style": {}}}], "style": {}}},
]
if fallback_only:
ok, result = create_wiki_doc(parent_token, title, fallback_doc())
return ok, result
if export_type == "bitable":
app_token, err = create_bitable_app(access_token, name)
if not app_token:
return False, f"多维表格创建失败:{err}"
url = f"{FEISHU_BASE_URL}/{app_token}"
# 在 Wiki 下建一篇文档,标题 + 链接到多维表格
blocks = [
{"block_type": 3, "heading1": {"elements": [{"text_run": {"content": name, "text_element_style": {}}}], "style": {}}},
{"block_type": 2, "text": {"elements": [{"text_run": {"content": f"多维表格链接:{url}", "text_element_style": {}}}], "style": {}}},
@@ -98,6 +111,12 @@ def upload_one_json(
title, children = blocks_from_export_json(data)
children = resolve_bitable_placeholders(children, access_token, default_name=name or "多维表格")
ok, result = create_wiki_doc(parent_token, title, children)
if ok:
return ok, result
# 失败时回退:用「标题 + 全文」建一篇文档,保证内容不丢
if "invalid param" in result or "block not support" in result.lower():
ok2, result2 = create_wiki_doc(parent_token, title, fallback_doc())
return ok2, result2
return ok, result
@@ -151,9 +170,27 @@ def main():
failed.append((rel, result))
time.sleep(0.4)
# 对非「多维表格权限」的失败项用「标题+全文」再试一次,尽量保证每文件都有文档
retried = []
for rel, msg in failed[:]:
if "多维表格创建失败" in msg:
continue
parent_rel = str(Path(rel).parent) if Path(rel).parent != Path(".") else ""
parent_token = token_map.get(parent_rel, args.wiki_parent)
path = root_dir / rel
ok, result = upload_one_json(path, parent_token, token, fallback_only=True)
if ok:
retried.append(rel)
failed.remove((rel, msg))
time.sleep(0.3)
if retried:
print(f"🔄 回退上传成功 {len(retried)} 个:{retried[:5]}{'...' if len(retried) > 5 else ''}")
print("=" * 60)
success_count = len(files) - len(failed)
print(f"📊 合计:成功 {success_count}/{len(files)},失败 {len(failed)}")
if failed:
print(f"⚠️ 失败 {len(failed)} 个:")
print(f"⚠️ 失败 {len(failed)}(多为多维表格需用户身份权限)")
for rel, msg in failed:
print(f" {rel}: {msg}")
else:

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
强制重新授权:删除本地 Token并打开带「多维表格」scope 的授权页。
权限开通后必须执行一次,新 Token 才会包含 bitable:app、base:app:create上传多维表格才能成功。
用法: python3 feishu_force_reauth.py
"""
import os
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
TOKEN_FILE = SCRIPT_DIR / ".feishu_tokens.json"
# 与 auto_log 一致,含 bitable + base:app:create
APP_ID = "cli_a48818290ef8100d"
SERVICE_PORT = 5050
SCOPE = "wiki:wiki+docx:document+drive:drive+bitable:app+base:app:create"
AUTH_URL = (
f"https://open.feishu.cn/open-apis/authen/v1/authorize"
f"?app_id={APP_ID}"
f"&redirect_uri=http%3A//localhost%3A{SERVICE_PORT}/api/auth/callback"
f"&scope={SCOPE}"
)
def main():
if TOKEN_FILE.exists():
TOKEN_FILE.unlink()
print("✅ 已删除本地 Token下次使用会走重新授权")
else:
print(" 本地无 Token 文件,将直接打开授权页")
print("📎 正在打开飞书授权页(含多维表格权限)…")
try:
subprocess.run(["open", AUTH_URL], check=True, capture_output=True)
except Exception:
print(f"请手动在浏览器打开:\n{AUTH_URL}")
print("授权完成后,再运行 batch_upload_json_to_feishu_wiki.py 或 upload_json_to_feishu_doc.py 即可。")
if __name__ == "__main__":
main()

View File

@@ -10,6 +10,7 @@ import json
import sys
import time
import argparse
import urllib.parse
import requests
from pathlib import Path
@@ -83,8 +84,31 @@ def detect_export_type(data: dict) -> tuple[str, str]:
return "docx", name_from_page or "未命名"
def _extract_text_or_url_from_block(b: dict) -> str:
"""从任意块中提取可展示的文本或 URL用于不支持类型的回退。"""
if b.get("text") and b["text"].get("elements"):
return "".join(el.get("text_run", {}).get("content", "") for el in b["text"]["elements"]).strip()
for key in ("heading1", "heading2", "heading3", "heading4"):
if b.get(key) and b[key].get("elements"):
return "".join(el.get("text_run", {}).get("content", "") for el in b[key]["elements"]).strip()
if b.get("iframe") and b["iframe"].get("component"):
url = b["iframe"]["component"].get("url", "")
if url:
return urllib.parse.unquote(url)
if b.get("mindnote") and b["mindnote"].get("token"):
return f"[思维笔记] token: {b['mindnote']['token']}"
if b.get("board") or b.get("bitable"):
return "[多维表格]"
return ""
def _text_block(content: str) -> dict:
"""构造一个正文块。"""
return {"block_type": 2, "text": {"elements": [{"text_run": {"content": content or " ", "text_element_style": {}}}], "style": {}}}
def _to_api_block(b: dict) -> dict | None:
"""将导出块转为 API 可用的块(去掉 block_id、parent_id,保留 block_type 与类型字段)"""
"""将导出块转为 API 可用的块(去掉 block_id、parent_id)。不支持的类型返回 None由调用方用 _extract + _text_block 回退"""
bt = b.get("block_type")
out = {"block_type": bt}
if bt == 2 and b.get("text"):
@@ -93,6 +117,10 @@ def _to_api_block(b: dict) -> dict | None:
out["heading1"] = b["heading1"]
elif bt == 4 and b.get("heading2"):
out["heading2"] = b["heading2"]
elif bt == 5 and (b.get("heading2") or b.get("heading3")):
# 三级标题API 部分环境不支持 block_type 5用 heading2(4) + 相同结构
out["block_type"] = 4
out["heading2"] = b.get("heading2") or b["heading3"]
elif bt == 6 and b.get("heading4"):
out["heading4"] = b["heading4"]
elif bt == 17 and b.get("todo"):
@@ -100,20 +128,19 @@ def _to_api_block(b: dict) -> dict | None:
elif bt == 19 and b.get("callout"):
out["callout"] = b["callout"]
elif bt == 43:
# 多维表格:导出为 board.tokenAPI 为 bitable.token占位后续用新建的 app_token 替换
token = (b.get("board") or b.get("bitable") or {}).get("token", "")
out["_bitable_placeholder"] = True
out["_bitable_token"] = token # 可能为原文档 token同租户可尝试直接嵌
out["bitable"] = {"token": token or "PLACEHOLDER"}
elif bt in (26, 29) or (bt not in (1,) and not any(b.get(k) for k in ("text", "heading1", "heading2", "heading3", "heading4", "todo", "callout", "board", "bitable"))):
# iframe(26)、mindnote(29) 等 API 不支持:不在此返回,由上层转为正文
return None
else:
# 其他类型尽量透传类型字段
for key in ("page", "board", "bitable", "sheet", "mindnote", "poll"):
if key in b and not key.startswith("_"):
out[key] = b[key]
break
if "_bitable_placeholder" not in out and "bitable" not in out and "board" in b:
out["_bitable_placeholder"] = True
out["bitable"] = {"token": (b.get("board") or {}).get("token", "PLACEHOLDER")}
for key in ("board", "bitable"):
if key in b:
out["_bitable_placeholder"] = True
out["bitable"] = {"token": (b.get("board") or b.get("bitable") or {}).get("token", "PLACEHOLDER")}
return out
return None
return out
@@ -184,11 +211,14 @@ def blocks_from_export_json(data: dict) -> tuple[str, list]:
elif bt == 43 and (b.get("board") or b.get("bitable")):
token = (b.get("board") or b.get("bitable") or {}).get("token", "")
children.append({"_bitable_placeholder": True, "block_type": 43, "bitable": {"token": token}, "name": "流量来源"})
elif bt not in (2, 43):
else:
api_block = _to_api_block(b)
if api_block and not api_block.get("_bitable_placeholder"):
if api_block:
children.append(api_block)
else:
fallback = _extract_text_or_url_from_block(b)
if fallback or bt in (26, 29):
children.append(_text_block(fallback or "[嵌入内容]"))
return title, children

View File

@@ -196,3 +196,4 @@
| 2026-03-02 02:35:50 | 🔄 卡若AI 同步 2026-03-02 02:35 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
| 2026-03-02 02:41:10 | 🔄 卡若AI 同步 2026-03-02 02:41 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
| 2026-03-02 02:45:42 | 🔄 卡若AI 同步 2026-03-02 02:45 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
| 2026-03-02 02:59:50 | 🔄 卡若AI 同步 2026-03-02 02:59 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |

View File

@@ -199,3 +199,4 @@
| 2026-03-02 02:35:50 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 02:35 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-02 02:41:10 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 02:41 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-02 02:45:42 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 02:45 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-02 02:59:50 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-02 02:59 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |