Refactor Feishu adapter to trim duplicated helpers

This commit is contained in:
Xinyi Wang
2026-03-13 16:51:50 +08:00
parent f16b5b7214
commit 44bda94a36
2 changed files with 59 additions and 124 deletions

View File

@@ -61,6 +61,14 @@ def public_access(allowed):
return not allowed or "*" in allowed return not allowed or "*" in allowed
def to_allowed_set(value):
if value is None:
return set()
if isinstance(value, str):
value = [value]
return {str(x).strip() for x in value if str(x).strip()}
def allowed_label(allowed): def allowed_label(allowed):
return "public" if public_access(allowed) else sorted(allowed) return "public" if public_access(allowed) else sorted(allowed)

169
fsapp.py
View File

@@ -1,11 +1,4 @@
import glob import json, os, re, sys, threading, time
import json
import os
import queue as Q
import re
import sys
import threading
import time
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_ROOT) sys.path.insert(0, PROJECT_ROOT)
@@ -15,9 +8,9 @@ import lark_oapi as lark
from lark_oapi.api.im.v1 import * from lark_oapi.api.im.v1 import *
from agentmain import GeneraticAgent from agentmain import GeneraticAgent
from chatapp_common import clean_reply, extract_files, format_restore, public_access, strip_files, to_allowed_set
from llmcore import mykeys from llmcore import mykeys
_TAG_PATS = [r"<" + t + r">.*?</" + t + r">" for t in ("thinking", "summary", "tool_use", "file_content")]
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"} _IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"}
_AUDIO_EXTS = {".opus", ".mp3", ".wav", ".m4a", ".aac"} _AUDIO_EXTS = {".opus", ".mp3", ".wav", ".m4a", ".aac"}
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"} _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
@@ -39,37 +32,13 @@ MEDIA_DIR = os.path.join(TEMP_DIR, "feishu_media")
os.makedirs(MEDIA_DIR, exist_ok=True) os.makedirs(MEDIA_DIR, exist_ok=True)
def _clean(text):
for pat in _TAG_PATS:
text = re.sub(pat, "", text, flags=re.DOTALL)
return re.sub(r"\n{3,}", "\n\n", text).strip() or "..."
def _extract_files(text):
return re.findall(r"\[FILE:([^\]]+)\]", text or "")
def _strip_files(text):
return re.sub(r"\[FILE:[^\]]+\]", "", text or "").strip()
def _display_text(text): def _display_text(text):
return _strip_files(_clean(text)) or "..." return strip_files(clean_reply(text)) or "..."
def _to_allowed_set(value):
if value is None:
return set()
if isinstance(value, str):
value = [value]
return {str(x).strip() for x in value if str(x).strip()}
def _parse_json(raw): def _parse_json(raw):
if not raw:
return {}
try: try:
return json.loads(raw) return json.loads(raw) if raw else {}
except Exception: except Exception:
return {} return {}
@@ -229,20 +198,16 @@ def _extract_post_content(content_json):
APP_ID = str(mykeys.get("fs_app_id", "") or "").strip() APP_ID = str(mykeys.get("fs_app_id", "") or "").strip()
APP_SECRET = str(mykeys.get("fs_app_secret", "") or "").strip() APP_SECRET = str(mykeys.get("fs_app_secret", "") or "").strip()
ALLOWED_USERS = _to_allowed_set(mykeys.get("fs_allowed_users", [])) ALLOWED_USERS = to_allowed_set(mykeys.get("fs_allowed_users", []))
PUBLIC_ACCESS = not ALLOWED_USERS or "*" in ALLOWED_USERS PUBLIC_ACCESS = public_access(ALLOWED_USERS)
agent = GeneraticAgent() agent = GeneraticAgent()
threading.Thread(target=agent.run, daemon=True).start() threading.Thread(target=agent.run, daemon=True).start()
client, user_tasks = None, {} client, user_tasks = None, {}
def create_client(): def create_client(): return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build()
return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build() def _card(text): return json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": text}]}, ensure_ascii=False)
def _card(text):
return json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": text}]}, ensure_ascii=False)
def send_message(open_id, content, msg_type="text", use_card=False): def send_message(open_id, content, msg_type="text", use_card=False):
@@ -272,6 +237,10 @@ def update_message(message_id, content):
return response.success() return response.success()
def _read_file_obj(response):
return response.file.read() if hasattr(response.file, "read") else response.file
def _upload_image_sync(file_path): def _upload_image_sync(file_path):
try: try:
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
@@ -305,51 +274,29 @@ def _upload_file_sync(file_path):
return None return None
def _download_image_sync(message_id, image_key): def _download_resource(message_id, file_key, resource_type, label):
try:
request = GetMessageResourceRequest.builder().message_id(message_id).file_key(image_key).type("image").build()
response = client.im.v1.message_resource.get(request)
if response.success():
data = response.file.read() if hasattr(response.file, "read") else response.file
return data, response.file_name
print(f"[ERROR] download image failed: {response.code}, {response.msg}")
except Exception as e:
print(f"[ERROR] download image failed {image_key}: {e}")
return None, None
def _download_file_sync(message_id, file_key, resource_type="file"):
if resource_type == "audio":
resource_type = "file"
try: try:
request = GetMessageResourceRequest.builder().message_id(message_id).file_key(file_key).type(resource_type).build() request = GetMessageResourceRequest.builder().message_id(message_id).file_key(file_key).type(resource_type).build()
response = client.im.v1.message_resource.get(request) response = client.im.v1.message_resource.get(request)
if response.success(): if response.success():
data = response.file.read() if hasattr(response.file, "read") else response.file return _read_file_obj(response), response.file_name
return data, response.file_name print(f"[ERROR] download {label} failed: {response.code}, {response.msg}")
print(f"[ERROR] download {resource_type} failed: {response.code}, {response.msg}")
except Exception as e: except Exception as e:
print(f"[ERROR] download {resource_type} failed {file_key}: {e}") print(f"[ERROR] download {label} failed {file_key}: {e}")
return None, None return None, None
def _download_and_save_media(msg_type, content_json, message_id): def _download_and_save_media(msg_type, content_json, message_id):
data, filename = None, None file_key = content_json.get("image_key") if msg_type == "image" else content_json.get("file_key")
if msg_type == "image": resource_type = "image" if msg_type == "image" else ("file" if msg_type == "audio" else msg_type)
image_key = content_json.get("image_key") default_ext = ".jpg" if msg_type == "image" else ".opus" if msg_type == "audio" else ""
if image_key and message_id: if not (file_key and message_id):
data, filename = _download_image_sync(message_id, image_key) return None, None
if not filename: data, filename = _download_resource(message_id, file_key, resource_type, msg_type)
filename = f"{image_key[:16]}.jpg" if data:
elif msg_type in ("audio", "file", "media"): filename = filename or f"{file_key[:16]}{default_ext}"
file_key = content_json.get("file_key")
if file_key and message_id:
data, filename = _download_file_sync(message_id, file_key, msg_type)
if not filename:
filename = file_key[:16]
if msg_type == "audio" and filename and not filename.endswith(".opus"): if msg_type == "audio" and filename and not filename.endswith(".opus"):
filename = f"{filename}.opus" filename += ".opus"
if data and filename:
file_path = os.path.join(MEDIA_DIR, os.path.basename(filename)) file_path = os.path.join(MEDIA_DIR, os.path.basename(filename))
with open(file_path, "wb") as f: with open(file_path, "wb") as f:
f.write(data) f.write(data)
@@ -358,13 +305,7 @@ def _download_and_save_media(msg_type, content_json, message_id):
def _describe_media(msg_type, file_path, filename): def _describe_media(msg_type, file_path, filename):
if msg_type == "image": return f"[{msg_type}: {filename}]\n[{'Image' if msg_type == 'image' else 'File'}: source: {file_path}]"
return f"[image: {filename}]\n[Image: source: {file_path}]"
if msg_type == "audio":
return f"[audio: {filename}]\n[File: source: {file_path}]"
if msg_type in ("file", "media"):
return f"[{msg_type}: {filename}]\n[File: source: {file_path}]"
return f"[{msg_type}]\n[File: source: {file_path}]"
def _send_local_file(open_id, file_path): def _send_local_file(open_id, file_path):
@@ -372,26 +313,30 @@ def _send_local_file(open_id, file_path):
send_message(open_id, f"⚠️ 文件不存在: {file_path}") send_message(open_id, f"⚠️ 文件不存在: {file_path}")
return False return False
ext = os.path.splitext(file_path)[1].lower() ext = os.path.splitext(file_path)[1].lower()
if ext in _IMAGE_EXTS: is_image = ext in _IMAGE_EXTS
image_key = _upload_image_sync(file_path) file_key = _upload_image_sync(file_path) if is_image else _upload_file_sync(file_path)
if image_key:
send_message(open_id, json.dumps({"image_key": image_key}, ensure_ascii=False), msg_type="image")
return True
else:
file_key = _upload_file_sync(file_path)
if file_key: if file_key:
msg_type = "media" if ext in _AUDIO_EXTS or ext in _VIDEO_EXTS else "file" key_name, msg_type = ("image_key", "image") if is_image else ("file_key", "media" if ext in _AUDIO_EXTS or ext in _VIDEO_EXTS else "file")
send_message(open_id, json.dumps({"file_key": file_key}, ensure_ascii=False), msg_type=msg_type) send_message(open_id, json.dumps({key_name: file_key}, ensure_ascii=False), msg_type=msg_type)
return True return True
send_message(open_id, f"⚠️ 文件发送失败: {os.path.basename(file_path)}") send_message(open_id, f"⚠️ 文件发送失败: {os.path.basename(file_path)}")
return False return False
def _send_generated_files(open_id, raw_text): def _send_generated_files(open_id, raw_text):
for file_path in _extract_files(raw_text): for file_path in extract_files(raw_text):
_send_local_file(open_id, file_path) _send_local_file(open_id, file_path)
def _append_media(parts, image_paths, msg_type, file_path, filename):
if not (file_path and filename):
parts.append(f"[{msg_type}: download failed]")
return
parts.append(_describe_media(msg_type, file_path, filename))
if msg_type == "image":
image_paths.append(file_path)
def _build_user_message(message): def _build_user_message(message):
msg_type = message.message_type msg_type = message.message_type
message_id = message.message_id message_id = message.message_id
@@ -407,19 +352,10 @@ def _build_user_message(message):
parts.append(text) parts.append(text)
for image_key in image_keys: for image_key in image_keys:
file_path, filename = _download_and_save_media("image", {"image_key": image_key}, message_id) file_path, filename = _download_and_save_media("image", {"image_key": image_key}, message_id)
if file_path and filename: _append_media(parts, image_paths, "image", file_path, filename)
parts.append(_describe_media("image", file_path, filename))
image_paths.append(file_path)
else:
parts.append("[image: download failed]")
elif msg_type in ("image", "audio", "file", "media"): elif msg_type in ("image", "audio", "file", "media"):
file_path, filename = _download_and_save_media(msg_type, content_json, message_id) file_path, filename = _download_and_save_media(msg_type, content_json, message_id)
if file_path and filename: _append_media(parts, image_paths, msg_type, file_path, filename)
parts.append(_describe_media(msg_type, file_path, filename))
if msg_type == "image":
image_paths.append(file_path)
else:
parts.append(f"[{msg_type}: download failed]")
elif msg_type in ("share_chat", "share_user", "interactive", "share_calendar_event", "system", "merge_forward"): elif msg_type in ("share_chat", "share_user", "interactive", "share_calendar_event", "system", "merge_forward"):
parts.append(_extract_share_card_content(content_json, msg_type)) parts.append(_extract_share_card_content(content_json, msg_type))
else: else:
@@ -500,22 +436,13 @@ def handle_command(open_id, cmd):
send_message(open_id, f"状态: {'🟢 空闲' if not agent.is_running else '🔴 运行中'}") send_message(open_id, f"状态: {'🟢 空闲' if not agent.is_running else '🔴 运行中'}")
elif cmd == "/restore": elif cmd == "/restore":
try: try:
files = glob.glob("./temp/model_responses_*.txt") restored_info, err = format_restore()
if not files: if err:
return send_message(open_id, "❌ 没有找到历史记录") return send_message(open_id, err)
latest = max(files, key=os.path.getmtime) restored, fname, count = restored_info
with open(latest, "r", encoding="utf-8") as f:
content = f.read()
users = re.findall(r"=== USER ===\n(.+?)(?==== |$)", content, re.DOTALL)
resps = re.findall(r"=== Response ===.*?\n(.+?)(?==== Prompt|$)", content, re.DOTALL)
count = 0
for u, r in zip(users, resps):
u, r = u.strip(), r.strip()[:500]
if u and r:
agent.history.extend([f"[USER]: {u}", f"[Agent] {r}"])
count += 1
agent.abort() agent.abort()
send_message(open_id, f"✅ 已恢复 {count} 轮对话\n来源: {os.path.basename(latest)}\n(仅恢复上下文,请输入新问题继续)") agent.history.extend(restored)
send_message(open_id, f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)")
except Exception as e: except Exception as e:
send_message(open_id, f"❌ 恢复失败: {e}") send_message(open_id, f"❌ 恢复失败: {e}")
else: else: