完善配置密钥治理与启动校验
- 为 configuration.py 增加环境变量占位符解析、配置归一化、脱敏快照与启动校验\n- 在 main.py 启动阶段接入配置校验日志,并在致命缺项时阻止进程继续启动\n- 新增 config.example.yaml,并将默认 config.yaml 改为安全占位模板,移除仓库内明文敏感信息\n- 调整 docker-entrypoint.sh 与文档,统一说明配置复制、环境变量注入与当前优化进展
This commit is contained in:
51
README.MD
51
README.MD
@@ -126,28 +126,49 @@ sudo apt-get install -y fonts-noto-color-emoji fonts-noto-cjk fonts-wqy-microhei
|
||||
|
||||
### 1. 配置文件
|
||||
|
||||
配置文件位于 `config.yaml`,包含以下主要配置项:
|
||||
推荐先复制 `config.example.yaml` 为 `config.yaml`,再通过环境变量注入敏感信息:
|
||||
|
||||
```bash
|
||||
# Linux / Mac
|
||||
cp config.example.yaml config.yaml
|
||||
export ABOT_DB_PASSWORD="你的数据库密码"
|
||||
export ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY="你的 Dify Key"
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item config.example.yaml config.yaml
|
||||
$env:ABOT_DB_PASSWORD="你的数据库密码"
|
||||
$env:ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY="你的 Dify Key"
|
||||
```
|
||||
|
||||
`config.yaml` 现已支持 `${ENV_NAME}` / `${ENV_NAME:默认值}` 两种写法:
|
||||
|
||||
- `${ABOT_DB_PASSWORD}`:必须由环境变量提供,否则启动时报错
|
||||
- `${ABOT_DB_HOST:127.0.0.1}`:若环境变量缺失,则回退默认值
|
||||
|
||||
启动时系统会自动执行配置完整性检查,并在日志中输出脱敏后的配置快照。包含以下主要配置项:
|
||||
|
||||
#### 数据库配置
|
||||
|
||||
```yaml
|
||||
db_config:
|
||||
pool_name: "wechat_boot_pool"
|
||||
pool_size: 10
|
||||
host: "your-db-host"
|
||||
user: "your-db-user"
|
||||
password: "your-db-password"
|
||||
database: "message_archive"
|
||||
charset: "utf8mb4"
|
||||
pool_name: "${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||
pool_size: "${ABOT_DB_POOL_SIZE:10}"
|
||||
host: "${ABOT_DB_HOST:127.0.0.1}"
|
||||
port: "${ABOT_DB_PORT:3306}"
|
||||
user: "${ABOT_DB_USER:root}"
|
||||
password: "${ABOT_DB_PASSWORD}"
|
||||
database: "${ABOT_DB_NAME:message_archive}"
|
||||
charset: "${ABOT_DB_CHARSET:utf8mb4}"
|
||||
```
|
||||
|
||||
#### Redis配置
|
||||
|
||||
```yaml
|
||||
redis_config:
|
||||
host: "your-redis-host"
|
||||
port: 6379
|
||||
db: 0
|
||||
host: "${ABOT_REDIS_HOST:127.0.0.1}"
|
||||
port: "${ABOT_REDIS_PORT:6379}"
|
||||
password: "${ABOT_REDIS_PASSWORD:}"
|
||||
db: "${ABOT_REDIS_DB:0}"
|
||||
decode_responses: true
|
||||
```
|
||||
#### ipad 客户端配置
|
||||
@@ -272,9 +293,9 @@ abot/
|
||||
### 开发规范
|
||||
|
||||
- 遵循PEP 8编码规范
|
||||
- 添加适当的注释
|
||||
- 编写单元测试
|
||||
- 更新文档
|
||||
- 添加适当的中文注释
|
||||
- 优先补齐文档与人工验证步骤
|
||||
- 敏感配置优先使用环境变量注入
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
@@ -342,4 +363,4 @@ python -m pip install --upgrade pip
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为本项目做出贡献的开发者。
|
||||
感谢所有为本项目做出贡献的开发者。
|
||||
|
||||
141
config.example.yaml
Normal file
141
config.example.yaml
Normal file
@@ -0,0 +1,141 @@
|
||||
environment: "${ABOT_ENVIRONMENT:development}"
|
||||
plugin_dir: "${ABOT_PLUGIN_DIR:plugins}"
|
||||
|
||||
db_config:
|
||||
pool_name: "${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||
pool_size: "${ABOT_DB_POOL_SIZE:10}"
|
||||
host: "${ABOT_DB_HOST:127.0.0.1}"
|
||||
# 新配置统一使用 port;prot 仅作为历史兼容字段保留。
|
||||
port: "${ABOT_DB_PORT:3306}"
|
||||
prot: "${ABOT_DB_PORT:3306}"
|
||||
user: "${ABOT_DB_USER:root}"
|
||||
password: "${ABOT_DB_PASSWORD}"
|
||||
database: "${ABOT_DB_NAME:message_archive}"
|
||||
charset: "${ABOT_DB_CHARSET:utf8mb4}"
|
||||
use_unicode: true
|
||||
get_warnings: true
|
||||
pool_reset_session: true
|
||||
|
||||
redis_config:
|
||||
host: "${ABOT_REDIS_HOST:127.0.0.1}"
|
||||
port: "${ABOT_REDIS_PORT:6379}"
|
||||
password: "${ABOT_REDIS_PASSWORD:}"
|
||||
db: "${ABOT_REDIS_DB:0}"
|
||||
decode_responses: true
|
||||
|
||||
# 邮件发送配置
|
||||
email_config:
|
||||
smtp_server: "${ABOT_EMAIL_SMTP_SERVER:smtp.163.com}"
|
||||
smtp_port: "${ABOT_EMAIL_SMTP_PORT:465}"
|
||||
sender_email: "${ABOT_EMAIL_SENDER:}"
|
||||
sender_password: "${ABOT_EMAIL_PASSWORD:}"
|
||||
alert_recipient: "${ABOT_EMAIL_ALERT_RECIPIENT:}"
|
||||
|
||||
glances:
|
||||
host: "${ABOT_GLANCES_HOST:127.0.0.1}"
|
||||
port: "${ABOT_GLANCES_PORT:61208}"
|
||||
|
||||
wx_config:
|
||||
# 微信管理账号,用于接收部分管理员指令。
|
||||
admin: [ "${ABOT_WX_ADMIN:admin}" ]
|
||||
|
||||
llm:
|
||||
default_backend: "${ABOT_LLM_DEFAULT_BACKEND:dify_workflow_chat}"
|
||||
backends:
|
||||
dify_workflow_chat:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "${ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
response_mode: "blocking"
|
||||
request_timeout: 120
|
||||
max_retries: 1
|
||||
retry_delay_seconds: 1.0
|
||||
dify_workflow_member_context:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "${ABOT_LLM_DIFY_MEMBER_CONTEXT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "text"
|
||||
response_mode: "streaming"
|
||||
request_timeout: 240
|
||||
dify_workflow_message_summary:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "${ABOT_LLM_DIFY_MESSAGE_SUMMARY_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "text"
|
||||
response_mode: "streaming"
|
||||
request_timeout: 180
|
||||
dify_workflow_douyu_daily_report:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "${ABOT_LLM_DIFY_DOUYU_REPORT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "text"
|
||||
response_mode: "blocking"
|
||||
request_timeout: 240
|
||||
dify_chat_global_news:
|
||||
provider: "dify"
|
||||
mode: "chat"
|
||||
api_key: "${ABOT_LLM_DIFY_GLOBAL_NEWS_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "chat-messages"
|
||||
response_mode: "blocking"
|
||||
request_timeout: 60
|
||||
openai_compatible_game_task:
|
||||
provider: "openai_compatible"
|
||||
api_url: "${ABOT_LLM_GAME_TASK_API_URL:https://api.example.com/v1/chat/completions}"
|
||||
api_key: "${ABOT_LLM_GAME_TASK_API_KEY:}"
|
||||
model: "${ABOT_LLM_GAME_TASK_MODEL:doubao-1-5-lite-32k-250115}"
|
||||
stream: false
|
||||
temperature: 0.2
|
||||
max_tokens: 1000
|
||||
timeout_seconds: 60
|
||||
openai_compatible_ai_auto_response:
|
||||
provider: "openai_compatible"
|
||||
api_base_url: "${ABOT_LLM_AUTO_REPLY_API_BASE_URL:https://api.example.com/v1}"
|
||||
endpoint: "chat/completions"
|
||||
api_key: "${ABOT_LLM_AUTO_REPLY_API_KEY:}"
|
||||
model: "${ABOT_LLM_AUTO_REPLY_MODEL:gpt-5.4}"
|
||||
stream: true
|
||||
temperature: 0.35
|
||||
max_tokens: 120
|
||||
timeout_seconds: 45
|
||||
max_retries: 3
|
||||
retry_delay_seconds: 1.0
|
||||
dify_workflow_ai_auto_response:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "${ABOT_LLM_DIFY_AUTO_REPLY_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "result_json"
|
||||
response_mode: "blocking"
|
||||
request_timeout: 15
|
||||
max_retries: 1
|
||||
retry_delay_seconds: 1.0
|
||||
openai_compatible_ai_gen_image:
|
||||
provider: "openai_compatible"
|
||||
api_base_url: "${ABOT_LLM_IMAGE_API_BASE_URL:https://api.example.com/v1}"
|
||||
endpoint: "chat/completions"
|
||||
api_key: "${ABOT_LLM_IMAGE_API_KEY:}"
|
||||
model: "${ABOT_LLM_IMAGE_MODEL:gpt-image-1}"
|
||||
stream: false
|
||||
timeout_seconds: 300
|
||||
max_retries: 2
|
||||
retry_delay_seconds: 1.0
|
||||
scenes:
|
||||
"chat.main": "dify_workflow_chat"
|
||||
"member.profile": "dify_workflow_member_context"
|
||||
"summary.daily": "dify_workflow_message_summary"
|
||||
"douyu.daily_report": "dify_workflow_douyu_daily_report"
|
||||
"news.global": "dify_chat_global_news"
|
||||
"game.task": "openai_compatible_game_task"
|
||||
"auto_reply.group": "dify_workflow_ai_auto_response"
|
||||
"member_roast": "openai_compatible_ai_auto_response"
|
||||
"image.generate": "openai_compatible_ai_gen_image"
|
||||
112
config.yaml
112
config.yaml
@@ -1,64 +1,62 @@
|
||||
environment: "${ABOT_ENVIRONMENT:development}"
|
||||
plugin_dir: "${ABOT_PLUGIN_DIR:plugins}"
|
||||
|
||||
db_config:
|
||||
pool_name: "wechat_boot_pool"
|
||||
pool_size: 10
|
||||
host: "192.168.2.41"
|
||||
prot: "3306"
|
||||
user: "root"
|
||||
password: "lw123456"
|
||||
database: "message_archive"
|
||||
charset: "utf8mb4"
|
||||
pool_name: "${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||
pool_size: "${ABOT_DB_POOL_SIZE:10}"
|
||||
host: "${ABOT_DB_HOST:127.0.0.1}"
|
||||
# 新配置统一使用 port;prot 仅作为历史兼容字段保留。
|
||||
port: "${ABOT_DB_PORT:3306}"
|
||||
prot: "${ABOT_DB_PORT:3306}"
|
||||
user: "${ABOT_DB_USER:root}"
|
||||
password: "${ABOT_DB_PASSWORD}"
|
||||
database: "${ABOT_DB_NAME:message_archive}"
|
||||
charset: "${ABOT_DB_CHARSET:utf8mb4}"
|
||||
use_unicode: true
|
||||
get_warnings: true
|
||||
pool_reset_session: true
|
||||
|
||||
redis_config:
|
||||
host: "192.168.2.40"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
host: "${ABOT_REDIS_HOST:127.0.0.1}"
|
||||
port: "${ABOT_REDIS_PORT:6379}"
|
||||
password: "${ABOT_REDIS_PASSWORD:}"
|
||||
db: "${ABOT_REDIS_DB:0}"
|
||||
decode_responses: true
|
||||
|
||||
|
||||
# 邮件发送配置
|
||||
email_config:
|
||||
smtp_server: "smtp.163.com"
|
||||
smtp_port: 465
|
||||
sender_email: "bovine_liu@163.com"
|
||||
sender_password: "CCWpEQzSdxQUqhDE"
|
||||
alert_recipient: "bovine_liu@163.com" # 警报邮件接收者
|
||||
smtp_server: "${ABOT_EMAIL_SMTP_SERVER:smtp.163.com}"
|
||||
smtp_port: "${ABOT_EMAIL_SMTP_PORT:465}"
|
||||
sender_email: "${ABOT_EMAIL_SENDER:}"
|
||||
sender_password: "${ABOT_EMAIL_PASSWORD:}"
|
||||
alert_recipient: "${ABOT_EMAIL_ALERT_RECIPIENT:}"
|
||||
|
||||
glances:
|
||||
host: "192.168.2.170"
|
||||
port: 61208
|
||||
|
||||
host: "${ABOT_GLANCES_HOST:127.0.0.1}"
|
||||
port: "${ABOT_GLANCES_PORT:61208}"
|
||||
|
||||
wx_config:
|
||||
#微信管理账号,用于接收部分管理员指令
|
||||
#菜单调整和系统更新
|
||||
admin: [ "Jyunere" ]
|
||||
# 微信管理账号,用于接收部分管理员指令。
|
||||
admin: [ "${ABOT_WX_ADMIN:admin}" ]
|
||||
|
||||
llm:
|
||||
default_backend: "dify_workflow_chat"
|
||||
default_backend: "${ABOT_LLM_DEFAULT_BACKEND:dify_workflow_chat}"
|
||||
backends:
|
||||
dify_workflow_chat:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "app-u5EnYq3ill19bm6pWJwGkY4D"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
response_mode: "blocking"
|
||||
# 聊天工作流偶尔会超过 40 秒:
|
||||
# 1. 原先 40 秒超时会导致客户端提前放弃;
|
||||
# 2. 本地统一客户端默认又会自动重试,容易在 Dify 后台看到同一问题连续触发 3 次;
|
||||
# 3. 这里把超时提高到 120 秒,并将重试次数收敛为 1,避免重复触发整条工作流。
|
||||
request_timeout: 120
|
||||
max_retries: 1
|
||||
retry_delay_seconds: 1.0
|
||||
dify_workflow_member_context:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "app-b2cj03DipGCIAmgBfcx7SKsT"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_MEMBER_CONTEXT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "text"
|
||||
response_mode: "streaming"
|
||||
@@ -66,8 +64,8 @@ llm:
|
||||
dify_workflow_message_summary:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "app-shCA6bo5l2VDmnvhg2BtuJbk"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_MESSAGE_SUMMARY_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "text"
|
||||
response_mode: "streaming"
|
||||
@@ -75,38 +73,35 @@ llm:
|
||||
dify_workflow_douyu_daily_report:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
# 斗鱼日报专用工作流:请替换为你在 Dify 上创建的“斗鱼日报”应用 Key。
|
||||
api_key: "app-S1oyi2udgIn197Vu0oOGUgAl"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_DOUYU_REPORT_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
# 工作流最终输出字段建议固定为 text,便于统一客户端直接读取结果文本。
|
||||
workflow_output_key: "text"
|
||||
response_mode: "blocking"
|
||||
# 斗鱼日报 payload 较大,适当提高超时时间,避免高峰时段超时回退。
|
||||
request_timeout: 240
|
||||
dify_chat_global_news:
|
||||
provider: "dify"
|
||||
mode: "chat"
|
||||
api_key: "app-rhhKkbvHd2IAQoGX7xTzXZJj"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_GLOBAL_NEWS_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "chat-messages"
|
||||
response_mode: "blocking"
|
||||
request_timeout: 60
|
||||
openai_compatible_game_task:
|
||||
provider: "openai_compatible"
|
||||
api_url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
api_key: "b8586595-eb81-483d-8e91-a35cc789729e"
|
||||
model: "doubao-1-5-lite-32k-250115"
|
||||
api_url: "${ABOT_LLM_GAME_TASK_API_URL:https://api.example.com/v1/chat/completions}"
|
||||
api_key: "${ABOT_LLM_GAME_TASK_API_KEY:}"
|
||||
model: "${ABOT_LLM_GAME_TASK_MODEL:doubao-1-5-lite-32k-250115}"
|
||||
stream: false
|
||||
temperature: 0.2
|
||||
max_tokens: 1000
|
||||
timeout_seconds: 60
|
||||
openai_compatible_ai_auto_response:
|
||||
provider: "openai_compatible"
|
||||
api_base_url: "http://192.168.2.240:3000/v1"
|
||||
api_base_url: "${ABOT_LLM_AUTO_REPLY_API_BASE_URL:https://api.example.com/v1}"
|
||||
endpoint: "chat/completions"
|
||||
api_key: "sk-hC6WMLAsTdItpywyrYdxT6pQ4E7NARGbUKuPWRH0zMheen9e"
|
||||
model: "gpt-5.4"
|
||||
api_key: "${ABOT_LLM_AUTO_REPLY_API_KEY:}"
|
||||
model: "${ABOT_LLM_AUTO_REPLY_MODEL:gpt-5.4}"
|
||||
stream: true
|
||||
temperature: 0.35
|
||||
max_tokens: 120
|
||||
@@ -116,35 +111,24 @@ llm:
|
||||
dify_workflow_ai_auto_response:
|
||||
provider: "dify"
|
||||
mode: "workflow"
|
||||
api_key: "app-ukHWWGoleANS5aZVmx28UAQ4"
|
||||
api_base_url: "http://192.168.2.240/v1"
|
||||
api_key: "${ABOT_LLM_DIFY_AUTO_REPLY_API_KEY:}"
|
||||
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||
endpoint: "workflows/run"
|
||||
workflow_output_key: "result_json"
|
||||
response_mode: "blocking"
|
||||
# 群聊自动回复强调时效性:
|
||||
# 1. Dify 请求不能等太久,否则容易出现“过了场子再补回”的违和感;
|
||||
# 2. 这里把单次请求超时收紧,并关闭重试,让过期消息尽快放弃。
|
||||
request_timeout: 15
|
||||
max_retries: 1
|
||||
retry_delay_seconds: 1.0
|
||||
openai_compatible_ai_gen_image:
|
||||
provider: "openai_compatible"
|
||||
# AI 绘图专用网关:
|
||||
# 1. 这里使用用户提供的 OpenAI 兼容服务地址;
|
||||
# 2. 插件会在此 base_url 基础上请求 images/generations;
|
||||
# 3. endpoint 保留为图片接口默认值,便于后续统一调整。
|
||||
api_base_url: "https://freeapi.dgbmc.top/v1"
|
||||
api_base_url: "${ABOT_LLM_IMAGE_API_BASE_URL:https://api.example.com/v1}"
|
||||
endpoint: "chat/completions"
|
||||
api_key: "sk-2XccrBRsX8OmxqCEsZjdDRczhHNaAG7Mn88mNVL7Y0w0tx72"
|
||||
# 图片模型默认使用 gpt-image-1;
|
||||
# 若网关只支持其他模型,可后续直接在这里替换。
|
||||
model: "gpt-image-2"
|
||||
api_key: "${ABOT_LLM_IMAGE_API_KEY:}"
|
||||
model: "${ABOT_LLM_IMAGE_MODEL:gpt-image-1}"
|
||||
stream: false
|
||||
timeout_seconds: 300
|
||||
max_retries: 2
|
||||
retry_delay_seconds: 1.0
|
||||
# 场景路由层:插件建议优先使用 scene,而不是直接绑定 backend。
|
||||
# 这样当模型或供应商切换时,只需要改这里,不需要逐个改插件配置。
|
||||
scenes:
|
||||
"chat.main": "dify_workflow_chat"
|
||||
"member.profile": "dify_workflow_member_context"
|
||||
|
||||
389
configuration.py
389
configuration.py
@@ -1,35 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging.config
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self) -> None:
|
||||
"""全局配置加载器。
|
||||
|
||||
设计目标:
|
||||
1. 继续兼容项目原有的 `config.yaml` 结构,避免一次性重构过大;
|
||||
2. 支持 `${ENV_NAME}` / `${ENV_NAME:default}` 形式的环境变量注入;
|
||||
3. 在启动阶段尽早发现缺项、弱配置和明文敏感信息,降低误配置风险;
|
||||
4. 为后续后台脱敏展示、配置巡检等能力预留统一入口。
|
||||
"""
|
||||
|
||||
# 环境变量占位符格式:
|
||||
# 1. `${ABOT_DB_PASSWORD}` 表示必须从环境变量读取;
|
||||
# 2. `${ABOT_DB_HOST:127.0.0.1}` 表示环境变量缺失时回退默认值;
|
||||
# 3. 这里允许字母、数字和下划线,足够覆盖常见部署变量命名。
|
||||
ENV_PATTERN = re.compile(r"\$\{([A-Za-z0-9_]+)(?::([^}]*))?\}")
|
||||
|
||||
# 敏感字段关键字:
|
||||
# 1. 用于识别需要脱敏的配置项;
|
||||
# 2. 同时用于扫描原始 YAML 中是否仍有明文敏感值;
|
||||
# 3. 采用“关键字包含”而不是完全等值,兼容 `sender_password` / `api_key` 等不同命名。
|
||||
SENSITIVE_KEYWORDS = {
|
||||
"password",
|
||||
"passwd",
|
||||
"secret",
|
||||
"token",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"access_key",
|
||||
"private_key",
|
||||
}
|
||||
|
||||
def __init__(self, config_path: str = None) -> None:
|
||||
self.project_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.config_path = config_path or os.path.join(self.project_dir, "config.yaml")
|
||||
self.raw_config = {}
|
||||
self.resolved_config = {}
|
||||
self.unresolved_placeholders = []
|
||||
self.validation_report = {"errors": [], "warnings": []}
|
||||
self.reload()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
pwd = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f"{pwd}/config.yaml", "rb") as fp:
|
||||
yconfig = yaml.safe_load(fp)
|
||||
"""从磁盘读取 YAML 配置。"""
|
||||
with open(self.config_path, "r", encoding="utf-8") as fp:
|
||||
yconfig = yaml.safe_load(fp) or {}
|
||||
return yconfig
|
||||
|
||||
def _resolve_env_placeholders_in_string(self, raw_value: str, path: str) -> str:
|
||||
"""解析字符串中的环境变量占位符。"""
|
||||
|
||||
def _replace(match: re.Match) -> str:
|
||||
env_name = str(match.group(1) or "").strip()
|
||||
default_value = match.group(2)
|
||||
env_value = os.environ.get(env_name)
|
||||
|
||||
# 优先使用环境变量的真实值;
|
||||
# 如果环境变量不存在,但模板给了默认值,则回退默认值;
|
||||
# 如果两者都没有,则记录为“启动期缺失”,由 validate() 输出致命错误。
|
||||
if env_value not in (None, ""):
|
||||
return str(env_value)
|
||||
if default_value is not None:
|
||||
return str(default_value)
|
||||
|
||||
self.unresolved_placeholders.append({
|
||||
"path": path,
|
||||
"env_name": env_name,
|
||||
})
|
||||
return ""
|
||||
|
||||
return self.ENV_PATTERN.sub(_replace, raw_value)
|
||||
|
||||
def _resolve_config_tree(self, node, path: str = "root"):
|
||||
"""递归解析整棵配置树中的占位符。"""
|
||||
if isinstance(node, dict):
|
||||
return {
|
||||
key: self._resolve_config_tree(value, f"{path}.{key}")
|
||||
for key, value in node.items()
|
||||
}
|
||||
if isinstance(node, list):
|
||||
return [
|
||||
self._resolve_config_tree(value, f"{path}[{index}]")
|
||||
for index, value in enumerate(node)
|
||||
]
|
||||
if isinstance(node, str):
|
||||
return self._resolve_env_placeholders_in_string(node, path)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def _safe_int(value, default: int):
|
||||
"""把 YAML / 环境变量中的数字字符串安全转成整数。"""
|
||||
try:
|
||||
if value in (None, ""):
|
||||
return default
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def _normalize_config(self, yconfig: dict) -> dict:
|
||||
"""对解析后的配置做一次结构与类型归一化。"""
|
||||
normalized = copy.deepcopy(yconfig or {})
|
||||
|
||||
# 数据库配置归一化:
|
||||
# 1. 历史配置长期使用 `prot` 拼写;
|
||||
# 2. `db.connection` 代码层已经统一读取 `port`;
|
||||
# 3. 因此这里同时回填 `port/prot`,确保新老配置都可运行。
|
||||
db_config = dict(normalized.get("db_config", {}) or {})
|
||||
db_port = db_config.get("port", db_config.get("prot", 3306))
|
||||
db_config["port"] = self._safe_int(db_port, 3306)
|
||||
db_config["prot"] = db_config["port"]
|
||||
db_config["pool_size"] = self._safe_int(db_config.get("pool_size", 10), 10)
|
||||
normalized["db_config"] = db_config
|
||||
|
||||
# Redis / 邮件 / Glances 配置中不少值来自环境变量,解析后先统一转型,
|
||||
# 这样后续业务代码就不需要到处防守“字符串数字”的情况。
|
||||
redis_config = dict(normalized.get("redis_config", {}) or {})
|
||||
redis_config["port"] = self._safe_int(redis_config.get("port", 6379), 6379)
|
||||
redis_config["db"] = self._safe_int(redis_config.get("db", 0), 0)
|
||||
redis_config["max_connections"] = self._safe_int(redis_config.get("max_connections", 30), 30)
|
||||
normalized["redis_config"] = redis_config
|
||||
|
||||
email_config = dict(normalized.get("email_config", {}) or {})
|
||||
email_config["smtp_port"] = self._safe_int(email_config.get("smtp_port", 465), 465)
|
||||
normalized["email_config"] = email_config
|
||||
|
||||
glances_config = dict(normalized.get("glances", {}) or {})
|
||||
glances_config["port"] = self._safe_int(glances_config.get("port", 61208), 61208)
|
||||
normalized["glances"] = glances_config
|
||||
|
||||
return normalized
|
||||
|
||||
@classmethod
|
||||
def _contains_placeholder(cls, value: str) -> bool:
|
||||
"""判断原始字符串是否仍包含环境变量模板。"""
|
||||
return bool(cls.ENV_PATTERN.search(str(value or "")))
|
||||
|
||||
@classmethod
|
||||
def _is_sensitive_key(cls, key: str) -> bool:
|
||||
lowered_key = str(key or "").strip().lower()
|
||||
return any(keyword in lowered_key for keyword in cls.SENSITIVE_KEYWORDS)
|
||||
|
||||
def _append_issue(self, bucket: list, code: str, path: str, message: str) -> None:
|
||||
"""统一追加配置问题,便于后续日志输出与后台展示。"""
|
||||
bucket.append({
|
||||
"code": code,
|
||||
"path": path,
|
||||
"message": message,
|
||||
})
|
||||
|
||||
def _validate_required_sections(self, report: dict) -> None:
|
||||
"""检查核心运行依赖是否完整。"""
|
||||
db_config = self.mariadb or {}
|
||||
redis_config = self.redis or {}
|
||||
llm_config = self.llm or {}
|
||||
llm_backends = dict(llm_config.get("backends", {}) or {})
|
||||
default_backend = str(llm_config.get("default_backend", "") or "").strip()
|
||||
|
||||
required_db_fields = {
|
||||
"host": "数据库 host",
|
||||
"user": "数据库 user",
|
||||
"password": "数据库 password",
|
||||
"database": "数据库 database",
|
||||
}
|
||||
for field_name, display_name in required_db_fields.items():
|
||||
if not str(db_config.get(field_name, "") or "").strip():
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"missing_db_field",
|
||||
f"db_config.{field_name}",
|
||||
f"{display_name} 未配置,机器人无法正常连接 MySQL。",
|
||||
)
|
||||
|
||||
if not db_config.get("port"):
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"missing_db_port",
|
||||
"db_config.port",
|
||||
"数据库 port 未配置,机器人无法正常连接 MySQL。",
|
||||
)
|
||||
|
||||
if not str(redis_config.get("host", "") or "").strip():
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"missing_redis_host",
|
||||
"redis_config.host",
|
||||
"Redis host 未配置,机器人无法正常连接 Redis。",
|
||||
)
|
||||
|
||||
if not redis_config.get("port"):
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"missing_redis_port",
|
||||
"redis_config.port",
|
||||
"Redis port 未配置,机器人无法正常连接 Redis。",
|
||||
)
|
||||
|
||||
if not llm_backends:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"missing_llm_backends",
|
||||
"llm.backends",
|
||||
"当前未配置任何 LLM backend,依赖 AI 的插件将不可用。",
|
||||
)
|
||||
return
|
||||
|
||||
if not default_backend:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"missing_default_llm_backend",
|
||||
"llm.default_backend",
|
||||
"未配置 llm.default_backend,建议指定默认 AI 路由。",
|
||||
)
|
||||
elif default_backend not in llm_backends:
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"invalid_default_llm_backend",
|
||||
"llm.default_backend",
|
||||
f"默认 backend `{default_backend}` 不存在于 llm.backends 中。",
|
||||
)
|
||||
|
||||
def _validate_email_config(self, report: dict) -> None:
|
||||
"""检查邮件告警配置是否处于“半配置”状态。"""
|
||||
email_config = self.email or {}
|
||||
sender_email = str(email_config.get("sender_email", "") or "").strip()
|
||||
sender_password = str(email_config.get("sender_password", "") or "").strip()
|
||||
alert_recipient = str(email_config.get("alert_recipient", "") or "").strip()
|
||||
|
||||
if sender_email and not sender_password:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"missing_email_password",
|
||||
"email_config.sender_password",
|
||||
"已配置 sender_email,但缺少 sender_password,邮件告警发送会失败。",
|
||||
)
|
||||
|
||||
if alert_recipient and (not sender_email or not sender_password):
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"email_alert_incomplete",
|
||||
"email_config.alert_recipient",
|
||||
"已配置告警接收人,但发件邮箱配置不完整,告警链路不可用。",
|
||||
)
|
||||
|
||||
def _validate_llm_config(self, report: dict) -> None:
|
||||
"""检查 LLM 配置的完整性与路由一致性。"""
|
||||
llm_config = self.llm or {}
|
||||
backends = dict(llm_config.get("backends", {}) or {})
|
||||
scenes = dict(llm_config.get("scenes", {}) or {})
|
||||
|
||||
for backend_name, backend_config in backends.items():
|
||||
backend_config = backend_config or {}
|
||||
provider = str(backend_config.get("provider", "") or "").strip()
|
||||
if not provider:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"missing_llm_provider",
|
||||
f"llm.backends.{backend_name}.provider",
|
||||
f"LLM backend `{backend_name}` 未配置 provider。",
|
||||
)
|
||||
|
||||
# 对接第三方 AI 服务时,api_key 通常是最容易漏配的关键项;
|
||||
# 这里把空值直接标成 warning,既不会误伤“暂未启用的 backend”,又能在启动期给出提醒。
|
||||
api_key = str(backend_config.get("api_key", "") or "").strip()
|
||||
if not api_key:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"missing_llm_api_key",
|
||||
f"llm.backends.{backend_name}.api_key",
|
||||
f"LLM backend `{backend_name}` 未配置 api_key,相关 AI 能力将不可用。",
|
||||
)
|
||||
|
||||
for scene_name, backend_name in scenes.items():
|
||||
backend_name = str(backend_name or "").strip()
|
||||
if backend_name and backend_name not in backends:
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"invalid_llm_scene_backend",
|
||||
f"llm.scenes.{scene_name}",
|
||||
f"场景 `{scene_name}` 指向了不存在的 backend `{backend_name}`。",
|
||||
)
|
||||
|
||||
def _validate_unresolved_placeholders(self, report: dict) -> None:
|
||||
"""把缺失环境变量转换为启动期可读错误。"""
|
||||
for unresolved_item in self.unresolved_placeholders:
|
||||
self._append_issue(
|
||||
report["errors"],
|
||||
"missing_environment_variable",
|
||||
unresolved_item.get("path", "root"),
|
||||
f"环境变量 `{unresolved_item.get('env_name', '')}` 未提供,且未设置默认值。",
|
||||
)
|
||||
|
||||
def _validate_plaintext_secrets(self, report: dict) -> None:
|
||||
"""扫描原始 YAML 中是否仍保留明文敏感配置。"""
|
||||
|
||||
def _walk(node, path: str = "root") -> None:
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
next_path = f"{path}.{key}"
|
||||
if isinstance(value, str) and self._is_sensitive_key(key):
|
||||
stripped_value = value.strip()
|
||||
if stripped_value and not self._contains_placeholder(stripped_value):
|
||||
self._append_issue(
|
||||
report["warnings"],
|
||||
"plaintext_sensitive_value",
|
||||
next_path,
|
||||
"该敏感配置仍以明文形式写在 YAML 中,建议改为环境变量注入。",
|
||||
)
|
||||
_walk(value, next_path)
|
||||
return
|
||||
|
||||
if isinstance(node, list):
|
||||
for index, value in enumerate(node):
|
||||
_walk(value, f"{path}[{index}]")
|
||||
|
||||
_walk(self.raw_config)
|
||||
|
||||
def validate(self) -> dict:
|
||||
"""返回当前配置的校验报告。"""
|
||||
report = {"errors": [], "warnings": []}
|
||||
self._validate_unresolved_placeholders(report)
|
||||
self._validate_required_sections(report)
|
||||
self._validate_email_config(report)
|
||||
self._validate_llm_config(report)
|
||||
self._validate_plaintext_secrets(report)
|
||||
return report
|
||||
|
||||
@staticmethod
|
||||
def _mask_secret_value(value: str) -> str:
|
||||
"""对敏感值做轻量脱敏,保留一点可辨识尾巴方便排查。"""
|
||||
text = str(value or "")
|
||||
if not text:
|
||||
return ""
|
||||
if len(text) <= 6:
|
||||
return "*" * len(text)
|
||||
return f"{text[:2]}{'*' * (len(text) - 4)}{text[-2:]}"
|
||||
|
||||
def _sanitize_config_tree(self, node, parent_key: str = ""):
|
||||
"""递归生成适合日志/后台展示的脱敏配置快照。"""
|
||||
if isinstance(node, dict):
|
||||
return {
|
||||
key: self._sanitize_config_tree(value, str(key))
|
||||
for key, value in node.items()
|
||||
}
|
||||
if isinstance(node, list):
|
||||
return [self._sanitize_config_tree(value, parent_key) for value in node]
|
||||
if isinstance(node, str) and self._is_sensitive_key(parent_key):
|
||||
return self._mask_secret_value(node)
|
||||
return node
|
||||
|
||||
def get_validation_report(self) -> dict:
|
||||
"""返回一份拷贝,避免外部误改内部状态。"""
|
||||
return copy.deepcopy(self.validation_report)
|
||||
|
||||
def has_fatal_issues(self) -> bool:
|
||||
"""是否存在阻止启动的致命配置错误。"""
|
||||
return bool(self.validation_report.get("errors"))
|
||||
|
||||
def get_sanitized_snapshot(self) -> dict:
|
||||
"""返回可安全打印/展示的脱敏配置快照。"""
|
||||
return self._sanitize_config_tree(self.resolved_config)
|
||||
|
||||
def reload(self) -> None:
|
||||
yconfig = self._load_config()
|
||||
"""重新加载配置,并刷新公开属性与校验结果。"""
|
||||
self.raw_config = self._load_config()
|
||||
self.unresolved_placeholders = []
|
||||
resolved_config = self._resolve_config_tree(copy.deepcopy(self.raw_config))
|
||||
self.resolved_config = self._normalize_config(resolved_config)
|
||||
|
||||
# DB config
|
||||
self.mariadb = yconfig.get("db_config", {})
|
||||
self.redis = yconfig.get("redis_config", {})
|
||||
# 为了兼容现有调用方,这里继续保留原有的顶层属性映射;
|
||||
# 后续如果逐步引入更严格的配置对象,也可以先不动业务代码。
|
||||
self.environment = str(self.resolved_config.get("environment", "development") or "development").strip()
|
||||
self.plugin_dir = str(self.resolved_config.get("plugin_dir", "plugins") or "plugins").strip()
|
||||
self.mariadb = self.resolved_config.get("db_config", {})
|
||||
self.redis = self.resolved_config.get("redis_config", {})
|
||||
self.email = self.resolved_config.get("email_config", {})
|
||||
self.glances = self.resolved_config.get("glances", {})
|
||||
self.wx_config = self.resolved_config.get("wx_config", {})
|
||||
self.llm = self.resolved_config.get("llm", {})
|
||||
|
||||
# Email config
|
||||
self.email = yconfig.get("email_config", {})
|
||||
# glances 监控配置
|
||||
self.glances = yconfig.get("glances", {})
|
||||
|
||||
# wx 相关配置
|
||||
self.wx_config = yconfig.get("wx_config", {})
|
||||
# LLM 集中配置
|
||||
self.llm = yconfig.get("llm", {})
|
||||
self.validation_report = self.validate()
|
||||
|
||||
@@ -5,37 +5,43 @@ mkdir -p /app/logs
|
||||
|
||||
if [ ! -f /app/config.yaml ]; then
|
||||
cat > /app/config.yaml <<EOF
|
||||
environment: "\${ABOT_ENVIRONMENT:production}"
|
||||
plugin_dir: "\${ABOT_PLUGIN_DIR:plugins}"
|
||||
|
||||
db_config:
|
||||
host: "${DB_HOST}"
|
||||
prot: "${DB_PORT}"
|
||||
user: "${DB_USER}"
|
||||
password: "${DB_PASSWORD}"
|
||||
database: "${DB_NAME}"
|
||||
pool_name: "\${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||
pool_size: "\${ABOT_DB_POOL_SIZE:10}"
|
||||
host: "\${DB_HOST:127.0.0.1}"
|
||||
port: "\${DB_PORT:3306}"
|
||||
prot: "\${DB_PORT:3306}"
|
||||
user: "\${DB_USER:root}"
|
||||
password: "\${DB_PASSWORD}"
|
||||
database: "\${DB_NAME:message_archive}"
|
||||
charset: "utf8mb4"
|
||||
use_unicode: true
|
||||
get_warnings: true
|
||||
pool_reset_session: true
|
||||
|
||||
redis_config:
|
||||
host: "${REDIS_HOST}"
|
||||
port: ${REDIS_PORT}
|
||||
password: "${REDIS_PASSWORD}"
|
||||
db: ${REDIS_DB}
|
||||
host: "\${REDIS_HOST:127.0.0.1}"
|
||||
port: "\${REDIS_PORT:6379}"
|
||||
password: "\${REDIS_PASSWORD:}"
|
||||
db: "\${REDIS_DB:0}"
|
||||
decode_responses: true
|
||||
|
||||
email_config:
|
||||
smtp_server: ""
|
||||
smtp_port: 465
|
||||
sender_email: ""
|
||||
sender_password: ""
|
||||
alert_recipient: ""
|
||||
smtp_server: "\${ABOT_EMAIL_SMTP_SERVER:smtp.163.com}"
|
||||
smtp_port: "\${ABOT_EMAIL_SMTP_PORT:465}"
|
||||
sender_email: "\${ABOT_EMAIL_SENDER:}"
|
||||
sender_password: "\${ABOT_EMAIL_PASSWORD:}"
|
||||
alert_recipient: "\${ABOT_EMAIL_ALERT_RECIPIENT:}"
|
||||
|
||||
glances:
|
||||
host: "127.0.0.1"
|
||||
port: 61208
|
||||
host: "\${ABOT_GLANCES_HOST:127.0.0.1}"
|
||||
port: "\${ABOT_GLANCES_PORT:61208}"
|
||||
|
||||
wx_config:
|
||||
admin: [ "admin" ]
|
||||
admin: [ "\${ABOT_WX_ADMIN:admin}" ]
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
- 已补充 MySQL / Redis 连接探测与统一 LLM 最近调用快照,基础设施与 AI 运行态可直接在首页查看
|
||||
- 已将 `trace_id` 通过异步上下文继续贯穿到统一 LLM 调用与微信发送动作,链路追踪粒度进一步提升
|
||||
- 已补充后台登录失败限流、会话超时、默认弱口令强提醒与密码复杂度校验,后台安全基线进一步收紧
|
||||
- 已引入全局配置环境变量注入、启动期完整性校验与 `config.example.yaml`,默认配置不再直接携带仓库内明文密钥
|
||||
|
||||
## 2. 项目现状判断
|
||||
|
||||
@@ -242,6 +243,13 @@
|
||||
- 后台展示配置时自动脱敏
|
||||
- 区分开发、测试、生产环境配置
|
||||
|
||||
当前进展:
|
||||
|
||||
- 第一阶段已完成:`configuration.py` 已支持 `${ENV_NAME}` / `${ENV_NAME:默认值}` 形式的环境变量注入
|
||||
- 第一阶段已完成:启动时已增加 MySQL、Redis、LLM、邮件等关键配置完整性检查,致命缺项会直接阻止启动
|
||||
- 第一阶段已完成:已补充 `config.example.yaml`,并将仓库内默认 `config.yaml` 改为安全占位模板
|
||||
- 后续可继续补充后台配置查看脱敏、分环境配置切换与插件级配置治理
|
||||
|
||||
预期收益:
|
||||
|
||||
- 大幅降低密钥泄露风险
|
||||
@@ -293,6 +301,10 @@
|
||||
|
||||
- 让每次改动后都有固定、可重复执行的验证步骤
|
||||
|
||||
当前排期说明:
|
||||
|
||||
- 按当前优化策略,该项暂时后置处理,放在本轮工程治理工作的最后再集中补齐
|
||||
|
||||
建议内容:
|
||||
|
||||
- 建立“日常改动验证清单”
|
||||
|
||||
37
main.py
37
main.py
@@ -63,8 +63,45 @@ logger.add(
|
||||
)
|
||||
|
||||
|
||||
def _log_config_validation(config: Config) -> None:
|
||||
"""输出启动期配置校验结果。"""
|
||||
validation_report = config.get_validation_report()
|
||||
errors = list(validation_report.get("errors", []) or [])
|
||||
warnings = list(validation_report.get("warnings", []) or [])
|
||||
|
||||
logger.info(
|
||||
"配置加载完成: "
|
||||
f"environment={config.environment}, "
|
||||
f"plugin_dir={config.plugin_dir}, "
|
||||
f"errors={len(errors)}, "
|
||||
f"warnings={len(warnings)}"
|
||||
)
|
||||
|
||||
# 这里只打印脱敏后的配置快照:
|
||||
# 1. 便于定位“到底加载了哪套配置”;
|
||||
# 2. 同时避免把数据库密码、API Key 再写进日志;
|
||||
# 3. 放在 DEBUG 级别,默认不会刷屏主日志。
|
||||
logger.debug(f"配置脱敏快照: {config.get_sanitized_snapshot()}")
|
||||
|
||||
for warning in warnings:
|
||||
logger.warning(
|
||||
f"配置告警[{warning.get('code', 'unknown')}] "
|
||||
f"{warning.get('path', 'root')}: {warning.get('message', '')}"
|
||||
)
|
||||
|
||||
for error in errors:
|
||||
logger.error(
|
||||
f"配置错误[{error.get('code', 'unknown')}] "
|
||||
f"{error.get('path', 'root')}: {error.get('message', '')}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise ValueError("启动终止:存在未修复的致命配置错误,请先修正 config.yaml 或环境变量。")
|
||||
|
||||
|
||||
def main():
|
||||
config = Config()
|
||||
_log_config_validation(config)
|
||||
|
||||
# 创建机器人实例
|
||||
robot = Robot(config)
|
||||
|
||||
@@ -62,9 +62,10 @@ ABOT 是一款功能丰富的微信机器人系统,旨在提升您的微信使
|
||||
- 安装依赖包:`pip install -r requirements.txt`
|
||||
|
||||
4. **配置文件设置**
|
||||
- 复制`config.yaml.template`为`config.yaml`
|
||||
- 复制`config.example.yaml`为`config.yaml`
|
||||
- 使用文本编辑器打开`config.yaml`
|
||||
- 按照注释说明配置AI模型API密钥、数据库连接等参数
|
||||
- 按照注释说明配置数据库连接、微信管理员与 AI 模型参数
|
||||
- 敏感信息优先通过环境变量注入,例如 `ABOT_DB_PASSWORD`、`ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY`
|
||||
|
||||
### 2.3 启动系统
|
||||
|
||||
@@ -555,4 +556,4 @@ ABOT 重视用户隐私保护,我们的隐私政策包括:
|
||||
|
||||
---
|
||||
|
||||
感谢您选择使用ABOT!我们致力于为您提供更好的微信自动化体验。如有任何问题或建议,欢迎随时联系我们。
|
||||
感谢您选择使用ABOT!我们致力于为您提供更好的微信自动化体验。如有任何问题或建议,欢迎随时联系我们。
|
||||
|
||||
Reference in New Issue
Block a user