flash-sim 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. flash/__init__.py +157 -0
  2. flash/_bootstrap.py +35 -0
  3. flash/_core/__init__.py +39 -0
  4. flash/_core/credentials/__init__.py +44 -0
  5. flash/_core/credentials/_config.py +191 -0
  6. flash/_core/credentials/_core.py +277 -0
  7. flash/_core/credentials/_flash_entries.py +83 -0
  8. flash/_core/credentials/_minimal.py +73 -0
  9. flash/_core/credentials/api_keys.py +225 -0
  10. flash/_core/credentials/deepseek.py +187 -0
  11. flash/_core/credentials/flash_ssh.py +472 -0
  12. flash/_core/credentials/gitee.py +220 -0
  13. flash/_core/credentials/manage.py +359 -0
  14. flash/_core/credentials/pypi.py +144 -0
  15. flash/_core/credentials.py +1103 -0
  16. flash/_core/interface.py +90 -0
  17. flash/_core/registry.py +35 -0
  18. flash/_core/schema.py +118 -0
  19. flash/config/__init__.py +105 -0
  20. flash/flash_run/PROCESS.md +435 -0
  21. flash/flash_run/README.md +205 -0
  22. flash/flash_run/__init__.py +25 -0
  23. flash/flash_run/env/README.md +272 -0
  24. flash/flash_run/env/__init__.py +12 -0
  25. flash/flash_run/env/env_manager.py +535 -0
  26. flash/flash_run/env/resource_config.py +650 -0
  27. flash/flash_run/remote/README.md +313 -0
  28. flash/flash_run/remote/__init__.py +20 -0
  29. flash/flash_run/remote/remote_check.py +70 -0
  30. flash/flash_run/remote/remote_deploy.py +792 -0
  31. flash/flash_run/remote/remote_workflow.py +142 -0
  32. flash/flash_run/remote/route_tester.py +465 -0
  33. flash/flash_run/remote/ssh_check.py +107 -0
  34. flash/input_gen/README.md +535 -0
  35. flash/input_gen/__init__.py +170 -0
  36. flash/input_gen/gen_Grid_markRefineDerefine/GEN_GRID_GUIDE.md +68 -0
  37. flash/input_gen/gen_Grid_markRefineDerefine/README.md +327 -0
  38. flash/input_gen/gen_Grid_markRefineDerefine/__init__.py +0 -0
  39. flash/input_gen/gen_Grid_markRefineDerefine/generator.py +567 -0
  40. flash/input_gen/gen_checker/GEN_CHECKER_GUIDE.md +148 -0
  41. flash/input_gen/gen_checker/__init__.py +6 -0
  42. flash/input_gen/gen_checker/checker.py +274 -0
  43. flash/input_gen/gen_checker/ploter/__init__.py +13 -0
  44. flash/input_gen/gen_checker/ploter/density_plotter.py +195 -0
  45. flash/input_gen/gen_checker/ploter/pulse_plotter.py +158 -0
  46. flash/input_gen/gen_checker/ploter/ray_plotter.py +277 -0
  47. flash/input_gen/gen_config/GEN_CONFIG_GUIDE.md +534 -0
  48. flash/input_gen/gen_config/__init__.py +9 -0
  49. flash/input_gen/gen_config/generator.py +205 -0
  50. flash/input_gen/gen_eos_op/GEN_EOS_OP_GUIDE.md +224 -0
  51. flash/input_gen/gen_eos_op/__init__.py +10 -0
  52. flash/input_gen/gen_eos_op/generator.py +481 -0
  53. flash/input_gen/gen_f90/__init__.py +0 -0
  54. flash/input_gen/gen_makefile/GEN_MAKEFILE_GUIDE.md +56 -0
  55. flash/input_gen/gen_makefile/__init__.py +9 -0
  56. flash/input_gen/gen_makefile/generator.py +32 -0
  57. flash/input_gen/gen_newpara/README.md +400 -0
  58. flash/input_gen/gen_newpara/RP_Reference.md +639 -0
  59. flash/input_gen/gen_newpara/__init__.py +19 -0
  60. flash/input_gen/gen_newpara/generator.py +643 -0
  61. flash/input_gen/gen_otherf90s/GEN_OTHER_F90S_GUIDE.md +231 -0
  62. flash/input_gen/gen_otherf90s/__init__.py +0 -0
  63. flash/input_gen/gen_par/GEN_PAR_GUIDE.md +657 -0
  64. flash/input_gen/gen_par/__init__.py +18 -0
  65. flash/input_gen/gen_par/defaults.py +510 -0
  66. flash/input_gen/gen_par/generator.py +445 -0
  67. flash/input_gen/gen_par/materials.py +141 -0
  68. flash/input_gen/gen_shell_script/GEN_SHELL_SCRIPT_GUIDE.md +120 -0
  69. flash/input_gen/gen_shell_script/__init__.py +12 -0
  70. flash/input_gen/gen_shell_script/generator.py +850 -0
  71. flash/input_gen/gen_sim_data/GEN_SIM_DATA_GUIDE.md +306 -0
  72. flash/input_gen/gen_sim_data/__init__.py +10 -0
  73. flash/input_gen/gen_sim_data/generator.py +87 -0
  74. flash/input_gen/gen_sim_init/GEN_SIM_INIT_GUIDE.md +216 -0
  75. flash/input_gen/gen_sim_init/__init__.py +9 -0
  76. flash/input_gen/gen_sim_init/generator.py +104 -0
  77. flash/input_gen/gen_sim_initblock/GEN_SIM_INITBLOCK_GUIDE.md +330 -0
  78. flash/input_gen/gen_sim_initblock/__init__.py +17 -0
  79. flash/input_gen/gen_sim_initblock/generator.py +432 -0
  80. flash/input_gen/gen_sim_initblock/grid.py +278 -0
  81. flash/input_gen/gen_sim_initblock/visualizer.py +250 -0
  82. flash/interface.py +398 -0
  83. flash/output_processors/__init__.py +50 -0
  84. flash/output_processors/conftest.py +41 -0
  85. flash/output_processors/docs/api_reference.md +597 -0
  86. flash/output_processors/docs/how_to_add_custom_variables.md +417 -0
  87. flash/output_processors/docs/output_processors_usage.md +596 -0
  88. flash/output_processors/docs/performance_tuning.md +365 -0
  89. flash/output_processors/docs/workflow.md +101 -0
  90. flash/output_processors/hdf5processor/__init__.py +21 -0
  91. flash/output_processors/hdf5processor/flash_hdf5.py +1224 -0
  92. flash/output_processors/loader/__init__.py +7 -0
  93. flash/output_processors/loader/data_loader.py +392 -0
  94. flash/output_processors/parallel.py +580 -0
  95. flash/output_processors/plotter/__init__.py +8 -0
  96. flash/output_processors/plotter/plot_generator.py +471 -0
  97. flash/output_processors/plotter/plot_style.py +240 -0
  98. flash/physics/ele_nonlocal/Knudsen_Nonlocal_Summary.md +576 -0
  99. flash/projects/__init__.py +0 -0
  100. flash/projects/projects_demo/README.md +94 -0
  101. flash/projects/projects_demo/__init__.py +17 -0
  102. flash/projects/projects_demo/ch_center_demo/__init__.py +16 -0
  103. flash/projects/projects_demo/ch_center_demo/plot_compare.py +169 -0
  104. flash/projects/projects_demo/ch_center_demo/run_compare.py +148 -0
  105. flash/scenarios/README.md +669 -0
  106. flash/scenarios/SNBtest/__init__.py +0 -0
  107. flash/scenarios/__init__.py +18 -0
  108. flash/scenarios/base.py +50 -0
  109. flash/scenarios/center_evolution/__init__.py +2 -0
  110. flash/scenarios/center_evolution/ch_center/__init__.py +361 -0
  111. flash/scenarios/center_evolution/ch_center/laserslab1d_local_custom.py +804 -0
  112. flash/scenarios/collision_compression/__init__.py +38 -0
  113. flash/scenarios/flash_demo/__init__.py +0 -0
  114. flash/scenarios/flash_demo/demo_hpc/__init__.py +0 -0
  115. flash/scenarios/flash_demo/demo_hpc/_plot_utils.py +128 -0
  116. flash/scenarios/flash_demo/demo_hpc/laserslab1d_hpc_demo_batch.py +1035 -0
  117. flash/scenarios/flash_demo/demo_hpc/laserslab1d_supercomputer_demo.py +778 -0
  118. flash/scenarios/flash_demo/demo_hpc/remote_analysis.py +428 -0
  119. flash/scenarios/flash_demo/demo_hpc/remote_plot_script.py +444 -0
  120. flash/scenarios/flash_demo/demo_hpc/remote_ssh_helper.py +457 -0
  121. flash/scenarios/flash_demo/demo_local/__init__.py +0 -0
  122. flash/scenarios/flash_demo/demo_local/laserslab1d_local_demo.py +343 -0
  123. flash/scenarios/flash_demo/hello_flash/README.md +447 -0
  124. flash/scenarios/flash_demo/hello_flash/analyze_density.py +416 -0
  125. flash/scenarios/flash_demo/hello_flash/deploy_flash.py +828 -0
  126. flash/scenarios/interpolator.py +286 -0
  127. flash/scenarios/plasma_preparation/__init__.py +1 -0
  128. flash/scenarios/registry.py +61 -0
  129. flash/scenarios/simulator.py +1045 -0
  130. flash/test/__init__.py +1 -0
  131. flash/test/flash_mathtest_custom01.py +413 -0
  132. flash/test/math_test.py +596 -0
  133. flash/utils/__init__.py +9 -0
  134. flash/utils/check_hdf5_time.py +22 -0
  135. flash/utils/convert_pdf_to_md.py +134 -0
  136. flash/utils/convert_text_to_md.py +102 -0
  137. flash/utils/extract_knowledge.py +200 -0
  138. flash_sim-0.0.1.dist-info/METADATA +811 -0
  139. flash_sim-0.0.1.dist-info/RECORD +143 -0
  140. flash_sim-0.0.1.dist-info/WHEEL +4 -0
  141. flash_sim-0.0.1.dist-info/entry_points.txt +3 -0
  142. flash_sim-0.0.1.dist-info/licenses/LICENSE +386 -0
  143. flash_sim-0.0.1.dist-info/licenses/NOTICE +138 -0
@@ -0,0 +1,187 @@
1
+ """
2
+ Flash 凭据管理 -- DeepSeek API 管理
3
+ ==========================================
4
+ 管理 DeepSeek API 密钥 (太极网关)。
5
+
6
+ 用法:
7
+ python -m flash._core.credentials.deepseek # 交互菜单
8
+ python -m flash._core.credentials.deepseek setup # 设置 DeepSeek API
9
+ python -m flash._core.credentials.deepseek show # 查看 DeepSeek API
10
+ """
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ # ── 直接运行时的自动转换 ────────────────────────────
16
+ if __name__ == "__main__" and __package__ is None:
17
+ _CRED_DIR = Path(__file__).resolve().parent
18
+ _CORE_DIR = _CRED_DIR.parent
19
+ _FLASH_DIR = _CORE_DIR.parent
20
+ _PARENT = _FLASH_DIR.parent
21
+
22
+ # standalone 模式: 将 flash/ 的父目录加入 sys.path
23
+ if str(_PARENT) not in sys.path:
24
+ sys.path.insert(0, str(_PARENT))
25
+
26
+ import runpy
27
+ sys.argv.insert(0, __file__)
28
+ runpy.run_module("flash._core.credentials.deepseek", run_name="__main__")
29
+ sys.exit(0)
30
+
31
+
32
+ # ── 导入 (模块方式, 统一使用相对导入) ─────────────────────────────
33
+ from ._core import (
34
+ ask_one,
35
+ get_credential_manager,
36
+ mask_secret,
37
+ )
38
+
39
+
40
+ # ── 设置 DeepSeek API 凭据 ─────────────────────────────────────
41
+
42
+ def setup_deepseek(cm, silent: bool = False) -> None:
43
+ """设置/修改 DeepSeek API 凭据。"""
44
+ data = cm.get("deepseek_api") or {}
45
+
46
+ current_url = data.get("base_url", "")
47
+ current_key = data.get("api_key", "")
48
+
49
+ if not silent:
50
+ print("\n DeepSeek API (太极网关)")
51
+ print(" (直接回车使用默认值)\n")
52
+
53
+ # API 地址
54
+ default_url = "https://gateway.taichuai.cn/modelhub/api/v1"
55
+ if current_url:
56
+ default_url = current_url
57
+
58
+ if not silent:
59
+ url = ask_one(" API 地址", default_url)
60
+ else:
61
+ url = default_url
62
+
63
+ # API Key
64
+ default_key = "(隐藏)"
65
+ if current_key:
66
+ default_key = "(已保存)"
67
+
68
+ if not silent:
69
+ key = ask_one(" API Key", default_key)
70
+ if key == "(隐藏)" or key == "(已保存)":
71
+ key = current_key
72
+ else:
73
+ # 用户输入了新 key
74
+ pass
75
+
76
+ if not key or key == "(隐藏)" or key == "(已保存)":
77
+ key = current_key
78
+
79
+ if not key:
80
+ print("\n [错误] API Key 不能为空。")
81
+ return
82
+
83
+ data = {"base_url": url, "api_key": key}
84
+ cm.set("deepseek_api", data)
85
+
86
+ if not silent:
87
+ print(f"\n ✅ DeepSeek API 凭据已保存。")
88
+ print(f" API 地址: {url}")
89
+ print(f" API Key: {mask_secret(key)}")
90
+
91
+
92
+ # ── 查看 DeepSeek API 凭据 ─────────────────────────────────────
93
+
94
+ def show_deepseek(cm) -> None:
95
+ """查看 DeepSeek API 凭据。"""
96
+ data = cm.get("deepseek_api")
97
+
98
+ print("\n" + "=" * 50)
99
+ print(" DeepSeek API 凭据")
100
+ print("=" * 50)
101
+
102
+ if not data:
103
+ print("\n [提示] 尚未设置 DeepSeek API 凭据。\n")
104
+ return
105
+
106
+ print(f"\n API 地址: {data.get('base_url', '(未设置)')}")
107
+ key = data.get("api_key", "")
108
+ if key:
109
+ print(f" API Key: {mask_secret(key)}")
110
+ else:
111
+ print(f" API Key: (未设置)")
112
+
113
+ print()
114
+
115
+
116
+ # ── 删除 DeepSeek API 凭据 ─────────────────────────────────────
117
+
118
+ def delete_deepseek(cm) -> None:
119
+ """删除 DeepSeek API 凭据。"""
120
+ data = cm.get("deepseek_api")
121
+ if not data:
122
+ print("\n [提示] 尚未设置 DeepSeek API 凭据。")
123
+ return
124
+
125
+ show_deepseek(cm)
126
+
127
+ try:
128
+ confirm = input("\n 确认删除 DeepSeek API 凭据? [y/N]: ").strip().lower()
129
+ except EOFError:
130
+ confirm = "n"
131
+
132
+ if confirm == "y":
133
+ cm.delete("deepseek_api")
134
+ print("\n ✅ DeepSeek API 凭据已删除。")
135
+ else:
136
+ print("\n [提示] 已取消。")
137
+
138
+
139
+ # ── 交互菜单 ─────────────────────────────────────────
140
+
141
+ def interactive_menu(cm) -> None:
142
+ """DeepSeek API 管理交互菜单。"""
143
+ while True:
144
+ print("\n" + "=" * 50)
145
+ print(" DeepSeek API 管理")
146
+ print("=" * 50)
147
+ print(" [1] 设置/修改 DeepSeek API")
148
+ print(" [2] 查看 DeepSeek API")
149
+ print(" [3] 删除 DeepSeek API")
150
+ print(" [0] 返回")
151
+ print("=" * 50)
152
+
153
+ try:
154
+ choice = input("\n 请选择 [0-3]: ").strip()
155
+ except EOFError:
156
+ break
157
+
158
+ if choice == "1":
159
+ setup_deepseek(cm)
160
+ elif choice == "2":
161
+ show_deepseek(cm)
162
+ elif choice == "3":
163
+ delete_deepseek(cm)
164
+ elif choice == "0":
165
+ break
166
+ else:
167
+ print("\n [错误] 无效选择。")
168
+
169
+
170
+ # ── 直接运行 ─────────────────────────────────────
171
+
172
+ if __name__ == "__main__":
173
+ cm = get_credential_manager()
174
+
175
+ if len(__import__("sys").argv) > 1:
176
+ arg = __import__("sys").argv[1].lower()
177
+ if arg in {"setup", "set"}:
178
+ setup_deepseek(cm)
179
+ elif arg in {"show", "view"}:
180
+ show_deepseek(cm)
181
+ elif arg in {"delete", "del", "remove"}:
182
+ delete_deepseek(cm)
183
+ else:
184
+ print(f"\n [错误] 未知参数: {arg}")
185
+ print(" 支持: setup, show, delete")
186
+ else:
187
+ interactive_menu(cm)
@@ -0,0 +1,472 @@
1
+ """
2
+ Flash 凭据管理 -- FLASH SSH 账户管理 (多账户+多线路版)
3
+ ======================================================
4
+ 支持对多个超算 SSH 账户的增/删/改/查,以及主账户管理。
5
+ 支持多线路测试与选择(自动选择最快线路)。
6
+
7
+ 用法:
8
+ python -m flash._core.credentials.flash_ssh # 交互菜单
9
+ python -m flash._core.credentials.flash_ssh add # 添加新超算账户
10
+ python -m flash._core.credentials.flash_ssh test # 测试所有线路
11
+ """
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ # ── 直接运行时的自动转换 ─────────────────────────────
17
+ # 如果 __package__ 为 None,说明是直接运行 (python flash_ssh.py),
18
+ # 此时用 runpy 以模块方式重新运行,使相对导入能正常工作。
19
+ if __name__ == "__main__" and __package__ is None:
20
+ _CRED_DIR = Path(__file__).resolve().parent
21
+ _CORE_DIR = _CRED_DIR.parent
22
+ _FLASH_DIR = _CORE_DIR.parent
23
+ _PARENT = _FLASH_DIR.parent
24
+
25
+ # standalone 模式: 将 flash/ 的父目录加入 sys.path
26
+ if str(_PARENT) not in sys.path:
27
+ sys.path.insert(0, str(_PARENT))
28
+
29
+ import runpy
30
+ sys.argv.insert(0, __file__)
31
+ runpy.run_module("flash._core.credentials.flash_ssh", run_name="__main__")
32
+ sys.exit(0)
33
+
34
+
35
+ import socket
36
+ import time
37
+ from typing import Optional, List, Dict, Any
38
+
39
+ # ── 导入 (模块方式, 统一使用相对导入) ─────────────────────────────
40
+ from ._core import (
41
+ ask_one,
42
+ collect_ssh_accounts,
43
+ get_credential_manager,
44
+ get_primary_ssh,
45
+ get_user_name,
46
+ get_default_password,
47
+ mask_secret,
48
+ next_ssh_number,
49
+ set_primary_ssh,
50
+ ssh_account_name,
51
+ )
52
+ from ._config import PRECONFIGURED_SSH, get_ssh_username, get_ssh_routes
53
+
54
+
55
+ # ── 线路测试 ──────────────────────────────────────────
56
+
57
+ def test_route(host: str, port: int, timeout: float = 5.0) -> Optional[float]:
58
+ """
59
+ 测试一条 SSH 线路的连通性和延迟。
60
+
61
+ 参数:
62
+ host: SSH 主机名
63
+ port: SSH 端口
64
+ timeout: 超时时间(秒)
65
+
66
+ 返回:
67
+ 延迟(秒),如果连接失败则返回 None
68
+ """
69
+ try:
70
+ start = time.time()
71
+ sock = socket.create_connection((host, port), timeout=timeout)
72
+ latency = time.time() - start
73
+ sock.close()
74
+ return latency
75
+ except (socket.timeout, socket.error, OSError):
76
+ return None
77
+
78
+
79
+ def test_all_routes(routes: List[Dict[str, Any]], timeout: float = 5.0) -> List[Dict[str, Any]]:
80
+ """
81
+ 测试所有线路,返回测试结果列表。
82
+
83
+ 返回:
84
+ [{"index": 0, "host": ..., "port": ..., "label": ..., "latency": 0.123, "ok": True}, ...]
85
+ """
86
+ results = []
87
+ for i, route in enumerate(routes):
88
+ host = route["host"]
89
+ port = route["port"]
90
+ label = route.get("label", f"{host}:{port}")
91
+ latency = test_route(host, port, timeout)
92
+ results.append({
93
+ "index": i,
94
+ "host": host,
95
+ "port": port,
96
+ "label": label,
97
+ "latency": latency,
98
+ "ok": latency is not None,
99
+ })
100
+ return results
101
+
102
+
103
+ def select_best_route(routes: List[Dict[str, Any]], timeout: float = 5.0) -> int:
104
+ """
105
+ 测试所有线路,自动选择延迟最低的线路。
106
+
107
+ 返回:
108
+ 最佳线路的索引(如果没有可用线路,返回 0)
109
+ """
110
+ results = test_all_routes(routes, timeout)
111
+ ok_results = [r for r in results if r["ok"]]
112
+ if not ok_results:
113
+ return 0
114
+ best = min(ok_results, key=lambda r: r["latency"])
115
+ return best["index"]
116
+
117
+
118
+ def show_route_test_results(results: List[Dict[str, Any]]) -> None:
119
+ """显示线路测试结果。"""
120
+ print("\n ─── 线路测试结果 ──────────────────────────────────")
121
+ for r in results:
122
+ status = f"✅ {r['latency']*1000:.1f} ms" if r["ok"] else "❌ 超时"
123
+ print(f" [{r['index']+1}] {r['label']:30s} {status}")
124
+ print(" ─────────────────────────────────────────────────────")
125
+
126
+
127
+ # ── SSH 账户操作 ──────────────────────────────────────────
128
+
129
+ def _get_ssh_username(name: str) -> str:
130
+ """获取 SSH 用户名(从配置中读取)。"""
131
+ return get_ssh_username(name)
132
+
133
+
134
+ def _get_routes(name: str) -> List[Dict[str, Any]]:
135
+ """获取账户的线路列表(从配置中读取)。"""
136
+ return get_ssh_routes(name)
137
+
138
+
139
+ def add_account(cm, number: int = None, interactive: bool = False) -> None:
140
+ """添加/修改 SSH 账户。"""
141
+ if number is None:
142
+ number = next_ssh_number(cm)
143
+ name = ssh_account_name(number)
144
+
145
+ # 尝试从预配置获取
146
+ ssh_username = get_ssh_username(name)
147
+ routes = get_ssh_routes(name)
148
+
149
+ # 如果是手动添加(不在预配置中),询问用户名
150
+ if ssh_username == name:
151
+ print(f"\n --- {'添加新' if not cm.get(name) else '修改'} SSH 账户: {name} ---")
152
+ ssh_username = ask_one(" SSH 用户名", name)
153
+ routes = []
154
+ else:
155
+ print(f"\n --- {'添加新' if not cm.get(name) else '修改'} SSH 账户: {name} ---")
156
+ print(f" SSH 用户名: {ssh_username}")
157
+
158
+ # 设置密码
159
+ default_pwd = get_default_password()
160
+ password = ask_one(" 密码", default_pwd)
161
+ data = {
162
+ "ssh_username": ssh_username,
163
+ "password": password,
164
+ "active_route": 0,
165
+ }
166
+ cm.set(name, data)
167
+
168
+ # 测试线路
169
+ if routes:
170
+ print(f"\n 📡 测试所有线路...")
171
+ results = test_all_routes(routes)
172
+ show_route_test_results(results)
173
+
174
+ # 自动选择最快线路
175
+ ok_results = [r for r in results if r["ok"]]
176
+ if ok_results:
177
+ best = min(ok_results, key=lambda r: r["latency"])
178
+ data["active_route"] = best["index"]
179
+ cm.set(name, data)
180
+ print(f" ✅ 自动选择线路: [{best['index']+1}] {best['label']} ({best['latency']*1000:.1f} ms)")
181
+ else:
182
+ print("\n [警告] 所有线路均不可达,请检查网络。")
183
+
184
+ print(f"\n ✅ 已保存账户: {name}")
185
+
186
+ # 如果是第一个账户,设为主账户
187
+ if number == 1 or not get_primary_ssh(cm):
188
+ set_primary_ssh(cm, name)
189
+ print(f" ✅ 已设为主账户: {name}")
190
+
191
+
192
+ def delete_account(cm) -> None:
193
+ """删除 SSH 账户。"""
194
+ accounts = collect_ssh_accounts(cm)
195
+ name = _pick_account(cm, accounts)
196
+ if not name:
197
+ return
198
+ primary = get_primary_ssh(cm)
199
+ if name == primary:
200
+ print("\n [警告] 正在删除主账户!")
201
+ confirm = input(" 确认删除? [y/N]: ").strip().lower()
202
+ if confirm != "y":
203
+ print("\n 已取消。")
204
+ return
205
+
206
+ cm.delete(name)
207
+ print(f"\n ✅ 已删除账户: {name}")
208
+
209
+ # 如果删除了主账户,重新设置
210
+ if name == primary and collect_ssh_accounts(cm):
211
+ new_primary = collect_ssh_accounts(cm)[0]
212
+ set_primary_ssh(cm, new_primary)
213
+ print(f" ✅ 新主账户: {new_primary}")
214
+
215
+
216
+ def show_account(cm) -> None:
217
+ """查看 SSH 账户详情。"""
218
+ accounts = collect_ssh_accounts(cm)
219
+ name = _pick_account(cm, accounts)
220
+ if not name:
221
+ return
222
+ data = cm.get(name)
223
+ if not data:
224
+ print(f"\n [错误] 账户不存在: {name}")
225
+ return
226
+
227
+ primary = get_primary_ssh(cm)
228
+ routes = _get_routes(name)
229
+ active = data.get("active_route", 0)
230
+
231
+ print(f"\n --- SSH 账户: {name}{' [主]' if name == primary else ''} ---")
232
+ print(f" ssh_username: {data.get('ssh_username', '(未设置)')}")
233
+ print(f" password: {'*' * 8}")
234
+ if routes:
235
+ print(f" 可用线路: {len(routes)} 条")
236
+ print(f" 当前线路: [{active+1}] {routes[active].get('label', routes[active]['host'])}")
237
+
238
+
239
+ def test_routes(cm) -> None:
240
+ """测试指定账户的所有线路。"""
241
+ accounts = collect_ssh_accounts(cm)
242
+ name = _pick_account(cm, accounts)
243
+ if not name:
244
+ return
245
+
246
+ routes = _get_routes(name)
247
+ if not routes:
248
+ print("\n [提示] 该账户没有配置线路。")
249
+ return
250
+
251
+ print(f"\n 📡 测试账户 {name} 的所有线路...")
252
+ results = test_all_routes(routes)
253
+ show_route_test_results(results)
254
+
255
+ # 询问是否选择最佳线路
256
+ ok_results = [r for r in results if r["ok"]]
257
+ if ok_results:
258
+ best = min(ok_results, key=lambda r: r["latency"])
259
+ choice = input(f"\n 选择最佳线路 [{best['index']+1}]? [Y/n]: ").strip().lower()
260
+ if choice != "n":
261
+ data = cm.get(name) or {}
262
+ data["active_route"] = best["index"]
263
+ cm.set(name, data)
264
+ print(f" ✅ 已选择线路: [{best['index']+1}] {best['label']}")
265
+
266
+
267
+ def select_route(cm) -> None:
268
+ """手动选择线路。"""
269
+ accounts = collect_ssh_accounts(cm)
270
+ name = _pick_account(cm, accounts)
271
+ if not name:
272
+ return
273
+
274
+ routes = _get_routes(name)
275
+ if not routes:
276
+ print("\n [提示] 该账户没有配置线路。")
277
+ return
278
+
279
+ print(f"\n --- 选择 {name} 的线路 ---")
280
+ for i, route in enumerate(routes):
281
+ label = route.get("label", f"{route['host']}:{route['port']}")
282
+ print(f" [{i+1}] {label}")
283
+
284
+ try:
285
+ idx = int(input(f"\n 请选择 [1-{len(routes)}, 0=自动测试]: ").strip()) - 1
286
+ if idx == -1:
287
+ # 自动测试
288
+ best_idx = select_best_route(routes)
289
+ data = cm.get(name) or {}
290
+ data["active_route"] = best_idx
291
+ cm.set(name, data)
292
+ print(f" ✅ 已自动选择: [{best_idx+1}] {routes[best_idx].get('label', '')}")
293
+ elif 0 <= idx < len(routes):
294
+ data = cm.get(name) or {}
295
+ data["active_route"] = idx
296
+ cm.set(name, data)
297
+ print(f" ✅ 已选择线路: [{idx+1}] {routes[idx].get('label', '')}")
298
+ else:
299
+ print("\n [错误] 无效选择。")
300
+ except ValueError:
301
+ print("\n [错误] 无效输入。")
302
+
303
+
304
+ def _pick_account(cm, accounts=None) -> Optional[str]:
305
+ """让用户选择一个 SSH 账户,返回账户名。"""
306
+ if accounts is None:
307
+ accounts = collect_ssh_accounts(cm)
308
+ if not accounts:
309
+ print("\n [提示] 尚未添加任何 SSH 账户。")
310
+ return None
311
+ if len(accounts) == 1:
312
+ return accounts[0]
313
+ print("\n 请选择 SSH 账户:")
314
+ for i, n in enumerate(accounts):
315
+ primary = get_primary_ssh(cm)
316
+ tag = " [主]" if n == primary else ""
317
+ print(f" [{i+1}] {n}{tag}")
318
+ try:
319
+ idx = int(input("\n 请选择 [1-{}]: ".format(len(accounts)))) - 1
320
+ if 0 <= idx < len(accounts):
321
+ return accounts[idx]
322
+ except ValueError:
323
+ pass
324
+ print("\n [错误] 无效选择。")
325
+ return None
326
+
327
+
328
+ def _add_preconfigured(cm, idx: int) -> None:
329
+ """添加预配置 SSH 账户(只需输入密码)。"""
330
+ if idx < 0 or idx >= len(PRECONFIGURED_SSH):
331
+ print("\n [错误] 无效选择。")
332
+ return
333
+
334
+ pre = PRECONFIGURED_SSH[idx]
335
+ name = pre["name"]
336
+ title = pre["title"]
337
+ ssh_username = pre["ssh_username"]
338
+ routes = pre["routes"]
339
+
340
+ print(f"\n ─── 添加预配置账户: {title} ──────────────────")
341
+ print(f" SSH 用户名: {ssh_username}")
342
+ print(f" 可用线路: {len(routes)} 条")
343
+ print()
344
+
345
+ default_pwd = get_default_password()
346
+ password = ask_one(" 密码", default_pwd)
347
+
348
+ data = {
349
+ "ssh_username": ssh_username,
350
+ "password": password,
351
+ "active_route": 0,
352
+ }
353
+
354
+ cm.set(name, data)
355
+ print(f"\n ✅ 已添加账户: {name}")
356
+ print(f" SSH 用户名: {ssh_username}")
357
+ print(f" 密码: {mask_secret(password)}")
358
+
359
+ # 测试线路
360
+ print(f"\n 📡 测试所有线路...")
361
+ results = test_all_routes(routes)
362
+ show_route_test_results(results)
363
+
364
+ ok_results = [r for r in results if r["ok"]]
365
+ if ok_results:
366
+ best = min(ok_results, key=lambda r: r["latency"])
367
+ data["active_route"] = best["index"]
368
+ cm.set(name, data)
369
+ print(f" ✅ 自动选择线路: [{best['index']+1}] {best['label']} ({best['latency']*1000:.1f} ms)")
370
+ else:
371
+ print(f"\n [警告] 所有线路均不可达,请检查网络。")
372
+
373
+
374
+ def interactive_menu(cm) -> None:
375
+ """交互菜单。"""
376
+ # 确保预配置账户已加载
377
+ try:
378
+ from ._config import autodiscover_configs
379
+ autodiscover_configs()
380
+ except (ImportError, ValueError):
381
+ pass
382
+
383
+ while True:
384
+ accounts = collect_ssh_accounts(cm)
385
+ primary = get_primary_ssh(cm)
386
+ print("\n" + "=" * 50)
387
+ print(" FLASH SSH 账户管理")
388
+ if accounts:
389
+ print(f" 主账户: {primary}")
390
+ print(f" 全部账户: {', '.join(accounts)}")
391
+ else:
392
+ print(" 当前未添加任何 SSH 账户")
393
+ if PRECONFIGURED_SSH:
394
+ print(f" (预配置账户: {', '.join([p['title'] for p in PRECONFIGURED_SSH])})")
395
+ print("=" * 50)
396
+
397
+ if not accounts and PRECONFIGURED_SSH:
398
+ # 显示预配置账户选项
399
+ print("\n 请选择:")
400
+ print(" [1] 添加预配置账户 (只需输入密码)")
401
+ print(" [2] 手动添加新账户")
402
+ print(" [0] 返回")
403
+ print("=" * 50)
404
+ try:
405
+ choice = input("\n 请选择 [0-2]: ").strip()
406
+ except EOFError:
407
+ break
408
+
409
+ if choice == "1":
410
+ print(f"\n 请选择预配置账户:")
411
+ for i, p in enumerate(PRECONFIGURED_SSH):
412
+ print(f" [{i+1}] {p['title']}")
413
+ try:
414
+ idx = int(input(f"\n 请选择 [1-{len(PRECONFIGURED_SSH)}]: ").strip()) - 1
415
+ _add_preconfigured(cm, idx)
416
+ except ValueError:
417
+ print("\n [错误] 无效选择。")
418
+ elif choice == "2":
419
+ add_account(cm)
420
+ elif choice == "0":
421
+ break
422
+ else:
423
+ print("\n [错误] 无效选择。")
424
+ else:
425
+ # 正常菜单
426
+ print(" [1] 添加/修改 SSH 账户")
427
+ print(" [2] 删除 SSH 账户")
428
+ print(" [3] 查看 SSH 账户详情")
429
+ print(" [4] 测试所有线路")
430
+ print(" [5] 手动选择线路")
431
+ print(" [6] 设置主 SSH 账户")
432
+ print(" [0] 返回")
433
+ print("=" * 50)
434
+ try:
435
+ choice = input("\n 请选择 [0-6]: ").strip()
436
+ except EOFError:
437
+ break
438
+
439
+ if choice == "1":
440
+ add_account(cm)
441
+ elif choice == "2":
442
+ delete_account(cm)
443
+ elif choice == "3":
444
+ show_account(cm)
445
+ elif choice == "4":
446
+ test_routes(cm)
447
+ elif choice == "5":
448
+ select_route(cm)
449
+ elif choice == "6":
450
+ accounts = collect_ssh_accounts(cm)
451
+ if not accounts:
452
+ print("\n [提示] 尚未添加任何 SSH 账户。")
453
+ continue
454
+ print("\n 请选择新的主账户:")
455
+ for i, n in enumerate(accounts):
456
+ print(f" [{i+1}] {n}")
457
+ try:
458
+ idx = int(input("\n 请选择 [1-{}]: ".format(len(accounts)))) - 1
459
+ if 0 <= idx < len(accounts):
460
+ set_primary_ssh(cm, accounts[idx])
461
+ print(f"\n ✅ 已设为主账户: {accounts[idx]}")
462
+ except ValueError:
463
+ print("\n [错误] 无效选择。")
464
+ elif choice == "0":
465
+ break
466
+ else:
467
+ print("\n [错误] 无效选择。")
468
+
469
+
470
+ if __name__ == "__main__":
471
+ cm = get_credential_manager()
472
+ interactive_menu(cm)