1 line
25 KiB
JSON
1 line
25 KiB
JSON
{"repo": "cht21-c/power-rag", "files": {"camera_sdk_agent/config.py": {"be6685690b6957a064116d162b9b4e4f7d6c1f57": {"t": 1785745451.102887, "n": 8134, "h": "0a821343e7c6891c", "c": "\"\"\"\nCamera SDK RAG Agent - Configuration Module (等保三级改造)\n\n变更:\n- 新增 _decrypt_env() 辅助函数,支持 Fernet 加密字段\n- 敏感字段(DEEPSEEK_API_KEY、MYSQL_PASSWORD)优先读取 _ENC 后缀加密值\n- 解密使用 MASTER_KEY 环境变量(不落盘)\n- print_config() 和 validate_config() 行为不变\n\"\"\"\n\nimport os\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Encryption support (等保三级 - 敏感配置加密存储)\n# ---------------------------------------------------------------------------\n\n_fernet_cache = None\n\n\ndef _get_fernet():\n \"\"\"延迟初始化 Fernet 实例(仅在需要解密时加载)。\"\"\"\n global _fernet_cache\n if _fernet_cache is not None:\n return _fernet_cache\n master_key = os.getenv(\"MASTER_KEY\", \"\")\n if not master_key:\n # 未设置 MASTER_KEY 时返回 None,调用方回退到明文\n return None\n try:\n from cryptography.fernet import Fernet\n _fernet_cache = Fernet(master_key.encode(\"utf-8\"))\n return _fernet_cache\n except ImportError:\n return None\n except Exception:\n return None\n\n\ndef _decrypt_env(env_var: str, default: str = \"\") -> str:\n \"\"\"读取环境变量,自动处理加密值。\n\n 优先级:\n 1. 直接读环境变量(非加密明文)\n 2. 如果为空,读 {env_var}_ENC 后缀的加密值,尝试解密\n 3. 回退到 default\n\n Fernet 加密值的特征是 base64 格式(gAAAAAB 前缀)。\n \"\"\"\n raw = os.getenv(env_var, \"\")\n if raw:\n return raw\n\n encrypted = os.getenv(f\"{env_var}_ENC\", \"\")\n if encrypted:\n f = _get_fernet()\n if f:\n try:\n return f.decrypt(encrypted.encode(\"utf-8\")).decode(\"utf-8\")\n except Exception:\n import warnings\n warnings.warn(f\"Failed to decrypt {env_var}_ENC. Check MASTER_KEY.\", RuntimeWarning)\n return \"\"\n else:\n import warnings\n warnings.warn(\n f\"{env_var}_ENC is set but MASTER_KEY is not. \"\n f\"Install cryptography: pip install cryptography\",\n RuntimeWarning,\n )\n return \"\"\n\n return default\n\n\n# ---------------------------------------------------------------------------\n# .env loading\n# ---------------------------------------------------------------------------\n\ntry:\n from dotenv import load_dotenv\n _env_path = Path(__file__).resolve().parent / \".env\"\n if _env_path.exists():\n load_dotenv(_env_path)\nexcept ImportError:\n pass\n\n# ---------------------------------------------------------------------------\n# Qdrant\n# ---------------------------------------------------------------------------\nQDRANT_HOST = os.getenv(\"QDRANT_HOST\", \"\")\nQDRANT_PORT = int(os.getenv(\"QDRANT_PORT\", \"6333\"))\nQDRANT_LOCAL_PATH = os.getenv(\"QDRANT_LOCAL_PATH\", str(Path(__file__).resolve().parent / \"qdrant_data\"))\nQDRANT_COLLECTION_NAME = os.getenv(\"QDRANT_COLLECTION_NAME\", \"camera_sdk_collection\")\nQDRANT_VECTOR_DIM = int(os.getenv(\"QDRANT_VECTOR_DIM\", \"1024\"))\nQDRANT_DISTANCE_METRIC = os.getenv(\"QDRANT_DISTANCE_METRIC\", \"Cosine\")\n\n# ---------------------------------------------------------------------------\n# Embedding\n# ---------------------------------------------------------------------------\nEMBED_MODEL_NAME = os.getenv(\"EMBED_MODEL_NAME\", \"BAAI/bge-m3\")\nEMBED_DEVICE = os.getenv(\"EMBED_DEVICE\", \"cpu\")\nEMBED_BATCH_SIZE = int(os.getenv(\"EMBED_BATCH_SIZE\", \"32\"))\n\n# ---------------------------------------------------------------------------\n# Chunking\n# ---------------------------------------------------------------------------\nCHUNK_SIZE_TOKENS = int(os.getenv(\"CHUNK_SIZE_TOKENS\", \"512\"))\nCHUNK_OVERLAP_TOKENS = int(os.getenv(\"CHUNK_OVERLAP_TOKENS\", \"64\"))\n\n# ---------------------------------------------------------------------------\n# Retrieval\n# ---------------------------------------------------------------------------\nVECTOR_TOP_K = int(os.getenv(\"VECTOR_TOP_K\", \"20\"))\nBM25_TOP_K = int(os.getenv(\"BM25_TOP_K\", \"20\"))\nFINAL_TOP_K = int(os.getenv(\"FINAL_TOP_K\", \"5\"))\nFUSION_K = int(os.getenv(\"FUSION_K\", \"60\"))\n\n# ---------------------------------------------------------------------------\n# P1 质量加固 —— 可配置阈值\n# ---------------------------------------------------------------------------\n# 幻觉护栏: RRF 融合分数阈值\n# 最高分低于此值 → 直接拒答(不调用 LLM)\nRAG_MIN_SCORE_STRICT = float(os.getenv(\"RAG_MIN_SCORE_STRICT\", \"0.02\"))\n# 最高分在此值和 STRICT 之间 → 生成但标注\"置信度较低\"\nRAG_MIN_SCORE_WARN = float(os.getenv(\"RAG_MIN_SCORE_WARN\", \"0.05\"))\n\n# 意图置信度阈值: LLM 分类 confidence 低于此值 → 反问用户澄清\nINTENT_CONFIDENCE_THRESHOLD = float(os.getenv(\"INTENT_CONFIDENCE_THRESHOLD\", \"0.55\"))\n\n# 图纸语义匹配: 图纸列表超过此数量时分批传入 LLM\nDRAWING_MATCH_MAX_ITEMS = int(os.getenv(\"DRAWING_MATCH_MAX_ITEMS\", \"500\"))\n\n# 二次验证开关: drawing 路由高置信度时是否启用独立复核\nVERIFY_ENABLED = os.getenv(\"VERIFY_ENABLED\", \"true\").lower() in (\"1\", \"true\", \"yes\")\n\n# ---------------------------------------------------------------------------\n# P2 工程健壮性 —— 可配置参数\n# ---------------------------------------------------------------------------\n# 模型路由: 候选模型列表(优先级从高到低)\nMODEL_CANDIDATES = os.getenv(\"MODEL_CANDIDATES\", \"deepseek-chat\").split(\",\")\n\n# 熔断器: 连续失败次数触发熔断\nCIRCUIT_BREAKER_FAIL_THRESHOLD = int(os.getenv(\"CIRCUIT_BREAKER_FAIL_THRESHOLD\", \"5\"))\n\n# 熔断器: 冷却时间(秒),OPEN -> HALF_OPEN\nCIRCUIT_BREAKER_COOLDOWN_SEC = int(os.getenv(\"CIRCUIT_BREAKER_COOLDOWN_SEC\", \"60\"))\n\n# 首包超时(秒),流式调用超过此时间未收到 token 视为失败\nSTREAM_FIRST_TOKEN_TIMEOUT_SEC = float(os.getenv(\"STREAM_FIRST_TOKEN_TIMEOUT_SEC\", \"10\"))\n\n# 记忆压缩: 保留最近 N 轮原文不压缩\nMEMORY_WINDOW_TURNS = int(os.getenv(\"MEMORY_WINDOW_TURNS\", \"3\"))\n\n# 记忆压缩: 总消息超过此 token 估算值触发压缩\nMEMORY_COMPRESS_TRIGGER_TOKENS = int(os.getenv(\"MEMORY_COMPRESS_TRIGGER_TOKENS\", \"3500\"))\n\n# 工具注册: 启用的工具名单(逗号分隔,空=全部启用)\nENABLED_TOOLS = os.getenv(\"ENABLED_TOOLS\", \"\").split(\",\") if os.getenv(\"ENABLED_TOOLS\") else None\n\n# ---------------------------------------------------------------------------\n# P2 结构化内容检测 —— 规则分块配置\n# ---------------------------------------------------------------------------\n# 结构化检测: 管道分隔字段数(如漏洞报告 4 字段: 名称|等级|IP|建议)\nSTRUCTURED_ROW_FIELD_COUNT = int(os.getenv(\"STRUCTURED_ROW_FIELD_COUNT\", \"4\"))\n\n# 结构化检测: 命中结构化模式的行数占比阈值(0.0-1.0,达到才判定为结构化块)\nSTRUCTURED_BLOCK_THRESHOLD = float(os.getenv(\"STRUCTURED_BLOCK_THRESHOLD\", \"0.6\"))\n\n# 规则分块: 规则分块时每个 chunk 最多容纳的记录行数\nMAX_RECORDS_PER_CHUNK = int(os.getenv(\"MAX_RECORDS_PER_CHUNK\", \"8\"))\n\n# LLM 上下文: 消息+检索内容的最大 token 预算\nLLM_MAX_CONTEXT_TOKENS = int(os.getenv(\"LLM_MAX_CONTEXT_TOKENS\", \"8000\"))\n\n# ---------------------------------------------------------------------------\n# DeepSeek API Configuration (等保三级:支持加密存储)\n# ---------------------------------------------------------------------------\nDEEPSEEK_API_KEY = _decrypt_env(\"DEEPSEEK_API_KEY\", \"\")\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = os.getenv(\"DEEPSEEK_MODEL\", \"deepseek-chat\")\n\n# ---------------------------------------------------------------------------\n# MySQL (等保三级:密码支持加密存储)\n# ---------------------------------------------------------------------------\n_MYSQL_PASSWORD = _decrypt_env(\"MYSQL_PASSWORD\", \"\")\n\n# Paths\nPROJECT_ROOT = Path(__file__).resolve().parent\nSDK_DOCS_DIR = PROJECT_ROOT / \"sdk_docs\"\n\n\n# ---------------------------------------------------------------------------\n# Validation / Printing\n# ---------------------------------------------------------------------------\n\ndef validate_config() -> bool:\n if not DEEPSEEK_API_KEY:\n raise ValueError(\n \"DEEPSEEK_API_KEY is not set. \"\n \"Set it via environment variable or in a .env file.\\n\"\n \"For encrypted storage, set DEEPSEEK_API_KEY_ENC and MASTER_KEY.\"\n )\n return True\n\n\ndef print_config():\n key_masked = (\n DEEPSEEK_API_KEY[:8] + \"...\" + DEEPSEEK_API_KEY[-4:]\n if DEEPSEEK_API_KEY else \"NOT SET\"\n )\n print(f\"Qdrant: {QDRANT_HOST}:{QDRANT_PORT}\")\n print(f\"Collection: {QDRANT_COLLECTION_NAME}\")\n print(f\"Embed Model: {EMBED_MODEL_NAME}\")\n print(f\"Embed Device: {EMBED_DEVICE}\")\n print(f\"Chunk Size: {CHUNK_SIZE_TOKENS} tokens (overlap {CHUNK_OVERLAP_TOKENS})\")\n print(f\"Retrieval: vector_top{VECTOR_TOP_K} + bm25_top{BM25_TOP_K} -> fusion_top{FINAL_TOP_K}\")\n print(f\"DeepSeek Model: {DEEPSEEK_MODEL}\")\n print(f\"API Key: {key_masked}\")\n print(f\"Docs Directory: {SDK_DOCS_DIR}\")\n"}, "cd4b65f4c0dd244c17b35b3534f69a1963fc58b8": {"t": 1785753387.0677438, "n": 8134, "h": "0a821343e7c6891c", "c": "\"\"\"\nCamera SDK RAG Agent - Configuration Module (等保三级改造)\n\n变更:\n- 新增 _decrypt_env() 辅助函数,支持 Fernet 加密字段\n- 敏感字段(DEEPSEEK_API_KEY、MYSQL_PASSWORD)优先读取 _ENC 后缀加密值\n- 解密使用 MASTER_KEY 环境变量(不落盘)\n- print_config() 和 validate_config() 行为不变\n\"\"\"\n\nimport os\nfrom pathlib import Path\n\n# ---------------------------------------------------------------------------\n# Encryption support (等保三级 - 敏感配置加密存储)\n# ---------------------------------------------------------------------------\n\n_fernet_cache = None\n\n\ndef _get_fernet():\n \"\"\"延迟初始化 Fernet 实例(仅在需要解密时加载)。\"\"\"\n global _fernet_cache\n if _fernet_cache is not None:\n return _fernet_cache\n master_key = os.getenv(\"MASTER_KEY\", \"\")\n if not master_key:\n # 未设置 MASTER_KEY 时返回 None,调用方回退到明文\n return None\n try:\n from cryptography.fernet import Fernet\n _fernet_cache = Fernet(master_key.encode(\"utf-8\"))\n return _fernet_cache\n except ImportError:\n return None\n except Exception:\n return None\n\n\ndef _decrypt_env(env_var: str, default: str = \"\") -> str:\n \"\"\"读取环境变量,自动处理加密值。\n\n 优先级:\n 1. 直接读环境变量(非加密明文)\n 2. 如果为空,读 {env_var}_ENC 后缀的加密值,尝试解密\n 3. 回退到 default\n\n Fernet 加密值的特征是 base64 格式(gAAAAAB 前缀)。\n \"\"\"\n raw = os.getenv(env_var, \"\")\n if raw:\n return raw\n\n encrypted = os.getenv(f\"{env_var}_ENC\", \"\")\n if encrypted:\n f = _get_fernet()\n if f:\n try:\n return f.decrypt(encrypted.encode(\"utf-8\")).decode(\"utf-8\")\n except Exception:\n import warnings\n warnings.warn(f\"Failed to decrypt {env_var}_ENC. Check MASTER_KEY.\", RuntimeWarning)\n return \"\"\n else:\n import warnings\n warnings.warn(\n f\"{env_var}_ENC is set but MASTER_KEY is not. \"\n f\"Install cryptography: pip install cryptography\",\n RuntimeWarning,\n )\n return \"\"\n\n return default\n\n\n# ---------------------------------------------------------------------------\n# .env loading\n# ---------------------------------------------------------------------------\n\ntry:\n from dotenv import load_dotenv\n _env_path = Path(__file__).resolve().parent / \".env\"\n if _env_path.exists():\n load_dotenv(_env_path)\nexcept ImportError:\n pass\n\n# ---------------------------------------------------------------------------\n# Qdrant\n# ---------------------------------------------------------------------------\nQDRANT_HOST = os.getenv(\"QDRANT_HOST\", \"\")\nQDRANT_PORT = int(os.getenv(\"QDRANT_PORT\", \"6333\"))\nQDRANT_LOCAL_PATH = os.getenv(\"QDRANT_LOCAL_PATH\", str(Path(__file__).resolve().parent / \"qdrant_data\"))\nQDRANT_COLLECTION_NAME = os.getenv(\"QDRANT_COLLECTION_NAME\", \"camera_sdk_collection\")\nQDRANT_VECTOR_DIM = int(os.getenv(\"QDRANT_VECTOR_DIM\", \"1024\"))\nQDRANT_DISTANCE_METRIC = os.getenv(\"QDRANT_DISTANCE_METRIC\", \"Cosine\")\n\n# ---------------------------------------------------------------------------\n# Embedding\n# ---------------------------------------------------------------------------\nEMBED_MODEL_NAME = os.getenv(\"EMBED_MODEL_NAME\", \"BAAI/bge-m3\")\nEMBED_DEVICE = os.getenv(\"EMBED_DEVICE\", \"cpu\")\nEMBED_BATCH_SIZE = int(os.getenv(\"EMBED_BATCH_SIZE\", \"32\"))\n\n# ---------------------------------------------------------------------------\n# Chunking\n# ---------------------------------------------------------------------------\nCHUNK_SIZE_TOKENS = int(os.getenv(\"CHUNK_SIZE_TOKENS\", \"512\"))\nCHUNK_OVERLAP_TOKENS = int(os.getenv(\"CHUNK_OVERLAP_TOKENS\", \"64\"))\n\n# ---------------------------------------------------------------------------\n# Retrieval\n# ---------------------------------------------------------------------------\nVECTOR_TOP_K = int(os.getenv(\"VECTOR_TOP_K\", \"20\"))\nBM25_TOP_K = int(os.getenv(\"BM25_TOP_K\", \"20\"))\nFINAL_TOP_K = int(os.getenv(\"FINAL_TOP_K\", \"5\"))\nFUSION_K = int(os.getenv(\"FUSION_K\", \"60\"))\n\n# ---------------------------------------------------------------------------\n# P1 质量加固 —— 可配置阈值\n# ---------------------------------------------------------------------------\n# 幻觉护栏: RRF 融合分数阈值\n# 最高分低于此值 → 直接拒答(不调用 LLM)\nRAG_MIN_SCORE_STRICT = float(os.getenv(\"RAG_MIN_SCORE_STRICT\", \"0.02\"))\n# 最高分在此值和 STRICT 之间 → 生成但标注\"置信度较低\"\nRAG_MIN_SCORE_WARN = float(os.getenv(\"RAG_MIN_SCORE_WARN\", \"0.05\"))\n\n# 意图置信度阈值: LLM 分类 confidence 低于此值 → 反问用户澄清\nINTENT_CONFIDENCE_THRESHOLD = float(os.getenv(\"INTENT_CONFIDENCE_THRESHOLD\", \"0.55\"))\n\n# 图纸语义匹配: 图纸列表超过此数量时分批传入 LLM\nDRAWING_MATCH_MAX_ITEMS = int(os.getenv(\"DRAWING_MATCH_MAX_ITEMS\", \"500\"))\n\n# 二次验证开关: drawing 路由高置信度时是否启用独立复核\nVERIFY_ENABLED = os.getenv(\"VERIFY_ENABLED\", \"true\").lower() in (\"1\", \"true\", \"yes\")\n\n# ---------------------------------------------------------------------------\n# P2 工程健壮性 —— 可配置参数\n# ---------------------------------------------------------------------------\n# 模型路由: 候选模型列表(优先级从高到低)\nMODEL_CANDIDATES = os.getenv(\"MODEL_CANDIDATES\", \"deepseek-chat\").split(\",\")\n\n# 熔断器: 连续失败次数触发熔断\nCIRCUIT_BREAKER_FAIL_THRESHOLD = int(os.getenv(\"CIRCUIT_BREAKER_FAIL_THRESHOLD\", \"5\"))\n\n# 熔断器: 冷却时间(秒),OPEN -> HALF_OPEN\nCIRCUIT_BREAKER_COOLDOWN_SEC = int(os.getenv(\"CIRCUIT_BREAKER_COOLDOWN_SEC\", \"60\"))\n\n# 首包超时(秒),流式调用超过此时间未收到 token 视为失败\nSTREAM_FIRST_TOKEN_TIMEOUT_SEC = float(os.getenv(\"STREAM_FIRST_TOKEN_TIMEOUT_SEC\", \"10\"))\n\n# 记忆压缩: 保留最近 N 轮原文不压缩\nMEMORY_WINDOW_TURNS = int(os.getenv(\"MEMORY_WINDOW_TURNS\", \"3\"))\n\n# 记忆压缩: 总消息超过此 token 估算值触发压缩\nMEMORY_COMPRESS_TRIGGER_TOKENS = int(os.getenv(\"MEMORY_COMPRESS_TRIGGER_TOKENS\", \"3500\"))\n\n# 工具注册: 启用的工具名单(逗号分隔,空=全部启用)\nENABLED_TOOLS = os.getenv(\"ENABLED_TOOLS\", \"\").split(\",\") if os.getenv(\"ENABLED_TOOLS\") else None\n\n# ---------------------------------------------------------------------------\n# P2 结构化内容检测 —— 规则分块配置\n# ---------------------------------------------------------------------------\n# 结构化检测: 管道分隔字段数(如漏洞报告 4 字段: 名称|等级|IP|建议)\nSTRUCTURED_ROW_FIELD_COUNT = int(os.getenv(\"STRUCTURED_ROW_FIELD_COUNT\", \"4\"))\n\n# 结构化检测: 命中结构化模式的行数占比阈值(0.0-1.0,达到才判定为结构化块)\nSTRUCTURED_BLOCK_THRESHOLD = float(os.getenv(\"STRUCTURED_BLOCK_THRESHOLD\", \"0.6\"))\n\n# 规则分块: 规则分块时每个 chunk 最多容纳的记录行数\nMAX_RECORDS_PER_CHUNK = int(os.getenv(\"MAX_RECORDS_PER_CHUNK\", \"8\"))\n\n# LLM 上下文: 消息+检索内容的最大 token 预算\nLLM_MAX_CONTEXT_TOKENS = int(os.getenv(\"LLM_MAX_CONTEXT_TOKENS\", \"8000\"))\n\n# ---------------------------------------------------------------------------\n# DeepSeek API Configuration (等保三级:支持加密存储)\n# ---------------------------------------------------------------------------\nDEEPSEEK_API_KEY = _decrypt_env(\"DEEPSEEK_API_KEY\", \"\")\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = os.getenv(\"DEEPSEEK_MODEL\", \"deepseek-chat\")\n\n# ---------------------------------------------------------------------------\n# MySQL (等保三级:密码支持加密存储)\n# ---------------------------------------------------------------------------\n_MYSQL_PASSWORD = _decrypt_env(\"MYSQL_PASSWORD\", \"\")\n\n# Paths\nPROJECT_ROOT = Path(__file__).resolve().parent\nSDK_DOCS_DIR = PROJECT_ROOT / \"sdk_docs\"\n\n\n# ---------------------------------------------------------------------------\n# Validation / Printing\n# ---------------------------------------------------------------------------\n\ndef validate_config() -> bool:\n if not DEEPSEEK_API_KEY:\n raise ValueError(\n \"DEEPSEEK_API_KEY is not set. \"\n \"Set it via environment variable or in a .env file.\\n\"\n \"For encrypted storage, set DEEPSEEK_API_KEY_ENC and MASTER_KEY.\"\n )\n return True\n\n\ndef print_config():\n key_masked = (\n DEEPSEEK_API_KEY[:8] + \"...\" + DEEPSEEK_API_KEY[-4:]\n if DEEPSEEK_API_KEY else \"NOT SET\"\n )\n print(f\"Qdrant: {QDRANT_HOST}:{QDRANT_PORT}\")\n print(f\"Collection: {QDRANT_COLLECTION_NAME}\")\n print(f\"Embed Model: {EMBED_MODEL_NAME}\")\n print(f\"Embed Device: {EMBED_DEVICE}\")\n print(f\"Chunk Size: {CHUNK_SIZE_TOKENS} tokens (overlap {CHUNK_OVERLAP_TOKENS})\")\n print(f\"Retrieval: vector_top{VECTOR_TOP_K} + bm25_top{BM25_TOP_K} -> fusion_top{FINAL_TOP_K}\")\n print(f\"DeepSeek Model: {DEEPSEEK_MODEL}\")\n print(f\"API Key: {key_masked}\")\n print(f\"Docs Directory: {SDK_DOCS_DIR}\")\n"}}, "scripts/encrypt_config.py": {"be6685690b6957a064116d162b9b4e4f7d6c1f57": {"t": 1785746870.7744243, "n": 2237, "h": "63f76472dfd7ec9b", "c": "#!/usr/bin/env python3\n\"\"\"\n敏感配置加密工具\n\n使用 Fernet 对称加密保护 .env 中的敏感字段。\n\n用法:\n # 1. 生成主密钥(一次性,输出到控制台)\n python scripts/encrypt_config.py generate-key\n\n # 2. 将主密钥设为环境变量(不落盘!)\n export MASTER_KEY=<上一步的输出>\n # Windows: set MASTER_KEY=<上一步的输出>\n\n # 3. 加密单个值\n python scripts/encrypt_config.py encrypt \"sk-your-api-key\"\n\n # 4. 解密(验证用)\n python scripts/encrypt_config.py decrypt \"<ciphered_text>\"\n\n配置方式:\n # .env 中敏感字段改用加密值:\n DEEPSEEK_API_KEY_ENC=gAAAAABl... # 加密密文\n MYSQL_PASSWORD_ENC=gAAAAABm...\n\"\"\"\n\nimport argparse\nimport os\nimport sys\n\nfrom cryptography.fernet import Fernet\n\n\ndef get_master_key() -> bytes:\n key = os.environ.get(\"MASTER_KEY\", \"\")\n if not key:\n print(\"ERROR: MASTER_KEY environment variable not set.\", file=sys.stderr)\n print(\"Set it with: export MASTER_KEY=<key>\", file=sys.stderr)\n sys.exit(1)\n try:\n return key.encode(\"utf-8\")\n except Exception:\n return key\n\n\ndef cmd_generate_key():\n \"\"\"生成一个新的 Fernet 主密钥。\"\"\"\n key = Fernet.generate_key()\n print(key.decode(\"utf-8\"))\n print(\"\\n# 请将此密钥设为环境变量(不落盘):\", file=sys.stderr)\n print(f\"# export MASTER_KEY={key.decode()}\", file=sys.stderr)\n\n\ndef cmd_encrypt(plaintext: str):\n \"\"\"加密一段明文。\"\"\"\n key = get_master_key()\n f = Fernet(key)\n token = f.encrypt(plaintext.encode(\"utf-8\"))\n print(token.decode(\"utf-8\"))\n\n\ndef cmd_decrypt(ciphertext: str):\n \"\"\"解密一段密文。\"\"\"\n key = get_master_key()\n f = Fernet(key)\n plain = f.decrypt(ciphertext.encode(\"utf-8\"))\n print(plain.decode(\"utf-8\"))\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"敏感配置加密工具\")\n sub = parser.add_subparsers(dest=\"command\", required=True)\n\n sub.add_parser(\"generate-key\", help=\"生成 Fernet 主密钥\")\n p_enc = sub.add_parser(\"encrypt\", help=\"加密明文\")\n p_enc.add_argument(\"plaintext\", help=\"要加密的明文\")\n p_dec = sub.add_parser(\"decrypt\", help=\"解密密文\")\n p_dec.add_argument(\"ciphertext\", help=\"要解密的密文\")\n\n args = parser.parse_args()\n\n if args.command == \"generate-key\":\n cmd_generate_key()\n elif args.command == \"encrypt\":\n cmd_encrypt(args.plaintext)\n elif args.command == \"decrypt\":\n cmd_decrypt(args.ciphertext)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "cd4b65f4c0dd244c17b35b3534f69a1963fc58b8": {"t": 1785753388.4209394, "n": 2237, "h": "63f76472dfd7ec9b", "c": "#!/usr/bin/env python3\n\"\"\"\n敏感配置加密工具\n\n使用 Fernet 对称加密保护 .env 中的敏感字段。\n\n用法:\n # 1. 生成主密钥(一次性,输出到控制台)\n python scripts/encrypt_config.py generate-key\n\n # 2. 将主密钥设为环境变量(不落盘!)\n export MASTER_KEY=<上一步的输出>\n # Windows: set MASTER_KEY=<上一步的输出>\n\n # 3. 加密单个值\n python scripts/encrypt_config.py encrypt \"sk-your-api-key\"\n\n # 4. 解密(验证用)\n python scripts/encrypt_config.py decrypt \"<ciphered_text>\"\n\n配置方式:\n # .env 中敏感字段改用加密值:\n DEEPSEEK_API_KEY_ENC=gAAAAABl... # 加密密文\n MYSQL_PASSWORD_ENC=gAAAAABm...\n\"\"\"\n\nimport argparse\nimport os\nimport sys\n\nfrom cryptography.fernet import Fernet\n\n\ndef get_master_key() -> bytes:\n key = os.environ.get(\"MASTER_KEY\", \"\")\n if not key:\n print(\"ERROR: MASTER_KEY environment variable not set.\", file=sys.stderr)\n print(\"Set it with: export MASTER_KEY=<key>\", file=sys.stderr)\n sys.exit(1)\n try:\n return key.encode(\"utf-8\")\n except Exception:\n return key\n\n\ndef cmd_generate_key():\n \"\"\"生成一个新的 Fernet 主密钥。\"\"\"\n key = Fernet.generate_key()\n print(key.decode(\"utf-8\"))\n print(\"\\n# 请将此密钥设为环境变量(不落盘):\", file=sys.stderr)\n print(f\"# export MASTER_KEY={key.decode()}\", file=sys.stderr)\n\n\ndef cmd_encrypt(plaintext: str):\n \"\"\"加密一段明文。\"\"\"\n key = get_master_key()\n f = Fernet(key)\n token = f.encrypt(plaintext.encode(\"utf-8\"))\n print(token.decode(\"utf-8\"))\n\n\ndef cmd_decrypt(ciphertext: str):\n \"\"\"解密一段密文。\"\"\"\n key = get_master_key()\n f = Fernet(key)\n plain = f.decrypt(ciphertext.encode(\"utf-8\"))\n print(plain.decode(\"utf-8\"))\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\"敏感配置加密工具\")\n sub = parser.add_subparsers(dest=\"command\", required=True)\n\n sub.add_parser(\"generate-key\", help=\"生成 Fernet 主密钥\")\n p_enc = sub.add_parser(\"encrypt\", help=\"加密明文\")\n p_enc.add_argument(\"plaintext\", help=\"要加密的明文\")\n p_dec = sub.add_parser(\"decrypt\", help=\"解密密文\")\n p_dec.add_argument(\"ciphertext\", help=\"要解密的密文\")\n\n args = parser.parse_args()\n\n if args.command == \"generate-key\":\n cmd_generate_key()\n elif args.command == \"encrypt\":\n cmd_encrypt(args.plaintext)\n elif args.command == \"decrypt\":\n cmd_decrypt(args.ciphertext)\n\n\nif __name__ == \"__main__\":\n main()\n"}}}} |