Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ DATABASE_URL=
SECURITY_DATABASE_URL=
# 仅供 Alembic/受控维护命令使用,不得注入后端或 Celery 常规进程。
MIGRATION_DATABASE_URL=
# 仅注入执行 retrain_model 的 Worker;必须是 beta10d_forecast_login 受限 PostgreSQL 身份。
# 该身份只读训练事实表,并只允许向 model_registry 插入 Candidate,不得使用应用或迁移身份替代。
MODEL_TRAINING_DATABASE_URL=
DATABASE_PRIMARY=postgresql
# Compatibility fallback is opt-in for legacy import/debug only. Normal development and production keep 0.
DATABASE_ALLOW_LEGACY_FALLBACK=0
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ docx_render_check_*/
.codex_tmp/
.codex_envs/
backups/
docs/codex/evidence/
wheelhouse/
browser_cache/
.browser_cache/
Expand Down Expand Up @@ -59,6 +60,7 @@ frontend/.vite/
/*-snapshot.md
/*-console.txt
/*-layout-check.json
/备-AGENTS.md

# Large local model/data artifacts
*.zip
Expand All @@ -76,6 +78,7 @@ model_artifacts/*/*
!model_artifacts/*/thresholds.json
!model_artifacts/*/training_config.json
/model_artifacts/model_day6b_*/
/model_artifacts/model_run_*/
*.db
*.sqlite
*.sqlite3
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.13.0-rc1
29 changes: 22 additions & 7 deletions backend/app/ai_assistant/attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def _safe_name(value: str) -> str:

def _scope(identity: IdentityContext) -> Path:
identity.require_valid(require_session=False)
digest = hashlib.sha256(f"{identity.tenant_id}\0{identity.user_id}".encode()).hexdigest()[:32]
digest = hashlib.sha256(
f"{identity.tenant_id}\0{identity.workspace_id}\0{identity.user_id}".encode()
).hexdigest()[:32]
return ROOT / digest


Expand Down Expand Up @@ -212,7 +214,8 @@ def upload_attachment(identity: IdentityContext, *, session_id: str, file_name:
"media_type": declared, "size_bytes": len(content), "sha256": hashlib.sha256(content).hexdigest(),
"status": "uploading", "parser": None, "created_at": now.isoformat(),
"expires_at": (now + timedelta(hours=int(os.environ.get("AI_ATTACHMENT_TTL_HOURS", "24")))).isoformat(),
"tenant_id": identity.tenant_id, "user_id": identity.user_id, "error": None, "chunks": [], "properties": {},
"tenant_id": identity.tenant_id, "workspace_id": identity.workspace_id,
"user_id": identity.user_id, "error": None, "chunks": [], "properties": {},
}
data_path.parent.mkdir(parents=True, exist_ok=True)
data_path.write_bytes(content)
Expand All @@ -237,14 +240,26 @@ def upload_attachment(identity: IdentityContext, *, session_id: str, file_name:
raise


def get_attachment(identity: IdentityContext, attachment_id: str, *, session_id: str | None = None) -> dict[str, Any]:
def _require_session_id(session_id: str) -> str:
normalized = str(session_id or "").strip()
if not SESSION_RE.fullmatch(normalized):
raise AttachmentError("VALIDATION_FAILED", "session_id 无效。", status_code=422)
return normalized


def get_attachment(identity: IdentityContext, attachment_id: str, *, session_id: str) -> dict[str, Any]:
session_id = _require_session_id(session_id)
data_path, meta_path = _paths(identity, attachment_id)
if not meta_path.is_file():
raise AttachmentError("ATTACHMENT_NOT_FOUND", "附件不存在。", status_code=404)
meta = json.loads(meta_path.read_text(encoding="utf-8"))
if meta.get("tenant_id") != identity.tenant_id or meta.get("user_id") != identity.user_id:
if (
meta.get("tenant_id") != identity.tenant_id
or meta.get("workspace_id") != identity.workspace_id
or meta.get("user_id") != identity.user_id
):
raise AttachmentError("PERMISSION_DENIED", "无权访问该附件。", status_code=403)
if session_id and meta.get("session_id") != session_id:
if meta.get("session_id") != session_id:
raise AttachmentError("PERMISSION_DENIED", "附件不属于当前会话。", status_code=403)
if datetime.fromisoformat(meta["expires_at"]) <= datetime.now(timezone.utc) and meta.get("status") not in TERMINAL_STATUSES:
_discard_attachment_payload(
Expand All @@ -257,8 +272,8 @@ def get_attachment(identity: IdentityContext, attachment_id: str, *, session_id:
return meta


def delete_attachment(identity: IdentityContext, attachment_id: str) -> dict[str, Any]:
meta = get_attachment(identity, attachment_id)
def delete_attachment(identity: IdentityContext, attachment_id: str, *, session_id: str) -> dict[str, Any]:
meta = get_attachment(identity, attachment_id, session_id=session_id)
data_path, meta_path = _paths(identity, attachment_id)
if meta.get("status") not in {"cancelled", "deleted"}:
terminal_status = "cancelled" if meta.get("status") in {"uploading", "parsing"} else "deleted"
Expand Down
5 changes: 4 additions & 1 deletion backend/app/ai_assistant/capability_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ def fallback_capability(primary: ModelCapability) -> ModelCapability | None:
"data.export": "data:export",
"strategy.generate": "strategy:generate",
"report.generate": "report:generate",
"report.approve": "report:review",
"report.reject": "report:review",
"report.publish": "report:publish",
"settings.write": "settings:write",
"assistant.use": "assistant:use",
"assistant.export": "assistant:export",
Expand All @@ -128,7 +131,7 @@ def capability_manifest(permissions: list[str]) -> dict[str, Any]:
return {
"routes": {key: permits(value) for key, value in ROUTE_PERMISSIONS.items()},
"actions": {key: permits(value) for key, value in ACTION_PERMISSIONS.items()},
"policy_version": "v2.12.0",
"policy_version": "v2.13.0-rc1",
}


Expand Down
Loading
Loading