feat: add WeChat bot frontend (wechatapp.py)
This commit is contained in:
205
frontends/wechatapp.py
Normal file
205
frontends/wechatapp.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import os, sys, re, threading, queue, time, socket, json, struct, base64, uuid, webbrowser
|
||||
from pathlib import Path
|
||||
import requests, qrcode
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from agentmain import GeneraticAgent
|
||||
|
||||
# ── WxBotClient (inline from wx_bot_client.py) ──
|
||||
API = 'https://ilinkai.weixin.qq.com'
|
||||
TOKEN_FILE = Path.home() / '.wxbot' / 'token.json'
|
||||
TOKEN_FILE.parent.mkdir(exist_ok=True)
|
||||
VER, MSG_USER, MSG_BOT, ITEM_TEXT, STATE_FINISH = '0.2.5', 1, 2, 1, 2
|
||||
|
||||
def _uin():
|
||||
return base64.b64encode(str(struct.unpack('>I', os.urandom(4))[0]).encode()).decode()
|
||||
|
||||
class WxBotClient:
|
||||
def __init__(self, token=None, token_file=None):
|
||||
self._tf = Path(token_file) if token_file else TOKEN_FILE
|
||||
self.token = token
|
||||
self.bot_id = None
|
||||
self._buf = ''
|
||||
if not self.token: self._load()
|
||||
|
||||
def _load(self):
|
||||
if self._tf.exists():
|
||||
d = json.loads(self._tf.read_text('utf-8'))
|
||||
self.token, self.bot_id, self._buf = d.get('bot_token',''), d.get('ilink_bot_id',''), d.get('updates_buf','')
|
||||
|
||||
def _save(self, **kw):
|
||||
d = {'bot_token': self.token or '', 'ilink_bot_id': self.bot_id or '',
|
||||
'updates_buf': self._buf or '', **kw}
|
||||
self._tf.write_text(json.dumps(d, ensure_ascii=False, indent=2), 'utf-8')
|
||||
|
||||
def _post(self, ep, body, timeout=15):
|
||||
h = {'Content-Type': 'application/json', 'AuthorizationType': 'ilink_bot_token',
|
||||
'X-WECHAT-UIN': _uin()}
|
||||
if self.token: h['Authorization'] = f'Bearer {self.token}'
|
||||
r = requests.post(f'{API}/{ep}', json=body, headers=h, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def login_qr(self, poll_interval=2):
|
||||
r = requests.get(f'{API}/ilink/bot/get_bot_qrcode', params={'bot_type': 3}, timeout=10)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
qr_id, url = d['qrcode'], d.get('qrcode_img_content', '')
|
||||
print(f'[QR登录] ID: {qr_id}')
|
||||
if url:
|
||||
img = self._tf.parent / 'wx_qr.png'
|
||||
qrcode.make(url).save(str(img)); webbrowser.open(str(img))
|
||||
last = ''
|
||||
while True:
|
||||
time.sleep(poll_interval)
|
||||
try:
|
||||
s = requests.get(f'{API}/ilink/bot/get_qrcode_status',
|
||||
params={'qrcode': qr_id}, timeout=60).json()
|
||||
except requests.exceptions.ReadTimeout:
|
||||
continue
|
||||
st = s.get('status', '')
|
||||
if st != last: print(f' 状态: {st}'); last = st
|
||||
if st == 'confirmed':
|
||||
self.token, self.bot_id = s.get('bot_token', ''), s.get('ilink_bot_id', '')
|
||||
self._save(login_time=time.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
print(f'[QR登录] 成功! bot_id={self.bot_id}')
|
||||
return s
|
||||
if st == 'expired': raise RuntimeError('二维码过期')
|
||||
|
||||
def get_updates(self, timeout=30):
|
||||
try:
|
||||
resp = self._post('ilink/bot/getupdates',
|
||||
{'get_updates_buf': self._buf or '', 'base_info': {'channel_version': VER}},
|
||||
timeout=timeout + 5)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
return []
|
||||
if resp.get('errcode'):
|
||||
print(f'[getUpdates] err: {resp.get("errcode")} {resp.get("errmsg","")}')
|
||||
if resp['errcode'] == -14: self._buf = ''; self._save()
|
||||
return []
|
||||
nb = resp.get('get_updates_buf', '')
|
||||
if nb: self._buf = nb; self._save()
|
||||
return resp.get('msgs') or []
|
||||
|
||||
def send_text(self, to_user_id, text, context_token=''):
|
||||
msg = {'from_user_id': '', 'to_user_id': to_user_id,
|
||||
'client_id': f'pyclient-{uuid.uuid4().hex[:16]}',
|
||||
'message_type': MSG_BOT, 'message_state': STATE_FINISH,
|
||||
'item_list': [{'type': ITEM_TEXT, 'text_item': {'text': text}}]}
|
||||
if context_token: msg['context_token'] = context_token
|
||||
return self._post('ilink/bot/sendmessage', {'msg': msg, 'base_info': {'channel_version': VER}})
|
||||
|
||||
def send_typing(self, to_user_id, typing_ticket='', cancel=False):
|
||||
return self._post('ilink/bot/sendtyping', {
|
||||
'to_user_id': to_user_id, 'typing_ticket': typing_ticket,
|
||||
'typing_status': 2 if cancel else 1, 'base_info': {'channel_version': VER}})
|
||||
|
||||
@staticmethod
|
||||
def extract_text(msg):
|
||||
return '\n'.join(it['text_item'].get('text', '')
|
||||
for it in msg.get('item_list', [])
|
||||
if it.get('type') == ITEM_TEXT and it.get('text_item'))
|
||||
|
||||
@staticmethod
|
||||
def is_user_msg(msg): return msg.get('message_type') == MSG_USER
|
||||
|
||||
def run_loop(self, on_message, poll_timeout=30):
|
||||
print(f'[Bot] 监听中... (bot_id={self.bot_id})')
|
||||
seen = set()
|
||||
while True:
|
||||
try:
|
||||
for msg in self.get_updates(poll_timeout):
|
||||
mid = msg.get('message_id', 0)
|
||||
if not self.is_user_msg(msg) or mid in seen: continue
|
||||
seen.add(mid)
|
||||
if len(seen) > 5000: seen = set(list(seen)[-2000:])
|
||||
try: on_message(self, msg)
|
||||
except Exception as e: print(f'[Bot] 回调异常: {e}')
|
||||
except KeyboardInterrupt: print('[Bot] 退出'); break
|
||||
except Exception as e: print(f'[Bot] 异常: {e},5s重试'); time.sleep(5)
|
||||
|
||||
agent = GeneraticAgent()
|
||||
agent.verbose = False
|
||||
|
||||
_TAG_PATS = [r'<' + t + r'>.*?</' + t + r'>' for t in ('thinking', 'summary', 'tool_use')]
|
||||
_TAG_PATS.append(r'<file_content>.*?</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 '...'
|
||||
|
||||
def _split(text, limit=1800):
|
||||
"""Split text into chunks respecting line boundaries."""
|
||||
if len(text) <= limit: return [text]
|
||||
chunks, cur = [], ''
|
||||
for line in text.split('\n'):
|
||||
if len(cur) + len(line) + 1 > limit and cur:
|
||||
chunks.append(cur); cur = line
|
||||
else:
|
||||
cur = cur + '\n' + line if cur else line
|
||||
if cur: chunks.append(cur)
|
||||
return chunks or ['...']
|
||||
|
||||
def on_message(bot, msg):
|
||||
text = bot.extract_text(msg).strip()
|
||||
uid = msg.get('from_user_id', '')
|
||||
ctx = msg.get('context_token', '')
|
||||
if not text: return
|
||||
print(f'[WX] 收到: {text[:60]}', file=sys.__stdout__)
|
||||
|
||||
# Commands
|
||||
if text in ('/stop', '/abort'):
|
||||
agent.abort()
|
||||
bot.send_text(uid, '已停止', context_token=ctx)
|
||||
return
|
||||
if text.startswith('/llm'):
|
||||
args = text.split()
|
||||
if len(args) > 1:
|
||||
try:
|
||||
n = int(args[1]); agent.next_llm(n)
|
||||
bot.send_text(uid, f'切换到 [{agent.llm_no}] {agent.get_llm_name()}', context_token=ctx)
|
||||
except (ValueError, IndexError):
|
||||
bot.send_text(uid, f'用法: /llm <0-{len(agent.list_llms())-1}>', context_token=ctx)
|
||||
else:
|
||||
lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in agent.list_llms()]
|
||||
bot.send_text(uid, 'LLMs:\n' + '\n'.join(lines), context_token=ctx)
|
||||
return
|
||||
|
||||
def _handle():
|
||||
prompt = f"If you need to show files to user, use [FILE:filepath] in your response.\n\n{text}"
|
||||
dq = agent.put_task(prompt, source="wechat")
|
||||
try: bot.send_typing(uid)
|
||||
except: pass
|
||||
# Wait for completion
|
||||
result = ''
|
||||
try:
|
||||
while True:
|
||||
item = dq.get(timeout=300)
|
||||
if 'done' in item: result = item['done']; break
|
||||
except queue.Empty:
|
||||
result = '[超时]'
|
||||
show = _clean(result)
|
||||
show = re.sub(r'\[FILE:[^\]]+\]', '', show).strip() or '...'
|
||||
for chunk in _split(show):
|
||||
try: bot.send_text(uid, chunk, context_token=ctx)
|
||||
except Exception as e: print(f'[WX] send err: {e}')
|
||||
time.sleep(0.3)
|
||||
|
||||
threading.Thread(target=_handle, daemon=True).start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM); _lock.bind(('127.0.0.1', 19528))
|
||||
except OSError:
|
||||
print('[WeChat] Another instance running, exiting.'); sys.exit(1)
|
||||
_logf = open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'temp', 'wechatapp.log'), 'a', encoding='utf-8', buffering=1)
|
||||
sys.stdout = sys.stderr = _logf
|
||||
print(f'[NEW] Process starting {time.strftime("%m-%d %H:%M")}')
|
||||
bot = WxBotClient()
|
||||
if not bot.token:
|
||||
sys.stdout = sys.stderr = sys.__stdout__ # restore for QR display
|
||||
bot.login_qr()
|
||||
sys.stdout = sys.stderr = _logf
|
||||
threading.Thread(target=agent.run, daemon=True).start()
|
||||
print(f'WeChat Bot 已启动 (bot_id={bot.bot_id})', file=sys.__stdout__)
|
||||
bot.run_loop(on_message)
|
||||
Reference in New Issue
Block a user