api-key-manager 2.1.2__tar.gz → 3.0.0__tar.gz
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.
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/PKG-INFO +62 -6
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/README.md +60 -4
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/PKG-INFO +62 -6
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/SOURCES.txt +7 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/__init__.py +2 -2
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/api_models.py +15 -10
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/checker.py +13 -8
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/cli.py +2 -2
- api_key_manager-3.0.0/key_manager/config.py +103 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/detector.py +371 -335
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/logger.py +2 -2
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/parser.py +9 -9
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/__init__.py +9 -1
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/ai302.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/anthropic.py +109 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/baichuan.py +69 -97
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/base.py +89 -32
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/cerebras.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/cohere.py +90 -90
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/cstcloud.py +94 -122
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/dashscope.py +92 -120
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/dashscope_coding.py +94 -122
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/deepseek.py +133 -166
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/dmxapi.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/doubao.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/fireworks.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/google.py +99 -99
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/grok.py +75 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/groq.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/huggingface.py +37 -54
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/hyperbolic.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/infini.py +102 -135
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/infini_coding.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/kimi.py +93 -121
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/kimi_coding.py +0 -28
- api_key_manager-3.0.0/key_manager/providers/longcat.py +76 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/mimo.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/mimo_plan.py +107 -140
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/minimax.py +69 -97
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/minimax_plan.py +94 -122
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/mistral.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/models_registry.py +2962 -2901
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/modelscope.py +120 -134
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/nvidia.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/ocoolai.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/openai.py +101 -140
- api_key_manager-3.0.0/key_manager/providers/opencode.py +89 -0
- api_key_manager-3.0.0/key_manager/providers/opencode_zen.py +89 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/openrouter.py +91 -119
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/perplexity.py +75 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/poe.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/ppio.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/replicate.py +37 -54
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/siliconflow.py +93 -121
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/stepfun.py +99 -132
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/tencent_hunyuan.py +94 -122
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/together.py +101 -134
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/yi.py +69 -97
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/zai.py +76 -109
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/zhipu.py +99 -127
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/providers/zhipu_coding.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/ssrf.py +29 -2
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/storage.py +15 -2
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/tester.py +16 -13
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/validator.py +16 -12
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/web.py +1676 -1557
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/webhook.py +6 -5
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/pyproject.toml +2 -2
- api_key_manager-3.0.0/tests/test_api_endpoints.py +685 -0
- api_key_manager-3.0.0/tests/test_base_check.py +255 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_detector.py +71 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_e2e.py +4 -1
- api_key_manager-3.0.0/tests/test_parser_supplement.py +216 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_provider_detection.py +83 -16
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_security.py +3 -3
- api_key_manager-3.0.0/tests/test_ssrf_supplement.py +81 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_tester.py +46 -20
- api_key_manager-3.0.0/tests/test_validator_supplement.py +196 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_webhook.py +1 -1
- api_key_manager-2.1.2/key_manager/config.py +0 -61
- api_key_manager-2.1.2/key_manager/providers/longcat.py +0 -123
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/dependency_links.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/entry_points.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/requires.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/api_key_manager.egg-info/top_level.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/__main__.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/core.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/errors.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/i18n.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/model_capabilities.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/proxy.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/py.typed +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/key_manager/url_override.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/setup.cfg +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_bug_fixes.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_checker.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_core.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_errors.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_i18n.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_logger.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_openapi.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_parser.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_providers.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_proxy.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_storage.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_validator.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-3.0.0}/tests/test_web_fixes.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-key-manager
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Batch manage API keys for
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Batch manage API keys for 45+ AI providers with CLI and Web interfaces
|
|
5
5
|
Author: Townrain
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Townrain/API-Key-Manager
|
|
@@ -45,12 +45,17 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
|
45
45
|
- **Web 界面** - 赛博朋克风格的管理界面
|
|
46
46
|
- **代理支持** - 支持 HTTP/SOCKS 代理
|
|
47
47
|
- **加密存储** - AES-256-GCM 加密存储 API 密钥,随机盐值
|
|
48
|
-
- **安全防护** - 路径遍历防护、SSRF
|
|
48
|
+
- **安全防护** - 路径遍历防护、SSRF 防护(含 IPv6)、时序安全认证、强制认证、CORS 配置
|
|
49
49
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
50
50
|
- **国际化** - 支持中英文错误信息
|
|
51
51
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
52
52
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
53
53
|
|
|
54
|
+
## 系统架构
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
## 支持的 AI 服务商
|
|
55
60
|
|
|
56
61
|
### 国际
|
|
@@ -192,11 +197,13 @@ python web.py
|
|
|
192
197
|
### 安全防护
|
|
193
198
|
|
|
194
199
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
195
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
200
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP(含 IPv6),DNS 重绑定防护
|
|
196
201
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
197
|
-
-
|
|
202
|
+
- **强制认证** - 未配置 API Key 时自动生成随机密钥,拒绝未认证请求
|
|
198
203
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
199
|
-
- **
|
|
204
|
+
- **CORS 配置** - 跨域资源共享安全配置
|
|
205
|
+
- **Webhook 安全** - 使用 `secrets.token_hex` 生成不可预测的 webhook ID
|
|
206
|
+
- **存储安全** - 密钥文件权限加固 (0o600),AES-256-GCM 加密,PBKDF2 600,000 次迭代
|
|
200
207
|
|
|
201
208
|
### API 认证
|
|
202
209
|
|
|
@@ -713,6 +720,55 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
713
720
|
|
|
714
721
|
## 更新日志
|
|
715
722
|
|
|
723
|
+
|
|
724
|
+
### v3.0.0 (2026-06-15)
|
|
725
|
+
|
|
726
|
+
- **安全修复**: 移除 API 响应中的明文密钥,只返回 key_masked
|
|
727
|
+
- **安全修复**: 强制认证,无配置时自动生成随机 API Key
|
|
728
|
+
- **安全修复**: 使用 hmac.compare_digest 防止时序攻击
|
|
729
|
+
- **安全修复**: 添加 CORS 中间件配置
|
|
730
|
+
- **性能优化**: PBKDF2 密钥缓存,迭代次数提升至 600,000 (OWASP 推荐)
|
|
731
|
+
- **性能优化**: 速率限制器添加 TTL 驱逐,防止内存泄漏
|
|
732
|
+
- **可靠性**: 修复 checker.py 合并逻辑错误
|
|
733
|
+
- **可靠性**: 替换所有裸 except 为 except Exception
|
|
734
|
+
- **可靠性**: 添加配置验证和错误处理
|
|
735
|
+
- **可靠性**: 添加 /healthz 健康检查端点
|
|
736
|
+
- **可靠性**: 修复 datetime.utcnow() 废弃警告
|
|
737
|
+
- **SSRF 防护**: 添加 IPv6 私有范围和 DNS 重绑定防护
|
|
738
|
+
- **Webhook 安全**: 使用 secrets.token_hex 替代 MD5 生成 webhook ID
|
|
739
|
+
- **存储安全**: 密钥文件权限加固 (0o600)
|
|
740
|
+
- **前端**: 模型检测列表添加自定义滚轮条样式
|
|
741
|
+
- **前端**: 修复模型类型下拉窗口被弹窗裁剪的问题
|
|
742
|
+
- **精确匹配修复**: 能力检测从子串匹配改为 ^$ 锚定精确匹配
|
|
743
|
+
- **API Bug 修复**: 修复 /api/stats/chart 运行时崩溃
|
|
744
|
+
- **测试补充**: 新增 8 个端点的测试,覆盖率 91.47%
|
|
745
|
+
|
|
746
|
+
### v2.2.2 (2026-06-14)
|
|
747
|
+
|
|
748
|
+
- **模型检测二级窗口**: 新增模型检测弹窗,支持搜索、类型筛选、全选、批量检测
|
|
749
|
+
- **模型类型图标**: 显示模型能力图标(视觉/推理/联网/工具/嵌入/重排/免费)
|
|
750
|
+
- **模型能力数据更新**: 从 Cherry Studio 同步最新模型能力数据(3184 条记录)
|
|
751
|
+
- **检测逻辑优化**: DeepSeek 欠费密钥正确识别,不再误判为其他服务商
|
|
752
|
+
- **指定模型检测**: 自动检测服务商时也支持指定模型检测
|
|
753
|
+
|
|
754
|
+
### v2.2.1 (2026-06-14)
|
|
755
|
+
|
|
756
|
+
- **检测逻辑修复**: 自动识别后必须调用 check() 验证 key 有效性
|
|
757
|
+
- **签名匹配修复**: 状态码不再硬编码 401,使用实际状态码
|
|
758
|
+
- **不可靠服务商**: ppio、nvidia、modelscope 不参与加分
|
|
759
|
+
- **模型选择器**: 新增模型下拉框,支持选择具体模型检测
|
|
760
|
+
- **Cherry Studio 同步**: 修复 ownedBy 映射(alibaba→dashscope 等)
|
|
761
|
+
- **测试补充**: 新增 test_parser_supplement、test_ssrf_supplement、test_validator_supplement
|
|
762
|
+
### v2.2.0 (2026-06-14)
|
|
763
|
+
|
|
764
|
+
- **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
|
|
765
|
+
- **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
|
|
766
|
+
- **并发优化**: /v1/models 和 chat/completions 都并发调用
|
|
767
|
+
- **超时控制**: 所有网络请求 5 秒超时
|
|
768
|
+
- **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
|
|
769
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
770
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
771
|
+
|
|
716
772
|
### v2.1.2 (2026-06-11)
|
|
717
773
|
|
|
718
774
|
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
- **Web 界面** - 赛博朋克风格的管理界面
|
|
13
13
|
- **代理支持** - 支持 HTTP/SOCKS 代理
|
|
14
14
|
- **加密存储** - AES-256-GCM 加密存储 API 密钥,随机盐值
|
|
15
|
-
- **安全防护** - 路径遍历防护、SSRF
|
|
15
|
+
- **安全防护** - 路径遍历防护、SSRF 防护(含 IPv6)、时序安全认证、强制认证、CORS 配置
|
|
16
16
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
17
17
|
- **国际化** - 支持中英文错误信息
|
|
18
18
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
19
19
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
20
20
|
|
|
21
|
+
## 系统架构
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
## 支持的 AI 服务商
|
|
22
27
|
|
|
23
28
|
### 国际
|
|
@@ -159,11 +164,13 @@ python web.py
|
|
|
159
164
|
### 安全防护
|
|
160
165
|
|
|
161
166
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
162
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
167
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP(含 IPv6),DNS 重绑定防护
|
|
163
168
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
164
|
-
-
|
|
169
|
+
- **强制认证** - 未配置 API Key 时自动生成随机密钥,拒绝未认证请求
|
|
165
170
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
166
|
-
- **
|
|
171
|
+
- **CORS 配置** - 跨域资源共享安全配置
|
|
172
|
+
- **Webhook 安全** - 使用 `secrets.token_hex` 生成不可预测的 webhook ID
|
|
173
|
+
- **存储安全** - 密钥文件权限加固 (0o600),AES-256-GCM 加密,PBKDF2 600,000 次迭代
|
|
167
174
|
|
|
168
175
|
### API 认证
|
|
169
176
|
|
|
@@ -680,6 +687,55 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
680
687
|
|
|
681
688
|
## 更新日志
|
|
682
689
|
|
|
690
|
+
|
|
691
|
+
### v3.0.0 (2026-06-15)
|
|
692
|
+
|
|
693
|
+
- **安全修复**: 移除 API 响应中的明文密钥,只返回 key_masked
|
|
694
|
+
- **安全修复**: 强制认证,无配置时自动生成随机 API Key
|
|
695
|
+
- **安全修复**: 使用 hmac.compare_digest 防止时序攻击
|
|
696
|
+
- **安全修复**: 添加 CORS 中间件配置
|
|
697
|
+
- **性能优化**: PBKDF2 密钥缓存,迭代次数提升至 600,000 (OWASP 推荐)
|
|
698
|
+
- **性能优化**: 速率限制器添加 TTL 驱逐,防止内存泄漏
|
|
699
|
+
- **可靠性**: 修复 checker.py 合并逻辑错误
|
|
700
|
+
- **可靠性**: 替换所有裸 except 为 except Exception
|
|
701
|
+
- **可靠性**: 添加配置验证和错误处理
|
|
702
|
+
- **可靠性**: 添加 /healthz 健康检查端点
|
|
703
|
+
- **可靠性**: 修复 datetime.utcnow() 废弃警告
|
|
704
|
+
- **SSRF 防护**: 添加 IPv6 私有范围和 DNS 重绑定防护
|
|
705
|
+
- **Webhook 安全**: 使用 secrets.token_hex 替代 MD5 生成 webhook ID
|
|
706
|
+
- **存储安全**: 密钥文件权限加固 (0o600)
|
|
707
|
+
- **前端**: 模型检测列表添加自定义滚轮条样式
|
|
708
|
+
- **前端**: 修复模型类型下拉窗口被弹窗裁剪的问题
|
|
709
|
+
- **精确匹配修复**: 能力检测从子串匹配改为 ^$ 锚定精确匹配
|
|
710
|
+
- **API Bug 修复**: 修复 /api/stats/chart 运行时崩溃
|
|
711
|
+
- **测试补充**: 新增 8 个端点的测试,覆盖率 91.47%
|
|
712
|
+
|
|
713
|
+
### v2.2.2 (2026-06-14)
|
|
714
|
+
|
|
715
|
+
- **模型检测二级窗口**: 新增模型检测弹窗,支持搜索、类型筛选、全选、批量检测
|
|
716
|
+
- **模型类型图标**: 显示模型能力图标(视觉/推理/联网/工具/嵌入/重排/免费)
|
|
717
|
+
- **模型能力数据更新**: 从 Cherry Studio 同步最新模型能力数据(3184 条记录)
|
|
718
|
+
- **检测逻辑优化**: DeepSeek 欠费密钥正确识别,不再误判为其他服务商
|
|
719
|
+
- **指定模型检测**: 自动检测服务商时也支持指定模型检测
|
|
720
|
+
|
|
721
|
+
### v2.2.1 (2026-06-14)
|
|
722
|
+
|
|
723
|
+
- **检测逻辑修复**: 自动识别后必须调用 check() 验证 key 有效性
|
|
724
|
+
- **签名匹配修复**: 状态码不再硬编码 401,使用实际状态码
|
|
725
|
+
- **不可靠服务商**: ppio、nvidia、modelscope 不参与加分
|
|
726
|
+
- **模型选择器**: 新增模型下拉框,支持选择具体模型检测
|
|
727
|
+
- **Cherry Studio 同步**: 修复 ownedBy 映射(alibaba→dashscope 等)
|
|
728
|
+
- **测试补充**: 新增 test_parser_supplement、test_ssrf_supplement、test_validator_supplement
|
|
729
|
+
### v2.2.0 (2026-06-14)
|
|
730
|
+
|
|
731
|
+
- **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
|
|
732
|
+
- **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
|
|
733
|
+
- **并发优化**: /v1/models 和 chat/completions 都并发调用
|
|
734
|
+
- **超时控制**: 所有网络请求 5 秒超时
|
|
735
|
+
- **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
|
|
736
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
737
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
738
|
+
|
|
683
739
|
### v2.1.2 (2026-06-11)
|
|
684
740
|
|
|
685
741
|
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-key-manager
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Batch manage API keys for
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Batch manage API keys for 45+ AI providers with CLI and Web interfaces
|
|
5
5
|
Author: Townrain
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Townrain/API-Key-Manager
|
|
@@ -45,12 +45,17 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
|
45
45
|
- **Web 界面** - 赛博朋克风格的管理界面
|
|
46
46
|
- **代理支持** - 支持 HTTP/SOCKS 代理
|
|
47
47
|
- **加密存储** - AES-256-GCM 加密存储 API 密钥,随机盐值
|
|
48
|
-
- **安全防护** - 路径遍历防护、SSRF
|
|
48
|
+
- **安全防护** - 路径遍历防护、SSRF 防护(含 IPv6)、时序安全认证、强制认证、CORS 配置
|
|
49
49
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
50
50
|
- **国际化** - 支持中英文错误信息
|
|
51
51
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
52
52
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
53
53
|
|
|
54
|
+
## 系统架构
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
## 支持的 AI 服务商
|
|
55
60
|
|
|
56
61
|
### 国际
|
|
@@ -192,11 +197,13 @@ python web.py
|
|
|
192
197
|
### 安全防护
|
|
193
198
|
|
|
194
199
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
195
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
200
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP(含 IPv6),DNS 重绑定防护
|
|
196
201
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
197
|
-
-
|
|
202
|
+
- **强制认证** - 未配置 API Key 时自动生成随机密钥,拒绝未认证请求
|
|
198
203
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
199
|
-
- **
|
|
204
|
+
- **CORS 配置** - 跨域资源共享安全配置
|
|
205
|
+
- **Webhook 安全** - 使用 `secrets.token_hex` 生成不可预测的 webhook ID
|
|
206
|
+
- **存储安全** - 密钥文件权限加固 (0o600),AES-256-GCM 加密,PBKDF2 600,000 次迭代
|
|
200
207
|
|
|
201
208
|
### API 认证
|
|
202
209
|
|
|
@@ -713,6 +720,55 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
713
720
|
|
|
714
721
|
## 更新日志
|
|
715
722
|
|
|
723
|
+
|
|
724
|
+
### v3.0.0 (2026-06-15)
|
|
725
|
+
|
|
726
|
+
- **安全修复**: 移除 API 响应中的明文密钥,只返回 key_masked
|
|
727
|
+
- **安全修复**: 强制认证,无配置时自动生成随机 API Key
|
|
728
|
+
- **安全修复**: 使用 hmac.compare_digest 防止时序攻击
|
|
729
|
+
- **安全修复**: 添加 CORS 中间件配置
|
|
730
|
+
- **性能优化**: PBKDF2 密钥缓存,迭代次数提升至 600,000 (OWASP 推荐)
|
|
731
|
+
- **性能优化**: 速率限制器添加 TTL 驱逐,防止内存泄漏
|
|
732
|
+
- **可靠性**: 修复 checker.py 合并逻辑错误
|
|
733
|
+
- **可靠性**: 替换所有裸 except 为 except Exception
|
|
734
|
+
- **可靠性**: 添加配置验证和错误处理
|
|
735
|
+
- **可靠性**: 添加 /healthz 健康检查端点
|
|
736
|
+
- **可靠性**: 修复 datetime.utcnow() 废弃警告
|
|
737
|
+
- **SSRF 防护**: 添加 IPv6 私有范围和 DNS 重绑定防护
|
|
738
|
+
- **Webhook 安全**: 使用 secrets.token_hex 替代 MD5 生成 webhook ID
|
|
739
|
+
- **存储安全**: 密钥文件权限加固 (0o600)
|
|
740
|
+
- **前端**: 模型检测列表添加自定义滚轮条样式
|
|
741
|
+
- **前端**: 修复模型类型下拉窗口被弹窗裁剪的问题
|
|
742
|
+
- **精确匹配修复**: 能力检测从子串匹配改为 ^$ 锚定精确匹配
|
|
743
|
+
- **API Bug 修复**: 修复 /api/stats/chart 运行时崩溃
|
|
744
|
+
- **测试补充**: 新增 8 个端点的测试,覆盖率 91.47%
|
|
745
|
+
|
|
746
|
+
### v2.2.2 (2026-06-14)
|
|
747
|
+
|
|
748
|
+
- **模型检测二级窗口**: 新增模型检测弹窗,支持搜索、类型筛选、全选、批量检测
|
|
749
|
+
- **模型类型图标**: 显示模型能力图标(视觉/推理/联网/工具/嵌入/重排/免费)
|
|
750
|
+
- **模型能力数据更新**: 从 Cherry Studio 同步最新模型能力数据(3184 条记录)
|
|
751
|
+
- **检测逻辑优化**: DeepSeek 欠费密钥正确识别,不再误判为其他服务商
|
|
752
|
+
- **指定模型检测**: 自动检测服务商时也支持指定模型检测
|
|
753
|
+
|
|
754
|
+
### v2.2.1 (2026-06-14)
|
|
755
|
+
|
|
756
|
+
- **检测逻辑修复**: 自动识别后必须调用 check() 验证 key 有效性
|
|
757
|
+
- **签名匹配修复**: 状态码不再硬编码 401,使用实际状态码
|
|
758
|
+
- **不可靠服务商**: ppio、nvidia、modelscope 不参与加分
|
|
759
|
+
- **模型选择器**: 新增模型下拉框,支持选择具体模型检测
|
|
760
|
+
- **Cherry Studio 同步**: 修复 ownedBy 映射(alibaba→dashscope 等)
|
|
761
|
+
- **测试补充**: 新增 test_parser_supplement、test_ssrf_supplement、test_validator_supplement
|
|
762
|
+
### v2.2.0 (2026-06-14)
|
|
763
|
+
|
|
764
|
+
- **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
|
|
765
|
+
- **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
|
|
766
|
+
- **并发优化**: /v1/models 和 chat/completions 都并发调用
|
|
767
|
+
- **超时控制**: 所有网络请求 5 秒超时
|
|
768
|
+
- **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
|
|
769
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
770
|
+
- **新增服务商**: OpenCode Go、OpenCode Zen
|
|
771
|
+
|
|
716
772
|
### v2.1.2 (2026-06-11)
|
|
717
773
|
|
|
718
774
|
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
@@ -62,6 +62,8 @@ key_manager/providers/modelscope.py
|
|
|
62
62
|
key_manager/providers/nvidia.py
|
|
63
63
|
key_manager/providers/ocoolai.py
|
|
64
64
|
key_manager/providers/openai.py
|
|
65
|
+
key_manager/providers/opencode.py
|
|
66
|
+
key_manager/providers/opencode_zen.py
|
|
65
67
|
key_manager/providers/openrouter.py
|
|
66
68
|
key_manager/providers/perplexity.py
|
|
67
69
|
key_manager/providers/poe.py
|
|
@@ -75,6 +77,8 @@ key_manager/providers/yi.py
|
|
|
75
77
|
key_manager/providers/zai.py
|
|
76
78
|
key_manager/providers/zhipu.py
|
|
77
79
|
key_manager/providers/zhipu_coding.py
|
|
80
|
+
tests/test_api_endpoints.py
|
|
81
|
+
tests/test_base_check.py
|
|
78
82
|
tests/test_bug_fixes.py
|
|
79
83
|
tests/test_checker.py
|
|
80
84
|
tests/test_core.py
|
|
@@ -85,12 +89,15 @@ tests/test_i18n.py
|
|
|
85
89
|
tests/test_logger.py
|
|
86
90
|
tests/test_openapi.py
|
|
87
91
|
tests/test_parser.py
|
|
92
|
+
tests/test_parser_supplement.py
|
|
88
93
|
tests/test_provider_detection.py
|
|
89
94
|
tests/test_providers.py
|
|
90
95
|
tests/test_proxy.py
|
|
91
96
|
tests/test_security.py
|
|
97
|
+
tests/test_ssrf_supplement.py
|
|
92
98
|
tests/test_storage.py
|
|
93
99
|
tests/test_tester.py
|
|
94
100
|
tests/test_validator.py
|
|
101
|
+
tests/test_validator_supplement.py
|
|
95
102
|
tests/test_web_fixes.py
|
|
96
103
|
tests/test_webhook.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Key Manager - Batch API key management for
|
|
1
|
+
"""Key Manager - Batch API key management for 45+ AI providers."""
|
|
2
2
|
from key_manager.core import KeyManager
|
|
3
3
|
from key_manager.providers import PROVIDERS, get_display_name
|
|
4
4
|
from key_manager.errors import KeyManagerError, ErrorCode
|
|
@@ -13,5 +13,5 @@ __all__ = [
|
|
|
13
13
|
"KeyStore",
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
-
__version__ = "
|
|
16
|
+
__version__ = "3.0.0"
|
|
17
17
|
|
|
@@ -71,6 +71,7 @@ class KeyListResponse(BaseModel):
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
class KeyExportItem(BaseModel):
|
|
74
|
+
key_masked: str = Field(..., description="Masked key for display")
|
|
74
75
|
provider: str = Field(..., description="Provider identifier")
|
|
75
76
|
max_tokens: int | None = Field(None, description="Maximum token limit")
|
|
76
77
|
max_concurrency: int | None = Field(None, description="Maximum concurrency limit")
|
|
@@ -111,11 +112,12 @@ class ImportResponse(BaseModel):
|
|
|
111
112
|
class CheckSingleRequest(BaseModel):
|
|
112
113
|
key: str = Field(..., min_length=1, description="API key to check")
|
|
113
114
|
provider: str = Field("", description="Provider name (auto-detected if empty)")
|
|
115
|
+
model: str | None = Field(None, description="Specific model to test (tests all if empty)")
|
|
114
116
|
custom_base_url: str | None = Field(None, description="Override provider base URL")
|
|
115
117
|
|
|
116
118
|
model_config = {
|
|
117
119
|
"json_schema_extra": {
|
|
118
|
-
"examples": [{"key": "sk-abc123", "provider": "openai"}]
|
|
120
|
+
"examples": [{"key": "sk-abc123", "provider": "openai", "model": "gpt-4o"}]
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -296,23 +298,26 @@ class StatsResponse(BaseModel):
|
|
|
296
298
|
total: int = Field(0, description="Total key count")
|
|
297
299
|
|
|
298
300
|
|
|
299
|
-
class StatsChartProviderEntry(BaseModel):
|
|
300
|
-
total: int = Field(0, description="Total keys for this provider")
|
|
301
|
-
valid: int = Field(0, description="Valid keys")
|
|
302
|
-
invalid: int = Field(0, description="Invalid keys")
|
|
303
|
-
error: int = Field(0, description="Errored keys")
|
|
304
|
-
|
|
305
|
-
|
|
306
301
|
class StatsChartStatuses(BaseModel):
|
|
307
302
|
valid: int = Field(0, description="Total valid keys")
|
|
308
303
|
invalid: int = Field(0, description="Total invalid keys")
|
|
309
304
|
error: int = Field(0, description="Total errored keys")
|
|
310
305
|
unknown: int = Field(0, description="Total unknown-status keys")
|
|
311
306
|
|
|
307
|
+
class StatsChartProviderEntry(BaseModel):
|
|
308
|
+
provider: str = Field(..., description="Provider identifier")
|
|
309
|
+
display_name: str = Field("", description="Human-readable provider name")
|
|
310
|
+
total: int = Field(0, description="Total keys for this provider")
|
|
311
|
+
valid: int = Field(0, description="Valid keys")
|
|
312
|
+
invalid: int = Field(0, description="Invalid keys")
|
|
313
|
+
error: int = Field(0, description="Errored keys")
|
|
314
|
+
statuses: StatsChartStatuses = Field(
|
|
315
|
+
default_factory=StatsChartStatuses, description="Status breakdown"
|
|
316
|
+
)
|
|
312
317
|
|
|
313
318
|
class StatsChartResponse(BaseModel):
|
|
314
|
-
providers:
|
|
315
|
-
default_factory=
|
|
319
|
+
providers: list[StatsChartProviderEntry] = Field(
|
|
320
|
+
default_factory=list, description="Per-provider breakdown for charts"
|
|
316
321
|
)
|
|
317
322
|
statuses: StatsChartStatuses = Field(
|
|
318
323
|
default_factory=StatsChartStatuses, description="Global status counts"
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import json
|
|
3
2
|
from datetime import datetime
|
|
4
|
-
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
import httpx
|
|
7
5
|
|
|
8
6
|
from .providers import PROVIDERS
|
|
9
7
|
from .validator import validate_keys
|
|
10
8
|
|
|
11
|
-
|
|
12
9
|
async def run_check(keys_file: str = "./data/keys.json",
|
|
13
10
|
results_file: str = "./data/check_results.json",
|
|
14
11
|
logs_dir: str = "./data/logs",
|
|
@@ -16,14 +13,16 @@ async def run_check(keys_file: str = "./data/keys.json",
|
|
|
16
13
|
timeout: int = 30,
|
|
17
14
|
proxy: str = None,
|
|
18
15
|
retry_failed: bool = True,
|
|
19
|
-
retry_count: int = 2
|
|
16
|
+
retry_count: int = 2,
|
|
17
|
+
config: dict = None) -> dict:
|
|
20
18
|
results = await validate_keys(
|
|
21
19
|
keys_file=keys_file,
|
|
22
20
|
results_file=results_file,
|
|
23
21
|
logs_dir=logs_dir,
|
|
24
22
|
concurrency=concurrency,
|
|
25
23
|
timeout=timeout,
|
|
26
|
-
proxy=proxy
|
|
24
|
+
proxy=proxy,
|
|
25
|
+
config=config
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
# Retry failed keys
|
|
@@ -40,12 +39,18 @@ async def run_check(keys_file: str = "./data/keys.json",
|
|
|
40
39
|
concurrency=concurrency,
|
|
41
40
|
timeout=timeout,
|
|
42
41
|
proxy=proxy,
|
|
43
|
-
status_filter="error"
|
|
42
|
+
status_filter="error",
|
|
43
|
+
config=config
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
#
|
|
46
|
+
# Recompute summary from details to avoid double-counting
|
|
47
|
+
# The retry only re-checks error keys, so we need to update the summary
|
|
48
|
+
# based on what changed
|
|
49
|
+
results["summary"]["error"]["count"] = retry_results["summary"]["error"]["count"]
|
|
50
|
+
results["summary"]["error"]["keys"] = retry_results["summary"]["error"]["keys"]
|
|
47
51
|
results["summary"]["valid"]["count"] += retry_results["summary"]["valid"]["count"]
|
|
52
|
+
results["summary"]["valid"]["keys"].extend(retry_results["summary"]["valid"]["keys"])
|
|
48
53
|
results["summary"]["invalid"]["count"] += retry_results["summary"]["invalid"]["count"]
|
|
49
|
-
results["summary"]["
|
|
54
|
+
results["summary"]["invalid"]["keys"].extend(retry_results["summary"]["invalid"]["keys"])
|
|
50
55
|
|
|
51
56
|
return results
|
|
@@ -2,7 +2,7 @@ import argparse
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
|
-
from datetime import datetime, timedelta
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from rich.console import Console
|
|
@@ -153,7 +153,7 @@ def cmd_report(args, config):
|
|
|
153
153
|
return
|
|
154
154
|
|
|
155
155
|
days = args.days or 7
|
|
156
|
-
cutoff = datetime.
|
|
156
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
157
157
|
|
|
158
158
|
stats = {}
|
|
159
159
|
for key, info in data["keys"].items():
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import yaml
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
DEFAULT_CONFIG = {
|
|
9
|
+
"scan": {
|
|
10
|
+
"directories": ["./data/input"],
|
|
11
|
+
"recursive": True,
|
|
12
|
+
},
|
|
13
|
+
"check": {
|
|
14
|
+
"interval_hours": 6,
|
|
15
|
+
"timeout_seconds": 30,
|
|
16
|
+
"concurrency": 100,
|
|
17
|
+
"retry_failed": True,
|
|
18
|
+
"retry_count": 2,
|
|
19
|
+
},
|
|
20
|
+
"test": {
|
|
21
|
+
"token_test": True,
|
|
22
|
+
"token_auto_detect": True,
|
|
23
|
+
"token_steps": [1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576],
|
|
24
|
+
"token_max_manual": None,
|
|
25
|
+
"concurrency_test": True,
|
|
26
|
+
"concurrency_steps": [1, 5, 10, 20, 50, 100],
|
|
27
|
+
"concurrency_timeout_seconds": 120,
|
|
28
|
+
},
|
|
29
|
+
"storage": {
|
|
30
|
+
"keys_file": "./data/keys.json",
|
|
31
|
+
"check_results_file": "./data/check_results.json",
|
|
32
|
+
"test_results_file": "./data/test_results.json",
|
|
33
|
+
"logs_dir": "./data/logs",
|
|
34
|
+
},
|
|
35
|
+
"auth": {
|
|
36
|
+
"api_key": "",
|
|
37
|
+
},
|
|
38
|
+
"rate_limit": {
|
|
39
|
+
"requests_per_minute": 60,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
45
|
+
result = base.copy()
|
|
46
|
+
for key, value in override.items():
|
|
47
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
48
|
+
result[key] = _deep_merge(result[key], value)
|
|
49
|
+
else:
|
|
50
|
+
result[key] = value
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_config(config: dict) -> dict:
|
|
55
|
+
"""Validate config values and warn about unknown keys."""
|
|
56
|
+
valid_keys = set(DEFAULT_CONFIG.keys())
|
|
57
|
+
unknown_keys = set(config.keys()) - valid_keys
|
|
58
|
+
if unknown_keys:
|
|
59
|
+
logger.warning(f"Unknown config keys (ignored): {unknown_keys}")
|
|
60
|
+
|
|
61
|
+
# Validate numeric values
|
|
62
|
+
check_config = config.get("check", {})
|
|
63
|
+
if check_config.get("concurrency", 100) < 1:
|
|
64
|
+
logger.warning("check.concurrency must be >= 1, using default")
|
|
65
|
+
check_config["concurrency"] = 100
|
|
66
|
+
if check_config.get("timeout_seconds", 30) < 1:
|
|
67
|
+
logger.warning("check.timeout_seconds must be >= 1, using default")
|
|
68
|
+
check_config["timeout_seconds"] = 30
|
|
69
|
+
|
|
70
|
+
rate_limit = config.get("rate_limit", {})
|
|
71
|
+
if rate_limit.get("requests_per_minute", 60) < 0:
|
|
72
|
+
logger.warning("rate_limit.requests_per_minute must be >= 0, using default")
|
|
73
|
+
rate_limit["requests_per_minute"] = 60
|
|
74
|
+
|
|
75
|
+
return config
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_config(path: str = "config.yaml") -> dict:
|
|
79
|
+
config_path = Path(path)
|
|
80
|
+
if config_path.exists():
|
|
81
|
+
try:
|
|
82
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
83
|
+
user_config = yaml.safe_load(f) or {}
|
|
84
|
+
if not isinstance(user_config, dict):
|
|
85
|
+
logger.warning(f"Config file {path} is not a dict, using defaults")
|
|
86
|
+
return copy.deepcopy(DEFAULT_CONFIG)
|
|
87
|
+
config = _deep_merge(DEFAULT_CONFIG, user_config)
|
|
88
|
+
return _validate_config(config)
|
|
89
|
+
except yaml.YAMLError as e:
|
|
90
|
+
logger.error(f"Failed to parse config file {path}: {e}")
|
|
91
|
+
return copy.deepcopy(DEFAULT_CONFIG)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Failed to load config file {path}: {e}")
|
|
94
|
+
return copy.deepcopy(DEFAULT_CONFIG)
|
|
95
|
+
return copy.deepcopy(DEFAULT_CONFIG)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def save_config(config: dict, path: str = "config.yaml"):
|
|
99
|
+
try:
|
|
100
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
101
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Failed to save config file {path}: {e}")
|