chore(config): 初始化项目配置与部署基础
This commit is contained in:
18
.env.example
Normal file
18
.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=*
|
||||
|
||||
# OpenAI-compatible LLM API
|
||||
LLM_API_KEY=your_llm_api_key
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_MODEL=gpt-4.1-mini
|
||||
|
||||
# Embedding model for RAG
|
||||
# Leave EMBEDDING_API_KEY empty to reuse LLM_API_KEY if desired.
|
||||
EMBEDDING_API_KEY=
|
||||
EMBEDDING_BASE_URL=
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
SCENARIO_CONFIG_DIR=configs
|
||||
UPLOAD_ROOT=data/uploads
|
||||
CHROMA_PATH=data/chroma
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . /app/
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
|
||||
35
README.md
35
README.md
@@ -146,6 +146,41 @@ docker compose up --build
|
||||
|
||||
当前文档目标已统一为完整 V1 闭环:真实 Chroma RAG、OpenAI 兼容 LLM、OpenAI 兼容 Embedding、工具注册和审计日志。开发阶段可以用测试桩验证页面和边界,但不作为 V1 验收结果。
|
||||
|
||||
## 环境变量
|
||||
|
||||
项目当前通过 `os.environ` 读取配置,核心变量如下:
|
||||
|
||||
```env
|
||||
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=*
|
||||
|
||||
LLM_API_KEY=your_llm_api_key
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_MODEL=gpt-4.1-mini
|
||||
|
||||
EMBEDDING_API_KEY=
|
||||
EMBEDDING_BASE_URL=
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
SCENARIO_CONFIG_DIR=configs
|
||||
UPLOAD_ROOT=data/uploads
|
||||
CHROMA_PATH=data/chroma
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `EMBEDDING_API_KEY` 为空时,代码会自动复用 `LLM_API_KEY`。
|
||||
- `EMBEDDING_BASE_URL` 为空时,代码会自动复用 `LLM_BASE_URL`。
|
||||
- `.env.example` 只作为模板,不应填写真实密钥并提交到仓库。
|
||||
- 当前代码会在 Django settings 初始化时自动加载根目录 `.env`,本地 `python manage.py runserver`、`pytest` 和 Docker Compose 可以复用同一套配置。
|
||||
- Docker Compose 当前在 `docker-compose.yml` 中通过 `env_file` 读取 `.env`。
|
||||
|
||||
常见做法:
|
||||
|
||||
- 本地开发:复制 `.env.example` 为 `.env`,填入真实参数后运行。
|
||||
- Docker 演示:确认 `.env` 已配置后,再执行 `docker compose up --build`。
|
||||
|
||||
## 文档入口
|
||||
|
||||
- [V1 总需求文档](docs/需求分析/1.V1总需求文档.md)
|
||||
|
||||
1
config/__init__.py
Normal file
1
config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
8
config/asgi.py
Normal file
8
config/asgi.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
108
config/settings.py
Normal file
108
config/settings.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_dotenv(dotenv_path: Path) -> None:
|
||||
if not dotenv_path.exists():
|
||||
return
|
||||
for raw_line in dotenv_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
|
||||
load_dotenv(BASE_DIR / ".env")
|
||||
|
||||
|
||||
def env_bool(name: str, default: bool = False) -> bool:
|
||||
value = os.environ.get(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key")
|
||||
DEBUG = env_bool("DJANGO_DEBUG", True)
|
||||
ALLOWED_HOSTS = [
|
||||
host.strip()
|
||||
for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",")
|
||||
if host.strip()
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"apps.scenarios",
|
||||
"apps.documents",
|
||||
"apps.chat",
|
||||
"apps.audit",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "data" / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
LANGUAGE_CODE = "zh-hans"
|
||||
TIME_ZONE = "Asia/Shanghai"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_ROOT = Path(os.environ.get("UPLOAD_ROOT", BASE_DIR / "data" / "uploads"))
|
||||
|
||||
SCENARIO_CONFIG_DIR = Path(os.environ.get("SCENARIO_CONFIG_DIR", BASE_DIR / "configs"))
|
||||
CHROMA_PATH = Path(os.environ.get("CHROMA_PATH", BASE_DIR / "data" / "chroma"))
|
||||
|
||||
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
|
||||
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-4.1-mini")
|
||||
EMBEDDING_API_KEY = os.environ.get("EMBEDDING_API_KEY", LLM_API_KEY)
|
||||
EMBEDDING_BASE_URL = os.environ.get("EMBEDDING_BASE_URL", LLM_BASE_URL)
|
||||
EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "text-embedding-3-small")
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
16
config/urls.py
Normal file
16
config/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("apps.scenarios.urls")),
|
||||
path("chat/", include("apps.chat.urls")),
|
||||
path("documents/", include("apps.documents.urls")),
|
||||
path("audit/", include("apps.audit.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
8
config/wsgi.py
Normal file
8
config/wsgi.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./configs:/app/configs
|
||||
@@ -18,6 +18,8 @@ python manage.py runserver
|
||||
|
||||
本地运行使用 SQLite、`data/uploads` 和 `data/chroma`。
|
||||
|
||||
当前本地方式会在启动时自动读取根目录 `.env`,因此 `runserver`、`pytest` 和日常脚本可以共享同一套配置。
|
||||
|
||||
## 3. Docker 运行方式
|
||||
|
||||
建议命令:
|
||||
@@ -47,6 +49,12 @@ V1 Docker Compose 只需要一个 Django Web 服务。Chroma 使用本地持久
|
||||
|
||||
`.env.example` 应提供这些变量的样例,不写真实密钥。
|
||||
|
||||
当前实现说明:
|
||||
|
||||
- 本地 Python 方式启动时,会先加载根目录 `.env`,再读取进程环境中的覆盖值。
|
||||
- Docker Compose 方式可通过 `env_file` 向容器注入环境变量;当前仓库默认读取 `.env`。
|
||||
- 因此本地运行和容器运行可以默认共用一份 `.env`,但演示前仍应确认密钥和模型参数是否正确。
|
||||
|
||||
## 5. 目录挂载设计
|
||||
|
||||
Docker 需要持久化以下目录:
|
||||
|
||||
@@ -97,6 +97,12 @@ V1 可使用标准库 `os.environ.get()`,不强制引入复杂配置库。
|
||||
|
||||
`DJANGO_ALLOWED_HOSTS` 使用逗号分隔,空值时默认 `["*"]`。
|
||||
|
||||
当前实现约束:
|
||||
|
||||
- 本地直接运行 Django 命令时,会先尝试解析根目录 `.env` 文件,再读取进程环境中的覆盖值。
|
||||
- Docker Compose 方式可以通过 `env_file` 传入同一批变量;当前仓库默认读取 `.env`。
|
||||
- `.env.example` 只保留占位符示例,不保存真实 API Key。
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
- `python manage.py check` 通过。
|
||||
|
||||
@@ -362,12 +362,28 @@ V1 模型适配器需要支持:
|
||||
环境变量示例:
|
||||
|
||||
```env
|
||||
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
|
||||
DJANGO_DEBUG=true
|
||||
DJANGO_ALLOWED_HOSTS=*
|
||||
|
||||
LLM_API_KEY=your_api_key
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_MODEL=gpt-4.1-mini
|
||||
EMBEDDING_API_KEY=
|
||||
EMBEDDING_BASE_URL=
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
SCENARIO_CONFIG_DIR=configs
|
||||
UPLOAD_ROOT=data/uploads
|
||||
CHROMA_PATH=data/chroma
|
||||
```
|
||||
|
||||
补充说明:
|
||||
|
||||
- `EMBEDDING_API_KEY` 为空时可复用 `LLM_API_KEY`。
|
||||
- `EMBEDDING_BASE_URL` 为空时可复用 `LLM_BASE_URL`。
|
||||
- `.env.example` 仅作为模板,不允许放真实密钥。
|
||||
- 当前 V1 代码会在 settings 初始化时自动读取根目录 `.env`,本地运行与 `pytest` 可复用同一套配置;当前 Docker Compose 配置也通过 `env_file` 读取 `.env`。
|
||||
|
||||
## 10. Dify 集成策略
|
||||
|
||||
V1 不把 Dify 作为核心依赖。
|
||||
|
||||
@@ -55,6 +55,12 @@ Config 模块是 Django 项目的基础配置模块,负责系统启动、路
|
||||
| `UPLOAD_ROOT` | `data/uploads` | 上传文件目录 |
|
||||
| `SCENARIO_CONFIG_DIR` | `configs` | 场景配置目录 |
|
||||
|
||||
补充要求:
|
||||
|
||||
- `.env.example` 仅作为模板文件,不得写入真实密钥。
|
||||
- 本地直接执行 `python manage.py runserver` 时,应自动读取根目录 `.env`。
|
||||
- Docker 运行时可通过 `env_file` 或容器环境变量注入同一组配置;当前仓库默认由 Compose 读取 `.env`。
|
||||
|
||||
## 5. 目录需求
|
||||
|
||||
系统启动时需要保证以下目录存在:
|
||||
@@ -91,6 +97,8 @@ python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
说明:上述命令执行前,应先准备好根目录 `.env`;当前 V1 代码会在启动时自动加载该文件。
|
||||
|
||||
Docker 启动:
|
||||
|
||||
```bash
|
||||
|
||||
14
manage.py
Normal file
14
manage.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = config.settings
|
||||
python_files = tests.py test_*.py *_tests.py
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Django>=5.1,<6.0
|
||||
PyYAML>=6.0,<7.0
|
||||
chromadb>=0.5,<1.0
|
||||
pytest>=8.0,<9.0
|
||||
pytest-django>=4.9,<5.0
|
||||
21
tests/test_project_configuration.py
Normal file
21
tests/test_project_configuration.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
def test_core_settings_expose_documented_paths():
|
||||
assert settings.SCENARIO_CONFIG_DIR.name == "configs"
|
||||
assert settings.CHROMA_PATH.name == "chroma"
|
||||
assert settings.MEDIA_ROOT.name == "uploads"
|
||||
assert settings.EMBEDDING_MODEL == os.environ.get(
|
||||
"EMBEDDING_MODEL",
|
||||
"text-embedding-3-small",
|
||||
)
|
||||
assert settings.EMBEDDING_BASE_URL == settings.LLM_BASE_URL
|
||||
assert settings.EMBEDDING_API_KEY == settings.LLM_API_KEY
|
||||
|
||||
|
||||
def test_home_url_is_registered(client):
|
||||
response = client.get(reverse("scenarios:index"))
|
||||
assert response.status_code == 200
|
||||
Reference in New Issue
Block a user