aria-code 0.52.0__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 (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1423 @@
1
+ """
2
+ aria_feishu_bot.py — Feishu (Lark) 多模态 AI 机器人
3
+ ======================================================
4
+ OpenClaw 同款设计:任意输入(文字/语音/图片/文件)→ Aria AI → 卡片回复
5
+
6
+ 两种运行模式:
7
+ 1. 嵌入 FastAPI(由 feishu_routes.py 调用)
8
+ 2. 独立运行 — python3 aria_feishu_bot.py [port]
9
+
10
+ 飞书端配置:
11
+ 1. 飞书开发者后台 → 创建自建应用
12
+ 2. 事件订阅 → Request URL: http://<host>/api/v1/feishu/event
13
+ 3. 权限:im:message / im:message:send_as_bot
14
+ 4. ~/.aria/.env 填写:
15
+ FEISHU_APP_ID=cli_xxx FEISHU_APP_SECRET=xxx
16
+ ANTHROPIC_API_KEY=xxx # 图片理解 / LLM
17
+ OPENAI_API_KEY=xxx # Whisper 语音转文字(可选)
18
+ FEISHU_ALLOWED_USER_IDS=uid1,uid2 # 留空=不限制
19
+
20
+ 支持的消息类型:
21
+ 📝 文字(非命令)→ Aria LLM 自然语言回答
22
+ 🎤 语音 → Whisper 转文字 → Aria LLM
23
+ 🖼️ 图片 → 视觉 LLM 分析(Claude / GPT-4V)
24
+ 📄 文件 → 自动解析 PDF/Excel/代码 → Aria LLM 总结
25
+
26
+ 结构化命令(/command):
27
+ /price AAPL /brief /screen
28
+ /report NVDA /run /price TSLA (调用 aria CLI -p 模式)
29
+ /alert add SYM cond v /alerts /status /help
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ import base64
36
+ import hashlib
37
+ import hmac
38
+ import json
39
+ import logging
40
+ import os
41
+ import re
42
+ import sys
43
+ import tempfile
44
+ import time
45
+ from pathlib import Path
46
+ from typing import Any, Dict, Optional
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ # Strip ANSI escape codes from aria CLI output
51
+ _ANSI_RE = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
52
+
53
+ # Lines that are tool/UI artifacts and must be stripped from bot replies
54
+ _BOT_NOISE_RE = re.compile(
55
+ r"^\s*(?:"
56
+ # ── diff / table lines (ASCII pipe AND Unicode box-drawing │ U+2502) ────
57
+ r"[│|][+\- \d]" # │298 │+ code |- old | context
58
+ r"|[│|]\s*$" # │ │ (empty cell borders)
59
+ r"|[┌┐└┘├┤┬┴┼─╌╍╴╶╷╸╹]" # box corners / connectors
60
+ # ── timing artifacts ────────────────────────────────────────────────────
61
+ r"|└\s*\d+[\.,]\d+s?\b"
62
+ r"|[└─]{1,3}\s*\d+[\.,]\d+\s*s"
63
+ # ── tool call / result bullets ──────────────────────────────────────────
64
+ r"| [●└■▸]"
65
+ r"| L \d"
66
+ # ── permission / confirmation dialog ────────────────────────────────────
67
+ r"|[›❯>]\s*\d+\." # › 1. Yes ❯ 1. Yes > 1. Yes
68
+ r"|\d+\.\s+Yes" # 1. Yes / 2. Yes, allow all
69
+ r"|\d+\.\s+No" # 3. No
70
+ r"|Enter number" # "Enter number (or Enter to keep current):"
71
+ r"|Cancelled"
72
+ # ── leftover Rich markup tags ────────────────────────────────────────────
73
+ r"|\[/?(?:cyan|dim|bold|red|green|yellow|blue|magenta|white|grey|reset)\]"
74
+ # ── horizontal rules ────────────────────────────────────────────────────
75
+ r"|\s*[━─═]{4,}\s*$"
76
+ r")"
77
+ )
78
+
79
+ # Inline timing / markup to strip from within a line
80
+ _INLINE_TIMING_RE = re.compile(r"\s*[└─]{1,2}\s*\d+[\.,]\d+\s*s\b")
81
+ _INLINE_RICH_TAG_RE = re.compile(r"\[/?(?:cyan|dim|bold|red|green|yellow|blue|magenta|white|grey|reset)\]")
82
+
83
+ _ARIA_CODE_DIR = Path(__file__).parent
84
+
85
+
86
+ def _swarm_orchestrator_path() -> Optional[Path]:
87
+ """Return the explicitly configured enterprise swarm entrypoint.
88
+
89
+ The previous implementation referenced a developer's local checkout,
90
+ which made the public bot command fail everywhere else. Enterprise swarm
91
+ orchestration is optional and may live in a separately deployed service,
92
+ so it must be configured instead of inferred from a workstation path.
93
+ """
94
+ raw = os.environ.get("ARIA_SWARM_ORCHESTRATOR", "").strip()
95
+ if not raw:
96
+ return None
97
+ return Path(raw).expanduser().resolve()
98
+
99
+ # ── Feishu API endpoints ───────────────────────────────────────────────────────
100
+
101
+ _FEISHU_API = "https://open.feishu.cn/open-apis"
102
+
103
+ # ── Token cache (tenant_access_token, expires ~2h) ────────────────────────────
104
+ _token_cache: dict[str, Any] = {"token": None, "expires_at": 0}
105
+
106
+
107
+ async def _get_access_token() -> Optional[str]:
108
+ """Fetch/cache tenant_access_token via app credentials."""
109
+ app_id = os.environ.get("FEISHU_APP_ID", "")
110
+ app_secret = os.environ.get("FEISHU_APP_SECRET", "")
111
+ if not app_id or not app_secret:
112
+ return None
113
+
114
+ now = time.time()
115
+ if _token_cache["token"] and _token_cache["expires_at"] > now + 60:
116
+ return _token_cache["token"]
117
+
118
+ try:
119
+ import httpx
120
+ async with httpx.AsyncClient(timeout=10) as client:
121
+ resp = await client.post(
122
+ f"{_FEISHU_API}/auth/v3/tenant_access_token/internal",
123
+ json={"app_id": app_id, "app_secret": app_secret},
124
+ )
125
+ data = resp.json()
126
+ token = data.get("tenant_access_token")
127
+ expire = int(data.get("expire", 7200))
128
+ _token_cache["token"] = token
129
+ _token_cache["expires_at"] = now + expire
130
+ return token
131
+ except Exception as exc:
132
+ logger.warning("Feishu token fetch failed: %s", exc)
133
+ return None
134
+
135
+
136
+ # ── Send message helpers ───────────────────────────────────────────────────────
137
+
138
+ async def _feishu_post(url: str, token: str, payload: dict) -> Optional[dict]:
139
+ """POST to Feishu API; log the response code on error."""
140
+ try:
141
+ import httpx
142
+ async with httpx.AsyncClient(timeout=15) as client:
143
+ resp = await client.post(
144
+ url,
145
+ headers={"Authorization": f"Bearer {token}",
146
+ "Content-Type": "application/json; charset=utf-8"},
147
+ json=payload,
148
+ )
149
+ data = resp.json()
150
+ code = data.get("code", 0)
151
+ if code != 0:
152
+ logger.error("Feishu API error %s: %s url=%s msg_id in payload=%s",
153
+ code, data.get("msg", ""), url.split("/")[-3:],
154
+ payload.get("receive_id", "—"))
155
+ else:
156
+ logger.debug("Feishu API ok: %s", url.split("/")[-2:])
157
+ return data
158
+ except Exception as exc:
159
+ logger.warning("Feishu POST failed: %s", exc)
160
+ return None
161
+
162
+
163
+ async def reply_text(message_id: str, text: str) -> None:
164
+ """Reply to a Feishu message with plain text (auto-truncated at 3000 chars)."""
165
+ if not message_id:
166
+ logger.error("reply_text: empty message_id — cannot reply")
167
+ return
168
+ token = await _get_access_token()
169
+ if not token:
170
+ logger.error("reply_text: no access token")
171
+ return
172
+ logger.info("reply_text → message_id=%s len=%d", message_id, len(text))
173
+ await _feishu_post(
174
+ f"{_FEISHU_API}/im/v1/messages/{message_id}/reply",
175
+ token,
176
+ {"msg_type": "text", "content": json.dumps({"text": text[:3000]})},
177
+ )
178
+
179
+
180
+ async def reply_card(message_id: str, title: str, body: str,
181
+ color: str = "blue", footer: str = "") -> None:
182
+ """Reply with an interactive card (title + Markdown body)."""
183
+ if not message_id:
184
+ logger.error("reply_card: empty message_id — cannot reply")
185
+ return
186
+ token = await _get_access_token()
187
+ if not token:
188
+ await reply_text(message_id, f"【{title}】\n{body}")
189
+ return
190
+ elements = _build_card_elements(body, footer)
191
+ card = {
192
+ "config": {"wide_screen_mode": True},
193
+ "header": {"title": {"tag": "plain_text", "content": title}, "template": color},
194
+ "elements": elements,
195
+ }
196
+ logger.info("reply_card → message_id=%s title=%s", message_id, title[:40])
197
+ result = await _feishu_post(
198
+ f"{_FEISHU_API}/im/v1/messages/{message_id}/reply",
199
+ token,
200
+ {"msg_type": "interactive", "content": json.dumps(card)},
201
+ )
202
+ # If card failed, fall back to plain text
203
+ if result and result.get("code") != 0:
204
+ logger.info("reply_card: card failed (code %s), falling back to text", result.get("code"))
205
+ await reply_text(message_id, f"【{title}】\n{body}")
206
+
207
+
208
+ async def reply_or_send(message_id: str, chat_id: str,
209
+ title: str, body: str,
210
+ color: str = "blue", footer: str = "") -> None:
211
+ """Try reply by message_id first; if that fails, send a new message to chat_id."""
212
+ if message_id:
213
+ result = await _reply_card_raw(message_id, title, body, color, footer)
214
+ if result is not None and result.get("code", 0) == 0:
215
+ return
216
+ logger.warning("reply failed (code=%s), falling back to send_card_to_chat",
217
+ result.get("code") if result else "no response")
218
+ if chat_id:
219
+ await send_card_to_chat(chat_id, title, body, color)
220
+ else:
221
+ logger.error("reply_or_send: both message_id and chat_id empty, cannot send")
222
+
223
+
224
+ def _build_card_elements(body: str, footer: str = "") -> list:
225
+ """Split body into visual sections for richer card layout."""
226
+ import re as _re
227
+ # Split on markdown `---` dividers or `##`/`###` section headers
228
+ _section_re = _re.compile(r'(?m)^[-─]{3,}\s*$')
229
+ raw_sections = _section_re.split(body.strip())
230
+ elements: list = []
231
+ for i, sec in enumerate(raw_sections):
232
+ sec = sec.strip()
233
+ if not sec:
234
+ continue
235
+ # Detect if section starts with a ## header and peel it off
236
+ _hdr_m = _re.match(r'^#{1,3}\s+(.+)\n', sec)
237
+ if _hdr_m:
238
+ hdr_text = _hdr_m.group(1).strip()
239
+ sec_body = sec[_hdr_m.end():].strip()
240
+ elements.append({"tag": "markdown", "content": f"**{hdr_text}**"})
241
+ if sec_body:
242
+ elements.append({"tag": "div", "text": {"tag": "lark_md", "content": sec_body[:900]}})
243
+ else:
244
+ elements.append({"tag": "div", "text": {"tag": "lark_md", "content": sec[:900]}})
245
+ if i < len(raw_sections) - 1:
246
+ elements.append({"tag": "hr"})
247
+
248
+ if not elements:
249
+ elements = [{"tag": "div", "text": {"tag": "lark_md", "content": body[:2000]}}]
250
+
251
+ if footer:
252
+ elements += [{"tag": "hr"}, {"tag": "note", "elements": [
253
+ {"tag": "plain_text", "content": footer}
254
+ ]}]
255
+ return elements
256
+
257
+
258
+ async def _reply_card_raw(message_id: str, title: str, body: str,
259
+ color: str = "blue", footer: str = "") -> Optional[dict]:
260
+ """Reply with a card; return raw API response dict."""
261
+ token = await _get_access_token()
262
+ if not token:
263
+ return None
264
+ elements = _build_card_elements(body, footer)
265
+ card = {
266
+ "config": {"wide_screen_mode": True},
267
+ "header": {"title": {"tag": "plain_text", "content": title}, "template": color},
268
+ "elements": elements,
269
+ }
270
+ return await _feishu_post(
271
+ f"{_FEISHU_API}/im/v1/messages/{message_id}/reply",
272
+ token,
273
+ {"msg_type": "interactive", "content": json.dumps(card)},
274
+ )
275
+
276
+
277
+ async def send_card_to_chat(chat_id: str, title: str, body: str,
278
+ color: str = "blue", receive_id_type: str = "chat_id") -> None:
279
+ """Send a new card message to a chat (group or user)."""
280
+ token = await _get_access_token()
281
+ if not token:
282
+ return
283
+ elements = _build_card_elements(body)
284
+ card = {
285
+ "config": {"wide_screen_mode": True},
286
+ "header": {"title": {"tag": "plain_text", "content": title}, "template": color},
287
+ "elements": elements,
288
+ }
289
+ try:
290
+ import httpx
291
+ async with httpx.AsyncClient(timeout=15) as client:
292
+ await client.post(
293
+ f"{_FEISHU_API}/im/v1/messages?receive_id_type={receive_id_type}",
294
+ headers={"Authorization": f"Bearer {token}"},
295
+ json={
296
+ "receive_id": chat_id,
297
+ "msg_type": "interactive",
298
+ "content": json.dumps({"card": card}),
299
+ },
300
+ )
301
+ except Exception as exc:
302
+ logger.warning("send_card_to_chat failed: %s", exc)
303
+
304
+
305
+ # ── Command router ─────────────────────────────────────────────────────────────
306
+
307
+ async def _handle_command(cmd: str, message_id: str, sender_id: str, chat_id: str = "") -> None:
308
+ """Parse a command string and reply with structured card."""
309
+ parts = cmd.strip().split()
310
+ if not parts:
311
+ return
312
+ verb = parts[0].lstrip("/").lower()
313
+
314
+ if verb == "help":
315
+ body = (
316
+ "**💬 直接发消息** — 自然语言提问,Aria AI 直接回答\n"
317
+ "**🎤 语音消息** — 自动转文字后 AI 分析\n"
318
+ "**🖼️ 图片** — 自动识别图表/截图内容\n"
319
+ "**📄 文件** — PDF/Excel/Word/代码 自动解析后 AI 总结\n\n"
320
+ "**结构化命令:**\n"
321
+ "`/price <symbol>` — 实时价格(支持 A 股 6 位代码)\n"
322
+ "`/brief` — 今日晨报摘要\n"
323
+ "`/screen` — 涨停预测 Top10\n"
324
+ "`/report <symbol>` — 个股研报(异步推送)\n"
325
+ "`/team <symbol>` — 🤖 多Agent研究(宏观+基本面+技术+风控)\n"
326
+ "`/football predict Arsenal vs Chelsea pl` — ⚽ 足球比赛预测\n"
327
+ "`/football standings pl` — 联赛积分榜(pl/bl/ll/sa/cl)\n"
328
+ "`/run <aria命令>` — 执行任意 Aria 命令,如 `/run /corr AAPL TSLA`\n"
329
+ "`/swarm <事件>` — 🚨 启动企业级跨部门多智能体协作\n"
330
+ "`/alert add <symbol> <cond> <value>` — 添加价格预警\n"
331
+ "  条件: `price_above` `price_below` `pct_change_above` `pct_change_below`\n"
332
+ "`/alerts` — 查看所有预警\n"
333
+ "`/status` — Daemon 运行状态\n"
334
+ "`/help` — 显示此帮助"
335
+ )
336
+ await reply_card(message_id, "📖 Aria 帮助", body, "blue")
337
+
338
+ elif verb == "price":
339
+ symbol = parts[1].upper() if len(parts) > 1 else ""
340
+ if not symbol:
341
+ await reply_text(message_id, "用法: /price <symbol>,例如 /price AAPL 或 /price 600036")
342
+ return
343
+ await reply_card(message_id, f"🔄 查询 {symbol}…", "正在获取实时行情,请稍候…", "blue")
344
+ try:
345
+ price, prev = await _fetch_price_feishu(symbol)
346
+ if price is None:
347
+ await reply_card(message_id, f"❌ {symbol}", "获取行情失败,请检查代码是否正确", "red")
348
+ return
349
+ pct = f"{(price - prev) / prev * 100:+.2f}%" if prev else "N/A"
350
+ color = "red" if prev and price > prev else "green" if prev and price < prev else "blue"
351
+ body = (
352
+ f"**当前价** ¥{price:.3f}\n"
353
+ f"**涨跌幅** {pct}\n"
354
+ f"**昨收** ¥{prev:.3f}" if prev else f"**当前价** {price:.4f}"
355
+ )
356
+ await reply_card(message_id, f"{'📈' if prev and price >= prev else '📉'} {symbol}", body, color)
357
+ except Exception as exc:
358
+ await reply_card(message_id, f"❌ {symbol}", f"查询失败: {exc}", "red")
359
+
360
+ elif verb == "brief":
361
+ await reply_card(message_id, "⏳ 生成晨报…", "正在获取市场数据,请稍候…", "blue")
362
+ try:
363
+ from aria_daemon import _run_morning_brief
364
+ brief = await _run_morning_brief()
365
+ await reply_card(message_id, "📊 Aria 晨报", brief[:2000], "green",
366
+ footer="Aria Code · 实时市场分析")
367
+ except Exception as exc:
368
+ await reply_card(message_id, "❌ 晨报生成失败", str(exc)[:300], "red")
369
+
370
+ elif verb == "screen":
371
+ await reply_card(message_id, "⏳ 筛选中…", "正在扫描 A 股涨停预测,请稍候…", "blue")
372
+ try:
373
+ from aria_daemon import _run_screener
374
+ result = await _run_screener()
375
+ await reply_card(message_id, "🔍 涨停预测 Top10", result[:2000], "turquoise")
376
+ except Exception as exc:
377
+ await reply_card(message_id, "❌ 筛选失败", str(exc)[:300], "red")
378
+
379
+ elif verb == "report":
380
+ symbol = parts[1].upper() if len(parts) > 1 else ""
381
+ if not symbol:
382
+ await reply_text(message_id, "用法: /report <symbol>,例如 /report NVDA")
383
+ return
384
+ await reply_card(message_id, f"⏳ 生成 {symbol} 研报…",
385
+ "正在进行多维度分析,通常需要 30-60 秒,完成后推送结果。", "blue")
386
+ asyncio.create_task(_async_report(symbol, message_id))
387
+
388
+ elif verb == "alert":
389
+ if len(parts) < 2:
390
+ await reply_text(message_id, "用法: /alert add <symbol> <cond> <value>")
391
+ return
392
+ sub = parts[1].lower()
393
+ if sub == "add" and len(parts) >= 5:
394
+ sym, cond, val = parts[2].upper(), parts[3], parts[4]
395
+ valid_conds = {"price_above", "price_below", "pct_change_above", "pct_change_below"}
396
+ if cond not in valid_conds:
397
+ await reply_card(message_id, "❌ 无效条件",
398
+ "支持的条件:\n" + "\n".join(f"• `{c}`" for c in sorted(valid_conds)), "red")
399
+ return
400
+ import sqlite3
401
+ from aria_daemon import _DB_PATH
402
+ with sqlite3.connect(_DB_PATH) as conn:
403
+ conn.execute(
404
+ "INSERT INTO alerts(id,symbol,condition,value,message,active) VALUES(?,?,?,?,?,1)",
405
+ (f"fs_{int(time.time())}_{sym}", sym, cond, float(val),
406
+ f"{sym} {cond.replace('_',' ')} {val}", )
407
+ )
408
+ conn.commit()
409
+ await reply_card(message_id, "✅ 预警已添加",
410
+ f"**{sym}** 当 {cond.replace('_',' ')} `{val}` 时触发通知", "green")
411
+ else:
412
+ await reply_text(message_id, "用法: /alert add <symbol> <cond> <value>")
413
+
414
+ elif verb == "alerts":
415
+ import sqlite3
416
+ from aria_daemon import _DB_PATH
417
+ rows = sqlite3.connect(_DB_PATH).execute(
418
+ "SELECT symbol,condition,value FROM alerts WHERE active=1 ORDER BY created_at DESC LIMIT 20"
419
+ ).fetchall()
420
+ if not rows:
421
+ await reply_card(message_id, "📋 当前预警", "暂无活跃预警", "blue")
422
+ else:
423
+ lines = "\n".join(f"• **{r[0]}** {r[1].replace('_',' ')} `{r[2]}`" for r in rows)
424
+ await reply_card(message_id, f"📋 活跃预警({len(rows)} 条)", lines, "blue")
425
+
426
+ elif verb == "status":
427
+ import sqlite3
428
+ from aria_daemon import _DB_PATH, _PID_FILE
429
+ pid_alive = _PID_FILE.exists()
430
+ conn = sqlite3.connect(_DB_PATH)
431
+ alert_count = conn.execute("SELECT COUNT(*) FROM alerts WHERE active=1").fetchone()[0]
432
+ sched_count = conn.execute("SELECT COUNT(*) FROM schedules WHERE active=1").fetchone()[0]
433
+ job_pending = conn.execute("SELECT COUNT(*) FROM webhook_jobs WHERE status='pending'").fetchone()[0]
434
+ body = (
435
+ f"**Daemon** {'🟢 运行中' if pid_alive else '🔴 未运行'}\n"
436
+ f"**活跃预警** {alert_count} 条\n"
437
+ f"**定时任务** {sched_count} 条\n"
438
+ f"**待处理 Jobs** {job_pending} 个"
439
+ )
440
+ await reply_card(message_id, "📡 Aria Daemon 状态", body,
441
+ "green" if pid_alive else "red")
442
+ elif verb == "football":
443
+ sub = parts[1].lower() if len(parts) > 1 else ""
444
+ rest = " ".join(parts[2:])
445
+ if sub == "predict" and " vs " in rest.lower():
446
+ await reply_card(message_id, "⚽ 预测中…", f"> {rest}", "blue")
447
+ asyncio.create_task(_handle_football_predict(rest, message_id))
448
+ elif sub == "standings":
449
+ league = rest.strip() or "pl"
450
+ await reply_card(message_id, "⚽ 获取积分榜…", f"联赛: {league.upper()}", "blue")
451
+ asyncio.create_task(_handle_football_standings(league, message_id))
452
+ else:
453
+ # Natural language after /football (e.g. "/football 预测加拿大跟波黑")
454
+ # → treat as NL query with football intent, route to LLM
455
+ nl_text = f"{sub} {rest}".strip() if rest else sub
456
+ _is_chinese = any('一' <= c <= '鿿' for c in nl_text)
457
+ _is_predict_kw = any(k in nl_text.lower() for k in (
458
+ "predict", "preview", "who wins", "who will", "预测", "谁赢", "比分", "胜率"
459
+ ))
460
+ if _is_chinese or _is_predict_kw:
461
+ asyncio.create_task(_handle_nl_query(nl_text, message_id, chat_id))
462
+ else:
463
+ await reply_card(
464
+ message_id, "⚽ /football 命令",
465
+ f"未识别子命令: `{sub}`\n\n**用法:**\n"
466
+ "- `/football predict Arsenal vs Chelsea pl` — 预测比赛\n"
467
+ "- `/football standings pl` — 积分榜\n"
468
+ "- 或直接用自然语言提问,例如:「预测加拿大跟波黑的比分」",
469
+ "yellow"
470
+ )
471
+
472
+ elif verb in ("team", "analyze"):
473
+ # /team <symbol> [--full] — multi-agent research team
474
+ sym_parts = [p for p in parts[1:] if not p.startswith("-")]
475
+ flags = [p for p in parts[1:] if p.startswith("-")]
476
+ symbol = sym_parts[0].upper() if sym_parts else ""
477
+ if not symbol:
478
+ await reply_card(message_id, "❓ 用法",
479
+ "`/team <symbol>` — 多Agent研究\n"
480
+ "`/team AAPL --full` — 完整7-agent模式\n"
481
+ "例: `/team NVDA` `/team 600519`", "blue")
482
+ return
483
+ flag_str = " " + " ".join(flags) if flags else ""
484
+ cmd = f"/team {symbol}{flag_str}"
485
+ await reply_card(message_id, f"🤖 多Agent分析 {symbol}…",
486
+ "正在启动4-agent并行分析,请稍候(约15-30s)…", "blue")
487
+ asyncio.create_task(_async_run_aria(cmd, message_id))
488
+
489
+ elif verb == "run":
490
+ # /run <aria-command> e.g. /run /price AAPL /run /corr AAPL TSLA NVDA
491
+ sub_cmd = " ".join(parts[1:]).strip() if len(parts) > 1 else ""
492
+ if not sub_cmd:
493
+ await reply_text(message_id, "用法: /run <aria命令>,例如 `/run /price AAPL`")
494
+ return
495
+ await reply_card(message_id, f"⚙️ 执行: {sub_cmd[:60]}", "正在运行,请稍候…", "blue")
496
+ asyncio.create_task(_async_run_aria(sub_cmd, message_id))
497
+
498
+ elif verb == "swarm":
499
+ # /swarm <事件> e.g. /swarm 俄罗斯仓库丢件
500
+ event = " ".join(parts[1:]).strip() if len(parts) > 1 else ""
501
+ if not event:
502
+ await reply_text(message_id, "用法: /swarm <事件>,例如 `/swarm 莫斯科01仓少件`")
503
+ return
504
+ await reply_card(message_id, "🚨 跨部门 Swarm 调度启动", f"事件: {event[:60]}\n\n正在唤醒 物流、财务、法务 Agent,请稍候…", "red")
505
+ asyncio.create_task(_async_run_swarm(event, message_id))
506
+
507
+ else:
508
+ await reply_card(message_id, "❓ 未知命令",
509
+ f"不认识 `/{verb}`,发送 `/help` 查看全部命令", "yellow")
510
+
511
+
512
+ async def _handle_football_predict(match_str: str, message_id: str) -> None:
513
+ """Handle /football predict <home> vs <away> [league] from Feishu."""
514
+ import re
515
+ m = re.match(r"(.+?)\s+vs\s+(.+?)(?:\s+(\w+))?$", match_str, re.IGNORECASE)
516
+ if not m:
517
+ await reply_card(message_id, "❌ 格式错误", "用法: `/football predict Arsenal vs Chelsea pl`", "red")
518
+ return
519
+ home_raw, away_raw, league = m.group(1).strip(), m.group(2).strip(), (m.group(3) or "pl")
520
+ try:
521
+ from football_data_client import _CN_TEAM_MAP, _FIFA_RATINGS, predict_match, predict_wc_match
522
+
523
+ # Translate Chinese team names → English for model lookup
524
+ home_en = _CN_TEAM_MAP.get(home_raw, home_raw)
525
+ away_en = _CN_TEAM_MAP.get(away_raw, away_raw)
526
+ home_low = home_en.lower().strip()
527
+ away_low = away_en.lower().strip()
528
+
529
+ # Use WC/national team model when both teams are in FIFA ratings table
530
+ if home_low in _FIFA_RATINGS and away_low in _FIFA_RATINGS:
531
+ pred = predict_wc_match(home_en, away_en, neutral_venue=True)
532
+ home_label = pred.get("home_name_cn") or home_raw
533
+ away_label = pred.get("away_name_cn") or away_raw
534
+ ranking_note = (
535
+ f"FIFA 排名: #{pred.get('home_ranking','?')} vs #{pred.get('away_ranking','?')}"
536
+ )
537
+ most_likely = pred["top_scorelines"][0]["score"] if pred.get("top_scorelines") else "?"
538
+ else:
539
+ pred = predict_match(home_en, away_en, league)
540
+ home_label, away_label = home_raw, away_raw
541
+ ranking_note = f"联赛: {league.upper()}"
542
+ most_likely = pred.get("most_likely_score") or (
543
+ pred["top_scorelines"][0]["score"] if pred.get("top_scorelines") else "?"
544
+ )
545
+
546
+ top = "\n".join(
547
+ f" {s['score']} — {s['prob']}%" for s in (pred.get("top_scorelines") or [])[:3]
548
+ )
549
+ body = (
550
+ f"**{home_label}** vs **{away_label}**\n"
551
+ f"*{ranking_note}*\n\n"
552
+ f"---\n"
553
+ f"🏆 主队胜: **{pred['home_win']:.0%}** "
554
+ f"平局: {pred['draw']:.0%} "
555
+ f"客队胜: {pred['away_win']:.0%}\n\n"
556
+ f"⚽ 预期进球: {pred['lambda_home']:.1f} – {pred['lambda_away']:.1f}\n"
557
+ f"📊 最可能比分: **{most_likely}**\n"
558
+ f"🎯 高概率比分:\n{top}\n\n"
559
+ f"双方均进球: {pred['btts']:.0%}\n"
560
+ f"*泊松模型量化预测,仅供参考*"
561
+ )
562
+ color = (
563
+ "green" if pred["home_win"] > pred["away_win"] + 0.1
564
+ else "red" if pred["away_win"] > pred["home_win"] + 0.1
565
+ else "yellow"
566
+ )
567
+ await reply_card(message_id, "⚽ 赛事预测", body, color)
568
+ except Exception as exc:
569
+ await reply_card(message_id, "❌ 预测失败", str(exc)[:300], "red")
570
+
571
+
572
+ async def _handle_football_standings(league: str, message_id: str) -> None:
573
+ """Handle /football standings from Feishu."""
574
+ try:
575
+ from football_data_client import get_standings
576
+ data = get_standings(league)
577
+ if not data:
578
+ await reply_card(message_id, "❌ 无法获取数据",
579
+ "请检查联赛代码或设置 FOOTBALL_DATA_API_KEY", "red")
580
+ return
581
+ rows = data["table"][:10]
582
+ lines = [f"**{data['league_name']}**\n"]
583
+ for r in rows:
584
+ form = r.get("form", "") or ""
585
+ lines.append(f"{r['pos']:2}. {r['team'][:18]:18} {r['pts']}分 {r['w']}W{r['d']}D{r['l']}L {form[:5]}")
586
+ await reply_card(message_id, "📊 积分榜 TOP10", "\n".join(lines), "blue")
587
+ except Exception as exc:
588
+ await reply_card(message_id, "❌ 获取失败", str(exc)[:300], "red")
589
+
590
+
591
+ async def _fetch_price_feishu(symbol: str):
592
+ """Thin wrapper around aria_daemon._fetch_price for use inside bot."""
593
+ try:
594
+ from aria_daemon import _fetch_price
595
+ return await _fetch_price(symbol)
596
+ except ImportError:
597
+ # Fallback: inline yfinance
598
+ import yfinance as yf
599
+ yfn = (symbol + (".SS" if symbol.startswith(("6", "5")) else ".SZ")
600
+ if symbol.isdigit() and len(symbol) == 6 else symbol)
601
+ info = yf.Ticker(yfn).fast_info
602
+ p = getattr(info, "last_price", None)
603
+ pc = getattr(info, "previous_close", None)
604
+ return (float(p) if p else None, float(pc) if pc else None)
605
+
606
+
607
+ async def _async_report(symbol: str, message_id: str) -> None:
608
+ """Background task: generate report and reply when done."""
609
+ try:
610
+ from aria_daemon import _run_report
611
+ result = await _run_report(symbol)
612
+ await reply_card(message_id, f"📄 {symbol} 研报完成", result[:2000], "turquoise",
613
+ footer="Aria Code · 多智能体分析")
614
+ except Exception as exc:
615
+ await reply_card(message_id, f"❌ {symbol} 研报失败", str(exc)[:300], "red")
616
+
617
+
618
+ async def _async_run_swarm(event: str, message_id: str) -> None:
619
+ """Background task: run the enterprise swarm orchestrator and reply."""
620
+ try:
621
+ orchestrator = _swarm_orchestrator_path()
622
+ if orchestrator is None:
623
+ raise RuntimeError(
624
+ "未配置企业 Swarm 编排器。请设置 ARIA_SWARM_ORCHESTRATOR 为可执行的 Python 脚本路径。"
625
+ )
626
+ if not orchestrator.is_file():
627
+ raise RuntimeError(f"企业 Swarm 编排器不存在:{orchestrator}")
628
+ proc = await asyncio.create_subprocess_exec(
629
+ sys.executable, str(orchestrator), event,
630
+ stdout=asyncio.subprocess.PIPE,
631
+ stderr=asyncio.subprocess.PIPE,
632
+ )
633
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=60)
634
+ out = stdout.decode("utf-8").strip()
635
+
636
+ # 提取各个 Agent 的处理日志
637
+ lines = out.split("\n")
638
+ body = ""
639
+ for line in lines:
640
+ if "SUPERVISOR" in line:
641
+ body += f"**[主管]** {line.split('SUPERVISOR]')[-1].strip()}\n"
642
+ elif "Logistics Agent" in line:
643
+ body += f"📦 **[物流]** {line.split('Logistics Agent]')[-1].strip()}\n"
644
+ elif "Finance Agent" in line:
645
+ body += f"💰 **[财务]** {line.split('Finance Agent]')[-1].strip()}\n"
646
+ elif "Legal Agent" in line:
647
+ body += f"⚖️ **[法务]** {line.split('Legal Agent]')[-1].strip()}\n"
648
+
649
+ await reply_card(message_id, "🎉 Swarm 处理报告", body[:2000] if body else "处理完成。", "turquoise", footer="Enterprise Supervisor Orchestration")
650
+ except Exception as exc:
651
+ await reply_card(message_id, "❌ Swarm 调度失败", str(exc)[:300], "red")
652
+
653
+
654
+ async def _async_run_aria(cmd: str, message_id: str) -> None:
655
+ """Background task: run aria CLI command and reply with result."""
656
+ result = await _query_aria_llm(cmd, timeout=120)
657
+ color = "red" if result.startswith("❌") or result.startswith("⏱️") else "turquoise"
658
+ await reply_card(message_id, "✅ Aria 执行完成", result[:2000], color,
659
+ footer=f"命令: {cmd[:80]}")
660
+
661
+
662
+ # ── Multimodal helpers ────────────────────────────────────────────────────────
663
+
664
+ def _is_allowed_user(user_id: str) -> bool:
665
+ """Check FEISHU_ALLOWED_USER_IDS allowlist (empty = allow all)."""
666
+ raw = os.environ.get("FEISHU_ALLOWED_USER_IDS", "").strip()
667
+ if not raw:
668
+ return True
669
+ return user_id in {u.strip() for u in raw.split(",") if u.strip()}
670
+
671
+
672
+ async def _query_aria_direct(text: str, timeout: int = 90) -> str:
673
+ """
674
+ Query the LLM directly via providers/llm/registry.py — no subprocess, no tool use.
675
+ Used for conversational NL queries where we want a clean text answer.
676
+ Falls back to _query_aria_llm on ImportError.
677
+ """
678
+ try:
679
+ import sys as _sys
680
+ _sys.path.insert(0, str(_ARIA_CODE_DIR))
681
+ from providers.llm.registry import stream_cloud_fallback
682
+ import asyncio as _aio
683
+
684
+ collected: list[str] = []
685
+ result = await _aio.wait_for(
686
+ stream_cloud_fallback(text, history=[], on_token=collected.append),
687
+ timeout=timeout,
688
+ )
689
+ if result.get("success") and collected:
690
+ return "".join(collected).strip()
691
+ if result.get("response"):
692
+ return result["response"].strip()
693
+ return "❌ LLM 返回空响应,请检查 API Key 配置。"
694
+ except ImportError:
695
+ pass # fall through to subprocess
696
+ except Exception as _exc:
697
+ logger.warning("_query_aria_direct failed: %s", _exc)
698
+ return await _query_aria_llm(text, timeout=timeout)
699
+
700
+
701
+ async def _query_aria_llm(text: str, timeout: int = 120) -> str:
702
+ """
703
+ Run a natural language query through aria-code's LLM via CLI -p mode.
704
+ Returns plain text output (ANSI stripped, tool noise filtered).
705
+ Use _query_aria_direct() for conversational queries — this is for
706
+ slash commands (/brief, /team, etc.) that need the full CLI context.
707
+ """
708
+ aria_cli = _ARIA_CODE_DIR / "aria_cli.py"
709
+ if not aria_cli.exists():
710
+ return "❌ aria_cli.py 未找到,请检查 ARIA_CODE_DIR 配置。"
711
+ try:
712
+ # ARIA_BOT_MODE=1: auto-approves tools + suppresses visual diffs in aria_cli
713
+ bot_env = {**os.environ, "ARIA_BOT_MODE": "1"}
714
+ proc = await asyncio.create_subprocess_exec(
715
+ sys.executable, str(aria_cli), "-p", text,
716
+ stdin=asyncio.subprocess.DEVNULL, # no interactive prompts
717
+ stdout=asyncio.subprocess.PIPE,
718
+ stderr=asyncio.subprocess.PIPE,
719
+ cwd=str(_ARIA_CODE_DIR),
720
+ env=bot_env,
721
+ )
722
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
723
+ raw = stdout.decode("utf-8", errors="replace")
724
+ # Strip ANSI codes, filter noise lines, collapse blank runs
725
+ clean_lines = []
726
+ blank_run = 0
727
+ for line in _ANSI_RE.sub("", raw).splitlines():
728
+ if _BOT_NOISE_RE.match(line):
729
+ continue
730
+ line = _INLINE_TIMING_RE.sub("", line)
731
+ line = _INLINE_RICH_TAG_RE.sub("", line)
732
+ if not line.strip():
733
+ blank_run += 1
734
+ if blank_run > 1: # collapse consecutive blank lines to one
735
+ continue
736
+ else:
737
+ blank_run = 0
738
+ clean_lines.append(line)
739
+ clean = "\n".join(clean_lines).strip()
740
+ return clean[:3000] if clean else (stderr.decode()[:500] or "(无输出)")
741
+ except asyncio.TimeoutError:
742
+ try: proc.kill()
743
+ except Exception: pass
744
+ return f"⏱️ 查询超时(>{timeout}s),请简化问题后重试。"
745
+ except Exception as exc:
746
+ return f"❌ aria LLM 调用失败: {exc}"
747
+
748
+
749
+ async def _download_feishu_resource(message_id: str, resource_key: str,
750
+ rtype: str = "file") -> Optional[bytes]:
751
+ """Download image / audio / file from Feishu message."""
752
+ token = await _get_access_token()
753
+ if not token:
754
+ return None
755
+ try:
756
+ import httpx
757
+ async with httpx.AsyncClient(timeout=60) as client:
758
+ resp = await client.get(
759
+ f"{_FEISHU_API}/im/v1/messages/{message_id}/resources/{resource_key}",
760
+ params={"type": rtype},
761
+ headers={"Authorization": f"Bearer {token}"},
762
+ )
763
+ if resp.status_code == 200:
764
+ return resp.content
765
+ except Exception as exc:
766
+ logger.warning("_download_feishu_resource failed: %s", exc)
767
+ return None
768
+
769
+
770
+ async def _transcribe_voice(audio_bytes: bytes) -> str:
771
+ """Speech-to-text: tries OpenAI Whisper API, then local faster-whisper."""
772
+ api_key = os.environ.get("OPENAI_API_KEY", "")
773
+ if api_key:
774
+ try:
775
+ import httpx
776
+ with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as f:
777
+ f.write(audio_bytes)
778
+ tmp = f.name
779
+ with open(tmp, "rb") as audio_file:
780
+ async with httpx.AsyncClient(timeout=60) as client:
781
+ resp = await client.post(
782
+ "https://api.openai.com/v1/audio/transcriptions",
783
+ headers={"Authorization": f"Bearer {api_key}"},
784
+ data={"model": "whisper-1"},
785
+ files={"file": ("voice.ogg", audio_file, "audio/ogg")},
786
+ )
787
+ data = resp.json()
788
+ return data.get("text", "(识别结果为空)")
789
+ except Exception as exc:
790
+ logger.warning("OpenAI Whisper failed: %s", exc)
791
+
792
+ # Fallback: local faster-whisper / openai-whisper
793
+ try:
794
+ from faster_whisper import WhisperModel
795
+ model = WhisperModel("base", device="cpu", compute_type="int8")
796
+ with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as f:
797
+ f.write(audio_bytes)
798
+ tmp = f.name
799
+ segments, _ = model.transcribe(tmp, language="zh")
800
+ return " ".join(seg.text for seg in segments).strip() or "(识别结果为空)"
801
+ except ImportError:
802
+ pass
803
+
804
+ return "❌ 语音转文字需要配置 OPENAI_API_KEY,或安装 faster-whisper (`pip install faster-whisper`)"
805
+
806
+
807
+ async def _analyze_image(image_bytes: bytes, caption: str = "") -> str:
808
+ """Analyze image via Claude vision (ANTHROPIC_API_KEY) or GPT-4V (OPENAI_API_KEY)."""
809
+ b64 = base64.b64encode(image_bytes).decode()
810
+ prompt = caption or "请详细分析这张图片的内容。如果是图表、K线图或截图,请做专业解读并给出结论。"
811
+
812
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
813
+ if anthropic_key:
814
+ try:
815
+ import httpx
816
+ async with httpx.AsyncClient(timeout=60) as client:
817
+ resp = await client.post(
818
+ "https://api.anthropic.com/v1/messages",
819
+ headers={
820
+ "x-api-key": anthropic_key,
821
+ "anthropic-version": "2023-06-01",
822
+ "content-type": "application/json",
823
+ },
824
+ json={
825
+ "model": "claude-sonnet-4-6",
826
+ "max_tokens": 1500,
827
+ "messages": [{
828
+ "role": "user",
829
+ "content": [
830
+ {"type": "image", "source": {
831
+ "type": "base64", "media_type": "image/jpeg", "data": b64}},
832
+ {"type": "text", "text": prompt},
833
+ ],
834
+ }],
835
+ },
836
+ )
837
+ data = resp.json()
838
+ return data["content"][0]["text"]
839
+ except Exception as exc:
840
+ logger.warning("Claude vision failed: %s", exc)
841
+
842
+ openai_key = os.environ.get("OPENAI_API_KEY", "")
843
+ if openai_key:
844
+ try:
845
+ import httpx
846
+ async with httpx.AsyncClient(timeout=60) as client:
847
+ resp = await client.post(
848
+ "https://api.openai.com/v1/chat/completions",
849
+ headers={"Authorization": f"Bearer {openai_key}"},
850
+ json={
851
+ "model": "gpt-4o",
852
+ "max_tokens": 1500,
853
+ "messages": [{
854
+ "role": "user",
855
+ "content": [
856
+ {"type": "image_url", "image_url": {
857
+ "url": f"data:image/jpeg;base64,{b64}"}},
858
+ {"type": "text", "text": prompt},
859
+ ],
860
+ }],
861
+ },
862
+ )
863
+ data = resp.json()
864
+ return data["choices"][0]["message"]["content"]
865
+ except Exception as exc:
866
+ logger.warning("GPT-4V failed: %s", exc)
867
+
868
+ return "❌ 图片分析需要配置 ANTHROPIC_API_KEY 或 OPENAI_API_KEY"
869
+
870
+
871
+ async def _analyze_file(file_bytes: bytes, filename: str) -> str:
872
+ """Extract text from a file and query Aria LLM for analysis."""
873
+ ext = Path(filename).suffix.lower()
874
+ text_content = ""
875
+
876
+ if ext == ".pdf":
877
+ try:
878
+ import pdfplumber
879
+ import io
880
+ with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
881
+ pages = [p.extract_text() or "" for p in pdf.pages[:20]]
882
+ text_content = "\n".join(pages)[:8000]
883
+ except ImportError:
884
+ try:
885
+ import pypdf
886
+ import io
887
+ reader = pypdf.PdfReader(io.BytesIO(file_bytes))
888
+ text_content = "\n".join(
889
+ p.extract_text() or "" for p in reader.pages[:20]
890
+ )[:8000]
891
+ except Exception as exc:
892
+ return f"❌ PDF 解析失败: {exc}"
893
+
894
+ elif ext in (".xlsx", ".xls"):
895
+ try:
896
+ import openpyxl
897
+ import io
898
+ wb = openpyxl.load_workbook(io.BytesIO(file_bytes), read_only=True)
899
+ rows = []
900
+ for sheet in wb.sheetnames[:3]:
901
+ ws = wb[sheet]
902
+ for row in list(ws.iter_rows(values_only=True))[:50]:
903
+ rows.append("\t".join(str(c) for c in row if c is not None))
904
+ text_content = f"[Excel: {filename}]\n" + "\n".join(rows)[:6000]
905
+ except Exception as exc:
906
+ return f"❌ Excel 解析失败: {exc}"
907
+
908
+ elif ext in (".docx",):
909
+ try:
910
+ import docx
911
+ import io
912
+ doc = docx.Document(io.BytesIO(file_bytes))
913
+ text_content = "\n".join(p.text for p in doc.paragraphs)[:8000]
914
+ except Exception as exc:
915
+ return f"❌ Word 解析失败: {exc}"
916
+
917
+ elif ext in (".py", ".js", ".ts", ".go", ".java", ".cpp", ".c", ".rs",
918
+ ".txt", ".md", ".json", ".yaml", ".yml", ".csv", ".toml"):
919
+ try:
920
+ text_content = file_bytes.decode("utf-8", errors="replace")[:8000]
921
+ except Exception:
922
+ return "❌ 文件编码无法识别"
923
+
924
+ else:
925
+ try:
926
+ text_content = file_bytes.decode("utf-8", errors="replace")[:4000]
927
+ except Exception:
928
+ return f"❌ 不支持的文件类型: {ext}"
929
+
930
+ if not text_content.strip():
931
+ return "❌ 文件内容为空或无法提取文本"
932
+
933
+ query = (
934
+ f"请分析以下文件({filename})的内容,给出主要信息、关键数据和洞察总结:\n\n"
935
+ f"```\n{text_content[:6000]}\n```"
936
+ )
937
+ return await _query_aria_llm(query, timeout=120)
938
+
939
+
940
+ # ── Event verifier ────────────────────────────────────────────────────────────
941
+
942
+ def verify_feishu_signature(timestamp: str, nonce: str, body_bytes: bytes,
943
+ encrypt_key: str) -> bool:
944
+ """Verify Feishu event signature (optional but recommended in production)."""
945
+ if not encrypt_key:
946
+ return True
947
+ s = (timestamp + nonce + encrypt_key).encode() + body_bytes
948
+ return hmac.compare_digest(
949
+ hashlib.sha256(s).hexdigest(),
950
+ "" # caller should pass the X-Lark-Signature header value
951
+ )
952
+
953
+
954
+ # ── Main event dispatcher (called by feishu_routes.py or standalone) ──────────
955
+
956
+ async def dispatch_event(raw: Dict[str, Any]) -> Dict[str, Any]:
957
+ """
958
+ Handle one Feishu event payload.
959
+ Supports: text / audio / image / file / post (富文本)
960
+ Returns a dict to be sent as JSON response (HTTP 200 required by Feishu).
961
+ """
962
+ # 1. URL verification challenge
963
+ if "challenge" in raw:
964
+ return {"challenge": raw["challenge"]}
965
+
966
+ header = raw.get("header", {})
967
+ event = raw.get("event", {})
968
+ event_type = header.get("event_type") or raw.get("type", "")
969
+
970
+ if event_type not in ("im.message.receive_v1", "message"):
971
+ return {"code": 0}
972
+
973
+ msg = event.get("message") or {}
974
+ msg_id = msg.get("message_id", "")
975
+ msg_type = msg.get("message_type", "text")
976
+ sender = event.get("sender", {}).get("sender_id", {})
977
+ user_id = sender.get("user_id", "") or sender.get("open_id", "")
978
+ chat_id = msg.get("chat_id", "")
979
+
980
+ logger.info("dispatch_event: type=%s msg_id=%s msg_type=%s user=%s chat=%s",
981
+ event_type, msg_id, msg_type, user_id, chat_id)
982
+
983
+ if not msg_id:
984
+ logger.error("dispatch_event: msg_id is EMPTY — event structure may differ: %s",
985
+ json.dumps(raw, ensure_ascii=False)[:600])
986
+ return {"code": 0}
987
+
988
+ if not _is_allowed_user(user_id):
989
+ logger.warning("Blocked user %s (not in FEISHU_ALLOWED_USER_IDS)", user_id)
990
+ return {"code": 0}
991
+
992
+ content_raw = msg.get("content", "{}")
993
+ try:
994
+ content = json.loads(content_raw)
995
+ except Exception:
996
+ content = {}
997
+
998
+ # ── Text message ──────────────────────────────────────────────────────────
999
+ if msg_type == "text":
1000
+ text = content.get("text", "").strip()
1001
+ # Strip @bot mention (飞书群里 @ 机器人会带前缀)
1002
+ if text.startswith("@"):
1003
+ text = " ".join(text.split()[1:]).strip()
1004
+ if not text:
1005
+ return {"code": 0}
1006
+
1007
+ if text.startswith("/"):
1008
+ logger.info("Feishu /cmd from %s: %s", user_id, text[:80])
1009
+ asyncio.create_task(_handle_command(text, msg_id, user_id, chat_id))
1010
+ else:
1011
+ # Free-form natural language → Aria LLM
1012
+ logger.info("Feishu NL query from %s: %s", user_id, text[:80])
1013
+ asyncio.create_task(_handle_nl_query(text, msg_id, chat_id))
1014
+
1015
+ # ── Voice / Audio ─────────────────────────────────────────────────────────
1016
+ elif msg_type == "audio":
1017
+ file_key = content.get("file_key", "")
1018
+ logger.info("Feishu audio from %s, key=%s", user_id, file_key)
1019
+ asyncio.create_task(_handle_audio(file_key, msg_id))
1020
+
1021
+ # ── Image ────────────────────────────────────────────────────────────────
1022
+ elif msg_type == "image":
1023
+ image_key = content.get("image_key", "")
1024
+ logger.info("Feishu image from %s, key=%s", user_id, image_key)
1025
+ asyncio.create_task(_handle_image(image_key, msg_id))
1026
+
1027
+ # ── File attachment ───────────────────────────────────────────────────────
1028
+ elif msg_type == "file":
1029
+ file_key = content.get("file_key", "")
1030
+ file_name = content.get("file_name", "attachment")
1031
+ logger.info("Feishu file from %s: %s", user_id, file_name)
1032
+ asyncio.create_task(_handle_file(file_key, file_name, msg_id))
1033
+
1034
+ # ── Rich text (post) — extract plain text ─────────────────────────────────
1035
+ elif msg_type == "post":
1036
+ try:
1037
+ # post content: {"zh_cn": {"title":"...","content":[[{"tag":"text","text":"..."},...]]}}
1038
+ lang_content = content.get("zh_cn") or content.get("en_us") or {}
1039
+ title = lang_content.get("title", "")
1040
+ paras = lang_content.get("content", [])
1041
+ texts = []
1042
+ for para in paras:
1043
+ for seg in para:
1044
+ if seg.get("tag") == "text":
1045
+ texts.append(seg.get("text", ""))
1046
+ plain = title + ("\n" if title else "") + " ".join(texts)
1047
+ if plain.strip():
1048
+ asyncio.create_task(_handle_nl_query(plain.strip(), msg_id))
1049
+ except Exception as exc:
1050
+ logger.warning("post parse error: %s", exc)
1051
+
1052
+ return {"code": 0}
1053
+
1054
+
1055
+ # ── Message type handlers (run as background tasks) ───────────────────────────
1056
+
1057
+ _MARKET_BRIEF_TRIGGERS = frozenset({
1058
+ "行情查询", "市场行情", "股市行情", "今日行情", "行情", "大盘", "大盘行情",
1059
+ "市场概况", "今日市场", "A股行情", "港股行情", "美股行情", "晨报",
1060
+ "market", "market overview", "stock market", "markets today",
1061
+ })
1062
+
1063
+ _FOOTBALL_PREDICT_TRIGGERS = (
1064
+ "预测", "谁赢", "谁会赢", "比分预测", "胜率", "分析比赛",
1065
+ "predict", "who wins", "match preview",
1066
+ )
1067
+
1068
+ # ── 常用 A股/港股 中文名 → 股票代码(用于 NL 解析)──────────────────────────
1069
+ _CN_COMPANY_TICKER: dict[str, str] = {
1070
+ # 银行
1071
+ "工商银行": "601398", "工行": "601398",
1072
+ "建设银行": "601939", "建行": "601939",
1073
+ "农业银行": "601288", "农行": "601288",
1074
+ "中国银行": "601988", "中行": "601988",
1075
+ "招商银行": "600036", "招行": "600036",
1076
+ "平安银行": "000001",
1077
+ "兴业银行": "601166",
1078
+ "浦发银行": "600000",
1079
+ "光大银行": "601818",
1080
+ # 券商/保险
1081
+ "中信证券": "600030",
1082
+ "海通证券": "600837",
1083
+ "中国平安": "601318", "平安": "601318",
1084
+ "中国人寿": "601628",
1085
+ # 能源/化工
1086
+ "中国石油": "601857", "中石油": "601857",
1087
+ "中国石化": "600028", "中石化": "600028",
1088
+ "中国神华": "601088",
1089
+ # 有色金属
1090
+ "江西铜业": "600362",
1091
+ "紫金矿业": "601899",
1092
+ "中金黄金": "600489",
1093
+ "山东黄金": "600547",
1094
+ "洛阳钼业": "603993",
1095
+ "铜陵有色": "000630",
1096
+ "中国铝业": "601600",
1097
+ "南方铜业": "SCCO",
1098
+ "自由港": "FCX", "自由港麦克莫兰": "FCX",
1099
+ # 消费
1100
+ "贵州茅台": "600519", "茅台": "600519",
1101
+ "五粮液": "000858",
1102
+ "洋河股份": "002304",
1103
+ "海天味业": "603288",
1104
+ "伊利股份": "600887",
1105
+ "格力电器": "000651", "格力": "000651",
1106
+ "美的集团": "000333", "美的": "000333",
1107
+ "海尔智家": "600690",
1108
+ # 科技/互联网
1109
+ "腾讯": "0700", "腾讯控股": "0700",
1110
+ "阿里巴巴": "BABA", "阿里": "BABA",
1111
+ "京东": "JD",
1112
+ "百度": "BIDU",
1113
+ "比亚迪": "002594",
1114
+ "宁德时代": "300750", "宁德": "300750",
1115
+ "中芯国际": "688981",
1116
+ "海康威视": "002415",
1117
+ # 地产
1118
+ "万科": "000002", "万科A": "000002",
1119
+ "碧桂园": "2007",
1120
+ "恒大": "3333",
1121
+ # 医药
1122
+ "恒瑞医药": "600276",
1123
+ "迈瑞医疗": "300760",
1124
+ # 全球大宗商品矿企
1125
+ "必和必拓": "BHP", "必拓": "BHP",
1126
+ "力拓": "RIO", "力拓集团": "RIO",
1127
+ "淡水河谷": "VALE",
1128
+ "嘉能可": "GLEN.L",
1129
+ }
1130
+
1131
+ # ── 大宗商品关键词 → (商品期货代码, 相关上市公司) ─────────────────────────────
1132
+ _COMMODITY_MAP: dict[str, tuple[str, list[str], str]] = {
1133
+ # keyword: (yfinance_symbol, [related_tickers], display_name)
1134
+ "铜": ("HG=F", ["FCX", "SCCO", "600362", "601899", "000630"], "铜 COMEX"),
1135
+ "黄金": ("GC=F", ["GLD", "NEM", "GOLD", "600547", "600489"], "黄金 COMEX"),
1136
+ "gold": ("GC=F", ["GLD", "NEM", "GOLD", "600547"], "Gold COMEX"),
1137
+ "白银": ("SI=F", ["SLV", "PAAS", "AG"], "白银 COMEX"),
1138
+ "原油": ("CL=F", ["XOM", "CVX", "601857", "600028"], "原油 WTI"),
1139
+ "oil": ("CL=F", ["XOM", "CVX", "BP", "601857"], "Crude Oil WTI"),
1140
+ "天然气":("NG=F", ["UNG", "LNG", "CQP"], "天然气 NYMEX"),
1141
+ "铁矿石":("TIO=F", ["BHP", "RIO", "VALE", "601088"], "铁矿石"),
1142
+ "铝": ("ALI=F", ["AA", "CENX", "601600"], "铝 LME"),
1143
+ "锂": ("", ["ALB", "SQM", "LTHM", "300750", "002594"], "锂矿/电池"),
1144
+ "锂矿": ("", ["ALB", "SQM", "LTHM", "300750"], "锂矿"),
1145
+ "小麦": ("ZW=F", ["ADM", "BG", "INGR"], "小麦 CBOT"),
1146
+ "大豆": ("ZS=F", ["ADM", "BG", "DE"], "大豆 CBOT"),
1147
+ }
1148
+
1149
+
1150
+ async def _fetch_market_snapshot() -> str:
1151
+ """
1152
+ Build a real-time market snapshot for A股 + 港股 + US indices directly via
1153
+ yfinance — bypasses the LLM subprocess so we always get actual data.
1154
+ """
1155
+ import yfinance as yf
1156
+ import asyncio
1157
+
1158
+ _INDICES = [
1159
+ ("^SSEC", "上证指数"),
1160
+ ("^HSI", "恒生指数"),
1161
+ ("^GSPC", "标普500"),
1162
+ ("^IXIC", "纳斯达克"),
1163
+ ("^N225", "日经225"),
1164
+ ("GC=F", "黄金"),
1165
+ ("CL=F", "原油"),
1166
+ ]
1167
+
1168
+ def _fetch_one(sym: str):
1169
+ try:
1170
+ ti = yf.Ticker(sym)
1171
+ fi = ti.fast_info
1172
+ p = getattr(fi, "last_price", None)
1173
+ pc = getattr(fi, "previous_close", None)
1174
+ if p and pc and pc > 0:
1175
+ pct = (p - pc) / pc * 100
1176
+ arrow = "▲" if pct >= 0 else "▼"
1177
+ return f"{arrow} {p:,.2f} ({pct:+.2f}%)"
1178
+ elif p:
1179
+ return f"¥{p:,.2f}"
1180
+ except Exception:
1181
+ pass
1182
+ return "—"
1183
+
1184
+ loop = asyncio.get_event_loop()
1185
+ lines = ["**主要市场行情**\n"]
1186
+ for sym, label in _INDICES:
1187
+ val = await loop.run_in_executor(None, _fetch_one, sym)
1188
+ lines.append(f"**{label}** {val}")
1189
+
1190
+ from datetime import datetime
1191
+ ts = datetime.now().strftime("%H:%M")
1192
+ lines.append(f"\n_更新时间: {ts}_")
1193
+ return "\n".join(lines)
1194
+
1195
+
1196
+ async def _fetch_commodity_with_stocks(keyword: str) -> str:
1197
+ """
1198
+ Fetch commodity futures price + related stock prices for a given commodity keyword.
1199
+ Returns a formatted multi-line string for the Feishu card body.
1200
+ """
1201
+ import yfinance as yf
1202
+ import asyncio
1203
+
1204
+ entry = _COMMODITY_MAP.get(keyword)
1205
+ if not entry:
1206
+ return ""
1207
+ futures_sym, related_tickers, display_name = entry
1208
+
1209
+ def _price_line(sym: str) -> str:
1210
+ try:
1211
+ ti = yf.Ticker(sym)
1212
+ fi = ti.fast_info
1213
+ p = getattr(fi, "last_price", None)
1214
+ pc = getattr(fi, "previous_close", None)
1215
+ name = sym
1216
+ # Try to get a short name
1217
+ try:
1218
+ info = ti.info
1219
+ name = info.get("shortName", sym)[:12]
1220
+ except Exception:
1221
+ pass
1222
+ if p and pc and pc > 0:
1223
+ pct = (p - pc) / pc * 100
1224
+ arrow = "▲" if pct >= 0 else "▼"
1225
+ currency = "¥" if sym.isdigit() else "$"
1226
+ return f"{arrow} **{name}** ({sym}) {currency}{p:,.2f} ({pct:+.2f}%)"
1227
+ elif p:
1228
+ currency = "¥" if sym.isdigit() else "$"
1229
+ return f"**{name}** ({sym}) {currency}{p:,.2f}"
1230
+ except Exception:
1231
+ pass
1232
+ return f"**{sym}** —"
1233
+
1234
+ loop = asyncio.get_event_loop()
1235
+ sections = []
1236
+
1237
+ # Commodity futures
1238
+ if futures_sym:
1239
+ fut_line = await loop.run_in_executor(None, _price_line, futures_sym)
1240
+ sections.append(f"**{display_name} 期货**\n{fut_line}")
1241
+
1242
+ # Related stocks
1243
+ stock_lines = []
1244
+ for sym in related_tickers[:5]:
1245
+ # A-share: append exchange suffix for yfinance
1246
+ yfn = sym
1247
+ if sym.isdigit() and len(sym) == 6:
1248
+ yfn = sym + (".SS" if sym.startswith(("6", "5")) else ".SZ")
1249
+ line = await loop.run_in_executor(None, _price_line, yfn)
1250
+ stock_lines.append(line)
1251
+
1252
+ if stock_lines:
1253
+ sections.append("**相关上市公司**\n" + "\n".join(stock_lines))
1254
+
1255
+ from datetime import datetime
1256
+ ts = datetime.now().strftime("%H:%M")
1257
+ return "\n\n".join(sections) + f"\n\n_数据时间: {ts}_"
1258
+
1259
+
1260
+ def _resolve_cn_company(text: str) -> str:
1261
+ """
1262
+ Replace Chinese company names in text with their ticker symbols.
1263
+ E.g. "江西铜业的走势" → "600362的走势"
1264
+ Returns modified text; if nothing matched, returns original.
1265
+ """
1266
+ result = text
1267
+ for cn_name, ticker in _CN_COMPANY_TICKER.items():
1268
+ if cn_name in result:
1269
+ result = result.replace(cn_name, ticker)
1270
+ return result
1271
+
1272
+
1273
+ async def _handle_nl_query(text: str, message_id: str, chat_id: str = "") -> None:
1274
+ """Route free-form natural language to Aria LLM and reply."""
1275
+ import re as _re_nl
1276
+ _low = text.strip().lower()
1277
+ _orig = text.strip()
1278
+
1279
+ # Fast-path: generic market brief request — call yfinance directly (bypasses LLM)
1280
+ if _low in _MARKET_BRIEF_TRIGGERS or (
1281
+ any(k in _low for k in ("行情查询", "市场行情", "大盘今日", "今日大盘")) and
1282
+ not any(c.isalpha() and c.upper() == c for c in text) # no uppercase ticker
1283
+ ):
1284
+ await reply_or_send(message_id, chat_id, "📊 获取市场概况…", "正在抓取主要指数数据…", "blue")
1285
+ try:
1286
+ snapshot = await _fetch_market_snapshot()
1287
+ await reply_or_send(message_id, chat_id, "📊 市场行情", snapshot, "turquoise",
1288
+ footer="Aria Code · yfinance 实时数据")
1289
+ except Exception as _e:
1290
+ await reply_or_send(message_id, chat_id, "❌ 行情获取失败", str(_e)[:200], "red")
1291
+ return
1292
+
1293
+ # Fast-path: commodity + related stocks (e.g. "铜的相关公司", "黄金走势")
1294
+ _commodity_kw = None
1295
+ for _ck in _COMMODITY_MAP:
1296
+ if _ck in _low:
1297
+ _commodity_kw = _ck
1298
+ break
1299
+ _needs_stocks = any(k in _low for k in ("相关公司", "相关股票", "产业链", "概念股", "走势", "行情", "估值"))
1300
+ if _commodity_kw and (_needs_stocks or len(_orig) <= 6):
1301
+ await reply_or_send(message_id, chat_id, f"🔍 查询{_commodity_kw}行情…",
1302
+ "正在获取期货及相关股票数据…", "blue")
1303
+ body = await _fetch_commodity_with_stocks(_commodity_kw)
1304
+ if body:
1305
+ await reply_or_send(message_id, chat_id, f"📦 {_commodity_kw} 市场概况",
1306
+ body, "turquoise", footer="Aria Code · 大宗商品数据")
1307
+ return
1308
+
1309
+ # Chinese company name → ticker resolution before sending to LLM
1310
+ resolved = _resolve_cn_company(_orig)
1311
+ if resolved != _orig:
1312
+ # Found at least one CN name; also annotate so LLM has context
1313
+ text = resolved + f" (原文: {_orig})"
1314
+
1315
+ # Fast-path: football match prediction — extract team names and call Poisson model directly
1316
+ _has_predict_kw = any(k in _low for k in _FOOTBALL_PREDICT_TRIGGERS)
1317
+ _has_football_kw = any(k in _low for k in ("足球", "世界杯", "欧冠", "英超", "比赛", "football", "soccer", "match", "world cup"))
1318
+ if _has_predict_kw or _has_football_kw:
1319
+ # Strip one or more leading context words (handles "预测今天加拿大跟波黑")
1320
+ _stripped = _re_nl.sub(
1321
+ r'^(?:预测|分析|谁赢|谁会赢|今天|明天|比赛|足球|世界杯|结果|比分|\s)+',
1322
+ '', _orig, flags=_re_nl.IGNORECASE
1323
+ )
1324
+ _vs_m = _re_nl.search(
1325
+ r'(.{2,20}?)\s*(?:vs\.?\s*|对阵\s*|对\s+|跟\s*|和\s*|pk\s*|——\s*|—\s*)(.{2,20})',
1326
+ _stripped, _re_nl.IGNORECASE
1327
+ )
1328
+ if _vs_m:
1329
+ home_t = _vs_m.group(1).strip().rstrip("的在")
1330
+ away_t = _vs_m.group(2).strip().rstrip("的在")
1331
+ if home_t and away_t and len(home_t) >= 2 and len(away_t) >= 2:
1332
+ await reply_or_send(message_id, chat_id, "⚽ 预测中…",
1333
+ f"> {home_t} vs {away_t}", "blue")
1334
+ await _handle_football_predict(f"{home_t} vs {away_t}", message_id)
1335
+ return
1336
+
1337
+ await reply_or_send(message_id, chat_id, "🤔 思考中…", f"> {_orig[:120]}", "blue")
1338
+ # Use direct LLM call (no subprocess, no tool execution) for conversational queries
1339
+ result = await _query_aria_direct(text, timeout=120)
1340
+ color = "red" if result.startswith("❌") else "green"
1341
+ await reply_or_send(message_id, chat_id, "💡 Aria 回答", result[:2000], color,
1342
+ footer="Aria Code · AI 分析")
1343
+
1344
+
1345
+ async def _handle_audio(file_key: str, message_id: str) -> None:
1346
+ """Download voice → transcribe → query Aria LLM."""
1347
+ await reply_card(message_id, "🎤 正在转写语音…", "下载中,请稍候…", "blue")
1348
+ audio_bytes = await _download_feishu_resource(message_id, file_key, rtype="file")
1349
+ if not audio_bytes:
1350
+ await reply_card(message_id, "❌ 语音下载失败", "无法获取语音文件", "red")
1351
+ return
1352
+ text = await _transcribe_voice(audio_bytes)
1353
+ if text.startswith("❌"):
1354
+ await reply_card(message_id, "❌ 语音转文字失败", text, "red")
1355
+ return
1356
+ await reply_card(message_id, "🎤 识别结果", f"**语音内容:**\n{text}\n\n---\n正在分析…", "blue")
1357
+ result = await _query_aria_llm(text, timeout=120)
1358
+ await reply_card(message_id, "💡 Aria 回答", result[:2000], "green",
1359
+ footer=f"语音转文字: {text[:60]}…")
1360
+
1361
+
1362
+ async def _handle_image(image_key: str, message_id: str) -> None:
1363
+ """Download image → visual LLM analysis."""
1364
+ await reply_card(message_id, "🖼️ 分析图片中…", "正在下载并识别,请稍候…", "blue")
1365
+ img_bytes = await _download_feishu_resource(message_id, image_key, rtype="image")
1366
+ if not img_bytes:
1367
+ await reply_card(message_id, "❌ 图片下载失败", "无法获取图片", "red")
1368
+ return
1369
+ result = await _analyze_image(img_bytes)
1370
+ await reply_card(message_id, "🖼️ 图片分析", result[:2000], "turquoise",
1371
+ footer="Aria Code · 视觉 AI")
1372
+
1373
+
1374
+ async def _handle_file(file_key: str, filename: str, message_id: str) -> None:
1375
+ """Download file → parse → Aria LLM analysis."""
1376
+ await reply_card(message_id, f"📄 解析文件:{filename}",
1377
+ "正在下载并解析,请稍候…", "blue")
1378
+ file_bytes = await _download_feishu_resource(message_id, file_key, rtype="file")
1379
+ if not file_bytes:
1380
+ await reply_card(message_id, "❌ 文件下载失败", "无法获取文件", "red")
1381
+ return
1382
+ result = await _analyze_file(file_bytes, filename)
1383
+ await reply_card(message_id, f"📄 {filename} 分析完成", result[:2000], "turquoise",
1384
+ footer="Aria Code · 文件智能解析")
1385
+
1386
+
1387
+ # ── Standalone HTTP server (for testing without FastAPI) ──────────────────────
1388
+
1389
+ async def _standalone_server(host: str = "0.0.0.0", port: int = 8888) -> None:
1390
+ """Minimal aiohttp-based server for standalone Feishu event reception."""
1391
+ try:
1392
+ from aiohttp import web
1393
+ except ImportError:
1394
+ logger.error("aiohttp not installed. pip install aiohttp")
1395
+ return
1396
+
1397
+ async def handle(request):
1398
+ try:
1399
+ body = await request.json()
1400
+ except Exception:
1401
+ return web.json_response({"code": 1, "msg": "bad json"}, status=400)
1402
+ result = await dispatch_event(body)
1403
+ return web.json_response(result)
1404
+
1405
+ app = web.Application()
1406
+ app.router.add_post("/feishu/event", handle)
1407
+ app.router.add_post("/api/v1/feishu/event", handle)
1408
+
1409
+ runner = web.AppRunner(app)
1410
+ await runner.setup()
1411
+ site = web.TCPSite(runner, host, port)
1412
+ await site.start()
1413
+ logger.info("Feishu standalone server listening on http://%s:%d/feishu/event", host, port)
1414
+ logger.info("Configure this URL in Feishu Developer Console → Event Subscription")
1415
+ await asyncio.Event().wait()
1416
+
1417
+
1418
+ if __name__ == "__main__":
1419
+ import sys
1420
+ logging.basicConfig(level=logging.INFO,
1421
+ format="%(asctime)s [feishu] %(levelname)s %(message)s")
1422
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 8888
1423
+ asyncio.run(_standalone_server(port=port))