precis-cli 0.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.
Files changed (442) hide show
  1. app/__init__.py +16 -0
  2. app/api/__init__.py +32 -0
  3. app/api/dependencies.py +185 -0
  4. app/api/main.py +301 -0
  5. app/api/middleware/__init__.py +34 -0
  6. app/api/middleware/exception_handler.py +126 -0
  7. app/api/middleware/request_logging.py +97 -0
  8. app/api/middleware/token_auth.py +215 -0
  9. app/api/models/__init__.py +151 -0
  10. app/api/models/connection_rules.py +98 -0
  11. app/api/models/files.py +99 -0
  12. app/api/models/full_validation.py +554 -0
  13. app/api/models/project.py +117 -0
  14. app/api/models/projects.py +80 -0
  15. app/api/models/schema.py +121 -0
  16. app/api/models/v2_responses.py +46 -0
  17. app/api/models/validation.py +303 -0
  18. app/api/models/workspace.py +118 -0
  19. app/api/routers/__init__.py +56 -0
  20. app/api/routers/ai/__init__.py +36 -0
  21. app/api/routers/ai/chat.py +243 -0
  22. app/api/routers/ai/generate.py +156 -0
  23. app/api/routers/ai/hardware.py +199 -0
  24. app/api/routers/ai/jobs.py +544 -0
  25. app/api/routers/ai/migrate.py +471 -0
  26. app/api/routers/ai/models.py +352 -0
  27. app/api/routers/ai/ollama.py +108 -0
  28. app/api/routers/ai/providers.py +491 -0
  29. app/api/routers/ai/router.py +44 -0
  30. app/api/routers/ai/stream.py +281 -0
  31. app/api/routers/ai/utils.py +182 -0
  32. app/api/routers/core/__init__.py +34 -0
  33. app/api/routers/core/connection_rules.py +188 -0
  34. app/api/routers/core/data_sources.py +374 -0
  35. app/api/routers/core/regex.py +333 -0
  36. app/api/routers/core/reporting.py +263 -0
  37. app/api/routers/files/__init__.py +24 -0
  38. app/api/routers/files/ops.py +179 -0
  39. app/api/routers/files/transfer.py +101 -0
  40. app/api/routers/preview/__init__.py +35 -0
  41. app/api/routers/preview/content_mode.py +264 -0
  42. app/api/routers/preview/header_row.py +154 -0
  43. app/api/routers/preview/models.py +81 -0
  44. app/api/routers/preview/path_mode.py +440 -0
  45. app/api/routers/preview/router.py +30 -0
  46. app/api/routers/project/__init__.py +61 -0
  47. app/api/routers/project/base.py +111 -0
  48. app/api/routers/project/constraint.py +345 -0
  49. app/api/routers/project/full_config.py +411 -0
  50. app/api/routers/project/full_config_writer.py +327 -0
  51. app/api/routers/project/helpers.py +167 -0
  52. app/api/routers/project/inspection_fix.py +330 -0
  53. app/api/routers/project/manifest.py +605 -0
  54. app/api/routers/project/models.py +177 -0
  55. app/api/routers/project/pattern.py +268 -0
  56. app/api/routers/project/regex.py +358 -0
  57. app/api/routers/project/scanner.py +157 -0
  58. app/api/routers/project/schema.py +567 -0
  59. app/api/routers/project/schema_helpers.py +149 -0
  60. app/api/routers/project/settings.py +399 -0
  61. app/api/routers/project/template.py +325 -0
  62. app/api/routers/project/validation.py +227 -0
  63. app/api/routers/project/view.py +177 -0
  64. app/api/routers/project/workspaces.py +167 -0
  65. app/api/routers/projects/__init__.py +25 -0
  66. app/api/routers/projects/create.py +121 -0
  67. app/api/routers/projects/open.py +103 -0
  68. app/api/routers/projects/scan.py +100 -0
  69. app/api/routers/validation/__init__.py +39 -0
  70. app/api/routers/validation/common.py +263 -0
  71. app/api/routers/validation/content_mode.py +401 -0
  72. app/api/routers/validation/history.py +235 -0
  73. app/api/routers/validation/inline_mode.py +196 -0
  74. app/api/routers/validation/path_mode.py +314 -0
  75. app/api/routers/validation/router.py +51 -0
  76. app/api/services/full_validation_response_builder.py +412 -0
  77. app/api/services/io_error_messages.py +41 -0
  78. app/api/services/preview_service.py +248 -0
  79. app/cli/__init__.py +18 -0
  80. app/cli/__main__.py +30 -0
  81. app/cli/shared_services/__init__.py +42 -0
  82. app/cli/shared_services/config_ops.py +581 -0
  83. app/cli/shared_services/generation_ops.py +160 -0
  84. app/cli/shared_services/project_ops.py +288 -0
  85. app/cli/shell/__init__.py +48 -0
  86. app/cli/shell/commands/__init__.py +63 -0
  87. app/cli/shell/commands/ai/__init__.py +260 -0
  88. app/cli/shell/commands/ai/chat.py +331 -0
  89. app/cli/shell/commands/ai/delete.py +165 -0
  90. app/cli/shell/commands/ai/diff.py +70 -0
  91. app/cli/shell/commands/ai/display.py +428 -0
  92. app/cli/shell/commands/ai/executor.py +225 -0
  93. app/cli/shell/commands/ai/executor_utils.py +271 -0
  94. app/cli/shell/commands/ai/generate.py +285 -0
  95. app/cli/shell/commands/ai/interaction.py +262 -0
  96. app/cli/shell/commands/ai/migrate.py +281 -0
  97. app/cli/shell/commands/ai/resolver.py +224 -0
  98. app/cli/shell/commands/ai/status.py +101 -0
  99. app/cli/shell/commands/ai/switch.py +158 -0
  100. app/cli/shell/commands/ai/utils.py +52 -0
  101. app/cli/shell/commands/base.py +343 -0
  102. app/cli/shell/commands/config/__init__.py +131 -0
  103. app/cli/shell/commands/config/base.py +80 -0
  104. app/cli/shell/commands/config/check.py +246 -0
  105. app/cli/shell/commands/config/edit.py +148 -0
  106. app/cli/shell/commands/config/get.py +111 -0
  107. app/cli/shell/commands/config/init.py +123 -0
  108. app/cli/shell/commands/config/inspect.py +164 -0
  109. app/cli/shell/commands/config/list.py +104 -0
  110. app/cli/shell/commands/config/set.py +119 -0
  111. app/cli/shell/commands/config/show.py +150 -0
  112. app/cli/shell/commands/exit.py +67 -0
  113. app/cli/shell/commands/help.py +104 -0
  114. app/cli/shell/commands/infer_schema.py +156 -0
  115. app/cli/shell/commands/open.py +201 -0
  116. app/cli/shell/commands/project.py +215 -0
  117. app/cli/shell/commands/provider.py +619 -0
  118. app/cli/shell/commands/system.py +170 -0
  119. app/cli/shell/commands/validate.py +455 -0
  120. app/cli/shell/completer.py +231 -0
  121. app/cli/shell/config_storage.py +191 -0
  122. app/cli/shell/exceptions.py +90 -0
  123. app/cli/shell/formatter.py +371 -0
  124. app/cli/shell/interactive_menu.py +348 -0
  125. app/cli/shell/main.py +286 -0
  126. app/cli/shell/parser.py +266 -0
  127. app/cli/start.py +180 -0
  128. app/cli_main.py +52 -0
  129. app/mcp_server.py +311 -0
  130. app/shared/core/__init__.py +18 -0
  131. app/shared/core/app_version.py +59 -0
  132. app/shared/core/config/__init__.py +119 -0
  133. app/shared/core/config/server.py +193 -0
  134. app/shared/core/data_source/__init__.py +106 -0
  135. app/shared/core/data_source/loader.py +348 -0
  136. app/shared/core/data_source/loaders/__init__.py +213 -0
  137. app/shared/core/data_source/loaders/base.py +215 -0
  138. app/shared/core/data_source/loaders/converter.py +337 -0
  139. app/shared/core/data_source/loaders/csv_loader.py +252 -0
  140. app/shared/core/data_source/loaders/excel_loader.py +666 -0
  141. app/shared/core/data_source/loaders/extractor.py +268 -0
  142. app/shared/core/data_source/loaders/json_loader.py +354 -0
  143. app/shared/core/data_source/loaders/registry.py +88 -0
  144. app/shared/core/data_source/loaders/sql_loader.py +313 -0
  145. app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
  146. app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
  147. app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
  148. app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
  149. app/shared/core/data_source/schema_info.py +50 -0
  150. app/shared/core/data_source/specs/__init__.py +128 -0
  151. app/shared/core/data_source/specs/base.py +258 -0
  152. app/shared/core/data_source/specs/csv_source.py +137 -0
  153. app/shared/core/data_source/specs/excel_source.py +141 -0
  154. app/shared/core/data_source/specs/file_base.py +252 -0
  155. app/shared/core/data_source/specs/json_source.py +226 -0
  156. app/shared/core/data_source/specs/sql_source.py +161 -0
  157. app/shared/core/encoding.py +36 -0
  158. app/shared/core/io/__init__.py +24 -0
  159. app/shared/core/io/yaml.py +379 -0
  160. app/shared/core/manifest_schema/__init__.py +52 -0
  161. app/shared/core/manifest_schema/types.py +99 -0
  162. app/shared/core/manifest_schema/version.py +175 -0
  163. app/shared/core/patterns/__init__.py +24 -0
  164. app/shared/core/patterns/loader.py +134 -0
  165. app/shared/core/patterns/writer.py +244 -0
  166. app/shared/core/project/__init__.py +47 -0
  167. app/shared/core/project/constraint/builders/__init__.py +50 -0
  168. app/shared/core/project/constraint/builders/base.py +100 -0
  169. app/shared/core/project/constraint/builders/composite.py +77 -0
  170. app/shared/core/project/constraint/builders/conditional.py +67 -0
  171. app/shared/core/project/constraint/builders/foreign_key.py +53 -0
  172. app/shared/core/project/constraint/builders/registry.py +64 -0
  173. app/shared/core/project/constraint/builders/scripted.py +51 -0
  174. app/shared/core/project/constraint/builders/single_column.py +86 -0
  175. app/shared/core/project/constraint/builders/unique.py +53 -0
  176. app/shared/core/project/constraint/factory.py +170 -0
  177. app/shared/core/project/constraint/reader.py +214 -0
  178. app/shared/core/project/constraint/registry.py +233 -0
  179. app/shared/core/project/constraint/types/__init__.py +63 -0
  180. app/shared/core/project/constraint/types/constraint_file.py +261 -0
  181. app/shared/core/project/constraint/types/refs.py +460 -0
  182. app/shared/core/project/constraint/types.py +28 -0
  183. app/shared/core/project/constraint/writer.py +181 -0
  184. app/shared/core/project/loader/__init__.py +56 -0
  185. app/shared/core/project/loader/loader.py +30 -0
  186. app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
  187. app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
  188. app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
  189. app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
  190. app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
  191. app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
  192. app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
  193. app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
  194. app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
  195. app/shared/core/project/loader/loader_parts/main.py +432 -0
  196. app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
  197. app/shared/core/project/loader/loader_parts/runtime.py +125 -0
  198. app/shared/core/project/loader/types.py +246 -0
  199. app/shared/core/project/manifest/coverage.py +391 -0
  200. app/shared/core/project/manifest/reader.py +260 -0
  201. app/shared/core/project/manifest/types.py +91 -0
  202. app/shared/core/project/manifest/types_parts/__init__.py +60 -0
  203. app/shared/core/project/manifest/types_parts/constants.py +40 -0
  204. app/shared/core/project/manifest/types_parts/data_source.py +68 -0
  205. app/shared/core/project/manifest/types_parts/info.py +59 -0
  206. app/shared/core/project/manifest/types_parts/manifest.py +294 -0
  207. app/shared/core/project/manifest/types_parts/refs.py +153 -0
  208. app/shared/core/project/manifest/types_parts/settings.py +92 -0
  209. app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
  210. app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
  211. app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
  212. app/shared/core/project/manifest/types_parts/template.py +66 -0
  213. app/shared/core/project/manifest/writer.py +262 -0
  214. app/shared/core/project/manual_data/__init__.py +27 -0
  215. app/shared/core/project/manual_data/types.py +75 -0
  216. app/shared/core/project/regex/reader.py +197 -0
  217. app/shared/core/project/regex/types.py +405 -0
  218. app/shared/core/project/regex/writer.py +123 -0
  219. app/shared/core/project/schema/reader.py +170 -0
  220. app/shared/core/project/schema/types.py +47 -0
  221. app/shared/core/project/schema/types_parts/__init__.py +36 -0
  222. app/shared/core/project/schema/types_parts/column.py +174 -0
  223. app/shared/core/project/schema/types_parts/column_utils.py +72 -0
  224. app/shared/core/project/schema/types_parts/constraint.py +165 -0
  225. app/shared/core/project/schema/types_parts/schema_id.py +66 -0
  226. app/shared/core/project/schema/types_parts/source.py +255 -0
  227. app/shared/core/project/schema/types_parts/source_options.py +347 -0
  228. app/shared/core/project/schema/types_parts/table.py +230 -0
  229. app/shared/core/project/schema/writer.py +139 -0
  230. app/shared/core/project/schema_ref_check.py +95 -0
  231. app/shared/core/project/template/__init__.py +27 -0
  232. app/shared/core/project/template/expander.py +263 -0
  233. app/shared/core/project/template/reader.py +120 -0
  234. app/shared/core/project/template/types.py +114 -0
  235. app/shared/core/project/transform/reader.py +76 -0
  236. app/shared/core/project/transform/types.py +116 -0
  237. app/shared/core/project/transform/writer.py +84 -0
  238. app/shared/core/pydantic_messages.py +59 -0
  239. app/shared/core/reporter/__init__.py +46 -0
  240. app/shared/core/reporter/reporter.py +220 -0
  241. app/shared/core/reporter/reporters/__init__.py +65 -0
  242. app/shared/core/reporter/reporters/base.py +188 -0
  243. app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
  244. app/shared/core/reporter/reporters/email_reporter.py +271 -0
  245. app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
  246. app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
  247. app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
  248. app/shared/core/utils/__init__.py +18 -0
  249. app/shared/core/utils/path_utils.py +60 -0
  250. app/shared/core/utils/regex_extract.py +113 -0
  251. app/shared/domain/__init__.py +114 -0
  252. app/shared/domain/constraints/__init__.py +76 -0
  253. app/shared/domain/constraints/allowed_values.py +211 -0
  254. app/shared/domain/constraints/base.py +141 -0
  255. app/shared/domain/constraints/charset.py +337 -0
  256. app/shared/domain/constraints/composite.py +174 -0
  257. app/shared/domain/constraints/condition_registry.py +130 -0
  258. app/shared/domain/constraints/conditional.py +629 -0
  259. app/shared/domain/constraints/date_logic.py +731 -0
  260. app/shared/domain/constraints/foreign_key.py +261 -0
  261. app/shared/domain/constraints/key_normalization.py +56 -0
  262. app/shared/domain/constraints/not_null.py +185 -0
  263. app/shared/domain/constraints/range.py +360 -0
  264. app/shared/domain/constraints/regex.py +218 -0
  265. app/shared/domain/constraints/scripted.py +276 -0
  266. app/shared/domain/constraints/unique.py +233 -0
  267. app/shared/domain/data_engine.py +384 -0
  268. app/shared/domain/data_types.py +113 -0
  269. app/shared/domain/data_types_parts/__init__.py +67 -0
  270. app/shared/domain/data_types_parts/base.py +204 -0
  271. app/shared/domain/data_types_parts/composite.py +250 -0
  272. app/shared/domain/data_types_parts/expression.py +202 -0
  273. app/shared/domain/data_types_parts/extracted.py +106 -0
  274. app/shared/domain/data_types_parts/json_types.py +234 -0
  275. app/shared/domain/data_types_parts/scalars.py +719 -0
  276. app/shared/domain/data_types_parts/sequence.py +129 -0
  277. app/shared/domain/dataset_schema.py +48 -0
  278. app/shared/domain/eval_sandbox.py +63 -0
  279. app/shared/domain/expression_system.py +366 -0
  280. app/shared/domain/regex_flags.py +53 -0
  281. app/shared/domain/schema/builder.py +223 -0
  282. app/shared/domain/schema/models.py +339 -0
  283. app/shared/domain/transforms/__init__.py +28 -0
  284. app/shared/domain/transforms/aggregate.py +141 -0
  285. app/shared/domain/transforms/base.py +171 -0
  286. app/shared/domain/transforms/cast_type.py +120 -0
  287. app/shared/domain/transforms/concat.py +103 -0
  288. app/shared/domain/transforms/conditional_assign.py +114 -0
  289. app/shared/domain/transforms/date_format.py +81 -0
  290. app/shared/domain/transforms/digits.py +77 -0
  291. app/shared/domain/transforms/drop_duplicates.py +94 -0
  292. app/shared/domain/transforms/fill_na.py +94 -0
  293. app/shared/domain/transforms/filter_rows.py +97 -0
  294. app/shared/domain/transforms/lookup.py +78 -0
  295. app/shared/domain/transforms/lower_case.py +70 -0
  296. app/shared/domain/transforms/map_value.py +90 -0
  297. app/shared/domain/transforms/math_expr.py +112 -0
  298. app/shared/domain/transforms/modulo.py +82 -0
  299. app/shared/domain/transforms/regex_extract.py +103 -0
  300. app/shared/domain/transforms/registry.py +95 -0
  301. app/shared/domain/transforms/replace.py +88 -0
  302. app/shared/domain/transforms/sort_rows.py +94 -0
  303. app/shared/domain/transforms/string_split.py +84 -0
  304. app/shared/domain/transforms/strip.py +73 -0
  305. app/shared/domain/transforms/substring.py +92 -0
  306. app/shared/domain/transforms/upper_case.py +70 -0
  307. app/shared/domain/transforms/weighted_sum.py +104 -0
  308. app/shared/domain/validation_constraints.py +69 -0
  309. app/shared/services/__init__.py +62 -0
  310. app/shared/services/ai/__init__.py +48 -0
  311. app/shared/services/ai/agent/__init__.py +40 -0
  312. app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
  313. app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
  314. app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
  315. app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
  316. app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
  317. app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
  318. app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
  319. app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
  320. app/shared/services/ai/agent/executor.py +554 -0
  321. app/shared/services/ai/agent/memory.py +256 -0
  322. app/shared/services/ai/agent/planner.py +282 -0
  323. app/shared/services/ai/agent/tool_registry.py +296 -0
  324. app/shared/services/ai/agent/tools/__init__.py +32 -0
  325. app/shared/services/ai/agent/tools/config_generate.py +133 -0
  326. app/shared/services/ai/agent/tools/config_refine.py +109 -0
  327. app/shared/services/ai/agent/tools/config_validate.py +403 -0
  328. app/shared/services/ai/agent/tools/merge_results.py +136 -0
  329. app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
  330. app/shared/services/ai/agent/tools/script_parse.py +359 -0
  331. app/shared/services/ai/agent/types.py +138 -0
  332. app/shared/services/ai/chat_agent_runner.py +596 -0
  333. app/shared/services/ai/chat_orchestrator.py +725 -0
  334. app/shared/services/ai/failure_messages.py +65 -0
  335. app/shared/services/ai/job_storage.py +274 -0
  336. app/shared/services/ai/migrate_service.py +550 -0
  337. app/shared/services/ai/streaming/__init__.py +65 -0
  338. app/shared/services/ai/streaming/event_journal.py +197 -0
  339. app/shared/services/ai/streaming/orchestrator.py +262 -0
  340. app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
  341. app/shared/services/ai/streaming/sse_response.py +148 -0
  342. app/shared/services/ai/streaming/types.py +73 -0
  343. app/shared/services/ai/types.py +213 -0
  344. app/shared/services/ai/utils.py +322 -0
  345. app/shared/services/diff/config_diff.py +320 -0
  346. app/shared/services/hardware.py +237 -0
  347. app/shared/services/llm/__init__.py +62 -0
  348. app/shared/services/llm/actions/__init__.py +42 -0
  349. app/shared/services/llm/actions/_canvas_validator.py +147 -0
  350. app/shared/services/llm/actions/_constraint_validator.py +401 -0
  351. app/shared/services/llm/actions/_regex_validator.py +85 -0
  352. app/shared/services/llm/actions/_schema_validator.py +82 -0
  353. app/shared/services/llm/actions/_settings_validator.py +76 -0
  354. app/shared/services/llm/actions/_transform_validator.py +78 -0
  355. app/shared/services/llm/actions/action_handlers.py +400 -0
  356. app/shared/services/llm/actions/action_parser.py +63 -0
  357. app/shared/services/llm/actions/action_processor.py +468 -0
  358. app/shared/services/llm/actions/action_validator.py +311 -0
  359. app/shared/services/llm/actions/diff_compute.py +195 -0
  360. app/shared/services/llm/actions/regex_handlers.py +284 -0
  361. app/shared/services/llm/actions/registry.py +336 -0
  362. app/shared/services/llm/actions/schema_handlers.py +335 -0
  363. app/shared/services/llm/actions/settings_handlers.py +180 -0
  364. app/shared/services/llm/actions/specs.py +259 -0
  365. app/shared/services/llm/actions/transform_handlers.py +279 -0
  366. app/shared/services/llm/actions/validation_types.py +147 -0
  367. app/shared/services/llm/cache/__init__.py +18 -0
  368. app/shared/services/llm/cache/response_cache.py +80 -0
  369. app/shared/services/llm/chat/__init__.py +35 -0
  370. app/shared/services/llm/chat/chat_system_prompt.py +561 -0
  371. app/shared/services/llm/chat/response_parser.py +296 -0
  372. app/shared/services/llm/config/__init__.py +45 -0
  373. app/shared/services/llm/config/crypto.py +135 -0
  374. app/shared/services/llm/config/loader.py +258 -0
  375. app/shared/services/llm/config/models.py +202 -0
  376. app/shared/services/llm/config/presets.py +128 -0
  377. app/shared/services/llm/config_generator.py +163 -0
  378. app/shared/services/llm/constraints/__init__.py +41 -0
  379. app/shared/services/llm/constraints/constraint_builder.py +177 -0
  380. app/shared/services/llm/constraints/constraint_deletion.py +84 -0
  381. app/shared/services/llm/constraints/constraint_id.py +174 -0
  382. app/shared/services/llm/constraints/frontend_instructions.py +333 -0
  383. app/shared/services/llm/constraints/inline_batch.py +279 -0
  384. app/shared/services/llm/discovery/__init__.py +35 -0
  385. app/shared/services/llm/discovery/scanner.py +181 -0
  386. app/shared/services/llm/generation/__init__.py +42 -0
  387. app/shared/services/llm/generation/agent_wiring.py +156 -0
  388. app/shared/services/llm/generation/config_builder.py +386 -0
  389. app/shared/services/llm/generation/errors.py +36 -0
  390. app/shared/services/llm/generation/existing_config.py +99 -0
  391. app/shared/services/llm/generation/profiler.py +279 -0
  392. app/shared/services/llm/generation/prompt_builder.py +199 -0
  393. app/shared/services/llm/generation/response_parser.py +144 -0
  394. app/shared/services/llm/generation/service.py +622 -0
  395. app/shared/services/llm/models.py +104 -0
  396. app/shared/services/llm/providers/__init__.py +48 -0
  397. app/shared/services/llm/providers/base.py +310 -0
  398. app/shared/services/llm/providers/cached_provider.py +90 -0
  399. app/shared/services/llm/providers/ollama.py +498 -0
  400. app/shared/services/llm/providers/openai.py +334 -0
  401. app/shared/services/llm/providers/registry.py +108 -0
  402. app/shared/services/llm/schema_resolver.py +141 -0
  403. app/shared/services/llm/suggestion_utils.py +219 -0
  404. app/shared/services/llm/validate_executor.py +163 -0
  405. app/shared/services/llm/yaml_io.py +406 -0
  406. app/shared/services/preview/__init__.py +18 -0
  407. app/shared/services/preview/loader.py +145 -0
  408. app/shared/services/preview/path_validation.py +138 -0
  409. app/shared/services/project_loader.py +57 -0
  410. app/shared/services/schema_inference.py +260 -0
  411. app/shared/services/schema_runtime_builder.py +120 -0
  412. app/shared/services/validation/__init__.py +52 -0
  413. app/shared/services/validation/chunked_loader.py +509 -0
  414. app/shared/services/validation/dag/__init__.py +35 -0
  415. app/shared/services/validation/dag/builder.py +141 -0
  416. app/shared/services/validation/dag/executor.py +225 -0
  417. app/shared/services/validation/dag/sorter.py +77 -0
  418. app/shared/services/validation/data_loader.py +231 -0
  419. app/shared/services/validation/engine.py +446 -0
  420. app/shared/services/validation/executor.py +1030 -0
  421. app/shared/services/validation/extractors.py +266 -0
  422. app/shared/services/validation/history.py +209 -0
  423. app/shared/services/validation/json_payload.py +136 -0
  424. app/shared/services/validation/loader.py +152 -0
  425. app/shared/services/validation/memory_monitor.py +179 -0
  426. app/shared/services/validation/postprocess.py +208 -0
  427. app/shared/services/validation/progress.py +64 -0
  428. app/shared/services/validation/report_export.py +222 -0
  429. app/shared/services/validation/resolver.py +180 -0
  430. app/shared/services/validation/service.py +418 -0
  431. app/shared/services/validation/types.py +238 -0
  432. app/shared/services/validation/validators/__init__.py +36 -0
  433. app/shared/services/validation/validators/adapter.py +182 -0
  434. app/shared/services/validation/validators/base.py +332 -0
  435. app/shared/services/validation/validators/composite.py +233 -0
  436. app/shared/services/validation/validators/date_logic.py +276 -0
  437. app/start_server.py +133 -0
  438. precis_cli-0.1.3.dist-info/METADATA +180 -0
  439. precis_cli-0.1.3.dist-info/RECORD +442 -0
  440. precis_cli-0.1.3.dist-info/WHEEL +5 -0
  441. precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
  442. precis_cli-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,619 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ # backend/app/cli/shell/commands/provider.py
17
+ """
18
+ @fileoverview CLI Provider 命令模块(交互式 AI Provider 管理器)
19
+
20
+ 功能概述:
21
+ - 提供交互式 AI Provider 管理界面,与前端 AI 设置面板对齐
22
+ - 支持添加、编辑、删除、测试 Provider
23
+ - 支持设置默认 Provider
24
+ - 支持查看配置文件路径和模板
25
+ - 支持热重载配置文件
26
+
27
+ 架构设计:
28
+ - ProviderCommand 继承 Command 基类
29
+ - execute() 进入交互式主循环
30
+ - 通过 config_storage 管理配置,通过 provider registry 测试连接
31
+ - 预设从 presets.py 加载
32
+
33
+ 输入示例:
34
+ precis> provider
35
+ precis> provider reload
36
+ precis> provider test openai
37
+
38
+ 输出示例:
39
+ 交互式菜单或操作结果
40
+ """
41
+
42
+ import asyncio
43
+ import getpass
44
+
45
+ from app.cli.shell.commands.base import Command, CommandResult, ProjectContext
46
+ from app.cli.shell.config_storage import (
47
+ get_cli_config,
48
+ reload_providers_config,
49
+ )
50
+ from app.cli.shell.formatter import Colors, Formatter
51
+ from app.cli.shell.interactive_menu import InteractiveMenu
52
+ from app.shared.services.llm.config.loader import loader
53
+ from app.shared.services.llm.config.models import AIProvider, ProviderType
54
+ from app.shared.services.llm.config.presets import get_preset_list
55
+ from app.shared.services.llm.providers.registry import create
56
+
57
+
58
+ class ProviderCommand(Command):
59
+ """交互式 AI Provider 管理器。
60
+
61
+ 提供与前端 AI 设置面板对齐的 CLI 管理界面:
62
+ - 查看已配置 Provider 列表(含状态、默认标记)
63
+ - 添加新 Provider(选预设 → 填 API Key → 选模型 → 命名)
64
+ - 编辑已有 Provider(名称、API Key、模型)
65
+ - 删除 Provider(带确认)
66
+ - 测试 Provider 连接
67
+ - 设置默认 Provider
68
+ - 查看配置文件路径和模板
69
+ """
70
+
71
+ def __init__(self) -> None:
72
+ super().__init__("provider")
73
+ self._config = get_cli_config()
74
+
75
+ @property
76
+ def description(self) -> str:
77
+ return "AI Provider 管理"
78
+
79
+ @property
80
+ def usage(self) -> str:
81
+ return "provider [reload|test <id>]"
82
+
83
+ @property
84
+ def help_text(self) -> str:
85
+ return """
86
+ 用法: provider [子命令] [参数]
87
+
88
+ 子命令:
89
+ provider 进入交互式 Provider 管理界面
90
+ provider reload 热重载 ai_providers.yaml 配置
91
+ provider test <id> 测试指定 Provider 的连接
92
+
93
+ 说明:
94
+ 交互式界面支持以下操作:
95
+ - 添加新 Provider(从预设选择)
96
+ - 编辑已有 Provider
97
+ - 删除 Provider
98
+ - 测试 Provider 连接
99
+ - 设置默认 Provider
100
+ - 查看配置文件路径和模板
101
+ """.strip()
102
+
103
+ def execute(self, args: list[str], context: ProjectContext) -> CommandResult:
104
+ """执行 provider 命令。
105
+
106
+ Args:
107
+ args: 命令参数列表
108
+ context: 项目上下文
109
+
110
+ Returns:
111
+ 操作结果
112
+ """
113
+ if not args:
114
+ return self._interactive_main_loop()
115
+
116
+ sub = args[0].lower()
117
+ if sub == "reload":
118
+ return self._reload_providers_config()
119
+ elif sub == "test" and len(args) > 1:
120
+ return self._test_provider(args[1])
121
+ else:
122
+ return self._interactive_main_loop()
123
+
124
+ # ── 主循环 ──────────────────────────────────────────────────────
125
+
126
+ def _interactive_main_loop(self) -> CommandResult:
127
+ """交互式主循环。"""
128
+ while True:
129
+ self._render_main_menu()
130
+ menu = InteractiveMenu("请选择操作:", show_cancel=True)
131
+ menu.add_item("add", "添加 Provider", "从预设添加新的 Provider")
132
+ menu.add_item("edit", "编辑 Provider", "修改已有 Provider 配置")
133
+ menu.add_item("delete", "删除 Provider", "删除已有 Provider")
134
+ menu.add_item("test", "测试连接", "测试 Provider 的连接状态")
135
+ menu.add_item("default", "设为默认", "设置默认使用的 Provider")
136
+ menu.add_item("advanced", "高级", "查看配置文件路径和模板")
137
+
138
+ choice = menu.show()
139
+ if choice is None:
140
+ return CommandResult.ok("已退出")
141
+ elif choice == "add":
142
+ self._add_provider()
143
+ elif choice == "edit":
144
+ self._edit_provider()
145
+ elif choice == "delete":
146
+ self._delete_provider()
147
+ elif choice == "test":
148
+ self._test_all_providers()
149
+ elif choice == "default":
150
+ self._set_default_provider()
151
+ elif choice == "advanced":
152
+ self._show_advanced()
153
+
154
+ def _render_main_menu(self) -> None:
155
+ """渲染主菜单头部信息。"""
156
+ print(Formatter.header("\nAI Provider 管理"))
157
+ print(Formatter.info(f"配置文件: {loader.config_path}"))
158
+
159
+ providers = self._config.list_providers()
160
+ active = self._config.get_active_provider()
161
+
162
+ if not providers:
163
+ print(Formatter.warning("\n[!] 当前未配置任何 Provider"))
164
+ else:
165
+ print()
166
+ for p in providers:
167
+ is_active = active and active.id == p.id
168
+ marker = Formatter.colorize(" [默认]", Colors.GREEN) if is_active else ""
169
+ provider_type = p.type.value if hasattr(p.type, "value") else str(p.type)
170
+ key_status = Formatter.success("[已配置]") if p.api_key else Formatter.warning("[未配置]")
171
+ print(f" {Formatter.colorize(p.name, Colors.BOLD)} ({p.id}) {key_status}{marker}")
172
+ print(f" {provider_type} | {p.model} | {p.base_url}")
173
+ print()
174
+
175
+ # ── 添加 Provider ───────────────────────────────────────────────
176
+
177
+ def _add_provider(self) -> None:
178
+ """添加新 Provider(选预设 → 模型 → 命名 → 输入 API Key)。"""
179
+ presets = get_preset_list()
180
+ if not presets:
181
+ print(Formatter.warning("\n[!] 没有可用的预设"))
182
+ return
183
+
184
+ print(Formatter.header("\n添加 Provider"))
185
+
186
+ # 选择预设
187
+ menu = InteractiveMenu("请选择服务商预设:")
188
+ for pr in presets:
189
+ menu.add_item(pr["id"], pr["name"], f"{pr['type']} | {pr['base_url']}")
190
+
191
+ preset_id = menu.show()
192
+ if preset_id is None:
193
+ print(Formatter.info("已取消"))
194
+ return
195
+
196
+ preset = next((p for p in presets if p["id"] == preset_id), None)
197
+ if not preset:
198
+ print(Formatter.error("预设不存在"))
199
+ return
200
+
201
+ # 选择模型
202
+ model = preset["default_model"]
203
+ if preset["models"]:
204
+ model_menu = InteractiveMenu("请选择模型:")
205
+ for m in preset["models"]:
206
+ model_menu.add_item(m, m, "")
207
+ chosen = model_menu.show()
208
+ if chosen:
209
+ model = chosen
210
+ elif chosen is None:
211
+ print(Formatter.info("已取消"))
212
+ return
213
+ else:
214
+ try:
215
+ custom = input(Formatter.colorize(f"\n请输入模型名称 (默认: {model}): ", Colors.CYAN)).strip()
216
+ if custom:
217
+ model = custom
218
+ except (KeyboardInterrupt, EOFError):
219
+ print()
220
+ print(Formatter.info("已取消"))
221
+ return
222
+
223
+ # 命名
224
+ name = preset["name"]
225
+ try:
226
+ custom_name = input(Formatter.colorize(f"\nProvider 名称 (默认: {name}): ", Colors.CYAN)).strip()
227
+ if custom_name:
228
+ name = custom_name
229
+ except (KeyboardInterrupt, EOFError):
230
+ print()
231
+ print(Formatter.info("已取消"))
232
+ return
233
+
234
+ # 输入 API Key(本地 Ollama 可直接回车跳过)
235
+ api_key = None
236
+ if preset["type"] != "ollama":
237
+ try:
238
+ key_input = getpass.getpass(
239
+ Formatter.colorize("\n请输入 API Key (直接回车可跳过,后续通过环境变量设置): ", Colors.CYAN)
240
+ ).strip()
241
+ if key_input:
242
+ api_key = key_input
243
+ except (KeyboardInterrupt, EOFError):
244
+ print()
245
+ print(Formatter.info("已取消"))
246
+ return
247
+
248
+ # 上下文窗口(可选,留空=自动探测)
249
+ context_window = None
250
+ try:
251
+ context_window, _ = self._prompt_context_window()
252
+ except (KeyboardInterrupt, EOFError):
253
+ print()
254
+ print(Formatter.info("已取消"))
255
+ return
256
+
257
+ # 生成 ID
258
+ provider_id = preset["id"]
259
+ existing_ids = {p.id for p in self._config.list_providers()}
260
+ if provider_id in existing_ids:
261
+ suffix = 2
262
+ while f"{provider_id}-{suffix}" in existing_ids:
263
+ suffix += 1
264
+ provider_id = f"{provider_id}-{suffix}"
265
+
266
+ provider = AIProvider(
267
+ id=provider_id,
268
+ name=name,
269
+ type=ProviderType.OPENAI if preset["type"] == "openai" else ProviderType.OLLAMA,
270
+ base_url=preset["base_url"],
271
+ model=model,
272
+ api_key=api_key,
273
+ context_window=context_window,
274
+ )
275
+ # §2.12 修复:用户显式输入的 key 标记为手工来源——否则该 id 若存在同名
276
+ # <ID>_API_KEY 环境变量,save 会把刚输入的 key 当 env 来源剔除、永不落盘
277
+ if api_key:
278
+ self._config.mark_api_key_manual(provider_id)
279
+ self._config.add_or_update_provider(provider)
280
+
281
+ print(Formatter.success(f"\n[*] 已添加 Provider: {name} ({provider_id})"))
282
+ print(Formatter.info(f" 模型: {model}"))
283
+ cw_display = str(context_window) if context_window else "自动探测(默认 200000)"
284
+ print(Formatter.info(f" 上下文窗口: {cw_display}"))
285
+ if preset["type"] != "ollama":
286
+ if api_key:
287
+ print(Formatter.info(" API Key: 已保存"))
288
+ else:
289
+ print(Formatter.warning(" API Key: 未配置,可通过环境变量或编辑 Provider 设置"))
290
+
291
+ # ── 编辑 Provider ───────────────────────────────────────────────
292
+
293
+ def _prompt_context_window(self, current: int | None = None) -> tuple[int | None, bool]:
294
+ """交互式输入 context_window。
295
+
296
+ Args:
297
+ current: 当前已配置的值(编辑场景传入,添加场景为 None)
298
+
299
+ Returns:
300
+ (值, 是否合法输入):
301
+ - 留空(直接回车)返回 (None, True),表示自动探测/回退到全局默认 200000;
302
+ - 非法输入(非整数或 < 1024)返回 (None, False),表示"已忽略",
303
+ 调用方应保留原值(编辑场景);
304
+ - 合法整数返回 (value, True)。
305
+ 用户中断(Ctrl+C / EOF)抛 KeyboardInterrupt/EOFError 由调用方处理。
306
+ """
307
+ prompt_current = str(current) if current else "自动探测(默认 200000)"
308
+ raw = input(
309
+ Formatter.colorize(
310
+ f" 上下文窗口 tokens (当前: {prompt_current}, 留空=自动): ",
311
+ Colors.CYAN,
312
+ )
313
+ ).strip()
314
+ if not raw:
315
+ return None, True
316
+ try:
317
+ value = int(raw)
318
+ except ValueError:
319
+ print(Formatter.warning(" [!] 非整数,已忽略"))
320
+ return None, False
321
+ if value < 1024:
322
+ print(Formatter.warning(" [!] 必须 >= 1024,已忽略"))
323
+ return None, False
324
+ return value, True
325
+
326
+ def _edit_provider(self) -> None:
327
+ """编辑已有 Provider。"""
328
+ providers = self._config.list_providers()
329
+ if not providers:
330
+ print(Formatter.warning("\n[!] 没有可编辑的 Provider"))
331
+ return
332
+
333
+ print(Formatter.header("\n编辑 Provider"))
334
+
335
+ menu = InteractiveMenu("请选择要编辑的 Provider:")
336
+ for p in providers:
337
+ key_status = "[已配置]" if p.api_key else "[未配置]"
338
+ menu.add_item(p.id, f"{p.name} ({p.id}) {key_status}", f"{p.model}")
339
+
340
+ provider_id = menu.show()
341
+ if provider_id is None:
342
+ print(Formatter.info("已取消"))
343
+ return
344
+
345
+ provider = self._config.get_provider(provider_id)
346
+ if not provider:
347
+ print(Formatter.error("Provider 不存在"))
348
+ return
349
+
350
+ # 编辑副本:后续字段修改只作用于副本,仅显式 "done" 才落盘;
351
+ # ESC/中断丢弃副本,存储内与磁盘上的配置均不被污染
352
+ provider = provider.model_copy(deep=True)
353
+ # §2.12 修复:仅在用户显式输入新 key 时标记手工来源(提交前统一标记,
354
+ # ESC 丢弃编辑则不标记——否则后续任何无关 save 会把 env 注入值当手工值落盘)
355
+ key_manually_set = False
356
+
357
+ # 编辑菜单
358
+ while True:
359
+ # 统一使用 AIProvider 的 type 字段
360
+ provider_type = provider.type.value if hasattr(provider.type, "value") else str(provider.type)
361
+ cw_display = str(provider.context_window) if provider.context_window else "自动探测(默认 200000)"
362
+ print(f"\n 编辑: {Formatter.colorize(provider.name, Colors.BOLD)} ({provider.id})")
363
+ print(f" 类型: {provider_type} | 模型: {provider.model}")
364
+ print(f" 端点: {provider.base_url}")
365
+ print(f" 上下文窗口: {cw_display}")
366
+ print(f" API Key: {'[已配置]' if provider.api_key else '[未配置]'}")
367
+
368
+ edit_menu = InteractiveMenu("请选择要修改的字段:")
369
+ edit_menu.add_item("name", "名称", f"当前: {provider.name}")
370
+ edit_menu.add_item("model", "模型", f"当前: {provider.model}")
371
+ edit_menu.add_item("context_window", "上下文窗口", f"当前: {cw_display}")
372
+ edit_menu.add_item("api_key", "API Key", f"当前: {'[已配置]' if provider.api_key else '[未配置]'}")
373
+ edit_menu.add_item("done", "完成并保存", "")
374
+
375
+ field = edit_menu.show()
376
+ if field is None:
377
+ # ESC/0 取消:丢弃编辑副本,不写盘
378
+ print(Formatter.info("已取消"))
379
+ return
380
+ if field == "done":
381
+ break
382
+
383
+ try:
384
+ if field == "name":
385
+ val = input(Formatter.colorize(f" 新名称 (当前: {provider.name}): ", Colors.CYAN)).strip()
386
+ if val:
387
+ provider.name = val
388
+ elif field == "model":
389
+ val = input(Formatter.colorize(f" 新模型 (当前: {provider.model}): ", Colors.CYAN)).strip()
390
+ if val:
391
+ provider.model = val
392
+ elif field == "context_window":
393
+ # 留空→None(自动探测);非法输入忽略并保留原值;合法整数直接写入
394
+ cw, valid = self._prompt_context_window(provider.context_window)
395
+ if valid:
396
+ provider.context_window = cw
397
+ elif field == "api_key":
398
+ key_input = getpass.getpass(
399
+ Formatter.colorize(
400
+ f" 新 API Key (当前: {'已配置' if provider.api_key else '未配置'}, 直接回车保持不变): ",
401
+ Colors.CYAN,
402
+ )
403
+ ).strip()
404
+ if key_input:
405
+ provider.api_key = key_input
406
+ key_manually_set = True
407
+ elif provider.api_key:
408
+ # 询问是否清空
409
+ confirm = (
410
+ input(Formatter.colorize(" 是否清空已保存的 API Key? (y/N): ", Colors.YELLOW))
411
+ .strip()
412
+ .lower()
413
+ )
414
+ if confirm in ("y", "yes"):
415
+ provider.api_key = None
416
+ except (KeyboardInterrupt, EOFError):
417
+ print()
418
+ print(Formatter.info("已取消"))
419
+ return
420
+
421
+ # §2.12 修复:用户显式输入的新 key 标记为手工来源后再落盘——否则该 id
422
+ # 若存在同名 <ID>_API_KEY 环境变量,save 会把新 key 当 env 来源剔除不落盘
423
+ if key_manually_set:
424
+ self._config.mark_api_key_manual(provider.id)
425
+ self._config.add_or_update_provider(provider)
426
+ print(Formatter.success(f"\n[*] 已更新: {provider.name}"))
427
+
428
+ # ── 删除 Provider ───────────────────────────────────────────────
429
+
430
+ def _delete_provider(self) -> None:
431
+ """删除 Provider。"""
432
+ providers = self._config.list_providers()
433
+ if not providers:
434
+ print(Formatter.warning("\n[!] 没有可删除的 Provider"))
435
+ return
436
+
437
+ print(Formatter.header("\n删除 Provider"))
438
+
439
+ menu = InteractiveMenu("请选择要删除的 Provider:")
440
+ for p in providers:
441
+ menu.add_item(p.id, f"{p.name} ({p.id})", f"{p.model}")
442
+
443
+ provider_id = menu.show()
444
+ if provider_id is None:
445
+ print(Formatter.info("已取消"))
446
+ return
447
+
448
+ provider = self._config.get_provider(provider_id)
449
+ if not provider:
450
+ print(Formatter.error("Provider 不存在"))
451
+ return
452
+
453
+ print(Formatter.warning(f"\n警告: 将删除 {provider.name} ({provider.id})"))
454
+ try:
455
+ confirm = input(Formatter.colorize("确认删除? (y/N): ", Colors.YELLOW)).strip().lower()
456
+ except (KeyboardInterrupt, EOFError):
457
+ print()
458
+ print(Formatter.info("已取消"))
459
+ return
460
+
461
+ if confirm not in ("y", "yes"):
462
+ print(Formatter.info("已取消"))
463
+ return
464
+
465
+ if self._config.delete_provider(provider_id):
466
+ print(Formatter.success(f"\n[*] 已删除: {provider.name}"))
467
+ else:
468
+ print(Formatter.error("删除失败"))
469
+
470
+ # ── 测试连接 ────────────────────────────────────────────────────
471
+
472
+ def _test_all_providers(self) -> None:
473
+ """测试所有 Provider 的连接。"""
474
+ providers = self._config.list_providers()
475
+ if not providers:
476
+ print(Formatter.warning("\n[!] 没有可测试的 Provider"))
477
+ return
478
+
479
+ print(Formatter.header("\n测试 Provider 连接"))
480
+
481
+ for p in providers:
482
+ self._test_single(p)
483
+ print()
484
+
485
+ def _test_single(self, provider: AIProvider) -> None:
486
+ """测试单个 Provider 的连接。"""
487
+ print(f" 测试 {Formatter.colorize(provider.name, Colors.BOLD)} ({provider.id})...", end=" ")
488
+ try:
489
+ prov = create(provider)
490
+ result = asyncio.run(prov.health())
491
+
492
+ status = result.get("status", "error")
493
+ if status == "ok":
494
+ latency = result.get("latency_ms", result.get("response_time_ms", "?"))
495
+ print(Formatter.success(f"✓ 正常 ({latency}ms)"))
496
+ else:
497
+ error = result.get("error", "未知错误")
498
+ print(Formatter.error(f"✗ {error}"))
499
+ except Exception as e:
500
+ print(Formatter.error(f"✗ {e}"))
501
+
502
+ def _test_provider(self, provider_id: str) -> CommandResult:
503
+ """测试指定 Provider 的连接(非交互式)。"""
504
+ provider = self._config.get_provider(provider_id)
505
+ if not provider:
506
+ return CommandResult.error(f"Provider '{provider_id}' 未配置")
507
+
508
+ self._test_single(provider)
509
+ return CommandResult.ok("测试完成")
510
+
511
+ # ── 设置默认 ────────────────────────────────────────────────────
512
+
513
+ def _set_default_provider(self) -> None:
514
+ """设置默认 Provider。"""
515
+ providers = self._config.list_providers()
516
+
517
+ if not providers:
518
+ print(Formatter.warning("\n[!] 没有已配置的 Provider"))
519
+ print(Formatter.info("请先使用 '添加 Provider' 添加 Provider"))
520
+ return
521
+
522
+ print(Formatter.header("\n设置默认 Provider"))
523
+
524
+ active = self._config.get_active_provider()
525
+ menu = InteractiveMenu("请选择默认 Provider:")
526
+ for p in providers:
527
+ is_active = active and active.id == p.id
528
+ marker = " [当前]" if is_active else ""
529
+ menu.add_item(p.id, f"{p.name} ({p.id}){marker}", f"{p.model}")
530
+
531
+ provider_id = menu.show()
532
+ if provider_id is None:
533
+ print(Formatter.info("已取消"))
534
+ return
535
+
536
+ provider = self._config.get_provider(provider_id)
537
+ if provider is not None and self._config.set_active_provider(provider_id):
538
+ print(Formatter.success(f"\n[*] 已设置默认 Provider: {provider.name}"))
539
+ else:
540
+ print(Formatter.error("设置失败"))
541
+
542
+ # ── 高级 ────────────────────────────────────────────────────────
543
+
544
+ def _show_advanced(self) -> None:
545
+ """显示高级信息(配置文件路径、模板)。"""
546
+ print(Formatter.header("\n高级设置"))
547
+
548
+ config_path = loader.config_path
549
+ print("\n 配置文件路径:")
550
+ print(f" {Formatter.info(str(config_path))}")
551
+
552
+ print("\n 配置模板:")
553
+ template = self._get_config_template()
554
+ for line in template.split("\n"):
555
+ print(f" {Formatter.dim(line)}")
556
+
557
+ tip = '提示: 修改配置文件后,使用 "provider reload" 立即生效'
558
+ print(f"\n {Formatter.dim(tip)}")
559
+
560
+ def _get_config_template(self) -> str:
561
+ """获取配置模板文本。"""
562
+ return """version: "2.0"
563
+
564
+ providers:
565
+ # OpenAI 或兼容 API
566
+ - id: openai
567
+ name: OpenAI
568
+ type: openai
569
+ base_url: https://api.openai.com/v1
570
+ api_key: sk-xxx
571
+ model: gpt-4o
572
+
573
+ # DeepSeek
574
+ - id: deepseek
575
+ name: DeepSeek
576
+ type: openai
577
+ base_url: https://api.deepseek.com
578
+ api_key: sk-xxx
579
+ model: deepseek-v4-flash
580
+
581
+ # 本地 Ollama(无需 API Key)
582
+ - id: ollama-local
583
+ name: Ollama Local
584
+ type: ollama
585
+ base_url: http://localhost:11434
586
+ api_key: null
587
+ model: llama3.2
588
+
589
+ defaults:
590
+ chat: openai
591
+
592
+ # 也支持通过环境变量设置 API Key(优先级高于配置文件):
593
+ # export OPENAI_API_KEY=sk-xxx
594
+ # export DEEPSEEK_API_KEY=sk-xxx""".strip()
595
+
596
+ # ── 重载 ────────────────────────────────────────────────────────
597
+
598
+ def _reload_providers_config(self) -> CommandResult:
599
+ """热重载 Provider 配置文件。"""
600
+ print(Formatter.header("\n热重载 Provider 配置"))
601
+ print(Formatter.info(f"配置文件: {loader.config_path}"))
602
+
603
+ if reload_providers_config():
604
+ providers = self._config.list_providers()
605
+ if providers:
606
+ print(Formatter.success("\n[*] 配置重载成功!"))
607
+ print(Formatter.info(f" 已配置 {len(providers)} 个 Provider:"))
608
+ for p in providers:
609
+ print(f" - {p.id}: {p.name} ({p.model})")
610
+ else:
611
+ print(Formatter.warning("\n[!] 配置文件为空,请先添加 Provider"))
612
+ return CommandResult.ok("配置已更新")
613
+ else:
614
+ return CommandResult.error(
615
+ "配置重载失败,请检查文件格式是否正确。\n提示: 配置文件必须是有效的 YAML 格式,包含 'providers' 字段。"
616
+ )
617
+
618
+
619
+ __all__ = ["ProviderCommand"]