From e9978c1669d08dba9d7caeee6a43ea76b8e021fa Mon Sep 17 00:00:00 2001 From: karuo Date: Mon, 2 Mar 2026 03:05:35 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20=E5=8D=A1=E8=8B=A5AI=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=202026-03-02=2003:05=20|=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E6=B0=B4=E6=A1=A5=E5=B9=B3=E5=8F=B0=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E3=80=81=E8=BF=90=E8=90=A5=E4=B8=AD=E6=9E=A2=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=20|=20=E6=8E=92=E9=99=A4=20>20MB:=2014=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md | 4 ++ .../参考资料/飞书多维表格权限开通说明_给卡罗维亚.md | 10 ++-- .../飞书管理/脚本/.feishu_tokens.json | 6 -- .../飞书管理/脚本/batch_upload_json_to_feishu_wiki.py | 43 +++++++++++++- .../飞书管理/脚本/feishu_force_reauth.py | 42 ++++++++++++++ .../飞书管理/脚本/upload_json_to_feishu_doc.py | 58 ++++++++++++++----- 运营中枢/工作台/gitea_push_log.md | 1 + 运营中枢/工作台/代码管理.md | 1 + 8 files changed, 138 insertions(+), 27 deletions(-) delete mode 100644 02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json create mode 100644 02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md b/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md index e47b2255..e7b44f25 100755 --- a/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md +++ b/02_卡人(水)/水桥_平台对接/飞书管理/SKILL.md @@ -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 存储 ``` diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书多维表格权限开通说明_给卡罗维亚.md b/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书多维表格权限开通说明_给卡罗维亚.md index c7e23572..dc1c3253 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书多维表格权限开通说明_给卡罗维亚.md +++ b/02_卡人(水)/水桥_平台对接/飞书管理/参考资料/飞书多维表格权限开通说明_给卡罗维亚.md @@ -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` 的授权入口重新授权。 --- diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json deleted file mode 100644 index 908dcd06..00000000 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "access_token": "u-7UdzAmYi576o0FmONFJQh4l5mqoBk1ipO8aaFBM00BO2", - "refresh_token": "ur-40tvc.eGNfRbWU4UWQvvUWl5kUMBk1WVhoaaUMw00wOi", - "name": "飞书用户", - "auth_time": "2026-03-02T02:30:21.403787" -} \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/batch_upload_json_to_feishu_wiki.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/batch_upload_json_to_feishu_wiki.py index fa245d9a..16a92c59 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/batch_upload_json_to_feishu_wiki.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/batch_upload_json_to_feishu_wiki.py @@ -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: diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py new file mode 100644 index 00000000..4a2c462a --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_force_reauth.py @@ -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() diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/upload_json_to_feishu_doc.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/upload_json_to_feishu_doc.py index c686489f..638d0fcf 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/upload_json_to_feishu_doc.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/upload_json_to_feishu_doc.py @@ -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 diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 13ef53ad..ae7c7a34 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 2ea36d20..187aaf08 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |