test_xiaoyi2.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #!/usr/bin/env python3
  2. """小艺 ↔ Agent67 桥接 — PaaS agent: ag_e7e376960efe (lambda-xiaoyi)"""
  3. import json, time, hmac, hashlib, base64, ssl, uuid, threading, sys
  4. AK = 'd031c15a2c7e4ad7a8c71312c31816c9'
  5. SK = '8FB5757D89AA777C31EE542A7C0BF966347143004C3591766762CF429AC21BBB'
  6. AGENT_ID = 'agent935da52acaeb48d09f6adc858c7d3ae2'
  7. WS1 = 'wss://hag.cloud.huawei.com/openclaw/v1/ws/link'
  8. WS2 = 'wss://116.63.174.231/openclaw/v1/ws/link'
  9. PAAS_AGENT_ID = 'ag_e7e376960efe'
  10. PAAS_API_KEY = 'ap_8c8694e7f49d9af60c30a92ae17971d6'
  11. import os
  12. from pathlib import Path
  13. _project_root = Path(__file__).resolve().parent
  14. sys.path.insert(0, str(_project_root))
  15. sys.path.insert(0, str(_project_root / 'agentexample'))
  16. _assistant = None
  17. LOG = open('/tmp/xiaoyi_test.log', 'w', buffering=1)
  18. def log(msg):
  19. line = f'[{time.strftime("%H:%M:%S")}] {msg}'
  20. print(line, flush=True)
  21. LOG.write(line + '\n')
  22. LOG.flush()
  23. def headers():
  24. ts = str(int(time.time() * 1000))
  25. sig = base64.b64encode(hmac.new(SK.encode(), ts.encode(), hashlib.sha256).digest()).decode()
  26. return {'x-access-key': AK, 'x-sign': sig, 'x-ts': ts, 'x-agent-id': AGENT_ID}
  27. def wrap(session_id, task_id, msg_id, result_obj):
  28. jsonrpc = {'jsonrpc': '2.0', 'id': msg_id, 'result': result_obj}
  29. return json.dumps({
  30. 'msgType': 'agent_response', 'agentId': AGENT_ID,
  31. 'sessionId': session_id, 'taskId': task_id,
  32. 'msgDetail': json.dumps(jsonrpc),
  33. })
  34. def run_server(name, url):
  35. import websocket
  36. sslopt = {}
  37. if '116.63' in url:
  38. sslopt = {'cert_reqs': ssl.CERT_NONE, 'check_hostname': False}
  39. ws = websocket.WebSocket(sslopt=sslopt)
  40. ws.timeout = 600
  41. log(f'[{name}] Connecting...')
  42. ws.connect(url, header=headers())
  43. log(f'[{name}] CONNECTED')
  44. # init
  45. ws.send(json.dumps({'msgType': 'clawd_bot_init', 'agentId': AGENT_ID}))
  46. log(f'[{name}] init sent')
  47. # heartbeat thread
  48. def heartbeat():
  49. while True:
  50. time.sleep(25)
  51. try:
  52. ws.send(json.dumps({'msgType': 'heartbeat', 'agentId': AGENT_ID, 'timestamp': int(time.time()*1000)}))
  53. except:
  54. break
  55. t = threading.Thread(target=heartbeat, daemon=True)
  56. t.start()
  57. # listen
  58. while True:
  59. try:
  60. raw = ws.recv()
  61. if not raw:
  62. continue
  63. data = json.loads(raw)
  64. if data.get('msgType') == 'heartbeat':
  65. continue
  66. log(f'📩 [{name}] {json.dumps(data, ensure_ascii=False)[:300]}')
  67. params = data.get('params', {})
  68. task_id = params.get('id', data.get('id', ''))
  69. session_id = params.get('sessionId', data.get('sessionId', ''))
  70. msg_id = data.get('id', '')
  71. parts = params.get('message', {}).get('parts', [])
  72. text = ' '.join(p.get('text', '') for p in parts if p.get('kind') == 'text').strip()
  73. if not text or not task_id:
  74. continue
  75. top_session = data.get('sessionId', '')
  76. log(f' User: {text}')
  77. log(f' param_session={session_id} top_session={top_session}')
  78. sid = top_session
  79. # 定期发 working 状态防止小艺超时
  80. agent_done = threading.Event()
  81. agent_reply = [None]
  82. def keep_alive():
  83. """每 8 秒发一次 reasoningText,保持连接活跃"""
  84. tick = 0
  85. msgs = ['正在思考...', '分析问题中...', '调用工具中...', '处理数据...', '即将完成...']
  86. while not agent_done.is_set():
  87. time.sleep(8)
  88. if agent_done.is_set():
  89. break
  90. tick += 1
  91. # 用 reasoningText (原始插件的做法),不用 status working
  92. ws.send(wrap(sid, task_id, msg_id, {
  93. 'taskId': task_id, 'kind': 'artifact-update',
  94. 'append': True, 'lastChunk': True, 'final': False,
  95. 'artifact': {'artifactId': f'reason_{int(time.time()*1000)}',
  96. 'parts': [{'kind': 'reasoningText', 'reasoningText': msgs[tick % len(msgs)] + '\n'}]},
  97. }))
  98. log(f' [{name}] ♻ reasoning #{tick}')
  99. ka_thread = threading.Thread(target=keep_alive, daemon=True)
  100. ka_thread.start()
  101. t0 = time.time()
  102. # 调用 agent67 (Claude Code)
  103. try:
  104. from agent67.core.assistant import PersonalAssistant
  105. global _assistant
  106. if _assistant is None:
  107. _assistant = PersonalAssistant(model="sonnet", use_api=False)
  108. reply = _assistant.chat(text)
  109. except Exception as e:
  110. log(f' [{name}] agent error: {e}')
  111. reply = f'🐂 lambda67: 抱歉,处理出错了: {e}'
  112. finally:
  113. agent_done.set()
  114. art_id = f'artifact_{int(time.time()*1000)}'
  115. # 1. 内容 (append=true, final=false)
  116. ws.send(wrap(sid, task_id, msg_id, {
  117. 'taskId': task_id, 'kind': 'artifact-update',
  118. 'append': True, 'lastChunk': False, 'final': False,
  119. 'artifact': {'artifactId': art_id, 'parts': [{'kind': 'text', 'text': reply}]},
  120. }))
  121. log(f' [{name}] → content (sid={sid[:16]})')
  122. # 2. 空结束标记 (append=true, final=true, text="")
  123. ws.send(wrap(sid, task_id, msg_id, {
  124. 'taskId': task_id, 'kind': 'artifact-update',
  125. 'append': True, 'lastChunk': True, 'final': True,
  126. 'artifact': {'artifactId': art_id, 'parts': [{'kind': 'text', 'text': ''}]},
  127. }))
  128. log(f' [{name}] → final (sid={sid[:16]})')
  129. # 3. status completed — 告诉小艺结束了
  130. status_id = f'done_{int(time.time()*1000)}'
  131. ws.send(wrap(sid, task_id, status_id, {
  132. 'taskId': task_id, 'kind': 'status-update', 'final': True,
  133. 'status': {'state': 'completed', 'message': {'role': 'agent', 'parts': []}},
  134. }))
  135. log(f' [{name}] → status completed')
  136. # 记录到 PaaS
  137. try:
  138. import urllib.request as _ur
  139. _paas_body = json.dumps({
  140. 'input': text[:500], 'output': reply[:2000],
  141. 'status': 'completed', 'duration_ms': int((time.time() - t0) * 1000),
  142. 'steps': 1, 'source': 'xiaoyi-openclaw',
  143. }).encode()
  144. _req = _ur.Request(
  145. f'http://127.0.0.1:8067/api/v1/agents/{PAAS_AGENT_ID}/runs/record',
  146. data=_paas_body,
  147. headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {PAAS_API_KEY}'},
  148. method='POST')
  149. _ur.urlopen(_req, timeout=5)
  150. except Exception:
  151. pass
  152. log(f' [{name}] DONE ✓')
  153. except Exception as e:
  154. log(f'[{name}] ERROR: {e}')
  155. break
  156. log('=== 小艺测试 v2 (websocket-client, 无自动 ping) ===')
  157. t1 = threading.Thread(target=run_server, args=('S1', WS1), daemon=True)
  158. t2 = threading.Thread(target=run_server, args=('S2', WS2), daemon=True)
  159. t1.start()
  160. t2.start()
  161. t1.join()
  162. t2.join()