🔄 卡若AI 同步 2026-03-02 03:05 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个
This commit is contained in:
@@ -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 存储
|
||||
```
|
||||
|
||||
|
||||
@@ -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` 的授权入口重新授权。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"access_token": "u-7UdzAmYi576o0FmONFJQh4l5mqoBk1ipO8aaFBM00BO2",
|
||||
"refresh_token": "ur-40tvc.eGNfRbWU4UWQvvUWl5kUMBk1WVhoaaUMw00wOi",
|
||||
"name": "飞书用户",
|
||||
"auth_time": "2026-03-02T02:30:21.403787"
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
42
02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py
Normal file
42
02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py
Normal 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()
|
||||
@@ -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.token,API 为 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
|
||||
|
||||
|
||||
|
||||
@@ -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 个 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user