aria-code 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
aria_relay_client.py ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aria_relay_client.py — 连接 Aria 中继服务器的 WebSocket 客户端
4
+ ================================================================
5
+ 本机运行,把中继服务器转发来的飞书消息交给 aria_feishu_bot 处理,
6
+ 并把 LLM 回复返回给中继服务器,再由服务器推送到飞书。
7
+
8
+ 启动方式:
9
+ python3 aria_relay_client.py # 单次连接(断线自动重连)
10
+ python3 aria_relay_client.py --once # 调试:收到第一条消息后退出
11
+
12
+ 所需环境变量(读取 ~/.aria/.env):
13
+ ARIA_RELAY_URL wss://relay.aria.ai(或自建服务器地址)
14
+ ARIA_RELAY_CLIENT_ID setup_wizard 生成的 12 位 hex id
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import json
21
+ import logging
22
+ import os
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+
27
+ logger = logging.getLogger("aria.relay_client")
28
+
29
+ # ── 加载 ~/.aria/.env ──────────────────────────────────────────────────────
30
+
31
+ def _load_env() -> None:
32
+ env_file = Path.home() / ".aria" / ".env"
33
+ if env_file.exists():
34
+ for line in env_file.read_text().splitlines():
35
+ line = line.strip()
36
+ if line and not line.startswith("#") and "=" in line:
37
+ k, _, v = line.partition("=")
38
+ k = k.strip()
39
+ if k not in os.environ:
40
+ os.environ[k] = v.strip()
41
+
42
+
43
+ _load_env()
44
+
45
+ _RELAY_URL = os.environ.get("ARIA_RELAY_URL", "wss://relay.aria.ai")
46
+ _CLIENT_ID = os.environ.get("ARIA_RELAY_CLIENT_ID", "")
47
+ _RECONNECT_DELAY_MAX = 60 # seconds
48
+ _RECONNECT_DELAY_BASE = 3
49
+
50
+
51
+ # ── 本地 aria_feishu_bot import ───────────────────────────────────────────────
52
+
53
+ def _get_feishu_bot():
54
+ aria_dir = Path(__file__).parent
55
+ if str(aria_dir) not in sys.path:
56
+ sys.path.insert(0, str(aria_dir))
57
+ try:
58
+ import aria_feishu_bot
59
+ return aria_feishu_bot
60
+ except ImportError as e:
61
+ logger.warning("aria_feishu_bot not importable: %s", e)
62
+ return None
63
+
64
+
65
+ # ── Message handler ───────────────────────────────────────────────────────────
66
+
67
+ async def _handle_message(raw_msg: dict, ws) -> None:
68
+ """
69
+ Server sends:
70
+ {"type": "message", "id": "req_xxx", "payload": <feishu_event_dict>}
71
+
72
+ We reply:
73
+ {"type": "response", "id": "req_xxx", "result": <any>}
74
+ """
75
+ req_id = raw_msg.get("id", "")
76
+ payload = raw_msg.get("payload", {})
77
+
78
+ bot = _get_feishu_bot()
79
+ if bot is None:
80
+ result = {"error": "aria_feishu_bot unavailable"}
81
+ else:
82
+ try:
83
+ result = await bot.dispatch_event(payload)
84
+ except Exception as e:
85
+ logger.exception("dispatch_event error")
86
+ result = {"error": str(e)[:300]}
87
+
88
+ reply = json.dumps({"type": "response", "id": req_id, "result": result})
89
+ await ws.send(reply)
90
+
91
+
92
+ # ── Main loop ─────────────────────────────────────────────────────────────────
93
+
94
+ async def _connect_and_serve(once: bool = False) -> None:
95
+ try:
96
+ import websockets # type: ignore
97
+ except ImportError:
98
+ logger.error("websockets package not installed — run: pip install websockets")
99
+ sys.exit(1)
100
+
101
+ if not _CLIENT_ID:
102
+ logger.error(
103
+ "ARIA_RELAY_CLIENT_ID is not set. "
104
+ "Run setup_wizard.py to generate your client ID."
105
+ )
106
+ sys.exit(1)
107
+
108
+ delay = _RECONNECT_DELAY_BASE
109
+ while True:
110
+ try:
111
+ logger.info("Connecting to %s (client_id=%s)", _RELAY_URL, _CLIENT_ID)
112
+ async with websockets.connect(
113
+ _RELAY_URL,
114
+ ping_interval=30,
115
+ ping_timeout=10,
116
+ open_timeout=15,
117
+ ) as ws:
118
+ # Register with server
119
+ await ws.send(json.dumps({
120
+ "type": "register",
121
+ "client_id": _CLIENT_ID,
122
+ }))
123
+ ack_raw = await asyncio.wait_for(ws.recv(), timeout=10)
124
+ ack = json.loads(ack_raw)
125
+ if not ack.get("ok"):
126
+ logger.error("Registration rejected: %s", ack.get("reason", "unknown"))
127
+ await asyncio.sleep(delay)
128
+ continue
129
+
130
+ logger.info("Registered. Waiting for messages…")
131
+ delay = _RECONNECT_DELAY_BASE # reset on success
132
+
133
+ async for raw in ws:
134
+ try:
135
+ msg = json.loads(raw)
136
+ except json.JSONDecodeError:
137
+ logger.warning("Invalid JSON from relay: %r", raw[:100])
138
+ continue
139
+
140
+ if msg.get("type") == "ping":
141
+ await ws.send(json.dumps({"type": "pong"}))
142
+ continue
143
+
144
+ if msg.get("type") == "message":
145
+ asyncio.create_task(_handle_message(msg, ws))
146
+
147
+ if once:
148
+ return
149
+
150
+ except (OSError, ConnectionRefusedError) as e:
151
+ logger.warning("Connection failed: %s — retry in %ds", e, delay)
152
+ except asyncio.CancelledError:
153
+ logger.info("Relay client cancelled")
154
+ return
155
+ except Exception as e:
156
+ logger.warning("Relay error: %s — retry in %ds", e, delay)
157
+
158
+ await asyncio.sleep(delay)
159
+ delay = min(delay * 2, _RECONNECT_DELAY_MAX)
160
+
161
+
162
+ def main() -> None:
163
+ import argparse
164
+ parser = argparse.ArgumentParser(description="Aria 中继客户端")
165
+ parser.add_argument("--once", action="store_true", help="接收一条消息后退出(调试用)")
166
+ parser.add_argument("--verbose", "-v", action="store_true")
167
+ args = parser.parse_args()
168
+
169
+ logging.basicConfig(
170
+ level=logging.DEBUG if args.verbose else logging.INFO,
171
+ format="%(asctime)s [aria-relay] %(message)s",
172
+ datefmt="%H:%M:%S",
173
+ )
174
+
175
+ try:
176
+ asyncio.run(_connect_and_serve(once=args.once))
177
+ except KeyboardInterrupt:
178
+ pass
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
aria_relay_server.py ADDED
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aria_relay_server.py — Aria 中继服务器
4
+ =======================================
5
+ 你(产品方)部署一次,所有用户共用。
6
+
7
+ 架构:
8
+ 用户飞书消息
9
+ → 飞书云 → POST /feishu/event
10
+ → relay_server 查 feishu_user_id → 对应 WebSocket
11
+ → relay_server 转发 payload 到用户本机
12
+ → aria_relay_client.dispatch_event()
13
+ → Aria LLM 回复
14
+ → relay_server 收到 response
15
+ → relay_server 调飞书 reply_message API
16
+ → 飞书消息卡片展示给用户
17
+
18
+ WebSocket 注册流程:
19
+ 1. 用户本机 aria_relay_client 连接 wss://relay.yourdomain.com
20
+ 2. 发送: {"type": "register", "client_id": "aria-xxxxxxxxxxxx"}
21
+ 3. 用户在飞书向 Aria Bot 发: /bind ARIA-BIND-ARIA-XXXXXXXXXXXX
22
+ 4. 服务器记录: feishu_user_id → client_id
23
+ 5. 后续消息经 WebSocket 透传
24
+
25
+ 依赖:
26
+ pip install fastapi uvicorn websockets httpx
27
+
28
+ 启动:
29
+ python3 aria_relay_server.py
30
+ # 或
31
+ uvicorn aria_relay_server:app --host 0.0.0.0 --port 8765
32
+
33
+ 所需环境变量:
34
+ FEISHU_APP_ID 飞书应用 App ID
35
+ FEISHU_APP_SECRET 飞书应用 App Secret
36
+ RELAY_SECRET WebSocket 注册鉴权(可选,留空则不鉴权)
37
+ DB_PATH SQLite 路径(默认 ./relay.db)
38
+ MESSAGE_TIMEOUT 等待用户本机回复的超时秒数(默认 90)
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import asyncio
44
+ import hashlib
45
+ import json
46
+ import logging
47
+ import os
48
+ import sqlite3
49
+ import time
50
+ import uuid
51
+ from contextlib import asynccontextmanager
52
+ from typing import Any, Optional
53
+
54
+ import httpx
55
+ from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
56
+ from fastapi.responses import JSONResponse
57
+
58
+ logger = logging.getLogger("aria.relay_server")
59
+
60
+ # ── Config ────────────────────────────────────────────────────────────────────
61
+
62
+ _FEISHU_APP_ID = os.environ.get("FEISHU_APP_ID", "")
63
+ _FEISHU_APP_SECRET = os.environ.get("FEISHU_APP_SECRET", "")
64
+ _RELAY_SECRET = os.environ.get("RELAY_SECRET", "")
65
+ _DB_PATH = os.environ.get("DB_PATH", "./relay.db")
66
+ _MSG_TIMEOUT = int(os.environ.get("MESSAGE_TIMEOUT", "90"))
67
+ _FEISHU_API = "https://open.feishu.cn/open-apis"
68
+
69
+
70
+ # ── SQLite store ──────────────────────────────────────────────────────────────
71
+
72
+ def _db() -> sqlite3.Connection:
73
+ conn = sqlite3.connect(_DB_PATH, check_same_thread=False)
74
+ conn.row_factory = sqlite3.Row
75
+ conn.execute("""
76
+ CREATE TABLE IF NOT EXISTS bindings (
77
+ feishu_user_id TEXT PRIMARY KEY,
78
+ client_id TEXT NOT NULL,
79
+ bound_at REAL NOT NULL
80
+ )
81
+ """)
82
+ conn.execute("""
83
+ CREATE TABLE IF NOT EXISTS pending_binds (
84
+ client_id TEXT PRIMARY KEY,
85
+ created_at REAL NOT NULL
86
+ )
87
+ """)
88
+ conn.commit()
89
+ return conn
90
+
91
+
92
+ _db_conn: Optional[sqlite3.Connection] = None
93
+
94
+
95
+ def get_db() -> sqlite3.Connection:
96
+ global _db_conn
97
+ if _db_conn is None:
98
+ _db_conn = _db()
99
+ return _db_conn
100
+
101
+
102
+ def _lookup_client(feishu_user_id: str) -> Optional[str]:
103
+ row = get_db().execute(
104
+ "SELECT client_id FROM bindings WHERE feishu_user_id = ?",
105
+ (feishu_user_id,),
106
+ ).fetchone()
107
+ return row["client_id"] if row else None
108
+
109
+
110
+ def _bind(feishu_user_id: str, client_id: str) -> None:
111
+ get_db().execute(
112
+ "INSERT OR REPLACE INTO bindings VALUES (?, ?, ?)",
113
+ (feishu_user_id, client_id, time.time()),
114
+ )
115
+ get_db().execute(
116
+ "DELETE FROM pending_binds WHERE client_id = ?", (client_id,)
117
+ )
118
+ get_db().commit()
119
+
120
+
121
+ def _register_pending(client_id: str) -> None:
122
+ get_db().execute(
123
+ "INSERT OR REPLACE INTO pending_binds VALUES (?, ?)",
124
+ (client_id, time.time()),
125
+ )
126
+ get_db().commit()
127
+
128
+
129
+ def _is_valid_client_id(client_id: str) -> bool:
130
+ row = get_db().execute(
131
+ "SELECT client_id FROM pending_binds WHERE client_id = ?", (client_id,)
132
+ ).fetchone()
133
+ return row is not None
134
+
135
+
136
+ # ── WebSocket connection registry ─────────────────────────────────────────────
137
+
138
+ _connections: dict[str, WebSocket] = {} # client_id → WebSocket
139
+ _pending_responses: dict[str, asyncio.Future] = {} # request_id → Future
140
+
141
+
142
+ # ── Feishu API helpers ────────────────────────────────────────────────────────
143
+
144
+ _feishu_token_cache: dict[str, Any] = {}
145
+
146
+
147
+ async def _get_tenant_token() -> str:
148
+ now = time.time()
149
+ if _feishu_token_cache.get("expires_at", 0) > now + 60:
150
+ return _feishu_token_cache["token"]
151
+
152
+ async with httpx.AsyncClient() as client:
153
+ resp = await client.post(
154
+ f"{_FEISHU_API}/auth/v3/tenant_access_token/internal",
155
+ json={"app_id": _FEISHU_APP_ID, "app_secret": _FEISHU_APP_SECRET},
156
+ timeout=10,
157
+ )
158
+ data = resp.json()
159
+
160
+ token = data.get("tenant_access_token", "")
161
+ expire = int(data.get("expire", 7200))
162
+ _feishu_token_cache.update({"token": token, "expires_at": now + expire})
163
+ return token
164
+
165
+
166
+ async def _reply_feishu(message_id: str, content: str, color: str = "blue") -> None:
167
+ token = await _get_tenant_token()
168
+ card = {
169
+ "msg_type": "interactive",
170
+ "card": {
171
+ "header": {
172
+ "title": {"tag": "plain_text", "content": "Aria"},
173
+ "template": color,
174
+ },
175
+ "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": content}}],
176
+ },
177
+ }
178
+ async with httpx.AsyncClient() as client:
179
+ await client.post(
180
+ f"{_FEISHU_API}/im/v1/messages/{message_id}/reply",
181
+ headers={"Authorization": f"Bearer {token}"},
182
+ json=card,
183
+ timeout=15,
184
+ )
185
+
186
+
187
+ async def _send_feishu_text(open_id: str, text: str) -> None:
188
+ token = await _get_tenant_token()
189
+ async with httpx.AsyncClient() as client:
190
+ await client.post(
191
+ f"{_FEISHU_API}/im/v1/messages",
192
+ headers={"Authorization": f"Bearer {token}"},
193
+ params={"receive_id_type": "open_id"},
194
+ json={
195
+ "receive_id": open_id,
196
+ "msg_type": "text",
197
+ "content": json.dumps({"text": text}),
198
+ },
199
+ timeout=15,
200
+ )
201
+
202
+
203
+ # ── Route message to local aria instance ─────────────────────────────────────
204
+
205
+ async def _route_to_local(feishu_user_id: str, payload: dict) -> Optional[Any]:
206
+ """Forward Feishu event to the user's connected local aria instance."""
207
+ client_id = _lookup_client(feishu_user_id)
208
+ if not client_id:
209
+ return None
210
+
211
+ ws = _connections.get(client_id)
212
+ if not ws:
213
+ return None
214
+
215
+ req_id = f"req_{uuid.uuid4().hex[:10]}"
216
+ future: asyncio.Future = asyncio.get_event_loop().create_future()
217
+ _pending_responses[req_id] = future
218
+
219
+ try:
220
+ await ws.send_text(json.dumps({
221
+ "type": "message",
222
+ "id": req_id,
223
+ "payload": payload,
224
+ }))
225
+ result = await asyncio.wait_for(future, timeout=_MSG_TIMEOUT)
226
+ return result
227
+ except asyncio.TimeoutError:
228
+ logger.warning("Timeout waiting for response from client_id=%s", client_id)
229
+ return {"error": "Aria 本机响应超时,请检查 aria_relay_client 是否在线"}
230
+ finally:
231
+ _pending_responses.pop(req_id, None)
232
+
233
+
234
+ # ── FastAPI app ───────────────────────────────────────────────────────────────
235
+
236
+ @asynccontextmanager
237
+ async def lifespan(app: FastAPI):
238
+ logging.basicConfig(
239
+ level=logging.INFO,
240
+ format="%(asctime)s [relay] %(message)s",
241
+ datefmt="%H:%M:%S",
242
+ )
243
+ logger.info("Aria Relay Server started db=%s", _DB_PATH)
244
+ get_db() # init tables
245
+ yield
246
+ logger.info("Relay Server shutting down")
247
+
248
+
249
+ app = FastAPI(title="Aria Relay Server", lifespan=lifespan)
250
+
251
+
252
+ # ── WebSocket endpoint (users' local machines) ────────────────────────────────
253
+
254
+ @app.websocket("/ws")
255
+ async def ws_endpoint(websocket: WebSocket):
256
+ await websocket.accept()
257
+ client_id: Optional[str] = None
258
+
259
+ try:
260
+ # First message must be register
261
+ raw = await asyncio.wait_for(websocket.receive_text(), timeout=15)
262
+ msg = json.loads(raw)
263
+
264
+ if msg.get("type") != "register":
265
+ await websocket.send_text(json.dumps({"ok": False, "reason": "first message must be register"}))
266
+ return
267
+
268
+ client_id = msg.get("client_id", "")
269
+ if not client_id:
270
+ await websocket.send_text(json.dumps({"ok": False, "reason": "client_id required"}))
271
+ return
272
+
273
+ # Validate RELAY_SECRET if configured
274
+ if _RELAY_SECRET and msg.get("secret") != _RELAY_SECRET:
275
+ await websocket.send_text(json.dumps({"ok": False, "reason": "invalid secret"}))
276
+ return
277
+
278
+ # Register in-memory + mark as pending bind (if first time)
279
+ _connections[client_id] = websocket
280
+ if _lookup_client.__module__: # always true; used as noop to be explicit
281
+ _register_pending(client_id)
282
+
283
+ await websocket.send_text(json.dumps({"ok": True, "client_id": client_id}))
284
+ logger.info("Client connected: %s", client_id)
285
+
286
+ # Message loop
287
+ async for raw in websocket.iter_text():
288
+ try:
289
+ response_msg = json.loads(raw)
290
+ except json.JSONDecodeError:
291
+ continue
292
+
293
+ if response_msg.get("type") == "pong":
294
+ continue
295
+
296
+ if response_msg.get("type") == "response":
297
+ req_id = response_msg.get("id", "")
298
+ future = _pending_responses.get(req_id)
299
+ if future and not future.done():
300
+ future.set_result(response_msg.get("result"))
301
+
302
+ except WebSocketDisconnect:
303
+ pass
304
+ except asyncio.TimeoutError:
305
+ logger.warning("Register timeout for new connection")
306
+ except Exception as e:
307
+ logger.exception("WebSocket error: %s", e)
308
+ finally:
309
+ if client_id:
310
+ _connections.pop(client_id, None)
311
+ logger.info("Client disconnected: %s", client_id)
312
+
313
+
314
+ # ── Feishu event endpoint ─────────────────────────────────────────────────────
315
+
316
+ @app.post("/feishu/event")
317
+ async def feishu_event(request: Request):
318
+ """Feishu Developer Console → Event Subscription → Request URL: /feishu/event"""
319
+ body = await request.body()
320
+ try:
321
+ payload = json.loads(body)
322
+ except Exception:
323
+ raise HTTPException(400, "Invalid JSON")
324
+
325
+ # URL verification challenge
326
+ if "challenge" in payload:
327
+ return {"challenge": payload["challenge"]}
328
+
329
+ # Extract sender + message_id
330
+ event = payload.get("event", {})
331
+ message = event.get("message", {})
332
+ sender = event.get("sender", {})
333
+ feishu_user_id = sender.get("sender_id", {}).get("open_id", "")
334
+ message_id = message.get("message_id", "")
335
+
336
+ if not feishu_user_id:
337
+ return {"code": 0}
338
+
339
+ # Handle /bind command
340
+ msg_type = message.get("message_type", "")
341
+ if msg_type == "text":
342
+ try:
343
+ text_content = json.loads(message.get("content", "{}")).get("text", "").strip()
344
+ except Exception:
345
+ text_content = ""
346
+
347
+ if text_content.upper().startswith("/BIND ") or text_content.upper().startswith("ARIA-BIND-"):
348
+ raw_code = text_content.upper().replace("/BIND ", "").strip()
349
+ # Normalize: "ARIA-BIND-ARIA-XXXX" → "aria-xxxx"
350
+ client_id_upper = raw_code.replace("ARIA-BIND-", "").replace("ARIA-", "aria-").lower()
351
+ _bind(feishu_user_id, client_id_upper)
352
+ await _send_feishu_text(
353
+ feishu_user_id,
354
+ f"✅ 绑定成功!你的 Aria 实例已连接。\n"
355
+ f"Client ID: {client_id_upper}\n"
356
+ f"现在可以直接发消息与你的 Aria 交互了。"
357
+ )
358
+ return {"code": 0}
359
+
360
+ # Route to local aria instance
361
+ result = await _route_to_local(feishu_user_id, payload)
362
+
363
+ if result is None:
364
+ # No binding found or client offline
365
+ if not _lookup_client(feishu_user_id):
366
+ await _send_feishu_text(
367
+ feishu_user_id,
368
+ "👋 你好!要开始使用 Aria,请:\n"
369
+ "1. 在你的电脑上安装 Aria Code\n"
370
+ "2. 运行 `python3 setup_wizard.py` 完成配置\n"
371
+ "3. 发送绑定码绑定你的账户"
372
+ )
373
+ else:
374
+ await _reply_feishu(
375
+ message_id,
376
+ "⚠️ Aria 本机未连接。请确保你的电脑上 `aria_relay_client.py` 正在运行。",
377
+ color="yellow",
378
+ )
379
+
380
+ return {"code": 0}
381
+
382
+
383
+ # ── Status endpoint ───────────────────────────────────────────────────────────
384
+
385
+ @app.get("/status")
386
+ async def status():
387
+ return {
388
+ "connected_clients": len(_connections),
389
+ "client_ids": list(_connections.keys()),
390
+ "total_bindings": get_db().execute("SELECT COUNT(*) FROM bindings").fetchone()[0],
391
+ "feishu_app_configured": bool(_FEISHU_APP_ID),
392
+ }
393
+
394
+
395
+ # ── Entry point ───────────────────────────────────────────────────────────────
396
+
397
+ if __name__ == "__main__":
398
+ import uvicorn
399
+ uvicorn.run(
400
+ "aria_relay_server:app",
401
+ host="0.0.0.0",
402
+ port=int(os.environ.get("PORT", "8765")),
403
+ reload=False,
404
+ log_level="info",
405
+ )