zhoujiaangyao commited on
Commit ·
3985de6
1
Parent(s): f966728
fix(feishu): rpc_failed 自动重试 + 剔除抓不到的本地图片
Browse files
backend/app/services/feishu_service.py
CHANGED
|
@@ -155,15 +155,29 @@ class FeishuService:
|
|
| 155 |
content = prepared.encode("utf-8")
|
| 156 |
|
| 157 |
folder_token = self.folder_token or self._root_folder_token()
|
| 158 |
-
file_token = self._upload_media(safe_title, content, folder_token)
|
| 159 |
-
ticket = self._create_import_task(safe_title, file_token, folder_token)
|
| 160 |
-
result = self._poll_import_task(ticket)
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
# ─── 导入流程内部步骤 ─────────────────────────────────────────────────────
|
| 169 |
def _upload_media(self, title: str, content: bytes, folder_token: str) -> str:
|
|
@@ -275,19 +289,21 @@ class FeishuService:
|
|
| 275 |
|
| 276 |
@staticmethod
|
| 277 |
def _prepare_markdown(markdown: str, image_base_url: Optional[str]) -> str:
|
| 278 |
-
"""
|
| 279 |
|
| 280 |
-
飞书
|
| 281 |
-
|
| 282 |
-
|
| 283 |
"""
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
|
| 288 |
def _repl(m: "re.Match[str]") -> str:
|
| 289 |
alt, path = m.group(1), m.group(2)
|
| 290 |
-
return f""
|
| 291 |
|
| 292 |
-
# 仅
|
| 293 |
return re.sub(r"!\[([^\]]*)\]\((/(?:static|uploads)/[^)]+)\)", _repl, markdown)
|
|
|
|
| 155 |
content = prepared.encode("utf-8")
|
| 156 |
|
| 157 |
folder_token = self.folder_token or self._root_folder_token()
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
# rpc_failed 多为飞书侧瞬时错误:整条导入流程自动重试几次。
|
| 160 |
+
last_exc: Optional[FeishuError] = None
|
| 161 |
+
for attempt in range(3):
|
| 162 |
+
try:
|
| 163 |
+
file_token = self._upload_media(safe_title, content, folder_token)
|
| 164 |
+
ticket = self._create_import_task(safe_title, file_token, folder_token)
|
| 165 |
+
result = self._poll_import_task(ticket)
|
| 166 |
+
token = result.get("token") or ""
|
| 167 |
+
doc_type = result.get("type") or "docx"
|
| 168 |
+
url = result.get("url") or self._fallback_doc_url(doc_type, token)
|
| 169 |
+
logger.info(f"飞书导入成功:{safe_title} -> {url}")
|
| 170 |
+
return {"url": url, "token": token, "type": doc_type, "title": safe_title}
|
| 171 |
+
except FeishuError as exc:
|
| 172 |
+
last_exc = exc
|
| 173 |
+
if "rpc_failed" in (exc.message or "") and attempt < 2:
|
| 174 |
+
logger.warning(f"飞书导入 rpc_failed,第 {attempt + 1} 次重试…")
|
| 175 |
+
time.sleep(2)
|
| 176 |
+
continue
|
| 177 |
+
raise
|
| 178 |
+
if last_exc:
|
| 179 |
+
raise last_exc
|
| 180 |
+
raise FeishuError("飞书导入失败:未知错误")
|
| 181 |
|
| 182 |
# ─── 导入流程内部步骤 ─────────────────────────────────────────────────────
|
| 183 |
def _upload_media(self, title: str, content: bytes, folder_token: str) -> str:
|
|
|
|
| 289 |
|
| 290 |
@staticmethod
|
| 291 |
def _prepare_markdown(markdown: str, image_base_url: Optional[str]) -> str:
|
| 292 |
+
"""处理站内相对图片(/static、/uploads):
|
| 293 |
|
| 294 |
+
- 若 image_base_url 是飞书服务端能抓到的「公网 http(s)」地址 → 补成绝对地址,图片入文档。
|
| 295 |
+
- 若是本机/内网(localhost / 127.* / 内网段)或为空 → 飞书根本抓不到,直接**删掉**这些图片,
|
| 296 |
+
只保留正文。避免飞书导入时为抓一张抓不到的图而整单失败(rpc_failed)。
|
| 297 |
"""
|
| 298 |
+
base = (image_base_url or "").rstrip("/")
|
| 299 |
+
public = bool(re.match(r"^https?://", base)) and not re.search(
|
| 300 |
+
r"://(localhost|127\.|0\.0\.0\.0|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|\[?::1)",
|
| 301 |
+
base,
|
| 302 |
+
)
|
| 303 |
|
| 304 |
def _repl(m: "re.Match[str]") -> str:
|
| 305 |
alt, path = m.group(1), m.group(2)
|
| 306 |
+
return f"" if public else ""
|
| 307 |
|
| 308 |
+
# 仅处理 ](/static...) 与 ](/uploads...) 这类站内相对图片
|
| 309 |
return re.sub(r"!\[([^\]]*)\]\((/(?:static|uploads)/[^)]+)\)", _repl, markdown)
|