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.
- app/__init__.py +16 -0
- app/api/__init__.py +32 -0
- app/api/dependencies.py +185 -0
- app/api/main.py +301 -0
- app/api/middleware/__init__.py +34 -0
- app/api/middleware/exception_handler.py +126 -0
- app/api/middleware/request_logging.py +97 -0
- app/api/middleware/token_auth.py +215 -0
- app/api/models/__init__.py +151 -0
- app/api/models/connection_rules.py +98 -0
- app/api/models/files.py +99 -0
- app/api/models/full_validation.py +554 -0
- app/api/models/project.py +117 -0
- app/api/models/projects.py +80 -0
- app/api/models/schema.py +121 -0
- app/api/models/v2_responses.py +46 -0
- app/api/models/validation.py +303 -0
- app/api/models/workspace.py +118 -0
- app/api/routers/__init__.py +56 -0
- app/api/routers/ai/__init__.py +36 -0
- app/api/routers/ai/chat.py +243 -0
- app/api/routers/ai/generate.py +156 -0
- app/api/routers/ai/hardware.py +199 -0
- app/api/routers/ai/jobs.py +544 -0
- app/api/routers/ai/migrate.py +471 -0
- app/api/routers/ai/models.py +352 -0
- app/api/routers/ai/ollama.py +108 -0
- app/api/routers/ai/providers.py +491 -0
- app/api/routers/ai/router.py +44 -0
- app/api/routers/ai/stream.py +281 -0
- app/api/routers/ai/utils.py +182 -0
- app/api/routers/core/__init__.py +34 -0
- app/api/routers/core/connection_rules.py +188 -0
- app/api/routers/core/data_sources.py +374 -0
- app/api/routers/core/regex.py +333 -0
- app/api/routers/core/reporting.py +263 -0
- app/api/routers/files/__init__.py +24 -0
- app/api/routers/files/ops.py +179 -0
- app/api/routers/files/transfer.py +101 -0
- app/api/routers/preview/__init__.py +35 -0
- app/api/routers/preview/content_mode.py +264 -0
- app/api/routers/preview/header_row.py +154 -0
- app/api/routers/preview/models.py +81 -0
- app/api/routers/preview/path_mode.py +440 -0
- app/api/routers/preview/router.py +30 -0
- app/api/routers/project/__init__.py +61 -0
- app/api/routers/project/base.py +111 -0
- app/api/routers/project/constraint.py +345 -0
- app/api/routers/project/full_config.py +411 -0
- app/api/routers/project/full_config_writer.py +327 -0
- app/api/routers/project/helpers.py +167 -0
- app/api/routers/project/inspection_fix.py +330 -0
- app/api/routers/project/manifest.py +605 -0
- app/api/routers/project/models.py +177 -0
- app/api/routers/project/pattern.py +268 -0
- app/api/routers/project/regex.py +358 -0
- app/api/routers/project/scanner.py +157 -0
- app/api/routers/project/schema.py +567 -0
- app/api/routers/project/schema_helpers.py +149 -0
- app/api/routers/project/settings.py +399 -0
- app/api/routers/project/template.py +325 -0
- app/api/routers/project/validation.py +227 -0
- app/api/routers/project/view.py +177 -0
- app/api/routers/project/workspaces.py +167 -0
- app/api/routers/projects/__init__.py +25 -0
- app/api/routers/projects/create.py +121 -0
- app/api/routers/projects/open.py +103 -0
- app/api/routers/projects/scan.py +100 -0
- app/api/routers/validation/__init__.py +39 -0
- app/api/routers/validation/common.py +263 -0
- app/api/routers/validation/content_mode.py +401 -0
- app/api/routers/validation/history.py +235 -0
- app/api/routers/validation/inline_mode.py +196 -0
- app/api/routers/validation/path_mode.py +314 -0
- app/api/routers/validation/router.py +51 -0
- app/api/services/full_validation_response_builder.py +412 -0
- app/api/services/io_error_messages.py +41 -0
- app/api/services/preview_service.py +248 -0
- app/cli/__init__.py +18 -0
- app/cli/__main__.py +30 -0
- app/cli/shared_services/__init__.py +42 -0
- app/cli/shared_services/config_ops.py +581 -0
- app/cli/shared_services/generation_ops.py +160 -0
- app/cli/shared_services/project_ops.py +288 -0
- app/cli/shell/__init__.py +48 -0
- app/cli/shell/commands/__init__.py +63 -0
- app/cli/shell/commands/ai/__init__.py +260 -0
- app/cli/shell/commands/ai/chat.py +331 -0
- app/cli/shell/commands/ai/delete.py +165 -0
- app/cli/shell/commands/ai/diff.py +70 -0
- app/cli/shell/commands/ai/display.py +428 -0
- app/cli/shell/commands/ai/executor.py +225 -0
- app/cli/shell/commands/ai/executor_utils.py +271 -0
- app/cli/shell/commands/ai/generate.py +285 -0
- app/cli/shell/commands/ai/interaction.py +262 -0
- app/cli/shell/commands/ai/migrate.py +281 -0
- app/cli/shell/commands/ai/resolver.py +224 -0
- app/cli/shell/commands/ai/status.py +101 -0
- app/cli/shell/commands/ai/switch.py +158 -0
- app/cli/shell/commands/ai/utils.py +52 -0
- app/cli/shell/commands/base.py +343 -0
- app/cli/shell/commands/config/__init__.py +131 -0
- app/cli/shell/commands/config/base.py +80 -0
- app/cli/shell/commands/config/check.py +246 -0
- app/cli/shell/commands/config/edit.py +148 -0
- app/cli/shell/commands/config/get.py +111 -0
- app/cli/shell/commands/config/init.py +123 -0
- app/cli/shell/commands/config/inspect.py +164 -0
- app/cli/shell/commands/config/list.py +104 -0
- app/cli/shell/commands/config/set.py +119 -0
- app/cli/shell/commands/config/show.py +150 -0
- app/cli/shell/commands/exit.py +67 -0
- app/cli/shell/commands/help.py +104 -0
- app/cli/shell/commands/infer_schema.py +156 -0
- app/cli/shell/commands/open.py +201 -0
- app/cli/shell/commands/project.py +215 -0
- app/cli/shell/commands/provider.py +619 -0
- app/cli/shell/commands/system.py +170 -0
- app/cli/shell/commands/validate.py +455 -0
- app/cli/shell/completer.py +231 -0
- app/cli/shell/config_storage.py +191 -0
- app/cli/shell/exceptions.py +90 -0
- app/cli/shell/formatter.py +371 -0
- app/cli/shell/interactive_menu.py +348 -0
- app/cli/shell/main.py +286 -0
- app/cli/shell/parser.py +266 -0
- app/cli/start.py +180 -0
- app/cli_main.py +52 -0
- app/mcp_server.py +311 -0
- app/shared/core/__init__.py +18 -0
- app/shared/core/app_version.py +59 -0
- app/shared/core/config/__init__.py +119 -0
- app/shared/core/config/server.py +193 -0
- app/shared/core/data_source/__init__.py +106 -0
- app/shared/core/data_source/loader.py +348 -0
- app/shared/core/data_source/loaders/__init__.py +213 -0
- app/shared/core/data_source/loaders/base.py +215 -0
- app/shared/core/data_source/loaders/converter.py +337 -0
- app/shared/core/data_source/loaders/csv_loader.py +252 -0
- app/shared/core/data_source/loaders/excel_loader.py +666 -0
- app/shared/core/data_source/loaders/extractor.py +268 -0
- app/shared/core/data_source/loaders/json_loader.py +354 -0
- app/shared/core/data_source/loaders/registry.py +88 -0
- app/shared/core/data_source/loaders/sql_loader.py +313 -0
- app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
- app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
- app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
- app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
- app/shared/core/data_source/schema_info.py +50 -0
- app/shared/core/data_source/specs/__init__.py +128 -0
- app/shared/core/data_source/specs/base.py +258 -0
- app/shared/core/data_source/specs/csv_source.py +137 -0
- app/shared/core/data_source/specs/excel_source.py +141 -0
- app/shared/core/data_source/specs/file_base.py +252 -0
- app/shared/core/data_source/specs/json_source.py +226 -0
- app/shared/core/data_source/specs/sql_source.py +161 -0
- app/shared/core/encoding.py +36 -0
- app/shared/core/io/__init__.py +24 -0
- app/shared/core/io/yaml.py +379 -0
- app/shared/core/manifest_schema/__init__.py +52 -0
- app/shared/core/manifest_schema/types.py +99 -0
- app/shared/core/manifest_schema/version.py +175 -0
- app/shared/core/patterns/__init__.py +24 -0
- app/shared/core/patterns/loader.py +134 -0
- app/shared/core/patterns/writer.py +244 -0
- app/shared/core/project/__init__.py +47 -0
- app/shared/core/project/constraint/builders/__init__.py +50 -0
- app/shared/core/project/constraint/builders/base.py +100 -0
- app/shared/core/project/constraint/builders/composite.py +77 -0
- app/shared/core/project/constraint/builders/conditional.py +67 -0
- app/shared/core/project/constraint/builders/foreign_key.py +53 -0
- app/shared/core/project/constraint/builders/registry.py +64 -0
- app/shared/core/project/constraint/builders/scripted.py +51 -0
- app/shared/core/project/constraint/builders/single_column.py +86 -0
- app/shared/core/project/constraint/builders/unique.py +53 -0
- app/shared/core/project/constraint/factory.py +170 -0
- app/shared/core/project/constraint/reader.py +214 -0
- app/shared/core/project/constraint/registry.py +233 -0
- app/shared/core/project/constraint/types/__init__.py +63 -0
- app/shared/core/project/constraint/types/constraint_file.py +261 -0
- app/shared/core/project/constraint/types/refs.py +460 -0
- app/shared/core/project/constraint/types.py +28 -0
- app/shared/core/project/constraint/writer.py +181 -0
- app/shared/core/project/loader/__init__.py +56 -0
- app/shared/core/project/loader/loader.py +30 -0
- app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
- app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
- app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
- app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
- app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
- app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
- app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
- app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
- app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
- app/shared/core/project/loader/loader_parts/main.py +432 -0
- app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
- app/shared/core/project/loader/loader_parts/runtime.py +125 -0
- app/shared/core/project/loader/types.py +246 -0
- app/shared/core/project/manifest/coverage.py +391 -0
- app/shared/core/project/manifest/reader.py +260 -0
- app/shared/core/project/manifest/types.py +91 -0
- app/shared/core/project/manifest/types_parts/__init__.py +60 -0
- app/shared/core/project/manifest/types_parts/constants.py +40 -0
- app/shared/core/project/manifest/types_parts/data_source.py +68 -0
- app/shared/core/project/manifest/types_parts/info.py +59 -0
- app/shared/core/project/manifest/types_parts/manifest.py +294 -0
- app/shared/core/project/manifest/types_parts/refs.py +153 -0
- app/shared/core/project/manifest/types_parts/settings.py +92 -0
- app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
- app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
- app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
- app/shared/core/project/manifest/types_parts/template.py +66 -0
- app/shared/core/project/manifest/writer.py +262 -0
- app/shared/core/project/manual_data/__init__.py +27 -0
- app/shared/core/project/manual_data/types.py +75 -0
- app/shared/core/project/regex/reader.py +197 -0
- app/shared/core/project/regex/types.py +405 -0
- app/shared/core/project/regex/writer.py +123 -0
- app/shared/core/project/schema/reader.py +170 -0
- app/shared/core/project/schema/types.py +47 -0
- app/shared/core/project/schema/types_parts/__init__.py +36 -0
- app/shared/core/project/schema/types_parts/column.py +174 -0
- app/shared/core/project/schema/types_parts/column_utils.py +72 -0
- app/shared/core/project/schema/types_parts/constraint.py +165 -0
- app/shared/core/project/schema/types_parts/schema_id.py +66 -0
- app/shared/core/project/schema/types_parts/source.py +255 -0
- app/shared/core/project/schema/types_parts/source_options.py +347 -0
- app/shared/core/project/schema/types_parts/table.py +230 -0
- app/shared/core/project/schema/writer.py +139 -0
- app/shared/core/project/schema_ref_check.py +95 -0
- app/shared/core/project/template/__init__.py +27 -0
- app/shared/core/project/template/expander.py +263 -0
- app/shared/core/project/template/reader.py +120 -0
- app/shared/core/project/template/types.py +114 -0
- app/shared/core/project/transform/reader.py +76 -0
- app/shared/core/project/transform/types.py +116 -0
- app/shared/core/project/transform/writer.py +84 -0
- app/shared/core/pydantic_messages.py +59 -0
- app/shared/core/reporter/__init__.py +46 -0
- app/shared/core/reporter/reporter.py +220 -0
- app/shared/core/reporter/reporters/__init__.py +65 -0
- app/shared/core/reporter/reporters/base.py +188 -0
- app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
- app/shared/core/reporter/reporters/email_reporter.py +271 -0
- app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
- app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
- app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
- app/shared/core/utils/__init__.py +18 -0
- app/shared/core/utils/path_utils.py +60 -0
- app/shared/core/utils/regex_extract.py +113 -0
- app/shared/domain/__init__.py +114 -0
- app/shared/domain/constraints/__init__.py +76 -0
- app/shared/domain/constraints/allowed_values.py +211 -0
- app/shared/domain/constraints/base.py +141 -0
- app/shared/domain/constraints/charset.py +337 -0
- app/shared/domain/constraints/composite.py +174 -0
- app/shared/domain/constraints/condition_registry.py +130 -0
- app/shared/domain/constraints/conditional.py +629 -0
- app/shared/domain/constraints/date_logic.py +731 -0
- app/shared/domain/constraints/foreign_key.py +261 -0
- app/shared/domain/constraints/key_normalization.py +56 -0
- app/shared/domain/constraints/not_null.py +185 -0
- app/shared/domain/constraints/range.py +360 -0
- app/shared/domain/constraints/regex.py +218 -0
- app/shared/domain/constraints/scripted.py +276 -0
- app/shared/domain/constraints/unique.py +233 -0
- app/shared/domain/data_engine.py +384 -0
- app/shared/domain/data_types.py +113 -0
- app/shared/domain/data_types_parts/__init__.py +67 -0
- app/shared/domain/data_types_parts/base.py +204 -0
- app/shared/domain/data_types_parts/composite.py +250 -0
- app/shared/domain/data_types_parts/expression.py +202 -0
- app/shared/domain/data_types_parts/extracted.py +106 -0
- app/shared/domain/data_types_parts/json_types.py +234 -0
- app/shared/domain/data_types_parts/scalars.py +719 -0
- app/shared/domain/data_types_parts/sequence.py +129 -0
- app/shared/domain/dataset_schema.py +48 -0
- app/shared/domain/eval_sandbox.py +63 -0
- app/shared/domain/expression_system.py +366 -0
- app/shared/domain/regex_flags.py +53 -0
- app/shared/domain/schema/builder.py +223 -0
- app/shared/domain/schema/models.py +339 -0
- app/shared/domain/transforms/__init__.py +28 -0
- app/shared/domain/transforms/aggregate.py +141 -0
- app/shared/domain/transforms/base.py +171 -0
- app/shared/domain/transforms/cast_type.py +120 -0
- app/shared/domain/transforms/concat.py +103 -0
- app/shared/domain/transforms/conditional_assign.py +114 -0
- app/shared/domain/transforms/date_format.py +81 -0
- app/shared/domain/transforms/digits.py +77 -0
- app/shared/domain/transforms/drop_duplicates.py +94 -0
- app/shared/domain/transforms/fill_na.py +94 -0
- app/shared/domain/transforms/filter_rows.py +97 -0
- app/shared/domain/transforms/lookup.py +78 -0
- app/shared/domain/transforms/lower_case.py +70 -0
- app/shared/domain/transforms/map_value.py +90 -0
- app/shared/domain/transforms/math_expr.py +112 -0
- app/shared/domain/transforms/modulo.py +82 -0
- app/shared/domain/transforms/regex_extract.py +103 -0
- app/shared/domain/transforms/registry.py +95 -0
- app/shared/domain/transforms/replace.py +88 -0
- app/shared/domain/transforms/sort_rows.py +94 -0
- app/shared/domain/transforms/string_split.py +84 -0
- app/shared/domain/transforms/strip.py +73 -0
- app/shared/domain/transforms/substring.py +92 -0
- app/shared/domain/transforms/upper_case.py +70 -0
- app/shared/domain/transforms/weighted_sum.py +104 -0
- app/shared/domain/validation_constraints.py +69 -0
- app/shared/services/__init__.py +62 -0
- app/shared/services/ai/__init__.py +48 -0
- app/shared/services/ai/agent/__init__.py +40 -0
- app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
- app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
- app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
- app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
- app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
- app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
- app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
- app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
- app/shared/services/ai/agent/executor.py +554 -0
- app/shared/services/ai/agent/memory.py +256 -0
- app/shared/services/ai/agent/planner.py +282 -0
- app/shared/services/ai/agent/tool_registry.py +296 -0
- app/shared/services/ai/agent/tools/__init__.py +32 -0
- app/shared/services/ai/agent/tools/config_generate.py +133 -0
- app/shared/services/ai/agent/tools/config_refine.py +109 -0
- app/shared/services/ai/agent/tools/config_validate.py +403 -0
- app/shared/services/ai/agent/tools/merge_results.py +136 -0
- app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
- app/shared/services/ai/agent/tools/script_parse.py +359 -0
- app/shared/services/ai/agent/types.py +138 -0
- app/shared/services/ai/chat_agent_runner.py +596 -0
- app/shared/services/ai/chat_orchestrator.py +725 -0
- app/shared/services/ai/failure_messages.py +65 -0
- app/shared/services/ai/job_storage.py +274 -0
- app/shared/services/ai/migrate_service.py +550 -0
- app/shared/services/ai/streaming/__init__.py +65 -0
- app/shared/services/ai/streaming/event_journal.py +197 -0
- app/shared/services/ai/streaming/orchestrator.py +262 -0
- app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
- app/shared/services/ai/streaming/sse_response.py +148 -0
- app/shared/services/ai/streaming/types.py +73 -0
- app/shared/services/ai/types.py +213 -0
- app/shared/services/ai/utils.py +322 -0
- app/shared/services/diff/config_diff.py +320 -0
- app/shared/services/hardware.py +237 -0
- app/shared/services/llm/__init__.py +62 -0
- app/shared/services/llm/actions/__init__.py +42 -0
- app/shared/services/llm/actions/_canvas_validator.py +147 -0
- app/shared/services/llm/actions/_constraint_validator.py +401 -0
- app/shared/services/llm/actions/_regex_validator.py +85 -0
- app/shared/services/llm/actions/_schema_validator.py +82 -0
- app/shared/services/llm/actions/_settings_validator.py +76 -0
- app/shared/services/llm/actions/_transform_validator.py +78 -0
- app/shared/services/llm/actions/action_handlers.py +400 -0
- app/shared/services/llm/actions/action_parser.py +63 -0
- app/shared/services/llm/actions/action_processor.py +468 -0
- app/shared/services/llm/actions/action_validator.py +311 -0
- app/shared/services/llm/actions/diff_compute.py +195 -0
- app/shared/services/llm/actions/regex_handlers.py +284 -0
- app/shared/services/llm/actions/registry.py +336 -0
- app/shared/services/llm/actions/schema_handlers.py +335 -0
- app/shared/services/llm/actions/settings_handlers.py +180 -0
- app/shared/services/llm/actions/specs.py +259 -0
- app/shared/services/llm/actions/transform_handlers.py +279 -0
- app/shared/services/llm/actions/validation_types.py +147 -0
- app/shared/services/llm/cache/__init__.py +18 -0
- app/shared/services/llm/cache/response_cache.py +80 -0
- app/shared/services/llm/chat/__init__.py +35 -0
- app/shared/services/llm/chat/chat_system_prompt.py +561 -0
- app/shared/services/llm/chat/response_parser.py +296 -0
- app/shared/services/llm/config/__init__.py +45 -0
- app/shared/services/llm/config/crypto.py +135 -0
- app/shared/services/llm/config/loader.py +258 -0
- app/shared/services/llm/config/models.py +202 -0
- app/shared/services/llm/config/presets.py +128 -0
- app/shared/services/llm/config_generator.py +163 -0
- app/shared/services/llm/constraints/__init__.py +41 -0
- app/shared/services/llm/constraints/constraint_builder.py +177 -0
- app/shared/services/llm/constraints/constraint_deletion.py +84 -0
- app/shared/services/llm/constraints/constraint_id.py +174 -0
- app/shared/services/llm/constraints/frontend_instructions.py +333 -0
- app/shared/services/llm/constraints/inline_batch.py +279 -0
- app/shared/services/llm/discovery/__init__.py +35 -0
- app/shared/services/llm/discovery/scanner.py +181 -0
- app/shared/services/llm/generation/__init__.py +42 -0
- app/shared/services/llm/generation/agent_wiring.py +156 -0
- app/shared/services/llm/generation/config_builder.py +386 -0
- app/shared/services/llm/generation/errors.py +36 -0
- app/shared/services/llm/generation/existing_config.py +99 -0
- app/shared/services/llm/generation/profiler.py +279 -0
- app/shared/services/llm/generation/prompt_builder.py +199 -0
- app/shared/services/llm/generation/response_parser.py +144 -0
- app/shared/services/llm/generation/service.py +622 -0
- app/shared/services/llm/models.py +104 -0
- app/shared/services/llm/providers/__init__.py +48 -0
- app/shared/services/llm/providers/base.py +310 -0
- app/shared/services/llm/providers/cached_provider.py +90 -0
- app/shared/services/llm/providers/ollama.py +498 -0
- app/shared/services/llm/providers/openai.py +334 -0
- app/shared/services/llm/providers/registry.py +108 -0
- app/shared/services/llm/schema_resolver.py +141 -0
- app/shared/services/llm/suggestion_utils.py +219 -0
- app/shared/services/llm/validate_executor.py +163 -0
- app/shared/services/llm/yaml_io.py +406 -0
- app/shared/services/preview/__init__.py +18 -0
- app/shared/services/preview/loader.py +145 -0
- app/shared/services/preview/path_validation.py +138 -0
- app/shared/services/project_loader.py +57 -0
- app/shared/services/schema_inference.py +260 -0
- app/shared/services/schema_runtime_builder.py +120 -0
- app/shared/services/validation/__init__.py +52 -0
- app/shared/services/validation/chunked_loader.py +509 -0
- app/shared/services/validation/dag/__init__.py +35 -0
- app/shared/services/validation/dag/builder.py +141 -0
- app/shared/services/validation/dag/executor.py +225 -0
- app/shared/services/validation/dag/sorter.py +77 -0
- app/shared/services/validation/data_loader.py +231 -0
- app/shared/services/validation/engine.py +446 -0
- app/shared/services/validation/executor.py +1030 -0
- app/shared/services/validation/extractors.py +266 -0
- app/shared/services/validation/history.py +209 -0
- app/shared/services/validation/json_payload.py +136 -0
- app/shared/services/validation/loader.py +152 -0
- app/shared/services/validation/memory_monitor.py +179 -0
- app/shared/services/validation/postprocess.py +208 -0
- app/shared/services/validation/progress.py +64 -0
- app/shared/services/validation/report_export.py +222 -0
- app/shared/services/validation/resolver.py +180 -0
- app/shared/services/validation/service.py +418 -0
- app/shared/services/validation/types.py +238 -0
- app/shared/services/validation/validators/__init__.py +36 -0
- app/shared/services/validation/validators/adapter.py +182 -0
- app/shared/services/validation/validators/base.py +332 -0
- app/shared/services/validation/validators/composite.py +233 -0
- app/shared/services/validation/validators/date_logic.py +276 -0
- app/start_server.py +133 -0
- precis_cli-0.1.3.dist-info/METADATA +180 -0
- precis_cli-0.1.3.dist-info/RECORD +442 -0
- precis_cli-0.1.3.dist-info/WHEEL +5 -0
- precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
- precis_cli-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
"""
|
|
17
|
+
@fileoverview LLM 响应解析器模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 解析 LLM 返回的 JSON 响应文本(健壮版本)
|
|
21
|
+
- 支持多种格式:标准 JSON、Markdown 代码块、单引号伪 JSON、截断 JSON
|
|
22
|
+
- 验证响应格式和动作有效性
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from app.shared.services.llm.actions.registry import (
|
|
33
|
+
ALL_ACTION_TYPES,
|
|
34
|
+
ALL_CONSTRAINT_TYPES,
|
|
35
|
+
SPEC_FIELD_FOR,
|
|
36
|
+
TRANSFORM_SUB_TYPES,
|
|
37
|
+
)
|
|
38
|
+
from app.shared.services.llm.suggestion_utils import normalize_constraint_type
|
|
39
|
+
from app.shared.services.llm.yaml_io import ActionParseError
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ActionParser:
|
|
45
|
+
"""LLM 响应动作解析器。"""
|
|
46
|
+
|
|
47
|
+
# 以下白名单均从动作注册表(单一事实源)派生,禁止本地硬编码。
|
|
48
|
+
VALID_CONSTRAINT_TYPES = sorted(ALL_CONSTRAINT_TYPES)
|
|
49
|
+
VALID_TRANSFORM_TYPES = sorted(TRANSFORM_SUB_TYPES)
|
|
50
|
+
VALID_ACTION_TYPES = list(ALL_ACTION_TYPES)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def parse_llm_response(response_text: str) -> dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
@methoddesc 解析 LLM 响应文本为结构化字典
|
|
56
|
+
|
|
57
|
+
业务用途:
|
|
58
|
+
- 接收 LLM 返回的原始字符串,依次尝试 5 种解析策略:
|
|
59
|
+
直接解析 → 清理 Markdown → 提取 JSON 块 → 修复单引号 → 截断恢复
|
|
60
|
+
- 只要任一策略成功即返回结果;全部失败抛出 ActionParseError
|
|
61
|
+
|
|
62
|
+
参数:
|
|
63
|
+
response_text: LLM 原始响应文本
|
|
64
|
+
|
|
65
|
+
返回:
|
|
66
|
+
解析后的字典
|
|
67
|
+
|
|
68
|
+
异常:
|
|
69
|
+
ActionParseError: 响应文本为空或所有策略都失败
|
|
70
|
+
"""
|
|
71
|
+
if not response_text or not response_text.strip():
|
|
72
|
+
raise ActionParseError("响应文本为空")
|
|
73
|
+
|
|
74
|
+
original_text = response_text.strip()
|
|
75
|
+
parse_attempts = [
|
|
76
|
+
("直接解析", lambda t: t),
|
|
77
|
+
("清理 Markdown", ActionParser._clean_json_text),
|
|
78
|
+
("提取 JSON 块", ActionParser._extract_json_block),
|
|
79
|
+
("修复单引号", ActionParser._fix_single_quotes),
|
|
80
|
+
("截断恢复", lambda t: ActionParser._try_recover_truncated(t)[0]),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
last_error = None
|
|
84
|
+
for attempt_name, cleaner in parse_attempts:
|
|
85
|
+
try:
|
|
86
|
+
cleaned = cleaner(original_text)
|
|
87
|
+
if not cleaned:
|
|
88
|
+
continue
|
|
89
|
+
parsed = json.loads(cleaned)
|
|
90
|
+
if isinstance(parsed, dict):
|
|
91
|
+
if attempt_name != "直接解析":
|
|
92
|
+
logger.debug(f"JSON 解析成功使用策略: {attempt_name}")
|
|
93
|
+
return parsed
|
|
94
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
95
|
+
last_error = e
|
|
96
|
+
continue
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug(f"解析策略 '{attempt_name}' 失败: {e}")
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
logger.error(f"原始文本前 500 字符: {original_text[:500]}")
|
|
102
|
+
raise ActionParseError(f"无法解析 AI 响应: {last_error}") from last_error
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _clean_json_text(text: str) -> str:
|
|
106
|
+
"""
|
|
107
|
+
@methoddesc 清理 Markdown 包裹
|
|
108
|
+
|
|
109
|
+
业务用途:
|
|
110
|
+
- 去除 ```json ... ``` 或 ``` ... ``` 的 Markdown 代码块包裹
|
|
111
|
+
- 不处理 Markdown 内部的内容
|
|
112
|
+
|
|
113
|
+
参数:
|
|
114
|
+
text: 原始文本
|
|
115
|
+
|
|
116
|
+
返回:
|
|
117
|
+
去除 Markdown 包裹后的文本
|
|
118
|
+
"""
|
|
119
|
+
text = text.strip()
|
|
120
|
+
if text.startswith("```"):
|
|
121
|
+
lines = text.split("\n")
|
|
122
|
+
if len(lines) >= 2:
|
|
123
|
+
text = "\n".join(lines[1:])
|
|
124
|
+
if text.endswith("```"):
|
|
125
|
+
text = text[:-3]
|
|
126
|
+
text = text.strip()
|
|
127
|
+
return text
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _extract_json_block(text: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
@methoddesc 从文本中提取 JSON 块
|
|
133
|
+
|
|
134
|
+
业务用途:
|
|
135
|
+
- 优先匹配 ```json ... ``` 或 ``` ... ``` 包裹的代码块
|
|
136
|
+
- 若无代码块,则提取最外层的 { } 或 [ ] 子串
|
|
137
|
+
- 用于处理 LLM 在 JSON 外附带解释文字的场景
|
|
138
|
+
|
|
139
|
+
参数:
|
|
140
|
+
text: 原始文本
|
|
141
|
+
|
|
142
|
+
返回:
|
|
143
|
+
提取出的 JSON 文本
|
|
144
|
+
"""
|
|
145
|
+
text = text.strip()
|
|
146
|
+
code_block_pattern = r"```(?:json)?\s*\n?(.*?)\n?```"
|
|
147
|
+
matches = re.findall(code_block_pattern, text, re.DOTALL)
|
|
148
|
+
if matches:
|
|
149
|
+
return str(max(matches, key=len)).strip()
|
|
150
|
+
first_brace = text.find("{")
|
|
151
|
+
last_brace = text.rfind("}")
|
|
152
|
+
if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
|
|
153
|
+
return text[first_brace : last_brace + 1]
|
|
154
|
+
first_bracket = text.find("[")
|
|
155
|
+
last_bracket = text.rfind("]")
|
|
156
|
+
if first_bracket != -1 and last_bracket != -1 and first_bracket < last_bracket:
|
|
157
|
+
return text[first_bracket : last_bracket + 1]
|
|
158
|
+
return text
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _fix_single_quotes(text: str) -> str:
|
|
162
|
+
"""
|
|
163
|
+
@methoddesc 将 JSON 中的单引号替换为双引号
|
|
164
|
+
|
|
165
|
+
业务用途:
|
|
166
|
+
- 部分 LLM 会输出 Python 风格单引号 JSON(标准 JSON 要求双引号)
|
|
167
|
+
- 仅在内容中不包含双引号时才替换,避免破坏正常字段
|
|
168
|
+
|
|
169
|
+
参数:
|
|
170
|
+
text: 原始文本
|
|
171
|
+
|
|
172
|
+
返回:
|
|
173
|
+
修复单引号后的文本
|
|
174
|
+
"""
|
|
175
|
+
text = ActionParser._extract_json_block(text)
|
|
176
|
+
|
|
177
|
+
def replace_quotes(match: re.Match[str]) -> str:
|
|
178
|
+
content = match.group(1)
|
|
179
|
+
if '"' not in content:
|
|
180
|
+
return f'"{content}"'
|
|
181
|
+
return match.group(0)
|
|
182
|
+
|
|
183
|
+
return re.sub(r"'([^']*)'", replace_quotes, text)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _try_recover_truncated(text: str) -> tuple[str, bool]:
|
|
187
|
+
"""
|
|
188
|
+
@methoddesc 尝试恢复被 LLM 截断的 JSON 响应
|
|
189
|
+
|
|
190
|
+
业务用途:
|
|
191
|
+
- 当 LLM 输出超过 token 限制被截断时,自动补全缺失的 }, ], "
|
|
192
|
+
- 启发式策略:补齐未闭合的大括号/方括号;若引号成奇数,则截断到最近的 , : { [ 之后
|
|
193
|
+
|
|
194
|
+
参数:
|
|
195
|
+
text: 截断的文本
|
|
196
|
+
|
|
197
|
+
返回:
|
|
198
|
+
(恢复后的文本, 是否进行了恢复) 二元组
|
|
199
|
+
"""
|
|
200
|
+
text = text.strip()
|
|
201
|
+
open_braces = text.count("{") - text.count("}")
|
|
202
|
+
open_brackets = text.count("[") - text.count("]")
|
|
203
|
+
result = text
|
|
204
|
+
recovered = False
|
|
205
|
+
if open_braces > 0:
|
|
206
|
+
result += "}" * open_braces
|
|
207
|
+
recovered = True
|
|
208
|
+
if open_brackets > 0:
|
|
209
|
+
result += "]" * open_brackets
|
|
210
|
+
recovered = True
|
|
211
|
+
if result.count('"') % 2 == 1:
|
|
212
|
+
last_quote = result.rfind('"')
|
|
213
|
+
if last_quote > 0:
|
|
214
|
+
for char in [",", ":", "{", "[", " "]:
|
|
215
|
+
pos = result.rfind(char, 0, last_quote)
|
|
216
|
+
if pos > 0:
|
|
217
|
+
result = result[: pos + 1]
|
|
218
|
+
recovered = True
|
|
219
|
+
break
|
|
220
|
+
if recovered:
|
|
221
|
+
logger.warning("[ResponseParser] 响应被截断,已自动恢复。恢复后的内容可能与 LLM 原始输出不同。")
|
|
222
|
+
return result, recovered
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def validate_response(response: dict[str, Any]) -> bool:
|
|
226
|
+
"""
|
|
227
|
+
@methoddesc 校验解析后的响应字典结构
|
|
228
|
+
|
|
229
|
+
业务用途:
|
|
230
|
+
- 确保 response 包含必要的 "reply" 和 "actions" 字段
|
|
231
|
+
- 若 actions 缺失或为 None,则规范化为空列表
|
|
232
|
+
- 调用方可基于校验结果决定是否进入后续动作执行
|
|
233
|
+
|
|
234
|
+
参数:
|
|
235
|
+
response: 解析后的字典(会被就地修改)
|
|
236
|
+
|
|
237
|
+
返回:
|
|
238
|
+
True 表示通过校验
|
|
239
|
+
"""
|
|
240
|
+
if "reply" not in response:
|
|
241
|
+
return False
|
|
242
|
+
if "actions" not in response:
|
|
243
|
+
return False
|
|
244
|
+
actions = response.get("actions", [])
|
|
245
|
+
if actions is None:
|
|
246
|
+
response["actions"] = []
|
|
247
|
+
actions = []
|
|
248
|
+
elif not isinstance(actions, list):
|
|
249
|
+
return False
|
|
250
|
+
for action in actions:
|
|
251
|
+
if not ActionParser._validate_action(action):
|
|
252
|
+
return False
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _validate_action(action: dict[str, Any]) -> bool:
|
|
257
|
+
if not isinstance(action, dict):
|
|
258
|
+
return False
|
|
259
|
+
if "actionType" not in action:
|
|
260
|
+
return False
|
|
261
|
+
action_type = action.get("actionType")
|
|
262
|
+
if action_type not in ActionParser.VALID_ACTION_TYPES:
|
|
263
|
+
return False
|
|
264
|
+
# VALIDATE_PROJECT / UPDATE_SETTINGS 的 spec 可选(校验可不带 tableName,设置可空壳)
|
|
265
|
+
# 其余动作必须携带对应 spec 字段
|
|
266
|
+
if action_type in ("VALIDATE_PROJECT", "UPDATE_SETTINGS"):
|
|
267
|
+
return True
|
|
268
|
+
# 从注册表查 spec 字段名(None 表示无 spec 要求)
|
|
269
|
+
spec_field = SPEC_FIELD_FOR.get(action_type)
|
|
270
|
+
if spec_field is None:
|
|
271
|
+
return True
|
|
272
|
+
if spec_field not in action:
|
|
273
|
+
return False
|
|
274
|
+
# 约束操作额外校验 type 字段在白名单内
|
|
275
|
+
if spec_field == "constraintSpec":
|
|
276
|
+
spec = action.get("constraintSpec", {})
|
|
277
|
+
# §2.9: 校验前过 normalize_constraint_type 归一(与写盘路径同一函数,
|
|
278
|
+
# 52662373 引入)——legacy 路径对大小写/别名零容忍会把小写 not_null 的
|
|
279
|
+
# 整条响应判非法。归一后写回正名,后续 handler 零改动拿到 PascalCase。
|
|
280
|
+
normalized = normalize_constraint_type(str(spec.get("type") or ""))
|
|
281
|
+
spec["type"] = normalized
|
|
282
|
+
if normalized not in ActionParser.VALID_CONSTRAINT_TYPES:
|
|
283
|
+
return False
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def parse_actions(llm_response: dict[str, Any]) -> list[dict[str, Any]]:
|
|
288
|
+
actions = llm_response.get("actions", [])
|
|
289
|
+
if not isinstance(actions, list):
|
|
290
|
+
return []
|
|
291
|
+
return [a for a in actions if ActionParser._validate_action(a)]
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def extract_reply(llm_response: dict[str, Any]) -> str:
|
|
295
|
+
reply = llm_response.get("reply", "")
|
|
296
|
+
return str(reply) if reply is not None else ""
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
"""
|
|
17
|
+
@fileoverview AI 配置模块聚合导出
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 聚合导出 AI 配置相关的模型、加载器
|
|
21
|
+
- 为上层 LLM 服务提供统一的配置入口
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 通过 __init__.py 聚合子模块公共接口,降低外部导入复杂度
|
|
25
|
+
- 遵循显式导出原则,使用 __all__ 控制公开 API
|
|
26
|
+
|
|
27
|
+
输入示例:
|
|
28
|
+
from app.shared.services.llm.config import AIConfig, ConfigLoader
|
|
29
|
+
|
|
30
|
+
输出示例:
|
|
31
|
+
AIConfig 实例与 ConfigLoader 实例可直接用于 provider 配置加载
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from .loader import ConfigLoader, loader
|
|
35
|
+
from .models import AIConfig, AIProvider, DeploymentType, NetworkConfig, ProviderType
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AIConfig",
|
|
39
|
+
"AIProvider",
|
|
40
|
+
"ProviderType",
|
|
41
|
+
"DeploymentType",
|
|
42
|
+
"NetworkConfig",
|
|
43
|
+
"ConfigLoader",
|
|
44
|
+
"loader",
|
|
45
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
"""
|
|
17
|
+
@fileoverview API Key 加密存储模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 使用 Fernet 对称加密保护 api_key 字段
|
|
21
|
+
- 密钥文件存储在 ~/.precis/.precis_key
|
|
22
|
+
- 加密后的值以 "enc:" 前缀标记,与明文和环境变量引用区分
|
|
23
|
+
- 首次使用自动生成密钥文件
|
|
24
|
+
- 向后兼容:明文 api_key 在 load 时正常读取,下次 save 时自动加密
|
|
25
|
+
|
|
26
|
+
架构设计:
|
|
27
|
+
- 加密/解密对 ConfigLoader 透明:save 时加密,load 时解密
|
|
28
|
+
- 密钥与用户绑定,不同操作系统的用户各有独立密钥
|
|
29
|
+
- _ENC_PREFIX = "enc:" 前缀用于区分加密值与明文值
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import os
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from cryptography.fernet import Fernet
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_ENC_PREFIX = "enc:"
|
|
43
|
+
|
|
44
|
+
_KEY_FILE_NAME = ".precis_key"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _key_file_path() -> Path:
|
|
48
|
+
"""密钥文件路径: ~/.precis/.precis_key"""
|
|
49
|
+
return Path.home() / ".precis" / _KEY_FILE_NAME
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_or_create_key() -> bytes:
|
|
53
|
+
"""
|
|
54
|
+
@methoddesc 获取或创建加密密钥
|
|
55
|
+
|
|
56
|
+
首次使用时自动生成 Fernet 密钥并保存到 ~/.precis/.precis_key。
|
|
57
|
+
密钥文件权限设为 0600(仅用户可读写),Windows 上无此限制。
|
|
58
|
+
|
|
59
|
+
返回:
|
|
60
|
+
Fernet 密钥字节(32 字节,base64 编码为 44 字节)
|
|
61
|
+
"""
|
|
62
|
+
key_path = _key_file_path()
|
|
63
|
+
|
|
64
|
+
if key_path.exists():
|
|
65
|
+
try:
|
|
66
|
+
key_data = key_path.read_bytes().strip()
|
|
67
|
+
if key_data:
|
|
68
|
+
return key_data
|
|
69
|
+
except Exception:
|
|
70
|
+
logger.warning("读取密钥文件失败,将重新生成")
|
|
71
|
+
|
|
72
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
new_key = Fernet.generate_key()
|
|
74
|
+
key_path.write_bytes(new_key)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
os.chmod(str(key_path), 0o600)
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
logger.info("已生成新的 API Key 加密密钥: %s", key_path)
|
|
82
|
+
return new_key
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_fernet() -> Fernet:
|
|
86
|
+
"""获取 Fernet 实例(懒加载密钥)"""
|
|
87
|
+
key = _get_or_create_key()
|
|
88
|
+
return Fernet(key)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def encrypt_api_key(plain: str) -> str:
|
|
92
|
+
"""
|
|
93
|
+
@methoddesc 加密 API Key
|
|
94
|
+
|
|
95
|
+
参数:
|
|
96
|
+
plain: 明文 API Key
|
|
97
|
+
|
|
98
|
+
返回:
|
|
99
|
+
"enc:" 前缀 + base64 编码的加密值
|
|
100
|
+
"""
|
|
101
|
+
if not plain:
|
|
102
|
+
return plain
|
|
103
|
+
f = _get_fernet()
|
|
104
|
+
encrypted = f.encrypt(plain.encode("utf-8"))
|
|
105
|
+
return _ENC_PREFIX + encrypted.decode("ascii")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def decrypt_api_key(encrypted: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
@methoddesc 解密 API Key
|
|
111
|
+
|
|
112
|
+
参数:
|
|
113
|
+
encrypted: "enc:" 前缀的加密值,或明文值(向后兼容)
|
|
114
|
+
|
|
115
|
+
返回:
|
|
116
|
+
明文 API Key
|
|
117
|
+
"""
|
|
118
|
+
if not encrypted:
|
|
119
|
+
return encrypted
|
|
120
|
+
|
|
121
|
+
if not encrypted.startswith(_ENC_PREFIX):
|
|
122
|
+
return encrypted
|
|
123
|
+
|
|
124
|
+
cipher_text = encrypted[len(_ENC_PREFIX) :]
|
|
125
|
+
try:
|
|
126
|
+
f = _get_fernet()
|
|
127
|
+
return f.decrypt(cipher_text.encode("ascii")).decode("utf-8")
|
|
128
|
+
except Exception:
|
|
129
|
+
logger.warning("API Key 解密失败,可能密钥文件已更换")
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def is_encrypted(value: str | None) -> bool:
|
|
134
|
+
"""判断值是否为加密格式"""
|
|
135
|
+
return value is not None and value.startswith(_ENC_PREFIX)
|