完善配置密钥治理与启动校验
- 为 configuration.py 增加环境变量占位符解析、配置归一化、脱敏快照与启动校验\n- 在 main.py 启动阶段接入配置校验日志,并在致命缺项时阻止进程继续启动\n- 新增 config.example.yaml,并将默认 config.yaml 改为安全占位模板,移除仓库内明文敏感信息\n- 调整 docker-entrypoint.sh 与文档,统一说明配置复制、环境变量注入与当前优化进展
This commit is contained in:
49
README.MD
49
README.MD
@@ -126,28 +126,49 @@ sudo apt-get install -y fonts-noto-color-emoji fonts-noto-cjk fonts-wqy-microhei
|
|||||||
|
|
||||||
### 1. 配置文件
|
### 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
|
```yaml
|
||||||
db_config:
|
db_config:
|
||||||
pool_name: "wechat_boot_pool"
|
pool_name: "${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||||
pool_size: 10
|
pool_size: "${ABOT_DB_POOL_SIZE:10}"
|
||||||
host: "your-db-host"
|
host: "${ABOT_DB_HOST:127.0.0.1}"
|
||||||
user: "your-db-user"
|
port: "${ABOT_DB_PORT:3306}"
|
||||||
password: "your-db-password"
|
user: "${ABOT_DB_USER:root}"
|
||||||
database: "message_archive"
|
password: "${ABOT_DB_PASSWORD}"
|
||||||
charset: "utf8mb4"
|
database: "${ABOT_DB_NAME:message_archive}"
|
||||||
|
charset: "${ABOT_DB_CHARSET:utf8mb4}"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Redis配置
|
#### Redis配置
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
redis_config:
|
redis_config:
|
||||||
host: "your-redis-host"
|
host: "${ABOT_REDIS_HOST:127.0.0.1}"
|
||||||
port: 6379
|
port: "${ABOT_REDIS_PORT:6379}"
|
||||||
db: 0
|
password: "${ABOT_REDIS_PASSWORD:}"
|
||||||
|
db: "${ABOT_REDIS_DB:0}"
|
||||||
decode_responses: true
|
decode_responses: true
|
||||||
```
|
```
|
||||||
#### ipad 客户端配置
|
#### ipad 客户端配置
|
||||||
@@ -272,9 +293,9 @@ abot/
|
|||||||
### 开发规范
|
### 开发规范
|
||||||
|
|
||||||
- 遵循PEP 8编码规范
|
- 遵循PEP 8编码规范
|
||||||
- 添加适当的注释
|
- 添加适当的中文注释
|
||||||
- 编写单元测试
|
- 优先补齐文档与人工验证步骤
|
||||||
- 更新文档
|
- 敏感配置优先使用环境变量注入
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
|||||||
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:
|
db_config:
|
||||||
pool_name: "wechat_boot_pool"
|
pool_name: "${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||||
pool_size: 10
|
pool_size: "${ABOT_DB_POOL_SIZE:10}"
|
||||||
host: "192.168.2.41"
|
host: "${ABOT_DB_HOST:127.0.0.1}"
|
||||||
prot: "3306"
|
# 新配置统一使用 port;prot 仅作为历史兼容字段保留。
|
||||||
user: "root"
|
port: "${ABOT_DB_PORT:3306}"
|
||||||
password: "lw123456"
|
prot: "${ABOT_DB_PORT:3306}"
|
||||||
database: "message_archive"
|
user: "${ABOT_DB_USER:root}"
|
||||||
charset: "utf8mb4"
|
password: "${ABOT_DB_PASSWORD}"
|
||||||
|
database: "${ABOT_DB_NAME:message_archive}"
|
||||||
|
charset: "${ABOT_DB_CHARSET:utf8mb4}"
|
||||||
use_unicode: true
|
use_unicode: true
|
||||||
get_warnings: true
|
get_warnings: true
|
||||||
pool_reset_session: true
|
pool_reset_session: true
|
||||||
|
|
||||||
redis_config:
|
redis_config:
|
||||||
host: "192.168.2.40"
|
host: "${ABOT_REDIS_HOST:127.0.0.1}"
|
||||||
port: 6379
|
port: "${ABOT_REDIS_PORT:6379}"
|
||||||
password: ""
|
password: "${ABOT_REDIS_PASSWORD:}"
|
||||||
db: 0
|
db: "${ABOT_REDIS_DB:0}"
|
||||||
decode_responses: true
|
decode_responses: true
|
||||||
|
|
||||||
|
|
||||||
# 邮件发送配置
|
# 邮件发送配置
|
||||||
email_config:
|
email_config:
|
||||||
smtp_server: "smtp.163.com"
|
smtp_server: "${ABOT_EMAIL_SMTP_SERVER:smtp.163.com}"
|
||||||
smtp_port: 465
|
smtp_port: "${ABOT_EMAIL_SMTP_PORT:465}"
|
||||||
sender_email: "bovine_liu@163.com"
|
sender_email: "${ABOT_EMAIL_SENDER:}"
|
||||||
sender_password: "CCWpEQzSdxQUqhDE"
|
sender_password: "${ABOT_EMAIL_PASSWORD:}"
|
||||||
alert_recipient: "bovine_liu@163.com" # 警报邮件接收者
|
alert_recipient: "${ABOT_EMAIL_ALERT_RECIPIENT:}"
|
||||||
|
|
||||||
glances:
|
glances:
|
||||||
host: "192.168.2.170"
|
host: "${ABOT_GLANCES_HOST:127.0.0.1}"
|
||||||
port: 61208
|
port: "${ABOT_GLANCES_PORT:61208}"
|
||||||
|
|
||||||
|
|
||||||
wx_config:
|
wx_config:
|
||||||
#微信管理账号,用于接收部分管理员指令
|
# 微信管理账号,用于接收部分管理员指令。
|
||||||
#菜单调整和系统更新
|
admin: [ "${ABOT_WX_ADMIN:admin}" ]
|
||||||
admin: [ "Jyunere" ]
|
|
||||||
|
|
||||||
llm:
|
llm:
|
||||||
default_backend: "dify_workflow_chat"
|
default_backend: "${ABOT_LLM_DEFAULT_BACKEND:dify_workflow_chat}"
|
||||||
backends:
|
backends:
|
||||||
dify_workflow_chat:
|
dify_workflow_chat:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "workflow"
|
mode: "workflow"
|
||||||
api_key: "app-u5EnYq3ill19bm6pWJwGkY4D"
|
api_key: "${ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY:}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
endpoint: "workflows/run"
|
endpoint: "workflows/run"
|
||||||
response_mode: "blocking"
|
response_mode: "blocking"
|
||||||
# 聊天工作流偶尔会超过 40 秒:
|
|
||||||
# 1. 原先 40 秒超时会导致客户端提前放弃;
|
|
||||||
# 2. 本地统一客户端默认又会自动重试,容易在 Dify 后台看到同一问题连续触发 3 次;
|
|
||||||
# 3. 这里把超时提高到 120 秒,并将重试次数收敛为 1,避免重复触发整条工作流。
|
|
||||||
request_timeout: 120
|
request_timeout: 120
|
||||||
max_retries: 1
|
max_retries: 1
|
||||||
retry_delay_seconds: 1.0
|
retry_delay_seconds: 1.0
|
||||||
dify_workflow_member_context:
|
dify_workflow_member_context:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "workflow"
|
mode: "workflow"
|
||||||
api_key: "app-b2cj03DipGCIAmgBfcx7SKsT"
|
api_key: "${ABOT_LLM_DIFY_MEMBER_CONTEXT_API_KEY:}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
endpoint: "workflows/run"
|
endpoint: "workflows/run"
|
||||||
workflow_output_key: "text"
|
workflow_output_key: "text"
|
||||||
response_mode: "streaming"
|
response_mode: "streaming"
|
||||||
@@ -66,8 +64,8 @@ llm:
|
|||||||
dify_workflow_message_summary:
|
dify_workflow_message_summary:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "workflow"
|
mode: "workflow"
|
||||||
api_key: "app-shCA6bo5l2VDmnvhg2BtuJbk"
|
api_key: "${ABOT_LLM_DIFY_MESSAGE_SUMMARY_API_KEY:}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
endpoint: "workflows/run"
|
endpoint: "workflows/run"
|
||||||
workflow_output_key: "text"
|
workflow_output_key: "text"
|
||||||
response_mode: "streaming"
|
response_mode: "streaming"
|
||||||
@@ -75,38 +73,35 @@ llm:
|
|||||||
dify_workflow_douyu_daily_report:
|
dify_workflow_douyu_daily_report:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "workflow"
|
mode: "workflow"
|
||||||
# 斗鱼日报专用工作流:请替换为你在 Dify 上创建的“斗鱼日报”应用 Key。
|
api_key: "${ABOT_LLM_DIFY_DOUYU_REPORT_API_KEY:}"
|
||||||
api_key: "app-S1oyi2udgIn197Vu0oOGUgAl"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
|
||||||
endpoint: "workflows/run"
|
endpoint: "workflows/run"
|
||||||
# 工作流最终输出字段建议固定为 text,便于统一客户端直接读取结果文本。
|
|
||||||
workflow_output_key: "text"
|
workflow_output_key: "text"
|
||||||
response_mode: "blocking"
|
response_mode: "blocking"
|
||||||
# 斗鱼日报 payload 较大,适当提高超时时间,避免高峰时段超时回退。
|
|
||||||
request_timeout: 240
|
request_timeout: 240
|
||||||
dify_chat_global_news:
|
dify_chat_global_news:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "chat"
|
mode: "chat"
|
||||||
api_key: "app-rhhKkbvHd2IAQoGX7xTzXZJj"
|
api_key: "${ABOT_LLM_DIFY_GLOBAL_NEWS_API_KEY:}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
endpoint: "chat-messages"
|
endpoint: "chat-messages"
|
||||||
response_mode: "blocking"
|
response_mode: "blocking"
|
||||||
request_timeout: 60
|
request_timeout: 60
|
||||||
openai_compatible_game_task:
|
openai_compatible_game_task:
|
||||||
provider: "openai_compatible"
|
provider: "openai_compatible"
|
||||||
api_url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
api_url: "${ABOT_LLM_GAME_TASK_API_URL:https://api.example.com/v1/chat/completions}"
|
||||||
api_key: "b8586595-eb81-483d-8e91-a35cc789729e"
|
api_key: "${ABOT_LLM_GAME_TASK_API_KEY:}"
|
||||||
model: "doubao-1-5-lite-32k-250115"
|
model: "${ABOT_LLM_GAME_TASK_MODEL:doubao-1-5-lite-32k-250115}"
|
||||||
stream: false
|
stream: false
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
max_tokens: 1000
|
max_tokens: 1000
|
||||||
timeout_seconds: 60
|
timeout_seconds: 60
|
||||||
openai_compatible_ai_auto_response:
|
openai_compatible_ai_auto_response:
|
||||||
provider: "openai_compatible"
|
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"
|
endpoint: "chat/completions"
|
||||||
api_key: "sk-hC6WMLAsTdItpywyrYdxT6pQ4E7NARGbUKuPWRH0zMheen9e"
|
api_key: "${ABOT_LLM_AUTO_REPLY_API_KEY:}"
|
||||||
model: "gpt-5.4"
|
model: "${ABOT_LLM_AUTO_REPLY_MODEL:gpt-5.4}"
|
||||||
stream: true
|
stream: true
|
||||||
temperature: 0.35
|
temperature: 0.35
|
||||||
max_tokens: 120
|
max_tokens: 120
|
||||||
@@ -116,35 +111,24 @@ llm:
|
|||||||
dify_workflow_ai_auto_response:
|
dify_workflow_ai_auto_response:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "workflow"
|
mode: "workflow"
|
||||||
api_key: "app-ukHWWGoleANS5aZVmx28UAQ4"
|
api_key: "${ABOT_LLM_DIFY_AUTO_REPLY_API_KEY:}"
|
||||||
api_base_url: "http://192.168.2.240/v1"
|
api_base_url: "${ABOT_LLM_DIFY_API_BASE_URL:http://127.0.0.1:8080/v1}"
|
||||||
endpoint: "workflows/run"
|
endpoint: "workflows/run"
|
||||||
workflow_output_key: "result_json"
|
workflow_output_key: "result_json"
|
||||||
response_mode: "blocking"
|
response_mode: "blocking"
|
||||||
# 群聊自动回复强调时效性:
|
|
||||||
# 1. Dify 请求不能等太久,否则容易出现“过了场子再补回”的违和感;
|
|
||||||
# 2. 这里把单次请求超时收紧,并关闭重试,让过期消息尽快放弃。
|
|
||||||
request_timeout: 15
|
request_timeout: 15
|
||||||
max_retries: 1
|
max_retries: 1
|
||||||
retry_delay_seconds: 1.0
|
retry_delay_seconds: 1.0
|
||||||
openai_compatible_ai_gen_image:
|
openai_compatible_ai_gen_image:
|
||||||
provider: "openai_compatible"
|
provider: "openai_compatible"
|
||||||
# AI 绘图专用网关:
|
api_base_url: "${ABOT_LLM_IMAGE_API_BASE_URL:https://api.example.com/v1}"
|
||||||
# 1. 这里使用用户提供的 OpenAI 兼容服务地址;
|
|
||||||
# 2. 插件会在此 base_url 基础上请求 images/generations;
|
|
||||||
# 3. endpoint 保留为图片接口默认值,便于后续统一调整。
|
|
||||||
api_base_url: "https://freeapi.dgbmc.top/v1"
|
|
||||||
endpoint: "chat/completions"
|
endpoint: "chat/completions"
|
||||||
api_key: "sk-2XccrBRsX8OmxqCEsZjdDRczhHNaAG7Mn88mNVL7Y0w0tx72"
|
api_key: "${ABOT_LLM_IMAGE_API_KEY:}"
|
||||||
# 图片模型默认使用 gpt-image-1;
|
model: "${ABOT_LLM_IMAGE_MODEL:gpt-image-1}"
|
||||||
# 若网关只支持其他模型,可后续直接在这里替换。
|
|
||||||
model: "gpt-image-2"
|
|
||||||
stream: false
|
stream: false
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
max_retries: 2
|
max_retries: 2
|
||||||
retry_delay_seconds: 1.0
|
retry_delay_seconds: 1.0
|
||||||
# 场景路由层:插件建议优先使用 scene,而不是直接绑定 backend。
|
|
||||||
# 这样当模型或供应商切换时,只需要改这里,不需要逐个改插件配置。
|
|
||||||
scenes:
|
scenes:
|
||||||
"chat.main": "dify_workflow_chat"
|
"chat.main": "dify_workflow_chat"
|
||||||
"member.profile": "dify_workflow_member_context"
|
"member.profile": "dify_workflow_member_context"
|
||||||
|
|||||||
389
configuration.py
389
configuration.py
@@ -1,35 +1,388 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging.config
|
import copy
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
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()
|
self.reload()
|
||||||
|
|
||||||
def _load_config(self) -> dict:
|
def _load_config(self) -> dict:
|
||||||
pwd = os.path.dirname(os.path.abspath(__file__))
|
"""从磁盘读取 YAML 配置。"""
|
||||||
with open(f"{pwd}/config.yaml", "rb") as fp:
|
with open(self.config_path, "r", encoding="utf-8") as fp:
|
||||||
yconfig = yaml.safe_load(fp)
|
yconfig = yaml.safe_load(fp) or {}
|
||||||
return yconfig
|
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:
|
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.validation_report = self.validate()
|
||||||
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", {})
|
|
||||||
|
|||||||
@@ -5,37 +5,43 @@ mkdir -p /app/logs
|
|||||||
|
|
||||||
if [ ! -f /app/config.yaml ]; then
|
if [ ! -f /app/config.yaml ]; then
|
||||||
cat > /app/config.yaml <<EOF
|
cat > /app/config.yaml <<EOF
|
||||||
|
environment: "\${ABOT_ENVIRONMENT:production}"
|
||||||
|
plugin_dir: "\${ABOT_PLUGIN_DIR:plugins}"
|
||||||
|
|
||||||
db_config:
|
db_config:
|
||||||
host: "${DB_HOST}"
|
pool_name: "\${ABOT_DB_POOL_NAME:wechat_boot_pool}"
|
||||||
prot: "${DB_PORT}"
|
pool_size: "\${ABOT_DB_POOL_SIZE:10}"
|
||||||
user: "${DB_USER}"
|
host: "\${DB_HOST:127.0.0.1}"
|
||||||
password: "${DB_PASSWORD}"
|
port: "\${DB_PORT:3306}"
|
||||||
database: "${DB_NAME}"
|
prot: "\${DB_PORT:3306}"
|
||||||
|
user: "\${DB_USER:root}"
|
||||||
|
password: "\${DB_PASSWORD}"
|
||||||
|
database: "\${DB_NAME:message_archive}"
|
||||||
charset: "utf8mb4"
|
charset: "utf8mb4"
|
||||||
use_unicode: true
|
use_unicode: true
|
||||||
get_warnings: true
|
get_warnings: true
|
||||||
pool_reset_session: true
|
pool_reset_session: true
|
||||||
|
|
||||||
redis_config:
|
redis_config:
|
||||||
host: "${REDIS_HOST}"
|
host: "\${REDIS_HOST:127.0.0.1}"
|
||||||
port: ${REDIS_PORT}
|
port: "\${REDIS_PORT:6379}"
|
||||||
password: "${REDIS_PASSWORD}"
|
password: "\${REDIS_PASSWORD:}"
|
||||||
db: ${REDIS_DB}
|
db: "\${REDIS_DB:0}"
|
||||||
decode_responses: true
|
decode_responses: true
|
||||||
|
|
||||||
email_config:
|
email_config:
|
||||||
smtp_server: ""
|
smtp_server: "\${ABOT_EMAIL_SMTP_SERVER:smtp.163.com}"
|
||||||
smtp_port: 465
|
smtp_port: "\${ABOT_EMAIL_SMTP_PORT:465}"
|
||||||
sender_email: ""
|
sender_email: "\${ABOT_EMAIL_SENDER:}"
|
||||||
sender_password: ""
|
sender_password: "\${ABOT_EMAIL_PASSWORD:}"
|
||||||
alert_recipient: ""
|
alert_recipient: "\${ABOT_EMAIL_ALERT_RECIPIENT:}"
|
||||||
|
|
||||||
glances:
|
glances:
|
||||||
host: "127.0.0.1"
|
host: "\${ABOT_GLANCES_HOST:127.0.0.1}"
|
||||||
port: 61208
|
port: "\${ABOT_GLANCES_PORT:61208}"
|
||||||
|
|
||||||
wx_config:
|
wx_config:
|
||||||
admin: [ "admin" ]
|
admin: [ "\${ABOT_WX_ADMIN:admin}" ]
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
- 已补充 MySQL / Redis 连接探测与统一 LLM 最近调用快照,基础设施与 AI 运行态可直接在首页查看
|
- 已补充 MySQL / Redis 连接探测与统一 LLM 最近调用快照,基础设施与 AI 运行态可直接在首页查看
|
||||||
- 已将 `trace_id` 通过异步上下文继续贯穿到统一 LLM 调用与微信发送动作,链路追踪粒度进一步提升
|
- 已将 `trace_id` 通过异步上下文继续贯穿到统一 LLM 调用与微信发送动作,链路追踪粒度进一步提升
|
||||||
- 已补充后台登录失败限流、会话超时、默认弱口令强提醒与密码复杂度校验,后台安全基线进一步收紧
|
- 已补充后台登录失败限流、会话超时、默认弱口令强提醒与密码复杂度校验,后台安全基线进一步收紧
|
||||||
|
- 已引入全局配置环境变量注入、启动期完整性校验与 `config.example.yaml`,默认配置不再直接携带仓库内明文密钥
|
||||||
|
|
||||||
## 2. 项目现状判断
|
## 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():
|
def main():
|
||||||
config = Config()
|
config = Config()
|
||||||
|
_log_config_validation(config)
|
||||||
|
|
||||||
# 创建机器人实例
|
# 创建机器人实例
|
||||||
robot = Robot(config)
|
robot = Robot(config)
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ ABOT 是一款功能丰富的微信机器人系统,旨在提升您的微信使
|
|||||||
- 安装依赖包:`pip install -r requirements.txt`
|
- 安装依赖包:`pip install -r requirements.txt`
|
||||||
|
|
||||||
4. **配置文件设置**
|
4. **配置文件设置**
|
||||||
- 复制`config.yaml.template`为`config.yaml`
|
- 复制`config.example.yaml`为`config.yaml`
|
||||||
- 使用文本编辑器打开`config.yaml`
|
- 使用文本编辑器打开`config.yaml`
|
||||||
- 按照注释说明配置AI模型API密钥、数据库连接等参数
|
- 按照注释说明配置数据库连接、微信管理员与 AI 模型参数
|
||||||
|
- 敏感信息优先通过环境变量注入,例如 `ABOT_DB_PASSWORD`、`ABOT_LLM_DIFY_WORKFLOW_CHAT_API_KEY`
|
||||||
|
|
||||||
### 2.3 启动系统
|
### 2.3 启动系统
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user