aria-code 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"""DiagnosticOpsCommandsMixin — bug, accuracy, cost, todo, doctor, datasource commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
|
|
9
|
+
# Status glyph + colour per architecture-layer status.
|
|
10
|
+
_ARCH_ICON = {
|
|
11
|
+
"done": ("✓", "#3fb950"),
|
|
12
|
+
"partial": ("◐", "#d29922"),
|
|
13
|
+
"planned": ("○", "dim"),
|
|
14
|
+
"blocked": ("✗", "#f85149"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_architecture_report(layers, counts, *, gaps_only: bool = False,
|
|
19
|
+
rich: bool = True) -> list:
|
|
20
|
+
"""Render the architecture contract as display lines (pure / testable).
|
|
21
|
+
|
|
22
|
+
``layers`` are ArchitectureLayer objects; ``counts`` is the status→n map.
|
|
23
|
+
With ``gaps_only`` the DONE layers are dropped so only work-to-do shows.
|
|
24
|
+
"""
|
|
25
|
+
total = sum(counts.values()) or len(layers)
|
|
26
|
+
done = counts.get("done", 0)
|
|
27
|
+
lines = []
|
|
28
|
+
if rich:
|
|
29
|
+
lines.append(f"[bold]架构契约[/bold] [dim]{done}/{total} 层完成[/dim]")
|
|
30
|
+
lines.append(" " + " ".join(
|
|
31
|
+
f"[{_ARCH_ICON[s][1]}]{_ARCH_ICON[s][0]}[/{_ARCH_ICON[s][1]}] {s} {counts.get(s, 0)}"
|
|
32
|
+
for s in ("done", "partial", "planned", "blocked")
|
|
33
|
+
))
|
|
34
|
+
else:
|
|
35
|
+
lines.append(f"架构契约 {done}/{total} 层完成")
|
|
36
|
+
lines.append("")
|
|
37
|
+
for layer in layers:
|
|
38
|
+
st = getattr(layer.status, "value", str(layer.status))
|
|
39
|
+
if gaps_only and st == "done":
|
|
40
|
+
continue
|
|
41
|
+
icon, color = _ARCH_ICON.get(st, ("•", "dim"))
|
|
42
|
+
if rich:
|
|
43
|
+
lines.append(f"[{color}]{icon}[/{color}] [bold]{layer.name}[/bold] "
|
|
44
|
+
f"[dim]{layer.responsibility}[/dim]")
|
|
45
|
+
else:
|
|
46
|
+
lines.append(f"{icon} {layer.name} {layer.responsibility}")
|
|
47
|
+
if st != "done":
|
|
48
|
+
for ns in (layer.next_steps or [])[:2]:
|
|
49
|
+
lines.append(f" → {ns}")
|
|
50
|
+
for bl in (layer.blockers or [])[:1]:
|
|
51
|
+
lines.append(f" [#f85149]⚠ {bl}[/#f85149]" if rich else f" ⚠ {bl}")
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DiagnosticOpsCommandsMixin:
|
|
56
|
+
"""Mixin: diagnostics, feedback, usage, and source inspection commands."""
|
|
57
|
+
|
|
58
|
+
def cmd_architecture(self, args: str):
|
|
59
|
+
"""显示分层架构契约(各层状态 + 每层下一步)。用法: /architecture [--gaps]"""
|
|
60
|
+
try:
|
|
61
|
+
from packages.aria_core import (
|
|
62
|
+
list_architecture_layers, architecture_status_counts)
|
|
63
|
+
except Exception as exc: # pragma: no cover - import guard
|
|
64
|
+
msg = f"架构契约不可用: {exc}"
|
|
65
|
+
console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
|
|
66
|
+
return
|
|
67
|
+
gaps_only = "gap" in args.lower()
|
|
68
|
+
lines = format_architecture_report(
|
|
69
|
+
list_architecture_layers(), architecture_status_counts(),
|
|
70
|
+
gaps_only=gaps_only, rich=HAS_RICH)
|
|
71
|
+
if HAS_RICH:
|
|
72
|
+
console.print(Panel("\n".join(lines), title="[bold]Aria 架构[/bold]",
|
|
73
|
+
border_style="#C08050", box=rich_box.ROUNDED,
|
|
74
|
+
padding=(0, 1)))
|
|
75
|
+
else:
|
|
76
|
+
print("\n".join(lines))
|
|
77
|
+
|
|
78
|
+
def cmd_bug(self, args: str):
|
|
79
|
+
desc = args.strip()
|
|
80
|
+
if not desc:
|
|
81
|
+
console.print("[dim]用法: /bug <描述你遇到的问题>[/dim]" if HAS_RICH
|
|
82
|
+
else "Usage: /bug <description>")
|
|
83
|
+
return
|
|
84
|
+
ctx_parts = []
|
|
85
|
+
for m in self.terminal.conversation[-6:]:
|
|
86
|
+
_c = (m.get("content", "") or "")[:300]
|
|
87
|
+
ctx_parts.append(f"{m.get('role','')}: {_c}")
|
|
88
|
+
ctx = "\n".join(ctx_parts)
|
|
89
|
+
import platform as _pf
|
|
90
|
+
env = (f"v{__version__} · {_pf.system()} · py{_pf.python_version()} · "
|
|
91
|
+
f"model={self.terminal.config.get('model','')}")
|
|
92
|
+
self.terminal._record_feedback("bug", ctx, comment=f"{desc}\n\n[env] {env}")
|
|
93
|
+
gh = "https://github.com/artherahq/aria-code/issues"
|
|
94
|
+
if HAS_RICH:
|
|
95
|
+
console.print(" [#C08050]✓ 已记录问题(本地)[/#C08050]")
|
|
96
|
+
console.print(f" [dim]上传需 /privacy opt-in · 或直接提 issue: {gh}[/dim]")
|
|
97
|
+
else:
|
|
98
|
+
print(f" ✓ Bug recorded locally. Upload via /privacy opt-in, or file: {gh}")
|
|
99
|
+
|
|
100
|
+
def cmd_accuracy(self, args: str):
|
|
101
|
+
res = self.terminal._verify_predictions(min_age_hours=24.0)
|
|
102
|
+
try:
|
|
103
|
+
from apps.cli.prediction_feedback import PredictionTracker
|
|
104
|
+
acc = PredictionTracker(CONFIG_DIR).accuracy()
|
|
105
|
+
except Exception:
|
|
106
|
+
acc = {}
|
|
107
|
+
if HAS_RICH:
|
|
108
|
+
console.print()
|
|
109
|
+
console.print(" [bold]预测战绩[/bold] [dim]LLM 方向判断 vs 实际行情[/dim]")
|
|
110
|
+
if res.get("settled"):
|
|
111
|
+
console.print(f" [dim]本次结算 {res['settled']} 笔:"
|
|
112
|
+
f"命中 [green]{res['correct']}[/green] / "
|
|
113
|
+
f"落空 [red]{res['wrong']}[/red][/dim]")
|
|
114
|
+
_acc = acc.get("accuracy")
|
|
115
|
+
_acc_str = f"{_acc:.0%}" if _acc is not None else "—"
|
|
116
|
+
console.print(
|
|
117
|
+
f" 累计:已结算 [bold]{acc.get('settled',0)}[/bold] · "
|
|
118
|
+
f"命中率 [#C08050]{_acc_str}[/#C08050] · "
|
|
119
|
+
f"待结算 [dim]{acc.get('pending',0)}[/dim]"
|
|
120
|
+
)
|
|
121
|
+
if not acc.get("total"):
|
|
122
|
+
console.print(" [dim]暂无记录 — 用 /team 或 /analyze 让 AI 给出方向判断后会自动追踪[/dim]")
|
|
123
|
+
else:
|
|
124
|
+
print(f" 预测战绩: 结算{res.get('settled',0)} 命中率"
|
|
125
|
+
f"{acc.get('accuracy')} 待结算{acc.get('pending',0)}")
|
|
126
|
+
|
|
127
|
+
def cmd_cost(self, args: str):
|
|
128
|
+
import time as _t
|
|
129
|
+
elapsed = _t.time() - self.terminal._session_start
|
|
130
|
+
inp = self.terminal._session_input_tokens
|
|
131
|
+
out = self.terminal._session_output_tokens
|
|
132
|
+
think = self.terminal._session_thinking_tokens
|
|
133
|
+
turns = self.terminal._session_turns
|
|
134
|
+
total = inp + out + think
|
|
135
|
+
|
|
136
|
+
is_local = self.terminal._last_provider in ("ollama", "ollama_cache", "local")
|
|
137
|
+
cost_usd = 0.0
|
|
138
|
+
if not is_local:
|
|
139
|
+
cost_usd = (inp * 0.14 + out * 0.28 + think * 1.10) / 1_000_000
|
|
140
|
+
|
|
141
|
+
hh = int(elapsed // 3600)
|
|
142
|
+
mm = int((elapsed % 3600) // 60)
|
|
143
|
+
ss = int(elapsed % 60)
|
|
144
|
+
duration = f"{hh}h {mm:02d}m {ss:02d}s" if hh else f"{mm}m {ss:02d}s"
|
|
145
|
+
|
|
146
|
+
if HAS_RICH:
|
|
147
|
+
console.print()
|
|
148
|
+
console.print("[bold]Session Usage[/bold]")
|
|
149
|
+
console.print()
|
|
150
|
+
console.print(f" [dim]{'Duration':<22}[/dim]{duration}")
|
|
151
|
+
console.print(f" [dim]{'Turns':<22}[/dim]{turns}")
|
|
152
|
+
console.print(f" [dim]{'Input tokens':<22}[/dim]{inp:,}")
|
|
153
|
+
console.print(f" [dim]{'Output tokens':<22}[/dim]{out:,}")
|
|
154
|
+
if think:
|
|
155
|
+
console.print(f" [dim]{'Thinking tokens':<22}[/dim]{think:,}")
|
|
156
|
+
console.print(f" [dim]{'Total tokens':<22}[/dim][bold]{total:,}[/bold]")
|
|
157
|
+
if is_local:
|
|
158
|
+
console.print(f" [dim]{'Est. cost':<22}[/dim][green]$0.00 (local)[/green]")
|
|
159
|
+
elif total > 0:
|
|
160
|
+
console.print(f" [dim]{'Est. cost':<22}[/dim]${cost_usd:.4f} USD")
|
|
161
|
+
console.print(f" [dim]{'Provider':<22}[/dim]{self.terminal._last_provider}")
|
|
162
|
+
console.print()
|
|
163
|
+
else:
|
|
164
|
+
print(f" Session: {duration} Turns: {turns}")
|
|
165
|
+
print(f" Tokens: {inp:,} in / {out:,} out / {total:,} total")
|
|
166
|
+
if not is_local and total > 0:
|
|
167
|
+
print(f" Est. cost: ${cost_usd:.4f}")
|
|
168
|
+
|
|
169
|
+
def cmd_todo(self, args: str):
|
|
170
|
+
import json as _json
|
|
171
|
+
todo_file = CONFIG_DIR / "todos.json"
|
|
172
|
+
|
|
173
|
+
def _load():
|
|
174
|
+
try:
|
|
175
|
+
if todo_file.exists():
|
|
176
|
+
return _json.loads(todo_file.read_text(encoding="utf-8"))
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
def _save(tasks):
|
|
182
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
todo_file.write_text(_json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
184
|
+
|
|
185
|
+
parts = args.strip().split(maxsplit=1)
|
|
186
|
+
sub = parts[0].lower() if parts else "list"
|
|
187
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
188
|
+
tasks = _load()
|
|
189
|
+
|
|
190
|
+
if sub in ("", "list", "ls"):
|
|
191
|
+
if not tasks:
|
|
192
|
+
console.print("[dim]No tasks. Add with: /todo add <task>[/dim]" if HAS_RICH else "No tasks")
|
|
193
|
+
return
|
|
194
|
+
if HAS_RICH:
|
|
195
|
+
console.print()
|
|
196
|
+
for i, t in enumerate(tasks):
|
|
197
|
+
status_icon = "[green]✓[/green]" if t.get("done") else "[yellow]○[/yellow]"
|
|
198
|
+
style = "dim" if t.get("done") else ""
|
|
199
|
+
text = t.get("text", "")
|
|
200
|
+
console.print(f" {status_icon} [dim]{i}[/dim] [{style}]{text}[/{style}]" if style
|
|
201
|
+
else f" {status_icon} [dim]{i}[/dim] {text}")
|
|
202
|
+
pending = sum(1 for t in tasks if not t.get("done"))
|
|
203
|
+
console.print(f"\n [dim]{pending}/{len(tasks)} pending[/dim]")
|
|
204
|
+
console.print()
|
|
205
|
+
else:
|
|
206
|
+
for i, t in enumerate(tasks):
|
|
207
|
+
mark = "✓" if t.get("done") else "○"
|
|
208
|
+
print(f" {mark} {i} {t.get('text', '')}")
|
|
209
|
+
elif sub == "add":
|
|
210
|
+
if not rest:
|
|
211
|
+
console.print("[dim]Usage: /todo add <task text>[/dim]" if HAS_RICH else "Usage: /todo add <task>")
|
|
212
|
+
return
|
|
213
|
+
task = {"text": rest, "done": False, "id": len(tasks)}
|
|
214
|
+
tasks.append(task)
|
|
215
|
+
_save(tasks)
|
|
216
|
+
console.print(f" [dim]✓ Added: {rest}[/dim]" if HAS_RICH else f"Added: {rest}")
|
|
217
|
+
elif sub in ("done", "check", "complete"):
|
|
218
|
+
try:
|
|
219
|
+
idx = int(rest)
|
|
220
|
+
tasks[idx]["done"] = True
|
|
221
|
+
_save(tasks)
|
|
222
|
+
console.print(f" [dim]✓ Done: {tasks[idx]['text']}[/dim]" if HAS_RICH
|
|
223
|
+
else f"Done: {tasks[idx]['text']}")
|
|
224
|
+
except (ValueError, IndexError):
|
|
225
|
+
console.print("[dim]Usage: /todo done <id>[/dim]" if HAS_RICH else "Usage: /todo done <id>")
|
|
226
|
+
elif sub in ("remove", "rm", "delete", "del"):
|
|
227
|
+
try:
|
|
228
|
+
idx = int(rest)
|
|
229
|
+
removed = tasks.pop(idx)
|
|
230
|
+
_save(tasks)
|
|
231
|
+
console.print(f" [dim]Removed: {removed['text']}[/dim]" if HAS_RICH
|
|
232
|
+
else f"Removed: {removed['text']}")
|
|
233
|
+
except (ValueError, IndexError):
|
|
234
|
+
console.print("[dim]Usage: /todo remove <id>[/dim]" if HAS_RICH else "bad index")
|
|
235
|
+
elif sub == "clear":
|
|
236
|
+
_save([])
|
|
237
|
+
console.print("[dim]All tasks cleared[/dim]" if HAS_RICH else "Cleared")
|
|
238
|
+
else:
|
|
239
|
+
full_text = (sub + " " + rest).strip()
|
|
240
|
+
task = {"text": full_text, "done": False, "id": len(tasks)}
|
|
241
|
+
tasks.append(task)
|
|
242
|
+
_save(tasks)
|
|
243
|
+
console.print(f" [dim]✓ Added: {full_text}[/dim]" if HAS_RICH else f"Added: {full_text}")
|
|
244
|
+
|
|
245
|
+
def cmd_doctor(self, args: str):
|
|
246
|
+
try:
|
|
247
|
+
from doctor import run_doctor
|
|
248
|
+
|
|
249
|
+
report = run_doctor(
|
|
250
|
+
self.terminal.config,
|
|
251
|
+
check_network="--network" in (args or "").split(),
|
|
252
|
+
)
|
|
253
|
+
if HAS_RICH:
|
|
254
|
+
from rich.table import Table as _DoctorTable
|
|
255
|
+
table = _DoctorTable(title="Aria Code doctor", box=rich_box.ROUNDED)
|
|
256
|
+
table.add_column("Status", width=8)
|
|
257
|
+
table.add_column("Check", style="bold")
|
|
258
|
+
table.add_column("Detail", style="dim")
|
|
259
|
+
table.add_column("Suggestion", style="dim")
|
|
260
|
+
icons = {"ok": "[green]OK[/green]", "warn": "[yellow]WARN[/yellow]", "err": "[red]ERR[/red]"}
|
|
261
|
+
for check in report.checks:
|
|
262
|
+
table.add_row(
|
|
263
|
+
icons.get(check.status, check.status.upper()),
|
|
264
|
+
check.name,
|
|
265
|
+
check.detail,
|
|
266
|
+
check.suggestion,
|
|
267
|
+
)
|
|
268
|
+
console.print()
|
|
269
|
+
console.print(table)
|
|
270
|
+
color = "green" if report.errors == 0 and report.warnings == 0 else ("yellow" if report.errors == 0 else "red")
|
|
271
|
+
console.print(f"[{color}]{report.passed} passed · {report.warnings} warnings · {report.errors} errors[/{color}]")
|
|
272
|
+
console.print()
|
|
273
|
+
else:
|
|
274
|
+
from doctor import format_doctor_plain
|
|
275
|
+
print(format_doctor_plain(report))
|
|
276
|
+
return
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
console.print(f"[yellow]doctor module unavailable, using legacy checks: {exc}[/yellow]" if HAS_RICH else f"doctor module unavailable: {exc}")
|
|
279
|
+
|
|
280
|
+
import importlib as _il, subprocess as _sp, shutil as _sh
|
|
281
|
+
cfg = self.terminal.config
|
|
282
|
+
ollama_url = cfg.get("ollama_url", "http://localhost:11434")
|
|
283
|
+
api_url = cfg.get("api_url", "http://localhost:8000")
|
|
284
|
+
|
|
285
|
+
checks: list[tuple] = []
|
|
286
|
+
|
|
287
|
+
def _ok(label, detail=""): checks.append(("ok", label, detail))
|
|
288
|
+
def _warn(label, detail=""): checks.append(("warn", label, detail))
|
|
289
|
+
def _err(label, detail=""): checks.append(("err", label, detail))
|
|
290
|
+
|
|
291
|
+
import sys as _sys
|
|
292
|
+
pyver = f"{_sys.version_info.major}.{_sys.version_info.minor}.{_sys.version_info.micro}"
|
|
293
|
+
if _sys.version_info >= (3, 9):
|
|
294
|
+
_ok("Python", pyver)
|
|
295
|
+
else:
|
|
296
|
+
_warn("Python", f"{pyver} (3.9+ recommended)")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
import urllib.request as _ur
|
|
300
|
+
_opener = _ur.build_opener(_ur.ProxyHandler({}))
|
|
301
|
+
_r = _opener.open(f"{ollama_url}/api/tags", timeout=3)
|
|
302
|
+
_data = json.loads(_r.read())
|
|
303
|
+
models = [m["name"] for m in _data.get("models", [])]
|
|
304
|
+
if models:
|
|
305
|
+
_ok("Ollama", f"{len(models)} models: {', '.join(models[:4])}")
|
|
306
|
+
else:
|
|
307
|
+
_warn("Ollama", "running but no models installed (ollama pull qwen2.5-coder:1.5b)")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
_err("Ollama", f"not reachable at {ollama_url} ({e})")
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
import urllib.request as _ur
|
|
313
|
+
_opener = _ur.build_opener(_ur.ProxyHandler({}))
|
|
314
|
+
_r = _opener.open(f"{api_url}/health", timeout=3)
|
|
315
|
+
_ok("Backend", f"running at {api_url}")
|
|
316
|
+
except Exception:
|
|
317
|
+
_warn("Backend", f"offline at {api_url} — local Ollama mode will be used")
|
|
318
|
+
|
|
319
|
+
key_checks = [
|
|
320
|
+
("finnhub", "股票行情"),
|
|
321
|
+
("alphavantage", "历史数据"),
|
|
322
|
+
("newsapi", "新闻"),
|
|
323
|
+
("brave", "网络搜索"),
|
|
324
|
+
("coingecko", "加密货币"),
|
|
325
|
+
]
|
|
326
|
+
for svc, desc in key_checks:
|
|
327
|
+
k = _get_provider_key(svc)
|
|
328
|
+
if k:
|
|
329
|
+
_ok(f"API key: {svc}", f"{desc} ({'*'*6}{k[-4:]})")
|
|
330
|
+
else:
|
|
331
|
+
_warn(f"API key: {svc}", f"{desc} 未配置 (/apikey set {svc} <key>)")
|
|
332
|
+
|
|
333
|
+
llm_keys = [("deepseek", "DeepSeek"), ("openai", "OpenAI"),
|
|
334
|
+
("siliconflow", "SiliconFlow"), ("moonshot", "Moonshot")]
|
|
335
|
+
_has_any_llm = False
|
|
336
|
+
for svc, name in llm_keys:
|
|
337
|
+
k = _get_provider_key(svc)
|
|
338
|
+
if k:
|
|
339
|
+
_ok(f"LLM key: {svc}", f"{name} configured")
|
|
340
|
+
_has_any_llm = True
|
|
341
|
+
if not _has_any_llm:
|
|
342
|
+
_warn("LLM keys", "No cloud LLM keys — Ollama must be running for AI responses")
|
|
343
|
+
|
|
344
|
+
_pkgs = [
|
|
345
|
+
("aiohttp", "async HTTP"),
|
|
346
|
+
("rich", "terminal UI"),
|
|
347
|
+
("prompt_toolkit", "autocomplete"),
|
|
348
|
+
("yfinance", "market data"),
|
|
349
|
+
("pandas", "data processing"),
|
|
350
|
+
("requests", "HTTP client"),
|
|
351
|
+
]
|
|
352
|
+
for pkg, desc in _pkgs:
|
|
353
|
+
try:
|
|
354
|
+
m = _il.import_module(pkg)
|
|
355
|
+
ver = getattr(m, "__version__", "?")
|
|
356
|
+
_ok(f"pkg: {pkg}", f"{desc} v{ver}")
|
|
357
|
+
except ImportError:
|
|
358
|
+
_warn(f"pkg: {pkg}", f"{desc} not installed (pip install {pkg})")
|
|
359
|
+
|
|
360
|
+
aria_md = pathlib.Path.cwd() / "ARIA.md"
|
|
361
|
+
if aria_md.exists():
|
|
362
|
+
lines = len(aria_md.read_text(encoding="utf-8").splitlines())
|
|
363
|
+
_ok("ARIA.md", f"{lines} lines of project context")
|
|
364
|
+
else:
|
|
365
|
+
_warn("ARIA.md", f"not found in {pathlib.Path.cwd()} (use /init to create)")
|
|
366
|
+
|
|
367
|
+
if _HAS_MCP:
|
|
368
|
+
try:
|
|
369
|
+
reg = self.terminal._mcp_registry
|
|
370
|
+
if reg and hasattr(reg, "list_tools"):
|
|
371
|
+
tools = reg.list_tools()
|
|
372
|
+
_ok("MCP", f"{len(tools)} tools from MCP servers")
|
|
373
|
+
else:
|
|
374
|
+
_warn("MCP", "registry not started yet")
|
|
375
|
+
except Exception:
|
|
376
|
+
_warn("MCP", "loaded but no active servers")
|
|
377
|
+
else:
|
|
378
|
+
_warn("MCP", "mcp_client not found — MCP support disabled")
|
|
379
|
+
|
|
380
|
+
tool_count = len(ARIA_TOOLS) + len(LOCAL_TOOLS)
|
|
381
|
+
_ok("Aria tools", f"{tool_count} tools loaded")
|
|
382
|
+
|
|
383
|
+
console.print() if HAS_RICH else None
|
|
384
|
+
if HAS_RICH:
|
|
385
|
+
console.print("[bold]Aria Code — Diagnostics[/bold]")
|
|
386
|
+
console.print()
|
|
387
|
+
icons = {"ok": "[green]✓[/green]", "warn": "[yellow]⚠[/yellow]", "err": "[red]✗[/red]"}
|
|
388
|
+
for status, label, detail in checks:
|
|
389
|
+
icon = icons[status]
|
|
390
|
+
detail_str = f" [dim]{detail}[/dim]" if detail else ""
|
|
391
|
+
console.print(f" {icon} {label:<28}{detail_str}")
|
|
392
|
+
console.print()
|
|
393
|
+
n_ok = sum(1 for s, *_ in checks if s == "ok")
|
|
394
|
+
n_w = sum(1 for s, *_ in checks if s == "warn")
|
|
395
|
+
n_e = sum(1 for s, *_ in checks if s == "err")
|
|
396
|
+
summary_color = "green" if n_e == 0 and n_w == 0 else ("yellow" if n_e == 0 else "red")
|
|
397
|
+
console.print(f" [{summary_color}]{n_ok} passed · {n_w} warnings · {n_e} errors[/{summary_color}]")
|
|
398
|
+
console.print()
|
|
399
|
+
|
|
400
|
+
_fh_ok = bool(_get_provider_key("finnhub"))
|
|
401
|
+
_av_ok = bool(_get_provider_key("alphavantage"))
|
|
402
|
+
_na_ok = bool(_get_provider_key("newsapi"))
|
|
403
|
+
_ak_ok = True
|
|
404
|
+
_llm_ok = any(_get_provider_key(p) for p in ("deepseek", "openai", "anthropic", "groq"))
|
|
405
|
+
|
|
406
|
+
_guide_needed = not (_fh_ok and _av_ok and _na_ok and _llm_ok)
|
|
407
|
+
if _guide_needed:
|
|
408
|
+
console.print("[bold]数据源配置指南[/bold] [dim](完整功能需要以下 key)[/dim]")
|
|
409
|
+
console.print()
|
|
410
|
+
console.print(" [dim]Use /doctor --network for network checks[/dim]")
|
|
411
|
+
else:
|
|
412
|
+
print("Diagnostics complete")
|
|
413
|
+
|
|
414
|
+
async def cmd_install(self, args: str):
|
|
415
|
+
"""
|
|
416
|
+
检测并安装缺失的依赖包(环境补全)。
|
|
417
|
+
|
|
418
|
+
/install 扫描全部已知依赖,列出缺失并询问是否安装
|
|
419
|
+
/install <pkg> [pkg] 直接安装指定 Python 包
|
|
420
|
+
/install --auto 根据最近一次提问的意图检测缺失项
|
|
421
|
+
/install --required 仅安装"必需"包,跳过可选项
|
|
422
|
+
/install --plan 只展示安装计划,不安装
|
|
423
|
+
/install --yes 非交互确认,直接安装选中包
|
|
424
|
+
"""
|
|
425
|
+
import shlex as _shlex
|
|
426
|
+
import subprocess as _sp
|
|
427
|
+
import sys as _sys
|
|
428
|
+
from apps.cli.preflight import (
|
|
429
|
+
build_full_dependency_report,
|
|
430
|
+
build_intent_preflight,
|
|
431
|
+
build_install_plan,
|
|
432
|
+
package_to_module,
|
|
433
|
+
select_install_packages,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
raw = (args or "").strip()
|
|
437
|
+
tokens = raw.split()
|
|
438
|
+
flags = {t for t in tokens if t.startswith("--")}
|
|
439
|
+
explicit_pkgs = [t for t in tokens if not t.startswith("--")]
|
|
440
|
+
|
|
441
|
+
# ── Resolve what to install ───────────────────────────────────────────
|
|
442
|
+
report = None
|
|
443
|
+
if explicit_pkgs:
|
|
444
|
+
# Direct package install — no detection needed
|
|
445
|
+
pip_packages = explicit_pkgs
|
|
446
|
+
command_hints: tuple = ()
|
|
447
|
+
env_hints: tuple = ()
|
|
448
|
+
else:
|
|
449
|
+
if "--auto" in flags:
|
|
450
|
+
# Detect from the user's last real question
|
|
451
|
+
last_msg = ""
|
|
452
|
+
for m in reversed(self.terminal.conversation):
|
|
453
|
+
if m.get("role") == "user" and m.get("content"):
|
|
454
|
+
last_msg = m["content"] if isinstance(m["content"], str) else ""
|
|
455
|
+
break
|
|
456
|
+
if not last_msg:
|
|
457
|
+
console.print("[yellow]没有可分析的历史提问,改用全量扫描[/yellow]" if HAS_RICH
|
|
458
|
+
else "No history; full scan")
|
|
459
|
+
report = build_full_dependency_report(include_optional="--required" not in flags)
|
|
460
|
+
else:
|
|
461
|
+
report = build_intent_preflight(last_msg)
|
|
462
|
+
else:
|
|
463
|
+
report = build_full_dependency_report(include_optional="--required" not in flags)
|
|
464
|
+
|
|
465
|
+
plan = build_install_plan(report)
|
|
466
|
+
pip_packages = list(plan.pip_packages)
|
|
467
|
+
command_hints = plan.command_hints
|
|
468
|
+
env_hints = plan.env_hints
|
|
469
|
+
|
|
470
|
+
# ── Nothing missing ───────────────────────────────────────────────────
|
|
471
|
+
if not pip_packages and not (report and (report.missing_commands or report.missing_env)):
|
|
472
|
+
console.print("[green]✓ 环境完整,没有检测到缺失的 Python 包[/green]" if HAS_RICH
|
|
473
|
+
else "All dependencies satisfied")
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
# ── Show findings ─────────────────────────────────────────────────────
|
|
477
|
+
if HAS_RICH:
|
|
478
|
+
console.print()
|
|
479
|
+
console.print("[bold]环境检测结果[/bold]")
|
|
480
|
+
if pip_packages:
|
|
481
|
+
console.print(f" [yellow]缺少 {len(pip_packages)} 个 Python 包:[/yellow]")
|
|
482
|
+
for p in pip_packages:
|
|
483
|
+
purpose = ""
|
|
484
|
+
if report:
|
|
485
|
+
for r in report.missing_python:
|
|
486
|
+
if r.package == p:
|
|
487
|
+
purpose = f" [dim]— {r.purpose}{'(可选)' if not r.required else ''}[/dim]"
|
|
488
|
+
break
|
|
489
|
+
console.print(f" • [cyan]{p}[/cyan]{purpose}")
|
|
490
|
+
if command_hints:
|
|
491
|
+
console.print(" [yellow]缺少命令行工具:[/yellow]")
|
|
492
|
+
for h in command_hints:
|
|
493
|
+
console.print(f" • [dim]{h}[/dim]")
|
|
494
|
+
if env_hints:
|
|
495
|
+
console.print(" [dim]未配置的环境变量(可选,不自动处理):[/dim]")
|
|
496
|
+
for h in env_hints:
|
|
497
|
+
console.print(f" • [dim]{h}[/dim]")
|
|
498
|
+
console.print()
|
|
499
|
+
else:
|
|
500
|
+
print(f"Missing packages: {', '.join(pip_packages)}")
|
|
501
|
+
for h in command_hints:
|
|
502
|
+
print(f" tool: {h}")
|
|
503
|
+
|
|
504
|
+
if not pip_packages:
|
|
505
|
+
if command_hints:
|
|
506
|
+
console.print("[dim]命令行工具需手动安装(见上方提示),Aria 不会自动执行系统级安装[/dim]"
|
|
507
|
+
if HAS_RICH else "Install CLI tools manually (see hints above)")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
# ── Select packages ───────────────────────────────────────────────────
|
|
511
|
+
if report:
|
|
512
|
+
if "--plan" in flags:
|
|
513
|
+
selection = select_install_packages(plan, report, mode="plan")
|
|
514
|
+
elif "--yes" in flags:
|
|
515
|
+
_mode = "required" if "--required" in flags else "all"
|
|
516
|
+
selection = select_install_packages(plan, report, mode=_mode)
|
|
517
|
+
else:
|
|
518
|
+
required_pkgs = [
|
|
519
|
+
r.package for r in report.missing_python
|
|
520
|
+
if r.required and r.package in pip_packages
|
|
521
|
+
]
|
|
522
|
+
optional_pkgs = [
|
|
523
|
+
r.package for r in report.missing_python
|
|
524
|
+
if not r.required and r.package in pip_packages
|
|
525
|
+
]
|
|
526
|
+
if HAS_RICH:
|
|
527
|
+
console.print("[bold]选择安装范围[/bold]")
|
|
528
|
+
console.print(" [cyan]all[/cyan] 安装全部缺失 Python 包")
|
|
529
|
+
console.print(" [cyan]required[/cyan] 只安装必需包")
|
|
530
|
+
console.print(" [cyan]optional[/cyan] 只安装可选增强包")
|
|
531
|
+
console.print(" [cyan]custom[/cyan] 手动输入包名或编号")
|
|
532
|
+
console.print(" [cyan]plan[/cyan] 只显示计划,不安装")
|
|
533
|
+
console.print(" [cyan]skip[/cyan] 跳过")
|
|
534
|
+
for idx, pkg in enumerate(pip_packages, 1):
|
|
535
|
+
kind = "required" if pkg in required_pkgs else "optional"
|
|
536
|
+
console.print(f" [dim]{idx}.[/dim] {pkg} [dim]({kind})[/dim]")
|
|
537
|
+
else:
|
|
538
|
+
print("Select install scope: all | required | optional | custom | plan | skip")
|
|
539
|
+
for idx, pkg in enumerate(pip_packages, 1):
|
|
540
|
+
kind = "required" if pkg in required_pkgs else "optional"
|
|
541
|
+
print(f" {idx}. {pkg} ({kind})")
|
|
542
|
+
default_mode = "required" if required_pkgs and optional_pkgs else "all"
|
|
543
|
+
try:
|
|
544
|
+
choice = console.input(f"安装范围 [{default_mode}]: ") if HAS_RICH else input(f"Install scope [{default_mode}]: ")
|
|
545
|
+
except (EOFError, KeyboardInterrupt):
|
|
546
|
+
console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
|
|
547
|
+
return
|
|
548
|
+
choice = (choice or default_mode).strip().lower()
|
|
549
|
+
custom_items: list[str] = []
|
|
550
|
+
if choice == "custom":
|
|
551
|
+
try:
|
|
552
|
+
raw_custom = console.input("输入包名或编号(空格/逗号分隔): ") if HAS_RICH else input("Packages or numbers: ")
|
|
553
|
+
except (EOFError, KeyboardInterrupt):
|
|
554
|
+
console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
|
|
555
|
+
return
|
|
556
|
+
for item in raw_custom.replace(",", " ").split():
|
|
557
|
+
if item.isdigit():
|
|
558
|
+
idx = int(item)
|
|
559
|
+
if 1 <= idx <= len(pip_packages):
|
|
560
|
+
custom_items.append(pip_packages[idx - 1])
|
|
561
|
+
else:
|
|
562
|
+
custom_items.append(item)
|
|
563
|
+
selection = select_install_packages(
|
|
564
|
+
plan, report, mode=choice, custom=custom_items
|
|
565
|
+
)
|
|
566
|
+
pip_packages = list(selection.pip_packages)
|
|
567
|
+
if selection.mode in {"plan", "dry-run", "dry_run"}:
|
|
568
|
+
console.print("[dim]已生成安装计划,未执行安装。[/dim]" if HAS_RICH else "Install plan only; no changes made.")
|
|
569
|
+
return
|
|
570
|
+
if not pip_packages:
|
|
571
|
+
console.print("[dim]没有选择任何 Python 包,未安装。[/dim]" if HAS_RICH else "No packages selected.")
|
|
572
|
+
return
|
|
573
|
+
if selection.skipped_packages and HAS_RICH:
|
|
574
|
+
console.print(f"[dim]跳过: {', '.join(selection.skipped_packages)}[/dim]")
|
|
575
|
+
elif "--plan" in flags:
|
|
576
|
+
console.print("[dim]显式包安装计划已显示,未执行安装。[/dim]" if HAS_RICH else "Install plan only; no changes made.")
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
# ── Confirm ───────────────────────────────────────────────────────────
|
|
580
|
+
pip_cmd = [_sys.executable, "-m", "pip", "install", *pip_packages]
|
|
581
|
+
pretty = " ".join(_shlex.quote(c) for c in pip_cmd)
|
|
582
|
+
if "--yes" not in flags:
|
|
583
|
+
prompt = f"将运行: {pretty}\n确认安装? [y/N]: "
|
|
584
|
+
try:
|
|
585
|
+
answer = console.input(prompt) if HAS_RICH else input(prompt)
|
|
586
|
+
except (EOFError, KeyboardInterrupt):
|
|
587
|
+
console.print("\n[dim]已取消[/dim]" if HAS_RICH else "Cancelled")
|
|
588
|
+
return
|
|
589
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
590
|
+
console.print("[dim]已取消,未安装任何包[/dim]" if HAS_RICH else "Cancelled")
|
|
591
|
+
return
|
|
592
|
+
elif HAS_RICH:
|
|
593
|
+
console.print(f"[dim]Auto install: {pretty}[/dim]")
|
|
594
|
+
|
|
595
|
+
# ── Install ───────────────────────────────────────────────────────────
|
|
596
|
+
console.print(f"\n[dim]⏳ 安装中: {' '.join(pip_packages)}…[/dim]" if HAS_RICH
|
|
597
|
+
else f"Installing {' '.join(pip_packages)}...")
|
|
598
|
+
try:
|
|
599
|
+
proc = await asyncio.get_event_loop().run_in_executor(
|
|
600
|
+
None,
|
|
601
|
+
lambda: _sp.run(pip_cmd, capture_output=True, text=True, timeout=300),
|
|
602
|
+
)
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
console.print(f"[red]安装失败: {exc}[/red]" if HAS_RICH else f"Install failed: {exc}")
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
if proc.returncode == 0:
|
|
608
|
+
# Verify each package now imports
|
|
609
|
+
import importlib as _il
|
|
610
|
+
_il.invalidate_caches()
|
|
611
|
+
ok_list, fail_list = [], []
|
|
612
|
+
for p in pip_packages:
|
|
613
|
+
mod = package_to_module(p)
|
|
614
|
+
try:
|
|
615
|
+
_il.import_module(mod)
|
|
616
|
+
ok_list.append(p)
|
|
617
|
+
except Exception:
|
|
618
|
+
fail_list.append(p)
|
|
619
|
+
if HAS_RICH:
|
|
620
|
+
console.print(f" [green]✓ 安装完成: {', '.join(ok_list) or '—'}[/green]")
|
|
621
|
+
if fail_list:
|
|
622
|
+
console.print(f" [yellow]⚠ 已安装但当前会话需重启才能加载: {', '.join(fail_list)}[/yellow]")
|
|
623
|
+
console.print(" [dim]提示: 部分包需重启 Aria 才能被工具加载[/dim]")
|
|
624
|
+
else:
|
|
625
|
+
print(f"Installed: {', '.join(ok_list)}")
|
|
626
|
+
else:
|
|
627
|
+
err_tail = (proc.stderr or proc.stdout or "")[-400:]
|
|
628
|
+
console.print(f"[red]pip 安装失败 (code {proc.returncode}):[/red]\n[dim]{err_tail}[/dim]"
|
|
629
|
+
if HAS_RICH else f"pip failed: {err_tail}")
|
|
630
|
+
|
|
631
|
+
async def cmd_datasource(self, args: str):
|
|
632
|
+
sub = args.strip().lower()
|
|
633
|
+
if sub.startswith("test "):
|
|
634
|
+
src_name = sub[5:].strip()
|
|
635
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
636
|
+
None, lambda: _test_datasource(src_name)
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
if sub == "config":
|
|
641
|
+
paths = [
|
|
642
|
+
"~/.aria/datasources.yaml",
|
|
643
|
+
"~/.aria/.env",
|
|
644
|
+
str(CONFIG_DIR / "providers.json"),
|
|
645
|
+
]
|
|
646
|
+
if HAS_RICH:
|
|
647
|
+
console.print(" [bold]数据源配置文件:[/bold]")
|
|
648
|
+
for p in paths:
|
|
649
|
+
import pathlib
|
|
650
|
+
full = pathlib.Path(p).expanduser()
|
|
651
|
+
exists = "[green]✓[/green]" if full.exists() else "[dim]✗ (未创建)[/dim]"
|
|
652
|
+
console.print(f" {exists} [dim]{p}[/dim]")
|
|
653
|
+
console.print("\n [dim]环境变量: TUSHARE_TOKEN FRED_API_KEY ALPHA_VANTAGE_KEY[/dim]")
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
from datasources.router import _SOURCE_REGISTRY, DataRouter
|
|
658
|
+
router = DataRouter()
|
|
659
|
+
except ImportError:
|
|
660
|
+
_print_error("datasources 模块未找到")
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
if HAS_RICH:
|
|
664
|
+
from rich.table import Table
|
|
665
|
+
from rich import box as rich_box
|
|
666
|
+
table = Table(title="数据源状态", box=rich_box.SIMPLE, header_style="bold dim")
|
|
667
|
+
table.add_column("名称", width=16)
|
|
668
|
+
table.add_column("市场", width=20)
|
|
669
|
+
table.add_column("需要Key", width=8)
|
|
670
|
+
table.add_column("状态", width=8)
|
|
671
|
+
table.add_column("说明")
|
|
672
|
+
_DESC = {
|
|
673
|
+
"yfinance": "Yahoo Finance (免费)",
|
|
674
|
+
"akshare": "AkShare A股 (免费)",
|
|
675
|
+
"tushare": "Tushare Pro (需Token)",
|
|
676
|
+
"fred": "美联储经济数据 (免费)",
|
|
677
|
+
"edgar": "SEC EDGAR 财报 (免费)",
|
|
678
|
+
"alpha_vantage": "Alpha Vantage (免费Key)",
|
|
679
|
+
"world_bank": "世界银行 (免费)",
|
|
680
|
+
}
|
|
681
|
+
for name, cls in _SOURCE_REGISTRY.items():
|
|
682
|
+
try:
|
|
683
|
+
src = cls()
|
|
684
|
+
configured = src.is_configured()
|
|
685
|
+
status = "[green]✓ 就绪[/green]" if configured else "[dim]✗ 未配置[/dim]"
|
|
686
|
+
needs_key = "是" if cls.requires_key else "否"
|
|
687
|
+
markets = ", ".join(getattr(cls, "markets", []))
|
|
688
|
+
except Exception:
|
|
689
|
+
status, needs_key, markets = "[red]错误[/red]", "?", "?"
|
|
690
|
+
table.add_row(name, markets, needs_key, status, _DESC.get(name, ""))
|
|
691
|
+
console.print(table)
|
|
692
|
+
console.print(" [dim]/datasource config — 配置文件路径[/dim]")
|
|
693
|
+
else:
|
|
694
|
+
for name, cls in _SOURCE_REGISTRY.items():
|
|
695
|
+
src = cls()
|
|
696
|
+
print(f" {name}: {'ready' if src.is_configured() else 'not configured'}")
|