Files
DadaoTask/backend/app/config.py
T
dadao 17be9d1d42
CI / backend (push) Canceled after 0s
feat(rich): 安卓能编辑富文本了,图片直链换成短期签名,表格不再撑破抽屉
三件欠账一次清:

1. 图片直链签名化
   - 新增 `backend/app/services/signing.py`:HMAC-SHA256,密钥由 `JWT_SECRET` 域分离派生
     (`attachment-raw-v1`),载荷 `workspace_id:attachment_id:exp`,默认 30 分钟
   - `/raw` 支持 `?exp=&sig=`,不需要登录态;签名里绑了工作区,跨租户链接直接拒
   - 附件对象多一个 `raw_url`,前端每次渲染现签,正文里只存不带凭据的 `/raw`
   - 保留旧 `?token=` 通道,给未升级的客户端过渡

2. 安卓富文本编辑器
   - `ui/RichTextEditorScreen.kt`:Markdown 工具栏(粗体/斜体/标题/列表/引用/代码/链接/图片),
     选区包裹与行首前缀都按光标位置来;贴图走附件接口,插入签名直链
   - 预览用 Markwon,图片用 Coil;正文落在一张白纸卡片里,不再贴屏幕边
   - 从任务详情与知识点都能进,保存按 `body_md` 回写(PATCH 带 version)

3. 表格
   - 只读渲染走 markdown-it 的 GFM 表,`display:block + overflow-x:auto`:
     宽表横向滚动、窄表铺满,都不撑破抽屉

验证:pytest 74 passed;richtext 15 / smoke 7 / tickets 8 / knowledge 7 全绿;
tsc -b 0 错;vite build 通过(主包 299.42 kB / gzip 74.96 kB);
安卓 assembleDebug 成功并在 API 35 模拟器走通编辑与预览。

仍欠:保存后回列表而非回详情抽屉、图片压缩与缩略图、HTML 片段按文本显示。
2026-10-29 03:32:53 +08:00

136 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""分层配置:dev / prod 全部走环境变量,启动即校验。"""
from __future__ import annotations
import os
from functools import lru_cache
from typing import Literal
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
DEV_JWT_SECRET = "dev-only-change-me" # noqa: S105 - 仅开发默认值,生产启动时会被校验拦下
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False
)
# ── 基础 ──────────────────────────────────────────────
app_name: str = "达道Task"
env: Literal["dev", "test", "prod"] = "dev"
api_prefix: str = "/api/v1"
cors_origins: list[str] = Field(
default_factory=lambda: ["http://localhost:5180", "http://127.0.0.1:5180"]
)
# ── 数据库 ────────────────────────────────────────────
# 应用角色:受 RLS 约束,非 owner、非 superuser
database_url: str = "postgresql+asyncpg://dadao_app:dadao_dev_pw@127.0.0.1:5433/dadaotask"
# 迁移角色:owner,可绕过 RLS,仅 alembic / 运维脚本使用
migration_database_url: str = (
"postgresql+asyncpg://dadao_owner:dadao_dev_pw@127.0.0.1:5433/dadaotask"
)
# 测试专用库:测试会 TRUNCATE 所有表,绝不能落到开发库上。
# 留空则退回 database_url(CI 里直接给 DATABASE_URL 即可)。
test_database_url: str | None = None
test_migration_database_url: str | None = None
db_echo: bool = False
db_pool_size: int = 10
# ── Redis ─────────────────────────────────────────────
redis_url: str = "redis://127.0.0.1:6380/0"
# ── 业务 ──────────────────────────────────────────────
# "今天/本周"这类视图按用户时区切日界,不能按 UTC —— 中国用户 23:30 建的任务
# 在 UTC 下已经是"明天"了。M7 多地域时改成按工作区配置。
default_timezone: str = "Asia/Shanghai"
# ── 认证 ──────────────────────────────────────────────
jwt_secret: str = DEV_JWT_SECRET
jwt_algorithm: str = "HS256"
access_token_minutes: int = 30
refresh_token_days: int = 14
sms_code_ttl_seconds: int = 300
# 同一手机号两次发码的最小间隔;测试里设为 0 可关闭节流
sms_throttle_seconds: int = 60
sms_dev_code: str = "123456"
sms_provider: Literal["dev", "aliyun", "tencent"] = "dev"
# ── 幂等 ──────────────────────────────────────────────
idempotency_enabled: bool = True
idempotency_ttl_seconds: int = 86_400
# ── 附件 ──────────────────────────────────────────────
# 本地磁盘目录(相对 backend/ 或绝对路径)。换对象存储时只改 services/storage.py。
upload_dir: str = "var/uploads"
# 单文件上限。20MB 够放文档与截图;再大就该走对象存储 + 分片了。
max_upload_mb: int = 20
# 图片直链的签名有效期。短一点更安全,太长等于把凭据长期贴在页面上;
# 30 分钟够一次阅读/编辑,过期后刷新页面会重新签发。
attachment_url_ttl_seconds: int = 1800
# ── 知识库向量库(独立实例,M3 起用)───────────────────
# 刻意与业务库分开放:fnsvr-01 上的 PG 没有 pgvector,也不该为了装扩展去动它;
# 向量检索吃内存,和任务库挤在同一实例会互相拖累。
# 未配置时所有向量相关能力(M3 才启用)直接不可用,不会静默降级成假结果。
vector_database_url: str | None = None
embedding_dim: int = 1024
embedding_model: str = "BAAI/bge-m3"
# ── 构建标记(部署校验用)──────────────────────────────
build_commit: str = "unknown"
build_time: str = "unknown"
@field_validator("cors_origins", mode="before")
@classmethod
def _split_origins(cls, value: object) -> object:
"""支持 CORS_ORIGINS="a,b" 这种写法。"""
if isinstance(value, str) and not value.startswith("["):
return [item.strip() for item in value.split(",") if item.strip()]
return value
def validate_runtime(self) -> None:
"""生产环境的致命配置错误,必须在启动时炸掉,而不是等待请求。"""
if self.env != "prod":
return
problems: list[str] = []
if self.jwt_secret == DEV_JWT_SECRET or len(self.jwt_secret) < 32:
problems.append("JWT_SECRET 仍是开发默认值或长度不足 32")
if "dadao_dev_pw" in self.database_url:
problems.append("DATABASE_URL 仍在用开发默认口令")
if self.sms_provider == "dev":
problems.append("生产环境不允许 SMS_PROVIDER=dev(任意验证码可登录)")
if problems:
raise RuntimeError("配置校验失败:" + ";".join(problems))
def resolved_database_url(self) -> str:
"""应用实际连的库:测试环境优先用独立测试库(测试会 TRUNCATE)。"""
if self.env == "test" and self.test_database_url:
return self.test_database_url
return self.database_url
def resolved_migration_database_url(self) -> str:
"""迁移/清表实际连的库:必须和应用同一个库,否则清表清到别处去了。"""
if self.env == "test" and self.test_migration_database_url:
return self.test_migration_database_url
return self.migration_database_url
def resolved_vector_database_url(self) -> str | None:
"""向量库连接串;没配就是 None,调用方必须显式处理"没有向量能力"。"""
return self.vector_database_url
@lru_cache
def get_settings() -> Settings:
return Settings()
def build_commit() -> str:
"""优先读环境变量(CI/镜像注入),退回 git。"""
env_value = os.getenv("BUILD_COMMIT")
if env_value:
return env_value
return get_settings().build_commit