aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1372 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ aria_daemon.py — Aria always-on background daemon.
4
+
5
+ Responsibilities:
6
+ 1. Price alert watchdog — checks SQLite alerts every 30 s
7
+ 2. APScheduler cron jobs — morning brief, market scan, custom schedules
8
+ 3. Telegram bot — bidirectional command channel
9
+ 4. Webhook job executor — runs pending jobs from the local webhook_jobs table
10
+ TradingView endpoint — POST /api/v1/webhook/tradingview (alias
11
+ /api/v1/tradingview/webhook) queues alerts
12
+ 5. APNs push delivery — fires on alert trigger or scheduled job completion
13
+
14
+ Start manually: python3 aria_daemon.py [--debug]
15
+ Install daemon: python3 aria_daemon.py --install (macOS LaunchAgent)
16
+ Uninstall: python3 aria_daemon.py --uninstall
17
+
18
+ Config via env vars (or ~/.aria/.env):
19
+ TELEGRAM_BOT_TOKEN — Telegram bot token from @BotFather
20
+ TELEGRAM_ALLOWED_IDS — comma-separated chat IDs (e.g. "123456,789012")
21
+ APNS_KEY_ID — Apple Developer key ID (10-char string)
22
+ APNS_TEAM_ID — Apple Developer team ID
23
+ APNS_BUNDLE_ID — App bundle ID (default: com.arthera.app)
24
+ APNS_SANDBOX — "true" for sandbox / TestFlight (default: true)
25
+ APNS_AUTH_KEY_P8 — .p8 key content, or place file at ~/.aria/apns.p8
26
+ WEBHOOK_TOKEN — Token for /api/v1/webhook/tradingview, sent in the
27
+ X-Aria-Token, X-TradingView-Secret or
28
+ Authorization: Bearer header (never the query
29
+ string). TRADINGVIEW_WEBHOOK_SECRET takes precedence.
30
+ ARIA_WEBHOOK_SECRET — Passphrase TradingView puts in the alert body
31
+ (TradingView cannot send headers)
32
+ ARIA_WEBHOOK_HOST — Daemon webhook host (default: 127.0.0.1)
33
+ ARIA_WEBHOOK_PORT — Daemon webhook port (default: 8765 — the same
34
+ default as the relay server; set this when both
35
+ run on one machine)
36
+ (the FastAPI backend URL is the `api_url` config key, not an env var —
37
+ set it with `/config set api_url=...`)
38
+ ARIA_CODE_DIR — Path to aria-code directory
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import asyncio
43
+ import hmac
44
+ import logging
45
+ import os
46
+ import signal
47
+ import sqlite3
48
+ import sys
49
+ import time
50
+ from datetime import datetime
51
+ from pathlib import Path
52
+ from typing import Optional
53
+
54
+ # ── Logging ───────────────────────────────────────────────────────────────────
55
+
56
+ # Importing this module creates the log file and daemon.db and loads .env into
57
+ # os.environ (aria_feishu_bot relies on that), so the directory can be moved:
58
+ # the test suite points it at a scratch directory instead of the user's own.
59
+ _ARIA_DIR = Path(os.environ.get("ARIA_DAEMON_DIR") or (Path.home() / ".aria")).expanduser()
60
+ _LOG_DIR = _ARIA_DIR / "logs"
61
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
62
+
63
+ logging.basicConfig(
64
+ level=logging.DEBUG if "--debug" in sys.argv else logging.INFO,
65
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
66
+ handlers=[
67
+ logging.StreamHandler(sys.stdout),
68
+ logging.FileHandler(_LOG_DIR / "daemon.log", encoding="utf-8"),
69
+ ],
70
+ )
71
+ logger = logging.getLogger("aria.daemon")
72
+
73
+ # ── Paths & config ────────────────────────────────────────────────────────────
74
+
75
+ _DB_PATH = _ARIA_DIR / "daemon.db"
76
+ _PID_FILE = _ARIA_DIR / "daemon.pid"
77
+ _ENV_FILE = _ARIA_DIR / ".env"
78
+
79
+ _ARIA_CODE_DIR = Path(os.environ.get("ARIA_CODE_DIR", Path(__file__).parent))
80
+ if str(_ARIA_CODE_DIR) not in sys.path:
81
+ sys.path.insert(0, str(_ARIA_CODE_DIR))
82
+
83
+
84
+ def _load_env() -> None:
85
+ """Load ~/.aria/.env into os.environ if it exists."""
86
+ if _ENV_FILE.exists():
87
+ for line in _ENV_FILE.read_text().splitlines():
88
+ line = line.strip()
89
+ if line and not line.startswith("#") and "=" in line:
90
+ k, _, v = line.partition("=")
91
+ os.environ.setdefault(k.strip(), v.strip())
92
+
93
+
94
+ _load_env()
95
+
96
+
97
+ # ── DB bootstrap ───────────────────────────────────────────────────────────────
98
+
99
+ def _init_db() -> None:
100
+ _ARIA_DIR.mkdir(parents=True, exist_ok=True)
101
+ with sqlite3.connect(_DB_PATH) as conn:
102
+ conn.executescript("""
103
+ CREATE TABLE IF NOT EXISTS device_tokens (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ token TEXT UNIQUE NOT NULL,
106
+ platform TEXT DEFAULT 'ios',
107
+ user_id TEXT,
108
+ bundle_id TEXT,
109
+ created_at TEXT DEFAULT (datetime('now')),
110
+ updated_at TEXT DEFAULT (datetime('now'))
111
+ );
112
+ CREATE TABLE IF NOT EXISTS alerts (
113
+ id TEXT PRIMARY KEY,
114
+ symbol TEXT NOT NULL,
115
+ condition TEXT NOT NULL,
116
+ value REAL NOT NULL,
117
+ message TEXT,
118
+ notify_push INTEGER DEFAULT 1,
119
+ notify_telegram INTEGER DEFAULT 1,
120
+ notify_email TEXT,
121
+ once INTEGER DEFAULT 1,
122
+ active INTEGER DEFAULT 1,
123
+ trigger_count INTEGER DEFAULT 0,
124
+ created_at TEXT DEFAULT (datetime('now')),
125
+ triggered_at TEXT
126
+ );
127
+ CREATE TABLE IF NOT EXISTS schedules (
128
+ id TEXT PRIMARY KEY,
129
+ name TEXT,
130
+ cron_expr TEXT NOT NULL,
131
+ command TEXT NOT NULL,
132
+ symbols TEXT DEFAULT '[]',
133
+ channels TEXT DEFAULT '["ios","telegram"]',
134
+ language TEXT DEFAULT 'zh',
135
+ user_id TEXT,
136
+ enabled INTEGER DEFAULT 1,
137
+ created_at TEXT DEFAULT (datetime('now')),
138
+ last_run TEXT,
139
+ next_run TEXT
140
+ );
141
+ CREATE TABLE IF NOT EXISTS webhook_jobs (
142
+ id TEXT PRIMARY KEY,
143
+ command TEXT NOT NULL,
144
+ payload TEXT DEFAULT '{}',
145
+ source TEXT DEFAULT 'external',
146
+ status TEXT DEFAULT 'pending',
147
+ result TEXT,
148
+ created_at TEXT DEFAULT (datetime('now')),
149
+ started_at TEXT,
150
+ done_at TEXT
151
+ );
152
+ CREATE TABLE IF NOT EXISTS push_log (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ alert_id TEXT,
155
+ device_token TEXT,
156
+ title TEXT,
157
+ body TEXT,
158
+ status TEXT DEFAULT 'pending',
159
+ created_at TEXT DEFAULT (datetime('now'))
160
+ );
161
+ """)
162
+ conn.commit()
163
+
164
+
165
+ _init_db()
166
+
167
+
168
+ # ── Price fetch (lightweight, no VPN proxy) ────────────────────────────────────
169
+
170
+ async def _fetch_price(symbol: str) -> tuple[Optional[float], Optional[float]]:
171
+ """Fast price lookup. Returns (current_price, prev_close) for alert evaluation."""
172
+ import math
173
+ try:
174
+ import yfinance as yf
175
+ if symbol.isdigit() and len(symbol) == 6:
176
+ yfn = symbol + (".SS" if symbol.startswith(("6", "5")) else ".SZ")
177
+ else:
178
+ yfn = symbol
179
+ ticker = yf.Ticker(yfn)
180
+ info = ticker.fast_info
181
+ raw_price = getattr(info, "last_price", None) or getattr(info, "previous_close", None)
182
+ raw_prev = getattr(info, "previous_close", None)
183
+ # yfinance can return float('nan') — treat as None
184
+ price = float(raw_price) if raw_price is not None and not math.isnan(float(raw_price)) else None
185
+ prev_close = float(raw_prev) if raw_prev is not None and not math.isnan(float(raw_prev)) else None
186
+ return (price, prev_close)
187
+ except Exception as exc:
188
+ logger.debug("_fetch_price %s: %s", symbol, exc)
189
+ return (None, None)
190
+
191
+
192
+ # ── Push notifications ────────────────────────────────────────────────────────
193
+
194
+ async def _push_alert(title: str, body: str, extra: Optional[dict] = None, alert_id: Optional[str] = None) -> None:
195
+ """Try to push via APNs; log any failure but never raise."""
196
+ try:
197
+ sys.path.insert(0, str(Path(__file__).parent.parent / "Arthera" / "apps" / "api" / "src"))
198
+ from services.apns_service import push_to_all
199
+ sent = await push_to_all(title, body, extra, alert_id)
200
+ logger.info("APNs push: %d device(s) notified for alert_id=%s", sent, alert_id)
201
+ except ImportError:
202
+ logger.debug("apns_service not reachable — skipping push")
203
+
204
+
205
+ # ── Telegram push (one-shot, no polling) ─────────────────────────────────────
206
+
207
+ async def _telegram_push(text: str) -> None:
208
+ token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
209
+ allowed_raw = os.environ.get("TELEGRAM_ALLOWED_IDS", "")
210
+ if not token or not allowed_raw:
211
+ return
212
+ chat_ids = [int(x.strip()) for x in allowed_raw.split(",") if x.strip().isdigit()]
213
+ try:
214
+ import httpx
215
+ async with httpx.AsyncClient(timeout=10) as client:
216
+ for cid in chat_ids:
217
+ await client.post(
218
+ f"https://api.telegram.org/bot{token}/sendMessage",
219
+ json={"chat_id": cid, "text": text, "parse_mode": "Markdown"},
220
+ )
221
+ except Exception as exc:
222
+ logger.warning("_telegram_push failed: %s", exc)
223
+
224
+
225
+ # ── Feishu push (webhook card) ────────────────────────────────────────────────
226
+
227
+ async def _feishu_push(title: str, body: str) -> None:
228
+ """POST an interactive card to a Feishu group webhook (FEISHU_WEBHOOK_URL)."""
229
+ url = os.environ.get("FEISHU_WEBHOOK_URL", "")
230
+ if not url:
231
+ return
232
+ color = "red" if any(w in title for w in ("预警", "Alert", "熔断", "ERROR")) else \
233
+ "green" if any(w in title for w in ("晨报", "完成", "Brief")) else "blue"
234
+ card = {
235
+ "msg_type": "interactive",
236
+ "card": {
237
+ "header": {
238
+ "title": {"tag": "plain_text", "content": title},
239
+ "template": color,
240
+ },
241
+ "elements": [
242
+ {"tag": "div", "text": {"tag": "lark_md", "content": body[:2000]}},
243
+ {"tag": "hr"},
244
+ {"tag": "note", "elements": [
245
+ {"tag": "plain_text",
246
+ "content": "Aria Daemon · " + __import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M")},
247
+ ]},
248
+ ],
249
+ },
250
+ }
251
+ try:
252
+ import httpx
253
+ async with httpx.AsyncClient(timeout=10) as client:
254
+ resp = await client.post(url, json=card)
255
+ if resp.status_code >= 400:
256
+ logger.warning("_feishu_push HTTP %s: %s", resp.status_code, resp.text[:120])
257
+ except Exception as exc:
258
+ logger.warning("_feishu_push failed: %s", exc)
259
+
260
+
261
+ # ── Alert watchdog ────────────────────────────────────────────────────────────
262
+
263
+ def _go_daemon_running() -> bool:
264
+ """Whether `aria-runtime daemon` runs on this folder. It owns price alerts
265
+ and schedules then; this daemon keeps its Telegram bot, webhook queue and
266
+ portfolio watchdog, and skips the rest so nothing fires twice."""
267
+ try:
268
+ pid = int((_ARIA_DIR / "daemon-go.pid").read_text().strip())
269
+ if pid <= 0 or pid == os.getpid():
270
+ return False
271
+ os.kill(pid, 0)
272
+ return True
273
+ except (OSError, ValueError):
274
+ return False
275
+
276
+
277
+ async def _alert_watchdog() -> None:
278
+ """Check all active alerts against live prices every 30 seconds."""
279
+ logger.info("Alert watchdog started")
280
+ while True:
281
+ if _go_daemon_running():
282
+ await asyncio.sleep(30)
283
+ continue
284
+ try:
285
+ with sqlite3.connect(_DB_PATH) as conn:
286
+ conn.row_factory = sqlite3.Row
287
+ rows = conn.execute(
288
+ "SELECT * FROM alerts WHERE active=1"
289
+ ).fetchall()
290
+ alerts = [dict(r) for r in rows]
291
+
292
+ # Group by symbol to minimise API calls
293
+ by_symbol: dict[str, list[dict]] = {}
294
+ for a in alerts:
295
+ by_symbol.setdefault(a["symbol"], []).append(a)
296
+
297
+ for symbol, sym_alerts in by_symbol.items():
298
+ price, prev_close = await _fetch_price(symbol)
299
+ if price is None:
300
+ continue
301
+ for alert in sym_alerts:
302
+ await _check_alert(alert, price, prev_close)
303
+
304
+ except Exception as exc:
305
+ logger.error("Alert watchdog error: %s", exc)
306
+
307
+ await asyncio.sleep(30)
308
+
309
+
310
+ async def _check_alert(alert: dict, price: float, prev_close: Optional[float] = None) -> None:
311
+ cond = alert["condition"]
312
+ val = float(alert["value"])
313
+
314
+ # pct_change 条件: prev_close=None 时跳过而非永远不触发
315
+ if cond in ("pct_change_above", "pct_change_below") and (not prev_close or prev_close <= 0):
316
+ logger.debug("Alert %s: prev_close unavailable, skipping pct_change check", alert["id"])
317
+ return
318
+
319
+ pct_chg = ((price - prev_close) / prev_close * 100) if prev_close and prev_close > 0 else None
320
+ fired = (
321
+ (cond == "price_above" and price > val) or
322
+ (cond == "price_below" and price < val) or
323
+ (cond == "pct_change_above" and pct_chg is not None and pct_chg > val) or
324
+ (cond == "pct_change_below" and pct_chg is not None and pct_chg < val)
325
+ )
326
+ if not fired:
327
+ return
328
+
329
+ symbol = alert["symbol"]
330
+ pct_str = f" ({pct_chg:+.2f}%)" if pct_chg is not None else ""
331
+ message = alert.get("message") or f"{symbol} {cond.replace('_', ' ')} {val}"
332
+ title = "🔔 ARIA 价格预警"
333
+ body = f"{symbol} ¥{price:.2f}{pct_str} — {message}"
334
+
335
+ logger.info("Alert triggered: %s | %s @ %.4f", alert["id"], symbol, price)
336
+
337
+ # Update DB
338
+ with sqlite3.connect(_DB_PATH) as conn:
339
+ conn.execute(
340
+ "UPDATE alerts SET trigger_count=trigger_count+1, triggered_at=datetime('now')"
341
+ + (", active=0" if alert.get("once", 1) else "")
342
+ + " WHERE id=?",
343
+ (alert["id"],),
344
+ )
345
+ conn.commit()
346
+
347
+ extra = {"symbol": symbol, "price": price, "signal": "ALERT"}
348
+
349
+ if alert.get("notify_push", 1):
350
+ await _push_alert(title, body, extra, alert["id"])
351
+
352
+ if alert.get("notify_telegram", 1):
353
+ await _telegram_push(f"*{title}*\n{body}")
354
+ await _feishu_push(title, body)
355
+ _system_notify(title, body)
356
+
357
+ # 触发后自动运行轻量分析(technical + risk),结果推送给用户
358
+ asyncio.create_task(_auto_analyze_alert(symbol, price, pct_str))
359
+
360
+
361
+ async def _auto_analyze_alert(symbol: str, price: float, pct_str: str) -> None:
362
+ """Run a lightweight technical+risk analysis after an alert fires and push the result."""
363
+ try:
364
+ sys.path.insert(0, str(_ARIA_CODE_DIR))
365
+ from agents.team import AgentTeam
366
+ team = AgentTeam(agent_names=["technical", "risk"])
367
+ result = await asyncio.wait_for(team.run(symbol), timeout=45.0)
368
+ signal = result.signal or "N/A"
369
+ conf = f"{result.confidence:.0%}" if result.confidence else "?"
370
+ points = "\n".join(f" • {p}" for p in (result.key_points or [])[:3])
371
+ body = (
372
+ f"🤖 *{symbol}* 预警后快速分析\n"
373
+ f"现价 {price:.4f}{pct_str}\n\n"
374
+ f"Signal: *{signal}* 置信度: {conf}\n"
375
+ f"{points}"
376
+ )
377
+ await _telegram_push(body)
378
+ await _feishu_push(f"🤖 {symbol} 预警分析", body.replace("*", "**"))
379
+ except asyncio.TimeoutError:
380
+ logger.warning("_auto_analyze_alert %s: timeout", symbol)
381
+ except Exception as exc:
382
+ logger.debug("_auto_analyze_alert %s: %s", symbol, exc)
383
+
384
+
385
+ # ── Telegram command handler ─────────────────────────────────────────────────
386
+
387
+ async def _run_report(symbol: str) -> str:
388
+ """Generate a quick text summary report."""
389
+ try:
390
+ price, _pc = await _fetch_price(symbol)
391
+ price_str = f"¥{price:.2f}" if price and len(symbol) == 6 and symbol.isdigit() else (f"${price:.2f}" if price else "N/A")
392
+
393
+ # Try to import aria-code market data for indicators
394
+ try:
395
+ import importlib.util
396
+ spec = importlib.util.spec_from_file_location(
397
+ "market_data_client",
398
+ _ARIA_CODE_DIR / "market_data_client.py",
399
+ )
400
+ mdc = importlib.util.module_from_spec(spec)
401
+ spec.loader.exec_module(mdc)
402
+ client = mdc.MarketDataClient()
403
+ quote = await asyncio.wait_for(client.quote(symbol), timeout=8.0)
404
+ name = quote.get("name", symbol)
405
+ chg = quote.get("change_pct", 0)
406
+ vol = quote.get("volume", 0)
407
+ chg_str = f"{chg:+.2f}%" if chg else ""
408
+ except Exception:
409
+ name, chg_str, vol = symbol, "", 0
410
+
411
+ lines = [
412
+ f"*{name}* `{symbol}` 快速分析",
413
+ "",
414
+ f"💰 价格: `{price_str}` {chg_str}",
415
+ f"📊 成交量: `{vol:,}`" if vol else "",
416
+ "",
417
+ f"⚠️ 深度研报请在 Terminal 使用 `/report {symbol}` 命令。",
418
+ "此处为轻量版摘要。",
419
+ ]
420
+ return "\n".join(l for l in lines if l is not None)
421
+
422
+ except Exception as exc:
423
+ logger.error("_run_report %s: %s", symbol, exc)
424
+ return f"⚠️ 分析 {symbol} 时出错: {exc}"
425
+
426
+
427
+ def _maybe_auto_execute(preview_result: dict) -> list[str]:
428
+ """Opt-in controlled auto-execution. Default OFF → draft-only.
429
+
430
+ Only executes when the broker config turns on ``automation.enabled`` AND every
431
+ gate in brokers.automation passes (dry-run off, risk passed, value/rate limits).
432
+ """
433
+ broker_id = preview_result.get("broker_id") or ""
434
+ preview_id = preview_result.get("preview_id") or ""
435
+ if not (broker_id and preview_id):
436
+ return ["Webhook 只生成预览,不会自动执行实盘/仿盘订单。"]
437
+ try:
438
+ from brokers.config import get_broker_config
439
+ from brokers.automation import AutoExecutePolicy, run_auto_execute
440
+ from brokers.registry import get_registry
441
+
442
+ policy = AutoExecutePolicy.from_config(get_broker_config(broker_id) or {})
443
+ if not policy.enabled:
444
+ return ["Webhook 只生成预览,不会自动执行(automation.enabled=false)。"]
445
+ reg = get_registry()
446
+ broker = reg.get(broker_id) or reg.ensure(broker_id)
447
+ result = run_auto_execute(broker, preview_id, policy)
448
+ if result.get("auto_executed"):
449
+ return [f"✅ 已按自动交易策略执行 (order_id: `{result.get('order_id', '')}`)"]
450
+ return ["⏸ 自动执行已跳过: " + "; ".join(str(r) for r in (result.get("reasons") or [])[:3])]
451
+ except Exception as exc:
452
+ logger.warning("auto-execute check failed: %s", exc)
453
+ return [f"自动执行检查失败,保留为预览: `{exc}`"]
454
+
455
+
456
+
457
+ def gateway_config() -> dict:
458
+ """The config the daemon hands to the runtime gateway.
459
+
460
+ This used to be a dict literal pinned to Ollama:
461
+
462
+ {"model": os.environ.get("ARIA_DAEMON_MODEL", "qwen2.5:7b"),
463
+ "ollama_url": ..., "local_provider": "ollama"}
464
+
465
+ so the daemon never saw the user's configuration. Someone running Aria on
466
+ a Vertex/Gemini model had every alert analysed by a local qwen2.5:7b, and
467
+ someone with no Ollama at all had the gateway raise on every alert, fall
468
+ back to the quick summary, and say so only at log level info.
469
+
470
+ The gateway itself is fully config-driven — analyze_alert_via_gateway
471
+ passes model/api_url/ollama_url straight through — so it was only ever the
472
+ daemon fabricating this.
473
+
474
+ Loaded per call rather than cached: the daemon is long-lived, and a config
475
+ change should reach the next alert without a restart. The two env vars stay
476
+ as operator overrides (ARIA_DAEMON_MODEL is in this module's env table).
477
+ """
478
+ from apps.cli.bootstrap import default_config
479
+ from apps.cli.config_paths import resolve_paths
480
+ from packages.aria_services.settings import SettingsService
481
+
482
+ paths = resolve_paths()
483
+ cfg = SettingsService(
484
+ config_dir=paths.config_dir,
485
+ config_file=paths.config_file,
486
+ sessions_dir=paths.sessions_dir,
487
+ defaults=default_config(),
488
+ ).load()
489
+
490
+ if os.environ.get("ARIA_DAEMON_MODEL"):
491
+ cfg["model"] = os.environ["ARIA_DAEMON_MODEL"]
492
+ if os.environ.get("OLLAMA_URL"):
493
+ cfg["ollama_url"] = os.environ["OLLAMA_URL"]
494
+ return cfg
495
+
496
+ async def _run_tradingview_alert(payload: dict) -> str:
497
+ """Handle a TradingView alert webhook job."""
498
+ try:
499
+ from apps.cli.tradingview_bridge import build_tradingview_order_preview, parse_tradingview_alert
500
+ alert = parse_tradingview_alert(payload)
501
+ except Exception as exc:
502
+ raise ValueError(f"TradingView alert 解析失败: {exc}") from exc
503
+
504
+ symbol = str(alert.get("symbol") or "").upper()
505
+ if not symbol:
506
+ raise ValueError("TradingView alert 缺少 symbol/ticker")
507
+
508
+ action = str(alert.get("action") or "ALERT").upper()
509
+ price = alert.get("price")
510
+ price_line = f"Price: `{price}`" if price not in (None, "") else "Price: `N/A`"
511
+ msg = str(alert.get("message") or "").strip()
512
+ header = [
513
+ f"*TradingView Alert* `{symbol}`",
514
+ f"Action: *{action}*",
515
+ price_line,
516
+ ]
517
+ if msg:
518
+ header.append(f"Message: {msg[:300]}")
519
+
520
+ preview_lines: list[str] = []
521
+ try:
522
+ preview_result = build_tradingview_order_preview(payload)
523
+ if preview_result.get("trade_preview_created"):
524
+ blockers = preview_result.get("execution_blockers") or []
525
+ preview_lines.extend([
526
+ "",
527
+ "*Trade Preview*",
528
+ f"preview_id: `{preview_result.get('preview_id')}`",
529
+ f"Mode: `{preview_result.get('mode')}` Broker: `{preview_result.get('broker_label')}`",
530
+ f"Can execute after confirmation: `{bool(preview_result.get('can_execute'))}`",
531
+ f"Confirm: `{preview_result.get('confirm_command')}`",
532
+ ])
533
+ if blockers:
534
+ preview_lines.append("Blockers: " + "; ".join(str(item) for item in blockers[:4]))
535
+ preview_lines.extend(_maybe_auto_execute(preview_result))
536
+ elif preview_result.get("success"):
537
+ reason = preview_result.get("reason", "no_trade_preview")
538
+ preview_lines.extend(["", "*Trade Preview*", f"未生成订单预览: `{reason}`"])
539
+ if preview_result.get("hint"):
540
+ preview_lines.append(str(preview_result["hint"]))
541
+ else:
542
+ preview_lines.extend(["", "*Trade Preview*", f"生成失败: `{preview_result.get('error')}`"])
543
+ except Exception as exc:
544
+ logger.warning("TradingView trade preview failed: %s", exc)
545
+ preview_lines.extend(["", "*Trade Preview*", f"生成失败: `{exc}`"])
546
+
547
+ # Channels contract: analysis goes through the shared runtime gateway
548
+ # (tool-less turn) instead of poking CLI internals; the legacy quick
549
+ # summary stays as the fallback when no model backend is reachable.
550
+ analysis = ""
551
+ try:
552
+ from apps.channels.intake import analyze_alert_via_gateway
553
+ from apps.channels.tradingview import task_prompt
554
+ analysis = await asyncio.wait_for(
555
+ analyze_alert_via_gateway(task_prompt(alert), gateway_config()), timeout=90.0
556
+ )
557
+ except Exception as exc:
558
+ # warning, not info: this is a silent capability downgrade — the alert
559
+ # still gets answered, but by the legacy summary rather than the model.
560
+ logger.warning("gateway analysis unavailable (%s) — using quick summary", exc)
561
+ if not analysis:
562
+ analysis = await _run_report(symbol)
563
+ return "\n".join(header + preview_lines) + "\n\n" + analysis
564
+
565
+
566
+ async def _run_morning_brief() -> str:
567
+ """Enriched morning market brief — A-share + US + crypto + FX."""
568
+ try:
569
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
570
+ lines = [f"*🌅 Aria 晨报 {now_str}*\n"]
571
+
572
+ # ── A-share indices ────────────────────────────────────────────────
573
+ cn_indices = [
574
+ ("上证指数", "000001.SS"), ("深证成指", "399001.SZ"),
575
+ ("创业板", "399006.SZ"), ("沪深300", "000300.SS"),
576
+ ]
577
+ lines.append("*🇨🇳 A股指数*")
578
+ for name, sym in cn_indices:
579
+ p, pc = await _fetch_price(sym)
580
+ if p:
581
+ arrow = "📈" if (pc or 0) >= 0 else "📉"
582
+ pct_str = f" {pc:+.2f}%" if pc is not None else ""
583
+ lines.append(f" {arrow} {name}: `¥{p:.2f}`{pct_str}")
584
+ else:
585
+ lines.append(f" — {name}: N/A")
586
+
587
+ # ── US markets ────────────────────────────────────────────────────
588
+ us_indices = [
589
+ ("S&P 500", "^GSPC"), ("纳斯达克", "^IXIC"),
590
+ ("道琼斯", "^DJI"), ("VIX恐慌", "^VIX"),
591
+ ]
592
+ lines.append("\n*🇺🇸 美股指数*")
593
+ for name, sym in us_indices:
594
+ p, pc = await _fetch_price(sym)
595
+ if p:
596
+ arrow = "📈" if (pc or 0) >= 0 else "📉"
597
+ pct_str = f" {pc:+.2f}%" if pc is not None else ""
598
+ lines.append(f" {arrow} {name}: `{p:.2f}`{pct_str}")
599
+ else:
600
+ lines.append(f" — {name}: N/A")
601
+
602
+ # ── Crypto ────────────────────────────────────────────────────────
603
+ crypto_pairs = [("BTC", "BTC-USD"), ("ETH", "ETH-USD"), ("黄金", "GC=F")]
604
+ lines.append("\n*₿ 加密 & 大宗*")
605
+ for name, sym in crypto_pairs:
606
+ p, pc = await _fetch_price(sym)
607
+ if p:
608
+ pct_str = f" {pc:+.2f}%" if pc is not None else ""
609
+ lines.append(f" {name}: `${p:,.2f}`{pct_str}")
610
+ else:
611
+ lines.append(f" {name}: N/A")
612
+
613
+ # ── FX ───────────────────────────────────────────────────────────
614
+ fx_pairs = [("USD/CNY", "CNY=X"), ("USD/JPY", "JPY=X")]
615
+ lines.append("\n*💱 汇率*")
616
+ for name, sym in fx_pairs:
617
+ p, _ = await _fetch_price(sym)
618
+ lines.append(f" {name}: `{p:.4f}`" if p else f" {name}: N/A")
619
+
620
+ # ── Portfolio P&L summary ─────────────────────────────────────────
621
+ pnl = await _get_portfolio_daily_pnl()
622
+ if pnl:
623
+ lines.append("\n*💼 持仓日内 P&L*")
624
+ lines.append(pnl)
625
+
626
+ lines.append("\n_数据来自 yfinance,仅供参考_")
627
+ return "\n".join(lines)
628
+ except Exception as exc:
629
+ logger.warning("morning brief error: %s", exc)
630
+ return f"⚠️ 晨报生成失败: {exc}"
631
+
632
+
633
+ async def _run_evening_brief() -> str:
634
+ """Post-market A-share evening summary."""
635
+ try:
636
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
637
+ lines = [f"*🌆 Aria 收盘简报 {now_str}*\n"]
638
+
639
+ cn_indices = [
640
+ ("上证指数", "000001.SS"), ("深证成指", "399001.SZ"),
641
+ ("创业板", "399006.SZ"), ("沪深300", "000300.SS"),
642
+ ]
643
+ lines.append("*今日收盘*")
644
+ for name, sym in cn_indices:
645
+ p, pc = await _fetch_price(sym)
646
+ if p:
647
+ arrow = "📈" if (pc or 0) >= 0 else "📉"
648
+ pct_str = f" {pc:+.2f}%" if pc is not None else ""
649
+ lines.append(f" {arrow} {name}: `¥{p:.2f}`{pct_str}")
650
+
651
+ pnl = await _get_portfolio_daily_pnl()
652
+ if pnl:
653
+ lines.append(f"\n*💼 今日持仓损益*\n{pnl}")
654
+
655
+ lines.append("\n_数据来自 yfinance · 请以实盘数据为准_")
656
+ return "\n".join(lines)
657
+ except Exception as exc:
658
+ return f"⚠️ 收盘简报失败: {exc}"
659
+
660
+
661
+ async def _run_weekly_recap() -> str:
662
+ """Friday end-of-week performance summary."""
663
+ try:
664
+ now_str = datetime.now().strftime("%Y 第%W周")
665
+ lines = [f"*📅 Aria 周报 {now_str}*\n"]
666
+
667
+ us_etfs = [("S&P500", "SPY"), ("纳指", "QQQ"), ("罗素", "IWM"), ("黄金", "GLD")]
668
+ lines.append("*本周美股 ETF 表现*")
669
+ for name, sym in us_etfs:
670
+ try:
671
+ import yfinance as _yf
672
+ df = _yf.download(sym, period="5d", progress=False, auto_adjust=True)
673
+ if not df.empty and len(df) >= 2:
674
+ if hasattr(df.columns, 'levels'):
675
+ df.columns = df.columns.droplevel(1)
676
+ w_ret = (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100
677
+ arrow = "📈" if w_ret >= 0 else "📉"
678
+ lines.append(f" {arrow} {name}: `{w_ret:+.2f}%`")
679
+ except Exception:
680
+ lines.append(f" — {name}: N/A")
681
+
682
+ cn_indices = [("上证", "000001.SS"), ("沪深300", "000300.SS"), ("创业板", "399006.SZ")]
683
+ lines.append("\n*本周A股指数*")
684
+ for name, sym in cn_indices:
685
+ try:
686
+ import yfinance as _yf
687
+ df = _yf.download(sym, period="5d", progress=False, auto_adjust=True)
688
+ if not df.empty and len(df) >= 2:
689
+ if hasattr(df.columns, 'levels'):
690
+ df.columns = df.columns.droplevel(1)
691
+ w_ret = (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100
692
+ arrow = "📈" if w_ret >= 0 else "📉"
693
+ lines.append(f" {arrow} {name}: `{w_ret:+.2f}%`")
694
+ except Exception:
695
+ lines.append(f" — {name}: N/A")
696
+
697
+ lines.append("\n_数据来自 yfinance,仅供参考_")
698
+ return "\n".join(lines)
699
+ except Exception as exc:
700
+ return f"⚠️ 周报生成失败: {exc}"
701
+
702
+
703
+ async def _get_portfolio_daily_pnl() -> str:
704
+ """Read portfolio positions from DB and compute daily P&L."""
705
+ try:
706
+ portfolio_db = _ARIA_DIR / "portfolio.db"
707
+ if not portfolio_db.exists():
708
+ return ""
709
+ with sqlite3.connect(portfolio_db) as conn:
710
+ conn.row_factory = sqlite3.Row
711
+ rows = conn.execute(
712
+ "SELECT symbol, quantity, avg_cost FROM positions WHERE quantity > 0 LIMIT 10"
713
+ ).fetchall()
714
+ if not rows:
715
+ return ""
716
+ total_pnl = 0.0
717
+ position_lines = []
718
+ for r in rows:
719
+ sym = r["symbol"]
720
+ qty = r["quantity"]
721
+ cost = r["avg_cost"]
722
+ price, pct = await _fetch_price(sym)
723
+ if price and cost and qty:
724
+ day_pnl = (price - cost) * qty
725
+ total_pnl += day_pnl
726
+ arrow = "📈" if day_pnl >= 0 else "📉"
727
+ position_lines.append(
728
+ f" {arrow} {sym}: ¥{price:.2f} 日盈亏 `{day_pnl:+.2f}`"
729
+ )
730
+ if not position_lines:
731
+ return ""
732
+ result = "\n".join(position_lines)
733
+ result += f"\n 合计: `{total_pnl:+.2f}`"
734
+ return result
735
+ except Exception as exc:
736
+ logger.debug("portfolio pnl error: %s", exc)
737
+ return ""
738
+
739
+
740
+ async def _portfolio_loss_watchdog() -> None:
741
+ """Check portfolio positions every 5 min; push alert if any position drops > threshold."""
742
+ LOSS_THRESHOLD_PCT = float(os.environ.get("ARIA_LOSS_ALERT_PCT", "5.0"))
743
+ logger.info("Portfolio loss watchdog started (threshold: %.1f%%)", LOSS_THRESHOLD_PCT)
744
+ while True:
745
+ try:
746
+ portfolio_db = _ARIA_DIR / "portfolio.db"
747
+ if portfolio_db.exists():
748
+ with sqlite3.connect(portfolio_db) as conn:
749
+ conn.row_factory = sqlite3.Row
750
+ rows = conn.execute(
751
+ "SELECT symbol, quantity, avg_cost FROM positions WHERE quantity > 0"
752
+ ).fetchall()
753
+ for r in rows:
754
+ sym = r["symbol"]
755
+ qty = float(r["quantity"])
756
+ cost = float(r["avg_cost"] or 0)
757
+ if qty <= 0 or cost <= 0:
758
+ continue
759
+ price, pct = await _fetch_price(sym)
760
+ if price and pct is not None and pct <= -LOSS_THRESHOLD_PCT:
761
+ day_loss = (price - cost) * qty
762
+ title = f"⚠️ {sym} 跌幅预警"
763
+ body = (
764
+ f"{sym} 当前价 ¥{price:.2f},"
765
+ f"日跌幅 {pct:.2f}%,"
766
+ f"持仓损益 {day_loss:+.2f} 元"
767
+ )
768
+ logger.info("Portfolio loss alert: %s %.2f%%", sym, pct)
769
+ await _push_alert(title, body)
770
+ await _telegram_push(f"*{title}*\n{body}")
771
+ await _feishu_push(title, body)
772
+ except Exception as exc:
773
+ logger.debug("portfolio watchdog error: %s", exc)
774
+ await asyncio.sleep(300)
775
+
776
+
777
+ def _system_notify(title: str, body: str) -> None:
778
+ """Fire a native OS desktop notification (non-blocking, best-effort)."""
779
+ try:
780
+ import platform as _pl
781
+ if _pl.system() == "Darwin":
782
+ import subprocess as _sp
783
+ safe_title = title.replace('"', '\\"').replace("'", "\\'")
784
+ safe_body = body.replace('"', '\\"').replace("'", "\\'")
785
+ _sp.Popen(
786
+ ["osascript", "-e",
787
+ f'display notification "{safe_body}" with title "{safe_title}" subtitle "Aria Code"'],
788
+ stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
789
+ )
790
+ elif _pl.system() == "Windows":
791
+ try:
792
+ from win10toast import ToastNotifier
793
+ ToastNotifier().show_toast(title, body, duration=6, threaded=True)
794
+ except ImportError:
795
+ pass
796
+ except Exception:
797
+ pass
798
+
799
+
800
+ async def _run_screener() -> str:
801
+ """Quick hot-stock screener using market_data_client if available."""
802
+ try:
803
+ spec = __import__("importlib.util").util.spec_from_file_location(
804
+ "market_data_client", _ARIA_CODE_DIR / "market_data_client.py"
805
+ )
806
+ mdc = __import__("importlib.util").util.module_from_spec(spec)
807
+ spec.loader.exec_module(mdc)
808
+ client = mdc.MarketDataClient()
809
+ result = await asyncio.wait_for(client.hot_ashare(limit=8), timeout=10.0)
810
+ stocks = result if isinstance(result, list) else []
811
+ if not stocks:
812
+ return "⚠️ 暂时无法获取行情数据(市场未开市或数据源异常)"
813
+ lines = ["*🔥 A股热门*\n"]
814
+ for s in stocks[:8]:
815
+ sym = s.get("symbol", "")
816
+ name = s.get("name", sym)
817
+ p = s.get("price", 0)
818
+ chg = s.get("change_pct", 0)
819
+ lines.append(f" `{sym}` {name} ¥{p:.2f} {chg:+.2f}%")
820
+ return "\n".join(lines)
821
+ except Exception as exc:
822
+ return f"⚠️ 筛选器出错: {exc}"
823
+
824
+
825
+ async def _run_chat(text: str) -> str:
826
+ """Simple chat fallback using aria-code LLM if configured."""
827
+ return f"💬 _收到你的消息: \"{text[:80]}\"_\n\n请在 Terminal 启动 `/aria` 获得完整对话体验。"
828
+
829
+
830
+ # ── Webhook job executor ─────────────────────────────────────────────────────
831
+
832
+ async def _webhook_executor() -> None:
833
+ """Poll webhook_jobs table for pending jobs and execute them."""
834
+ logger.info("Webhook executor started")
835
+ while True:
836
+ try:
837
+ with sqlite3.connect(_DB_PATH) as conn:
838
+ conn.row_factory = sqlite3.Row
839
+ rows = conn.execute(
840
+ "SELECT * FROM webhook_jobs WHERE status='pending' ORDER BY created_at LIMIT 5"
841
+ ).fetchall()
842
+ for row in rows:
843
+ asyncio.create_task(_execute_job(dict(row)))
844
+ except Exception as exc:
845
+ logger.error("Webhook executor error: %s", exc)
846
+ await asyncio.sleep(2)
847
+
848
+
849
+ async def _webhook_http_server() -> None:
850
+ """Small local HTTP endpoint for TradingView webhooks."""
851
+ try:
852
+ from aiohttp import web
853
+ from apps.cli.tradingview_bridge import enqueue_tradingview_alert
854
+ except ImportError as exc:
855
+ logger.warning("Webhook HTTP server disabled: %s", exc)
856
+ return
857
+
858
+ token = os.environ.get("TRADINGVIEW_WEBHOOK_SECRET") or os.environ.get("WEBHOOK_TOKEN", "")
859
+ host = os.environ.get("ARIA_WEBHOOK_HOST", "127.0.0.1")
860
+ port = int(os.environ.get("ARIA_WEBHOOK_PORT", "8765"))
861
+
862
+ def _authorized(request) -> bool:
863
+ if not token:
864
+ return True
865
+ # Headers only: a token in the query string ends up in proxy and
866
+ # access logs and in browser history. TradingView, which cannot set
867
+ # headers, authenticates with the body passphrase (ARIA_WEBHOOK_SECRET).
868
+ supplied = (
869
+ request.headers.get("X-Aria-Token")
870
+ or request.headers.get("X-TradingView-Secret")
871
+ or request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
872
+ or ""
873
+ )
874
+ return hmac.compare_digest(supplied.encode("utf-8"), token.encode("utf-8"))
875
+
876
+ async def _handle_tradingview(request):
877
+ # Open-mode guard (channels contract): with neither an endpoint token
878
+ # nor ARIA_WEBHOOK_SECRET configured, only loopback clients may submit —
879
+ # an open endpoint reachable off-host would accept injected alerts.
880
+ try:
881
+ from apps.channels.intake import should_refuse_open_intake
882
+ except ImportError:
883
+ # Without the guard we cannot tell whether this client may use an
884
+ # open endpoint, so it may not: fail closed rather than skip it.
885
+ logger.error("open-intake guard unavailable; refusing TradingView webhook")
886
+ return web.json_response(
887
+ {"success": False, "error": "webhook intake guard unavailable"}, status=503,
888
+ )
889
+ secret_configured = bool(os.environ.get("ARIA_WEBHOOK_SECRET", "").strip())
890
+ if should_refuse_open_intake(
891
+ getattr(request, "remote", None),
892
+ token_configured=bool(token),
893
+ secret_configured=secret_configured,
894
+ ):
895
+ return web.json_response(
896
+ {"success": False,
897
+ "error": "open-mode intake refused for non-local client; set ARIA_WEBHOOK_SECRET or WEBHOOK_TOKEN"},
898
+ status=403,
899
+ )
900
+ if not _authorized(request):
901
+ return web.json_response({"success": False, "error": "unauthorized"}, status=401)
902
+ text = await request.text()
903
+ try:
904
+ import json as _json
905
+ payload = _json.loads(text) if text.strip() else {}
906
+ except Exception:
907
+ payload = {"message": text}
908
+ if request.query.get("channels") and isinstance(payload, dict):
909
+ import json as _json
910
+ payload["channels"] = _json.dumps(
911
+ [item.strip() for item in request.query["channels"].split(",") if item.strip()]
912
+ )
913
+ if isinstance(payload, dict):
914
+ for key in ("broker_id", "quantity", "qty", "target_weight", "order_type"):
915
+ if request.query.get(key):
916
+ payload[key] = request.query[key]
917
+ result = enqueue_tradingview_alert(payload, db_path=_DB_PATH)
918
+ status = 202 if result.get("success") else 400
919
+ return web.json_response(result, status=status)
920
+
921
+ app = web.Application()
922
+ app.router.add_post("/api/v1/webhook/tradingview", _handle_tradingview)
923
+ app.router.add_post("/api/v1/tradingview/webhook", _handle_tradingview)
924
+
925
+ runner = web.AppRunner(app)
926
+ await runner.setup()
927
+ site = web.TCPSite(runner, host, port)
928
+ try:
929
+ await site.start()
930
+ except OSError as exc:
931
+ logger.error(
932
+ "TradingView webhook could not listen on %s:%d (%s). The relay server "
933
+ "also defaults to 8765; set ARIA_WEBHOOK_PORT to another port.", host, port, exc,
934
+ )
935
+ await runner.cleanup()
936
+ return
937
+ logger.info("TradingView webhook listening on http://%s:%d/api/v1/webhook/tradingview", host, port)
938
+ try:
939
+ while True:
940
+ await asyncio.sleep(3600)
941
+ finally:
942
+ await runner.cleanup()
943
+
944
+
945
+ async def _execute_job(job: dict) -> None:
946
+ job_id = job["id"]
947
+ command = job["command"]
948
+ try:
949
+ import json as _json
950
+ payload = _json.loads(job.get("payload") or "{}")
951
+ except Exception:
952
+ payload = {}
953
+
954
+ with sqlite3.connect(_DB_PATH) as conn:
955
+ conn.execute(
956
+ "UPDATE webhook_jobs SET status='running', started_at=datetime('now') WHERE id=?",
957
+ (job_id,),
958
+ )
959
+ conn.commit()
960
+
961
+ try:
962
+ if command.startswith("chat:"):
963
+ result = await _run_chat(command[5:])
964
+ elif command.startswith("report ") or command.startswith("/report "):
965
+ sym = command.split()[-1].upper()
966
+ result = await _run_report(sym)
967
+ elif command == "tradingview_alert":
968
+ result = await _run_tradingview_alert(payload)
969
+ elif command in ("morning-brief", "/morning-brief", "brief"):
970
+ result = await _run_morning_brief()
971
+ elif command in ("screen", "/screen"):
972
+ result = await _run_screener()
973
+ else:
974
+ raise ValueError(f"未知 Webhook 命令: {command!r}(支持: chat: / report / morning-brief / screen)")
975
+
976
+ with sqlite3.connect(_DB_PATH) as conn:
977
+ conn.execute(
978
+ "UPDATE webhook_jobs SET status='done', result=?, done_at=datetime('now') WHERE id=?",
979
+ (result[:2000], job_id),
980
+ )
981
+ conn.commit()
982
+
983
+ # If channels include telegram, send result
984
+ channels = []
985
+ try:
986
+ import json as _j
987
+ raw_channels = payload.get("channels", "[]")
988
+ if isinstance(raw_channels, list):
989
+ channels = raw_channels
990
+ else:
991
+ channels = _j.loads(raw_channels or "[]")
992
+ except Exception:
993
+ pass
994
+ if command == "tradingview_alert" and not channels:
995
+ channels = ["telegram", "feishu"]
996
+ if "telegram" in channels:
997
+ await _telegram_push(result)
998
+ if "feishu" in channels:
999
+ await _feishu_push("⏰ Webhook 任务完成", result[:2000])
1000
+
1001
+ except Exception as exc:
1002
+ logger.error("Job %s failed: %s", job_id, exc)
1003
+ with sqlite3.connect(_DB_PATH) as conn:
1004
+ conn.execute(
1005
+ "UPDATE webhook_jobs SET status='error', result=?, done_at=datetime('now') WHERE id=?",
1006
+ (str(exc)[:500], job_id),
1007
+ )
1008
+ conn.commit()
1009
+
1010
+
1011
+ # ── Shipper digest ───────────────────────────────────────────────────────────
1012
+
1013
+ async def _run_shipper_digests() -> None:
1014
+ """Push each bound shipper group its digest (apps/channels/digest.py)."""
1015
+ try:
1016
+ from apps.channels.conversation import ConversationStore
1017
+ from apps.channels.digest import run_digests
1018
+
1019
+ senders = {}
1020
+ import aria_feishu_bot
1021
+ # With the app's credentials (self-hosted), or through the relay client
1022
+ # this daemon runs in relay mode — the bot knows which it has.
1023
+ if aria_feishu_bot.can_send():
1024
+ senders["feishu"] = aria_feishu_bot.send_digest_card
1025
+ report = await run_digests(ConversationStore(), senders)
1026
+ for line in report:
1027
+ logger.info("shipper digest %s: %s", line["conversation"], line["status"])
1028
+ except Exception as exc:
1029
+ logger.error("shipper digest job failed: %s", exc)
1030
+
1031
+
1032
+ # ── APScheduler cron ─────────────────────────────────────────────────────────
1033
+
1034
+ def _start_scheduler() -> None:
1035
+ """Load schedules from DB and register with APScheduler."""
1036
+ try:
1037
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
1038
+ from apscheduler.triggers.cron import CronTrigger
1039
+
1040
+ scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
1041
+
1042
+ with sqlite3.connect(_DB_PATH) as conn:
1043
+ conn.row_factory = sqlite3.Row
1044
+ rows = conn.execute(
1045
+ "SELECT * FROM schedules WHERE enabled=1"
1046
+ ).fetchall()
1047
+
1048
+ for row in rows:
1049
+ s = dict(row)
1050
+ try:
1051
+ trigger = CronTrigger.from_crontab(s["cron_expr"], timezone="Asia/Shanghai")
1052
+ scheduler.add_job(
1053
+ _run_scheduled_job,
1054
+ trigger=trigger,
1055
+ args=[s],
1056
+ id=s["id"],
1057
+ name=s.get("name") or s["command"],
1058
+ replace_existing=True,
1059
+ misfire_grace_time=300,
1060
+ )
1061
+ logger.info("Scheduled: %s [%s]", s.get("name", s["id"]), s["cron_expr"])
1062
+ except Exception as exc:
1063
+ logger.error("Failed to schedule %s: %s", s["id"], exc)
1064
+
1065
+ # Default scheduled jobs (always register regardless of user schedules)
1066
+ # A-share pre-market brief: 08:55 weekdays (before 09:30 open)
1067
+ scheduler.add_job(
1068
+ _run_morning_brief_and_push,
1069
+ CronTrigger.from_crontab("55 8 * * 1-5", timezone="Asia/Shanghai"),
1070
+ id="default_morning_brief",
1071
+ name="A股开盘前晨报",
1072
+ replace_existing=True,
1073
+ misfire_grace_time=600,
1074
+ )
1075
+ # US pre-market brief: 21:00 weekdays CN time (US market opens 21:30 CN)
1076
+ scheduler.add_job(
1077
+ _run_morning_brief_and_push,
1078
+ CronTrigger.from_crontab("0 21 * * 1-5", timezone="Asia/Shanghai"),
1079
+ id="default_us_brief",
1080
+ name="美股开盘前简报",
1081
+ replace_existing=True,
1082
+ misfire_grace_time=600,
1083
+ )
1084
+ # A-share post-market evening brief: 15:35 weekdays
1085
+ scheduler.add_job(
1086
+ _run_evening_brief_and_push,
1087
+ CronTrigger.from_crontab("35 15 * * 1-5", timezone="Asia/Shanghai"),
1088
+ id="default_evening_brief",
1089
+ name="A股收盘晚报",
1090
+ replace_existing=True,
1091
+ misfire_grace_time=600,
1092
+ )
1093
+ # Weekly recap: Friday 16:30 CN time
1094
+ scheduler.add_job(
1095
+ _run_weekly_recap_and_push,
1096
+ CronTrigger.from_crontab("30 16 * * 5", timezone="Asia/Shanghai"),
1097
+ id="default_weekly_recap",
1098
+ name="周报",
1099
+ replace_existing=True,
1100
+ misfire_grace_time=1800,
1101
+ )
1102
+ logger.info("Registered 4 default scheduled jobs (morning / US / evening / weekly)")
1103
+
1104
+ # Shipper digest: each group bound to a shipper gets what needs
1105
+ # attention. Only when the operator has said where the data is.
1106
+ if os.environ.get("ARIA_SHIPPER_FEEDS", "").strip():
1107
+ cron = os.environ.get("ARIA_DIGEST_CRON", "30 8 * * 1-6").strip()
1108
+ scheduler.add_job(
1109
+ _run_shipper_digests,
1110
+ CronTrigger.from_crontab(cron, timezone="Asia/Shanghai"),
1111
+ id="default_shipper_digest",
1112
+ name="货主库存与运费提醒",
1113
+ replace_existing=True,
1114
+ misfire_grace_time=1800,
1115
+ )
1116
+ logger.info("Registered shipper digest [%s]", cron)
1117
+
1118
+ scheduler.start()
1119
+ logger.info("APScheduler started with %d job(s)", len(scheduler.get_jobs()))
1120
+ return scheduler
1121
+ except ImportError:
1122
+ logger.warning("APScheduler not installed — cron disabled. pip install apscheduler")
1123
+ return None
1124
+
1125
+
1126
+ async def _run_scheduled_job(schedule: dict) -> None:
1127
+ if _go_daemon_running():
1128
+ logger.info("Scheduled job %s left to aria-runtime daemon", schedule.get("name", schedule["id"]))
1129
+ return
1130
+ logger.info("Running scheduled job: %s", schedule.get("name", schedule["id"]))
1131
+ import json as _j
1132
+ channels = _j.loads(schedule.get("channels") or '["ios","telegram"]')
1133
+ cmd = schedule["command"]
1134
+ result = ""
1135
+ try:
1136
+ if cmd in ("morning-brief", "morning"):
1137
+ result = await _run_morning_brief()
1138
+ elif cmd in ("evening-brief", "evening"):
1139
+ result = await _run_evening_brief()
1140
+ elif cmd in ("weekly-recap", "weekly"):
1141
+ result = await _run_weekly_recap()
1142
+ elif cmd == "screen":
1143
+ result = await _run_screener()
1144
+ elif cmd.startswith("report "):
1145
+ result = await _run_report(cmd.split()[-1].upper())
1146
+ elif cmd.startswith("custom:"):
1147
+ result = f"Custom job: {cmd[7:]}"
1148
+ else:
1149
+ raise ValueError(
1150
+ f"未知定时命令: {cmd!r}(支持: morning-brief / evening-brief / weekly-recap / screen / report <symbol> / custom:<payload>)"
1151
+ )
1152
+ except Exception as exc:
1153
+ result = f"⚠️ 定时任务 [{cmd}] 失败: {exc}"
1154
+
1155
+ if "telegram" in channels:
1156
+ await _telegram_push(f"⏰ *定时任务完成*\n{result}")
1157
+ if "feishu" in channels:
1158
+ await _feishu_push(f"⏰ 定时任务:{cmd}", result[:2000])
1159
+ if "ios" in channels:
1160
+ await _push_alert(
1161
+ "Aria 定时任务",
1162
+ result[:150],
1163
+ {"command": cmd},
1164
+ )
1165
+
1166
+ with sqlite3.connect(_DB_PATH) as conn:
1167
+ conn.execute(
1168
+ "UPDATE schedules SET last_run=datetime('now') WHERE id=?",
1169
+ (schedule["id"],),
1170
+ )
1171
+ conn.commit()
1172
+
1173
+
1174
+ async def _run_morning_brief_and_push() -> None:
1175
+ if _go_daemon_running():
1176
+ return
1177
+ brief = await _run_morning_brief()
1178
+ await _telegram_push(brief)
1179
+ await _feishu_push("📊 Aria 晨报", brief[:2000])
1180
+ await _push_alert("Aria 晨报", brief[:150])
1181
+ _system_notify("Aria 晨报", brief[:120])
1182
+
1183
+
1184
+ async def _run_evening_brief_and_push() -> None:
1185
+ if _go_daemon_running():
1186
+ return
1187
+ brief = await _run_evening_brief()
1188
+ await _telegram_push(brief)
1189
+ await _feishu_push("🌆 Aria 收盘简报", brief[:2000])
1190
+ await _push_alert("Aria 收盘", brief[:150])
1191
+ _system_notify("Aria 收盘简报", brief[:120])
1192
+
1193
+
1194
+ async def _run_weekly_recap_and_push() -> None:
1195
+ if _go_daemon_running():
1196
+ return
1197
+ recap = await _run_weekly_recap()
1198
+ await _telegram_push(recap)
1199
+ await _feishu_push("📅 Aria 周报", recap[:2000])
1200
+ await _push_alert("Aria 周报", recap[:150])
1201
+ _system_notify("Aria 周报", recap[:120])
1202
+
1203
+
1204
+ # ── PID management ────────────────────────────────────────────────────────────
1205
+
1206
+ def _write_pid() -> None:
1207
+ _ARIA_DIR.mkdir(parents=True, exist_ok=True)
1208
+ _PID_FILE.write_text(str(os.getpid()))
1209
+
1210
+
1211
+ def _remove_pid() -> None:
1212
+ try:
1213
+ _PID_FILE.unlink(missing_ok=True)
1214
+ except Exception:
1215
+ pass
1216
+
1217
+
1218
+ # ── Install / uninstall as macOS LaunchAgent ──────────────────────────────────
1219
+
1220
+ def _install_launchagent() -> None:
1221
+ python = sys.executable
1222
+ script = str(Path(__file__).resolve())
1223
+ log_out = str(_LOG_DIR / "daemon.log")
1224
+ log_err = str(_LOG_DIR / "daemon.err")
1225
+ plist_dir = Path.home() / "Library" / "LaunchAgents"
1226
+ plist_dir.mkdir(parents=True, exist_ok=True)
1227
+ plist_path = plist_dir / "com.aria.daemon.plist"
1228
+
1229
+ plist = f"""<?xml version="1.0" encoding="UTF-8"?>
1230
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
1231
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1232
+ <plist version="1.0">
1233
+ <dict>
1234
+ <key>Label</key>
1235
+ <string>com.aria.daemon</string>
1236
+ <key>ProgramArguments</key>
1237
+ <array>
1238
+ <string>{python}</string>
1239
+ <string>{script}</string>
1240
+ </array>
1241
+ <key>RunAtLoad</key>
1242
+ <true/>
1243
+ <key>KeepAlive</key>
1244
+ <true/>
1245
+ <key>StandardOutPath</key>
1246
+ <string>{log_out}</string>
1247
+ <key>StandardErrorPath</key>
1248
+ <string>{log_err}</string>
1249
+ <key>EnvironmentVariables</key>
1250
+ <dict>
1251
+ <key>PATH</key>
1252
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:{str(Path(python).parent)}</string>
1253
+ <key>ARIA_CODE_DIR</key>
1254
+ <string>{str(_ARIA_CODE_DIR)}</string>
1255
+ </dict>
1256
+ <key>ThrottleInterval</key>
1257
+ <integer>30</integer>
1258
+ </dict>
1259
+ </plist>
1260
+ """
1261
+ plist_path.write_text(plist)
1262
+ os.system(f"launchctl unload '{plist_path}' 2>/dev/null; launchctl load -w '{plist_path}'")
1263
+ print(f"✅ Aria Daemon installed as LaunchAgent: {plist_path}")
1264
+ print(f" Logs: {log_out}")
1265
+ print(f" To uninstall: python3 {script} --uninstall")
1266
+
1267
+
1268
+ def _uninstall_launchagent() -> None:
1269
+ plist_path = Path.home() / "Library" / "LaunchAgents" / "com.aria.daemon.plist"
1270
+ if plist_path.exists():
1271
+ os.system(f"launchctl unload '{plist_path}' 2>/dev/null")
1272
+ plist_path.unlink()
1273
+ print("✅ Aria Daemon LaunchAgent removed")
1274
+ else:
1275
+ print("ℹ️ LaunchAgent not installed")
1276
+ _remove_pid()
1277
+
1278
+
1279
+ # ── Main ──────────────────────────────────────────────────────────────────────
1280
+
1281
+ async def main() -> None:
1282
+ logger.info("═" * 55)
1283
+ logger.info(" Aria Daemon starting PID=%d", os.getpid())
1284
+ logger.info("═" * 55)
1285
+ _write_pid()
1286
+
1287
+ loop = asyncio.get_event_loop()
1288
+ # Graceful shutdown on SIGINT / SIGTERM
1289
+ for sig in (signal.SIGINT, signal.SIGTERM):
1290
+ loop.add_signal_handler(sig, lambda: asyncio.create_task(_shutdown()))
1291
+
1292
+ tasks = [
1293
+ asyncio.create_task(_alert_watchdog(), name="alert_watchdog"),
1294
+ asyncio.create_task(_webhook_executor(), name="webhook_executor"),
1295
+ asyncio.create_task(_webhook_http_server(), name="webhook_http_server"),
1296
+ asyncio.create_task(_portfolio_loss_watchdog(), name="portfolio_watchdog"),
1297
+ ]
1298
+
1299
+ scheduler = _start_scheduler()
1300
+
1301
+ # Start Telegram bot if token configured
1302
+ tg_token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
1303
+ if tg_token:
1304
+ # Commands are answered by services/aria-bots (aria-runtime daemon for
1305
+ # market data, the Go engine for everything else).
1306
+ from aria_telegram_bot import TelegramBot, handle_command as _telegram_command
1307
+ allowed_raw = os.environ.get("TELEGRAM_ALLOWED_IDS", "")
1308
+ allowed_ids = set(
1309
+ int(x.strip()) for x in allowed_raw.split(",") if x.strip().isdigit()
1310
+ )
1311
+ bot = TelegramBot(token=tg_token, allowed_chat_ids=allowed_ids)
1312
+ me = await bot.get_me()
1313
+ if me:
1314
+ logger.info("Telegram bot: @%s", me.get("username", "?"))
1315
+ await _telegram_push("🤖 *Aria Daemon 已启动*\n发送 `/help` 查看命令。")
1316
+ tasks.append(asyncio.create_task(bot.start(_telegram_command), name="telegram_bot"))
1317
+ else:
1318
+ logger.info("TELEGRAM_BOT_TOKEN not set — Telegram bot disabled")
1319
+
1320
+ # Start Feishu relay client if relay mode configured
1321
+ relay_url = os.environ.get("ARIA_RELAY_URL", "")
1322
+ relay_client_id = os.environ.get("ARIA_RELAY_CLIENT_ID", "")
1323
+ relay_mode = os.environ.get("ARIA_RELAY_MODE", "")
1324
+ if relay_url and relay_client_id and relay_mode == "relay":
1325
+ try:
1326
+ from aria_relay_client import _connect_and_serve as _relay_serve
1327
+ tasks.append(asyncio.create_task(_relay_serve(), name="feishu_relay"))
1328
+ logger.info("Feishu relay client started → %s (client=%s)", relay_url, relay_client_id)
1329
+ except ImportError:
1330
+ logger.warning("aria_relay_client.py not found — Feishu relay disabled")
1331
+
1332
+ logger.info("All workers started. Daemon running.")
1333
+
1334
+ try:
1335
+ await asyncio.gather(*tasks)
1336
+ except asyncio.CancelledError:
1337
+ pass
1338
+ finally:
1339
+ _remove_pid()
1340
+ if scheduler:
1341
+ scheduler.shutdown(wait=False)
1342
+ logger.info("Aria Daemon stopped")
1343
+
1344
+
1345
+ async def _shutdown() -> None:
1346
+ logger.info("Shutdown signal received")
1347
+ for task in asyncio.all_tasks():
1348
+ if task.get_name() not in ("main_task",):
1349
+ task.cancel()
1350
+
1351
+
1352
+ if __name__ == "__main__":
1353
+ if "--install" in sys.argv:
1354
+ _install_launchagent()
1355
+ elif "--uninstall" in sys.argv:
1356
+ _uninstall_launchagent()
1357
+ elif "--status" in sys.argv:
1358
+ pid_file = _ARIA_DIR / "daemon.pid"
1359
+ if pid_file.exists():
1360
+ try:
1361
+ pid = int(pid_file.read_text().strip())
1362
+ os.kill(pid, 0)
1363
+ print(f"✅ Aria Daemon is running (PID {pid})")
1364
+ except ProcessLookupError:
1365
+ print("⚠️ PID file exists but process not running")
1366
+ else:
1367
+ print("⚫ Aria Daemon is not running")
1368
+ else:
1369
+ try:
1370
+ asyncio.run(main())
1371
+ except KeyboardInterrupt:
1372
+ pass