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.
- aria_bots/__init__.py +7 -0
- aria_bots/channels/README.md +97 -0
- aria_bots/channels/__init__.py +1 -0
- aria_bots/channels/approvals.py +263 -0
- aria_bots/channels/conversation.py +280 -0
- aria_bots/channels/digest.py +186 -0
- aria_bots/channels/registry.py +95 -0
- aria_bots/engine.py +144 -0
- aria_bots/feishu_bot.py +1385 -0
- aria_bots/logistics/__init__.py +1 -0
- aria_bots/logistics/carriers.py +253 -0
- aria_bots/logistics/freight.py +154 -0
- aria_bots/logistics/inventory.py +242 -0
- aria_bots/logistics/mcp_server.py +156 -0
- aria_bots/logistics/records.py +119 -0
- aria_bots/logistics/tenancy.py +152 -0
- aria_bots/market.py +91 -0
- aria_bots/relay_client.py +326 -0
- aria_bots/telegram_bot.py +299 -0
- aria_code/__init__.py +111 -0
- aria_code/_bin/aria-native.exe +0 -0
- aria_code/_bin/aria-runtime.exe +0 -0
- aria_code/_version.py +20 -0
- aria_code/account_memory.py +296 -0
- aria_code/account_session.py +166 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +49 -0
- aria_code/agents/base.py +329 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +315 -0
- aria_code/agents/financial/fundamental.py +178 -0
- aria_code/agents/financial/macro.py +111 -0
- aria_code/agents/financial/news.py +231 -0
- aria_code/agents/financial/risk.py +146 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +277 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +392 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +168 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +500 -0
- aria_code/agents/video/__init__.py +16 -0
- aria_code/agents/video/director.py +376 -0
- aria_code/agents/video/qa_agent.py +143 -0
- aria_code/agents/video/script_doctor.py +112 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/navigation.schema.json +110 -0
- aria_code/agents/warehouse/task_agent.py +172 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +9 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/approvals.py +11 -0
- aria_code/apps/channels/conversation.py +11 -0
- aria_code/apps/channels/digest.py +11 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +11 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/app_server.py +656 -0
- aria_code/apps/cli/auto_update.py +125 -0
- aria_code/apps/cli/bootstrap.py +206 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/chat_turn.py +1327 -0
- aria_code/apps/cli/cloud_models.py +231 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +34 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +378 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +215 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
- aria_code/apps/cli/commands/broker_cmds.py +1236 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +189 -0
- aria_code/apps/cli/commands/core_cmds.py +2748 -0
- aria_code/apps/cli/commands/data_cmds.py +521 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/engineering_cmds.py +399 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +544 -0
- aria_code/apps/cli/commands/market_cmds.py +1419 -0
- aria_code/apps/cli/commands/market_context.py +432 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2374 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
- aria_code/apps/cli/commands/report.py +476 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +723 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
- aria_code/apps/cli/commands/workflow_cmds.py +629 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +158 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/conversation_policy.py +53 -0
- aria_code/apps/cli/deterministic.py +200 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/earnings_view.py +172 -0
- aria_code/apps/cli/endpoint_migration.py +64 -0
- aria_code/apps/cli/exec_events.py +255 -0
- aria_code/apps/cli/file_references.py +155 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +239 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
- aria_code/apps/cli/handlers/market_handlers.py +2570 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/headless.py +352 -0
- aria_code/apps/cli/helpers.py +326 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +310 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +305 -0
- aria_code/apps/cli/intent_shadow.py +72 -0
- aria_code/apps/cli/intent_signals.py +347 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +550 -0
- aria_code/apps/cli/main.py +39 -0
- aria_code/apps/cli/market_hours.py +100 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/mixin_binding.py +32 -0
- aria_code/apps/cli/model_catalog.py +652 -0
- aria_code/apps/cli/model_probe.py +169 -0
- aria_code/apps/cli/model_retirement.py +65 -0
- aria_code/apps/cli/native_branding.py +12 -0
- aria_code/apps/cli/native_bridge.py +118 -0
- aria_code/apps/cli/native_entry.py +58 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +78 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +71 -0
- aria_code/apps/cli/prompts/select.py +193 -0
- aria_code/apps/cli/prompts/system_prompts.py +562 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +775 -0
- aria_code/apps/cli/providers/chat_routing.py +139 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
- aria_code/apps/cli/providers/runtime_bridge.py +586 -0
- aria_code/apps/cli/providers/vertexai_stream.py +592 -0
- aria_code/apps/cli/release_download.py +159 -0
- aria_code/apps/cli/review_runner.py +111 -0
- aria_code/apps/cli/runtime_consumer.py +769 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_refresh.py +123 -0
- aria_code/apps/cli/session_store.py +125 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/task_isolation.py +211 -0
- aria_code/apps/cli/terminal_chrome.py +412 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +932 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +113 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +181 -0
- aria_code/apps/cli/tools/market_tools.py +626 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +656 -0
- aria_code/apps/cli/tools/write_tools.py +961 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/transactions.py +269 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +257 -0
- aria_code/apps/cli/updater.py +373 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1663 -0
- aria_code/apps/cli/web_login.py +136 -0
- aria_code/apps/cli/workflow_runner.py +127 -0
- aria_code/apps/cli/workspace_route.py +121 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +5244 -0
- aria_code/aria_daemon.py +1372 -0
- aria_code/aria_feishu_bot.py +16 -0
- aria_code/aria_mcp_server.py +61 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_telegram_bot.py +11 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +16 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1985 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +190 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_server.py +276 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sec_statements.py +217 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/doctor.py +860 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/_finance_core/__init__.py +5 -0
- aria_code/domain/_finance_core/backtest_engine.py +325 -0
- aria_code/domain/_finance_core/performance_stats.py +101 -0
- aria_code/domain/backtest_engine.py +75 -0
- aria_code/domain/backtest_report.py +488 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/performance_stats.py +6 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/behavior.py +113 -0
- aria_code/evals/gap.py +93 -0
- aria_code/evals/harness.py +949 -0
- aria_code/evals/review_probe.py +132 -0
- aria_code/evals/runner.py +425 -0
- aria_code/evals/scorecard.py +322 -0
- aria_code/evals/summary.py +125 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +459 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +744 -0
- aria_code/memory_manager.py +273 -0
- aria_code/model_capability.py +512 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +26 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +58 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +232 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +162 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1705 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +393 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +75 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +304 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +137 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +160 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/_paths.py +92 -0
- aria_code/packages/aria_tools/registry.py +129 -0
- aria_code/packages/quant_engine/__init__.py +33 -0
- aria_code/packages/quant_engine/_namespace.py +24 -0
- aria_code/packages/quant_engine/backtest/__init__.py +10 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +102 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +76 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/healthcare/__init__.py +40 -0
- aria_code/packs/logistics/__init__.py +171 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/google_video_provider.py +256 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/arthera_gateway.py +90 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +137 -0
- aria_code/providers/llm/openai_compat.py +350 -0
- aria_code/providers/llm/registry.py +457 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +549 -0
- aria_code/providers/ollama_models.py +79 -0
- aria_code/providers/openai_params.py +32 -0
- aria_code/providers/tool_messages.py +100 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/review_service.py +497 -0
- aria_code/runtime/__init__.py +176 -0
- aria_code/runtime/acceptance.py +632 -0
- aria_code/runtime/agent_loop.py +1851 -0
- aria_code/runtime/agent_profiles.py +75 -0
- aria_code/runtime/approval.py +71 -0
- aria_code/runtime/baseline.py +219 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +481 -0
- aria_code/runtime/consult.py +65 -0
- aria_code/runtime/contract.py +282 -0
- aria_code/runtime/delivery.py +432 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +167 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/native_index.py +153 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/processes.py +343 -0
- aria_code/runtime/project_graph.py +614 -0
- aria_code/runtime/repo_map.py +712 -0
- aria_code/runtime/research_bundle.py +105 -0
- aria_code/runtime/review.py +269 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +522 -0
- aria_code/runtime/scoped_runs.py +113 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/semantic_diff.py +171 -0
- aria_code/runtime/subagent.py +721 -0
- aria_code/runtime/symbol_edit.py +117 -0
- aria_code/runtime/task_brief.py +46 -0
- aria_code/runtime/task_context.py +145 -0
- aria_code/runtime/task_ledger.py +108 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/task_outcome.py +33 -0
- aria_code/runtime/task_verification.py +266 -0
- aria_code/runtime/task_worktree.py +448 -0
- aria_code/runtime/tool_executor.py +329 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/transactions.py +216 -0
- aria_code/runtime/windows_job.py +115 -0
- aria_code/runtime/workflows.py +222 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +27 -0
- aria_code/safety/client_isolation.py +178 -0
- aria_code/safety/permissions.py +339 -0
- aria_code/safety/risk.py +699 -0
- aria_code/safety/sandbox.py +148 -0
- aria_code/safety/service.py +102 -0
- aria_code/service_endpoints.py +14 -0
- aria_code/setup_wizard.py +684 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +509 -0
- aria_code/tools/data_analysis_tools.py +831 -0
- aria_code/tools/enterprise_finance_tools.py +202 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +880 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3488 -0
- aria_code/tools/logistics_carriers.py +11 -0
- aria_code/tools/logistics_inventory.py +11 -0
- aria_code/tools/logistics_io.py +11 -0
- aria_code/tools/logistics_tenancy.py +11 -0
- aria_code/tools/logistics_tools.py +11 -0
- aria_code/tools/macro_tools.py +412 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +662 -0
- aria_code/tools/spreadsheet_tools.py +399 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/tools/video_tools.py +500 -0
- aria_code/training/__init__.py +1 -0
- aria_code/training/compare.py +117 -0
- aria_code/training/prelude_commands.py +111 -0
- aria_code/training/prelude_eval.py +390 -0
- aria_code/training/prelude_real.py +183 -0
- aria_code/training/prelude_train.py +248 -0
- aria_code/training/sft_dataset.py +230 -0
- aria_code/training/trajectory_dataset.py +263 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +404 -0
- aria_code/ui/completer.py +571 -0
- aria_code/ui/console.py +285 -0
- aria_code/ui/input_box.py +505 -0
- aria_code/ui/mentions.py +206 -0
- aria_code/ui/picker.py +248 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/actions.py +297 -0
- aria_code/ui/render/finance.py +1732 -0
- aria_code/ui/render/market.py +253 -0
- aria_code/ui/render/output.py +954 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +367 -0
- aria_code/ui/robot.py +239 -0
- aria_code/ui/startup_dashboard.py +110 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +518 -0
- aria_code/video_workflow/__init__.py +8 -0
- aria_code/video_workflow/__main__.py +61 -0
- aria_code/video_workflow/engines/__init__.py +18 -0
- aria_code/video_workflow/engines/base.py +69 -0
- aria_code/video_workflow/engines/custom_model.py +204 -0
- aria_code/video_workflow/engines/google_veo.py +94 -0
- aria_code/video_workflow/engines/local_engine.py +71 -0
- aria_code/video_workflow/engines/registry.py +98 -0
- aria_code/video_workflow/google_models.py +278 -0
- aria_code/video_workflow/music/__init__.py +5 -0
- aria_code/video_workflow/music/foley_service.py +132 -0
- aria_code/video_workflow/music/service.py +212 -0
- aria_code/video_workflow/pipeline.py +620 -0
- aria_code/video_workflow/plan.py +202 -0
- aria_code/video_workflow/session.py +297 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +219 -0
- aria_code/workspace/verify.py +167 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.167.0.dist-info/METADATA +330 -0
- aria_code-0.167.0.dist-info/RECORD +601 -0
- aria_code-0.167.0.dist-info/WHEEL +5 -0
- aria_code-0.167.0.dist-info/entry_points.txt +5 -0
- aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.167.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,2374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModelCommandsMixin — Model/config commands: model, apikey, providers, cloud, config, tools, skills.
|
|
3
|
+
|
|
4
|
+
Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
|
|
5
|
+
by _rebind_mixin_globals() called at module load time.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from aria_code.apps.cli.i18n import ui_text
|
|
10
|
+
from aria_code.packages.aria_core.paths import aria_home
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import json
|
|
15
|
+
import asyncio
|
|
16
|
+
import datetime
|
|
17
|
+
import time
|
|
18
|
+
import shlex
|
|
19
|
+
import sys
|
|
20
|
+
import os
|
|
21
|
+
from typing import Dict, Any, Optional
|
|
22
|
+
|
|
23
|
+
# Import path → the name you actually type after `pip install`. Namespace
|
|
24
|
+
# packages make these differ, and guessing from the module name produces
|
|
25
|
+
# advice that fails: `google.genai` is shipped as `google-genai`, so the
|
|
26
|
+
# obvious `pip install google` installs an unrelated stub.
|
|
27
|
+
_PIP_NAMES = {
|
|
28
|
+
"google.genai": "google-genai",
|
|
29
|
+
"google.generativeai": "google-generativeai",
|
|
30
|
+
"google.cloud": "google-cloud",
|
|
31
|
+
"anthropic": "anthropic",
|
|
32
|
+
"openai": "openai",
|
|
33
|
+
"mistralai": "mistralai",
|
|
34
|
+
"cohere": "cohere",
|
|
35
|
+
"yfinance": "yfinance",
|
|
36
|
+
"akshare": "akshare",
|
|
37
|
+
"ccxt": "ccxt",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _pip_name(module: str) -> str:
|
|
42
|
+
"""Best installable name for *module*, longest known prefix first."""
|
|
43
|
+
if module in _PIP_NAMES:
|
|
44
|
+
return _PIP_NAMES[module]
|
|
45
|
+
for known, package in sorted(_PIP_NAMES.items(), key=lambda kv: -len(kv[0])):
|
|
46
|
+
if module.startswith(known + "."):
|
|
47
|
+
return package
|
|
48
|
+
# Unknown: the top-level name is the best available guess, and it is right
|
|
49
|
+
# for the ordinary single-package case.
|
|
50
|
+
return module.split(".")[0].replace("_", "-")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _actionable(provider: str, error: str) -> str:
|
|
54
|
+
"""Turn a probe failure into something the user can act on.
|
|
55
|
+
|
|
56
|
+
The picker used to show ``ollama_err[:40]``, which for the most common
|
|
57
|
+
failure produced ``[ollama: <urlopen error [Errno 61] Connection ref]`` —
|
|
58
|
+
a raw Python exception, truncated mid-word, naming neither the cause nor
|
|
59
|
+
the remedy. A connection refused on the local runtime port has exactly one
|
|
60
|
+
meaning and one fix, so say them.
|
|
61
|
+
"""
|
|
62
|
+
text = str(error or "")
|
|
63
|
+
low = text.lower()
|
|
64
|
+
if "errno 61" in low or "connection refused" in low:
|
|
65
|
+
return "未运行 · 先执行 ollama serve" if provider == "ollama" else f"{provider} 未运行"
|
|
66
|
+
if "timed out" in low or "timeout" in low:
|
|
67
|
+
return "连接超时 · 检查服务地址与网络"
|
|
68
|
+
if "name or service not known" in low or "nodename nor servname" in low:
|
|
69
|
+
return "地址无法解析 · 检查 ollama_url 配置"
|
|
70
|
+
if "no module named" in low:
|
|
71
|
+
module = text.split("'")[1] if "'" in text else text.strip()
|
|
72
|
+
return f"缺少 {module} · pip install {_pip_name(module)}"
|
|
73
|
+
collapsed = " ".join(text.split())
|
|
74
|
+
return collapsed if len(collapsed) <= 48 else collapsed[:47] + "…"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def detect_ollama_models_rich(*args, **kwargs):
|
|
78
|
+
from aria_code.apps.cli.helpers import detect_ollama_models_rich as fn
|
|
79
|
+
return fn(*args, **kwargs)
|
|
80
|
+
def _sync_write_policy(*args, **kwargs):
|
|
81
|
+
from aria_code.aria_cli import _sync_write_policy as fn
|
|
82
|
+
return fn(*args, **kwargs)
|
|
83
|
+
def _load_providers_json(*args, **kwargs):
|
|
84
|
+
from aria_code.aria_cli import _load_providers_json as fn
|
|
85
|
+
return fn(*args, **kwargs)
|
|
86
|
+
def _get__PROVIDER_KEY_MAP():
|
|
87
|
+
from aria_code.aria_cli import _PROVIDER_KEY_MAP as val
|
|
88
|
+
return val
|
|
89
|
+
def _get__LLM_SIGNUP_URLS():
|
|
90
|
+
from aria_code.aria_cli import _LLM_SIGNUP_URLS as val
|
|
91
|
+
return val
|
|
92
|
+
def _get__PROVIDER_BASE_URLS():
|
|
93
|
+
from aria_code.aria_cli import _PROVIDER_BASE_URLS as val
|
|
94
|
+
return val
|
|
95
|
+
def _get_ARIA_TOOLS():
|
|
96
|
+
# Owned by apps/cli/tool_registry.py; aria_cli fills it in place.
|
|
97
|
+
from ..tool_registry import ARIA_TOOLS as val
|
|
98
|
+
return val
|
|
99
|
+
def _get_THINKING_MODES():
|
|
100
|
+
from aria_code.aria_cli import THINKING_MODES as val
|
|
101
|
+
return val
|
|
102
|
+
def _get__PROVIDER_GUIDE():
|
|
103
|
+
from aria_code.aria_cli import _PROVIDER_GUIDE as val
|
|
104
|
+
return val
|
|
105
|
+
def _get__DATA_KEY_MAP():
|
|
106
|
+
from aria_code.aria_cli import _DATA_KEY_MAP as val
|
|
107
|
+
return val
|
|
108
|
+
def _run_picker_in_thread(*args, **kwargs):
|
|
109
|
+
from aria_code.aria_cli import _run_picker_in_thread as fn
|
|
110
|
+
return fn(*args, **kwargs)
|
|
111
|
+
import pathlib
|
|
112
|
+
def _get_MODEL_ALIASES():
|
|
113
|
+
from aria_code.aria_cli import MODEL_ALIASES as val
|
|
114
|
+
return val
|
|
115
|
+
def _save_providers_json(*args, **kwargs):
|
|
116
|
+
from aria_code.aria_cli import _save_providers_json as fn
|
|
117
|
+
return fn(*args, **kwargs)
|
|
118
|
+
def _test_api_key(*args, **kwargs):
|
|
119
|
+
from aria_code.aria_cli import _test_api_key as fn
|
|
120
|
+
return fn(*args, **kwargs)
|
|
121
|
+
def _get_provider_key(*args, **kwargs):
|
|
122
|
+
from aria_code.apps.cli.provider_keys import get_provider_key as fn
|
|
123
|
+
return fn(*args, **kwargs)
|
|
124
|
+
def _get__DATA_SIGNUP_URLS():
|
|
125
|
+
from aria_code.aria_cli import _DATA_SIGNUP_URLS as val
|
|
126
|
+
return val
|
|
127
|
+
def _arrow_select(*args, **kwargs):
|
|
128
|
+
# Re-exported by aria_cli; taken from its own module here.
|
|
129
|
+
from aria_code.ui.picker import arrow_select as fn
|
|
130
|
+
return fn(*args, **kwargs)
|
|
131
|
+
def _get_SKILLS():
|
|
132
|
+
from aria_code.apps.cli.skills_catalog import SKILLS as val
|
|
133
|
+
return val
|
|
134
|
+
def _get_LOCAL_TOOLS():
|
|
135
|
+
# Owned by apps/cli/tool_registry.py; aria_cli fills it in place.
|
|
136
|
+
from ..tool_registry import LOCAL_TOOLS as val
|
|
137
|
+
return val
|
|
138
|
+
def get_model_capability(*args, **kwargs):
|
|
139
|
+
from aria_code.apps.cli._optional import get_model_capability as fn
|
|
140
|
+
return fn(*args, **kwargs)
|
|
141
|
+
def _save_data_key(*args, **kwargs):
|
|
142
|
+
from aria_code.aria_cli import _save_data_key as fn
|
|
143
|
+
return fn(*args, **kwargs)
|
|
144
|
+
def _get_MODELS():
|
|
145
|
+
from aria_code.apps.cli.model_catalog import MODELS as val
|
|
146
|
+
return val
|
|
147
|
+
def load_config(*args, **kwargs):
|
|
148
|
+
from aria_code.aria_cli import load_config as fn
|
|
149
|
+
return fn(*args, **kwargs)
|
|
150
|
+
def _null_ctx(*args, **kwargs):
|
|
151
|
+
from ._ui import null_ctx as fn
|
|
152
|
+
return fn(*args, **kwargs)
|
|
153
|
+
def _get__PROVIDER_DESC():
|
|
154
|
+
from aria_code.aria_cli import _PROVIDER_DESC as val
|
|
155
|
+
return val
|
|
156
|
+
def _get_PROVIDERS_FILE():
|
|
157
|
+
from aria_code.aria_cli import PROVIDERS_FILE as val
|
|
158
|
+
return val
|
|
159
|
+
def _get__HAS_MODEL_CAP():
|
|
160
|
+
from aria_code.apps.cli._optional import HAS_MODEL_CAP as val
|
|
161
|
+
return val
|
|
162
|
+
def _load_data_keys(*args, **kwargs):
|
|
163
|
+
from aria_code.apps.cli.provider_keys import load_data_keys as fn
|
|
164
|
+
return fn(*args, **kwargs)
|
|
165
|
+
|
|
166
|
+
import json
|
|
167
|
+
import asyncio
|
|
168
|
+
import datetime
|
|
169
|
+
import time
|
|
170
|
+
import shlex
|
|
171
|
+
import sys
|
|
172
|
+
import os
|
|
173
|
+
from typing import Dict, Any, Optional
|
|
174
|
+
|
|
175
|
+
# A real logger. This was a `def logger(*args, **kwargs)` forwarding to
|
|
176
|
+
# aria_cli, so every `logger.debug(...)` raised AttributeError — and all but
|
|
177
|
+
# one of them sit in an except block, where the failure replaced whatever
|
|
178
|
+
# error was being reported.
|
|
179
|
+
logger = logging.getLogger(__name__)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _installed_catalogue(rich_models):
|
|
183
|
+
"""Keep cloud routes separate; local catalogue entries need a real install."""
|
|
184
|
+
from aria_code.apps.cli.model_catalog import is_provider_qualified
|
|
185
|
+
installed = {m["name"] for m in rich_models}
|
|
186
|
+
return {k: m for k, m in _get_MODELS().items()
|
|
187
|
+
if (is_provider_qualified(m["id"]) and not m["id"].startswith("ollama/"))
|
|
188
|
+
or m["id"] in installed}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ModelCommandsMixin:
|
|
192
|
+
"""Mixin: Model/config commands: model, apikey, providers, cloud, config, tools, skills."""
|
|
193
|
+
|
|
194
|
+
def _listed_model_ids(self) -> list:
|
|
195
|
+
"""The ids behind the numbers /models prints, in the order it prints them.
|
|
196
|
+
|
|
197
|
+
The number is the most prominent thing on each row, so it has to mean
|
|
198
|
+
something; the printer and this must therefore read the same order
|
|
199
|
+
from the same place. Community models are keyed by name, which is
|
|
200
|
+
their id.
|
|
201
|
+
"""
|
|
202
|
+
if hasattr(self, "_last_listed_models"):
|
|
203
|
+
return list(self._last_listed_models)
|
|
204
|
+
ids = [m["id"] for m in _get_MODELS().values() if "/" in m["id"]]
|
|
205
|
+
for name in getattr(self, "_last_listed_community", []) or []:
|
|
206
|
+
if name not in ids:
|
|
207
|
+
ids.append(name)
|
|
208
|
+
return ids
|
|
209
|
+
|
|
210
|
+
async def cmd_model(self, args: str):
|
|
211
|
+
name = args.strip()
|
|
212
|
+
|
|
213
|
+
# /models prints a numbered list, so a number is the obvious thing to
|
|
214
|
+
# type back. It used to be taken as a model name and matched nothing,
|
|
215
|
+
# which left the numbers on screen decorative.
|
|
216
|
+
if name.isdigit():
|
|
217
|
+
listed = self._listed_model_ids()
|
|
218
|
+
index = int(name)
|
|
219
|
+
if 1 <= index <= len(listed):
|
|
220
|
+
name = listed[index - 1]
|
|
221
|
+
else:
|
|
222
|
+
msg = ui_text(self, f"没有第 {index} 个模型;用 /models 查看当前列表。",
|
|
223
|
+
f"There is no model {index}; run /models for the current list.")
|
|
224
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# A model Google has retired would fail on the next request; switch
|
|
228
|
+
# to its replacement and say so (model_retirement.py).
|
|
229
|
+
from aria_code.apps.cli.model_retirement import retired_replacement
|
|
230
|
+
|
|
231
|
+
_replacement = retired_replacement(name)
|
|
232
|
+
if _replacement:
|
|
233
|
+
msg = ui_text(self, f"ℹ {name} 已被 Google 停用,改用 {_replacement}。",
|
|
234
|
+
f"ℹ {name} has been retired by Google; using {_replacement}.")
|
|
235
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
236
|
+
name = _replacement
|
|
237
|
+
|
|
238
|
+
# ── "provider/model" format (Open Interpreter style) ─────────────────
|
|
239
|
+
# Examples: /model deepseek/deepseek-chat /model ollama/qwen2.5:7b
|
|
240
|
+
# /model openai/gpt-4.5 /model openai/o3 /model openai/o4-mini
|
|
241
|
+
if "/" in name and not name.startswith("http"):
|
|
242
|
+
_prov, _mod = name.split("/", 1)
|
|
243
|
+
from aria_code.apps.cli.providers.chat_routing import normalize_provider_name
|
|
244
|
+
|
|
245
|
+
_prov = normalize_provider_name(_prov)
|
|
246
|
+
_mod = _mod.strip()
|
|
247
|
+
_local_backends = {"ollama", "lmstudio", "vllm", "llamacpp", "jan", "custom"}
|
|
248
|
+
if _prov not in _local_backends:
|
|
249
|
+
# Cloud provider — check API key. Google models also run on
|
|
250
|
+
# Vertex AI with Google Cloud credentials and no key; this
|
|
251
|
+
# refused every `/model google/…` without a Gemini API key,
|
|
252
|
+
# so a team running on Google Cloud could not pick Gemini 3
|
|
253
|
+
# or Gemma at all.
|
|
254
|
+
from aria_code.apps.cli.providers.base import google_cloud_available
|
|
255
|
+
_key = _get_provider_key(_prov)
|
|
256
|
+
_vertex_ok = _prov in ("google", "gemini", "vertexai") and google_cloud_available()
|
|
257
|
+
if not _key and not _vertex_ok:
|
|
258
|
+
if _prov in ("google", "gemini", "vertexai"):
|
|
259
|
+
msg = ui_text(self,
|
|
260
|
+
f"⚠ 需要 Google Cloud 凭据或 API key:运行 gcloud auth application-default login,"
|
|
261
|
+
f"或 /apikey set {_prov} <key>",
|
|
262
|
+
f"⚠ Needs Google Cloud credentials or an API key: run "
|
|
263
|
+
f"gcloud auth application-default login, or /apikey set {_prov} <key>")
|
|
264
|
+
else:
|
|
265
|
+
msg = (f"⚠ {_prov} API key 未配置。"
|
|
266
|
+
f"运行: /apikey set {_prov} <key>")
|
|
267
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
268
|
+
return
|
|
269
|
+
if _prov == "ollama":
|
|
270
|
+
try:
|
|
271
|
+
from aria_code.providers.ollama_models import ollama_base_url, resolve_model_async
|
|
272
|
+
_mod = await resolve_model_async(ollama_base_url(self.terminal.config), _mod)
|
|
273
|
+
except ValueError as exc:
|
|
274
|
+
self.context.console.print(str(exc))
|
|
275
|
+
return
|
|
276
|
+
self.terminal.config["local_provider"] = _prov
|
|
277
|
+
self.terminal.config["model"] = _mod
|
|
278
|
+
self.terminal.config["local_mode"] = _prov in _local_backends
|
|
279
|
+
# /model is an explicit provider selection. A legacy
|
|
280
|
+
# backend_chat=true value must not silently override it and route
|
|
281
|
+
# the next turn through Aria SSE instead of the selected runtime.
|
|
282
|
+
self.terminal.config["backend_chat"] = False
|
|
283
|
+
self.context.save_config(self.terminal.config)
|
|
284
|
+
msg = f"✓ 已切换到 {_prov}/{_mod}"
|
|
285
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Direct selection by number: /model 1 /model 2 … (Codex style)
|
|
289
|
+
if name.isdigit():
|
|
290
|
+
idx = int(name) - 1
|
|
291
|
+
keys = list(_get_MODELS().keys())
|
|
292
|
+
if 0 <= idx < len(keys):
|
|
293
|
+
self._set_model(keys[idx])
|
|
294
|
+
else:
|
|
295
|
+
self.context.console.print(f"[dim]No model #{name}[/dim]" if self.context.has_rich else f"No model #{name}")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Direct selection by key (case-insensitive): /model qwen7b
|
|
299
|
+
if name.lower() in _get_MODELS():
|
|
300
|
+
self._set_model(name.lower())
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Direct selection by alias: /model st / s / p / coder
|
|
304
|
+
if name.lower() in _get_MODEL_ALIASES():
|
|
305
|
+
self._set_model(_get_MODEL_ALIASES()[name.lower()])
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Direct selection by full Ollama model ID: /model qwen2.5-coder:1.5b
|
|
309
|
+
if name and ":" in name:
|
|
310
|
+
self._set_model_by_id(name, provider="ollama")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# ── Interactive picker (Codex style: numbered list + descriptions) ────
|
|
314
|
+
from aria_code.providers.ollama_models import ollama_base_url
|
|
315
|
+
ollama_url = ollama_base_url(self.terminal.config)
|
|
316
|
+
current_provider = str(
|
|
317
|
+
self.terminal.config.get("local_provider") or "ollama"
|
|
318
|
+
).lower()
|
|
319
|
+
current_id = self.terminal.config.get("model", "qwen2.5:7b")
|
|
320
|
+
try:
|
|
321
|
+
from aria_code.apps.cli.i18n import t as _i18nt
|
|
322
|
+
_lang = self.terminal.config.get("ui_lang", "en") or "en"
|
|
323
|
+
_i18n = lambda k: _i18nt(k, lang=_lang)
|
|
324
|
+
except Exception:
|
|
325
|
+
_lang = "en"
|
|
326
|
+
_i18n = lambda k: k
|
|
327
|
+
|
|
328
|
+
_local_backends = {"ollama", "lmstudio", "vllm", "llamacpp", "jan", "custom"}
|
|
329
|
+
if current_provider == "ollama":
|
|
330
|
+
rich_models, ollama_err = detect_ollama_models_rich(ollama_url)
|
|
331
|
+
elif current_provider in _local_backends:
|
|
332
|
+
try:
|
|
333
|
+
from local_llm_provider import LocalLLMProvider
|
|
334
|
+
|
|
335
|
+
_runtime = LocalLLMProvider.from_config(self.terminal.config)
|
|
336
|
+
_runtime_models = _runtime.list_models()
|
|
337
|
+
rich_models = [
|
|
338
|
+
{
|
|
339
|
+
"name": model_name,
|
|
340
|
+
"size_label": "",
|
|
341
|
+
"family": "",
|
|
342
|
+
"quant": "",
|
|
343
|
+
"execution": "local",
|
|
344
|
+
"remote_model": "",
|
|
345
|
+
"remote_host": "",
|
|
346
|
+
"context_window": 0,
|
|
347
|
+
"capabilities": [],
|
|
348
|
+
}
|
|
349
|
+
for model_name in _runtime_models
|
|
350
|
+
]
|
|
351
|
+
ollama_err = None if _runtime.is_available() else f"{current_provider} unavailable"
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
rich_models, ollama_err = [], str(exc)
|
|
354
|
+
else:
|
|
355
|
+
# Cloud catalog APIs vary widely and can be expensive. Keep the
|
|
356
|
+
# selected model visible and report credential readiness; users can
|
|
357
|
+
# switch directly with /model provider/model.
|
|
358
|
+
if _get_provider_key(current_provider):
|
|
359
|
+
rich_models = [{
|
|
360
|
+
"name": current_id,
|
|
361
|
+
"size_label": "",
|
|
362
|
+
"family": "",
|
|
363
|
+
"quant": "",
|
|
364
|
+
"execution": "remote",
|
|
365
|
+
"remote_model": current_id,
|
|
366
|
+
"remote_host": current_provider,
|
|
367
|
+
"context_window": 0,
|
|
368
|
+
"capabilities": [],
|
|
369
|
+
}]
|
|
370
|
+
ollama_err = None
|
|
371
|
+
else:
|
|
372
|
+
rich_models = []
|
|
373
|
+
ollama_err = f"API key missing · /apikey set {current_provider} <key>"
|
|
374
|
+
if current_provider != "ollama":
|
|
375
|
+
# Local catalogue entries are shown only when installed, whatever
|
|
376
|
+
# the current provider, so a cloud user can still switch to one.
|
|
377
|
+
try:
|
|
378
|
+
local_models, _ = detect_ollama_models_rich(ollama_url)
|
|
379
|
+
except Exception:
|
|
380
|
+
local_models = []
|
|
381
|
+
seen = {m["name"] for m in rich_models}
|
|
382
|
+
rich_models = rich_models + [m for m in local_models or [] if m.get("name") not in seen]
|
|
383
|
+
display_models = _installed_catalogue(rich_models)
|
|
384
|
+
installed_names = {m["name"] for m in rich_models}
|
|
385
|
+
aria_ids = {m["id"] for m in display_models.values()}
|
|
386
|
+
runtime_by_id = {m["name"]: m for m in rich_models}
|
|
387
|
+
|
|
388
|
+
# ── Build picker title (one line, shown inside arrow_select header) ──
|
|
389
|
+
_sel_model = _i18n("select_model")
|
|
390
|
+
_installed = _i18n("installed")
|
|
391
|
+
if ollama_err:
|
|
392
|
+
_picker_title = f"{_sel_model} [{current_provider}: {_actionable(current_provider, ollama_err)}]"
|
|
393
|
+
else:
|
|
394
|
+
n_local = sum(1 for m in rich_models if m.get("execution") != "remote")
|
|
395
|
+
n_remote = len(rich_models) - n_local
|
|
396
|
+
if current_provider == "ollama":
|
|
397
|
+
_picker_title = (
|
|
398
|
+
f"{_sel_model} Ollama {len(rich_models)} ready"
|
|
399
|
+
f" · {n_local} local · {n_remote} cloud"
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
_picker_title = (
|
|
403
|
+
f"{_sel_model} {current_provider} {len(rich_models)} ready"
|
|
404
|
+
" · /model <provider>/<model>"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _status_tag(mid: str, badge: str) -> str:
|
|
408
|
+
"""Return runtime status, not a hard-coded catalogue badge."""
|
|
409
|
+
runtime = runtime_by_id.get(mid)
|
|
410
|
+
if runtime and runtime.get("execution") == "remote":
|
|
411
|
+
return "☁"
|
|
412
|
+
return "●" if runtime else "○"
|
|
413
|
+
|
|
414
|
+
# Get terminal width for safe label truncation
|
|
415
|
+
try:
|
|
416
|
+
_term_cols = os.get_terminal_size().columns
|
|
417
|
+
except Exception:
|
|
418
|
+
_term_cols = 80
|
|
419
|
+
if getattr(self.terminal, "_native_select", None) is not None:
|
|
420
|
+
_term_cols = 110 # the host's menu is about this wide; not a TTY to measure
|
|
421
|
+
|
|
422
|
+
def _cjk_width(s: str) -> int:
|
|
423
|
+
"""Display-column width (CJK = 2 cols each)."""
|
|
424
|
+
w = 0
|
|
425
|
+
for ch in s:
|
|
426
|
+
cp = ord(ch)
|
|
427
|
+
w += 2 if (0x2E80 <= cp <= 0xA4CF or 0xAC00 <= cp <= 0xD7AF or
|
|
428
|
+
0xFF01 <= cp <= 0xFF60 or 0x3000 <= cp <= 0x303F) else 1
|
|
429
|
+
return w
|
|
430
|
+
|
|
431
|
+
def _cjk_truncate(s: str, max_cols: int) -> str:
|
|
432
|
+
"""Truncate s so its display width ≤ max_cols, adding … if cut."""
|
|
433
|
+
w, out = 0, ""
|
|
434
|
+
for ch in s:
|
|
435
|
+
cw = 2 if (0x2E80 <= ord(ch) <= 0xA4CF or
|
|
436
|
+
0xAC00 <= ord(ch) <= 0xD7AF or
|
|
437
|
+
0xFF01 <= ord(ch) <= 0xFF60 or
|
|
438
|
+
0x3000 <= ord(ch) <= 0x303F) else 1
|
|
439
|
+
if w + cw > max_cols:
|
|
440
|
+
return out + "…"
|
|
441
|
+
out += ch
|
|
442
|
+
w += cw
|
|
443
|
+
return out
|
|
444
|
+
|
|
445
|
+
def _short_desc(m: dict) -> str:
|
|
446
|
+
"""Single-line description with right-aligned meta tags."""
|
|
447
|
+
desc = m.get("description", "")
|
|
448
|
+
badge = m.get("badge", "")
|
|
449
|
+
extras = []
|
|
450
|
+
runtime = runtime_by_id.get(m["id"], {})
|
|
451
|
+
runtime_ctx = int(runtime.get("context_window") or 0)
|
|
452
|
+
runtime_caps = set(runtime.get("capabilities") or [])
|
|
453
|
+
if runtime_ctx:
|
|
454
|
+
extras.append(f"ctx={runtime_ctx//1024}K")
|
|
455
|
+
if "tools" in runtime_caps: extras.append("tools")
|
|
456
|
+
if "thinking" in runtime_caps: extras.append("think")
|
|
457
|
+
elif _get__HAS_MODEL_CAP():
|
|
458
|
+
cap = get_model_capability(m["id"])
|
|
459
|
+
extras.append(f"ctx={cap.context_window//1024}K")
|
|
460
|
+
if cap.tool_calls: extras.append("tools")
|
|
461
|
+
if cap.thinking: extras.append("think")
|
|
462
|
+
else:
|
|
463
|
+
extras.append(f"ctx={m.get('num_ctx', 8192)//1024}K")
|
|
464
|
+
if runtime.get("execution") == "remote":
|
|
465
|
+
extras.insert(0, "Cloud")
|
|
466
|
+
elif m["id"] in installed_names:
|
|
467
|
+
extras.insert(0, "Local")
|
|
468
|
+
elif badge in ("Fast", "Code", "Think"):
|
|
469
|
+
extras.insert(0, badge)
|
|
470
|
+
meta = " " + " · ".join(extras) if extras else ""
|
|
471
|
+
# prefix " N. ☁ ModelName " ≈ 24 cols; give description 60% of remaining
|
|
472
|
+
_prefix_cols = 24
|
|
473
|
+
_avail = max(30, _term_cols - _prefix_cols - len(meta) - 2)
|
|
474
|
+
_desc_budget = max(20, _avail * 3 // 4) # 75% of available → description
|
|
475
|
+
desc = _cjk_truncate(desc, _desc_budget)
|
|
476
|
+
return f"{desc}{meta}"
|
|
477
|
+
|
|
478
|
+
# Build option list (Codex: numbered, no separators within Aria section)
|
|
479
|
+
options: list = [] # (label_str, desc_str) for _arrow_select
|
|
480
|
+
all_ids: list = []
|
|
481
|
+
|
|
482
|
+
# ── Native frontend: pick in the host's menu, not from a printed list ─
|
|
483
|
+
# Arrow keys and Enter, the way Claude and ChatGPT do it. The list used
|
|
484
|
+
# to be printed and the user had to type a number back.
|
|
485
|
+
native_select = getattr(self.terminal, "_native_select", None)
|
|
486
|
+
if native_select is not None and not name:
|
|
487
|
+
await self._pick_model_in_host(
|
|
488
|
+
native_select, picker_title=_picker_title, current_id=current_id,
|
|
489
|
+
current_provider=current_provider, rich_models=rich_models,
|
|
490
|
+
aria_ids=aria_ids, status_tag=_status_tag, short_desc=_short_desc,
|
|
491
|
+
lang=_lang,
|
|
492
|
+
)
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# ── Print numbered list only in non-interactive (-p) mode ────────────
|
|
496
|
+
# In interactive TTY mode the arrow picker below already shows all items.
|
|
497
|
+
# Printing twice causes the visual duplication seen in the session log.
|
|
498
|
+
_is_tty = sys.stdin.isatty()
|
|
499
|
+
idx_counter = 1
|
|
500
|
+
if not _is_tty:
|
|
501
|
+
# Non-interactive (-p mode): show static numbered list then return.
|
|
502
|
+
# The arrow picker cannot run without a TTY.
|
|
503
|
+
community_list = [cm for cm in rich_models if cm["name"] not in aria_ids]
|
|
504
|
+
# What /model <number> will count through, recorded as it is printed.
|
|
505
|
+
self._last_listed_community = [cm["name"] for cm in community_list]
|
|
506
|
+
self._last_listed_models = [m["id"] for m in display_models.values()] + self._last_listed_community
|
|
507
|
+
for key, m in display_models.items():
|
|
508
|
+
mid = m["id"]
|
|
509
|
+
is_cur = mid == current_id
|
|
510
|
+
status = _status_tag(mid, m.get("badge", ""))
|
|
511
|
+
cur_tag = " (current)" if is_cur else ""
|
|
512
|
+
desc = _short_desc(m)
|
|
513
|
+
line = f" {idx_counter}. {status} {m['name']:<14s} {desc}{cur_tag}"
|
|
514
|
+
self.context.console.print(line) if self.context.has_rich else print(line)
|
|
515
|
+
idx_counter += 1
|
|
516
|
+
if community_list:
|
|
517
|
+
self.context.console.print() if self.context.has_rich else print()
|
|
518
|
+
lbl = " Community (Ollama)"
|
|
519
|
+
self.context.console.print(f"[dim]{lbl}[/dim]") if self.context.has_rich else print(lbl)
|
|
520
|
+
for cm in community_list:
|
|
521
|
+
mid = cm["name"]
|
|
522
|
+
is_cur = mid == current_id
|
|
523
|
+
cur_tag = " (current)" if is_cur else ""
|
|
524
|
+
line = f" {idx_counter}. ● {mid}{cur_tag}"
|
|
525
|
+
self.context.console.print(line) if self.context.has_rich else print(line)
|
|
526
|
+
idx_counter += 1
|
|
527
|
+
self.context.console.print() if self.context.has_rich else print()
|
|
528
|
+
hint = "Use /model <number> from this list, or /model <id>. E.g. /model 1 or /model deepseek/deepseek-chat"
|
|
529
|
+
self.context.console.print(f" [dim]{hint}[/dim]") if self.context.has_rich else print(f" {hint}")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
# ── Build compact options for _arrow_select ────────────────────────
|
|
533
|
+
# In TTY mode: include short description (static list is suppressed above).
|
|
534
|
+
# In non-TTY: descriptions already shown in static list, keep labels short.
|
|
535
|
+
num = 1
|
|
536
|
+
picker_models = (
|
|
537
|
+
list(display_models.values())
|
|
538
|
+
if current_provider == "ollama"
|
|
539
|
+
else [
|
|
540
|
+
{
|
|
541
|
+
"id": item["name"], "name": item["name"],
|
|
542
|
+
"description": (
|
|
543
|
+
"可用模型" if str(_lang).lower().startswith("zh")
|
|
544
|
+
else "available model"
|
|
545
|
+
),
|
|
546
|
+
"num_ctx": item.get("context_window") or 8192,
|
|
547
|
+
}
|
|
548
|
+
for item in rich_models
|
|
549
|
+
]
|
|
550
|
+
)
|
|
551
|
+
for m in picker_models:
|
|
552
|
+
mid = m["id"]
|
|
553
|
+
status = _status_tag(mid, m.get("badge", ""))
|
|
554
|
+
is_cur = " ✓" if mid == current_id else ""
|
|
555
|
+
if _is_tty:
|
|
556
|
+
desc_part = f" {_short_desc(m)}"
|
|
557
|
+
else:
|
|
558
|
+
desc_part = ""
|
|
559
|
+
label = f" {num}. {status} {m['name']}{is_cur}{desc_part}"
|
|
560
|
+
options.append((label, ""))
|
|
561
|
+
all_ids.append(mid)
|
|
562
|
+
num += 1
|
|
563
|
+
|
|
564
|
+
community = (
|
|
565
|
+
[cm for cm in rich_models if cm["name"] not in aria_ids]
|
|
566
|
+
if current_provider == "ollama" else []
|
|
567
|
+
)
|
|
568
|
+
if community:
|
|
569
|
+
_comm_label = _i18n("community_models")
|
|
570
|
+
options.append((f" ── {_comm_label} ──", ""))
|
|
571
|
+
all_ids.append(None)
|
|
572
|
+
for cm in community:
|
|
573
|
+
mid = cm["name"]
|
|
574
|
+
is_cur = " ◀" if mid == current_id else ""
|
|
575
|
+
options.append((f" {num}. ● {mid}{is_cur}", ""))
|
|
576
|
+
all_ids.append(mid)
|
|
577
|
+
num += 1
|
|
578
|
+
|
|
579
|
+
if ollama_err and not rich_models:
|
|
580
|
+
_unreach = _i18n("ollama_unreachable")
|
|
581
|
+
options.append((f" ── {_unreach} ──────────", ""))
|
|
582
|
+
all_ids.append(None)
|
|
583
|
+
|
|
584
|
+
if not any(model_id is not None for model_id in all_ids):
|
|
585
|
+
message = (
|
|
586
|
+
f"{current_provider}: "
|
|
587
|
+
f"{_actionable(current_provider, ollama_err) if ollama_err else 'no models available'}"
|
|
588
|
+
)
|
|
589
|
+
self.context.console.print(f"[yellow]{message}[/yellow]") if self.context.has_rich else print(message)
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
# ── Run thread-based arrow picker (short labels = no line wrap) ────
|
|
593
|
+
current_idx = next((i for i, mid in enumerate(all_ids) if mid == current_id), 0)
|
|
594
|
+
|
|
595
|
+
while True:
|
|
596
|
+
_controls_hint = (
|
|
597
|
+
"↑↓ Enter Esc/q 取消"
|
|
598
|
+
if str(_lang).lower().startswith("zh")
|
|
599
|
+
else "↑↓ Enter Esc/q Cancel"
|
|
600
|
+
)
|
|
601
|
+
choice = await _run_picker_in_thread(
|
|
602
|
+
options, current_idx,
|
|
603
|
+
_picker_title,
|
|
604
|
+
max_visible=min(9, len(options)),
|
|
605
|
+
controls_hint=_controls_hint,
|
|
606
|
+
)
|
|
607
|
+
if choice < 0:
|
|
608
|
+
_msg = _i18n("cancelled")
|
|
609
|
+
self.context.console.print(f"[dim]{_msg}[/dim]" if self.context.has_rich else _msg)
|
|
610
|
+
return
|
|
611
|
+
if all_ids[choice] is None:
|
|
612
|
+
current_idx = min(choice + 1, len(options) - 1)
|
|
613
|
+
continue
|
|
614
|
+
break
|
|
615
|
+
|
|
616
|
+
self._set_model_by_id(all_ids[choice], provider=current_provider)
|
|
617
|
+
|
|
618
|
+
async def _pick_model_in_host(self, native_select, *, picker_title, current_id, current_provider,
|
|
619
|
+
rich_models, aria_ids, status_tag, short_desc, lang):
|
|
620
|
+
"""Offer every model in the frontend's menu and apply the choice.
|
|
621
|
+
|
|
622
|
+
One row per model: the name (with a mark for the current one and for
|
|
623
|
+
whether it is installed) and, beneath it, what it is for and its
|
|
624
|
+
limits. Aria's catalogue comes first, then community Ollama models.
|
|
625
|
+
"""
|
|
626
|
+
zh = str(lang).lower().startswith("zh")
|
|
627
|
+
rows: list = [] # (model_id, label, help)
|
|
628
|
+
groups: list = []
|
|
629
|
+
catalogue = "Aria 精选" if zh else "Aria catalogue"
|
|
630
|
+
for m in _installed_catalogue(rich_models).values():
|
|
631
|
+
mid = m["id"]
|
|
632
|
+
tags = short_desc(m)
|
|
633
|
+
current = (" ✓ 当前" if zh else " ✓ current") if mid == current_id else ""
|
|
634
|
+
# The id leads the detail: several catalogue entries share a display name
|
|
635
|
+
# ("Qwen 2.5"), and the id is what tells them apart.
|
|
636
|
+
rows.append((mid, f"{status_tag(mid, m.get('badge', ''))} {m['name']}{current}", f"{mid} · {tags}"))
|
|
637
|
+
groups.append(catalogue)
|
|
638
|
+
if current_provider == "ollama":
|
|
639
|
+
community = "社区模型 (Ollama)" if zh else "Community models (Ollama)"
|
|
640
|
+
for cm in rich_models:
|
|
641
|
+
if cm["name"] in aria_ids:
|
|
642
|
+
continue
|
|
643
|
+
current = (" ✓ 当前" if zh else " ✓ current") if cm["name"] == current_id else ""
|
|
644
|
+
size = " · ".join(x for x in (cm.get("size_label"), cm.get("family"), cm.get("quant")) if x)
|
|
645
|
+
rows.append((cm["name"], f"● {cm['name']}{current}", size or community))
|
|
646
|
+
groups.append(community)
|
|
647
|
+
# Every provider that has a key lists what it serves today. A hand-kept
|
|
648
|
+
# catalogue goes stale; the provider's own list does not.
|
|
649
|
+
await self._add_cloud_models(rows, groups, current_id, zh)
|
|
650
|
+
if not rows:
|
|
651
|
+
return
|
|
652
|
+
start = next((i for i, row in enumerate(rows) if row[0] == current_id), 0)
|
|
653
|
+
options = [(label, help_text) for _mid, label, help_text in rows]
|
|
654
|
+
# select() blocks on the user's answer, so keep it off the event loop.
|
|
655
|
+
import asyncio as _asyncio
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
choice = await _asyncio.to_thread(native_select, options, start, picker_title, picker=True, groups=groups)
|
|
659
|
+
except (KeyboardInterrupt, ValueError):
|
|
660
|
+
choice = -1
|
|
661
|
+
if not isinstance(choice, int) or not 0 <= choice < len(rows):
|
|
662
|
+
msg = "已取消,模型未改变。" if zh else "Cancelled; the model is unchanged."
|
|
663
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
664
|
+
return
|
|
665
|
+
chosen = rows[choice][0]
|
|
666
|
+
if current_provider == "ollama" and chosen in {m["name"] for m in rich_models}:
|
|
667
|
+
self._set_model_by_id(chosen, provider="ollama")
|
|
668
|
+
elif "/" in chosen:
|
|
669
|
+
# provider/model: the route that checks credentials and sets the provider.
|
|
670
|
+
await self.cmd_model(chosen)
|
|
671
|
+
else:
|
|
672
|
+
self._set_model_by_id(chosen)
|
|
673
|
+
|
|
674
|
+
async def _add_cloud_models(self, rows: list, groups: list, current_id: str, zh: bool) -> None:
|
|
675
|
+
"""Append a section per provider with a key: what its model list endpoint returns now."""
|
|
676
|
+
import asyncio as _asyncio
|
|
677
|
+
from aria_code.apps.cli import cloud_models
|
|
678
|
+
|
|
679
|
+
def key_for(provider: str) -> str:
|
|
680
|
+
key = _get_provider_key(provider)
|
|
681
|
+
if not key and provider == "google":
|
|
682
|
+
key = os.environ.get("GEMINI_API_KEY", "")
|
|
683
|
+
return key
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
found = await _asyncio.to_thread(cloud_models.collect, key_for)
|
|
687
|
+
except Exception:
|
|
688
|
+
return
|
|
689
|
+
listed = {row[0] for row in rows}
|
|
690
|
+
for provider, models in found.items():
|
|
691
|
+
heading = cloud_models.PROVIDERS[provider][0]
|
|
692
|
+
fresh = [x for x in models if f"{provider}/{x['id']}" not in listed]
|
|
693
|
+
if not fresh:
|
|
694
|
+
continue
|
|
695
|
+
group = f"{heading} · {len(fresh)}" + (" 个可用" if zh else " available")
|
|
696
|
+
for item in fresh:
|
|
697
|
+
full = f"{provider}/{item['id']}"
|
|
698
|
+
is_current = current_id in (full, item["id"])
|
|
699
|
+
tag = (" ✓ 当前" if zh else " ✓ current") if is_current else ""
|
|
700
|
+
detail = " · ".join(x for x in (full, item["detail"]) if x)
|
|
701
|
+
rows.append((full, f"☁ {item['name']}{tag}", detail))
|
|
702
|
+
groups.append(group)
|
|
703
|
+
|
|
704
|
+
def _set_model(self, key: str):
|
|
705
|
+
"""Set model by _get_MODELS() key."""
|
|
706
|
+
m = _get_MODELS()[key]
|
|
707
|
+
self._set_model_by_id(m["id"])
|
|
708
|
+
|
|
709
|
+
def _set_model_by_id(self, model_id: str, provider: str | None = None):
|
|
710
|
+
"""Set model by Ollama model ID (works for both built-in and community models)."""
|
|
711
|
+
matched = next((m for m in _get_MODELS().values() if m["id"] == model_id), None)
|
|
712
|
+
target_provider = provider or ("ollama" if matched is not None else self.terminal.config.get("local_provider", "ollama"))
|
|
713
|
+
if target_provider == "ollama":
|
|
714
|
+
try:
|
|
715
|
+
from aria_code.providers.ollama_models import ollama_base_url, resolve_model_sync
|
|
716
|
+
model_id = resolve_model_sync(ollama_base_url(self.terminal.config), model_id)
|
|
717
|
+
except ValueError as exc:
|
|
718
|
+
self.context.console.print(str(exc))
|
|
719
|
+
return
|
|
720
|
+
self.terminal.config["model"] = model_id
|
|
721
|
+
self.terminal.config["local_provider"] = target_provider
|
|
722
|
+
self.terminal.config["local_mode"] = (
|
|
723
|
+
self.terminal.config["local_provider"]
|
|
724
|
+
in {"ollama", "lmstudio", "vllm", "llamacpp", "jan", "custom"}
|
|
725
|
+
)
|
|
726
|
+
self.terminal.config["backend_chat"] = False
|
|
727
|
+
self.terminal._actual_model = None # reset: new config model, no known fallback yet
|
|
728
|
+
self.context.save_config(self.terminal.config)
|
|
729
|
+
# Pretty label
|
|
730
|
+
for m in _get_MODELS().values():
|
|
731
|
+
if m["id"] == model_id:
|
|
732
|
+
runtime = "Ollama Cloud" if m.get("badge") == "Cloud" else "Ollama"
|
|
733
|
+
if self.context.has_rich:
|
|
734
|
+
self.context.console.print(f"[bold]Model:[/bold] [bold]{m['name']} {m['version']}[/bold] "
|
|
735
|
+
f"[dim]· {runtime}[/dim]")
|
|
736
|
+
else:
|
|
737
|
+
print(f"Model: {m['name']} {m['version']} ({m['tag']})")
|
|
738
|
+
return
|
|
739
|
+
# Community / unknown model
|
|
740
|
+
if self.context.has_rich:
|
|
741
|
+
self.context.console.print(f"[bold]Model:[/bold] [bold]{model_id}[/bold] [dim](local)[/dim]")
|
|
742
|
+
else:
|
|
743
|
+
print(f"Model: {model_id} (local)")
|
|
744
|
+
|
|
745
|
+
def cmd_thinking(self, args: str):
|
|
746
|
+
mode = args.strip().lower()
|
|
747
|
+
|
|
748
|
+
# Direct set: /thinking on
|
|
749
|
+
if mode in ("on", "thinking"):
|
|
750
|
+
self.terminal.config["thinking_mode"] = "thinking"
|
|
751
|
+
elif mode in ("off", "instant"):
|
|
752
|
+
self.terminal.config["thinking_mode"] = "instant"
|
|
753
|
+
elif mode == "auto":
|
|
754
|
+
self.terminal.config["thinking_mode"] = "auto"
|
|
755
|
+
elif mode:
|
|
756
|
+
# Unknown mode, show picker
|
|
757
|
+
pass
|
|
758
|
+
else:
|
|
759
|
+
# Interactive picker
|
|
760
|
+
current = self.terminal.config.get("thinking_mode", "auto")
|
|
761
|
+
mode_keys = list(_get_THINKING_MODES().keys())
|
|
762
|
+
current_idx = mode_keys.index(current) if current in mode_keys else 0
|
|
763
|
+
options = [(info["label"], info["description"]) for info in _get_THINKING_MODES().values()]
|
|
764
|
+
choice = _arrow_select(options, selected=current_idx, title="Thinking Mode")
|
|
765
|
+
if 0 <= choice < len(mode_keys):
|
|
766
|
+
self.terminal.config["thinking_mode"] = mode_keys[choice]
|
|
767
|
+
else:
|
|
768
|
+
if self.context.has_rich:
|
|
769
|
+
self.context.console.print("[dim]No change[/dim]")
|
|
770
|
+
else:
|
|
771
|
+
print("No change")
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
self.context.save_config(self.terminal.config)
|
|
775
|
+
result = self.terminal.config["thinking_mode"]
|
|
776
|
+
info = _get_THINKING_MODES().get(result, {})
|
|
777
|
+
if self.context.has_rich:
|
|
778
|
+
self.context.console.print(f"[green]Thinking: {info.get('label', result)}[/green] [dim]{info.get('description', '')}[/dim]")
|
|
779
|
+
else:
|
|
780
|
+
print(f"Thinking: {result}")
|
|
781
|
+
|
|
782
|
+
def cmd_skills(self, args: str):
|
|
783
|
+
"""List all available skills grouped by category."""
|
|
784
|
+
import shlex
|
|
785
|
+
|
|
786
|
+
raw_parts = shlex.split(args.strip()) if args.strip() else []
|
|
787
|
+
if raw_parts and raw_parts[0].lower() == "add":
|
|
788
|
+
if len(raw_parts) < 2:
|
|
789
|
+
message = "Usage: /skills add owner/repository [--ref tag-or-sha]"
|
|
790
|
+
self.context.console.print(f"[yellow]{message}[/yellow]") if self.context.has_rich else print(message)
|
|
791
|
+
return
|
|
792
|
+
ref = ""
|
|
793
|
+
if "--ref" in raw_parts:
|
|
794
|
+
ref_index = raw_parts.index("--ref")
|
|
795
|
+
if ref_index + 1 >= len(raw_parts):
|
|
796
|
+
message = "--ref requires a tag, branch, or commit"
|
|
797
|
+
self.context.console.print(f"[red]{message}[/red]") if self.context.has_rich else print(message)
|
|
798
|
+
return
|
|
799
|
+
ref = raw_parts[ref_index + 1]
|
|
800
|
+
try:
|
|
801
|
+
from aria_code.packages.aria_skills import install_catalog
|
|
802
|
+
|
|
803
|
+
installed = install_catalog(raw_parts[1], ref=ref)
|
|
804
|
+
message = (
|
|
805
|
+
f"Installed {installed.source.full_name} at {installed.revision[:12]} "
|
|
806
|
+
f"with {len(installed.skills)} verified skills"
|
|
807
|
+
)
|
|
808
|
+
self.context.console.print(f"[green]{message}[/green]") if self.context.has_rich else print(message)
|
|
809
|
+
except Exception as exc:
|
|
810
|
+
message = f"Skill catalog install failed: {exc}"
|
|
811
|
+
self.context.console.print(f"[red]{message}[/red]") if self.context.has_rich else print(message)
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
categories = {}
|
|
815
|
+
for s in _get_SKILLS():
|
|
816
|
+
cat = s["category"]
|
|
817
|
+
if cat not in categories:
|
|
818
|
+
categories[cat] = []
|
|
819
|
+
categories[cat].append(s)
|
|
820
|
+
|
|
821
|
+
cat_labels = {
|
|
822
|
+
"research": "Research",
|
|
823
|
+
"analysis": "Analysis",
|
|
824
|
+
"strategy": "Strategy",
|
|
825
|
+
"risk": "Risk Management",
|
|
826
|
+
"quant": "Quantitative",
|
|
827
|
+
"crypto": "Crypto",
|
|
828
|
+
"tools": "Tools",
|
|
829
|
+
"code": "Code Generation",
|
|
830
|
+
}
|
|
831
|
+
try:
|
|
832
|
+
from aria_code.packages.aria_skills import (
|
|
833
|
+
discover_external_skills,
|
|
834
|
+
recent_skill_activation_traces,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
external_skills = discover_external_skills()
|
|
838
|
+
activation_traces = recent_skill_activation_traces()
|
|
839
|
+
except Exception:
|
|
840
|
+
external_skills = []
|
|
841
|
+
activation_traces = []
|
|
842
|
+
|
|
843
|
+
subcommand = args.strip().lower()
|
|
844
|
+
if subcommand in {"trace", "traces"}:
|
|
845
|
+
if self.context.has_rich:
|
|
846
|
+
self.context.console.print("\n [bold]Skill Activation Trace[/bold]")
|
|
847
|
+
if not activation_traces:
|
|
848
|
+
self.context.console.print(" [dim]No skill activations recorded in this session.[/dim]\n")
|
|
849
|
+
for trace in activation_traces:
|
|
850
|
+
state = "[green]active[/green]" if trace.activated else "[red]blocked[/red]"
|
|
851
|
+
self.context.console.print(
|
|
852
|
+
f" {state} [bold]{trace.qualified_name}[/bold] "
|
|
853
|
+
f"[dim]{trace.reason} · score {trace.score:g} · {trace.integrity}[/dim]"
|
|
854
|
+
)
|
|
855
|
+
self.context.console.print()
|
|
856
|
+
else:
|
|
857
|
+
print("\nSkill Activation Trace")
|
|
858
|
+
for trace in activation_traces:
|
|
859
|
+
state = "active" if trace.activated else "blocked"
|
|
860
|
+
print(
|
|
861
|
+
f" {state} {trace.qualified_name} {trace.reason} "
|
|
862
|
+
f"score={trace.score:g} integrity={trace.integrity}"
|
|
863
|
+
)
|
|
864
|
+
return
|
|
865
|
+
|
|
866
|
+
if subcommand in {"doctor", "status"}:
|
|
867
|
+
if self.context.has_rich:
|
|
868
|
+
self.context.console.print("\n [bold]Portable Skill Doctor[/bold]")
|
|
869
|
+
for skill in external_skills:
|
|
870
|
+
style = "green" if skill.integrity == "verified" else "yellow"
|
|
871
|
+
self.context.console.print(
|
|
872
|
+
f" [{style}]{skill.integrity:8s}[/{style}] "
|
|
873
|
+
f"[bold]{skill.qualified_name}[/bold] "
|
|
874
|
+
f"[dim]v{skill.plugin_version or '-'} · "
|
|
875
|
+
f"scripts={skill.policy.script_execution}[/dim]"
|
|
876
|
+
)
|
|
877
|
+
self.context.console.print()
|
|
878
|
+
else:
|
|
879
|
+
print("\nPortable Skill Doctor")
|
|
880
|
+
for skill in external_skills:
|
|
881
|
+
print(
|
|
882
|
+
f" {skill.integrity:8s} {skill.qualified_name} "
|
|
883
|
+
f"v{skill.plugin_version or '-'} scripts={skill.policy.script_execution}"
|
|
884
|
+
)
|
|
885
|
+
return
|
|
886
|
+
|
|
887
|
+
if self.context.has_rich:
|
|
888
|
+
self.context.console.print()
|
|
889
|
+
for cat, skills in categories.items():
|
|
890
|
+
label = cat_labels.get(cat, cat.title())
|
|
891
|
+
self.context.console.print(f" [bold]{label}[/bold]")
|
|
892
|
+
for s in skills:
|
|
893
|
+
args_hint = f" [dim]{s.get('args', '')}[/dim]" if s.get("args") else ""
|
|
894
|
+
self.context.console.print(f" [bold]{s['command']:20s}[/bold][dim]{s['description']}[/dim]{args_hint}")
|
|
895
|
+
self.context.console.print()
|
|
896
|
+
|
|
897
|
+
if external_skills:
|
|
898
|
+
self.context.console.print(" [bold]Portable Workflows[/bold]")
|
|
899
|
+
for skill in external_skills:
|
|
900
|
+
self.context.console.print(
|
|
901
|
+
f" [bold]${skill.qualified_name}[/bold] "
|
|
902
|
+
f"[dim]v{skill.plugin_version or '-'} · {skill.integrity}[/dim]\n"
|
|
903
|
+
f" [dim]{skill.description}[/dim]"
|
|
904
|
+
)
|
|
905
|
+
self.context.console.print()
|
|
906
|
+
|
|
907
|
+
self.context.console.print("[dim] Type a skill command to execute, e.g. /deep-analysis AAPL[/dim]\n")
|
|
908
|
+
else:
|
|
909
|
+
print("\nSkills:")
|
|
910
|
+
for cat, skills in categories.items():
|
|
911
|
+
label = cat_labels.get(cat, cat.title())
|
|
912
|
+
print(f"\n [{label}]")
|
|
913
|
+
for s in skills:
|
|
914
|
+
print(f" {s['command']:20s} {s['description']}")
|
|
915
|
+
if external_skills:
|
|
916
|
+
print("\n [Portable Workflows]")
|
|
917
|
+
for skill in external_skills:
|
|
918
|
+
print(
|
|
919
|
+
f" ${skill.qualified_name} v{skill.plugin_version or '-'} "
|
|
920
|
+
f"{skill.integrity} {skill.description}"
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
async def _execute_skill(self, skill: dict, args: str):
|
|
924
|
+
"""Execute a skill by expanding its prompt template and sending to AI."""
|
|
925
|
+
parts = args.strip().upper().split() if args.strip() else []
|
|
926
|
+
cmd = skill["command"]
|
|
927
|
+
|
|
928
|
+
# Skill invocation header — matches the ⏺ tool-call rhythm
|
|
929
|
+
_skill_name = skill.get("name") or cmd.lstrip("/")
|
|
930
|
+
_arg_hint = f" [dim]{args.strip()}[/dim]" if args.strip() else ""
|
|
931
|
+
if self.context.has_rich:
|
|
932
|
+
self.context.console.print(f"\n [#C08050]⏺[/#C08050] [bold]技能 · {_skill_name}[/bold]{_arg_hint}")
|
|
933
|
+
else:
|
|
934
|
+
print(f"\n ⏺ 技能 · {_skill_name} {args.strip()}")
|
|
935
|
+
|
|
936
|
+
# Build the prompt from template
|
|
937
|
+
template = skill["prompt"]
|
|
938
|
+
|
|
939
|
+
if cmd == "/deep-analysis":
|
|
940
|
+
symbol = parts[0] if parts else "AAPL"
|
|
941
|
+
prompt = template.format(symbol=symbol)
|
|
942
|
+
|
|
943
|
+
elif cmd == "/trade-idea":
|
|
944
|
+
context = f" in {' '.join(parts)}" if parts else " in the US market"
|
|
945
|
+
prompt = template.format(context=context)
|
|
946
|
+
|
|
947
|
+
elif cmd == "/risk-report":
|
|
948
|
+
if parts:
|
|
949
|
+
symbols = ", ".join(parts)
|
|
950
|
+
else:
|
|
951
|
+
symbols = ", ".join(self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL"]))
|
|
952
|
+
prompt = template.format(symbols=symbols)
|
|
953
|
+
|
|
954
|
+
elif cmd == "/factor-screen":
|
|
955
|
+
factor = " ".join(parts).lower() if parts else "momentum"
|
|
956
|
+
prompt = template.format(factor=factor)
|
|
957
|
+
|
|
958
|
+
elif cmd == "/backtest-report":
|
|
959
|
+
strategy = parts[0].lower() if len(parts) > 0 else "momentum"
|
|
960
|
+
symbol = parts[1] if len(parts) > 1 else "SPY"
|
|
961
|
+
start = parts[2] if len(parts) > 2 else "2023-01-01"
|
|
962
|
+
end = parts[3] if len(parts) > 3 else "2025-01-01"
|
|
963
|
+
prompt = template.format(strategy=strategy, symbol=symbol, start=start, end=end)
|
|
964
|
+
|
|
965
|
+
elif cmd == "/morning-brief":
|
|
966
|
+
extra = f"\nFocus on: {' '.join(parts)}" if parts else ""
|
|
967
|
+
prompt = template.format(extra=extra)
|
|
968
|
+
|
|
969
|
+
elif cmd == "/macro-outlook":
|
|
970
|
+
context = f" for {' '.join(parts)}" if parts else " for the US and global economy"
|
|
971
|
+
prompt = template.format(context=context)
|
|
972
|
+
|
|
973
|
+
elif cmd == "/crypto-scan":
|
|
974
|
+
extra = f"\nFocus on: {' '.join(parts)}" if parts else ""
|
|
975
|
+
prompt = template.format(extra=extra)
|
|
976
|
+
|
|
977
|
+
elif cmd == "/watchlist-scan":
|
|
978
|
+
symbols = ", ".join(self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL"]))
|
|
979
|
+
prompt = template.format(symbols=symbols)
|
|
980
|
+
|
|
981
|
+
elif cmd == "/sector-rotation":
|
|
982
|
+
prompt = template
|
|
983
|
+
|
|
984
|
+
elif cmd == "/gen-strategy":
|
|
985
|
+
strategy = parts[0].lower() if len(parts) > 0 else "momentum"
|
|
986
|
+
symbol = parts[1] if len(parts) > 1 else "SPY"
|
|
987
|
+
prompt = template.format(strategy=strategy, symbol=symbol)
|
|
988
|
+
|
|
989
|
+
elif cmd == "/gen-analysis":
|
|
990
|
+
topic = " ".join(parts[:2]).lower() if parts else "technical analysis"
|
|
991
|
+
symbols = ", ".join(parts[2:]) if len(parts) > 2 else "SPY"
|
|
992
|
+
prompt = template.format(topic=topic, symbols=symbols)
|
|
993
|
+
|
|
994
|
+
elif cmd == "/gen-bot":
|
|
995
|
+
exchange = parts[0].lower() if len(parts) > 0 else "binance"
|
|
996
|
+
strategy = " ".join(parts[1:]).lower() if len(parts) > 1 else "grid trading"
|
|
997
|
+
prompt = template.format(exchange=exchange, strategy=strategy)
|
|
998
|
+
|
|
999
|
+
else:
|
|
1000
|
+
prompt = template
|
|
1001
|
+
|
|
1002
|
+
# Show skill activation
|
|
1003
|
+
if self.context.has_rich:
|
|
1004
|
+
tools = ", ".join(skill.get("tools_hint", [])[:3])
|
|
1005
|
+
self.context.console.print(f"[bold]Skill:[/bold] [bold]{skill['name']}[/bold] [dim]tools: {tools}[/dim]")
|
|
1006
|
+
else:
|
|
1007
|
+
print(f"Skill: {skill['name']}")
|
|
1008
|
+
|
|
1009
|
+
await self.terminal.send_message(prompt)
|
|
1010
|
+
|
|
1011
|
+
def cmd_tools(self, args: str):
|
|
1012
|
+
if self.context.has_rich:
|
|
1013
|
+
self.context.console.print()
|
|
1014
|
+
self.context.console.print(" [bold]Local Tools[/bold] [dim](Code Agent)[/dim]")
|
|
1015
|
+
for i, (name, (_, desc)) in enumerate(_get_LOCAL_TOOLS().items(), 1):
|
|
1016
|
+
self.context.console.print(f" [bold]{name:28s}[/bold][dim]{desc}[/dim]")
|
|
1017
|
+
self.context.console.print()
|
|
1018
|
+
|
|
1019
|
+
self.context.console.print(f" [bold]Remote Tools[/bold] [dim]({len(_get_ARIA_TOOLS())})[/dim]")
|
|
1020
|
+
for i, (name, desc) in enumerate(_get_ARIA_TOOLS(), 1):
|
|
1021
|
+
self.context.console.print(f" [bold]{name:28s}[/bold][dim]{desc}[/dim]")
|
|
1022
|
+
self.context.console.print()
|
|
1023
|
+
else:
|
|
1024
|
+
print("\nLocal Tools (Code Agent):")
|
|
1025
|
+
for i, (name, (_, desc)) in enumerate(_get_LOCAL_TOOLS().items(), 1):
|
|
1026
|
+
print(f" {i:2d}. {name:30s} {desc}")
|
|
1027
|
+
print("\nRemote Aria Tools (22):")
|
|
1028
|
+
for i, (name, desc) in enumerate(_get_ARIA_TOOLS(), 1):
|
|
1029
|
+
print(f" {i:2d}. {name:30s} {desc}")
|
|
1030
|
+
|
|
1031
|
+
async def cmd_collab(self, args: str):
|
|
1032
|
+
"""Consult ChatGPT/OpenAI and Claude/Anthropic through their APIs.
|
|
1033
|
+
|
|
1034
|
+
Consumer-app subscriptions deliberately are not treated as credentials.
|
|
1035
|
+
This keeps external model use explicit, attributable, and opt-in.
|
|
1036
|
+
"""
|
|
1037
|
+
from aria_code.apps.cli.providers.collaboration import (
|
|
1038
|
+
collaboration_readiness, consult, resolve_collaborator,
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
parts = args.strip().split(maxsplit=1)
|
|
1042
|
+
action = parts[0].lower() if parts else "status"
|
|
1043
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
1044
|
+
config = self.terminal.config
|
|
1045
|
+
|
|
1046
|
+
if action in ("status", "help"):
|
|
1047
|
+
rows = collaboration_readiness(config, _get_provider_key)
|
|
1048
|
+
if self.context.has_rich:
|
|
1049
|
+
self.context.console.print()
|
|
1050
|
+
self.context.console.print(" [bold]多模型协作(API)[/bold]")
|
|
1051
|
+
self.context.console.print(" [dim]ChatGPT/Claude 桌面订阅不会被读取;请配置各自 API key。[/dim]")
|
|
1052
|
+
self.context.console.print()
|
|
1053
|
+
for row in rows:
|
|
1054
|
+
icon = "[green]●[/green]" if row["configured"] else "[dim]○[/dim]"
|
|
1055
|
+
state = "[green]已就绪[/green]" if row["configured"] else "[dim]未配置[/dim]"
|
|
1056
|
+
self.context.console.print(
|
|
1057
|
+
f" {icon} [bold]{row['label']:18s}[/bold] {state}"
|
|
1058
|
+
f" [dim]{row['model']}[/dim]"
|
|
1059
|
+
)
|
|
1060
|
+
self.context.console.print()
|
|
1061
|
+
self.context.console.print(" [dim]/apikey set openai <key> 配置 ChatGPT/OpenAI API[/dim]")
|
|
1062
|
+
self.context.console.print(" [dim]/apikey set anthropic <key> 配置 Claude API[/dim]")
|
|
1063
|
+
self.context.console.print(" [dim]/collab use chatgpt [model] 将主对话切换为 OpenAI[/dim]")
|
|
1064
|
+
self.context.console.print(" [dim]/collab use claude [model] 将主对话切换为 Claude[/dim]")
|
|
1065
|
+
self.context.console.print(" [dim]/collab model <名称> <模型> 设置协作咨询使用的模型[/dim]")
|
|
1066
|
+
self.context.console.print(" [dim]/collab ask <问题> 并行获取两份独立意见(会消耗 API 配额)[/dim]")
|
|
1067
|
+
self.context.console.print()
|
|
1068
|
+
else:
|
|
1069
|
+
print("Multi-model collaboration (API; desktop subscriptions are not API keys):")
|
|
1070
|
+
for row in rows:
|
|
1071
|
+
state = "ready" if row["configured"] else "not configured"
|
|
1072
|
+
print(f" {'✓' if row['configured'] else '○'} {row['label']}: {state} ({row['model']})")
|
|
1073
|
+
print(" /collab use chatgpt [model] | /collab use claude [model]")
|
|
1074
|
+
print(" /collab model <name> <model>")
|
|
1075
|
+
print(" /collab ask <prompt>")
|
|
1076
|
+
return
|
|
1077
|
+
|
|
1078
|
+
if action == "model":
|
|
1079
|
+
tokens = rest.split(maxsplit=1)
|
|
1080
|
+
target = resolve_collaborator(tokens[0] if tokens else "", config)
|
|
1081
|
+
model = tokens[1].strip() if len(tokens) > 1 else ""
|
|
1082
|
+
if target is None or not model:
|
|
1083
|
+
msg = "Usage: /collab model chatgpt <model> | /collab model claude <model>"
|
|
1084
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1085
|
+
return
|
|
1086
|
+
config[f"collab_{target.alias}_model"] = model
|
|
1087
|
+
self.context.save_config(config)
|
|
1088
|
+
msg = f"✓ {target.label} 协作模型已设为 {model}"
|
|
1089
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1090
|
+
return
|
|
1091
|
+
|
|
1092
|
+
if action == "use":
|
|
1093
|
+
tokens = rest.split(maxsplit=1)
|
|
1094
|
+
name = tokens[0] if tokens else ""
|
|
1095
|
+
target = resolve_collaborator(name, config)
|
|
1096
|
+
if target is None:
|
|
1097
|
+
msg = "Usage: /collab use chatgpt [model] | /collab use claude [model]"
|
|
1098
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1099
|
+
return
|
|
1100
|
+
if not _get_provider_key(target.provider):
|
|
1101
|
+
msg = f"⚠ {target.label} API key 未配置:/apikey set {target.provider} <key>"
|
|
1102
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1103
|
+
return
|
|
1104
|
+
model = tokens[1].strip() if len(tokens) > 1 else target.default_model
|
|
1105
|
+
await self.cmd_model(f"{target.provider}/{model}")
|
|
1106
|
+
return
|
|
1107
|
+
|
|
1108
|
+
if action == "ask":
|
|
1109
|
+
if not rest:
|
|
1110
|
+
msg = "Usage: /collab ask <问题>"
|
|
1111
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1112
|
+
return
|
|
1113
|
+
targets = [
|
|
1114
|
+
target for name in ("chatgpt", "claude")
|
|
1115
|
+
if (target := resolve_collaborator(name, config)) is not None
|
|
1116
|
+
and _get_provider_key(target.provider)
|
|
1117
|
+
]
|
|
1118
|
+
if not targets:
|
|
1119
|
+
msg = "没有已配置的协作模型。先运行 /collab status。"
|
|
1120
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1121
|
+
return
|
|
1122
|
+
notice = f"正在向 {len(targets)} 个模型征询独立意见…"
|
|
1123
|
+
self.context.console.print(f"[dim]{notice}[/dim]") if self.context.has_rich else print(notice)
|
|
1124
|
+
from aria_code.providers.llm.base import Message
|
|
1125
|
+
from aria_code.providers.llm.registry import get_provider
|
|
1126
|
+
|
|
1127
|
+
results = await consult(
|
|
1128
|
+
rest, targets, get_provider=get_provider, message_factory=Message,
|
|
1129
|
+
timeout=float(config.get("collab_timeout", 60.0)),
|
|
1130
|
+
)
|
|
1131
|
+
for result in results:
|
|
1132
|
+
label = str(result["label"])
|
|
1133
|
+
model = str(result["model"])
|
|
1134
|
+
if result["success"]:
|
|
1135
|
+
if self.context.has_rich:
|
|
1136
|
+
from rich.panel import Panel
|
|
1137
|
+
self.context.console.print(Panel(
|
|
1138
|
+
str(result["response"]), title=f"{label} · {model}",
|
|
1139
|
+
border_style="cyan", padding=(0, 1),
|
|
1140
|
+
))
|
|
1141
|
+
else:
|
|
1142
|
+
print(f"\n[{label} · {model}]\n{result['response']}")
|
|
1143
|
+
else:
|
|
1144
|
+
msg = f"{label} 未完成:{result['error'] or 'empty response'}"
|
|
1145
|
+
self.context.console.print(f"[yellow]⚠ {msg}[/yellow]") if self.context.has_rich else print(f"⚠ {msg}")
|
|
1146
|
+
return
|
|
1147
|
+
|
|
1148
|
+
msg = "Usage: /collab [status|model|use|ask]"
|
|
1149
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1150
|
+
|
|
1151
|
+
async def cmd_apikey(self, args: str):
|
|
1152
|
+
"""Manage Cloud API keys.
|
|
1153
|
+
|
|
1154
|
+
Usage:
|
|
1155
|
+
/apikey — 交互式向导:选择 provider → 输入 key → 测试连接
|
|
1156
|
+
/apikey set <p> <k> — 直接保存
|
|
1157
|
+
/apikey list — 列出所有已配置 key
|
|
1158
|
+
/apikey remove <p> — 删除 key
|
|
1159
|
+
/apikey test <p> — 测试连接
|
|
1160
|
+
"""
|
|
1161
|
+
parts = args.strip().split()
|
|
1162
|
+
sub = parts[0].lower() if parts else ""
|
|
1163
|
+
|
|
1164
|
+
# ── 无参数 or "add" → 交互式向导 ─────────────────────────────────────
|
|
1165
|
+
if not sub or sub in ("add", "wizard"):
|
|
1166
|
+
await self._cmd_apikey_wizard()
|
|
1167
|
+
return
|
|
1168
|
+
|
|
1169
|
+
pjson = _load_providers_json() # dict of {provider: {api_key, base_url, ...}}
|
|
1170
|
+
|
|
1171
|
+
if sub == "set-url":
|
|
1172
|
+
# /apikey set-url <provider> <base_url>
|
|
1173
|
+
# 允许自定义端点(中转代理、国内镜像等),示例:
|
|
1174
|
+
# /apikey set-url openai https://my-proxy.com
|
|
1175
|
+
# /apikey set-url siliconflow https://api.siliconflow.cn
|
|
1176
|
+
if len(parts) < 3:
|
|
1177
|
+
msg = "Usage: /apikey set-url <provider> <base_url>"
|
|
1178
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
1179
|
+
return
|
|
1180
|
+
provider = parts[1].lower()
|
|
1181
|
+
url = parts[2].rstrip("/")
|
|
1182
|
+
entry = pjson.get(provider, {})
|
|
1183
|
+
entry["base_url"] = url
|
|
1184
|
+
pjson[provider] = entry
|
|
1185
|
+
_save_providers_json(pjson)
|
|
1186
|
+
msg = f"✓ {provider.capitalize()} base_url 已更新: {url}"
|
|
1187
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
if sub == "set":
|
|
1191
|
+
if len(parts) < 3:
|
|
1192
|
+
msg = ("Usage: /apikey set <provider> <key> (e.g. /apikey set deepseek sk-...)\n"
|
|
1193
|
+
" /apikey set-url <provider> <base_url> (自定义代理端点)")
|
|
1194
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
1195
|
+
return
|
|
1196
|
+
provider = parts[1].lower()
|
|
1197
|
+
key = parts[2]
|
|
1198
|
+
_all_known = set(_get__PROVIDER_KEY_MAP()) | set(_get__DATA_KEY_MAP()) | set(_get__PROVIDER_BASE_URLS())
|
|
1199
|
+
if provider not in _all_known:
|
|
1200
|
+
known_llm = ", ".join(sorted(_get__PROVIDER_KEY_MAP().keys()))
|
|
1201
|
+
known_data = ", ".join(sorted(_get__DATA_KEY_MAP().keys()))
|
|
1202
|
+
msg = (f"Unknown provider '{provider}'.\n"
|
|
1203
|
+
f" LLM providers: {known_llm}\n"
|
|
1204
|
+
f" Data services: {known_data}")
|
|
1205
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1206
|
+
return
|
|
1207
|
+
|
|
1208
|
+
# ── Data service key ──────────────────────────────────────────────
|
|
1209
|
+
if provider in _get__DATA_KEY_MAP():
|
|
1210
|
+
_save_data_key(provider, key)
|
|
1211
|
+
env_var = _get__DATA_KEY_MAP()[provider]
|
|
1212
|
+
os.environ[env_var] = key # take effect immediately
|
|
1213
|
+
masked = key[:6] + "****" + key[-4:] if len(key) > 10 else "****"
|
|
1214
|
+
signup = _get__DATA_SIGNUP_URLS().get(provider, "")
|
|
1215
|
+
msg = f"✓ {provider.capitalize()} 数据服务 key 已保存 ({masked})"
|
|
1216
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1217
|
+
return
|
|
1218
|
+
|
|
1219
|
+
# ── LLM provider key (original logic) ────────────────────────────
|
|
1220
|
+
# Persist to providers.json
|
|
1221
|
+
entry = pjson.get(provider, {})
|
|
1222
|
+
entry["api_key"] = key
|
|
1223
|
+
if provider in _get__PROVIDER_BASE_URLS():
|
|
1224
|
+
entry.setdefault("base_url", _get__PROVIDER_BASE_URLS()[provider])
|
|
1225
|
+
pjson[provider] = entry
|
|
1226
|
+
_save_providers_json(pjson)
|
|
1227
|
+
# Also set in current process env so it works immediately
|
|
1228
|
+
env_var = _get__PROVIDER_KEY_MAP().get(provider)
|
|
1229
|
+
if env_var:
|
|
1230
|
+
os.environ[env_var] = key
|
|
1231
|
+
masked = key[:6] + "****" + key[-4:] if len(key) > 10 else "****"
|
|
1232
|
+
msg = f"✓ {provider.capitalize()} API key 已保存 ({masked})"
|
|
1233
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1234
|
+
|
|
1235
|
+
elif sub == "list":
|
|
1236
|
+
_LLM_ORDER = [
|
|
1237
|
+
# 国际
|
|
1238
|
+
"deepseek", "anthropic", "openai", "google", "xai",
|
|
1239
|
+
"groq", "mistral", "cohere", "perplexity", "together",
|
|
1240
|
+
# 国内
|
|
1241
|
+
"siliconflow", "dashscope", "moonshot", "zhipu",
|
|
1242
|
+
"baidu", "bytedance", "minimax", "stepfun", "01ai",
|
|
1243
|
+
]
|
|
1244
|
+
_DATA_ORDER = ["finnhub", "alphavantage", "twelvedata", "polygon",
|
|
1245
|
+
"fmp", "newsapi", "coingecko", "tavily", "brave"]
|
|
1246
|
+
data_configured = _load_data_keys()
|
|
1247
|
+
|
|
1248
|
+
if self.context.has_rich:
|
|
1249
|
+
from rich.table import Table
|
|
1250
|
+
from rich import box as _rbox
|
|
1251
|
+
self.context.console.print()
|
|
1252
|
+
self.context.console.print(" [bold]🤖 LLM 服务 Keys[/bold] [dim]— /apikey 进入向导[/dim]")
|
|
1253
|
+
self.context.console.print()
|
|
1254
|
+
for prov in _LLM_ORDER:
|
|
1255
|
+
env_var = _get__PROVIDER_KEY_MAP().get(prov, "")
|
|
1256
|
+
key_val = os.getenv(env_var or "") or pjson.get(prov, {}).get("api_key", "")
|
|
1257
|
+
desc = _get__PROVIDER_DESC().get(prov, "")
|
|
1258
|
+
if key_val:
|
|
1259
|
+
masked = key_val[:6] + "****" + key_val[-4:] if len(key_val) > 10 else "****"
|
|
1260
|
+
self.context.console.print(f" [green]●[/green] [green]{prov:<14}[/green] {masked} [dim]{desc}[/dim]")
|
|
1261
|
+
else:
|
|
1262
|
+
self.context.console.print(f" [dim]○ {prov:<14} 未配置 {desc}[/dim]")
|
|
1263
|
+
self.context.console.print()
|
|
1264
|
+
self.context.console.print(" [bold]📊 数据服务 Keys[/bold] [dim]— 后端离线时直连数据源[/dim]")
|
|
1265
|
+
self.context.console.print()
|
|
1266
|
+
for svc in _DATA_ORDER:
|
|
1267
|
+
key_val = data_configured.get(svc, "")
|
|
1268
|
+
desc = _get__PROVIDER_DESC().get(svc, "")
|
|
1269
|
+
if key_val:
|
|
1270
|
+
masked = key_val[:6] + "****" + key_val[-4:] if len(key_val) > 10 else "****"
|
|
1271
|
+
self.context.console.print(f" [green]●[/green] [green]{svc:<14}[/green] {masked} [dim]{desc}[/dim]")
|
|
1272
|
+
else:
|
|
1273
|
+
self.context.console.print(f" [dim]○ {svc:<14} 未配置 {desc}[/dim]")
|
|
1274
|
+
self.context.console.print()
|
|
1275
|
+
self.context.console.print(" [dim]提示: /apikey 进入交互向导 · /apikey test <provider> 测试连接[/dim]")
|
|
1276
|
+
self.context.console.print()
|
|
1277
|
+
else:
|
|
1278
|
+
print("\n LLM Providers:")
|
|
1279
|
+
for prov in _LLM_ORDER:
|
|
1280
|
+
env_var = _get__PROVIDER_KEY_MAP().get(prov, "")
|
|
1281
|
+
key_val = os.getenv(env_var or "") or pjson.get(prov, {}).get("api_key", "")
|
|
1282
|
+
status = key_val[:6] + "****" if key_val else "未配置"
|
|
1283
|
+
print(f" {prov:14s} {status}")
|
|
1284
|
+
print("\n Data Services:")
|
|
1285
|
+
for svc in _DATA_ORDER:
|
|
1286
|
+
key_val = data_configured.get(svc, "")
|
|
1287
|
+
status = key_val[:6] + "****" if key_val else "未配置"
|
|
1288
|
+
print(f" {svc:16s} {status}")
|
|
1289
|
+
|
|
1290
|
+
elif sub == "remove":
|
|
1291
|
+
if len(parts) < 2:
|
|
1292
|
+
self.context.console.print("[dim]Usage: /apikey remove <provider>[/dim]") if self.context.has_rich else print("Usage: /apikey remove <provider>")
|
|
1293
|
+
return
|
|
1294
|
+
provider = parts[1].lower()
|
|
1295
|
+
# LLM section
|
|
1296
|
+
if provider in pjson:
|
|
1297
|
+
pjson[provider].pop("api_key", None)
|
|
1298
|
+
if not pjson[provider]:
|
|
1299
|
+
del pjson[provider]
|
|
1300
|
+
_save_providers_json(pjson)
|
|
1301
|
+
# Data section
|
|
1302
|
+
if provider in _get__DATA_KEY_MAP():
|
|
1303
|
+
try:
|
|
1304
|
+
if _get_PROVIDERS_FILE().exists():
|
|
1305
|
+
raw = json.loads(_get_PROVIDERS_FILE().read_text(encoding="utf-8"))
|
|
1306
|
+
if provider in raw.get("data", {}):
|
|
1307
|
+
del raw["data"][provider]
|
|
1308
|
+
from aria_code.packages.aria_core.secure_file import write_secret_json
|
|
1309
|
+
write_secret_json(_get_PROVIDERS_FILE(), raw)
|
|
1310
|
+
except Exception as _e:
|
|
1311
|
+
logger.debug("apikey delete from file failed: %s", _e)
|
|
1312
|
+
# Clear from env
|
|
1313
|
+
env_var = _get__PROVIDER_KEY_MAP().get(provider) or _get__DATA_KEY_MAP().get(provider)
|
|
1314
|
+
if env_var and env_var in os.environ:
|
|
1315
|
+
del os.environ[env_var]
|
|
1316
|
+
msg = f"✓ {provider.capitalize()} key 已删除"
|
|
1317
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1318
|
+
|
|
1319
|
+
elif sub == "test":
|
|
1320
|
+
if len(parts) < 2:
|
|
1321
|
+
self.context.console.print("[dim]Usage: /apikey test <provider>[/dim]") if self.context.has_rich else print("Usage: /apikey test <provider>")
|
|
1322
|
+
return
|
|
1323
|
+
provider = parts[1].lower()
|
|
1324
|
+
key = _get_provider_key(provider) or _load_data_keys().get(provider, "")
|
|
1325
|
+
if not key:
|
|
1326
|
+
msg = f"⚠ {provider} API key 未配置,先运行 /apikey {provider}"
|
|
1327
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
1328
|
+
return
|
|
1329
|
+
self.context.console.print(f"[dim] 正在测试 {provider}…[/dim]") if self.context.has_rich else print(f" 测试 {provider}…")
|
|
1330
|
+
import asyncio as _aio
|
|
1331
|
+
loop = _aio.get_event_loop()
|
|
1332
|
+
ok, result_msg = await loop.run_in_executor(None, _test_api_key, provider, key)
|
|
1333
|
+
color = "green" if ok else "yellow"
|
|
1334
|
+
self.context.console.print(f" [{color}]{result_msg}[/{color}]") if self.context.has_rich else print(f" {result_msg}")
|
|
1335
|
+
|
|
1336
|
+
else:
|
|
1337
|
+
self.context.console.print("[dim]Usage: /apikey [set|list|remove|test] — 或直接 /apikey 进入向导[/dim]") if self.context.has_rich else print("Usage: /apikey [set|list|remove|test]")
|
|
1338
|
+
|
|
1339
|
+
async def _cmd_apikey_wizard(self):
|
|
1340
|
+
"""交互式 API Key 配置向导:选 provider → 查看指引 → 输入 key → 测试连接。"""
|
|
1341
|
+
import getpass as _getpass
|
|
1342
|
+
|
|
1343
|
+
pjson = _load_providers_json()
|
|
1344
|
+
data_cfg = _load_data_keys()
|
|
1345
|
+
|
|
1346
|
+
def _is_configured(name: str) -> bool:
|
|
1347
|
+
env = _get__PROVIDER_KEY_MAP().get(name) or _get__DATA_KEY_MAP().get(name)
|
|
1348
|
+
if env and os.getenv(env):
|
|
1349
|
+
return True
|
|
1350
|
+
if name in pjson and pjson[name].get("api_key"):
|
|
1351
|
+
return True
|
|
1352
|
+
if name in data_cfg:
|
|
1353
|
+
return True
|
|
1354
|
+
return False
|
|
1355
|
+
|
|
1356
|
+
# ── 分组构建 picker ───────────────────────────────────────────────────
|
|
1357
|
+
_LLM_ORDER = [
|
|
1358
|
+
# 国际
|
|
1359
|
+
"deepseek", "anthropic", "openai", "google", "xai",
|
|
1360
|
+
"groq", "mistral", "cohere", "perplexity", "together",
|
|
1361
|
+
# 国内
|
|
1362
|
+
"siliconflow", "dashscope", "moonshot", "zhipu",
|
|
1363
|
+
"baidu", "bytedance", "minimax", "stepfun", "01ai",
|
|
1364
|
+
]
|
|
1365
|
+
_DATA_ORDER = ["finnhub", "alphavantage", "twelvedata", "polygon",
|
|
1366
|
+
"fmp", "newsapi", "coingecko", "tavily", "brave"]
|
|
1367
|
+
|
|
1368
|
+
all_items = [] # (label, desc, key_name | None)
|
|
1369
|
+
|
|
1370
|
+
all_items.append(("─── 🤖 LLM 服务 (对话·分析·推理) ", "", None))
|
|
1371
|
+
for k in _LLM_ORDER:
|
|
1372
|
+
dot = "[green]●[/green]" if _is_configured(k) else "[dim]○[/dim]"
|
|
1373
|
+
desc = _get__PROVIDER_DESC().get(k, "")
|
|
1374
|
+
configured_tag = " ✓" if _is_configured(k) else ""
|
|
1375
|
+
all_items.append((f" {k:<14}{configured_tag}", desc, k))
|
|
1376
|
+
|
|
1377
|
+
all_items.append(("─── 📊 数据服务 (行情·财报·新闻) ", "", None))
|
|
1378
|
+
for k in _DATA_ORDER:
|
|
1379
|
+
desc = _get__PROVIDER_DESC().get(k, "")
|
|
1380
|
+
configured_tag = " ✓" if _is_configured(k) else ""
|
|
1381
|
+
all_items.append((f" {k:<14}{configured_tag}", desc, k))
|
|
1382
|
+
|
|
1383
|
+
picker_opts = [(label, desc) for label, desc, _ in all_items]
|
|
1384
|
+
sep_indices = {i for i, (_, _, key) in enumerate(all_items) if key is None}
|
|
1385
|
+
key_at = {i: key for i, (_, _, key) in enumerate(all_items) if key}
|
|
1386
|
+
|
|
1387
|
+
# 默认选中第一个真实条目
|
|
1388
|
+
first_real = next(i for i in range(len(all_items)) if i not in sep_indices)
|
|
1389
|
+
selected = first_real
|
|
1390
|
+
|
|
1391
|
+
while True:
|
|
1392
|
+
self.context.console.print() if self.context.has_rich else None
|
|
1393
|
+
|
|
1394
|
+
if self.context.has_rich:
|
|
1395
|
+
from rich.panel import Panel as _Panel
|
|
1396
|
+
from rich import box as _rbox
|
|
1397
|
+
self.context.console.print(_Panel(
|
|
1398
|
+
" ↑↓ 上下选择 · Enter 确认 · ESC/q 退出向导\n"
|
|
1399
|
+
" [green]●[/green] 已配置 [dim]○[/dim] 未配置 ✓ 表示 key 已存在",
|
|
1400
|
+
border_style="dim", box=_rbox.ROUNDED, padding=(0, 2),
|
|
1401
|
+
))
|
|
1402
|
+
|
|
1403
|
+
idx = _arrow_select(picker_opts, selected=selected, title="选择要配置的 Provider", max_visible=20)
|
|
1404
|
+
|
|
1405
|
+
if idx < 0:
|
|
1406
|
+
self.context.console.print("[dim]已退出向导[/dim]") if self.context.has_rich else print("已退出")
|
|
1407
|
+
return
|
|
1408
|
+
|
|
1409
|
+
if idx in sep_indices:
|
|
1410
|
+
nxt = next((i for i in range(idx + 1, len(all_items)) if i not in sep_indices), first_real)
|
|
1411
|
+
selected = nxt
|
|
1412
|
+
continue
|
|
1413
|
+
|
|
1414
|
+
provider = key_at[idx]
|
|
1415
|
+
selected = idx
|
|
1416
|
+
|
|
1417
|
+
# ── 显示获取指引 ──────────────────────────────────────────────────
|
|
1418
|
+
guide = _get__PROVIDER_GUIDE().get(provider, "")
|
|
1419
|
+
signup = _get__LLM_SIGNUP_URLS().get(provider) or _get__DATA_SIGNUP_URLS().get(provider, "")
|
|
1420
|
+
if self.context.has_rich:
|
|
1421
|
+
from rich.panel import Panel as _Panel
|
|
1422
|
+
from rich import box as _rbox
|
|
1423
|
+
guide_body = guide
|
|
1424
|
+
if signup:
|
|
1425
|
+
guide_body += f"\n\n[bold cyan]🔗 {signup}[/bold cyan]"
|
|
1426
|
+
current_key = _get_provider_key(provider)
|
|
1427
|
+
if current_key:
|
|
1428
|
+
masked = current_key[:6] + "****" + current_key[-4:] if len(current_key) > 10 else "****"
|
|
1429
|
+
guide_body += f"\n\n[green]当前 key: {masked}[/green] (直接回车保留现有 key)"
|
|
1430
|
+
self.context.console.print()
|
|
1431
|
+
self.context.console.print(_Panel(
|
|
1432
|
+
guide_body,
|
|
1433
|
+
title=f"[bold]{provider.upper()} 配置指引[/bold]",
|
|
1434
|
+
border_style="cyan", box=_rbox.ROUNDED, padding=(0, 2),
|
|
1435
|
+
))
|
|
1436
|
+
else:
|
|
1437
|
+
print(f"\n=== {provider.upper()} ===")
|
|
1438
|
+
print(guide)
|
|
1439
|
+
if signup:
|
|
1440
|
+
print(f"注册地址: {signup}")
|
|
1441
|
+
|
|
1442
|
+
# ── 输入 key ──────────────────────────────────────────────────────
|
|
1443
|
+
prompt_str = f" 请输入 {provider} API Key (输入后不显示): "
|
|
1444
|
+
try:
|
|
1445
|
+
raw_key = _getpass.getpass(prompt_str)
|
|
1446
|
+
except (KeyboardInterrupt, EOFError):
|
|
1447
|
+
self.context.console.print("\n[dim]已跳过[/dim]") if self.context.has_rich else print("\n已跳过")
|
|
1448
|
+
continue
|
|
1449
|
+
|
|
1450
|
+
raw_key = raw_key.strip()
|
|
1451
|
+
if not raw_key:
|
|
1452
|
+
# 保留现有 key,直接回到 picker
|
|
1453
|
+
msg = "未输入 key,保留现有配置"
|
|
1454
|
+
self.context.console.print(f"[dim] {msg}[/dim]") if self.context.has_rich else print(f" {msg}")
|
|
1455
|
+
continue
|
|
1456
|
+
|
|
1457
|
+
# ── 保存 ──────────────────────────────────────────────────────────
|
|
1458
|
+
if provider in _get__DATA_KEY_MAP():
|
|
1459
|
+
_save_data_key(provider, raw_key)
|
|
1460
|
+
env_var = _get__DATA_KEY_MAP()[provider]
|
|
1461
|
+
os.environ[env_var] = raw_key
|
|
1462
|
+
else:
|
|
1463
|
+
pjson_fresh = _load_providers_json()
|
|
1464
|
+
entry = pjson_fresh.get(provider, {})
|
|
1465
|
+
entry["api_key"] = raw_key
|
|
1466
|
+
if provider in _get__PROVIDER_BASE_URLS():
|
|
1467
|
+
entry.setdefault("base_url", _get__PROVIDER_BASE_URLS()[provider])
|
|
1468
|
+
pjson_fresh[provider] = entry
|
|
1469
|
+
_save_providers_json(pjson_fresh)
|
|
1470
|
+
env_var = _get__PROVIDER_KEY_MAP().get(provider)
|
|
1471
|
+
if env_var:
|
|
1472
|
+
os.environ[env_var] = raw_key
|
|
1473
|
+
pjson = pjson_fresh
|
|
1474
|
+
|
|
1475
|
+
masked = raw_key[:6] + "****" + raw_key[-4:] if len(raw_key) > 10 else "****"
|
|
1476
|
+
msg = f"✓ {provider} key 已保存 ({masked})"
|
|
1477
|
+
self.context.console.print(f"[green] {msg}[/green]") if self.context.has_rich else print(f" {msg}")
|
|
1478
|
+
|
|
1479
|
+
# ── 连接测试 ──────────────────────────────────────────────────────
|
|
1480
|
+
print(" 正在测试连接…", end="", flush=True)
|
|
1481
|
+
import asyncio as _aio
|
|
1482
|
+
loop = _aio.get_event_loop()
|
|
1483
|
+
ok, result_msg = await loop.run_in_executor(None, _test_api_key, provider, raw_key)
|
|
1484
|
+
print("\r", end="") # 清除"正在测试"那行
|
|
1485
|
+
if self.context.has_rich:
|
|
1486
|
+
color = "green" if ok else "yellow"
|
|
1487
|
+
self.context.console.print(f" [{color}]{result_msg}[/{color}]")
|
|
1488
|
+
else:
|
|
1489
|
+
print(f" {result_msg}")
|
|
1490
|
+
|
|
1491
|
+
# ── 继续配置其他 provider? ───────────────────────────────────────
|
|
1492
|
+
self.context.console.print() if self.context.has_rich else None
|
|
1493
|
+
try:
|
|
1494
|
+
again = input(" 继续配置其他 provider? (y/N) › ").strip().lower()
|
|
1495
|
+
except (KeyboardInterrupt, EOFError):
|
|
1496
|
+
again = "n"
|
|
1497
|
+
if again not in ("y", "yes", "是"):
|
|
1498
|
+
self.context.console.print("[dim] 向导已完成。输入 /apikey list 查看所有配置。[/dim]") if self.context.has_rich else print("向导完成")
|
|
1499
|
+
return
|
|
1500
|
+
|
|
1501
|
+
def _providers_init(self, *, force: bool = False):
|
|
1502
|
+
"""按本机实际环境生成 ~/.aria/providers.yaml。"""
|
|
1503
|
+
from pathlib import Path as _P
|
|
1504
|
+
|
|
1505
|
+
try:
|
|
1506
|
+
from aria_code.providers.llm.autoconfig import probe_environment, render_providers_yaml
|
|
1507
|
+
except Exception as exc:
|
|
1508
|
+
self.context.console.print(f"[red]无法加载配置生成器: {exc}[/red]" if self.context.has_rich else f"无法加载配置生成器: {exc}")
|
|
1509
|
+
return
|
|
1510
|
+
|
|
1511
|
+
self.context.console.print("[dim]正在探测本机可用的模型来源…[/dim]" if self.context.has_rich else "正在探测…")
|
|
1512
|
+
findings = probe_environment()
|
|
1513
|
+
|
|
1514
|
+
for f in findings:
|
|
1515
|
+
mark = "[green]✓[/green]" if f.available else "[dim]·[/dim]"
|
|
1516
|
+
line = f" {mark} {f.provider:<14} {f.detail}"
|
|
1517
|
+
self.context.console.print(line if self.context.has_rich else line.replace("[green]✓[/green]", "✓").replace("[dim]·[/dim]", "·"))
|
|
1518
|
+
|
|
1519
|
+
target = _P.home() / ".aria" / "providers.yaml"
|
|
1520
|
+
content = render_providers_yaml(findings)
|
|
1521
|
+
|
|
1522
|
+
# 已有配置绝不静默覆盖——那可能是用户手写并调试了很久的东西。
|
|
1523
|
+
if target.exists() and not force:
|
|
1524
|
+
preview = target.with_suffix(".yaml.generated")
|
|
1525
|
+
preview.parent.mkdir(parents=True, exist_ok=True)
|
|
1526
|
+
preview.write_text(content, encoding="utf-8")
|
|
1527
|
+
msg = (f"\n已有配置 {target} 未改动。\n"
|
|
1528
|
+
f"生成结果写到 {preview},比对满意后自行替换,\n"
|
|
1529
|
+
f"或用 /providers init --force 直接覆盖。")
|
|
1530
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]" if self.context.has_rich else msg)
|
|
1531
|
+
return
|
|
1532
|
+
|
|
1533
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1534
|
+
target.write_text(content, encoding="utf-8")
|
|
1535
|
+
self.context.console.print(f"\n[green]✓[/green] 已写入 {target}" if self.context.has_rich else f"\n✓ 已写入 {target}")
|
|
1536
|
+
|
|
1537
|
+
if not any(f.available for f in findings):
|
|
1538
|
+
tip = "这台机器上还没有可用来源。最快:brew install ollama && ollama serve && ollama pull qwen2.5:7b"
|
|
1539
|
+
self.context.console.print(f"[yellow]{tip}[/yellow]" if self.context.has_rich else tip)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def cmd_providers(self, args: str):
|
|
1543
|
+
"""Show all LLM providers: local backends + cloud API status (Open Interpreter style).
|
|
1544
|
+
|
|
1545
|
+
`/providers init` 额外生成一份按本机实际环境定制的 providers.yaml——
|
|
1546
|
+
探测到什么就写什么,缺什么就把"去哪拿"写成注释放进同一个文件,
|
|
1547
|
+
用户不必在文档和配置之间来回对照。
|
|
1548
|
+
"""
|
|
1549
|
+
if args.strip().lower().startswith("init"):
|
|
1550
|
+
return self._providers_init(force="--force" in args)
|
|
1551
|
+
|
|
1552
|
+
if self.context.has_rich:
|
|
1553
|
+
self.context.console.print()
|
|
1554
|
+
|
|
1555
|
+
# ── Section 1: Local backends ────────────────────────────────────────
|
|
1556
|
+
try:
|
|
1557
|
+
from local_llm_provider import probe_local_backends, BACKEND_DEFAULTS
|
|
1558
|
+
results = probe_local_backends()
|
|
1559
|
+
current_provider = self.terminal.config.get("local_provider", "ollama")
|
|
1560
|
+
# Count Ollama models if online
|
|
1561
|
+
_ollama_count = ""
|
|
1562
|
+
if results.get("ollama"):
|
|
1563
|
+
try:
|
|
1564
|
+
_omodels, _ = detect_ollama_models_rich(
|
|
1565
|
+
self.terminal.config.get("ollama_url", "http://localhost:11434"))
|
|
1566
|
+
_ollama_count = f" [dim]{len(_omodels)} 个模型[/dim]" if _omodels else ""
|
|
1567
|
+
except Exception:
|
|
1568
|
+
pass
|
|
1569
|
+
|
|
1570
|
+
if self.context.has_rich:
|
|
1571
|
+
self.context.console.print(" [bold]本地 Backend[/bold]")
|
|
1572
|
+
self.context.console.print()
|
|
1573
|
+
else:
|
|
1574
|
+
print(" == Local Backends ==")
|
|
1575
|
+
|
|
1576
|
+
for name, available in results.items():
|
|
1577
|
+
info = BACKEND_DEFAULTS.get(name, {})
|
|
1578
|
+
url = info.get("default_url", "")
|
|
1579
|
+
color = "green" if available else "dim"
|
|
1580
|
+
icon = "✅" if available else "○"
|
|
1581
|
+
active = " ◀ active" if name == current_provider else ""
|
|
1582
|
+
extra = _ollama_count if (name == "ollama" and available) else ""
|
|
1583
|
+
if self.context.has_rich:
|
|
1584
|
+
self.context.console.print(
|
|
1585
|
+
f" {icon} [{color}]{name:12s}[/{color}]"
|
|
1586
|
+
f" [dim]{url:30s}[/dim]{extra}"
|
|
1587
|
+
f"[green]{active}[/green]"
|
|
1588
|
+
)
|
|
1589
|
+
else:
|
|
1590
|
+
status = "✓" if available else "✗"
|
|
1591
|
+
print(f" {status} {name:12s} {url}{active}")
|
|
1592
|
+
except ImportError:
|
|
1593
|
+
pass
|
|
1594
|
+
|
|
1595
|
+
# ── Section 2: Cloud provider API keys ───────────────────────────────
|
|
1596
|
+
pjson = _load_providers_json()
|
|
1597
|
+
_CLOUD_LIST = [
|
|
1598
|
+
# ── 国际云端 ────────────────────────────────────────────────
|
|
1599
|
+
("deepseek", "DeepSeek", "deepseek/deepseek-chat"),
|
|
1600
|
+
("anthropic", "Anthropic", "anthropic/claude-sonnet-4-6"),
|
|
1601
|
+
("openai", "OpenAI", "openai/gpt-4.5"),
|
|
1602
|
+
("google", "Google Gemini", "google/gemini-3.8-flash"),
|
|
1603
|
+
("xai", "xAI Grok", "xai/grok-3-fast"),
|
|
1604
|
+
("groq", "Groq", "groq/llama-3.3-70b-versatile"),
|
|
1605
|
+
("mistral", "Mistral", "mistral/mistral-large-latest"),
|
|
1606
|
+
("cohere", "Cohere", "cohere/command-r-plus"),
|
|
1607
|
+
("perplexity", "Perplexity", "perplexity/sonar-pro"),
|
|
1608
|
+
("together", "Together", "together/meta-llama/Meta-Llama-3.1-70B"),
|
|
1609
|
+
# ── 国内云端 ────────────────────────────────────────────────
|
|
1610
|
+
("siliconflow", "SiliconFlow", "siliconflow/Qwen/Qwen2.5-7B-Instruct"),
|
|
1611
|
+
("dashscope", "DashScope", "dashscope/qwen-max"),
|
|
1612
|
+
("moonshot", "Moonshot Kimi", "moonshot/moonshot-v1-128k"),
|
|
1613
|
+
("zhipu", "Zhipu GLM", "zhipu/glm-4-plus"),
|
|
1614
|
+
("baidu", "Baidu ERNIE", "baidu/ernie-4.5-turbo-128k"),
|
|
1615
|
+
("bytedance", "ByteDance", "bytedance/<endpoint-id>"),
|
|
1616
|
+
("minimax", "MiniMax", "minimax/MiniMax-Text-01"),
|
|
1617
|
+
("stepfun", "StepFun", "stepfun/step-2-16k"),
|
|
1618
|
+
("01ai", "01.AI Yi", "01ai/yi-large"),
|
|
1619
|
+
]
|
|
1620
|
+
if self.context.has_rich:
|
|
1621
|
+
self.context.console.print()
|
|
1622
|
+
self.context.console.print(" [bold]Cloud Provider API[/bold]")
|
|
1623
|
+
self.context.console.print()
|
|
1624
|
+
else:
|
|
1625
|
+
print()
|
|
1626
|
+
print(" == Cloud Providers ==")
|
|
1627
|
+
|
|
1628
|
+
for prov, label, example_model in _CLOUD_LIST:
|
|
1629
|
+
env_var = _get__PROVIDER_KEY_MAP().get(prov, "")
|
|
1630
|
+
key = (os.getenv(env_var, "") if env_var else "") or \
|
|
1631
|
+
(pjson.get(prov, {}).get("api_key", "") if isinstance(pjson, dict) else "")
|
|
1632
|
+
if key:
|
|
1633
|
+
masked = key[:6] + "****" + key[-4:] if len(key) > 10 else "****"
|
|
1634
|
+
if self.context.has_rich:
|
|
1635
|
+
self.context.console.print(f" 🔑 [green]{label:14s}[/green] [dim]{masked}[/dim]")
|
|
1636
|
+
else:
|
|
1637
|
+
print(f" ✓ {label:14s} {masked}")
|
|
1638
|
+
else:
|
|
1639
|
+
hint = f"/apikey set {prov} <key>"
|
|
1640
|
+
if self.context.has_rich:
|
|
1641
|
+
self.context.console.print(f" ○ [dim]{label:14s} 未配置 → {hint}[/dim]")
|
|
1642
|
+
else:
|
|
1643
|
+
print(f" ✗ {label:14s} {hint}")
|
|
1644
|
+
|
|
1645
|
+
# ── Custom endpoint ──────────────────────────────────────────────────
|
|
1646
|
+
custom_ep = self.terminal.config.get("custom_endpoint", "")
|
|
1647
|
+
custom_m = self.terminal.config.get("custom_model", "")
|
|
1648
|
+
if custom_ep:
|
|
1649
|
+
if self.context.has_rich:
|
|
1650
|
+
self.context.console.print()
|
|
1651
|
+
self.context.console.print(f" 🔧 [bold]Custom endpoint[/bold] [dim]{custom_ep}[/dim] model=[cyan]{custom_m or '?'}[/cyan]")
|
|
1652
|
+
else:
|
|
1653
|
+
print(f"\n Custom: {custom_ep} model={custom_m}")
|
|
1654
|
+
|
|
1655
|
+
# ── Data service keys section ─────────────────────────────────────────
|
|
1656
|
+
_data_keys = _load_data_keys()
|
|
1657
|
+
_DATA_DISPLAY = [
|
|
1658
|
+
("finnhub", "Finnhub", "股票+新闻"),
|
|
1659
|
+
("newsapi", "NewsAPI", "财经新闻"),
|
|
1660
|
+
("brave", "Brave Search", "网页搜索"),
|
|
1661
|
+
("alphavantage", "Alpha Vantage", "历史数据"),
|
|
1662
|
+
("coingecko", "CoinGecko Pro", "加密数据"),
|
|
1663
|
+
("twelvedata", "Twelve Data", "全球行情"),
|
|
1664
|
+
]
|
|
1665
|
+
if self.context.has_rich:
|
|
1666
|
+
self.context.console.print()
|
|
1667
|
+
self.context.console.print(" [bold]📊 数据服务 API[/bold] [dim](后端离线时的本地数据源)[/dim]")
|
|
1668
|
+
self.context.console.print()
|
|
1669
|
+
else:
|
|
1670
|
+
print("\n == Data Service APIs ==")
|
|
1671
|
+
for svc, label, desc in _DATA_DISPLAY:
|
|
1672
|
+
key_val = _data_keys.get(svc, "")
|
|
1673
|
+
if key_val:
|
|
1674
|
+
masked = key_val[:6] + "****" + key_val[-4:] if len(key_val) > 10 else "****"
|
|
1675
|
+
signup = _get__DATA_SIGNUP_URLS().get(svc, "")
|
|
1676
|
+
if self.context.has_rich:
|
|
1677
|
+
self.context.console.print(f" 🔑 [green]{label:18s}[/green] [dim]{masked} {desc}[/dim]")
|
|
1678
|
+
else:
|
|
1679
|
+
print(f" ✓ {label:18s} {masked}")
|
|
1680
|
+
else:
|
|
1681
|
+
hint = f"/apikey set {svc} <key>"
|
|
1682
|
+
signup = _get__DATA_SIGNUP_URLS().get(svc, "")
|
|
1683
|
+
if self.context.has_rich:
|
|
1684
|
+
self.context.console.print(f" ○ [dim]{label:18s} 未配置 → {hint}[/dim]")
|
|
1685
|
+
else:
|
|
1686
|
+
print(f" ✗ {label:18s} {hint}")
|
|
1687
|
+
|
|
1688
|
+
# ── Free data source registry (akshare / yfinance / tushare) ────────────
|
|
1689
|
+
try:
|
|
1690
|
+
from aria_code.datasources.router import DataRouter as _DR
|
|
1691
|
+
free_sources = _DR().list_sources()
|
|
1692
|
+
except Exception:
|
|
1693
|
+
free_sources = []
|
|
1694
|
+
|
|
1695
|
+
if free_sources:
|
|
1696
|
+
if self.context.has_rich:
|
|
1697
|
+
self.context.console.print()
|
|
1698
|
+
self.context.console.print(" [bold]免费行情数据源[/bold] [dim](datasources/router — no API key required)[/dim]")
|
|
1699
|
+
self.context.console.print()
|
|
1700
|
+
else:
|
|
1701
|
+
print("\n == Free Market Data Sources ==")
|
|
1702
|
+
for s in free_sources:
|
|
1703
|
+
ok_icon = "[green]✓[/green]" if s["configured"] else "[dim]○[/dim]"
|
|
1704
|
+
key_tag = " [dim](no key)[/dim]" if not s["needs_key"] else " [dim](API key)[/dim]"
|
|
1705
|
+
mkts = ", ".join(s.get("markets", []))
|
|
1706
|
+
if self.context.has_rich:
|
|
1707
|
+
self.context.console.print(
|
|
1708
|
+
f" {ok_icon} [bold]{s['name']:12s}[/bold] "
|
|
1709
|
+
f"[dim]{mkts:22s}[/dim]{key_tag}"
|
|
1710
|
+
)
|
|
1711
|
+
else:
|
|
1712
|
+
ok = "✓" if s["configured"] else "○"
|
|
1713
|
+
key = "(no key)" if not s["needs_key"] else "(key)"
|
|
1714
|
+
print(f" {ok} {s['name']:12s} {mkts:22s} {key}")
|
|
1715
|
+
if self.context.has_rich:
|
|
1716
|
+
self.context.console.print(" [dim]Config: ~/.aria/datasources.yaml[/dim]")
|
|
1717
|
+
|
|
1718
|
+
if self.context.has_rich:
|
|
1719
|
+
self.context.console.print()
|
|
1720
|
+
self.context.console.print(" [dim]配置 LLM Key: /apikey set deepseek <key>[/dim]")
|
|
1721
|
+
self.context.console.print(" [dim]配置数据 Key: /apikey set finnhub <key>[/dim]")
|
|
1722
|
+
self.context.console.print(" [dim]切换模型: /model deepseek/deepseek-chat[/dim]")
|
|
1723
|
+
self.context.console.print(" [dim]首次向导: /setup[/dim]")
|
|
1724
|
+
self.context.console.print(" [dim]自定义端点: /config set custom_endpoint=http://...[/dim]")
|
|
1725
|
+
self.context.console.print()
|
|
1726
|
+
|
|
1727
|
+
async def cmd_cloud(self, args: str):
|
|
1728
|
+
"""
|
|
1729
|
+
Manage Alibaba Cloud data service connection.
|
|
1730
|
+
|
|
1731
|
+
Usage:
|
|
1732
|
+
/cloud status — show connection status & circuit breaker state
|
|
1733
|
+
/cloud set <url> — set cloud_api_server URL (e.g. http://your-aliyun-ip:8000)
|
|
1734
|
+
/cloud data <url> — set akshare_data_server URL (e.g. http://your-aliyun-ip:8002)
|
|
1735
|
+
/cloud token <jwt-token> — set API token
|
|
1736
|
+
/cloud health — live health-check both services
|
|
1737
|
+
/cloud reset — reset circuit breakers
|
|
1738
|
+
"""
|
|
1739
|
+
try:
|
|
1740
|
+
from aliyun_data_client import AliyunDataClient, save_cloud_config, summarize_cloud_health
|
|
1741
|
+
except ImportError:
|
|
1742
|
+
if self.context.has_rich:
|
|
1743
|
+
self.context.console.print(" [red]aliyun_data_client.py not found[/red]")
|
|
1744
|
+
else:
|
|
1745
|
+
print(" aliyun_data_client.py not found")
|
|
1746
|
+
return
|
|
1747
|
+
|
|
1748
|
+
parts = args.strip().split(None, 2)
|
|
1749
|
+
sub = parts[0].lower() if parts else "status"
|
|
1750
|
+
|
|
1751
|
+
if sub == "set" and len(parts) >= 2:
|
|
1752
|
+
url = parts[1]
|
|
1753
|
+
save_cloud_config(cloud_url=url)
|
|
1754
|
+
AliyunDataClient.reset()
|
|
1755
|
+
if self.context.has_rich:
|
|
1756
|
+
self.context.console.print(f" [green]Cloud API URL set to: {url}[/green]")
|
|
1757
|
+
self.context.console.print(" [dim]Saved to ~/.arthera/config.json[/dim]")
|
|
1758
|
+
return
|
|
1759
|
+
|
|
1760
|
+
if sub == "data" and len(parts) >= 2:
|
|
1761
|
+
url = parts[1]
|
|
1762
|
+
save_cloud_config(data_url=url)
|
|
1763
|
+
AliyunDataClient.reset()
|
|
1764
|
+
if self.context.has_rich:
|
|
1765
|
+
self.context.console.print(f" [green]AKShare Data URL set to: {url}[/green]")
|
|
1766
|
+
self.context.console.print(" [dim]Saved to ~/.arthera/config.json[/dim]")
|
|
1767
|
+
return
|
|
1768
|
+
|
|
1769
|
+
if sub == "token" and len(parts) >= 2:
|
|
1770
|
+
token = parts[1]
|
|
1771
|
+
save_cloud_config(api_token=token)
|
|
1772
|
+
AliyunDataClient.reset()
|
|
1773
|
+
if self.context.has_rich:
|
|
1774
|
+
self.context.console.print(f" [green]API token saved (length {len(token)})[/green]")
|
|
1775
|
+
return
|
|
1776
|
+
|
|
1777
|
+
if sub == "reset":
|
|
1778
|
+
AliyunDataClient.reset()
|
|
1779
|
+
if self.context.has_rich:
|
|
1780
|
+
self.context.console.print(" [green]Circuit breakers reset, config reloaded[/green]")
|
|
1781
|
+
return
|
|
1782
|
+
|
|
1783
|
+
client = AliyunDataClient.get()
|
|
1784
|
+
|
|
1785
|
+
if sub == "health":
|
|
1786
|
+
if self.context.has_rich:
|
|
1787
|
+
self.context.console.print(" [dim]Checking health…[/dim]")
|
|
1788
|
+
with self.context.console.status("[dim]Checking cloud services…[/dim]", spinner="dots") if self.context.has_rich else _null_ctx():
|
|
1789
|
+
cloud_h = await client.health_cloud()
|
|
1790
|
+
data_h = await client.health_data()
|
|
1791
|
+
st = client.status()
|
|
1792
|
+
summary = summarize_cloud_health(cloud_h, data_h, st)
|
|
1793
|
+
|
|
1794
|
+
def _svc_label(name: str, health: dict) -> str:
|
|
1795
|
+
status = str(health.get("status", "?"))
|
|
1796
|
+
ok = status in ("healthy", "ok", "ready", "online")
|
|
1797
|
+
color = "green" if ok else "red"
|
|
1798
|
+
icon = "✓" if ok else "✗"
|
|
1799
|
+
breaker = st.get("cloud_cb" if name == "cloud_api_server" else "data_cb", "?")
|
|
1800
|
+
return f" [{color}]●[/{color}] {name} {icon} {status} [dim]breaker={breaker}[/dim]"
|
|
1801
|
+
|
|
1802
|
+
def _print_health_detail(title: str, health: dict):
|
|
1803
|
+
if self.context.has_rich:
|
|
1804
|
+
self.context.console.print()
|
|
1805
|
+
self.context.console.print(_svc_label(title, health))
|
|
1806
|
+
detail_keys = [
|
|
1807
|
+
(k, v) for k, v in health.items()
|
|
1808
|
+
if k not in {"status", "services", "cloud_url", "data_url"}
|
|
1809
|
+
]
|
|
1810
|
+
if detail_keys:
|
|
1811
|
+
for k, v in detail_keys:
|
|
1812
|
+
self.context.console.print(f" [dim]{k}: {v}[/dim]")
|
|
1813
|
+
services = health.get("services") or {}
|
|
1814
|
+
if services:
|
|
1815
|
+
for svc, svc_status in services.items():
|
|
1816
|
+
svc_ok = "online" in str(svc_status) or "ready" in str(svc_status)
|
|
1817
|
+
svc_icon = "✓" if svc_ok else "○"
|
|
1818
|
+
svc_color = "green" if svc_ok else "yellow"
|
|
1819
|
+
self.context.console.print(f" [dim]{svc_icon} {svc}: [{svc_color}]{svc_status}[/{svc_color}][/dim]")
|
|
1820
|
+
else:
|
|
1821
|
+
print(f" {title}: {health.get('status', '?')}")
|
|
1822
|
+
for k, v in health.items():
|
|
1823
|
+
if k not in {"status", "services", "cloud_url", "data_url"}:
|
|
1824
|
+
print(f" {k}: {v}")
|
|
1825
|
+
for svc, svc_status in (health.get("services") or {}).items():
|
|
1826
|
+
print(f" {svc}: {svc_status}")
|
|
1827
|
+
|
|
1828
|
+
if self.context.has_rich:
|
|
1829
|
+
self.context.console.print()
|
|
1830
|
+
color = "green" if summary.status == "ok" else "yellow" if summary.status == "warn" else "red"
|
|
1831
|
+
self.context.console.print(f" [bold]Summary[/bold] [{color}]{summary.detail}[/{color}]")
|
|
1832
|
+
self.context.console.print(f" [dim]breaker_open={summary.breaker_open} token_set={summary.token_set}[/dim]")
|
|
1833
|
+
self.context.console.print(f" [dim]suggestion: {summary.suggestion}[/dim]")
|
|
1834
|
+
self.context.console.print(f" [dim]cloud_api_server: {client.cloud_url}[/dim]")
|
|
1835
|
+
self.context.console.print(f" [dim]akshare_data_server: {client.data_url}[/dim]")
|
|
1836
|
+
_print_health_detail("cloud_api_server", cloud_h)
|
|
1837
|
+
_print_health_detail("akshare_data_server", data_h)
|
|
1838
|
+
self.context.console.print()
|
|
1839
|
+
else:
|
|
1840
|
+
print(f" Summary: {summary.detail} ({summary.status})")
|
|
1841
|
+
print(f" breaker_open={summary.breaker_open} token_set={summary.token_set}")
|
|
1842
|
+
print(f" suggestion: {summary.suggestion}")
|
|
1843
|
+
_print_health_detail("cloud_api_server", cloud_h)
|
|
1844
|
+
_print_health_detail("akshare_data_server", data_h)
|
|
1845
|
+
return
|
|
1846
|
+
|
|
1847
|
+
# Default: /cloud status
|
|
1848
|
+
st = client.status()
|
|
1849
|
+
if self.context.has_rich:
|
|
1850
|
+
self.context.console.print()
|
|
1851
|
+
self.context.console.print(" [bold]Alibaba Cloud Data Services[/bold]")
|
|
1852
|
+
self.context.console.print()
|
|
1853
|
+
health_summary = st.get("health_summary") or {}
|
|
1854
|
+
color = "green" if health_summary.get("status") == "ok" else "yellow" if health_summary.get("status") == "warn" else "red"
|
|
1855
|
+
if health_summary:
|
|
1856
|
+
self.context.console.print(f" [bold]Health[/bold] [{color}]{health_summary.get('detail', '')}[/{color}]")
|
|
1857
|
+
self.context.console.print(f" [dim]breaker_open={health_summary.get('breaker_open', 0)} token_set={health_summary.get('token_set', False)}[/dim]")
|
|
1858
|
+
_c = "green" if st["cloud_cb"] == "closed" else "red"
|
|
1859
|
+
_d = "green" if st["data_cb"] == "closed" else "red"
|
|
1860
|
+
self.context.console.print(f" [{_c}]●[/{_c}] cloud_api_server [dim]{st['cloud_url']}[/dim]"
|
|
1861
|
+
f" [{_c}]{st['cloud_cb']}[/{_c}]")
|
|
1862
|
+
self.context.console.print(f" [{_d}]●[/{_d}] akshare_data_server [dim]{st['data_url']}[/dim]"
|
|
1863
|
+
f" [{_d}]{st['data_cb']}[/{_d}]")
|
|
1864
|
+
tok_str = "[green]set[/green]" if st["has_token"] else "[dim]not set[/dim]"
|
|
1865
|
+
self.context.console.print(f" Auth token: {tok_str}")
|
|
1866
|
+
self.context.console.print()
|
|
1867
|
+
self.context.console.print(" [dim]Configure: /cloud set <url> /cloud data <url> /cloud token <jwt>[/dim]")
|
|
1868
|
+
self.context.console.print(" [dim]Health: /cloud health[/dim]")
|
|
1869
|
+
self.context.console.print()
|
|
1870
|
+
else:
|
|
1871
|
+
health_summary = st.get("health_summary") or {}
|
|
1872
|
+
if health_summary:
|
|
1873
|
+
print(f" Health: {health_summary.get('detail', '')} ({health_summary.get('status', '')})")
|
|
1874
|
+
print(f" Cloud: {st['cloud_url']} ({st['cloud_cb']})")
|
|
1875
|
+
print(f" Data: {st['data_url']} ({st['data_cb']})")
|
|
1876
|
+
print(f" Token: {'set' if st['has_token'] else 'not set'}")
|
|
1877
|
+
|
|
1878
|
+
def cmd_permissions(self, args: str):
|
|
1879
|
+
"""Tool permission policy manager.
|
|
1880
|
+
|
|
1881
|
+
/permissions — show current policy table
|
|
1882
|
+
/permissions allow <tool> — auto-approve this tool forever
|
|
1883
|
+
/permissions deny <tool> — permanently block this tool
|
|
1884
|
+
/permissions ask <tool> — always prompt before this tool
|
|
1885
|
+
/permissions reset — clear all custom rules
|
|
1886
|
+
/permissions remove <tool>— remove this tool from policy
|
|
1887
|
+
"""
|
|
1888
|
+
try:
|
|
1889
|
+
from aria_code.runtime.tool_policy import (
|
|
1890
|
+
load_tool_policy, save_tool_policy,
|
|
1891
|
+
add_to_policy, remove_from_policy,
|
|
1892
|
+
)
|
|
1893
|
+
except ImportError as _e:
|
|
1894
|
+
self.context.console.print(f"[red]runtime.tool_policy not available: {_e}[/red]") if self.context.has_rich else print(f"Error: {_e}")
|
|
1895
|
+
return
|
|
1896
|
+
|
|
1897
|
+
parts = args.strip().split(maxsplit=1)
|
|
1898
|
+
sub = parts[0].lower() if parts else "show"
|
|
1899
|
+
|
|
1900
|
+
if sub in ("allow", "deny", "ask"):
|
|
1901
|
+
if len(parts) < 2:
|
|
1902
|
+
msg = f"Usage: /permissions {sub} <tool_name>"
|
|
1903
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
1904
|
+
return
|
|
1905
|
+
tool = parts[1].strip()
|
|
1906
|
+
add_to_policy(tool, sub)
|
|
1907
|
+
labels = {"allow": ("[green]✓ 白名单[/green]", "auto-approve"), "deny": ("[red]✗ 黑名单[/red]", "block"), "ask": ("[yellow]? 询问[/yellow]", "ask-always")}
|
|
1908
|
+
rich_label, plain_label = labels[sub]
|
|
1909
|
+
if self.context.has_rich:
|
|
1910
|
+
self.context.console.print(f" {rich_label} [bold]{tool}[/bold] [dim]已更新[/dim]")
|
|
1911
|
+
else:
|
|
1912
|
+
print(f" {plain_label}: {tool}")
|
|
1913
|
+
return
|
|
1914
|
+
|
|
1915
|
+
if sub == "reset":
|
|
1916
|
+
save_tool_policy({"allowed": [], "denied": [], "ask_always": []})
|
|
1917
|
+
msg = "✓ 工具权限策略已重置"
|
|
1918
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1919
|
+
return
|
|
1920
|
+
|
|
1921
|
+
if sub in ("remove", "rm"):
|
|
1922
|
+
if len(parts) < 2:
|
|
1923
|
+
msg = "Usage: /permissions remove <tool_name>"
|
|
1924
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
1925
|
+
return
|
|
1926
|
+
tool = parts[1].strip()
|
|
1927
|
+
if remove_from_policy(tool):
|
|
1928
|
+
msg = f"✓ '{tool}' 已从策略中移除"
|
|
1929
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
1930
|
+
else:
|
|
1931
|
+
msg = f"'{tool}' 不在任何策略列表中"
|
|
1932
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
1933
|
+
return
|
|
1934
|
+
|
|
1935
|
+
# Default: show policy table
|
|
1936
|
+
policy = load_tool_policy()
|
|
1937
|
+
allowed = policy.get("allowed", [])
|
|
1938
|
+
denied = policy.get("denied", [])
|
|
1939
|
+
ask_always = policy.get("ask_always", [])
|
|
1940
|
+
|
|
1941
|
+
if self.context.has_rich:
|
|
1942
|
+
from rich.table import Table
|
|
1943
|
+
from rich import box as _rbox
|
|
1944
|
+
self.context.console.print()
|
|
1945
|
+
self.context.console.print(" [bold]Tool Permissions[/bold] [dim](~/.arthera/tool_policy.json)[/dim]")
|
|
1946
|
+
self.context.console.print()
|
|
1947
|
+
t = Table(box=_rbox.SIMPLE, padding=(0, 1), show_header=True)
|
|
1948
|
+
t.add_column("Tool", style="bold", min_width=22)
|
|
1949
|
+
t.add_column("Policy", min_width=12)
|
|
1950
|
+
t.add_column("Effect", style="dim")
|
|
1951
|
+
for tool in sorted(allowed):
|
|
1952
|
+
t.add_row(tool, "[green]✓ allow[/green]", "auto-approve, never prompt")
|
|
1953
|
+
for tool in sorted(denied):
|
|
1954
|
+
t.add_row(tool, "[red]✗ deny[/red]", "always reject, never run")
|
|
1955
|
+
for tool in sorted(ask_always):
|
|
1956
|
+
t.add_row(tool, "[yellow]? ask[/yellow]", "always prompt even in auto mode")
|
|
1957
|
+
if not (allowed or denied or ask_always):
|
|
1958
|
+
t.add_row("[dim]— no custom rules —[/dim]", "", "")
|
|
1959
|
+
self.context.console.print(t)
|
|
1960
|
+
self.context.console.print(" [dim]/permissions allow <tool> — 白名单(自动批准)[/dim]")
|
|
1961
|
+
self.context.console.print(" [dim]/permissions deny <tool> — 黑名单(始终拒绝)[/dim]")
|
|
1962
|
+
self.context.console.print(" [dim]/permissions ask <tool> — 始终询问[/dim]")
|
|
1963
|
+
self.context.console.print(" [dim]/permissions remove <tool> — 移除规则[/dim]")
|
|
1964
|
+
self.context.console.print(" [dim]/permissions reset — 清空所有规则[/dim]")
|
|
1965
|
+
self.context.console.print()
|
|
1966
|
+
else:
|
|
1967
|
+
print("\n Tool Permissions (~/.arthera/tool_policy.json)")
|
|
1968
|
+
print(f" Allow: {', '.join(allowed) or 'none'}")
|
|
1969
|
+
print(f" Deny: {', '.join(denied) or 'none'}")
|
|
1970
|
+
print(f" Ask: {', '.join(ask_always) or 'none'}")
|
|
1971
|
+
print()
|
|
1972
|
+
print(" Usage: /permissions allow|deny|ask|remove|reset <tool>")
|
|
1973
|
+
|
|
1974
|
+
def cmd_config(self, args: str):
|
|
1975
|
+
"""Show or set CLI configuration."""
|
|
1976
|
+
from aria_code.apps.cli.config_paths import config_snapshot
|
|
1977
|
+
parts = args.strip().split(maxsplit=1)
|
|
1978
|
+
if not parts or parts[0] == "show":
|
|
1979
|
+
# Show current config
|
|
1980
|
+
cfg = self.terminal.config
|
|
1981
|
+
if self.context.has_rich:
|
|
1982
|
+
self.context.console.print()
|
|
1983
|
+
self.context.console.print("[bold]Configuration[/bold]")
|
|
1984
|
+
self.context.console.print()
|
|
1985
|
+
snap = config_snapshot()
|
|
1986
|
+
for key in ("api_url", "ollama_url", "local_provider", "model",
|
|
1987
|
+
"provider_fallback", "llm_base_url", "thinking_mode",
|
|
1988
|
+
"command_policy", "permission_mode", "network_enabled",
|
|
1989
|
+
"approval_mode", "auto_approve_level", "review_gate", "review_model", "delivery_report",
|
|
1990
|
+
"write_policy", "lsp_autocheck", "input_style", "input_theme",
|
|
1991
|
+
"response_footer", "auto_compact_context",
|
|
1992
|
+
"auto_compact_threshold", "auto_save_sessions"):
|
|
1993
|
+
val = cfg.get(key, "-")
|
|
1994
|
+
self.context.console.print(f" [dim]{key:<24s}[/dim]{val}")
|
|
1995
|
+
self.context.console.print(f" [dim]{'config_dir':<24s}[/dim]{snap['config_dir']}")
|
|
1996
|
+
self.context.console.print(f" [dim]{'config_file':<24s}[/dim]{snap['config_file']}")
|
|
1997
|
+
self.context.console.print(f" [dim]{'sessions_dir':<24s}[/dim]{snap['sessions_dir']}")
|
|
1998
|
+
self.context.console.print(f" [dim]{'user_output_root':<24s}[/dim]{snap['user_output_root']}")
|
|
1999
|
+
# Show notification/search config from resolved config.json
|
|
2000
|
+
try:
|
|
2001
|
+
import json as _j
|
|
2002
|
+
_ncfg_path = pathlib.Path(snap["config_file"])
|
|
2003
|
+
_ncfg = _j.loads(_ncfg_path.read_text()) if _ncfg_path.exists() else {}
|
|
2004
|
+
if _wh := _ncfg.get("notify_webhook"):
|
|
2005
|
+
self.context.console.print(f" [dim]{'notify_webhook':<24s}[/dim]{_wh[:50]}{'…' if len(_wh)>50 else ''}")
|
|
2006
|
+
except Exception:
|
|
2007
|
+
pass
|
|
2008
|
+
import os as _os_show
|
|
2009
|
+
if _os_show.getenv("BRAVE_SEARCH_API_KEY"):
|
|
2010
|
+
self.context.console.print(f" [dim]{'brave_key':<24s}[/dim][green]已配置[/green]")
|
|
2011
|
+
else:
|
|
2012
|
+
self.context.console.print(f" [dim]{'brave_key':<24s}[/dim][dim]未设置 — /config set brave_key=BSAAxxx[/dim]")
|
|
2013
|
+
# Security check: warn if providers.json has plaintext api_key
|
|
2014
|
+
_pf = pathlib.Path(snap["providers_file"])
|
|
2015
|
+
if _pf.exists():
|
|
2016
|
+
try:
|
|
2017
|
+
_pd = _j.loads(_pf.read_text())
|
|
2018
|
+
_has_plain = any(
|
|
2019
|
+
v.get("api_key") for v in _pd.values()
|
|
2020
|
+
if isinstance(v, dict) and v.get("api_key")
|
|
2021
|
+
and not str(v["api_key"]).startswith("${")
|
|
2022
|
+
)
|
|
2023
|
+
if _has_plain:
|
|
2024
|
+
self.context.console.print()
|
|
2025
|
+
self.context.console.print(
|
|
2026
|
+
" [yellow]⚠ ~/.arthera/providers.json 含明文 API Key[/yellow]\n"
|
|
2027
|
+
" [dim] 建议迁移到环境变量: export OPENAI_API_KEY=sk-...[/dim]\n"
|
|
2028
|
+
" [dim] 然后删除 providers.json 中的 api_key 字段[/dim]"
|
|
2029
|
+
)
|
|
2030
|
+
except Exception:
|
|
2031
|
+
pass
|
|
2032
|
+
self.context.console.print()
|
|
2033
|
+
else:
|
|
2034
|
+
for key in ("api_url", "ollama_url", "local_provider", "model",
|
|
2035
|
+
"provider_fallback", "llm_base_url", "thinking_mode",
|
|
2036
|
+
"command_policy", "permission_mode", "network_enabled",
|
|
2037
|
+
"approval_mode", "auto_approve_level", "review_gate", "review_model", "delivery_report",
|
|
2038
|
+
"write_policy", "lsp_autocheck", "input_style", "input_theme",
|
|
2039
|
+
"response_footer", "auto_compact_context",
|
|
2040
|
+
"auto_compact_threshold"):
|
|
2041
|
+
print(f" {key}: {cfg.get(key, '-')}")
|
|
2042
|
+
elif len(parts) == 2 and parts[0] == "set":
|
|
2043
|
+
# Parse key=value
|
|
2044
|
+
kv = parts[1].split("=", 1)
|
|
2045
|
+
if len(kv) == 2:
|
|
2046
|
+
key, val = kv[0].strip(), kv[1].strip()
|
|
2047
|
+
# Validate known config keys
|
|
2048
|
+
if key == "command_policy":
|
|
2049
|
+
if val not in {"safe", "balanced", "full"}:
|
|
2050
|
+
msg = "command_policy must be one of: safe | balanced | full"
|
|
2051
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2052
|
+
return
|
|
2053
|
+
elif key == "permission_mode":
|
|
2054
|
+
if val not in {"read-only", "workspace-write", "full-access"}:
|
|
2055
|
+
msg = "permission_mode must be one of: read-only | workspace-write | full-access"
|
|
2056
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2057
|
+
return
|
|
2058
|
+
elif key == "approval_mode":
|
|
2059
|
+
if val not in {"manual", "risk"}:
|
|
2060
|
+
msg = ("approval_mode must be one of: manual | risk "
|
|
2061
|
+
"(risk: auto-approve actions at or below auto_approve_level)")
|
|
2062
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2063
|
+
return
|
|
2064
|
+
elif key == "auto_approve_level":
|
|
2065
|
+
if val not in {"0", "1", "2"}:
|
|
2066
|
+
msg = "auto_approve_level must be 0, 1 or 2 (L3 and L4 always ask)"
|
|
2067
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2068
|
+
return
|
|
2069
|
+
val = int(val)
|
|
2070
|
+
elif key in {"network_enabled", "data_sharing", "feedback_upload", "review_gate", "delivery_report"}:
|
|
2071
|
+
if val.lower() in {"true", "1", "yes", "on"}:
|
|
2072
|
+
val = True
|
|
2073
|
+
elif val.lower() in {"false", "0", "no", "off"}:
|
|
2074
|
+
val = False
|
|
2075
|
+
else:
|
|
2076
|
+
msg = f"{key} must be: true | false"
|
|
2077
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2078
|
+
return
|
|
2079
|
+
elif key == "review_model":
|
|
2080
|
+
# Another provider's model reads the diff; "off" (or empty) goes back to the builder.
|
|
2081
|
+
from aria_code.apps.cli.providers.chat_routing import KNOWN_MODEL_PROVIDERS, normalize_provider_name
|
|
2082
|
+
|
|
2083
|
+
if val.lower() in {"", "off", "none"}:
|
|
2084
|
+
val = ""
|
|
2085
|
+
else:
|
|
2086
|
+
provider = normalize_provider_name(val.split("/", 1)[0]) if "/" in val else ""
|
|
2087
|
+
if provider not in KNOWN_MODEL_PROVIDERS:
|
|
2088
|
+
msg = ("review_model must name a provider and model, e.g. anthropic/claude-sonnet-5-5, "
|
|
2089
|
+
"openai/<model> or google/gemini-3.8-flash (or 'off'). "
|
|
2090
|
+
"The diff and goal are sent to that provider.")
|
|
2091
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2092
|
+
return
|
|
2093
|
+
elif key == "thinking_mode":
|
|
2094
|
+
if val not in {"auto", "instant", "thinking"}:
|
|
2095
|
+
msg = "thinking_mode must be one of: auto | instant | thinking"
|
|
2096
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2097
|
+
return
|
|
2098
|
+
elif key == "provider_fallback":
|
|
2099
|
+
if val not in {"off", "configured", "auto"}:
|
|
2100
|
+
msg = "provider_fallback must be: off | configured | auto"
|
|
2101
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2102
|
+
return
|
|
2103
|
+
elif key == "local_provider":
|
|
2104
|
+
from aria_code.apps.cli.providers.chat_routing import normalize_provider_name
|
|
2105
|
+
|
|
2106
|
+
val = normalize_provider_name(val)
|
|
2107
|
+
elif key == "model":
|
|
2108
|
+
resolved = _get_MODEL_ALIASES().get(val) or (val if val in _get_MODELS() else None)
|
|
2109
|
+
if not resolved:
|
|
2110
|
+
valid = ", ".join(sorted(_get_MODEL_ALIASES().keys()))
|
|
2111
|
+
msg = f"Unknown model '{val}'. Valid: {valid}"
|
|
2112
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2113
|
+
return
|
|
2114
|
+
val = _get_MODELS()[resolved]["id"]
|
|
2115
|
+
elif key == "auto_save_sessions":
|
|
2116
|
+
if val.lower() in {"true", "1", "yes", "on"}:
|
|
2117
|
+
val = True
|
|
2118
|
+
elif val.lower() in {"false", "0", "no", "off"}:
|
|
2119
|
+
val = False
|
|
2120
|
+
else:
|
|
2121
|
+
msg = "auto_save_sessions must be: true | false"
|
|
2122
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2123
|
+
return
|
|
2124
|
+
elif key == "auto_compact_context":
|
|
2125
|
+
if val.lower() in {"true", "1", "yes", "on"}:
|
|
2126
|
+
val = True
|
|
2127
|
+
elif val.lower() in {"false", "0", "no", "off"}:
|
|
2128
|
+
val = False
|
|
2129
|
+
else:
|
|
2130
|
+
msg = "auto_compact_context must be: true | false"
|
|
2131
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2132
|
+
return
|
|
2133
|
+
elif key == "lsp_autocheck":
|
|
2134
|
+
if val.lower() in {"true", "1", "yes", "on"}:
|
|
2135
|
+
val = True
|
|
2136
|
+
elif val.lower() in {"false", "0", "no", "off"}:
|
|
2137
|
+
val = False
|
|
2138
|
+
else:
|
|
2139
|
+
msg = "lsp_autocheck must be: true | false"
|
|
2140
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2141
|
+
return
|
|
2142
|
+
elif key == "auto_compact_threshold":
|
|
2143
|
+
try:
|
|
2144
|
+
val = float(val)
|
|
2145
|
+
except Exception:
|
|
2146
|
+
msg = "auto_compact_threshold must be a number between 0.50 and 0.95"
|
|
2147
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2148
|
+
return
|
|
2149
|
+
if not 0.50 <= val <= 0.95:
|
|
2150
|
+
msg = "auto_compact_threshold must be between 0.50 and 0.95"
|
|
2151
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2152
|
+
return
|
|
2153
|
+
elif key == "write_policy":
|
|
2154
|
+
if val not in {"desktop_only", "confirm_outside", "always_confirm"}:
|
|
2155
|
+
msg = "write_policy must be: desktop_only | confirm_outside | always_confirm"
|
|
2156
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2157
|
+
return
|
|
2158
|
+
elif key == "local_mode":
|
|
2159
|
+
if val.lower() in {"true", "1", "yes", "on"}:
|
|
2160
|
+
val = True
|
|
2161
|
+
elif val.lower() in {"false", "0", "no", "off"}:
|
|
2162
|
+
val = False
|
|
2163
|
+
else:
|
|
2164
|
+
msg = "local_mode must be: true | false"
|
|
2165
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2166
|
+
return
|
|
2167
|
+
elif key == "banner":
|
|
2168
|
+
if val not in {"full", "compact", "off"}:
|
|
2169
|
+
msg = "banner must be: full | compact | off"
|
|
2170
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2171
|
+
return
|
|
2172
|
+
elif key == "input_style":
|
|
2173
|
+
if val not in {"panel", "box", "plain"}:
|
|
2174
|
+
msg = "input_style must be: panel | box | plain"
|
|
2175
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2176
|
+
return
|
|
2177
|
+
elif key == "input_theme":
|
|
2178
|
+
if val not in {"auto", "dark", "light"}:
|
|
2179
|
+
msg = "input_theme must be: auto | dark | light"
|
|
2180
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2181
|
+
return
|
|
2182
|
+
elif key == "response_footer":
|
|
2183
|
+
if val not in {"compact", "full", "off"}:
|
|
2184
|
+
msg = "response_footer must be: compact | full | off"
|
|
2185
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2186
|
+
return
|
|
2187
|
+
elif key == "ui_lang":
|
|
2188
|
+
if val not in {"zh", "en", "ja", "ko", "auto"}:
|
|
2189
|
+
msg = "ui_lang must be: zh | en | auto (auto = detect from OS locale)"
|
|
2190
|
+
self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
|
|
2191
|
+
return
|
|
2192
|
+
if val == "auto":
|
|
2193
|
+
try:
|
|
2194
|
+
from aria_code.apps.cli.i18n import detect_system_lang as _dsl
|
|
2195
|
+
val = _dsl()
|
|
2196
|
+
except Exception:
|
|
2197
|
+
val = "en"
|
|
2198
|
+
msg = f"✓ UI 语言已设为 {val} (重启生效)" if val == "zh" else f"✓ UI language set to {val} (takes effect on restart)"
|
|
2199
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2200
|
+
self.terminal.config[key] = val
|
|
2201
|
+
self.context.save_config(self.terminal.config)
|
|
2202
|
+
return
|
|
2203
|
+
elif key == "notify_webhook":
|
|
2204
|
+
# /config set notify_webhook=https://qyapi.weixin.qq.com/...
|
|
2205
|
+
# 写入 ~/.arthera/config.json(notification_tools 直接读取)
|
|
2206
|
+
try:
|
|
2207
|
+
_ncfg_path = aria_home() / "config.json"
|
|
2208
|
+
_ncfg = json.loads(_ncfg_path.read_text()) if _ncfg_path.exists() else {}
|
|
2209
|
+
_ncfg["notify_webhook"] = val
|
|
2210
|
+
_ncfg_path.write_text(json.dumps(_ncfg, indent=2, ensure_ascii=False))
|
|
2211
|
+
except Exception as _e:
|
|
2212
|
+
logger.debug("notify_webhook save failed: %s", _e)
|
|
2213
|
+
msg = f"✓ 通知 Webhook 已设为 {val[:60]}…" if len(val) > 60 else f"✓ 通知 Webhook 已设为 {val}"
|
|
2214
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2215
|
+
return
|
|
2216
|
+
elif key == "brave_key":
|
|
2217
|
+
# /config set brave_key=BSAAxxx → 写入 ~/.aria/.env
|
|
2218
|
+
_env_path = pathlib.Path.home() / ".aria" / ".env"
|
|
2219
|
+
_env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2220
|
+
existing = _env_path.read_text() if _env_path.exists() else ""
|
|
2221
|
+
import re as _re_cfg
|
|
2222
|
+
if "BRAVE_SEARCH_API_KEY" in existing:
|
|
2223
|
+
existing = _re_cfg.sub(r"BRAVE_SEARCH_API_KEY=.*", f"BRAVE_SEARCH_API_KEY={val}", existing)
|
|
2224
|
+
else:
|
|
2225
|
+
existing = existing.rstrip("\n") + f"\nBRAVE_SEARCH_API_KEY={val}\n"
|
|
2226
|
+
_env_path.write_text(existing)
|
|
2227
|
+
_env_path.chmod(0o600)
|
|
2228
|
+
import os as _os_cfg
|
|
2229
|
+
_os_cfg.environ["BRAVE_SEARCH_API_KEY"] = val
|
|
2230
|
+
msg = "✓ Brave Search API key 已保存到~/.aria/.env (生效于当前会话)"
|
|
2231
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2232
|
+
return
|
|
2233
|
+
elif key == "custom_endpoint":
|
|
2234
|
+
# /config set custom_endpoint=http://my-litellm:4000/v1
|
|
2235
|
+
# Automatically sets local_provider=custom
|
|
2236
|
+
self.terminal.config["local_provider"] = "custom"
|
|
2237
|
+
self.terminal.config["custom_endpoint"] = val
|
|
2238
|
+
_sync_write_policy(self.terminal.config)
|
|
2239
|
+
self.context.save_config(self.terminal.config)
|
|
2240
|
+
msg = f"✓ 自定义 endpoint 设为 {val} (local_provider=custom)"
|
|
2241
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2242
|
+
return
|
|
2243
|
+
elif key == "custom_model":
|
|
2244
|
+
# /config set custom_model=gpt-4o
|
|
2245
|
+
self.terminal.config["custom_model"] = val
|
|
2246
|
+
if self.terminal.config.get("local_provider") == "custom":
|
|
2247
|
+
self.terminal.config["model"] = val
|
|
2248
|
+
_sync_write_policy(self.terminal.config)
|
|
2249
|
+
self.context.save_config(self.terminal.config)
|
|
2250
|
+
self.context.console.print(f" [dim]custom_model[/dim] = {val}" if self.context.has_rich else f" custom_model = {val}")
|
|
2251
|
+
return
|
|
2252
|
+
self.terminal.config[key] = val
|
|
2253
|
+
_sync_write_policy(self.terminal.config)
|
|
2254
|
+
self.context.save_config(self.terminal.config)
|
|
2255
|
+
self.context.console.print(f" [dim]{key}[/dim] = {val}" if self.context.has_rich else f" {key} = {val}")
|
|
2256
|
+
else:
|
|
2257
|
+
self.context.console.print("[dim]Usage: /config set key=value[/dim]" if self.context.has_rich
|
|
2258
|
+
else "Usage: /config set key=value")
|
|
2259
|
+
elif parts[0] == "reload":
|
|
2260
|
+
fresh = load_config()
|
|
2261
|
+
self.terminal.config.update(fresh)
|
|
2262
|
+
msg = f"Config reloaded from {config_snapshot()['config_file']}"
|
|
2263
|
+
self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
|
|
2264
|
+
|
|
2265
|
+
elif parts[0] == "allow":
|
|
2266
|
+
# /config allow <tool_name> — permanently auto-approve this tool
|
|
2267
|
+
if len(parts) < 2:
|
|
2268
|
+
msg = "Usage: /config allow <tool_name> (e.g. /config allow read_file)"
|
|
2269
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
2270
|
+
return
|
|
2271
|
+
tool = parts[1].strip()
|
|
2272
|
+
try:
|
|
2273
|
+
from aria_code.runtime.tool_policy import add_to_policy
|
|
2274
|
+
add_to_policy(tool, "allow")
|
|
2275
|
+
msg = f"✓ 工具 '{tool}' 加入永久白名单(始终自动批准,无需确认)"
|
|
2276
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2277
|
+
except Exception as _e:
|
|
2278
|
+
self.context.console.print(f"[red]Error: {_e}[/red]") if self.context.has_rich else print(f"Error: {_e}")
|
|
2279
|
+
|
|
2280
|
+
elif parts[0] == "deny":
|
|
2281
|
+
# /config deny <tool_name> — permanently block this tool
|
|
2282
|
+
if len(parts) < 2:
|
|
2283
|
+
msg = "Usage: /config deny <tool_name> (e.g. /config deny run_command)"
|
|
2284
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
2285
|
+
return
|
|
2286
|
+
tool = parts[1].strip()
|
|
2287
|
+
try:
|
|
2288
|
+
from aria_code.runtime.tool_policy import add_to_policy
|
|
2289
|
+
add_to_policy(tool, "deny")
|
|
2290
|
+
msg = f"✓ 工具 '{tool}' 加入黑名单(始终拒绝,不会执行)"
|
|
2291
|
+
self.context.console.print(f"[red]{msg}[/red]") if self.context.has_rich else print(msg)
|
|
2292
|
+
except Exception as _e:
|
|
2293
|
+
self.context.console.print(f"[red]Error: {_e}[/red]") if self.context.has_rich else print(f"Error: {_e}")
|
|
2294
|
+
|
|
2295
|
+
elif parts[0] == "ask":
|
|
2296
|
+
# /config ask <tool_name> — always prompt before this tool
|
|
2297
|
+
if len(parts) < 2:
|
|
2298
|
+
msg = "Usage: /config ask <tool_name> (e.g. /config ask write_file)"
|
|
2299
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
2300
|
+
return
|
|
2301
|
+
tool = parts[1].strip()
|
|
2302
|
+
try:
|
|
2303
|
+
from aria_code.runtime.tool_policy import add_to_policy
|
|
2304
|
+
add_to_policy(tool, "ask")
|
|
2305
|
+
msg = f"✓ 工具 '{tool}' 设为始终询问(每次执行前都弹出确认)"
|
|
2306
|
+
self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
|
|
2307
|
+
except Exception as _e:
|
|
2308
|
+
self.context.console.print(f"[red]Error: {_e}[/red]") if self.context.has_rich else print(f"Error: {_e}")
|
|
2309
|
+
|
|
2310
|
+
elif parts[0] == "policy":
|
|
2311
|
+
# /config policy — show all policy settings
|
|
2312
|
+
# /config policy reset — reset to defaults
|
|
2313
|
+
sub = parts[1].lower() if len(parts) > 1 else "show"
|
|
2314
|
+
try:
|
|
2315
|
+
from aria_code.runtime.tool_policy import load_tool_policy, save_tool_policy, remove_from_policy
|
|
2316
|
+
if sub == "reset":
|
|
2317
|
+
save_tool_policy({"allowed": [], "denied": [], "ask_always": []})
|
|
2318
|
+
msg = "✓ 工具权限策略已重置为默认值"
|
|
2319
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2320
|
+
elif sub in ("remove", "rm") and len(parts) > 2:
|
|
2321
|
+
tool = parts[2].strip()
|
|
2322
|
+
removed = remove_from_policy(tool)
|
|
2323
|
+
if removed:
|
|
2324
|
+
msg = f"✓ '{tool}' 已从策略中移除"
|
|
2325
|
+
self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
|
|
2326
|
+
else:
|
|
2327
|
+
msg = f"'{tool}' 不在任何策略列表中"
|
|
2328
|
+
self.context.console.print(f"[dim]{msg}[/dim]") if self.context.has_rich else print(msg)
|
|
2329
|
+
else:
|
|
2330
|
+
policy = load_tool_policy()
|
|
2331
|
+
if self.context.has_rich:
|
|
2332
|
+
self.context.console.print()
|
|
2333
|
+
self.context.console.print(" [bold]工具权限策略[/bold] [dim]~/.arthera/tool_policy.json[/dim]")
|
|
2334
|
+
self.context.console.print()
|
|
2335
|
+
allowed = policy.get("allowed", [])
|
|
2336
|
+
denied = policy.get("denied", [])
|
|
2337
|
+
asked = policy.get("ask_always", [])
|
|
2338
|
+
if allowed:
|
|
2339
|
+
self.context.console.print(f" [green]✓ 白名单(自动允许):[/green] {', '.join(allowed)}")
|
|
2340
|
+
else:
|
|
2341
|
+
self.context.console.print(" [dim]✓ 白名单: 空[/dim]")
|
|
2342
|
+
if denied:
|
|
2343
|
+
self.context.console.print(f" [red]✗ 黑名单(始终拒绝):[/red] {', '.join(denied)}")
|
|
2344
|
+
else:
|
|
2345
|
+
self.context.console.print(" [dim]✗ 黑名单: 空[/dim]")
|
|
2346
|
+
if asked:
|
|
2347
|
+
self.context.console.print(f" [yellow]? 始终询问:[/yellow] {', '.join(asked)}")
|
|
2348
|
+
else:
|
|
2349
|
+
self.context.console.print(" [dim]? 始终询问: 空[/dim]")
|
|
2350
|
+
self.context.console.print()
|
|
2351
|
+
self.context.console.print(" [dim]/config allow <tool> — 加入白名单[/dim]")
|
|
2352
|
+
self.context.console.print(" [dim]/config deny <tool> — 加入黑名单[/dim]")
|
|
2353
|
+
self.context.console.print(" [dim]/config ask <tool> — 设为始终询问[/dim]")
|
|
2354
|
+
self.context.console.print(" [dim]/config policy remove <tool> — 移除策略[/dim]")
|
|
2355
|
+
self.context.console.print(" [dim]/config policy reset — 清空所有策略[/dim]")
|
|
2356
|
+
self.context.console.print()
|
|
2357
|
+
else:
|
|
2358
|
+
print("\n Tool Policy:")
|
|
2359
|
+
print(f" Allowed: {', '.join(policy.get('allowed', [])) or 'none'}")
|
|
2360
|
+
print(f" Denied: {', '.join(policy.get('denied', [])) or 'none'}")
|
|
2361
|
+
print(f" Ask-always: {', '.join(policy.get('ask_always', [])) or 'none'}")
|
|
2362
|
+
except Exception as _e:
|
|
2363
|
+
self.context.console.print(f"[red]Error: {_e}[/red]") if self.context.has_rich else print(f"Error: {_e}")
|
|
2364
|
+
|
|
2365
|
+
else:
|
|
2366
|
+
self.context.console.print(
|
|
2367
|
+
"[dim]Usage: /config [show] | /config set key=value | /config reload\n"
|
|
2368
|
+
" /config allow <tool> | /config deny <tool> | /config ask <tool>\n"
|
|
2369
|
+
" /config policy [reset|remove <tool>][/dim]"
|
|
2370
|
+
if self.context.has_rich else
|
|
2371
|
+
"Usage: /config [show] | /config set key=value | /config reload\n"
|
|
2372
|
+
" /config allow <tool> | /config deny <tool> | /config ask <tool>\n"
|
|
2373
|
+
" /config policy [reset|remove <tool>]"
|
|
2374
|
+
)
|