from typing import Any, Literal from pydantic import BaseModel, Field FieldKind = Literal["text", "password", "url", "select", "combobox", "toggle", "number", "status"] class SettingOption(BaseModel): value: str label: str help: str class SettingField(BaseModel): key: str label: str description: str kind: FieldKind required: bool = False secret: bool = False default: Any = None placeholder: str = "" options: list[SettingOption] = Field(default_factory=list) generator: Literal["hex24", "hex32", "base64_32"] | None = None advanced: bool = False visible_when: dict[str, Any] | None = None class SettingCategory(BaseModel): id: str label: str description: str test_targets: list[str] = Field(default_factory=list) required_for_workflow: bool = False fields: list[SettingField] def option(value: str, label: str, help_text: str) -> SettingOption: return SettingOption(value=value, label=label, help=help_text) CATEGORIES = [ SettingCategory( id="deployment", label="基础运行", description="由安装器自动管理,通常不需要手动填写。修改数据库密码需要重启服务。", test_targets=["infrastructure"], required_for_workflow=True, fields=[ SettingField( key="database_status", label="PostgreSQL", description="保存项目、版本、任务和结构化设计状态。密码由启动脚本自动生成。", kind="status", required=True, ), SettingField( key="redis_status", label="Redis", description="用于任务队列、缓存和并发锁。密码由启动脚本自动生成。", kind="status", required=True, ), SettingField( key="encryption_status", label="配置加密", description="模型 API Key 会加密保存;主密钥首次启动时自动生成,不显示在页面中。", kind="status", required=True, ), ], ), SettingCategory( id="storage", label="文件存储", description="保存原始户型、清洗图、3D 白模和效果图。你的 NAS MinIO 可以直接使用。", test_targets=["storage"], required_for_workflow=True, fields=[ SettingField( key="s3_endpoint", label="MinIO 内部地址", description="后端访问 NAS 的 S3 API 地址,不是 MinIO 控制台地址。", kind="url", required=True, placeholder="http://192.168.200.36:9000", ), SettingField( key="s3_public_endpoint", label="浏览器可访问地址", description="上传和预览文件时使用;纯内网部署通常与内部地址相同。", kind="url", required=True, placeholder="http://192.168.200.36:9000", ), SettingField( key="s3_access_key_id", label="服务账号 Access Key", description="建议在 MinIO 中单独创建服务账号,不要使用 Root 账号。", kind="password", required=True, secret=True, ), SettingField( key="s3_secret_access_key", label="服务账号 Secret Key", description="仅加密保存于后端,不会返回到浏览器。", kind="password", required=True, secret=True, ), SettingField( key="s3_bucket_inputs", label="原始文件空间", description="保存用户上传的 PDF、DXF 和图片;测试会提示是否缺失。", kind="text", required=True, default="renovation-inputs", ), SettingField( key="s3_bucket_derived", label="中间结果空间", description="保存 SVG、控制图、蒙版和 GLB 白模。", kind="text", required=True, default="renovation-derived", ), SettingField( key="s3_bucket_renders", label="效果图空间", description="保存方向图、效果图和局部修改版本。", kind="text", required=True, default="renovation-renders", ), SettingField( key="s3_force_path_style", label="MinIO 兼容模式", description="MinIO 通常保持开启;AWS S3 可以关闭。", kind="toggle", default=True, advanced=True, ), ], ), SettingCategory( id="document", label="图纸识别", description="矢量 PDF 优先直接解析;只有扫描图或文字层损坏时才调用 OCR。", test_targets=["baidu_ocr"], fields=[ SettingField( key="baidu_ocr_enabled", label="启用百度 OCR", description="开启后,扫描户型图会使用百度 OCR 识别尺寸和文字。", kind="toggle", default=False, ), SettingField( key="baidu_ocr_api_key", label="百度 OCR API Key", description="来自百度智能云 OCR 应用。", kind="password", required=True, secret=True, visible_when={"baidu_ocr_enabled": True}, ), SettingField( key="baidu_ocr_secret_key", label="百度 OCR Secret Key", description="只用于换取访问令牌,不会返回前端。", kind="password", required=True, secret=True, visible_when={"baidu_ocr_enabled": True}, ), ], ), SettingCategory( id="models", label="模型池", description="分别添加大语言模型与多模态模型。每个模型独立配置、独立测试,再用于总调度、空间理解或生图路由。", required_for_workflow=True, fields=[], ), SettingCategory( id="gpu", label="本地算力", description="GPU 主要用于户型分割、深度估计和本地视觉模型,不是调用云端生图 API 的必需项。", test_targets=["gpu"], fields=[ SettingField( key="gpu_mode", label="运行方式", description="同机 Worker 会自动使用本机地址;只有独立 GPU 服务器才需要手填地址。", kind="select", default="disabled", options=[ option("disabled", "暂不使用本地 GPU", "全部使用云端 API,最容易部署。"), option("local", "本机 GPU Worker", "API 通过本机 Worker 调用显卡,页面无需填写地址。"), option("remote", "局域网 GPU 服务器", "GPU 在另一台机器时填写内网地址。"), ], ), SettingField( key="gpu_service_url", label="GPU 服务地址", description="仅远程模式需要,例如 http://192.168.200.50:8100。", kind="url", required=True, placeholder="http://192.168.200.50:8100", visible_when={"gpu_mode": "remote"}, ), SettingField( key="gpu_service_token", label="内部访问令牌", description="用于阻止其他内网设备随意调用 GPU;可以一键生成。", kind="password", required=True, secret=True, generator="hex32", visible_when={"gpu_mode__not": "disabled"}, ), ], ), SettingCategory( id="observability", label="监控与追踪", description="均为可选项。Langfuse 用于评估模型链路,Sentry 用于发现程序异常。", test_targets=["langfuse", "sentry"], fields=[ SettingField( key="langfuse_enabled", label="启用 Langfuse", description="记录每次 Agent 调用、模型耗时、费用与评测结果。", kind="toggle", default=False, ), SettingField( key="langfuse_host", label="Langfuse 地址", description="支持自部署地址,例如 http://192.168.200.20:3001。", kind="url", required=True, visible_when={"langfuse_enabled": True}, ), SettingField( key="langfuse_public_key", label="Langfuse Public Key", description="从 Langfuse 项目设置中复制。", kind="password", required=True, secret=True, visible_when={"langfuse_enabled": True}, ), SettingField( key="langfuse_secret_key", label="Langfuse Secret Key", description="只保存在后端。", kind="password", required=True, secret=True, visible_when={"langfuse_enabled": True}, ), SettingField( key="sentry_enabled", label="启用 Sentry", description="收集异常和性能问题,支持自部署。", kind="toggle", default=False, ), SettingField( key="sentry_dsn", label="Sentry DSN", description="从 Sentry 项目的 Client Keys 页面复制。", kind="password", required=True, secret=True, visible_when={"sentry_enabled": True}, ), SettingField( key="sentry_traces_sample_rate", label="性能采样率", description="0 表示关闭,0.1 表示记录约 10% 的请求。", kind="select", default="0", visible_when={"sentry_enabled": True}, options=[ option("0", "仅错误,不采集性能", "最省资源,适合初期。"), option("0.05", "采样 5%", "适合请求量较大的生产环境。"), option("0.1", "采样 10%", "排障信息和资源消耗较均衡。"), option("1", "全部采样", "仅建议短期排障使用。"), ], ), ], ), ] FIELDS = {field.key: field for category in CATEGORIES for field in category.fields} SECRET_FIELDS = {key for key, field in FIELDS.items() if field.secret} EDITABLE_FIELDS = {key for key, field in FIELDS.items() if field.kind != "status"}