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 验收结果。
|
当前文档目标已统一为完整 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)
|
- [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`。
|
本地运行使用 SQLite、`data/uploads` 和 `data/chroma`。
|
||||||
|
|
||||||
|
当前本地方式会在启动时自动读取根目录 `.env`,因此 `runserver`、`pytest` 和日常脚本可以共享同一套配置。
|
||||||
|
|
||||||
## 3. Docker 运行方式
|
## 3. Docker 运行方式
|
||||||
|
|
||||||
建议命令:
|
建议命令:
|
||||||
@@ -47,6 +49,12 @@ V1 Docker Compose 只需要一个 Django Web 服务。Chroma 使用本地持久
|
|||||||
|
|
||||||
`.env.example` 应提供这些变量的样例,不写真实密钥。
|
`.env.example` 应提供这些变量的样例,不写真实密钥。
|
||||||
|
|
||||||
|
当前实现说明:
|
||||||
|
|
||||||
|
- 本地 Python 方式启动时,会先加载根目录 `.env`,再读取进程环境中的覆盖值。
|
||||||
|
- Docker Compose 方式可通过 `env_file` 向容器注入环境变量;当前仓库默认读取 `.env`。
|
||||||
|
- 因此本地运行和容器运行可以默认共用一份 `.env`,但演示前仍应确认密钥和模型参数是否正确。
|
||||||
|
|
||||||
## 5. 目录挂载设计
|
## 5. 目录挂载设计
|
||||||
|
|
||||||
Docker 需要持久化以下目录:
|
Docker 需要持久化以下目录:
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ V1 可使用标准库 `os.environ.get()`,不强制引入复杂配置库。
|
|||||||
|
|
||||||
`DJANGO_ALLOWED_HOSTS` 使用逗号分隔,空值时默认 `["*"]`。
|
`DJANGO_ALLOWED_HOSTS` 使用逗号分隔,空值时默认 `["*"]`。
|
||||||
|
|
||||||
|
当前实现约束:
|
||||||
|
|
||||||
|
- 本地直接运行 Django 命令时,会先尝试解析根目录 `.env` 文件,再读取进程环境中的覆盖值。
|
||||||
|
- Docker Compose 方式可以通过 `env_file` 传入同一批变量;当前仓库默认读取 `.env`。
|
||||||
|
- `.env.example` 只保留占位符示例,不保存真实 API Key。
|
||||||
|
|
||||||
## 8. 验收标准
|
## 8. 验收标准
|
||||||
|
|
||||||
- `python manage.py check` 通过。
|
- `python manage.py check` 通过。
|
||||||
|
|||||||
@@ -362,12 +362,28 @@ V1 模型适配器需要支持:
|
|||||||
环境变量示例:
|
环境变量示例:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
DJANGO_SECRET_KEY=replace-with-a-local-secret-key
|
||||||
|
DJANGO_DEBUG=true
|
||||||
|
DJANGO_ALLOWED_HOSTS=*
|
||||||
|
|
||||||
LLM_API_KEY=your_api_key
|
LLM_API_KEY=your_api_key
|
||||||
LLM_BASE_URL=https://api.openai.com/v1
|
LLM_BASE_URL=https://api.openai.com/v1
|
||||||
LLM_MODEL=gpt-4.1-mini
|
LLM_MODEL=gpt-4.1-mini
|
||||||
|
EMBEDDING_API_KEY=
|
||||||
|
EMBEDDING_BASE_URL=
|
||||||
EMBEDDING_MODEL=text-embedding-3-small
|
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 集成策略
|
## 10. Dify 集成策略
|
||||||
|
|
||||||
V1 不把 Dify 作为核心依赖。
|
V1 不把 Dify 作为核心依赖。
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ Config 模块是 Django 项目的基础配置模块,负责系统启动、路
|
|||||||
| `UPLOAD_ROOT` | `data/uploads` | 上传文件目录 |
|
| `UPLOAD_ROOT` | `data/uploads` | 上传文件目录 |
|
||||||
| `SCENARIO_CONFIG_DIR` | `configs` | 场景配置目录 |
|
| `SCENARIO_CONFIG_DIR` | `configs` | 场景配置目录 |
|
||||||
|
|
||||||
|
补充要求:
|
||||||
|
|
||||||
|
- `.env.example` 仅作为模板文件,不得写入真实密钥。
|
||||||
|
- 本地直接执行 `python manage.py runserver` 时,应自动读取根目录 `.env`。
|
||||||
|
- Docker 运行时可通过 `env_file` 或容器环境变量注入同一组配置;当前仓库默认由 Compose 读取 `.env`。
|
||||||
|
|
||||||
## 5. 目录需求
|
## 5. 目录需求
|
||||||
|
|
||||||
系统启动时需要保证以下目录存在:
|
系统启动时需要保证以下目录存在:
|
||||||
@@ -91,6 +97,8 @@ python manage.py migrate
|
|||||||
python manage.py runserver
|
python manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:上述命令执行前,应先准备好根目录 `.env`;当前 V1 代码会在启动时自动加载该文件。
|
||||||
|
|
||||||
Docker 启动:
|
Docker 启动:
|
||||||
|
|
||||||
```bash
|
```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