* 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>
103 lines
6.1 KiB
Python
103 lines
6.1 KiB
Python
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() |