feat: 添加飞书机器人集成 (#13)

* feat: add Feishu bot integration

- Add fsapp.py: Feishu bot webhook handler (root directory)
- Add assets/SETUP_FEISHU.md: Setup guide for Feishu integration
- Add assets/install_python_windows.bat: Windows Python installer script

* fix: 历史注入仅在飞书场景生效,避免混入本地CLI历史

* fix: fsapp调用put_task时传source='feishu'以触发历史注入

---------

Co-authored-by: 张洲嘉 <zhangzhoujia@zhangzhoujiadeMacBook-Air.local>
This commit is contained in:
AspasZhang
2026-03-09 08:45:11 +08:00
committed by GitHub
parent 14f1009ddc
commit 7d444d065e
4 changed files with 521 additions and 1 deletions

103
fsapp.py Normal file
View File

@@ -0,0 +1,103 @@
import os, sys, threading, asyncio, time, re, json
import queue as Q
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_ROOT); os.chdir(PROJECT_ROOT)
import lark_oapi as lark
from lark_oapi.api.im.v1 import *
from lark_oapi.api.contact.v3 import *
from agentmain import GeneraticAgent
import mykey
_TAG_PATS = [r'<' + t + r'>.*?</' + t + r'>' for t in ('thinking', 'summary', 'tool_use', 'file_content')]
def _clean(t):
for p in _TAG_PATS: t = re.sub(p, '', t, flags=re.DOTALL)
return re.sub(r'\n{3,}', '\n\n', t).strip() or '...'
APP_ID, APP_SECRET = getattr(mykey, 'fs_app_id', None), getattr(mykey, 'fs_app_secret', None)
ALLOWED_USERS = set(getattr(mykey, 'fs_allowed_users', []))
agent = GeneraticAgent()
threading.Thread(target=agent.run, daemon=True).start()
client, user_tasks = None, {}
def create_client():
return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build()
_card = lambda t: json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": t}]})
def send_message(open_id, content, msg_type="text", use_card=False):
ct, mt = (_card(content), "interactive") if use_card else (json.dumps({"text": content}), "text")
body = CreateMessageRequest.builder().receive_id_type("open_id").request_body(
CreateMessageRequestBody.builder().receive_id(open_id).msg_type(mt).content(ct).build()).build()
r = client.im.v1.message.create(body)
return r.data.message_id if r.success() else (print(f"发送失败: {r.code}, {r.msg}"), None)[1]
def update_message(message_id, content):
body = PatchMessageRequest.builder().message_id(message_id).request_body(
PatchMessageRequestBody.builder().content(_card(content)).build()).build()
r = client.im.v1.message.patch(body)
if not r.success(): print(f"[ERROR] update_message 失败: {r.code}, {r.msg}")
return r.success()
def handle_message(data):
event, message, sender = data.event, data.event.message, data.event.sender
open_id, msg_type = sender.sender_id.open_id, message.message_type
if ALLOWED_USERS and open_id not in ALLOWED_USERS: return print(f"未授权用户: {open_id}")
if msg_type != "text": return send_message(open_id, "⚠️ 目前只支持文本消息")
text = json.loads(message.content).get("text", "").strip()
if not text: return
print(f"收到消息 [{open_id}]: {text}")
if text.startswith("/"): return handle_command(open_id, text)
def run_agent():
user_tasks[open_id] = {'running': True}
try:
msg_id, dq, last_text = send_message(open_id, "思考中...", use_card=True), agent.put_task(text, source='feishu'), ""
while user_tasks.get(open_id, {}).get('running', False):
time.sleep(3)
item = None
try:
while True: item = dq.get_nowait()
except: pass
if item is None: continue
raw, done = item.get("done") or item.get("next", ""), "done" in item
show = _clean(raw)
if len(show) > 3500:
# 智能截断:避免切断代码块
cut = show[-3000:]
if cut.count('```') % 2 == 1: cut = '```\n' + cut # 补开头
msg_id, last_text, show = send_message(open_id, "(继续...)", use_card=True), "", cut
display = show if done else show + ""
if display != last_text and msg_id: update_message(msg_id, display); last_text = display
if done: break
if not user_tasks.get(open_id, {}).get('running', True): send_message(open_id, "⏹️ 已停止")
except Exception as e:
import traceback; print(f"[ERROR] run_agent 异常: {e}"); traceback.print_exc()
send_message(open_id, f"❌ 错误: {str(e)}")
finally: user_tasks.pop(open_id, None)
threading.Thread(target=run_agent, daemon=True).start()
def handle_command(open_id, cmd):
import glob
if cmd == "/stop":
if open_id in user_tasks: user_tasks[open_id]['running'] = False
agent.abort(); send_message(open_id, "⏹️ 正在停止...")
elif cmd == "/help":
send_message(open_id, "📖 命令列表:\n/stop - 停止当前任务\n/status - 查看状态\n/restore - 恢复上次对话历史\n/new - 开启新对话\n/help - 显示帮助")
elif cmd == "/status":
send_message(open_id, f"状态: {'🟢 空闲' if not agent.is_busy() else '🔴 运行中'}")
elif cmd == "/restore":
try:
files = glob.glob('./temp/model_responses_*.txt')
if not files: return send_message(open_id, "❌ 没有找到历史记录")
latest = max(files, key=os.path.getmtime)
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()
send_message(open_id, f"✅ 已恢复 {count} 轮对话\n来源: {os.path.basename(latest)}\n(仅恢复上下文,请输入新问题继续)")
except Exception as e: send_message(open_id, f"❌ 恢复失败: {e}")
else: send_message(open_id, f"❓ 未知命令: {cmd}")
def main():
global client
if not APP_ID or not APP_SECRET: print("错误: 请在 mykey.py 中配置 fs_app_id 和 fs_app_secret"); sys.exit(1)
client = create_client()
handler = lark.EventDispatcherHandler.builder("", "").register_p2_im_message_receive_v1(handle_message).build()
cli = lark.ws.Client(APP_ID, APP_SECRET, event_handler=handler, log_level=lark.LogLevel.INFO)
print("=" * 50 + "\n飞书 Agent 已启动(长连接模式)\n" + f"App ID: {APP_ID}\n等待消息...\n" + "=" * 50)
cli.start()
if __name__ == "__main__": main()