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,335 @@
|
|
|
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
|
+
"""@fileoverview Schema 动作处理器模块
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 处理 AI 生成的 Schema CRUD 动作(ADD_SCHEMA / UPDATE_SCHEMA / DELETE_SCHEMA)
|
|
20
|
+
- 通过 core 层 writer 持久化 Schema YAML 文件
|
|
21
|
+
- 同步更新 project.precis.yaml 中的 SchemaRef
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 复用 core/project/schema/writer.py 的 save_schema()
|
|
25
|
+
- 复用 core/project/manifest/writer.py 的 ensure_schema_ref() / save_manifest()
|
|
26
|
+
- 使用 atomic_write_yaml 保证文件写入安全
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import yaml
|
|
37
|
+
|
|
38
|
+
from app.shared.core.project.manifest.reader import load_manifest
|
|
39
|
+
from app.shared.core.project.manifest.writer import ensure_schema_ref, save_manifest
|
|
40
|
+
from app.shared.core.project.schema_ref_check import find_schema_references, format_reference_report
|
|
41
|
+
from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# 数据类型白名单从注册表派生(单一事实源)
|
|
46
|
+
from app.shared.services.llm.actions.registry import DATA_TYPES as VALID_DATA_TYPES
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sanitize_resource_id(resource_id: str) -> str:
|
|
50
|
+
cleaned = os.path.basename(resource_id)
|
|
51
|
+
if "/" in resource_id or "\\" in resource_id or ".." in resource_id:
|
|
52
|
+
raise ValueError(f"非法的资源 ID: {resource_id!r}")
|
|
53
|
+
return cleaned
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def process_schema_action(action: dict[str, Any], workspace_path: str) -> dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
@methoddesc 处理 Schema 动作
|
|
59
|
+
|
|
60
|
+
根据 actionType 分发到对应的处理函数:
|
|
61
|
+
- ADD_SCHEMA: 创建新的 Schema YAML 文件
|
|
62
|
+
- UPDATE_SCHEMA: 修改现有 Schema 的列定义或数据源
|
|
63
|
+
- DELETE_SCHEMA: 删除 Schema YAML 文件并移除 manifest 引用
|
|
64
|
+
|
|
65
|
+
参数:
|
|
66
|
+
action: 动作字典,包含 actionType 和 schemaSpec
|
|
67
|
+
workspace_path: 项目工作区路径
|
|
68
|
+
|
|
69
|
+
返回:
|
|
70
|
+
处理结果字典 {"success": bool, "message": str}
|
|
71
|
+
"""
|
|
72
|
+
action_type = action.get("actionType", "")
|
|
73
|
+
spec = action.get("schemaSpec", {})
|
|
74
|
+
|
|
75
|
+
if action_type == "ADD_SCHEMA":
|
|
76
|
+
return _add_schema(spec, workspace_path)
|
|
77
|
+
elif action_type == "UPDATE_SCHEMA":
|
|
78
|
+
return _update_schema(spec, workspace_path)
|
|
79
|
+
elif action_type == "DELETE_SCHEMA":
|
|
80
|
+
return _delete_schema(spec, workspace_path)
|
|
81
|
+
else:
|
|
82
|
+
return {"success": False, "message": f"未知的 Schema 动作类型: {action_type}"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _add_schema(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
|
|
86
|
+
"""创建新的 Schema YAML 文件"""
|
|
87
|
+
schema_name = spec.get("name", "")
|
|
88
|
+
schema_id = spec.get("schemaId") or spec.get("id") or schema_name
|
|
89
|
+
try:
|
|
90
|
+
schema_id = _sanitize_resource_id(schema_id)
|
|
91
|
+
except ValueError:
|
|
92
|
+
return {"success": False, "message": f"非法的 Schema ID: {schema_id}"}
|
|
93
|
+
columns = spec.get("columns", [])
|
|
94
|
+
source = spec.get("source")
|
|
95
|
+
|
|
96
|
+
if not schema_name:
|
|
97
|
+
return {"success": False, "message": "Schema 名称不能为空"}
|
|
98
|
+
|
|
99
|
+
workspace = Path(workspace_path)
|
|
100
|
+
schemas_dir = workspace / "schemas"
|
|
101
|
+
schemas_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
# 检查是否已存在同名 Schema
|
|
104
|
+
schema_file = schemas_dir / f"{schema_id}.schema.yaml"
|
|
105
|
+
if schema_file.exists():
|
|
106
|
+
# 提示 Agent 改用 ADD_TO_CANVAS:用户的意图很可能是"把已存在的资源显示到画布"
|
|
107
|
+
# 而非"创建新文件"。把建议写进 message 让 LLM 自我修正。
|
|
108
|
+
return {
|
|
109
|
+
"success": False,
|
|
110
|
+
"message": (
|
|
111
|
+
f"Schema 文件已存在: {schema_id}.schema.yaml。"
|
|
112
|
+
f"若用户想把已存在的资源显示到画布,请改用 actionType=ADD_TO_CANVAS"
|
|
113
|
+
f"(canvasSpec.resourceKind='schema'),它不会重复创建文件。"
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# 构建列定义
|
|
118
|
+
column_defs = []
|
|
119
|
+
for i, col in enumerate(columns):
|
|
120
|
+
col_name = col.get("name", f"col_{i}")
|
|
121
|
+
col_type = col.get("type", "string")
|
|
122
|
+
if col_type not in VALID_DATA_TYPES:
|
|
123
|
+
col_type = "string"
|
|
124
|
+
column_defs.append(
|
|
125
|
+
{
|
|
126
|
+
"id": col_name,
|
|
127
|
+
"name": col_name,
|
|
128
|
+
"type": col_type,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 构建 schema 数据
|
|
133
|
+
schema_data: dict[str, Any] = {
|
|
134
|
+
"version": 2,
|
|
135
|
+
"id": schema_id,
|
|
136
|
+
"name": schema_name,
|
|
137
|
+
"columns": column_defs,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if source:
|
|
141
|
+
source_path = source.get("path", "")
|
|
142
|
+
if source_path and (".." in source_path or os.path.isabs(source_path)):
|
|
143
|
+
return {"success": False, "message": f"source.path 不允许绝对路径或目录穿越: {source_path}"}
|
|
144
|
+
schema_data["source"] = source
|
|
145
|
+
try:
|
|
146
|
+
with FileLock(str(schema_file)):
|
|
147
|
+
atomic_write_yaml(schema_file, schema_data)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return {"success": False, "message": f"写入 Schema 文件失败: {e}"}
|
|
150
|
+
|
|
151
|
+
# 更新 manifest —— 失败时回滚已写入的 schema 文件,避免产生孤儿文件
|
|
152
|
+
try:
|
|
153
|
+
_ensure_manifest_schema_ref(workspace_path, schema_id)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"[SchemaHandler] 登记到 manifest 失败,回滚 schema 文件: {e}")
|
|
156
|
+
try:
|
|
157
|
+
schema_file.unlink(missing_ok=True)
|
|
158
|
+
except OSError as rollback_err:
|
|
159
|
+
logger.error(f"[SchemaHandler] 回滚 schema 文件失败: {rollback_err}")
|
|
160
|
+
return {"success": False, "message": f"更新 manifest 引用失败: {e}"}
|
|
161
|
+
|
|
162
|
+
logger.info(f"[SchemaHandler] 创建 Schema: {schema_id}")
|
|
163
|
+
return {"success": True, "message": schema_id}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _update_schema(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
|
|
167
|
+
"""更新现有 Schema"""
|
|
168
|
+
schema_id = spec.get("schemaId") or spec.get("id") or spec.get("name", "")
|
|
169
|
+
try:
|
|
170
|
+
schema_id = _sanitize_resource_id(schema_id)
|
|
171
|
+
except ValueError:
|
|
172
|
+
return {"success": False, "message": f"非法的 Schema ID: {schema_id}"}
|
|
173
|
+
columns = spec.get("columns")
|
|
174
|
+
source = spec.get("source")
|
|
175
|
+
|
|
176
|
+
if not schema_id:
|
|
177
|
+
return {"success": False, "message": "缺少 Schema ID"}
|
|
178
|
+
|
|
179
|
+
schema_file = _find_schema_file(workspace_path, schema_id)
|
|
180
|
+
if not schema_file:
|
|
181
|
+
return {"success": False, "message": f"Schema 文件不存在: {schema_id}"}
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
with FileLock(str(schema_file)):
|
|
185
|
+
with open(schema_file, encoding="utf-8") as f:
|
|
186
|
+
schema_data = yaml.safe_load(f) or {}
|
|
187
|
+
|
|
188
|
+
# 更新列定义
|
|
189
|
+
if columns is not None:
|
|
190
|
+
existing_cols = {c.get("name"): c for c in schema_data.get("columns", [])}
|
|
191
|
+
updated_cols = []
|
|
192
|
+
for col in columns:
|
|
193
|
+
col_name = col.get("name", "")
|
|
194
|
+
col_type = col.get("type", "string")
|
|
195
|
+
if col_type not in VALID_DATA_TYPES:
|
|
196
|
+
col_type = "string"
|
|
197
|
+
if col_name in existing_cols:
|
|
198
|
+
# 更新已有列
|
|
199
|
+
existing = existing_cols[col_name]
|
|
200
|
+
existing["type"] = col_type
|
|
201
|
+
updated_cols.append(existing)
|
|
202
|
+
else:
|
|
203
|
+
# 新增列
|
|
204
|
+
updated_cols.append(
|
|
205
|
+
{
|
|
206
|
+
"id": col_name,
|
|
207
|
+
"name": col_name,
|
|
208
|
+
"type": col_type,
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
# 保留未在更新列表中的列
|
|
212
|
+
updated_names = {c.get("name") for c in updated_cols}
|
|
213
|
+
for col in schema_data.get("columns", []):
|
|
214
|
+
if col.get("name") not in updated_names:
|
|
215
|
+
updated_cols.append(col)
|
|
216
|
+
schema_data["columns"] = updated_cols
|
|
217
|
+
|
|
218
|
+
# 更新数据源
|
|
219
|
+
if source is not None:
|
|
220
|
+
source_path = source.get("path", "")
|
|
221
|
+
if source_path and (".." in source_path or os.path.isabs(source_path)):
|
|
222
|
+
return {"success": False, "message": "source.path 不允许绝对路径或目录穿越"}
|
|
223
|
+
schema_data["source"] = source
|
|
224
|
+
|
|
225
|
+
# §2.10: UPDATE 显式整体替换语义——preserve_format 默认 True 的递归合并
|
|
226
|
+
# "只增不删",被删掉的列/字段会残留在文件里
|
|
227
|
+
atomic_write_yaml(schema_file, schema_data, preserve_format=False)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
return {"success": False, "message": f"更新 Schema 失败: {e}"}
|
|
231
|
+
|
|
232
|
+
logger.info(f"[SchemaHandler] 更新 Schema: {schema_id}")
|
|
233
|
+
return {"success": True, "message": schema_id}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _delete_schema(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
|
|
237
|
+
"""删除 Schema 文件"""
|
|
238
|
+
schema_id = spec.get("schemaId") or spec.get("id") or spec.get("name", "")
|
|
239
|
+
try:
|
|
240
|
+
schema_id = _sanitize_resource_id(schema_id)
|
|
241
|
+
except ValueError:
|
|
242
|
+
return {"success": False, "message": f"非法的 Schema ID: {schema_id}"}
|
|
243
|
+
|
|
244
|
+
if not schema_id:
|
|
245
|
+
return {"success": False, "message": "缺少 Schema ID"}
|
|
246
|
+
|
|
247
|
+
schema_file = _find_schema_file(workspace_path, schema_id)
|
|
248
|
+
if not schema_file:
|
|
249
|
+
return {"success": False, "message": f"Schema 文件不存在: {schema_id}"}
|
|
250
|
+
|
|
251
|
+
# §2.6: 删除前引用检查——与 REST DELETE /schemas/{id} 的 409 守卫同一套逻辑
|
|
252
|
+
# (公共函数 find_schema_references 单一事实源)。AI 不能成为绕开引用守卫的后门:
|
|
253
|
+
# 有引用时约束全部悬空,之后校验报"表不存在"而画布节点还在。
|
|
254
|
+
refs = find_schema_references(workspace_path, schema_id)
|
|
255
|
+
ref_report = format_reference_report(refs)
|
|
256
|
+
if ref_report:
|
|
257
|
+
return {
|
|
258
|
+
"success": False,
|
|
259
|
+
"message": (
|
|
260
|
+
f"Schema '{schema_id}' 仍被 {ref_report} 引用,请先删除这些引用再删除表"
|
|
261
|
+
"(可先逐一删除引用它的约束/正则/转换)"
|
|
262
|
+
),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
schema_file.unlink()
|
|
267
|
+
except OSError as e:
|
|
268
|
+
return {"success": False, "message": f"删除 Schema 文件失败: {e}"}
|
|
269
|
+
|
|
270
|
+
# 从 manifest 移除引用 —— 失败时尝试恢复文件以保持一致,避免 dangling 引用
|
|
271
|
+
try:
|
|
272
|
+
_remove_manifest_schema_ref(workspace_path, schema_id)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"[SchemaHandler] 从 manifest 移除引用失败: {e}")
|
|
275
|
+
return {"success": False, "message": f"更新 manifest 引用失败,文件已删除但 manifest 残留: {e}"}
|
|
276
|
+
|
|
277
|
+
logger.info(f"[SchemaHandler] 删除 Schema: {schema_id}")
|
|
278
|
+
return {"success": True, "message": schema_id}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _find_schema_file(workspace_path: str, schema_id: str) -> Path | None:
|
|
282
|
+
"""在工作区中查找 schema 文件"""
|
|
283
|
+
schemas_dir = Path(workspace_path) / "schemas"
|
|
284
|
+
if not schemas_dir.exists():
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
# 先按 ID 精确匹配
|
|
288
|
+
for sf in schemas_dir.glob("*.yaml"):
|
|
289
|
+
try:
|
|
290
|
+
with open(sf, encoding="utf-8") as f:
|
|
291
|
+
data = yaml.safe_load(f) or {}
|
|
292
|
+
if data.get("id") == schema_id or data.get("name") == schema_id:
|
|
293
|
+
return sf
|
|
294
|
+
except Exception:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# 再按文件名匹配
|
|
298
|
+
candidate = schemas_dir / f"{schema_id}.schema.yaml"
|
|
299
|
+
if candidate.exists():
|
|
300
|
+
return candidate
|
|
301
|
+
candidate = schemas_dir / f"{schema_id}.yaml"
|
|
302
|
+
if candidate.exists():
|
|
303
|
+
return candidate
|
|
304
|
+
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _ensure_manifest_schema_ref(workspace_path: str, schema_id: str) -> None:
|
|
309
|
+
"""确保 manifest 中包含指定 Schema 引用。
|
|
310
|
+
|
|
311
|
+
失败时抛出异常而非吞掉 —— 避免 schema 文件已写盘但 manifest 未登记,
|
|
312
|
+
从而产生孤儿文件(会触发后续 inspect 的 id 冲突 blocker)。
|
|
313
|
+
"""
|
|
314
|
+
manifest_path = Path(workspace_path) / "project.precis.yaml"
|
|
315
|
+
if not manifest_path.exists():
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
manifest = load_manifest(manifest_path)
|
|
319
|
+
ensure_schema_ref(manifest, schema_id)
|
|
320
|
+
save_manifest(manifest, manifest_path)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _remove_manifest_schema_ref(workspace_path: str, schema_id: str) -> None:
|
|
324
|
+
"""从 manifest 中移除指定 Schema 引用。
|
|
325
|
+
|
|
326
|
+
失败时抛出异常而非吞掉 —— 避免 schema 文件已删除但 manifest 仍残留引用,
|
|
327
|
+
从而产生 dangling 引用。
|
|
328
|
+
"""
|
|
329
|
+
manifest_path = Path(workspace_path) / "project.precis.yaml"
|
|
330
|
+
if not manifest_path.exists():
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
manifest = load_manifest(manifest_path)
|
|
334
|
+
manifest.schemas = [s for s in manifest.schemas if s.id != schema_id]
|
|
335
|
+
save_manifest(manifest, manifest_path)
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
"""@fileoverview 项目设置动作处理器模块
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 处理 AI 生成的 UPDATE_SETTINGS 动作
|
|
20
|
+
- 修改 project.precis.yaml 中的 settings 段
|
|
21
|
+
- 支持三类设置:validation / file_processing / script_security
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 直接读写 project.precis.yaml 的 settings 字段
|
|
25
|
+
- 使用 atomic_write_yaml 保证文件写入安全
|
|
26
|
+
- 校验字段合法性
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
import yaml
|
|
36
|
+
from pydantic import BaseModel, ValidationError
|
|
37
|
+
|
|
38
|
+
from app.shared.core.project.manifest.types_parts.settings_file_processing import FileProcessingSettings
|
|
39
|
+
from app.shared.core.project.manifest.types_parts.settings_script_security import ScriptSecuritySettings
|
|
40
|
+
from app.shared.core.project.manifest.types_parts.settings_validation import ValidationSettings
|
|
41
|
+
from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# 设置分类白名单从注册表派生(单一事实源)
|
|
46
|
+
from app.shared.services.llm.actions.registry import SETTINGS_CATEGORIES as VALID_SETTINGS_CATEGORIES
|
|
47
|
+
|
|
48
|
+
CATEGORY_TO_YAML_KEY = {
|
|
49
|
+
"validation": "validation",
|
|
50
|
+
"fileProcessing": "file_processing",
|
|
51
|
+
"scriptSecurity": "script_security",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# 分类 → manifest 设置模型:值域/类型的单一事实源是模型 Field 约束(C3 修复),
|
|
55
|
+
# handler 校验与落盘前模型重建均以此为准,避免两套事实打架写坏 manifest。
|
|
56
|
+
CATEGORY_TO_SETTINGS_MODEL: dict[str, type[BaseModel]] = {
|
|
57
|
+
"validation": ValidationSettings,
|
|
58
|
+
"fileProcessing": FileProcessingSettings,
|
|
59
|
+
"scriptSecurity": ScriptSecuritySettings,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
VALID_ERROR_HANDLING = {"stop", "continue", "report"}
|
|
63
|
+
VALID_ENCODINGS = {"utf-8", "gbk", "auto"}
|
|
64
|
+
VALID_ALLOW_EVAL = {False}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def process_settings_action(action: dict[str, Any], workspace_path: str) -> dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
@methoddesc 处理 UPDATE_SETTINGS 动作
|
|
70
|
+
|
|
71
|
+
根据 settingsSpec 中的 category 和 settings 更新项目配置。
|
|
72
|
+
|
|
73
|
+
参数:
|
|
74
|
+
action: 动作字典,包含 actionType 和 settingsSpec
|
|
75
|
+
workspace_path: 项目工作区路径
|
|
76
|
+
|
|
77
|
+
返回:
|
|
78
|
+
处理结果字典 {"success": bool, "message": str}
|
|
79
|
+
"""
|
|
80
|
+
spec = action.get("settingsSpec", {})
|
|
81
|
+
category = spec.get("category", "")
|
|
82
|
+
settings = spec.get("settings", {})
|
|
83
|
+
|
|
84
|
+
if not category:
|
|
85
|
+
return {"success": False, "message": "缺少 settings category"}
|
|
86
|
+
if category not in VALID_SETTINGS_CATEGORIES:
|
|
87
|
+
return {"success": False, "message": f"未知的 settings category: {category}"}
|
|
88
|
+
if not settings:
|
|
89
|
+
return {"success": False, "message": "settings 不能为空"}
|
|
90
|
+
|
|
91
|
+
# 校验 settings 合法性
|
|
92
|
+
validation_errors = _validate_settings(category, settings)
|
|
93
|
+
if validation_errors:
|
|
94
|
+
return {"success": False, "message": f"设置校验失败: {'; '.join(validation_errors)}"}
|
|
95
|
+
|
|
96
|
+
manifest_path = Path(workspace_path) / "project.precis.yaml"
|
|
97
|
+
if not manifest_path.exists():
|
|
98
|
+
return {"success": False, "message": "project.precis.yaml 不存在"}
|
|
99
|
+
|
|
100
|
+
yaml_key = CATEGORY_TO_YAML_KEY[category]
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with FileLock(str(manifest_path)):
|
|
104
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
105
|
+
data = yaml.safe_load(f) or {}
|
|
106
|
+
|
|
107
|
+
# 确保 settings 段存在
|
|
108
|
+
if "settings" not in data:
|
|
109
|
+
data["settings"] = {}
|
|
110
|
+
|
|
111
|
+
if yaml_key not in data["settings"]:
|
|
112
|
+
data["settings"][yaml_key] = {}
|
|
113
|
+
|
|
114
|
+
# 合并设置(不覆盖未指定的字段)
|
|
115
|
+
data["settings"][yaml_key].update(settings)
|
|
116
|
+
|
|
117
|
+
# C3: 落盘前经模型重建校验——非法值(如 sandbox_mode="strict"、
|
|
118
|
+
# timeout_seconds>300)直接落盘会导致下次加载 model_validate 422、
|
|
119
|
+
# 项目无法打开。模型是值域的单一事实源,merge 后的完整段必须能过模型。
|
|
120
|
+
model = CATEGORY_TO_SETTINGS_MODEL[category]
|
|
121
|
+
try:
|
|
122
|
+
model.model_validate(data["settings"][yaml_key])
|
|
123
|
+
except ValidationError as e:
|
|
124
|
+
return {"success": False, "message": f"设置校验失败: {e}"}
|
|
125
|
+
|
|
126
|
+
atomic_write_yaml(manifest_path, data)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return {"success": False, "message": f"更新项目设置失败: {e}"}
|
|
130
|
+
|
|
131
|
+
logger.info(f"[SettingsHandler] 更新设置: {category}")
|
|
132
|
+
return {"success": True, "message": f"settings.{category}"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_settings(category: str, settings: dict[str, Any]) -> list[str]:
|
|
136
|
+
"""校验设置字段的合法性"""
|
|
137
|
+
errors = []
|
|
138
|
+
|
|
139
|
+
if category == "validation":
|
|
140
|
+
if "error_handling" in settings:
|
|
141
|
+
if settings["error_handling"] not in VALID_ERROR_HANDLING:
|
|
142
|
+
errors.append(f"error_handling 必须为: {', '.join(VALID_ERROR_HANDLING)}")
|
|
143
|
+
if "timeout_seconds" in settings:
|
|
144
|
+
val = settings["timeout_seconds"]
|
|
145
|
+
# 对齐 ValidationSettings: int 型, 1-300 (bool 是 int 子类,须显式排除)
|
|
146
|
+
if isinstance(val, bool) or not isinstance(val, int) or not 1 <= val <= 300:
|
|
147
|
+
errors.append("timeout_seconds 必须为 1-300 的整数")
|
|
148
|
+
if "batch_max_files" in settings:
|
|
149
|
+
val = settings["batch_max_files"]
|
|
150
|
+
# 对齐 ValidationSettings: int 型, 1-1000
|
|
151
|
+
if isinstance(val, bool) or not isinstance(val, int) or not 1 <= val <= 1000:
|
|
152
|
+
errors.append("batch_max_files 必须为 1-1000 的整数")
|
|
153
|
+
|
|
154
|
+
elif category == "fileProcessing":
|
|
155
|
+
if "default_encoding" in settings:
|
|
156
|
+
if settings["default_encoding"] not in VALID_ENCODINGS:
|
|
157
|
+
errors.append(f"default_encoding 必须为: {', '.join(VALID_ENCODINGS)}")
|
|
158
|
+
if "csv_delimiter" in settings:
|
|
159
|
+
val = settings["csv_delimiter"]
|
|
160
|
+
if not isinstance(val, str) or len(val) != 1:
|
|
161
|
+
errors.append("csv_delimiter 必须为单字符")
|
|
162
|
+
|
|
163
|
+
elif category == "scriptSecurity":
|
|
164
|
+
if "timeout_seconds" in settings:
|
|
165
|
+
val = settings["timeout_seconds"]
|
|
166
|
+
# 对齐 ScriptSecuritySettings: int 型, 1-60
|
|
167
|
+
if isinstance(val, bool) or not isinstance(val, int) or not 1 <= val <= 60:
|
|
168
|
+
errors.append("timeout_seconds 必须为 1-60 的整数")
|
|
169
|
+
if "allow_eval" in settings:
|
|
170
|
+
if settings["allow_eval"] not in VALID_ALLOW_EVAL:
|
|
171
|
+
errors.append("allow_eval 不允许通过 AI 修改")
|
|
172
|
+
if "sandbox_mode" in settings:
|
|
173
|
+
val = settings["sandbox_mode"]
|
|
174
|
+
# 对齐 ScriptSecuritySettings.sandbox_mode: bool 型——
|
|
175
|
+
# 旧白名单 {"strict","normal"} 与模型矛盾:唯一合法值 true 被拒、
|
|
176
|
+
# 非法字符串被放行,落盘后下次加载 model_validate 直接 422。
|
|
177
|
+
if not isinstance(val, bool):
|
|
178
|
+
errors.append("sandbox_mode 必须为布尔值")
|
|
179
|
+
|
|
180
|
+
return errors
|