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,1414 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WorkspaceCommandsMixin — Workspace commands: packages, file, project, init, setup, memory.
|
|
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
|
+
|
|
10
|
+
class WorkspaceCommandsMixin:
|
|
11
|
+
"""Mixin: Workspace commands: packages, file, project, init, setup, memory."""
|
|
12
|
+
|
|
13
|
+
async def cmd_packages(self, args: str):
|
|
14
|
+
"""Show Aria Code package facades and Arthera package bridge status."""
|
|
15
|
+
try:
|
|
16
|
+
from packages.aria_agents import list_agent_manifests
|
|
17
|
+
from packages.aria_core import (
|
|
18
|
+
build_package_manifest,
|
|
19
|
+
list_architecture_layers,
|
|
20
|
+
required_architecture_layer_names,
|
|
21
|
+
write_package_manifest,
|
|
22
|
+
)
|
|
23
|
+
from packages.aria_infra import (
|
|
24
|
+
aria_code_identity,
|
|
25
|
+
build_package_doctor_report,
|
|
26
|
+
discover_arthera_packages,
|
|
27
|
+
)
|
|
28
|
+
from packages.aria_mcp import (
|
|
29
|
+
arthera_quant_engine_server_config,
|
|
30
|
+
default_exposures,
|
|
31
|
+
load_mcp_config,
|
|
32
|
+
merge_server_config,
|
|
33
|
+
mcp_server_status,
|
|
34
|
+
mcp_tools_to_specs,
|
|
35
|
+
write_mcp_config,
|
|
36
|
+
)
|
|
37
|
+
from packages.aria_services import (
|
|
38
|
+
list_service_specs,
|
|
39
|
+
list_service_usage_specs,
|
|
40
|
+
required_service_names,
|
|
41
|
+
)
|
|
42
|
+
from packages.aria_services.provider_health import GLOBAL_PROVIDER_HEALTH
|
|
43
|
+
from packages.aria_skills import builtin_skill_specs
|
|
44
|
+
from packages.aria_tools import build_registry_from_legacy
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
_print_error(f"packages facade unavailable: {exc}")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
sub = args.strip().lower()
|
|
50
|
+
identity = aria_code_identity(__version__)
|
|
51
|
+
arthera = discover_arthera_packages()
|
|
52
|
+
tool_registry = build_registry_from_legacy(LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS)
|
|
53
|
+
services = list_service_specs()
|
|
54
|
+
agent_count = len(list_agent_manifests())
|
|
55
|
+
skill_count = len(builtin_skill_specs())
|
|
56
|
+
mcp_exposure_count = len(default_exposures())
|
|
57
|
+
server_cfg = arthera_quant_engine_server_config()
|
|
58
|
+
mcp_config_path = MCP_CONFIG_PATH if _HAS_MCP else pathlib.Path.home() / ".arthera" / "mcp_servers.json"
|
|
59
|
+
|
|
60
|
+
if sub.startswith("export-manifest") or sub.startswith("manifest"):
|
|
61
|
+
raw_parts = args.strip().split(maxsplit=1)
|
|
62
|
+
out_path = None
|
|
63
|
+
if len(raw_parts) > 1:
|
|
64
|
+
out_path = pathlib.Path(raw_parts[1]).expanduser()
|
|
65
|
+
else:
|
|
66
|
+
from artifacts import artifact_dir
|
|
67
|
+
out_path = artifact_dir("manifests", "packages") / "aria_package_manifest.json"
|
|
68
|
+
|
|
69
|
+
reg = getattr(self.terminal, "_mcp_registry", None)
|
|
70
|
+
mcp_tools = []
|
|
71
|
+
if reg:
|
|
72
|
+
try:
|
|
73
|
+
mcp_tools = [
|
|
74
|
+
tool for tool in reg.all_tools()
|
|
75
|
+
if tool.get("server") == "arthera_quant_engine"
|
|
76
|
+
]
|
|
77
|
+
except Exception:
|
|
78
|
+
mcp_tools = []
|
|
79
|
+
arthera_specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
|
|
80
|
+
manifest = build_package_manifest(
|
|
81
|
+
identity=identity,
|
|
82
|
+
services=services,
|
|
83
|
+
tools=tool_registry.list(),
|
|
84
|
+
agents=list_agent_manifests(),
|
|
85
|
+
skills=builtin_skill_specs(),
|
|
86
|
+
mcp_exposures=default_exposures(),
|
|
87
|
+
arthera_packages=arthera,
|
|
88
|
+
arthera_mcp_tools=arthera_specs,
|
|
89
|
+
)
|
|
90
|
+
write_package_manifest(out_path, manifest)
|
|
91
|
+
if HAS_RICH:
|
|
92
|
+
console.print()
|
|
93
|
+
console.print(f" [green]Package manifest exported[/green] [dim]{out_path}[/dim]")
|
|
94
|
+
console.print(
|
|
95
|
+
f" [dim]tools={len(manifest['capabilities']['tools'])} "
|
|
96
|
+
f"services={len(manifest['capabilities']['services'])} "
|
|
97
|
+
f"agents={len(manifest['capabilities']['agents'])} "
|
|
98
|
+
f"skills={len(manifest['capabilities']['skills'])} "
|
|
99
|
+
f"arthera_mcp_tools={len(manifest['capabilities']['arthera_mcp_tools'])}[/dim]\n"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
print(f"Package manifest exported: {out_path}")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if sub in ("doctor", "doctor arthera", "arthera doctor"):
|
|
106
|
+
reg = getattr(self.terminal, "_mcp_registry", None)
|
|
107
|
+
runtime_status = []
|
|
108
|
+
mcp_tools = []
|
|
109
|
+
if reg:
|
|
110
|
+
try:
|
|
111
|
+
runtime_status = reg.status()
|
|
112
|
+
mcp_tools = [
|
|
113
|
+
tool for tool in reg.all_tools()
|
|
114
|
+
if tool.get("server") == "arthera_quant_engine"
|
|
115
|
+
]
|
|
116
|
+
except Exception:
|
|
117
|
+
runtime_status = []
|
|
118
|
+
mcp_tools = []
|
|
119
|
+
status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
|
|
120
|
+
specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
|
|
121
|
+
from artifacts import artifact_dir
|
|
122
|
+
manifest_path = artifact_dir("manifests", "packages", create=False) / "aria_package_manifest.json"
|
|
123
|
+
report = build_package_doctor_report(
|
|
124
|
+
arthera=arthera,
|
|
125
|
+
mcp_status=status,
|
|
126
|
+
tool_count=len(specs),
|
|
127
|
+
manifest_can_export=True,
|
|
128
|
+
manifest_path=manifest_path,
|
|
129
|
+
services=services,
|
|
130
|
+
required_services=required_service_names(),
|
|
131
|
+
architecture_layers=list_architecture_layers(),
|
|
132
|
+
required_architecture_layers=required_architecture_layer_names(),
|
|
133
|
+
provider_health=GLOBAL_PROVIDER_HEALTH.snapshot(),
|
|
134
|
+
)
|
|
135
|
+
if HAS_RICH:
|
|
136
|
+
from rich.table import Table as _Table
|
|
137
|
+
color = "green" if report.status == "ok" else "yellow" if report.status == "warn" else "red"
|
|
138
|
+
console.print()
|
|
139
|
+
console.print(
|
|
140
|
+
f" [bold]{identity.product}[/bold] "
|
|
141
|
+
f"[dim]· {identity.company} packages doctor ·[/dim] [{color}]{report.status}[/{color}]\n"
|
|
142
|
+
)
|
|
143
|
+
tbl = _Table(
|
|
144
|
+
box=rich_box.ROUNDED,
|
|
145
|
+
border_style="dim",
|
|
146
|
+
show_header=True,
|
|
147
|
+
header_style="bold dim",
|
|
148
|
+
)
|
|
149
|
+
tbl.add_column("Check", width=22)
|
|
150
|
+
tbl.add_column("Status", width=8)
|
|
151
|
+
tbl.add_column("Detail")
|
|
152
|
+
tbl.add_column("Next")
|
|
153
|
+
for check in report.checks:
|
|
154
|
+
st_color = "green" if check.status == "ok" else "yellow" if check.status == "warn" else "red"
|
|
155
|
+
tbl.add_row(
|
|
156
|
+
check.name,
|
|
157
|
+
f"[{st_color}]{check.status}[/{st_color}]",
|
|
158
|
+
check.detail,
|
|
159
|
+
check.remediation,
|
|
160
|
+
)
|
|
161
|
+
console.print(tbl)
|
|
162
|
+
console.print()
|
|
163
|
+
else:
|
|
164
|
+
print(f"{identity.product} packages doctor: {report.status}")
|
|
165
|
+
for check in report.checks:
|
|
166
|
+
print(f"{check.status:5s} {check.name}: {check.detail} {check.remediation}")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if sub in ("status", "status arthera", "arthera status"):
|
|
170
|
+
runtime_status = []
|
|
171
|
+
reg = getattr(self.terminal, "_mcp_registry", None)
|
|
172
|
+
if reg:
|
|
173
|
+
try:
|
|
174
|
+
runtime_status = reg.status()
|
|
175
|
+
except Exception:
|
|
176
|
+
runtime_status = []
|
|
177
|
+
status = mcp_server_status(mcp_config_path, "arthera_quant_engine", runtime_status)
|
|
178
|
+
|
|
179
|
+
if HAS_RICH:
|
|
180
|
+
console.print()
|
|
181
|
+
console.print(
|
|
182
|
+
f" [bold]{identity.product}[/bold] "
|
|
183
|
+
f"[dim]· {identity.company} package bridge status[/dim]\n"
|
|
184
|
+
)
|
|
185
|
+
rows = [
|
|
186
|
+
("config", status["config_path"]),
|
|
187
|
+
("configured", "yes" if status["configured"] else "no"),
|
|
188
|
+
("server", status["server_path"] or "—"),
|
|
189
|
+
("server file", "found" if status["server_file_exists"] else "missing"),
|
|
190
|
+
("runtime", "running" if status["running"] else "not running"),
|
|
191
|
+
("tools", str(status["tool_count"])),
|
|
192
|
+
]
|
|
193
|
+
for key, value in rows:
|
|
194
|
+
style = "green" if value in ("yes", "found", "running") else "yellow" if value in ("no", "missing", "not running") else "dim"
|
|
195
|
+
console.print(f" [dim]{key:12s}[/dim] [{style}]{value}[/{style}]")
|
|
196
|
+
if status["tools"]:
|
|
197
|
+
console.print(" [dim]tool names:[/dim] " + ", ".join(status["tools"][:12]))
|
|
198
|
+
if not status["configured"]:
|
|
199
|
+
console.print(" [dim]Run /packages connect arthera to write the MCP bridge.[/dim]")
|
|
200
|
+
elif not status["running"]:
|
|
201
|
+
console.print(" [dim]Run /mcp reload or /packages connect arthera --reload.[/dim]")
|
|
202
|
+
console.print()
|
|
203
|
+
else:
|
|
204
|
+
print(f"{identity.product} · {identity.company} package bridge status")
|
|
205
|
+
for key, value in status.items():
|
|
206
|
+
if key != "tools":
|
|
207
|
+
print(f"{key}: {value}")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if sub in ("tools arthera", "arthera tools", "tools"):
|
|
211
|
+
reg = getattr(self.terminal, "_mcp_registry", None)
|
|
212
|
+
mcp_tools = []
|
|
213
|
+
if reg:
|
|
214
|
+
try:
|
|
215
|
+
mcp_tools = [
|
|
216
|
+
tool for tool in reg.all_tools()
|
|
217
|
+
if tool.get("server") == "arthera_quant_engine"
|
|
218
|
+
]
|
|
219
|
+
except Exception:
|
|
220
|
+
mcp_tools = []
|
|
221
|
+
specs = mcp_tools_to_specs(mcp_tools, "arthera_quant_engine")
|
|
222
|
+
|
|
223
|
+
if HAS_RICH:
|
|
224
|
+
from rich.table import Table as _Table
|
|
225
|
+
console.print()
|
|
226
|
+
console.print(
|
|
227
|
+
f" [bold]{identity.product}[/bold] "
|
|
228
|
+
f"[dim]· {identity.company} MCP tool manifests[/dim]\n"
|
|
229
|
+
)
|
|
230
|
+
if not specs:
|
|
231
|
+
console.print(" [yellow]No Arthera MCP tools discovered.[/yellow]")
|
|
232
|
+
console.print(" [dim]Run /packages connect arthera --reload, then retry /packages tools arthera.[/dim]\n")
|
|
233
|
+
return
|
|
234
|
+
tbl = _Table(
|
|
235
|
+
title="[bold]Arthera QuantEngine Tools[/bold]",
|
|
236
|
+
box=rich_box.ROUNDED,
|
|
237
|
+
border_style="dim",
|
|
238
|
+
show_header=True,
|
|
239
|
+
header_style="bold dim",
|
|
240
|
+
)
|
|
241
|
+
tbl.add_column("Tool", width=34)
|
|
242
|
+
tbl.add_column("Permissions", width=24)
|
|
243
|
+
tbl.add_column("Capabilities", width=30)
|
|
244
|
+
tbl.add_column("Schema")
|
|
245
|
+
for spec in specs:
|
|
246
|
+
perms = ", ".join(p.value for p in spec.permissions)
|
|
247
|
+
caps = ", ".join(spec.capabilities)
|
|
248
|
+
schema_state = "yes" if spec.schema else "no"
|
|
249
|
+
tbl.add_row(spec.name, perms, caps, schema_state)
|
|
250
|
+
console.print(tbl)
|
|
251
|
+
console.print()
|
|
252
|
+
else:
|
|
253
|
+
if not specs:
|
|
254
|
+
print("No Arthera MCP tools discovered. Run /packages connect arthera --reload.")
|
|
255
|
+
return
|
|
256
|
+
for spec in specs:
|
|
257
|
+
print(f"{spec.name}: {', '.join(spec.capabilities)}")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if sub in ("services", "service", "usage", "use", "map"):
|
|
261
|
+
usage_specs = list_service_usage_specs()
|
|
262
|
+
if HAS_RICH:
|
|
263
|
+
from rich.table import Table as _Table
|
|
264
|
+
console.print()
|
|
265
|
+
console.print(
|
|
266
|
+
f" [bold]{identity.product}[/bold] "
|
|
267
|
+
f"[dim]· service usage map[/dim]\n"
|
|
268
|
+
)
|
|
269
|
+
tbl = _Table(
|
|
270
|
+
title="[bold]Project Services[/bold]",
|
|
271
|
+
box=rich_box.ROUNDED,
|
|
272
|
+
border_style="dim",
|
|
273
|
+
show_header=True,
|
|
274
|
+
header_style="bold dim",
|
|
275
|
+
)
|
|
276
|
+
tbl.add_column("Service", width=22)
|
|
277
|
+
tbl.add_column("Purpose", width=34)
|
|
278
|
+
tbl.add_column("CLI", width=32)
|
|
279
|
+
tbl.add_column("Arthera / MCP", width=34)
|
|
280
|
+
tbl.add_column("Next")
|
|
281
|
+
for spec in usage_specs:
|
|
282
|
+
mcp = ", ".join(spec.mcp_tools[:4]) if spec.mcp_tools else "—"
|
|
283
|
+
packages = ", ".join(spec.package_sources[:2])
|
|
284
|
+
tbl.add_row(
|
|
285
|
+
spec.name,
|
|
286
|
+
spec.purpose,
|
|
287
|
+
", ".join(spec.cli_entrypoints),
|
|
288
|
+
f"{packages}\n[dim]{mcp}[/dim]",
|
|
289
|
+
spec.next_step,
|
|
290
|
+
)
|
|
291
|
+
console.print(tbl)
|
|
292
|
+
console.print("[dim]连接 Arthera MCP: /packages connect arthera --reload;券商接入: /broker guide[/dim]\n")
|
|
293
|
+
else:
|
|
294
|
+
for spec in usage_specs:
|
|
295
|
+
print(f"{spec.name}: {spec.purpose} -> {', '.join(spec.cli_entrypoints)}")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if sub.startswith("connect arthera") or sub in ("connect", "connect-quant", "connect quant"):
|
|
299
|
+
existing = load_mcp_config(mcp_config_path)
|
|
300
|
+
updated = merge_server_config(existing, server_cfg)
|
|
301
|
+
write_mcp_config(mcp_config_path, updated)
|
|
302
|
+
server_path = pathlib.Path(str(server_cfg["args"][0]))
|
|
303
|
+
ready = server_path.exists()
|
|
304
|
+
if HAS_RICH:
|
|
305
|
+
console.print()
|
|
306
|
+
console.print(
|
|
307
|
+
f" [bold]{identity.product}[/bold] "
|
|
308
|
+
f"[dim]connected to {identity.company} package bridge[/dim]"
|
|
309
|
+
)
|
|
310
|
+
status = "[green]ready[/green]" if ready else "[yellow]configured, server file not found[/yellow]"
|
|
311
|
+
console.print(f" {status} [dim]{server_cfg['name']}[/dim]")
|
|
312
|
+
console.print(f" [dim]config:[/dim] {mcp_config_path}")
|
|
313
|
+
console.print(f" [dim]server:[/dim] {server_path}")
|
|
314
|
+
else:
|
|
315
|
+
print(f"Connected {identity.product} -> {server_cfg['name']}")
|
|
316
|
+
print(f"config: {mcp_config_path}")
|
|
317
|
+
print(f"server: {server_path} ({'ready' if ready else 'missing'})")
|
|
318
|
+
|
|
319
|
+
if "--reload" in sub or " reload" in sub:
|
|
320
|
+
await self.cmd_mcp("reload")
|
|
321
|
+
else:
|
|
322
|
+
if HAS_RICH:
|
|
323
|
+
console.print(" [dim]Run /mcp reload to start the server.[/dim]\n")
|
|
324
|
+
else:
|
|
325
|
+
print("Run /mcp reload to start the server.")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if HAS_RICH:
|
|
329
|
+
from rich.table import Table as _Table
|
|
330
|
+
|
|
331
|
+
console.print()
|
|
332
|
+
console.print(
|
|
333
|
+
f" [bold]{identity.product}[/bold] "
|
|
334
|
+
f"[dim]v{identity.version} · {identity.company} product[/dim]"
|
|
335
|
+
)
|
|
336
|
+
console.print(f" [dim]{identity.description}[/dim]\n")
|
|
337
|
+
|
|
338
|
+
tbl = _Table(
|
|
339
|
+
title="[bold]Package Facades[/bold]",
|
|
340
|
+
box=rich_box.ROUNDED,
|
|
341
|
+
border_style="dim",
|
|
342
|
+
show_header=True,
|
|
343
|
+
header_style="bold dim",
|
|
344
|
+
)
|
|
345
|
+
tbl.add_column("Package")
|
|
346
|
+
tbl.add_column("Status")
|
|
347
|
+
tbl.add_column("Surface")
|
|
348
|
+
tbl.add_row("aria_core", "ready", "CapabilityManifest, permissions")
|
|
349
|
+
tbl.add_row("aria_services", "ready", f"{len(services)} service boundaries")
|
|
350
|
+
tbl.add_row("aria_tools", "ready", f"{len(tool_registry.list())} tools")
|
|
351
|
+
tbl.add_row("aria_agents", "ready", f"{agent_count} agents")
|
|
352
|
+
tbl.add_row("aria_skills", "ready", f"{skill_count} skills")
|
|
353
|
+
tbl.add_row("aria_mcp", "ready", f"{mcp_exposure_count} planned exposures")
|
|
354
|
+
tbl.add_row("aria_infra", "ready", "Arthera package discovery")
|
|
355
|
+
console.print(tbl)
|
|
356
|
+
|
|
357
|
+
console.print()
|
|
358
|
+
console.print("[bold]Arthera Packages[/bold]")
|
|
359
|
+
from ui.render.output import display_path as _display_path
|
|
360
|
+
if arthera.available:
|
|
361
|
+
console.print(f" [green]found[/green] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
|
|
362
|
+
for name, path in sorted(arthera.packages.items()):
|
|
363
|
+
console.print(f" [dim]·[/dim] [bold]{name:14s}[/bold] [dim]{_display_path(path, fallback='package')}[/dim]")
|
|
364
|
+
if arthera.mcp_servers:
|
|
365
|
+
console.print(" [dim]MCP server candidates:[/dim]")
|
|
366
|
+
for path in arthera.mcp_servers[:5]:
|
|
367
|
+
console.print(f" [dim]{_display_path(path, fallback='server')}[/dim]")
|
|
368
|
+
else:
|
|
369
|
+
console.print(f" [yellow]not found[/yellow] [dim]{_display_path(arthera.root, fallback='package root')}[/dim]")
|
|
370
|
+
|
|
371
|
+
console.print()
|
|
372
|
+
console.print("[bold]Recommended MCP bridge[/bold]")
|
|
373
|
+
console.print(f" [dim]name:[/dim] {server_cfg['name']}")
|
|
374
|
+
console.print(f" [dim]command:[/dim] {server_cfg['command']} {' '.join(server_cfg['args'])}")
|
|
375
|
+
console.print(f" [dim]env PYTHONPATH:[/dim] {server_cfg['env']['PYTHONPATH']}")
|
|
376
|
+
console.print(f" [dim]config:[/dim] {mcp_config_path}")
|
|
377
|
+
console.print(" [dim]Run /packages connect arthera to write this MCP bridge.[/dim]\n")
|
|
378
|
+
else:
|
|
379
|
+
print(f"{identity.product} v{identity.version} · {identity.company} product")
|
|
380
|
+
print(f"services={len(services)} tools={len(tool_registry.list())} agents={agent_count} skills={skill_count} mcp={mcp_exposure_count}")
|
|
381
|
+
print(f"Arthera packages: {'found' if arthera.available else 'not found'} {arthera.root}")
|
|
382
|
+
print(f"Recommended MCP: {server_cfg}")
|
|
383
|
+
|
|
384
|
+
async def cmd_file(self, args: str):
|
|
385
|
+
"""
|
|
386
|
+
/file load <路径> — 加载文件到会话
|
|
387
|
+
/file analyze [1|2|3|4] — 分层分析(1=摘要 2=深度 3=领域 4=建议)
|
|
388
|
+
/file ask <问题> — 就已加载文件提问
|
|
389
|
+
/file list — 列出会话中的所有文件
|
|
390
|
+
/file switch <文件名> — 切换活跃文件
|
|
391
|
+
/file clear [文件名] — 清除文件
|
|
392
|
+
/file check — 检查可用解析器
|
|
393
|
+
"""
|
|
394
|
+
import asyncio as _asyncio
|
|
395
|
+
loop = _asyncio.get_event_loop()
|
|
396
|
+
parts = args.strip().split(None, 1) if args.strip() else []
|
|
397
|
+
sub = parts[0].lower() if parts else "help"
|
|
398
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
399
|
+
|
|
400
|
+
# ── 确保 file_session 已初始化 ────────────────────────────────────────
|
|
401
|
+
if self.terminal._file_session is None:
|
|
402
|
+
try:
|
|
403
|
+
from file_analysis_tools import FileSession
|
|
404
|
+
self.terminal._file_session = FileSession()
|
|
405
|
+
except ImportError as e:
|
|
406
|
+
if HAS_RICH:
|
|
407
|
+
console.print(f"[red]file_analysis_tools 未加载: {e}[/red]")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
fs = self.terminal._file_session
|
|
411
|
+
|
|
412
|
+
# ────────────────── /file load ────────────────────────────────────────
|
|
413
|
+
if sub == "load":
|
|
414
|
+
if not rest:
|
|
415
|
+
if HAS_RICH:
|
|
416
|
+
console.print("[dim]用法: /file load <文件路径>[/dim]")
|
|
417
|
+
console.print("[dim]支持: PDF DOCX XLSX CSV JSON TXT MD 图片 代码文件[/dim]")
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
if HAS_RICH:
|
|
421
|
+
console.print(f"[dim]正在解析 {rest}...[/dim]")
|
|
422
|
+
|
|
423
|
+
# Include images only for vision-capable models
|
|
424
|
+
_curr_model = self.terminal.config.get("model", "")
|
|
425
|
+
include_img = False
|
|
426
|
+
if _HAS_MODEL_CAP:
|
|
427
|
+
try:
|
|
428
|
+
_mc = get_model_capability(_curr_model)
|
|
429
|
+
include_img = bool(_mc.vision)
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
from file_analysis_tools import parse_file, check_parsers
|
|
434
|
+
fc = await loop.run_in_executor(
|
|
435
|
+
None, lambda: parse_file(rest, include_images=include_img))
|
|
436
|
+
|
|
437
|
+
if not fc.success:
|
|
438
|
+
if HAS_RICH:
|
|
439
|
+
console.print(f"[red]解析失败: {fc.error}[/red]")
|
|
440
|
+
# Show which parsers are available
|
|
441
|
+
parsers = check_parsers()
|
|
442
|
+
missing = [k for k, v in parsers.items() if not v]
|
|
443
|
+
if missing:
|
|
444
|
+
console.print(f"[yellow]⚠ 未安装解析器: {', '.join(missing)}[/yellow]")
|
|
445
|
+
console.print(f"[dim]安装命令: pip install {' '.join(missing)}[/dim]")
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
fs.load(rest, include_images=include_img)
|
|
449
|
+
self.terminal._file_ctx_injected = False # Reset so next msg injects file
|
|
450
|
+
|
|
451
|
+
if HAS_RICH:
|
|
452
|
+
from rich.panel import Panel as _P
|
|
453
|
+
from rich import box as _box
|
|
454
|
+
info_lines = [
|
|
455
|
+
f"[green]✓[/green] [bold]{fc.filename}[/bold]",
|
|
456
|
+
f"[dim]类型: {fc.file_type.upper()} 大小: {fc.size_kb:.1f} KB "
|
|
457
|
+
f"提取: {fc.char_count:,} 字符[/dim]",
|
|
458
|
+
]
|
|
459
|
+
for k, v in fc.metadata.items():
|
|
460
|
+
if k in ("pages","rows","columns","lines","language",
|
|
461
|
+
"sheets","title","author","symbols"):
|
|
462
|
+
val = v[:5] if isinstance(v, list) else v
|
|
463
|
+
info_lines.append(f"[dim]{k}: {val}[/dim]")
|
|
464
|
+
if fc.truncated:
|
|
465
|
+
info_lines.append(f"[yellow]⚠ 内容已截断(文件较大)[/yellow]")
|
|
466
|
+
if fc.tables:
|
|
467
|
+
info_lines.append(f"[dim]包含 {len(fc.tables)} 个表格[/dim]")
|
|
468
|
+
info_lines.append(f"\n[dim]发送任何消息即可开始分析,或使用 /file analyze 1-4[/dim]")
|
|
469
|
+
console.print(_P("\n".join(info_lines),
|
|
470
|
+
title="[bold]📄 文件已加载[/bold]",
|
|
471
|
+
border_style="green", box=_box.ROUNDED))
|
|
472
|
+
|
|
473
|
+
# ────────────────── /file analyze ─────────────────────────────────────
|
|
474
|
+
elif sub == "analyze":
|
|
475
|
+
fc = fs.get_active()
|
|
476
|
+
if not fc:
|
|
477
|
+
if HAS_RICH: console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
# Determine layer(s) to run
|
|
481
|
+
layer_arg = rest.strip()
|
|
482
|
+
if layer_arg == "all":
|
|
483
|
+
layers_to_run = [1, 2, 3, 4]
|
|
484
|
+
else:
|
|
485
|
+
try:
|
|
486
|
+
layers_to_run = [int(layer_arg)] if layer_arg else [1, 2]
|
|
487
|
+
except ValueError:
|
|
488
|
+
layers_to_run = [1, 2]
|
|
489
|
+
|
|
490
|
+
layer_names = {1: "📌 快速摘要", 2: "🔍 深度分析", 3: "💡 领域洞察", 4: "✅ 行动建议"}
|
|
491
|
+
|
|
492
|
+
from file_analysis_tools import build_analysis_prompt
|
|
493
|
+
|
|
494
|
+
for layer in layers_to_run:
|
|
495
|
+
if HAS_RICH:
|
|
496
|
+
console.print(f"\n[bold]{layer_names.get(layer, f'层{layer}')}[/bold]")
|
|
497
|
+
console.print(f"[dim]{'─'*50}[/dim]")
|
|
498
|
+
|
|
499
|
+
prompt = build_analysis_prompt(fc, layer=layer)
|
|
500
|
+
# Send to LLM via the normal message pipeline
|
|
501
|
+
await self.terminal.send_message(
|
|
502
|
+
prompt,
|
|
503
|
+
system_override=(
|
|
504
|
+
"你是专业文档分析助手,具备金融、法律、技术、不动产等多领域知识。"
|
|
505
|
+
"分析要精确、结构化,优先使用数字和具体事实。"
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if len(layers_to_run) > 1 and layer < layers_to_run[-1]:
|
|
510
|
+
if HAS_RICH:
|
|
511
|
+
console.print(f"\n[dim]{'═'*60}[/dim]")
|
|
512
|
+
console.print(f"[dim]进入下一层分析...[/dim]\n")
|
|
513
|
+
|
|
514
|
+
# ────────────────── /file ask ──────────────────────────────────────────
|
|
515
|
+
elif sub == "ask":
|
|
516
|
+
fc = fs.get_active()
|
|
517
|
+
if not fc:
|
|
518
|
+
if HAS_RICH: console.print("[dim]请先使用 /file load <路径> 加载文件[/dim]")
|
|
519
|
+
return
|
|
520
|
+
question = rest.strip()
|
|
521
|
+
if not question:
|
|
522
|
+
if HAS_RICH:
|
|
523
|
+
from rich.prompt import Prompt as _Prompt
|
|
524
|
+
question = _Prompt.ask(" 请输入问题")
|
|
525
|
+
else:
|
|
526
|
+
question = input("请输入问题: ")
|
|
527
|
+
if not question:
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
from file_analysis_tools import build_analysis_prompt
|
|
531
|
+
prompt = build_analysis_prompt(fc, layer=0, question=question)
|
|
532
|
+
await self.terminal.send_message(
|
|
533
|
+
prompt,
|
|
534
|
+
system_override=(
|
|
535
|
+
"你是专业文档分析助手。请基于用户提供的文件内容准确回答问题,"
|
|
536
|
+
"若文件中无法找到答案,请如实说明。"
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# ────────────────── /file list ─────────────────────────────────────────
|
|
541
|
+
elif sub == "list":
|
|
542
|
+
files = fs.list_files()
|
|
543
|
+
if not files:
|
|
544
|
+
if HAS_RICH: console.print("[dim]会话中暂无已加载文件。使用 /file load <路径>[/dim]")
|
|
545
|
+
return
|
|
546
|
+
if HAS_RICH:
|
|
547
|
+
from rich.table import Table as _T
|
|
548
|
+
from rich import box as _box
|
|
549
|
+
tb = _T(title="[bold]📂 已加载文件[/bold]", box=_box.ROUNDED)
|
|
550
|
+
tb.add_column("状态", width=4); tb.add_column("文件名")
|
|
551
|
+
tb.add_column("类型", style="dim"); tb.add_column("大小KB", justify="right", style="dim")
|
|
552
|
+
tb.add_column("字符数", justify="right", style="dim"); tb.add_column("截断", style="dim")
|
|
553
|
+
for f in files:
|
|
554
|
+
status = "[green]●[/green]" if f["active"] else "[dim]○[/dim]"
|
|
555
|
+
tb.add_row(status, f["filename"], f["type"].upper(),
|
|
556
|
+
str(f["size_kb"]), f"{f['chars']:,}",
|
|
557
|
+
"[yellow]是[/yellow]" if f["truncated"] else "否")
|
|
558
|
+
console.print(tb)
|
|
559
|
+
console.print("[dim]/file ask <问题> 向活跃文件提问[/dim]")
|
|
560
|
+
|
|
561
|
+
# ────────────────── /file switch ──────────────────────────────────────
|
|
562
|
+
elif sub == "switch":
|
|
563
|
+
if not rest:
|
|
564
|
+
if HAS_RICH: console.print("[dim]用法: /file switch <文件名>[/dim]")
|
|
565
|
+
return
|
|
566
|
+
if fs.set_active(rest):
|
|
567
|
+
fc = fs.get_active()
|
|
568
|
+
self.terminal._file_ctx_injected = False
|
|
569
|
+
if HAS_RICH:
|
|
570
|
+
console.print(f"[green]✓ 已切换到: {fc.filename}[/green]")
|
|
571
|
+
else:
|
|
572
|
+
if HAS_RICH: console.print(f"[red]未找到文件: {rest}[/red]")
|
|
573
|
+
|
|
574
|
+
# ────────────────── /file clear ──────────────────────────────────────
|
|
575
|
+
elif sub == "clear":
|
|
576
|
+
fs.clear(rest if rest else None)
|
|
577
|
+
self.terminal._file_ctx_injected = False
|
|
578
|
+
msg = f"已清除文件: {rest}" if rest else "已清除所有已加载文件"
|
|
579
|
+
if HAS_RICH: console.print(f"[dim]{msg}[/dim]")
|
|
580
|
+
|
|
581
|
+
# ────────────────── /file check ───────────────────────────────────────
|
|
582
|
+
elif sub == "check":
|
|
583
|
+
from file_analysis_tools import check_parsers
|
|
584
|
+
parsers = check_parsers()
|
|
585
|
+
if HAS_RICH:
|
|
586
|
+
from rich.table import Table as _T
|
|
587
|
+
from rich import box as _box
|
|
588
|
+
tb = _T(title="[bold]📦 文件解析器状态[/bold]", box=_box.ROUNDED)
|
|
589
|
+
tb.add_column("库"); tb.add_column("状态"); tb.add_column("安装命令", style="dim")
|
|
590
|
+
_CMDS = {
|
|
591
|
+
"pdfplumber": "pip install pdfplumber",
|
|
592
|
+
"pypdf": "pip install pypdf",
|
|
593
|
+
"python-docx":"pip install python-docx",
|
|
594
|
+
"pandas": "pip install pandas",
|
|
595
|
+
"openpyxl": "pip install openpyxl",
|
|
596
|
+
"beautifulsoup4": "pip install beautifulsoup4",
|
|
597
|
+
"Pillow": "pip install Pillow",
|
|
598
|
+
}
|
|
599
|
+
for lib, ok in parsers.items():
|
|
600
|
+
status = "[green]✓ 已安装[/green]" if ok else "[red]✗ 未安装[/red]"
|
|
601
|
+
tb.add_row(lib, status, "" if ok else _CMDS.get(lib,""))
|
|
602
|
+
console.print(tb)
|
|
603
|
+
formats_ok = []
|
|
604
|
+
if parsers.get("pdfplumber") or parsers.get("pypdf"):
|
|
605
|
+
formats_ok.append("PDF")
|
|
606
|
+
if parsers.get("python-docx"):
|
|
607
|
+
formats_ok.append("Word/DOCX")
|
|
608
|
+
if parsers.get("pandas") and parsers.get("openpyxl"):
|
|
609
|
+
formats_ok.append("Excel/CSV")
|
|
610
|
+
formats_ok.extend(["JSON", "TXT/MD", "代码文件"])
|
|
611
|
+
console.print(f"[dim]可解析格式: {', '.join(formats_ok)}[/dim]")
|
|
612
|
+
|
|
613
|
+
# ────────────────── /file help ─────────────────────────────────────────
|
|
614
|
+
else:
|
|
615
|
+
if HAS_RICH:
|
|
616
|
+
console.print("[bold]📄 /file 文件分析命令[/bold]")
|
|
617
|
+
rows = [
|
|
618
|
+
("/file load <路径>", "加载文件 (PDF/DOCX/XLSX/CSV/JSON/TXT/代码/图片)"),
|
|
619
|
+
("/file analyze 1", "快速摘要 (300字)"),
|
|
620
|
+
("/file analyze 2", "深度内容分析 (结构/要点/异常)"),
|
|
621
|
+
("/file analyze 3", "领域专项分析 (财务/法律/技术/不动产)"),
|
|
622
|
+
("/file analyze 4", "行动建议与风险清单"),
|
|
623
|
+
("/file analyze all", "依次运行 4 层分析"),
|
|
624
|
+
("/file ask <问题>", "就文件内容多轮提问"),
|
|
625
|
+
("/file list", "查看已加载文件"),
|
|
626
|
+
("/file switch <文件名>", "切换分析目标文件"),
|
|
627
|
+
("/file clear", "清除所有已加载文件"),
|
|
628
|
+
("/file check", "检查已安装的解析器"),
|
|
629
|
+
]
|
|
630
|
+
from rich.table import Table as _T
|
|
631
|
+
from rich import box as _box
|
|
632
|
+
tb = _T(box=_box.MINIMAL)
|
|
633
|
+
tb.add_column("命令", style="cyan"); tb.add_column("说明", style="dim")
|
|
634
|
+
for cmd, desc in rows:
|
|
635
|
+
tb.add_row(cmd, desc)
|
|
636
|
+
console.print(tb)
|
|
637
|
+
|
|
638
|
+
async def cmd_project(self, args: str):
|
|
639
|
+
"""项目分析 (Claude Code / Codex 风格): /project load|tree|grep|ask|task|status|info <参数>"""
|
|
640
|
+
try:
|
|
641
|
+
from project_tools import ProjectSession, scan_project, format_grep_results
|
|
642
|
+
_HAS_PT = True
|
|
643
|
+
except ImportError:
|
|
644
|
+
_HAS_PT = False
|
|
645
|
+
|
|
646
|
+
if not _HAS_PT:
|
|
647
|
+
console.print("[red]❌ project_tools.py 未找到,请确保文件存在。[/red]")
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
parts = args.strip().split(maxsplit=1)
|
|
651
|
+
sub = parts[0].lower() if parts else "info"
|
|
652
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
653
|
+
ps = self.terminal._project_session # type: ignore[attr-defined]
|
|
654
|
+
|
|
655
|
+
# ── load ──────────────────────────────────────────────────────────────
|
|
656
|
+
if sub == "load":
|
|
657
|
+
if not rest:
|
|
658
|
+
console.print("[yellow]用法: /project load <目录路径>[/yellow]")
|
|
659
|
+
return
|
|
660
|
+
from pathlib import Path as _Path
|
|
661
|
+
target = _Path(rest).expanduser().resolve()
|
|
662
|
+
if not target.exists():
|
|
663
|
+
console.print(f"[red]路径不存在: {target}[/red]")
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
console.print(f"[dim]正在扫描项目: {target} …[/dim]")
|
|
667
|
+
try:
|
|
668
|
+
new_ps = scan_project(str(target), max_files=2000)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
console.print(f"[red]扫描失败: {e}[/red]")
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
self.terminal._project_session = new_ps # type: ignore[attr-defined]
|
|
674
|
+
self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
|
|
675
|
+
|
|
676
|
+
# Auto-archive project into global memory
|
|
677
|
+
if getattr(self.terminal, "memory_mgr", None):
|
|
678
|
+
try:
|
|
679
|
+
_ps_s = new_ps.summary()
|
|
680
|
+
self.terminal.memory_mgr.upsert_project(_ps_s["name"], {
|
|
681
|
+
"root": _ps_s["root"],
|
|
682
|
+
"type": _ps_s["type"],
|
|
683
|
+
"languages": _ps_s.get("languages", []),
|
|
684
|
+
})
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
s = new_ps.summary()
|
|
689
|
+
tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
|
|
690
|
+
tb.add_column("k", style="dim", width=14)
|
|
691
|
+
tb.add_column("v", style="cyan")
|
|
692
|
+
tb.add_row("项目名", s["name"])
|
|
693
|
+
tb.add_row("路径", s["root"])
|
|
694
|
+
tb.add_row("类型", s["type"])
|
|
695
|
+
tb.add_row("语言", ", ".join(s["languages"][:4]))
|
|
696
|
+
tb.add_row("文件数", str(s["total_files"]))
|
|
697
|
+
tb.add_row("代码行", f"{s['total_lines']:,}")
|
|
698
|
+
tb.add_row("总大小", f"{s['total_size_kb']} KB")
|
|
699
|
+
if s["git"].get("branch"):
|
|
700
|
+
tb.add_row("Git 分支", s["git"]["branch"])
|
|
701
|
+
if s["git"].get("changed_count"):
|
|
702
|
+
tb.add_row("变更文件", str(s["git"]["changed_count"]))
|
|
703
|
+
console.print(f"\n[bold]项目已加载 ✓[/bold]")
|
|
704
|
+
console.print(tb)
|
|
705
|
+
console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:6])}[/dim]")
|
|
706
|
+
console.print("[dim]现在可以直接对话,Aria 将根据项目上下文回答。[/dim]\n")
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
# ── 未加载时提示 ──────────────────────────────────────────────────────
|
|
710
|
+
if ps is None:
|
|
711
|
+
console.print("[yellow]请先加载项目: /project load <目录路径>[/yellow]")
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
# ── tree ──────────────────────────────────────────────────────────────
|
|
715
|
+
if sub == "tree":
|
|
716
|
+
depth_arg = rest.strip()
|
|
717
|
+
max_lines = 120
|
|
718
|
+
if depth_arg.isdigit():
|
|
719
|
+
max_lines = int(depth_arg) * 30 # rough approximation
|
|
720
|
+
tree_str = ps.get_tree(max_lines=max_lines)
|
|
721
|
+
console.print(f"\n[bold]{ps.name}/[/bold]")
|
|
722
|
+
console.print(f"[dim]{tree_str}[/dim]")
|
|
723
|
+
console.print(f"\n[dim]共 {ps.stats.get('total_files', 0)} 个文件[/dim]\n")
|
|
724
|
+
|
|
725
|
+
# ── grep / search ─────────────────────────────────────────────────────
|
|
726
|
+
elif sub in ("grep", "search"):
|
|
727
|
+
if not rest:
|
|
728
|
+
console.print("[yellow]用法: /project grep <正则表达式> [glob模式][/yellow]")
|
|
729
|
+
return
|
|
730
|
+
parts2 = rest.split(maxsplit=1)
|
|
731
|
+
pattern = parts2[0]
|
|
732
|
+
glob = parts2[1] if len(parts2) > 1 else "**/*"
|
|
733
|
+
console.print(f"[dim]搜索 \"{pattern}\" …[/dim]")
|
|
734
|
+
results = ps.grep(pattern, glob=glob, max_results=60)
|
|
735
|
+
console.print(format_grep_results(results, pattern))
|
|
736
|
+
|
|
737
|
+
# ── status ────────────────────────────────────────────────────────────
|
|
738
|
+
elif sub == "status":
|
|
739
|
+
gi = ps.git_info
|
|
740
|
+
if not gi:
|
|
741
|
+
console.print("[dim]当前项目不是 Git 仓库[/dim]")
|
|
742
|
+
return
|
|
743
|
+
console.print(f"\n[bold]Git 状态[/bold] — {ps.name}")
|
|
744
|
+
console.print(f" 分支: [cyan]{gi.get('branch','?')}[/cyan] "
|
|
745
|
+
f"变更: [yellow]{gi.get('changed_count', 0)}[/yellow] 个文件")
|
|
746
|
+
if gi.get("changed_files"):
|
|
747
|
+
for f in gi["changed_files"][:15]:
|
|
748
|
+
console.print(f" [dim]{f}[/dim]")
|
|
749
|
+
if gi.get("recent_commits"):
|
|
750
|
+
console.print("\n[bold]最近提交:[/bold]")
|
|
751
|
+
for c in gi["recent_commits"][:5]:
|
|
752
|
+
console.print(f" [dim]{c}[/dim]")
|
|
753
|
+
console.print()
|
|
754
|
+
|
|
755
|
+
# ── info ──────────────────────────────────────────────────────────────
|
|
756
|
+
elif sub in ("info", "summary", ""):
|
|
757
|
+
s = ps.summary()
|
|
758
|
+
tb = _T(box=_box.ROUNDED, show_header=False, padding=(0, 1))
|
|
759
|
+
tb.add_column("k", style="dim", width=14)
|
|
760
|
+
tb.add_column("v", style="cyan")
|
|
761
|
+
tb.add_row("项目名", s["name"])
|
|
762
|
+
tb.add_row("路径", s["root"])
|
|
763
|
+
tb.add_row("类型", s["type"])
|
|
764
|
+
tb.add_row("主要语言", ", ".join(s["languages"][:4]))
|
|
765
|
+
tb.add_row("文件数", str(s["total_files"]))
|
|
766
|
+
tb.add_row("代码行", f"{s['total_lines']:,}")
|
|
767
|
+
tb.add_row("大小", f"{s['total_size_kb']} KB")
|
|
768
|
+
if s["git"].get("branch"):
|
|
769
|
+
tb.add_row("Git 分支", s["git"]["branch"])
|
|
770
|
+
console.print(tb)
|
|
771
|
+
console.print(f"\n[dim]关键文件: {', '.join(s['key_files'][:8])}[/dim]\n")
|
|
772
|
+
|
|
773
|
+
# ── read ──────────────────────────────────────────────────────────────
|
|
774
|
+
elif sub == "read":
|
|
775
|
+
if not rest:
|
|
776
|
+
console.print("[yellow]用法: /project read <文件路径>[/yellow]")
|
|
777
|
+
return
|
|
778
|
+
ok, content = ps.read_file(rest)
|
|
779
|
+
if not ok:
|
|
780
|
+
console.print(f"[red]{content}[/red]")
|
|
781
|
+
return
|
|
782
|
+
lang = rest.rsplit(".", 1)[-1] if "." in rest else "text"
|
|
783
|
+
console.print(f"\n[bold]{rest}[/bold]")
|
|
784
|
+
if HAS_RICH:
|
|
785
|
+
from rich.syntax import Syntax
|
|
786
|
+
console.print(Syntax(content, lang, theme="monokai", line_numbers=True,
|
|
787
|
+
word_wrap=False))
|
|
788
|
+
else:
|
|
789
|
+
print(content)
|
|
790
|
+
|
|
791
|
+
# ── clear ─────────────────────────────────────────────────────────────
|
|
792
|
+
elif sub == "clear":
|
|
793
|
+
self.terminal._project_session = None # type: ignore[attr-defined]
|
|
794
|
+
self.terminal._project_ctx_injected = False # type: ignore[attr-defined]
|
|
795
|
+
console.print("[dim]项目上下文已清除[/dim]")
|
|
796
|
+
|
|
797
|
+
# ── ask / task → forward to AI with project context ───────────────────
|
|
798
|
+
elif sub in ("ask", "task"):
|
|
799
|
+
if not rest:
|
|
800
|
+
console.print(f"[yellow]用法: /project {sub} <问题或任务描述>[/yellow]")
|
|
801
|
+
return
|
|
802
|
+
# Delegate to the AI; project context is injected automatically via send_message
|
|
803
|
+
prefix = "请基于当前项目完成以下任务:\n" if sub == "task" else ""
|
|
804
|
+
await self.terminal.send_message(prefix + rest) # type: ignore[attr-defined]
|
|
805
|
+
|
|
806
|
+
# ── help ─────────────────────────────────────────────────────────────
|
|
807
|
+
else:
|
|
808
|
+
rows = [
|
|
809
|
+
("/project load <path>", "加载项目目录,构建文件索引"),
|
|
810
|
+
("/project tree [depth]", "显示文件树结构"),
|
|
811
|
+
("/project grep <pattern>", "跨文件正则搜索"),
|
|
812
|
+
("/project read <file>", "查看项目中的文件内容"),
|
|
813
|
+
("/project status", "Git 状态 + 最近提交"),
|
|
814
|
+
("/project info", "项目摘要(类型/语言/规模)"),
|
|
815
|
+
("/project ask <question>", "向 AI 提问(使用项目上下文)"),
|
|
816
|
+
("/project task <description>", "让 AI 执行任务(工具调用模式)"),
|
|
817
|
+
("/project clear", "卸载当前项目上下文"),
|
|
818
|
+
]
|
|
819
|
+
tb = _T(box=_box.MINIMAL)
|
|
820
|
+
tb.add_column("命令", style="cyan")
|
|
821
|
+
tb.add_column("说明", style="dim")
|
|
822
|
+
for cmd, desc in rows:
|
|
823
|
+
tb.add_row(cmd, desc)
|
|
824
|
+
console.print(f"\n[bold]/project — 项目分析命令[/bold]\n")
|
|
825
|
+
console.print(tb)
|
|
826
|
+
console.print()
|
|
827
|
+
|
|
828
|
+
async def cmd_init(self, args: str):
|
|
829
|
+
"""Bootstrap an ARIA.md memory file, or scaffold a new project.
|
|
830
|
+
|
|
831
|
+
Without arguments: scans the current directory and generates ARIA.md.
|
|
832
|
+
With a template name: creates a fully-runnable project scaffold.
|
|
833
|
+
|
|
834
|
+
Usage:
|
|
835
|
+
/init — generate ARIA.md for current project
|
|
836
|
+
/init --force — regenerate even if ARIA.md already exists
|
|
837
|
+
/init list — list available scaffold templates
|
|
838
|
+
/init quant [dir] — quantitative strategy project
|
|
839
|
+
/init analysis [dir] — data analysis project
|
|
840
|
+
/init fastapi [dir] — FastAPI financial data service
|
|
841
|
+
/init dashboard [dir] — Plotly Dash interactive dashboard
|
|
842
|
+
"""
|
|
843
|
+
global _PROJECT_CONTEXT
|
|
844
|
+
cwd = pathlib.Path.cwd()
|
|
845
|
+
|
|
846
|
+
# ── /init <template> — project scaffold ──────────────────────────────
|
|
847
|
+
_tmpl_key = args.strip().lower().split()[0] if args.strip() else ""
|
|
848
|
+
if _tmpl_key == "list":
|
|
849
|
+
rows = [(k, v["desc"]) for k, v in self._SCAFFOLD_TEMPLATES.items()]
|
|
850
|
+
if HAS_RICH:
|
|
851
|
+
from rich.table import Table as _Table
|
|
852
|
+
t = _Table(box=None, show_header=True, header_style="bold cyan", padding=(0,2))
|
|
853
|
+
t.add_column("模板", style="green")
|
|
854
|
+
t.add_column("说明")
|
|
855
|
+
for k, d in rows:
|
|
856
|
+
t.add_row(k, d)
|
|
857
|
+
console.print("\n [bold]可用脚手架模板[/bold]")
|
|
858
|
+
console.print(t)
|
|
859
|
+
console.print("\n 用法: [cyan]/init <模板名> [目录名][/cyan]\n")
|
|
860
|
+
else:
|
|
861
|
+
print("可用模板:"); [print(f" {k}: {d}") for k, d in rows]
|
|
862
|
+
return
|
|
863
|
+
|
|
864
|
+
if _tmpl_key in self._SCAFFOLD_TEMPLATES:
|
|
865
|
+
tmpl = self._SCAFFOLD_TEMPLATES[_tmpl_key]
|
|
866
|
+
# Optional second arg: target directory name
|
|
867
|
+
_args_parts = args.strip().split()
|
|
868
|
+
_target_name = _args_parts[1] if len(_args_parts) > 1 else f"{_tmpl_key}_project"
|
|
869
|
+
_target_path = pathlib.Path(_target_name).expanduser()
|
|
870
|
+
if _target_path.is_absolute():
|
|
871
|
+
target_dir = _target_path
|
|
872
|
+
else:
|
|
873
|
+
from artifacts import user_projects_dir as _user_projects_dir
|
|
874
|
+
target_dir = _user_projects_dir() / _target_name
|
|
875
|
+
if target_dir.exists():
|
|
876
|
+
console.print(f"[yellow]目录已存在: {target_dir}[/yellow]") if HAS_RICH else print(f"目录已存在: {target_dir}")
|
|
877
|
+
else:
|
|
878
|
+
target_dir.mkdir(parents=True)
|
|
879
|
+
created = self._create_scaffold(target_dir, tmpl)
|
|
880
|
+
if HAS_RICH:
|
|
881
|
+
from rich.panel import Panel as _SPanel
|
|
882
|
+
from rich import box as _sbox
|
|
883
|
+
lines = "\n".join(f" [dim]{pathlib.Path(p)}[/dim]" for p in created)
|
|
884
|
+
console.print(_SPanel(
|
|
885
|
+
f"[green]✅ 项目脚手架已创建[/green] [bold]{_target_name}[/bold]\n\n{lines}\n\n"
|
|
886
|
+
f"[dim]cd \"{target_dir}\" && pip install -r requirements.txt[/dim]",
|
|
887
|
+
title=f"[bold cyan]/init {_tmpl_key}[/bold cyan]",
|
|
888
|
+
border_style="cyan",
|
|
889
|
+
box=_sbox.ROUNDED,
|
|
890
|
+
padding=(1, 2),
|
|
891
|
+
))
|
|
892
|
+
else:
|
|
893
|
+
print(f"✅ 创建: {target_dir}"); [print(f" {p}") for p in created]
|
|
894
|
+
# Optionally generate ARIA.md inside the new project
|
|
895
|
+
try:
|
|
896
|
+
from apps.cli.project_aria import build_project_aria_md
|
|
897
|
+
_aria_tgt = target_dir / "ARIA.md"
|
|
898
|
+
if not _aria_tgt.exists():
|
|
899
|
+
_aria_tgt.write_text(
|
|
900
|
+
build_project_aria_md(
|
|
901
|
+
project_name=_target_name,
|
|
902
|
+
stack=tmpl["desc"],
|
|
903
|
+
entry="main.py",
|
|
904
|
+
purpose=f"由 /init {_tmpl_key} 生成的脚手架项目",
|
|
905
|
+
notes=[
|
|
906
|
+
"脚手架目录已经就绪,后续功能与服务应继续按模块拆分。",
|
|
907
|
+
],
|
|
908
|
+
),
|
|
909
|
+
encoding="utf-8",
|
|
910
|
+
)
|
|
911
|
+
except Exception:
|
|
912
|
+
pass
|
|
913
|
+
return
|
|
914
|
+
# ── /init [--force] — generate ARIA.md for current project ──────────
|
|
915
|
+
|
|
916
|
+
aria_md = cwd / "ARIA.md"
|
|
917
|
+
force = "--force" in args
|
|
918
|
+
|
|
919
|
+
if aria_md.exists() and not force:
|
|
920
|
+
msg = f"ARIA.md already exists. Use /init --force to regenerate."
|
|
921
|
+
console.print(f"[yellow]{msg}[/yellow]") if HAS_RICH else print(msg)
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
# Scan for common project signal files
|
|
925
|
+
_SCAN_FILES = [
|
|
926
|
+
"README.md", "README.rst", "README.txt",
|
|
927
|
+
"package.json", "pyproject.toml", "setup.py", "setup.cfg",
|
|
928
|
+
"requirements.txt", "Pipfile", "poetry.lock",
|
|
929
|
+
"Cargo.toml", "go.mod", "pom.xml", "build.gradle",
|
|
930
|
+
"Makefile", "Dockerfile", ".env.example",
|
|
931
|
+
"CLAUDE.md", ".ariarc",
|
|
932
|
+
]
|
|
933
|
+
snippets, found_files = [], []
|
|
934
|
+
for fname in _SCAN_FILES:
|
|
935
|
+
fp = cwd / fname
|
|
936
|
+
if fp.exists():
|
|
937
|
+
found_files.append(fname)
|
|
938
|
+
try:
|
|
939
|
+
snippets.append(f"### {fname}\n{fp.read_text(errors='replace')[:1200]}")
|
|
940
|
+
except Exception:
|
|
941
|
+
pass
|
|
942
|
+
|
|
943
|
+
code_exts = {".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c"}
|
|
944
|
+
code_files = sorted(
|
|
945
|
+
f.name for f in cwd.iterdir()
|
|
946
|
+
if f.is_file() and f.suffix in code_exts
|
|
947
|
+
)[:10]
|
|
948
|
+
|
|
949
|
+
scan_summary = "\n\n".join(snippets[:5])
|
|
950
|
+
|
|
951
|
+
prompt = (
|
|
952
|
+
f"分析以下项目信息,生成一个 ARIA.md 记忆文件。\n\n"
|
|
953
|
+
f"目录: {cwd}\n"
|
|
954
|
+
f"发现的配置文件: {', '.join(found_files) or '无'}\n"
|
|
955
|
+
f"代码文件: {', '.join(code_files) or '无'}\n\n"
|
|
956
|
+
f"文件内容:\n{scan_summary}\n\n"
|
|
957
|
+
f"请生成符合以下格式的 ARIA.md(只输出文件内容本身,不加任何解释):\n\n"
|
|
958
|
+
f"# Memory\n\n"
|
|
959
|
+
f"- **Project**: <项目名称>\n"
|
|
960
|
+
f"- **Stack**: <语言/框架>\n"
|
|
961
|
+
f"- **Entry**: <主入口文件>\n"
|
|
962
|
+
f"- **Conventions**: <代码规范或约定>\n"
|
|
963
|
+
f"- **Notes**: <其他重要信息>\n"
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
console.print("[dim]分析项目结构中...[/dim]") if HAS_RICH else print("Analyzing project...")
|
|
967
|
+
await self.terminal.send_message(prompt)
|
|
968
|
+
|
|
969
|
+
# Extract the last assistant response and write to ARIA.md
|
|
970
|
+
if self.terminal.conversation:
|
|
971
|
+
last_ai = next(
|
|
972
|
+
(m["content"] for m in reversed(self.terminal.conversation)
|
|
973
|
+
if m.get("role") == "assistant"),
|
|
974
|
+
None,
|
|
975
|
+
)
|
|
976
|
+
if last_ai:
|
|
977
|
+
content = _strip_markdown_fences(last_ai).strip()
|
|
978
|
+
# Strip injected market-data blocks (lines starting with ## 📊 or *⚠️*)
|
|
979
|
+
# that the market-data prefetch may have appended to the AI response.
|
|
980
|
+
import re as _re_init
|
|
981
|
+
content = _re_init.sub(
|
|
982
|
+
r'\n*## 📊.*?(?=\n#|\Z)', '', content, flags=_re_init.DOTALL
|
|
983
|
+
).strip()
|
|
984
|
+
content = _re_init.sub(r'\n*\*⚠️.*?\*\n*', '\n', content).strip()
|
|
985
|
+
if not content.startswith("# Memory"):
|
|
986
|
+
content = "# Memory\n\n" + content
|
|
987
|
+
aria_md.write_text(content + "\n", encoding="utf-8")
|
|
988
|
+
_PROJECT_CONTEXT = _load_project_context()
|
|
989
|
+
msg = f"ARIA.md created at {aria_md}"
|
|
990
|
+
console.print(f"\n[green]{msg}[/green]") if HAS_RICH else print(f"\n{msg}")
|
|
991
|
+
|
|
992
|
+
async def cmd_setup(self, args: str):
|
|
993
|
+
"""Guided first-run setup wizard (Open Interpreter style).
|
|
994
|
+
|
|
995
|
+
Usage: /setup
|
|
996
|
+
"""
|
|
997
|
+
import getpass as _gp
|
|
998
|
+
|
|
999
|
+
_is_interactive = sys.stdin.isatty()
|
|
1000
|
+
|
|
1001
|
+
if HAS_RICH:
|
|
1002
|
+
console.print()
|
|
1003
|
+
console.print("[bold cyan]━━ Aria Setup Wizard ━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]")
|
|
1004
|
+
console.print()
|
|
1005
|
+
|
|
1006
|
+
# ── Step 1: Detect LOCAL backends only (not cloud LLM providers) ───────
|
|
1007
|
+
_LOCAL_BACKENDS_ONLY = {"ollama", "lmstudio", "vllm", "llamacpp", "jan"}
|
|
1008
|
+
try:
|
|
1009
|
+
from local_llm_provider import probe_all_backends, BACKEND_DEFAULTS
|
|
1010
|
+
_all_backends = probe_all_backends()
|
|
1011
|
+
# Filter to only true local backends — cloud providers appear in Step 3
|
|
1012
|
+
backends = {k: v for k, v in _all_backends.items() if k in _LOCAL_BACKENDS_ONLY}
|
|
1013
|
+
except ImportError:
|
|
1014
|
+
backends = {}
|
|
1015
|
+
|
|
1016
|
+
console.print(" [bold]Step 1/4 · 本地 Backend[/bold]") if HAS_RICH else print("Step 1: Local Backends")
|
|
1017
|
+
ollama_online = backends.get("ollama", False)
|
|
1018
|
+
for name, ok in backends.items():
|
|
1019
|
+
icon = "✅" if ok else "○"
|
|
1020
|
+
color = "green" if ok else "dim"
|
|
1021
|
+
url = BACKEND_DEFAULTS.get(name, {}).get("default_url", "") if "BACKEND_DEFAULTS" in dir() else ""
|
|
1022
|
+
if HAS_RICH:
|
|
1023
|
+
console.print(f" {icon} [{color}]{name:12s}[/{color}] [dim]{url}[/dim]")
|
|
1024
|
+
else:
|
|
1025
|
+
print(f" {'✓' if ok else '✗'} {name:12s} {url}")
|
|
1026
|
+
console.print() if HAS_RICH else print()
|
|
1027
|
+
|
|
1028
|
+
# ── Step 2: Pick default Ollama model (if Ollama online) ────────────
|
|
1029
|
+
if ollama_online and _is_interactive:
|
|
1030
|
+
console.print(" [bold]Step 2/4 · 选择默认本地模型[/bold]") if HAS_RICH else print("Step 2: Default model")
|
|
1031
|
+
rich_models, _ = detect_ollama_models_rich(
|
|
1032
|
+
self.terminal.config.get("ollama_url", "http://localhost:11434")
|
|
1033
|
+
)
|
|
1034
|
+
if rich_models:
|
|
1035
|
+
model_names = [m["name"] for m in rich_models]
|
|
1036
|
+
current_id = self.terminal.config.get("model", "")
|
|
1037
|
+
sel_idx = next((i for i, n in enumerate(model_names) if n == current_id), 0)
|
|
1038
|
+
options = [(f" {n}", "") for n in model_names]
|
|
1039
|
+
picked = _arrow_select(options, sel_idx, "选择默认模型")
|
|
1040
|
+
if picked is not None:
|
|
1041
|
+
chosen = model_names[picked]
|
|
1042
|
+
self.terminal.config["model"] = chosen
|
|
1043
|
+
save_config(self.terminal.config)
|
|
1044
|
+
msg = f"✓ 默认模型设为 {chosen}"
|
|
1045
|
+
console.print(f" [green]{msg}[/green]") if HAS_RICH else print(f" {msg}")
|
|
1046
|
+
console.print() if HAS_RICH else print()
|
|
1047
|
+
else:
|
|
1048
|
+
console.print(" [dim]Step 2/4 · (Ollama 未运行,跳过模型选择)[/dim]") if HAS_RICH else print(" Skipping model select (Ollama offline)")
|
|
1049
|
+
console.print() if HAS_RICH else print()
|
|
1050
|
+
|
|
1051
|
+
# ── Step 3: Cloud API keys ───────────────────────────────────────────
|
|
1052
|
+
console.print(" [bold]Step 3/4 · Cloud API Key 配置[/bold]") if HAS_RICH else print("Step 3: Cloud API Keys")
|
|
1053
|
+
_SETUP_PROVIDERS = [
|
|
1054
|
+
("deepseek", "DeepSeek", "推荐:deepseek-chat,性价比最高"),
|
|
1055
|
+
("openai", "OpenAI", "GPT-4o,o1等"),
|
|
1056
|
+
("groq", "Groq", "免费 llama/mixtral 推理,极速"),
|
|
1057
|
+
("anthropic", "Anthropic", "Claude 3.5/3.7"),
|
|
1058
|
+
]
|
|
1059
|
+
for prov, label, desc in _SETUP_PROVIDERS:
|
|
1060
|
+
existing_key = _get_provider_key(prov)
|
|
1061
|
+
if existing_key:
|
|
1062
|
+
masked = existing_key[:6] + "****" + existing_key[-4:]
|
|
1063
|
+
console.print(f" 🔑 {label:12s} [dim]已配置 ({masked})[/dim]") if HAS_RICH else print(f" {label}: 已配置")
|
|
1064
|
+
continue
|
|
1065
|
+
if _is_interactive:
|
|
1066
|
+
console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if HAS_RICH else print(f" {label}: {desc}")
|
|
1067
|
+
try:
|
|
1068
|
+
key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
|
|
1069
|
+
except Exception:
|
|
1070
|
+
key = ""
|
|
1071
|
+
if key:
|
|
1072
|
+
self.cmd_apikey(f"set {prov} {key}")
|
|
1073
|
+
else:
|
|
1074
|
+
console.print(f" ○ {label:12s} [dim]未配置 → /apikey set {prov} <key>[/dim]") if HAS_RICH else print(f" {label}: not configured")
|
|
1075
|
+
console.print() if HAS_RICH else print()
|
|
1076
|
+
|
|
1077
|
+
# ── Step 3.5: Data Service API keys ──────────────────────────────────
|
|
1078
|
+
console.print(" [bold]Step 3.5/4 · 市场数据服务 Key(后端离线时使用)[/bold]") if HAS_RICH else print("Step 3.5: Data Service Keys")
|
|
1079
|
+
_SETUP_DATA = [
|
|
1080
|
+
("finnhub", "Finnhub", "股票实时行情+新闻", "https://finnhub.io/register"),
|
|
1081
|
+
("newsapi", "NewsAPI", "财经新闻聚合", "https://newsapi.org/register"),
|
|
1082
|
+
("brave", "Brave Search", "网页搜索", "https://api.search.brave.com/app/keys"),
|
|
1083
|
+
("alphavantage", "Alpha Vantage", "股票历史数据", "https://www.alphavantage.co/support/#api-key"),
|
|
1084
|
+
]
|
|
1085
|
+
_existing_data = _load_data_keys()
|
|
1086
|
+
for svc, label, desc, signup_url in _SETUP_DATA:
|
|
1087
|
+
existing_key = _existing_data.get(svc, "")
|
|
1088
|
+
if existing_key:
|
|
1089
|
+
masked = existing_key[:6] + "****" + existing_key[-4:]
|
|
1090
|
+
console.print(f" 🔑 {label:16s} [dim]已配置 ({masked})[/dim]") if HAS_RICH else print(f" {label}: configured")
|
|
1091
|
+
continue
|
|
1092
|
+
if _is_interactive:
|
|
1093
|
+
console.print(f" [cyan]{label}[/cyan] [dim]({desc})[/dim]") if HAS_RICH else print(f" {label}: {desc}")
|
|
1094
|
+
console.print(f" [dim]注册:{signup_url}[/dim]") if HAS_RICH else print(f" Register: {signup_url}")
|
|
1095
|
+
try:
|
|
1096
|
+
key = _gp.getpass(f" Enter {label} API key (留空跳过): ").strip()
|
|
1097
|
+
except Exception:
|
|
1098
|
+
key = ""
|
|
1099
|
+
if key:
|
|
1100
|
+
self.cmd_apikey(f"set {svc} {key}")
|
|
1101
|
+
else:
|
|
1102
|
+
if HAS_RICH:
|
|
1103
|
+
console.print(f" ○ {label:16s} [dim]未配置 → /apikey set {svc} <key>[/dim]")
|
|
1104
|
+
console.print(f" [dim]注册:{signup_url}[/dim]")
|
|
1105
|
+
else:
|
|
1106
|
+
print(f" {label}: not configured → /apikey set {svc} <key>")
|
|
1107
|
+
console.print() if HAS_RICH else print()
|
|
1108
|
+
|
|
1109
|
+
# ── Step 3.8: MCP servers ────────────────────────────────────────────
|
|
1110
|
+
sub = args.strip().lower()
|
|
1111
|
+
if sub in ("mcp", "all"):
|
|
1112
|
+
console.print(" [bold]Step 3.8/4 · MCP 服务器[/bold]") if HAS_RICH else print("Step 3.8: MCP Servers")
|
|
1113
|
+
_mcp_cfg_path = Path.home() / ".arthera" / "mcp_servers.json"
|
|
1114
|
+
if _mcp_cfg_path.exists():
|
|
1115
|
+
try:
|
|
1116
|
+
import json as _j2
|
|
1117
|
+
_mcp_data = _j2.loads(_mcp_cfg_path.read_text())
|
|
1118
|
+
_servers = _mcp_data.get("servers", [])
|
|
1119
|
+
enabled_srv = [s for s in _servers if s.get("enabled", False)]
|
|
1120
|
+
disabled_srv = [s for s in _servers if not s.get("enabled", True)]
|
|
1121
|
+
for s in enabled_srv:
|
|
1122
|
+
console.print(f" ✅ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if HAS_RICH else print(f" ✓ {s['name']}")
|
|
1123
|
+
for s in disabled_srv:
|
|
1124
|
+
note = s.get("_setup", "")
|
|
1125
|
+
console.print(f" ○ {s['name']:16s} [dim]{s.get('description','')[:50]}[/dim]") if HAS_RICH else print(f" ✗ {s['name']}")
|
|
1126
|
+
if note:
|
|
1127
|
+
console.print(f" [dim]安装: {note}[/dim]") if HAS_RICH else print(f" Setup: {note}")
|
|
1128
|
+
if disabled_srv:
|
|
1129
|
+
console.print() if HAS_RICH else print()
|
|
1130
|
+
console.print(" [dim]安装后编辑 ~/.arthera/mcp_servers.json 将对应项 enabled 改为 true[/dim]") if HAS_RICH else print(" Edit mcp_servers.json: set enabled=true after installing")
|
|
1131
|
+
except Exception:
|
|
1132
|
+
pass
|
|
1133
|
+
console.print() if HAS_RICH else print()
|
|
1134
|
+
|
|
1135
|
+
# ── Step 4: Messaging channels (Feishu / Telegram) ──────────────────
|
|
1136
|
+
if sub in ("feishu", "telegram", "notify", "all", ""):
|
|
1137
|
+
console.print(" [bold]Step 4/5 · 消息通知连接[/bold]") if HAS_RICH else print("Step 4: Messaging")
|
|
1138
|
+
_env_path = Path.home() / ".aria" / ".env"
|
|
1139
|
+
_env_vars: dict = {}
|
|
1140
|
+
if _env_path.exists():
|
|
1141
|
+
for _line in _env_path.read_text().splitlines():
|
|
1142
|
+
if "=" in _line and not _line.startswith("#"):
|
|
1143
|
+
k, _, v = _line.partition("=")
|
|
1144
|
+
_env_vars[k.strip()] = v.strip()
|
|
1145
|
+
|
|
1146
|
+
# Feishu status
|
|
1147
|
+
_fs_mode = _env_vars.get("ARIA_RELAY_MODE", "")
|
|
1148
|
+
_fs_id = _env_vars.get("ARIA_RELAY_CLIENT_ID", "")
|
|
1149
|
+
_fs_app = _env_vars.get("FEISHU_APP_ID", "")
|
|
1150
|
+
if _fs_mode == "relay" and _fs_id:
|
|
1151
|
+
_fs_status = f"[green]✓ 中继模式[/green] ID: {_fs_id[:12]}…"
|
|
1152
|
+
elif _fs_mode == "own_app" and _fs_app:
|
|
1153
|
+
_fs_status = f"[green]✓ 自建应用[/green] {_fs_app}"
|
|
1154
|
+
else:
|
|
1155
|
+
_fs_status = "[dim]未配置[/dim] → /setup feishu"
|
|
1156
|
+
|
|
1157
|
+
# Telegram status
|
|
1158
|
+
_tg_token = _env_vars.get("TELEGRAM_BOT_TOKEN", "")
|
|
1159
|
+
_tg_ids = _env_vars.get("TELEGRAM_ALLOWED_IDS", "")
|
|
1160
|
+
if _tg_token and _tg_token != "your_bot_token_here":
|
|
1161
|
+
_tg_status = f"[green]✓ 已配置[/green] Chat IDs: {_tg_ids or '(未设置)'}"
|
|
1162
|
+
else:
|
|
1163
|
+
_tg_status = "[dim]未配置[/dim] → /setup telegram"
|
|
1164
|
+
|
|
1165
|
+
if HAS_RICH:
|
|
1166
|
+
console.print(f" 飞书 {_fs_status}")
|
|
1167
|
+
console.print(f" Telegram {_tg_status}")
|
|
1168
|
+
else:
|
|
1169
|
+
print(f" Feishu: {_fs_mode or 'not configured'}")
|
|
1170
|
+
print(f" Telegram: {'configured' if _tg_token else 'not configured'}")
|
|
1171
|
+
|
|
1172
|
+
# Sub-command: launch wizard for just this channel
|
|
1173
|
+
if sub in ("feishu", "telegram"):
|
|
1174
|
+
console.print() if HAS_RICH else print()
|
|
1175
|
+
try:
|
|
1176
|
+
import importlib.util as _ilu
|
|
1177
|
+
_wiz_path = Path(__file__).parent.parent.parent.parent / "setup_wizard.py"
|
|
1178
|
+
_spec = _ilu.spec_from_file_location("_aria_setup_wizard", str(_wiz_path))
|
|
1179
|
+
_wiz = _ilu.module_from_spec(_spec)
|
|
1180
|
+
_spec.loader.exec_module(_wiz)
|
|
1181
|
+
_e = _wiz._load_env()
|
|
1182
|
+
if sub == "feishu":
|
|
1183
|
+
_wiz.setup_feishu(_e)
|
|
1184
|
+
else:
|
|
1185
|
+
_wiz.setup_telegram(_e)
|
|
1186
|
+
_wiz._save_env(_e)
|
|
1187
|
+
except Exception as _we:
|
|
1188
|
+
_fallback_flag = "--feishu" if sub == "feishu" else "--telegram"
|
|
1189
|
+
if HAS_RICH:
|
|
1190
|
+
console.print(f" [yellow]请运行: python3 setup_wizard.py {_fallback_flag}[/yellow]")
|
|
1191
|
+
else:
|
|
1192
|
+
print(f" Run: python3 setup_wizard.py {_fallback_flag}")
|
|
1193
|
+
return
|
|
1194
|
+
|
|
1195
|
+
console.print() if HAS_RICH else print()
|
|
1196
|
+
|
|
1197
|
+
# ── Step 5: Summary ─────────────────────────────────────────────────
|
|
1198
|
+
console.print(" [bold]Step 5/5 · 配置完成[/bold]") if HAS_RICH else print("Step 5: Done")
|
|
1199
|
+
model = self.terminal.config.get("model", "?")
|
|
1200
|
+
provider = self.terminal.config.get("local_provider", "ollama")
|
|
1201
|
+
console.print(f" 模型: [cyan]{model}[/cyan] Provider: [cyan]{provider}[/cyan]") if HAS_RICH else print(f" Model: {model} Provider: {provider}")
|
|
1202
|
+
console.print() if HAS_RICH else print()
|
|
1203
|
+
console.print(
|
|
1204
|
+
" [dim]提示: /model — 切换模型 /providers — 查看所有 provider\n"
|
|
1205
|
+
" /setup feishu — 配置飞书 /setup telegram — 配置 Telegram[/dim]"
|
|
1206
|
+
) if HAS_RICH else print(" Tip: /model /providers /setup feishu /setup telegram")
|
|
1207
|
+
console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold cyan]") if HAS_RICH else print("─" * 50)
|
|
1208
|
+
console.print() if HAS_RICH else print()
|
|
1209
|
+
|
|
1210
|
+
def cmd_memory(self, args: str):
|
|
1211
|
+
"""Manage persistent memory: project ARIA.md and global user profile.
|
|
1212
|
+
|
|
1213
|
+
Usage:
|
|
1214
|
+
/memory show — display current project ARIA.md
|
|
1215
|
+
/memory add <fact> — append fact to project ARIA.md
|
|
1216
|
+
/memory clear — wipe project ARIA.md memory section
|
|
1217
|
+
/memory search <query> — search across ARIA.md + sessions
|
|
1218
|
+
/memory profile — show global ~/.arthera/ARIA.md (injected every session)
|
|
1219
|
+
/memory profile add <text> — append to global profile
|
|
1220
|
+
/memory profile clear — reset global profile
|
|
1221
|
+
/memory global — legacy global Memory entries
|
|
1222
|
+
"""
|
|
1223
|
+
global _PROJECT_CONTEXT
|
|
1224
|
+
aria_md = pathlib.Path.cwd() / "ARIA.md"
|
|
1225
|
+
parts = args.strip().split(maxsplit=1)
|
|
1226
|
+
sub = parts[0].lower() if parts else "show"
|
|
1227
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
1228
|
+
|
|
1229
|
+
if sub == "show":
|
|
1230
|
+
if not aria_md.exists():
|
|
1231
|
+
msg = f"No ARIA.md in {pathlib.Path.cwd()}"
|
|
1232
|
+
console.print(f"[dim]{msg}[/dim]") if HAS_RICH else print(msg)
|
|
1233
|
+
return
|
|
1234
|
+
content = aria_md.read_text(encoding="utf-8")
|
|
1235
|
+
if HAS_RICH:
|
|
1236
|
+
try:
|
|
1237
|
+
from rich.markdown import Markdown as _RMd
|
|
1238
|
+
console.print(_RMd(content))
|
|
1239
|
+
except Exception:
|
|
1240
|
+
console.print(content)
|
|
1241
|
+
else:
|
|
1242
|
+
print(content)
|
|
1243
|
+
|
|
1244
|
+
elif sub == "add":
|
|
1245
|
+
if not rest:
|
|
1246
|
+
console.print("[dim]Usage: /memory add <fact>[/dim]") if HAS_RICH else print("Usage: /memory add <fact>")
|
|
1247
|
+
return
|
|
1248
|
+
self.cmd_note(rest)
|
|
1249
|
+
|
|
1250
|
+
elif sub == "clear":
|
|
1251
|
+
if aria_md.exists():
|
|
1252
|
+
aria_md.write_text("# Memory\n\n", encoding="utf-8")
|
|
1253
|
+
_PROJECT_CONTEXT = _load_project_context()
|
|
1254
|
+
console.print("[dim]Memory cleared.[/dim]") if HAS_RICH else print("Memory cleared.")
|
|
1255
|
+
else:
|
|
1256
|
+
console.print("[dim]Nothing to clear.[/dim]") if HAS_RICH else print("Nothing to clear.")
|
|
1257
|
+
|
|
1258
|
+
elif sub == "search":
|
|
1259
|
+
# Semantic search in ARIA.md and strategy vault using simple grep
|
|
1260
|
+
# (ChromaDB RAG upgrade planned for Phase 2)
|
|
1261
|
+
if not rest:
|
|
1262
|
+
console.print("[dim]Usage: /memory search <query>[/dim]") if HAS_RICH else print("Usage: /memory search <query>")
|
|
1263
|
+
return
|
|
1264
|
+
query_low = rest.lower()
|
|
1265
|
+
results = []
|
|
1266
|
+
# 1. Search ARIA.md
|
|
1267
|
+
if aria_md.exists():
|
|
1268
|
+
for line in aria_md.read_text(encoding="utf-8").splitlines():
|
|
1269
|
+
if query_low in line.lower() and line.strip():
|
|
1270
|
+
results.append(("ARIA.md", line.strip()))
|
|
1271
|
+
# 2. Search session history titles
|
|
1272
|
+
for sess_file in sorted(SESSIONS_DIR.glob("*.json"), key=lambda p: -p.stat().st_mtime)[:20]:
|
|
1273
|
+
try:
|
|
1274
|
+
sess = json.loads(sess_file.read_text(encoding="utf-8"))
|
|
1275
|
+
title = sess.get("metadata", {}).get("title", "")
|
|
1276
|
+
if query_low in title.lower():
|
|
1277
|
+
results.append(("Session", title[:80]))
|
|
1278
|
+
except Exception:
|
|
1279
|
+
pass
|
|
1280
|
+
# 3. Search strategy vault
|
|
1281
|
+
try:
|
|
1282
|
+
from strategy_vault import get_vault as _gv
|
|
1283
|
+
vault = _gv()
|
|
1284
|
+
for s in (vault.list() or []):
|
|
1285
|
+
name = str(s.get("name", ""))
|
|
1286
|
+
msg = str(s.get("message", ""))
|
|
1287
|
+
if query_low in name.lower() or query_low in msg.lower():
|
|
1288
|
+
results.append(("Strategy", f"{name}: {msg[:60]}"))
|
|
1289
|
+
except Exception:
|
|
1290
|
+
pass
|
|
1291
|
+
|
|
1292
|
+
if results:
|
|
1293
|
+
if HAS_RICH:
|
|
1294
|
+
console.print()
|
|
1295
|
+
console.print(f" [bold]记忆搜索: '{rest}'[/bold] [dim]{len(results)} 条结果[/dim]")
|
|
1296
|
+
console.print()
|
|
1297
|
+
for src, text in results[:15]:
|
|
1298
|
+
console.print(f" [dim]{src:<12s}[/dim] {text}")
|
|
1299
|
+
console.print()
|
|
1300
|
+
else:
|
|
1301
|
+
print(f" Search '{rest}': {len(results)} results")
|
|
1302
|
+
for src, text in results[:15]:
|
|
1303
|
+
print(f" [{src}] {text}")
|
|
1304
|
+
else:
|
|
1305
|
+
msg = f"未找到与 '{rest}' 相关的记忆"
|
|
1306
|
+
console.print(f"[dim]{msg}[/dim]") if HAS_RICH else print(msg)
|
|
1307
|
+
|
|
1308
|
+
elif sub == "profile":
|
|
1309
|
+
# Per-user ARIA.md at ~/.arthera/ARIA.md — injected into every session
|
|
1310
|
+
_profile_path = pathlib.Path.home() / ".arthera" / "ARIA.md"
|
|
1311
|
+
gparts = rest.strip().split(maxsplit=1)
|
|
1312
|
+
gsub = gparts[0].lower() if gparts else "show"
|
|
1313
|
+
grest = gparts[1].strip() if len(gparts) > 1 else ""
|
|
1314
|
+
|
|
1315
|
+
if gsub == "show":
|
|
1316
|
+
if not _profile_path.exists():
|
|
1317
|
+
if HAS_RICH:
|
|
1318
|
+
console.print(f"[dim]~/.arthera/ARIA.md 还不存在。用 /memory profile add <内容> 创建。[/dim]")
|
|
1319
|
+
else:
|
|
1320
|
+
print("~/.arthera/ARIA.md not found. Use /memory profile add <text> to create.")
|
|
1321
|
+
return
|
|
1322
|
+
content = _profile_path.read_text(encoding="utf-8")
|
|
1323
|
+
if HAS_RICH:
|
|
1324
|
+
try:
|
|
1325
|
+
from rich.markdown import Markdown as _RMd3
|
|
1326
|
+
console.print()
|
|
1327
|
+
console.print(f" [dim]~/.arthera/ARIA.md[/dim]")
|
|
1328
|
+
console.print(_RMd3(content))
|
|
1329
|
+
except Exception:
|
|
1330
|
+
console.print(content)
|
|
1331
|
+
else:
|
|
1332
|
+
print(content)
|
|
1333
|
+
|
|
1334
|
+
elif gsub == "add":
|
|
1335
|
+
if not grest:
|
|
1336
|
+
console.print("[dim]Usage: /memory profile add <内容>[/dim]") if HAS_RICH else print("Usage: /memory profile add <text>")
|
|
1337
|
+
return
|
|
1338
|
+
_profile_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1339
|
+
now_str = datetime.now().strftime("%Y-%m-%d")
|
|
1340
|
+
if _profile_path.exists():
|
|
1341
|
+
existing = _profile_path.read_text(encoding="utf-8")
|
|
1342
|
+
if "## 偏好与背景" not in existing and "## Preferences" not in existing:
|
|
1343
|
+
existing += "\n\n## 偏好与背景\n"
|
|
1344
|
+
existing += f"\n- [{now_str}] {grest}"
|
|
1345
|
+
_profile_path.write_text(existing, encoding="utf-8")
|
|
1346
|
+
else:
|
|
1347
|
+
_profile_path.write_text(
|
|
1348
|
+
f"# 用户背景\n\n## 偏好与背景\n\n- [{now_str}] {grest}\n",
|
|
1349
|
+
encoding="utf-8",
|
|
1350
|
+
)
|
|
1351
|
+
# Refresh project context so change takes effect immediately
|
|
1352
|
+
_PROJECT_CONTEXT = _load_project_context()
|
|
1353
|
+
if HAS_RICH:
|
|
1354
|
+
console.print(f" [dim]✓ 已写入 ~/.arthera/ARIA.md — 下次对话自动注入[/dim]")
|
|
1355
|
+
else:
|
|
1356
|
+
print(f"Saved to ~/.arthera/ARIA.md")
|
|
1357
|
+
|
|
1358
|
+
elif gsub == "clear":
|
|
1359
|
+
if _profile_path.exists():
|
|
1360
|
+
_profile_path.write_text("# 用户背景\n\n", encoding="utf-8")
|
|
1361
|
+
_PROJECT_CONTEXT = _load_project_context()
|
|
1362
|
+
console.print("[dim]~/.arthera/ARIA.md 已清空。[/dim]") if HAS_RICH else print("Profile cleared.")
|
|
1363
|
+
else:
|
|
1364
|
+
console.print("[dim]文件不存在,无需清空。[/dim]") if HAS_RICH else print("Nothing to clear.")
|
|
1365
|
+
|
|
1366
|
+
else:
|
|
1367
|
+
if HAS_RICH:
|
|
1368
|
+
console.print("[dim]Usage: /memory profile [show|add <内容>|clear][/dim]")
|
|
1369
|
+
else:
|
|
1370
|
+
print("Usage: /memory profile [show|add <text>|clear]")
|
|
1371
|
+
|
|
1372
|
+
elif sub == "global":
|
|
1373
|
+
# Global user memory (cross-project, cross-session)
|
|
1374
|
+
if not self.memory_mgr:
|
|
1375
|
+
console.print("[dim]Memory manager not available.[/dim]") if HAS_RICH else print("Memory manager not available.")
|
|
1376
|
+
return
|
|
1377
|
+
gparts = rest.strip().split(maxsplit=1)
|
|
1378
|
+
gsub = gparts[0].lower() if gparts else "show"
|
|
1379
|
+
grest = gparts[1].strip() if len(gparts) > 1 else ""
|
|
1380
|
+
|
|
1381
|
+
if gsub == "show":
|
|
1382
|
+
entries = self.memory_mgr.list_all()
|
|
1383
|
+
if not entries:
|
|
1384
|
+
console.print("[dim]全局 Memory 为空。用 /memory global add <内容> 添加。[/dim]") if HAS_RICH else print("Global memory is empty.")
|
|
1385
|
+
return
|
|
1386
|
+
if HAS_RICH:
|
|
1387
|
+
from rich.markdown import Markdown as _RMd2
|
|
1388
|
+
for e in entries:
|
|
1389
|
+
console.print(f"\n[bold cyan]{e['title']}[/bold cyan] [dim]{e['file']}[/dim]")
|
|
1390
|
+
console.print(_RMd2(e["content"]) if e["content"] else "[dim](empty)[/dim]")
|
|
1391
|
+
else:
|
|
1392
|
+
for e in entries:
|
|
1393
|
+
print(f"\n## {e['title']}\n{e['content']}")
|
|
1394
|
+
|
|
1395
|
+
elif gsub == "add":
|
|
1396
|
+
if not grest:
|
|
1397
|
+
console.print("[dim]Usage: /memory global add <内容>[/dim]") if HAS_RICH else print("Usage: /memory global add <content>")
|
|
1398
|
+
return
|
|
1399
|
+
self.memory_mgr.append("user_profile", grest, title="User Profile")
|
|
1400
|
+
console.print(f"[dim]已写入全局 Memory: {grest[:60]}[/dim]") if HAS_RICH else print(f"Saved: {grest[:60]}")
|
|
1401
|
+
|
|
1402
|
+
elif gsub == "clear":
|
|
1403
|
+
n = self.memory_mgr.clear_all()
|
|
1404
|
+
console.print(f"[dim]全局 Memory 已清空(删除 {n} 个文件)。[/dim]") if HAS_RICH else print(f"Global memory cleared ({n} files).")
|
|
1405
|
+
|
|
1406
|
+
else:
|
|
1407
|
+
console.print("[dim]Usage: /memory global [show|add <内容>|clear][/dim]") if HAS_RICH else print("Usage: /memory global [show|add|clear]")
|
|
1408
|
+
|
|
1409
|
+
else:
|
|
1410
|
+
if HAS_RICH:
|
|
1411
|
+
console.print("[dim]Usage: /memory [show|add <fact>|clear|search <query>|profile|global][/dim]")
|
|
1412
|
+
console.print("[dim] /memory profile add <内容> — 写入全局用户背景(每次会话自动注入)[/dim]")
|
|
1413
|
+
else:
|
|
1414
|
+
print("Usage: /memory [show|add <fact>|clear|search <query>|profile|global]")
|