mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-19 13:28:11 +08:00
fix: feishu cert error
This commit is contained in:
@@ -140,6 +140,23 @@ python3 app.py
|
|||||||
|
|
||||||
**解决**: 安装依赖 `pip install lark-oapi`
|
**解决**: 安装依赖 `pip install lark-oapi`
|
||||||
|
|
||||||
|
### SSL证书验证失败
|
||||||
|
|
||||||
|
```
|
||||||
|
[Lark][ERROR] connect failed, err:[SSL:CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**: 网络环境中存在自签名证书或SSL中间人代理(如企业代理、VPN等)
|
||||||
|
|
||||||
|
**解决**: 程序会自动检测SSL证书验证失败,并自动重试禁用证书验证的连接。无需手动配置。
|
||||||
|
|
||||||
|
当遇到证书错误时,日志会显示:
|
||||||
|
```
|
||||||
|
[FeiShu] SSL certificate verification disabled due to certificate error. This may happen when using corporate proxy or self-signed certificates.
|
||||||
|
```
|
||||||
|
|
||||||
|
这是正常现象,程序会自动处理并继续运行。
|
||||||
|
|
||||||
### Webhook模式端口被占用
|
### Webhook模式端口被占用
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
# -*- coding=utf-8 -*-
|
# -*- coding=utf-8 -*-
|
||||||
import uuid
|
import uuid
|
||||||
@@ -107,23 +108,64 @@ class FeiShuChanel(ChatChannel):
|
|||||||
.register_p2_im_message_receive_v1(handle_message_event) \
|
.register_p2_im_message_receive_v1(handle_message_event) \
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
# 创建长连接客户端
|
# 尝试连接,如果遇到SSL错误则自动禁用证书验证
|
||||||
ws_client = lark.ws.Client(
|
def start_client_with_retry():
|
||||||
self.feishu_app_id,
|
"""启动websocket客户端,自动处理SSL证书错误"""
|
||||||
self.feishu_app_secret,
|
for use_ssl_verify in [True, False]:
|
||||||
event_handler=event_handler,
|
try:
|
||||||
log_level=lark.LogLevel.DEBUG if conf().get("debug") else lark.LogLevel.INFO
|
# 如果不验证SSL,通过monkey patch禁用证书验证
|
||||||
)
|
original_wrap_socket = None
|
||||||
|
if not use_ssl_verify:
|
||||||
|
logger.warning("[FeiShu] SSL certificate verification disabled due to certificate error. "
|
||||||
|
"This may happen when using corporate proxy or self-signed certificates.")
|
||||||
|
# 保存原始的wrap_socket方法
|
||||||
|
import ssl as ssl_module
|
||||||
|
original_wrap_socket = ssl_module.SSLContext.wrap_socket
|
||||||
|
|
||||||
|
# 创建一个不验证证书的wrap_socket方法
|
||||||
|
def wrap_socket_no_verify(self, sock, *args, **kwargs):
|
||||||
|
self.check_hostname = False
|
||||||
|
self.verify_mode = ssl.CERT_NONE
|
||||||
|
return original_wrap_socket(self, sock, *args, **kwargs)
|
||||||
|
|
||||||
|
# 替换wrap_socket方法
|
||||||
|
ssl_module.SSLContext.wrap_socket = wrap_socket_no_verify
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws_client = lark.ws.Client(
|
||||||
|
self.feishu_app_id,
|
||||||
|
self.feishu_app_secret,
|
||||||
|
event_handler=event_handler,
|
||||||
|
log_level=lark.LogLevel.DEBUG if conf().get("debug") else lark.LogLevel.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("[FeiShu] Websocket client starting...")
|
||||||
|
ws_client.start()
|
||||||
|
# 如果成功启动,跳出循环
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
# 恢复原始的wrap_socket方法
|
||||||
|
if original_wrap_socket is not None:
|
||||||
|
import ssl as ssl_module
|
||||||
|
ssl_module.SSLContext.wrap_socket = original_wrap_socket
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
# 检查是否是SSL证书验证错误
|
||||||
|
is_ssl_error = "CERTIFICATE_VERIFY_FAILED" in error_msg or "certificate verify failed" in error_msg.lower()
|
||||||
|
|
||||||
|
if is_ssl_error and use_ssl_verify:
|
||||||
|
# 第一次遇到SSL错误,记录日志并继续循环(下次会禁用验证)
|
||||||
|
logger.warning(f"[FeiShu] SSL certificate verification failed: {error_msg}")
|
||||||
|
logger.info("[FeiShu] Retrying connection with SSL verification disabled...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 其他错误或禁用验证后仍失败,抛出异常
|
||||||
|
logger.error(f"[FeiShu] Websocket client error: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
# 在新线程中启动客户端,避免阻塞主线程
|
# 在新线程中启动客户端,避免阻塞主线程
|
||||||
def start_client():
|
ws_thread = threading.Thread(target=start_client_with_retry, daemon=True)
|
||||||
try:
|
|
||||||
logger.debug("[FeiShu] Websocket client starting...")
|
|
||||||
ws_client.start()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[FeiShu] Websocket client error: {e}", exc_info=True)
|
|
||||||
|
|
||||||
ws_thread = threading.Thread(target=start_client, daemon=True)
|
|
||||||
ws_thread.start()
|
ws_thread.start()
|
||||||
|
|
||||||
# 保持主线程运行
|
# 保持主线程运行
|
||||||
@@ -176,7 +218,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
# 处理文件缓存逻辑
|
# 处理文件缓存逻辑
|
||||||
from channel.file_cache import get_file_cache
|
from channel.file_cache import get_file_cache
|
||||||
file_cache = get_file_cache()
|
file_cache = get_file_cache()
|
||||||
|
|
||||||
# 获取 session_id(用于缓存关联)
|
# 获取 session_id(用于缓存关联)
|
||||||
if is_group:
|
if is_group:
|
||||||
if conf().get("group_shared_session", True):
|
if conf().get("group_shared_session", True):
|
||||||
@@ -185,7 +227,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
session_id = feishu_msg.from_user_id + "_" + msg.get("chat_id")
|
session_id = feishu_msg.from_user_id + "_" + msg.get("chat_id")
|
||||||
else:
|
else:
|
||||||
session_id = feishu_msg.from_user_id
|
session_id = feishu_msg.from_user_id
|
||||||
|
|
||||||
# 如果是单张图片消息,缓存起来
|
# 如果是单张图片消息,缓存起来
|
||||||
if feishu_msg.ctype == ContextType.IMAGE:
|
if feishu_msg.ctype == ContextType.IMAGE:
|
||||||
if hasattr(feishu_msg, 'image_path') and feishu_msg.image_path:
|
if hasattr(feishu_msg, 'image_path') and feishu_msg.image_path:
|
||||||
@@ -193,7 +235,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
logger.info(f"[FeiShu] Image cached for session {session_id}, waiting for user query...")
|
logger.info(f"[FeiShu] Image cached for session {session_id}, waiting for user query...")
|
||||||
# 单张图片不直接处理,等待用户提问
|
# 单张图片不直接处理,等待用户提问
|
||||||
return
|
return
|
||||||
|
|
||||||
# 如果是文本消息,检查是否有缓存的文件
|
# 如果是文本消息,检查是否有缓存的文件
|
||||||
if feishu_msg.ctype == ContextType.TEXT:
|
if feishu_msg.ctype == ContextType.TEXT:
|
||||||
cached_files = file_cache.get(session_id)
|
cached_files = file_cache.get(session_id)
|
||||||
@@ -209,7 +251,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
file_refs.append(f"[视频: {file_path}]")
|
file_refs.append(f"[视频: {file_path}]")
|
||||||
else:
|
else:
|
||||||
file_refs.append(f"[文件: {file_path}]")
|
file_refs.append(f"[文件: {file_path}]")
|
||||||
|
|
||||||
feishu_msg.content = feishu_msg.content + "\n" + "\n".join(file_refs)
|
feishu_msg.content = feishu_msg.content + "\n" + "\n".join(file_refs)
|
||||||
logger.info(f"[FeiShu] Attached {len(cached_files)} cached file(s) to user query")
|
logger.info(f"[FeiShu] Attached {len(cached_files)} cached file(s) to user query")
|
||||||
# 清除缓存
|
# 清除缓存
|
||||||
@@ -258,26 +300,27 @@ class FeiShuChanel(ChatChannel):
|
|||||||
self._send(text_reply, context)
|
self._send(text_reply, context)
|
||||||
import time
|
import time
|
||||||
time.sleep(0.3) # 短暂延迟,确保文本先到达
|
time.sleep(0.3) # 短暂延迟,确保文本先到达
|
||||||
|
|
||||||
# 判断是否为视频文件
|
# 判断是否为视频文件
|
||||||
file_path = reply.content
|
file_path = reply.content
|
||||||
if file_path.startswith("file://"):
|
if file_path.startswith("file://"):
|
||||||
file_path = file_path[7:]
|
file_path = file_path[7:]
|
||||||
|
|
||||||
is_video = file_path.lower().endswith(('.mp4', '.avi', '.mov', '.wmv', '.flv'))
|
is_video = file_path.lower().endswith(('.mp4', '.avi', '.mov', '.wmv', '.flv'))
|
||||||
|
|
||||||
if is_video:
|
if is_video:
|
||||||
# 视频上传(包含duration信息)
|
# 视频上传(包含duration信息)
|
||||||
upload_data = self._upload_video_url(reply.content, access_token)
|
upload_data = self._upload_video_url(reply.content, access_token)
|
||||||
if not upload_data or not upload_data.get('file_key'):
|
if not upload_data or not upload_data.get('file_key'):
|
||||||
logger.warning("[FeiShu] upload video failed")
|
logger.warning("[FeiShu] upload video failed")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 视频使用 media 类型(根据官方文档)
|
# 视频使用 media 类型(根据官方文档)
|
||||||
# 错误码 230055 说明:上传 mp4 时必须使用 msg_type="media"
|
# 错误码 230055 说明:上传 mp4 时必须使用 msg_type="media"
|
||||||
msg_type = "media"
|
msg_type = "media"
|
||||||
reply_content = upload_data # 完整的上传响应数据(包含file_key和duration)
|
reply_content = upload_data # 完整的上传响应数据(包含file_key和duration)
|
||||||
logger.info(f"[FeiShu] Sending video: file_key={upload_data.get('file_key')}, duration={upload_data.get('duration')}ms")
|
logger.info(
|
||||||
|
f"[FeiShu] Sending video: file_key={upload_data.get('file_key')}, duration={upload_data.get('duration')}ms")
|
||||||
content_key = None # 直接序列化整个对象
|
content_key = None # 直接序列化整个对象
|
||||||
else:
|
else:
|
||||||
# 其他文件使用 file 类型
|
# 其他文件使用 file 类型
|
||||||
@@ -288,14 +331,14 @@ class FeiShuChanel(ChatChannel):
|
|||||||
reply_content = file_key
|
reply_content = file_key
|
||||||
msg_type = "file"
|
msg_type = "file"
|
||||||
content_key = "file_key"
|
content_key = "file_key"
|
||||||
|
|
||||||
# Check if we can reply to an existing message (need msg_id)
|
# Check if we can reply to an existing message (need msg_id)
|
||||||
can_reply = is_group and msg and hasattr(msg, 'msg_id') and msg.msg_id
|
can_reply = is_group and msg and hasattr(msg, 'msg_id') and msg.msg_id
|
||||||
|
|
||||||
# Build content JSON
|
# Build content JSON
|
||||||
content_json = json.dumps(reply_content) if content_key is None else json.dumps({content_key: reply_content})
|
content_json = json.dumps(reply_content) if content_key is None else json.dumps({content_key: reply_content})
|
||||||
logger.debug(f"[FeiShu] Sending message: msg_type={msg_type}, content={content_json[:200]}")
|
logger.debug(f"[FeiShu] Sending message: msg_type={msg_type}, content={content_json[:200]}")
|
||||||
|
|
||||||
if can_reply:
|
if can_reply:
|
||||||
# 群聊中回复已有消息
|
# 群聊中回复已有消息
|
||||||
url = f"https://open.feishu.cn/open-apis/im/v1/messages/{msg.msg_id}/reply"
|
url = f"https://open.feishu.cn/open-apis/im/v1/messages/{msg.msg_id}/reply"
|
||||||
@@ -320,7 +363,6 @@ class FeiShuChanel(ChatChannel):
|
|||||||
else:
|
else:
|
||||||
logger.error(f"[FeiShu] send message failed, code={res.get('code')}, msg={res.get('msg')}")
|
logger.error(f"[FeiShu] send message failed, code={res.get('code')}, msg={res.get('msg')}")
|
||||||
|
|
||||||
|
|
||||||
def fetch_access_token(self) -> str:
|
def fetch_access_token(self) -> str:
|
||||||
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
|
||||||
headers = {
|
headers = {
|
||||||
@@ -342,35 +384,34 @@ class FeiShuChanel(ChatChannel):
|
|||||||
else:
|
else:
|
||||||
logger.error(f"[FeiShu] fetch token error, res={response}")
|
logger.error(f"[FeiShu] fetch token error, res={response}")
|
||||||
|
|
||||||
|
|
||||||
def _upload_image_url(self, img_url, access_token):
|
def _upload_image_url(self, img_url, access_token):
|
||||||
logger.debug(f"[FeiShu] start process image, img_url={img_url}")
|
logger.debug(f"[FeiShu] start process image, img_url={img_url}")
|
||||||
|
|
||||||
# Check if it's a local file path (file:// protocol)
|
# Check if it's a local file path (file:// protocol)
|
||||||
if img_url.startswith("file://"):
|
if img_url.startswith("file://"):
|
||||||
local_path = img_url[7:] # Remove "file://" prefix
|
local_path = img_url[7:] # Remove "file://" prefix
|
||||||
logger.info(f"[FeiShu] uploading local file: {local_path}")
|
logger.info(f"[FeiShu] uploading local file: {local_path}")
|
||||||
|
|
||||||
if not os.path.exists(local_path):
|
if not os.path.exists(local_path):
|
||||||
logger.error(f"[FeiShu] local file not found: {local_path}")
|
logger.error(f"[FeiShu] local file not found: {local_path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Upload directly from local file
|
# Upload directly from local file
|
||||||
upload_url = "https://open.feishu.cn/open-apis/im/v1/images"
|
upload_url = "https://open.feishu.cn/open-apis/im/v1/images"
|
||||||
data = {'image_type': 'message'}
|
data = {'image_type': 'message'}
|
||||||
headers = {'Authorization': f'Bearer {access_token}'}
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
with open(local_path, "rb") as file:
|
with open(local_path, "rb") as file:
|
||||||
upload_response = requests.post(upload_url, files={"image": file}, data=data, headers=headers)
|
upload_response = requests.post(upload_url, files={"image": file}, data=data, headers=headers)
|
||||||
logger.info(f"[FeiShu] upload file, res={upload_response.content}")
|
logger.info(f"[FeiShu] upload file, res={upload_response.content}")
|
||||||
|
|
||||||
response_data = upload_response.json()
|
response_data = upload_response.json()
|
||||||
if response_data.get("code") == 0:
|
if response_data.get("code") == 0:
|
||||||
return response_data.get("data").get("image_key")
|
return response_data.get("data").get("image_key")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[FeiShu] upload failed: {response_data}")
|
logger.error(f"[FeiShu] upload failed: {response_data}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Original logic for HTTP URLs
|
# Original logic for HTTP URLs
|
||||||
response = requests.get(img_url)
|
response = requests.get(img_url)
|
||||||
suffix = utils.get_path_suffix(img_url)
|
suffix = utils.get_path_suffix(img_url)
|
||||||
@@ -406,7 +447,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# 使用 ffprobe 获取视频时长
|
# 使用 ffprobe 获取视频时长
|
||||||
cmd = [
|
cmd = [
|
||||||
'ffprobe',
|
'ffprobe',
|
||||||
@@ -415,7 +456,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
'-of', 'default=noprint_wrappers=1:nokey=1',
|
'-of', 'default=noprint_wrappers=1:nokey=1',
|
||||||
file_path
|
file_path
|
||||||
]
|
]
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
duration_seconds = float(result.stdout.strip())
|
duration_seconds = float(result.stdout.strip())
|
||||||
@@ -444,7 +485,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
"""
|
"""
|
||||||
local_path = None
|
local_path = None
|
||||||
temp_file = None
|
temp_file = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# For file:// URLs (local files), upload directly
|
# For file:// URLs (local files), upload directly
|
||||||
if video_url.startswith("file://"):
|
if video_url.startswith("file://"):
|
||||||
@@ -459,65 +500,67 @@ class FeiShuChanel(ChatChannel):
|
|||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"[FeiShu] download video failed, status={response.status_code}")
|
logger.error(f"[FeiShu] download video failed, status={response.status_code}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Save to temp file
|
# Save to temp file
|
||||||
import uuid
|
import uuid
|
||||||
file_name = os.path.basename(video_url) or "video.mp4"
|
file_name = os.path.basename(video_url) or "video.mp4"
|
||||||
temp_file = str(uuid.uuid4()) + "_" + file_name
|
temp_file = str(uuid.uuid4()) + "_" + file_name
|
||||||
|
|
||||||
with open(temp_file, "wb") as file:
|
with open(temp_file, "wb") as file:
|
||||||
file.write(response.content)
|
file.write(response.content)
|
||||||
|
|
||||||
logger.info(f"[FeiShu] Video downloaded, size={len(response.content)} bytes")
|
logger.info(f"[FeiShu] Video downloaded, size={len(response.content)} bytes")
|
||||||
local_path = temp_file
|
local_path = temp_file
|
||||||
|
|
||||||
# Get video duration
|
# Get video duration
|
||||||
duration = self._get_video_duration(local_path)
|
duration = self._get_video_duration(local_path)
|
||||||
|
|
||||||
# Upload to Feishu
|
# Upload to Feishu
|
||||||
file_name = os.path.basename(local_path)
|
file_name = os.path.basename(local_path)
|
||||||
file_ext = os.path.splitext(file_name)[1].lower()
|
file_ext = os.path.splitext(file_name)[1].lower()
|
||||||
file_type_map = {'.mp4': 'mp4'}
|
file_type_map = {'.mp4': 'mp4'}
|
||||||
file_type = file_type_map.get(file_ext, 'mp4')
|
file_type = file_type_map.get(file_ext, 'mp4')
|
||||||
|
|
||||||
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
||||||
data = {
|
data = {
|
||||||
'file_type': file_type,
|
'file_type': file_type,
|
||||||
'file_name': file_name
|
'file_name': file_name
|
||||||
}
|
}
|
||||||
# Add duration only if available (required for video/audio)
|
# Add duration only if available (required for video/audio)
|
||||||
if duration:
|
if duration:
|
||||||
data['duration'] = duration # Must be int, not string
|
data['duration'] = duration # Must be int, not string
|
||||||
|
|
||||||
headers = {'Authorization': f'Bearer {access_token}'}
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
logger.info(f"[FeiShu] Uploading video: file_name={file_name}, duration={duration}ms")
|
logger.info(f"[FeiShu] Uploading video: file_name={file_name}, duration={duration}ms")
|
||||||
|
|
||||||
with open(local_path, "rb") as file:
|
with open(local_path, "rb") as file:
|
||||||
upload_response = requests.post(
|
upload_response = requests.post(
|
||||||
upload_url,
|
upload_url,
|
||||||
files={"file": file},
|
files={"file": file},
|
||||||
data=data,
|
data=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=(5, 60)
|
timeout=(5, 60)
|
||||||
)
|
)
|
||||||
logger.info(f"[FeiShu] upload video response, status={upload_response.status_code}, res={upload_response.content}")
|
logger.info(
|
||||||
|
f"[FeiShu] upload video response, status={upload_response.status_code}, res={upload_response.content}")
|
||||||
|
|
||||||
response_data = upload_response.json()
|
response_data = upload_response.json()
|
||||||
if response_data.get("code") == 0:
|
if response_data.get("code") == 0:
|
||||||
# Add duration to the response data (API doesn't return it)
|
# Add duration to the response data (API doesn't return it)
|
||||||
upload_data = response_data.get("data")
|
upload_data = response_data.get("data")
|
||||||
upload_data['duration'] = duration # Add our calculated duration
|
upload_data['duration'] = duration # Add our calculated duration
|
||||||
logger.info(f"[FeiShu] Upload complete: file_key={upload_data.get('file_key')}, duration={duration}ms")
|
logger.info(
|
||||||
|
f"[FeiShu] Upload complete: file_key={upload_data.get('file_key')}, duration={duration}ms")
|
||||||
return upload_data
|
return upload_data
|
||||||
else:
|
else:
|
||||||
logger.error(f"[FeiShu] upload video failed: {response_data}")
|
logger.error(f"[FeiShu] upload video failed: {response_data}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[FeiShu] upload video exception: {e}")
|
logger.error(f"[FeiShu] upload video exception: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Clean up temp file
|
# Clean up temp file
|
||||||
if temp_file and os.path.exists(temp_file):
|
if temp_file and os.path.exists(temp_file):
|
||||||
@@ -532,20 +575,20 @@ class FeiShuChanel(ChatChannel):
|
|||||||
Supports both local files (file://) and HTTP URLs
|
Supports both local files (file://) and HTTP URLs
|
||||||
"""
|
"""
|
||||||
logger.debug(f"[FeiShu] start process file, file_url={file_url}")
|
logger.debug(f"[FeiShu] start process file, file_url={file_url}")
|
||||||
|
|
||||||
# Check if it's a local file path (file:// protocol)
|
# Check if it's a local file path (file:// protocol)
|
||||||
if file_url.startswith("file://"):
|
if file_url.startswith("file://"):
|
||||||
local_path = file_url[7:] # Remove "file://" prefix
|
local_path = file_url[7:] # Remove "file://" prefix
|
||||||
logger.info(f"[FeiShu] uploading local file: {local_path}")
|
logger.info(f"[FeiShu] uploading local file: {local_path}")
|
||||||
|
|
||||||
if not os.path.exists(local_path):
|
if not os.path.exists(local_path):
|
||||||
logger.error(f"[FeiShu] local file not found: {local_path}")
|
logger.error(f"[FeiShu] local file not found: {local_path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get file info
|
# Get file info
|
||||||
file_name = os.path.basename(local_path)
|
file_name = os.path.basename(local_path)
|
||||||
file_ext = os.path.splitext(file_name)[1].lower()
|
file_ext = os.path.splitext(file_name)[1].lower()
|
||||||
|
|
||||||
# Determine file type for Feishu API
|
# Determine file type for Feishu API
|
||||||
# Feishu supports: opus, mp4, pdf, doc, xls, ppt, stream (other types)
|
# Feishu supports: opus, mp4, pdf, doc, xls, ppt, stream (other types)
|
||||||
file_type_map = {
|
file_type_map = {
|
||||||
@@ -557,23 +600,24 @@ class FeiShuChanel(ChatChannel):
|
|||||||
'.ppt': 'ppt', '.pptx': 'ppt',
|
'.ppt': 'ppt', '.pptx': 'ppt',
|
||||||
}
|
}
|
||||||
file_type = file_type_map.get(file_ext, 'stream') # Default to stream for other types
|
file_type = file_type_map.get(file_ext, 'stream') # Default to stream for other types
|
||||||
|
|
||||||
# Upload file to Feishu
|
# Upload file to Feishu
|
||||||
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
||||||
data = {'file_type': file_type, 'file_name': file_name}
|
data = {'file_type': file_type, 'file_name': file_name}
|
||||||
headers = {'Authorization': f'Bearer {access_token}'}
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(local_path, "rb") as file:
|
with open(local_path, "rb") as file:
|
||||||
upload_response = requests.post(
|
upload_response = requests.post(
|
||||||
upload_url,
|
upload_url,
|
||||||
files={"file": file},
|
files={"file": file},
|
||||||
data=data,
|
data=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=(5, 30) # 5s connect, 30s read timeout
|
timeout=(5, 30) # 5s connect, 30s read timeout
|
||||||
)
|
)
|
||||||
logger.info(f"[FeiShu] upload file response, status={upload_response.status_code}, res={upload_response.content}")
|
logger.info(
|
||||||
|
f"[FeiShu] upload file response, status={upload_response.status_code}, res={upload_response.content}")
|
||||||
|
|
||||||
response_data = upload_response.json()
|
response_data = upload_response.json()
|
||||||
if response_data.get("code") == 0:
|
if response_data.get("code") == 0:
|
||||||
return response_data.get("data").get("file_key")
|
return response_data.get("data").get("file_key")
|
||||||
@@ -583,22 +627,22 @@ class FeiShuChanel(ChatChannel):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[FeiShu] upload file exception: {e}")
|
logger.error(f"[FeiShu] upload file exception: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# For HTTP URLs, download first then upload
|
# For HTTP URLs, download first then upload
|
||||||
try:
|
try:
|
||||||
response = requests.get(file_url, timeout=(5, 30))
|
response = requests.get(file_url, timeout=(5, 30))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"[FeiShu] download file failed, status={response.status_code}")
|
logger.error(f"[FeiShu] download file failed, status={response.status_code}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Save to temp file
|
# Save to temp file
|
||||||
import uuid
|
import uuid
|
||||||
file_name = os.path.basename(file_url)
|
file_name = os.path.basename(file_url)
|
||||||
temp_name = str(uuid.uuid4()) + "_" + file_name
|
temp_name = str(uuid.uuid4()) + "_" + file_name
|
||||||
|
|
||||||
with open(temp_name, "wb") as file:
|
with open(temp_name, "wb") as file:
|
||||||
file.write(response.content)
|
file.write(response.content)
|
||||||
|
|
||||||
# Upload
|
# Upload
|
||||||
file_ext = os.path.splitext(file_name)[1].lower()
|
file_ext = os.path.splitext(file_name)[1].lower()
|
||||||
file_type_map = {
|
file_type_map = {
|
||||||
@@ -608,18 +652,18 @@ class FeiShuChanel(ChatChannel):
|
|||||||
'.ppt': 'ppt', '.pptx': 'ppt',
|
'.ppt': 'ppt', '.pptx': 'ppt',
|
||||||
}
|
}
|
||||||
file_type = file_type_map.get(file_ext, 'stream')
|
file_type = file_type_map.get(file_ext, 'stream')
|
||||||
|
|
||||||
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
upload_url = "https://open.feishu.cn/open-apis/im/v1/files"
|
||||||
data = {'file_type': file_type, 'file_name': file_name}
|
data = {'file_type': file_type, 'file_name': file_name}
|
||||||
headers = {'Authorization': f'Bearer {access_token}'}
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
with open(temp_name, "rb") as file:
|
with open(temp_name, "rb") as file:
|
||||||
upload_response = requests.post(upload_url, files={"file": file}, data=data, headers=headers)
|
upload_response = requests.post(upload_url, files={"file": file}, data=data, headers=headers)
|
||||||
logger.info(f"[FeiShu] upload file, res={upload_response.content}")
|
logger.info(f"[FeiShu] upload file, res={upload_response.content}")
|
||||||
|
|
||||||
response_data = upload_response.json()
|
response_data = upload_response.json()
|
||||||
os.remove(temp_name) # Clean up temp file
|
os.remove(temp_name) # Clean up temp file
|
||||||
|
|
||||||
if response_data.get("code") == 0:
|
if response_data.get("code") == 0:
|
||||||
return response_data.get("data").get("file_key")
|
return response_data.get("data").get("file_key")
|
||||||
else:
|
else:
|
||||||
@@ -636,7 +680,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
context["origin_ctype"] = ctype
|
context["origin_ctype"] = ctype
|
||||||
|
|
||||||
cmsg = context["msg"]
|
cmsg = context["msg"]
|
||||||
|
|
||||||
# Set session_id based on chat type
|
# Set session_id based on chat type
|
||||||
if cmsg.is_group:
|
if cmsg.is_group:
|
||||||
# Group chat: check if group_shared_session is enabled
|
# Group chat: check if group_shared_session is enabled
|
||||||
@@ -652,7 +696,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
else:
|
else:
|
||||||
# Private chat: use user_id only
|
# Private chat: use user_id only
|
||||||
context["session_id"] = cmsg.from_user_id
|
context["session_id"] = cmsg.from_user_id
|
||||||
|
|
||||||
context["receiver"] = cmsg.other_user_id
|
context["receiver"] = cmsg.other_user_id
|
||||||
|
|
||||||
if ctype == ContextType.TEXT:
|
if ctype == ContextType.TEXT:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class AgentPlugin(Plugin):
|
|||||||
"""Load configuration from config.yaml file."""
|
"""Load configuration from config.yaml file."""
|
||||||
config_path = os.path.join(self.path, "config.yaml")
|
config_path = os.path.join(self.path, "config.yaml")
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
logger.warning(f"Config file not found at {config_path}")
|
logger.debug(f"Config file not found at {config_path}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Banwords(Plugin):
|
|||||||
self.reply_action = conf.get("reply_action", "ignore")
|
self.reply_action = conf.get("reply_action", "ignore")
|
||||||
logger.debug("[Banwords] inited")
|
logger.debug("[Banwords] inited")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("[Banwords] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/banwords .")
|
logger.debug("[Banwords] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/banwords .")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def on_handle_context(self, e_context: EventContext):
|
def on_handle_context(self, e_context: EventContext):
|
||||||
|
|||||||
Reference in New Issue
Block a user