157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
import os
|
||
import json
|
||
import lark_oapi as lark
|
||
from lark_oapi.api.im.v1 import *
|
||
import llm_agent
|
||
import searcher
|
||
|
||
# ================= 飞书应用配置 =================
|
||
# 你需要在本机的环境变量中设置这两个值,或者在这里替换成硬编码进行调试
|
||
APP_ID = os.getenv("FEISHU_APP_ID", "填写你的_cli_app_id")
|
||
APP_SECRET = os.getenv("FEISHU_APP_SECRET", "填写你的_app_secret")
|
||
|
||
# ================= 初始化客户端 =================
|
||
# 此 Client 用来主动发请求:如发文字消息,传文件等
|
||
lark_client = lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).build()
|
||
|
||
|
||
def reply_text(message_id: str, text: str):
|
||
"""回复纯文本消息给指定的飞书消息ID"""
|
||
print(f">> 正在发送文本回复: {text}")
|
||
request = ReplyMessageRequest.builder() \
|
||
.message_id(message_id) \
|
||
.request_body(ReplyMessageRequestBody.builder()
|
||
.content(json.dumps({"text": text}))
|
||
.msg_type("text")
|
||
.build()) \
|
||
.build()
|
||
|
||
response = lark_client.im.v1.message.reply(request)
|
||
if not response.success():
|
||
print(f"发送文本消息失败! 代码: {response.code}, 信息: {response.msg}")
|
||
|
||
|
||
def reply_file(message_id: str, file_path: str):
|
||
"""上传文件并回复给指定消息"""
|
||
print(f">> 正在上传文件到飞书云端: {file_path}")
|
||
filename = os.path.basename(file_path)
|
||
|
||
# 1. 读文件字节
|
||
with open(file_path, "rb") as f:
|
||
file_bytes = f.read()
|
||
|
||
# 2. 上传文件到飞书的开放平台文件中心
|
||
create_file_req = CreateFileRequest.builder() \
|
||
.request_body(CreateFileRequestBody.builder()
|
||
.file_type("stream")
|
||
.file_name(filename)
|
||
.file(file_bytes)
|
||
.build()) \
|
||
.build()
|
||
|
||
file_resp = lark_client.im.v1.file.create(create_file_req)
|
||
if not file_resp.success():
|
||
print(f"上传文件失败: [{file_resp.code}] {file_resp.msg}")
|
||
reply_text(message_id, "文件上传飞书服务器失败,可能是文件格式拦截或网络问题,请稍后再试。")
|
||
return
|
||
|
||
file_key = file_resp.data.file_key
|
||
print(f"上传成功!获得 key: {file_key},准备发生该附件消息...")
|
||
|
||
# 3. 把获得的文件 file_key 作为消息发出
|
||
msg_req = ReplyMessageRequest.builder() \
|
||
.message_id(message_id) \
|
||
.request_body(ReplyMessageRequestBody.builder()
|
||
.content(json.dumps({"file_key": file_key}))
|
||
.msg_type("file")
|
||
.build()) \
|
||
.build()
|
||
|
||
msg_resp = lark_client.im.v1.message.reply(msg_req)
|
||
if not msg_resp.success():
|
||
print(f"发送文件消息失败: {msg_resp.code}, {msg_resp.msg}")
|
||
else:
|
||
print(">> 文件已成功发送给用户!\n")
|
||
|
||
|
||
def on_message_receive(data: P2pImMessageReceiveV1) -> None:
|
||
"""这是一个事件监听推流回调函数,每次收到一条飞书单聊消息时,就会运行它"""
|
||
event = data.event
|
||
message = event.message
|
||
message_id = message.message_id
|
||
msg_type = message.message_type
|
||
|
||
# 我们只对文本消息感兴趣,忽略表情、图片等避免死循环和崩溃
|
||
if msg_type != "text":
|
||
return
|
||
|
||
content_str = message.content
|
||
try:
|
||
content_dict = json.loads(content_str)
|
||
text = content_dict.get("text", "").strip()
|
||
except Exception:
|
||
text = content_str.strip()
|
||
|
||
print(f"\n======================================")
|
||
print(f"[收到消息] 用户发送:{text}")
|
||
print(f"======================================")
|
||
|
||
# 交给 LLM (Gemma 4B) 解析意图
|
||
print("🤖 正在让 Gemma 4B 分析该意图...")
|
||
llm_result = llm_agent.process_message(text)
|
||
|
||
intent = llm_result.get("intent", "reply")
|
||
reply_str = llm_result.get("reply_text", "")
|
||
|
||
# 无论找没有找文件,如果有“过渡安抚”的话,都先回复过去
|
||
if reply_str:
|
||
reply_text(message_id, reply_str)
|
||
|
||
# 如果意图是派发文件,则触发 searcher 与发送动作
|
||
if intent == "send_file":
|
||
keyword = llm_result.get("keyword", "")
|
||
if not keyword:
|
||
print("解析出送文件意图,但没有关键字,跳过。")
|
||
return
|
||
|
||
print(f"🔍 Gemma指令: 在本地搜索包含【{keyword}】的文件...")
|
||
file_path = searcher.search_file(keyword)
|
||
|
||
if file_path:
|
||
print(f"找到目标: {file_path}")
|
||
reply_file(message_id, file_path)
|
||
else:
|
||
print("搜寻无果。")
|
||
reply_text(message_id, f"抱歉,我在本地资料苦里没找到名称带有「{keyword}」的文件。")
|
||
|
||
|
||
# ================= 定义长连接的事件分发器 =================
|
||
# 通过此 Builder 挂载你需要监听处理的事件类型
|
||
# 长连接(WS)模式下,无需填入加密串和校验 Token,留空即可
|
||
event_handler = lark.EventDispatcherHandler.builder("", "") \
|
||
.register_p2p_im_message_receive_v1(on_message_receive) \
|
||
.build()
|
||
|
||
if __name__ == "__main__":
|
||
print(r"""
|
||
_____ _ _ ____ _
|
||
| ___|___ ___| |__ | | | __ ) ___ | |_
|
||
| |_ / _ \/ __| '_ \ | | _____ | _ \ / _ \| __|
|
||
| _| __/\__ \ | | | | |__|_____| | |_) | (_) | |_
|
||
|_| \___||___/_| |_| |____| |____/ \___/ \__|
|
||
""")
|
||
print("\n[ 系统引导 ] 正在启动基于 Gemma 4B 的飞书智能自动回复助手...")
|
||
|
||
if "填写你的" in APP_ID:
|
||
print("\n❌ 警告:你还没有填写真实的 APP_ID 和 APP_SECRET")
|
||
print("请在代码头部或环境变量中,设置你的飞书应用凭证(如何获取请参阅 README.md 教程)。")
|
||
else:
|
||
# 打开长连接监听
|
||
# 注意: 这里的 log_level 用 DEBUG 会打印很多底层握手日志,这里我们将其适当改大或忽略避免刷屏
|
||
ws_client = lark.ws.Client(APP_ID, APP_SECRET, event_handler=event_handler, log_level=lark.LogLevel.INFO)
|
||
try:
|
||
print("[ 服务状态 ] 长连接订阅已成功建立,请向您的飞书机器人发消息测试...")
|
||
ws_client.start()
|
||
except KeyboardInterrupt:
|
||
print("\n[ 服务状态 ] 监听到关闭指令,停止运行。")
|