支持从本地配置文件加载 DeepSeek 密钥
This commit is contained in:
+4
-3
@@ -1,4 +1,5 @@
|
||||
# Reference only: am.py and start.bat do not automatically load .env files.
|
||||
# PowerShell: $env:DEEPSEEK_API_KEY = "<your-key>"
|
||||
# Linux/macOS: export DEEPSEEK_API_KEY="<your-key>"
|
||||
# Copy this file to .env in the repository root and enter your DeepSeek API key.
|
||||
# The application reads .env at startup. Restart after changing this file.
|
||||
# The process environment takes precedence, including an explicitly empty value.
|
||||
# Keep real credentials in .env; never put them in this example or source files.
|
||||
DEEPSEEK_API_KEY=
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
| Human support | `logistics_chat/human_service.py`; `logistics_chat/routes/human.py` |
|
||||
| Business / files | `logistics_chat/business_service.py`, `logistics_chat/file_service.py`; `logistics_chat/routes/` |
|
||||
| UI | `templates/`; `static/css/theme.css`: tokens; page CSS; `static/js/chat.js`: `doSend`, `I18N`; `static/js/admin.js`: `poll`, `sendReply`, `setChatView` |
|
||||
| Startup / hooks | `logistics_chat/__init__.py`: `create_app`; `config.py`, `hooks.py`, `runtime.py` in that package |
|
||||
| Startup / hooks | `logistics_chat/__init__.py`: `create_app`; `config.py`: `default_config`; `hooks.py`, `runtime.py` in that package |
|
||||
| CLI / tests | `start.bat`: `probe_python`; `scripts/`, `tests/`, `examples/requests/` |
|
||||
|
||||
## Efficient Search & File Boundaries
|
||||
@@ -35,12 +35,12 @@ Run from the repository root:
|
||||
```powershell
|
||||
py -3 -m venv .venv
|
||||
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
|
||||
$env:DEEPSEEK_API_KEY = "<your-key>"
|
||||
Copy-Item .env.example .env # First run only; set DEEPSEEK_API_KEY inside .env.
|
||||
.\.venv\Scripts\python.exe am.py
|
||||
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
Commands use project `.venv`. Optional `start.bat` prefers active `VIRTUAL_ENV`; verify selection with `start.bat --check-python`. No frontend build is required.
|
||||
Optional `start.bat` prefers active `VIRTUAL_ENV`; verify selection with `start.bat --check-python`. No frontend build is required.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -58,4 +58,4 @@ History uses concise Chinese summaries. Keep commits focused. PRs explain behavi
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
Never commit credentials or runtime/customer data. Revoke exposed keys. Keep `uploads/.gitkeep`. `.env` is not loaded automatically. Imports are inert; `create_app` initializes runtime storage unless `INITIALIZE_RUNTIME=False`. Default HOST remains `0.0.0.0`. Missing `agents/orders/tickets` initialization and complete admin authentication remain unresolved.
|
||||
Never commit credentials or runtime/customer data. Revoke exposed keys. Keep `uploads/.gitkeep`. Startup reads the root `.env`; environment variables override it. Restart after changes. Imports are inert; `create_app` initializes runtime storage unless `INITIALIZE_RUNTIME=False`. Default HOST remains `0.0.0.0`. Missing `agents/orders/tickets` initialization and complete admin authentication remain unresolved.
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
```powershell
|
||||
py -3 -m venv .venv
|
||||
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
|
||||
$env:DEEPSEEK_API_KEY = "<your-key>"
|
||||
Copy-Item .env.example .env # 仅首次运行;已有 .env 时跳过
|
||||
# 编辑 .env,填写 DEEPSEEK_API_KEY 后再启动
|
||||
.\.venv\Scripts\python.exe am.py
|
||||
```
|
||||
|
||||
@@ -96,17 +97,20 @@ $env:DEEPSEEK_API_KEY = "<your-key>"
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install -r requirements.txt
|
||||
export DEEPSEEK_API_KEY="<your-key>"
|
||||
cp .env.example .env # 仅首次运行;已有 .env 时跳过
|
||||
# 编辑 .env,填写 DEEPSEEK_API_KEY 后再启动
|
||||
.venv/bin/python am.py
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
- [`.env.example`](.env.example) 仅列出配置格式,当前程序和启动脚本**不会自动读取 `.env`**;请在环境变量中配置密钥。
|
||||
- PowerShell 的 `$env:DEEPSEEK_API_KEY` 只影响当前终端及其子进程。双击启动脚本时,需要提前配置系统环境变量并重新打开窗口。
|
||||
- 本地密钥放在项目根目录的 `.env` 中:首次运行复制 [`.env.example`](.env.example),填写 `DEEPSEEK_API_KEY=你的密钥`。`.env` 已被 Git 忽略,示例文件中的值应保持为空。
|
||||
- 应用启动时自动读取根目录 `.env`,不依赖终端的当前目录;`am.py`、Windows 启动脚本和 WSGI 应用工厂均使用同一配置入口。修改密钥后须重启服务。
|
||||
- 配置优先级为:显式传给 `create_app` 的配置 > 进程环境变量 > `.env` > 空值。环境变量即使为空也会覆盖 `.env`;本地填写后仍提示未配置时,请检查并移除旧的空环境变量。
|
||||
- 部署环境仍可注入 `DEEPSEEK_API_KEY`。PowerShell 的 `$env:DEEPSEEK_API_KEY` 只影响当前终端及其子进程,双击启动脚本可直接使用 `.env`,无需配置系统环境变量。
|
||||
- 密钥申请或更换可在 [DeepSeek 平台](https://platform.deepseek.com/api_keys) 完成;不要把密钥写入脚本、提交到 Git,或打印到日志。
|
||||
- 当前服务监听 `0.0.0.0:5000`,局域网设备可能访问。仅供本机开发时,将 `logistics_chat/config.py` 中默认的 `HOST` 设置为 `"127.0.0.1"`。
|
||||
- 历史启动脚本曾提交明文密钥。当前文件已移除该配置,但 Git 历史仍保留旧内容,应在平台作废旧密钥并通过环境变量配置新密钥。
|
||||
- 历史启动脚本曾提交明文密钥。当前文件已移除该配置,但 Git 历史仍保留旧内容,应在平台作废旧密钥,并在 `.env` 或部署环境中配置新密钥。
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -116,7 +120,7 @@ baiyue-logistics-chat/
|
||||
├── rates_extra.py 扩展数据旧导入路径的兼容转发
|
||||
├── logistics_chat/
|
||||
│ ├── __init__.py create_app 应用工厂
|
||||
│ ├── config.py 环境与绝对路径配置
|
||||
│ ├── config.py .env、环境变量与绝对路径配置
|
||||
│ ├── db.py SQLite、历史持久化、备份
|
||||
│ ├── knowledge.py 知识与关键词检索
|
||||
│ ├── pricing.py 运费解析与计算
|
||||
@@ -164,7 +168,7 @@ baiyue-logistics-chat/
|
||||
|
||||
安装 Node.js 后,Python 测试还会运行 `tests/frontend/admin.test.cjs`,验证发送失败保留草稿、重复点击、切换会话、输入法及手机抽屉;也可单独执行 `node --test tests/frontend/admin.test.cjs`,无需安装 npm 依赖。未安装 Node.js 时该项明确跳过。
|
||||
|
||||
覆盖范围包括运费边界与单位换算、知识检索、运单识别、旧 SQLite schema 迁移、事务回滚、附件、人工状态、历史、CSV、SSE 压缩/追问/异常/取消,以及 Windows 解释器选择。Windows 启动测试在其他系统上跳过,三个页面的外置资源也由接口测试检查。
|
||||
覆盖范围包括配置文件加载与优先级、运费边界与单位换算、知识检索、运单识别、旧 SQLite schema 迁移、事务回滚、附件、人工状态、历史、CSV、SSE 压缩/追问/异常/取消,以及 Windows 解释器选择。Windows 启动测试在其他系统上跳过,三个页面的外置资源也由接口测试检查。
|
||||
|
||||
模型请求使用模拟响应,数据库和附件使用临时目录;这不代表完成真实 DeepSeek 或真实物流接口联调。旧文档提及的原始 52 个业务测试和 29 个场景记录未随仓库提供,当前结果以实际测试输出为准。
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ PowerShell 下把目录和 `-g '*.py'` 分开传给 `rg`。报价字典可能整
|
||||
| 附件 | `logistics_chat/file_service.py`:`upload_file`、`cleanup_uploads`;`routes/files.py`:`uploaded_file` | 扩展名/大小/目录由配置提供;客户端 `addMsg`,客服端 `appendMsg` |
|
||||
| 登录、统计、订单、留言、导出 | `logistics_chat/business_service.py`:`admin_login`、`admin_stats`、`my_orders`、`add_ticket`、`list_tickets`、`export_chat` | `routes/admin.py`、`routes/business.py`;登录页 `static/js/admin_login.js` |
|
||||
| 页面 | `templates/index.html`、`templates/admin.html`、`templates/admin_login.html` | 先加载共享 `static/css/theme.css`,再加载对应 `chat`、`admin`、`admin_login` 的 CSS/JS;保留全局事件入口 |
|
||||
| 回归 | `tests/test_rules.py`、`test_storage.py`、`test_api.py`、`test_startup.py`、`test_maintenance_scripts.py`、`test_frontend.py`;`tests/frontend/admin.test.cjs` | 数据、HTTP 与 SSE 使用隔离配置和模拟模型;Node 内置测试验证草稿、异步发送和服务资料抽屉;浏览器使用临时测试库 |
|
||||
| 回归 | `tests/test_rules.py`、`test_storage.py`、`test_api.py`、`test_config.py`、`test_startup.py`、`test_maintenance_scripts.py`、`test_frontend.py`;`tests/frontend/admin.test.cjs` | 配置测试隔离 `.env` 和环境变量;数据、HTTP 与 SSE 使用隔离配置和模拟模型;Node 内置测试验证草稿、异步发送和服务资料抽屉;浏览器使用临时测试库 |
|
||||
|
||||
除标明的根目录文件外,表内缩写的 Python 文件均相对 `logistics_chat/`。
|
||||
|
||||
@@ -42,7 +42,7 @@ am.py # CLI;显式请求 am.app 时创建默认 WSGI
|
||||
rates_extra.py # 旧导入路径的兼容转发
|
||||
logistics_chat/
|
||||
├── __init__.py # create_app 与显式运行时初始化
|
||||
├── config.py # 环境变量、稳定根路径、服务/资源配置
|
||||
├── config.py # 根目录 .env、环境变量、稳定根路径、服务/资源配置
|
||||
├── db.py # 连接、schema、历史持久化、备份
|
||||
├── knowledge.py # 知识与关键词检索
|
||||
├── pricing.py # 运费规则、解析、换算、附加费说明
|
||||
@@ -79,6 +79,7 @@ tests/ # 按业务模块组织的回归
|
||||
- 所有模型 HTTP 经 `ai_client.post_completion`。保留流式问答 `(5, 90)`、压缩 `(5, 30)`、追问 `(5, 15)` 三组连接/读取超时;不能因共用客户端而合并期限。流结束或取消时释放 HTTP 响应。
|
||||
- 模块导入不建库、不备份、不清理附件、不访问模型。`create_app` 默认显式初始化运行目录、数据库、备份与旧附件清理;`INITIALIZE_RUNTIME=False` 可禁止这些动作,此时调用方负责提供已有 schema。访问兼容入口 `am.app` 视为显式创建应用。
|
||||
- `config.ROOT_DIR` 指向仓库根目录,模板/静态目录显式提供给 Flask。数据库、上传、备份、日志使用配置中的绝对路径,避免包文件移动改变数据位置。
|
||||
- `default_config` 用 `python-dotenv` 读取根目录 `.env`,不修改 `os.environ`;只读取已支持的 `DEEPSEEK_API_KEY`,不展开 `${...}`。显式工厂配置优先于进程环境(含空值),再使用文件值。配置在应用创建时确定,修改后重启;真实 `.env` 不纳入 Git。
|
||||
- 限流、输入状态由应用实例持有,不能每请求重置或跨测试应用共享。维护 CLI 和纯规则可用标准库加载,包入口延迟导入 Web 依赖。
|
||||
- 单文件围绕一个职责;超过约 300–400 行时检查职责,而非按行数机械拆分。报价数据保持单一来源;兼容转发不复制字典。页面脚本按页面保留既有执行顺序和全局函数,不擅自改为 ES Modules。
|
||||
- 配色、字体基线和通用控件放在 `theme.css`,页面布局放在对应 CSS。客户页通过 `body.dark` 覆盖主题令牌;新增可翻译文案加入 `chat.js` 的 `I18N`,静态文案用 `data-i18n`。调整样式后检查桌面、390px 窄屏、侧栏、聊天卡片和客服空/活动状态。
|
||||
|
||||
@@ -36,7 +36,7 @@ def create_chat_stream(msg, conv_id):
|
||||
yield sse({'d': 1})
|
||||
return (track_stream(), {'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
if not get_setting('DEEPSEEK_API_KEY'):
|
||||
return (sse({'e': '服务未配置 API Key,请设置环境变量 DEEPSEEK_API_KEY 后重启服务'}), {})
|
||||
return (sse({'e': '服务未配置 API Key,请在 .env 或环境变量中设置 DEEPSEEK_API_KEY 后重启服务'}), {})
|
||||
know_text = search_knowledge(msg)
|
||||
ship = parse_shipping_intent(msg)
|
||||
ship['dim_unit'] = detect_dim_unit(msg)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Configuration defaults; importing this module does not initialize storage."""
|
||||
"""Read local configuration without changing the process environment or storage."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -10,12 +10,16 @@ MAX_UPLOAD_MB = 10
|
||||
|
||||
|
||||
def default_config():
|
||||
from dotenv import dotenv_values
|
||||
|
||||
local_config = dotenv_values(ROOT_DIR / '.env', encoding='utf-8-sig', interpolate=False)
|
||||
api_key = os.environ.get('DEEPSEEK_API_KEY', local_config.get('DEEPSEEK_API_KEY'))
|
||||
return {
|
||||
'DATABASE': ROOT_DIR / 'history.db',
|
||||
'UPLOAD_DIR': ROOT_DIR / 'uploads',
|
||||
'BACKUP_DIR': ROOT_DIR / 'backups',
|
||||
'LOG_DIR': ROOT_DIR / 'logs',
|
||||
'DEEPSEEK_API_KEY': os.environ.get('DEEPSEEK_API_KEY', '').strip(),
|
||||
'DEEPSEEK_API_KEY': (api_key or '').strip(),
|
||||
'MODEL_URL': 'https://api.deepseek.com/v1/chat/completions',
|
||||
'MODEL_NAME': 'deepseek-chat',
|
||||
'MAX_CONTEXT': 4,
|
||||
|
||||
Binary file not shown.
@@ -59,21 +59,8 @@ if not errorlevel 1 (
|
||||
)
|
||||
echo OK - port is free.
|
||||
|
||||
echo [2/3] Checking DEEPSEEK_API_KEY ...
|
||||
if not defined DEEPSEEK_API_KEY (
|
||||
echo.
|
||||
echo [WARN] DEEPSEEK_API_KEY is empty in this window.
|
||||
echo .env files are not loaded automatically.
|
||||
echo If you just ran setx, reopen this window or restart VS Code.
|
||||
echo setx DEEPSEEK_API_KEY "sk-xxxxxxxx"
|
||||
echo.
|
||||
pause
|
||||
) else (
|
||||
echo OK - key detected.
|
||||
)
|
||||
|
||||
echo [3/3] Checking dependencies ...
|
||||
"%PYTHON_EXE%" %PYTHON_ARGS% -c "import flask, requests, waitress" >nul 2>nul
|
||||
echo [2/3] Checking dependencies ...
|
||||
"%PYTHON_EXE%" %PYTHON_ARGS% -c "import flask, requests, waitress, dotenv" >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo Missing packages, installing ...
|
||||
"%PYTHON_EXE%" %PYTHON_ARGS% -m pip install -r requirements.txt
|
||||
@@ -85,6 +72,15 @@ if errorlevel 1 (
|
||||
)
|
||||
echo OK - dependencies ready.
|
||||
|
||||
echo [3/3] Checking DeepSeek configuration ...
|
||||
"%PYTHON_EXE%" %PYTHON_ARGS% -c "import sys; from logistics_chat.config import default_config; sys.exit(0 if default_config()['DEEPSEEK_API_KEY'] else 1)" >nul
|
||||
if errorlevel 1 (
|
||||
echo [WARN] No DeepSeek API key configured.
|
||||
echo Copy .env.example to .env and set DEEPSEEK_API_KEY, then restart.
|
||||
) else (
|
||||
echo OK - key detected in .env or environment.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Starting: http://127.0.0.1:5000
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Configuration boundaries; credentials and model calls use synthetic fixtures."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, sentinel
|
||||
|
||||
from logistics_chat import config, create_app
|
||||
|
||||
|
||||
class ConfigurationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name)
|
||||
root_patch = patch.object(config, 'ROOT_DIR', self.root)
|
||||
root_patch.start()
|
||||
self.addCleanup(root_patch.stop)
|
||||
environment = patch.dict(os.environ)
|
||||
environment.start()
|
||||
self.addCleanup(environment.stop)
|
||||
os.environ.pop('DEEPSEEK_API_KEY', None)
|
||||
|
||||
def write_env(self, text):
|
||||
(self.root / '.env').write_text(text, encoding='utf-8')
|
||||
|
||||
def application(self, **overrides):
|
||||
return create_app({'TESTING': True, 'INITIALIZE_RUNTIME': False, **overrides})
|
||||
|
||||
def test_missing_file_leaves_ai_disabled_without_creating_files(self):
|
||||
self.assertEqual(config.default_config()['DEEPSEEK_API_KEY'], '')
|
||||
self.assertEqual(list(self.root.iterdir()), [])
|
||||
|
||||
def test_file_accepts_bom_quotes_comments_and_whitespace(self):
|
||||
self.write_env('\ufeff# Local configuration\nexport DEEPSEEK_API_KEY=" file-key " # comment\n')
|
||||
self.assertEqual(self.application().config['DEEPSEEK_API_KEY'], 'file-key')
|
||||
self.assertNotIn('DEEPSEEK_API_KEY', os.environ)
|
||||
|
||||
def test_empty_or_unassigned_file_value_leaves_ai_disabled(self):
|
||||
for line in ('DEEPSEEK_API_KEY=', 'DEEPSEEK_API_KEY', 'OTHER_SETTING=unused'):
|
||||
with self.subTest(line=line):
|
||||
self.write_env(line + '\n')
|
||||
self.assertEqual(config.default_config()['DEEPSEEK_API_KEY'], '')
|
||||
|
||||
def test_environment_overrides_file_including_an_explicit_empty_value(self):
|
||||
self.write_env('DEEPSEEK_API_KEY=file-key\n')
|
||||
for value, expected in ((' environment-key ', 'environment-key'), ('', '')):
|
||||
with self.subTest(value=value):
|
||||
os.environ['DEEPSEEK_API_KEY'] = value
|
||||
self.assertEqual(self.application().config['DEEPSEEK_API_KEY'], expected)
|
||||
|
||||
def test_file_values_are_literal_and_do_not_expand_environment_variables(self):
|
||||
os.environ['SYNTHETIC_CONFIG_VALUE'] = 'expanded'
|
||||
self.write_env("DEEPSEEK_API_KEY='literal-${SYNTHETIC_CONFIG_VALUE}'\n")
|
||||
self.assertEqual(config.default_config()['DEEPSEEK_API_KEY'], 'literal-${SYNTHETIC_CONFIG_VALUE}')
|
||||
|
||||
def test_file_is_resolved_from_repository_root_instead_of_working_directory(self):
|
||||
self.write_env('DEEPSEEK_API_KEY=repository-key\n')
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
(Path(directory) / '.env').write_text('DEEPSEEK_API_KEY=wrong-key\n', encoding='utf-8')
|
||||
previous = Path.cwd()
|
||||
try:
|
||||
os.chdir(directory)
|
||||
self.assertEqual(self.application().config['DEEPSEEK_API_KEY'], 'repository-key')
|
||||
finally:
|
||||
os.chdir(previous)
|
||||
|
||||
def test_explicit_factory_configuration_has_highest_priority(self):
|
||||
self.write_env('DEEPSEEK_API_KEY=file-key\n')
|
||||
os.environ['DEEPSEEK_API_KEY'] = 'environment-key'
|
||||
for value in ('override-key', ''):
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(self.application(DEEPSEEK_API_KEY=value).config['DEEPSEEK_API_KEY'], value)
|
||||
|
||||
def test_new_factory_reads_updated_file_without_changing_an_existing_app(self):
|
||||
self.write_env('DEEPSEEK_API_KEY=first-key\n')
|
||||
first = self.application()
|
||||
self.write_env('DEEPSEEK_API_KEY=second-key\n')
|
||||
self.assertEqual(self.application().config['DEEPSEEK_API_KEY'], 'second-key')
|
||||
self.assertEqual(first.config['DEEPSEEK_API_KEY'], 'first-key')
|
||||
self.assertNotIn('DEEPSEEK_API_KEY', os.environ)
|
||||
|
||||
def test_model_client_sends_file_key_without_a_real_http_request(self):
|
||||
from logistics_chat.ai_client import post_completion
|
||||
|
||||
self.write_env('DEEPSEEK_API_KEY=file-key\n')
|
||||
app = self.application()
|
||||
with app.app_context(), patch('logistics_chat.ai_client.requests.post', return_value=sentinel.response) as post:
|
||||
response = post_completion({'model': 'deepseek-chat'}, timeout=(5, 90), stream=True)
|
||||
self.assertIs(response, sentinel.response)
|
||||
self.assertEqual(post.call_args.kwargs['headers']['Authorization'], 'Bearer file-key')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user