Files
DadaoTask/backend/app/services/signing.py
T
dadao d079564d6c
CI / backend (push) Canceled after 0s
feat(knowledge): 语义检索打通——切片、混合打分、只搜已发布
之前知识只能"存",不能"找"。这一版把链路补上:切片 → 向量 → 打分 → 带来源返回。

- chunk 表 + 迁移 0006:正文按标题归段切、代码块整块保留、超长按句切、
  相邻片重叠 80 字;RLS 与 GRANT 与其它租户表一致
- 向量提供方做成协议:内置 hash(字符二元组,零依赖可复现,作为基线)
  与 OpenAI 兼容端点两种,换实现不动业务代码
- 检索是混合打分:向量 0.6 + pg_trgm 0.4。专有编号、单号这类必须原样命中的词
  靠词法兜,换个说法表达的同一件事靠向量兜
- 三条纪律:只搜 published(过滤写在 SQL 里)、低于阈值宁可不返回、
  每条命中都带来源标题与 item_id
- 发布时自动建索引,改完正文用 POST /knowledge/{id}/reindex 重建
- Web 知识页加检索框与命中卡片;新增 e2e/retrieval.mjs 6 项

为什么不是 pgvector:本机 Docker 起不来、fnsvr-01 没有该扩展且明确不动。
先用 real[] + SQL 余弦把链路跑通,将来换列类型与索引即可,接口不变。

后端 79 passed,e2e smoke/knowledge/richtext/retrieval 全绿。
2026-10-29 04:28:09 +08:00

48 lines
1.9 KiB
Python

"""附件直链的短期签名。
为什么需要它:Markdown 里的图片是 `<img src="…">`,浏览器不会给它加 Authorization 头,
所以图片地址必须自带凭据。把**登录令牌**塞进 query 是最省事的做法,但代价明确:
令牌会进 Nginx access log、浏览器历史、以及任何复制粘贴出去的链接里,有效期还长达 30 分钟。
这里换成一个只为"这一张图"服务、只活 30 分钟的签名:
`?exp=<unix 秒>&sig=<hmac 前 32 位>`。它不能用来调任何别的接口,泄漏了也只影响一张图的一小段时间。
"""
import hashlib
import hmac
import time
import uuid
from app.config import get_settings
# 域分离:即使有人拿 JWT 密钥来试,也拿不到同样的签名(反之亦然)
_DOMAIN = b"attachment-raw-v1"
def _key() -> bytes:
base = get_settings().jwt_secret.encode("utf-8")
return hmac.new(base, _DOMAIN, hashlib.sha256).digest()
def _digest(workspace_id: uuid.UUID, attachment_id: uuid.UUID, exp: int) -> str:
# 工作区也签进去,避免拿 A 工作区的签名去读 B 工作区的文件
payload = f"{workspace_id}:{attachment_id}:{exp}".encode()
return hmac.new(_key(), payload, hashlib.sha256).hexdigest()[:32]
def sign(workspace_id: uuid.UUID, attachment_id: uuid.UUID, ttl_seconds: int | None = None) -> str:
"""返回可直接拼在 `/raw` 后面的查询串。"""
ttl = ttl_seconds if ttl_seconds is not None else get_settings().attachment_url_ttl_seconds
exp = int(time.time()) + ttl
return f"exp={exp}&sig={_digest(workspace_id, attachment_id, exp)}"
def verify(
workspace_id: uuid.UUID, attachment_id: uuid.UUID, exp: int | None, sig: str | None
) -> bool:
if not exp or not sig:
return False
if exp < int(time.time()): # 过期就是过期,不给宽限
return False
return hmac.compare_digest(_digest(workspace_id, attachment_id, exp), sig)