Merge remote-tracking branch 'origin/master' into master
This commit is contained in:
commit
f2ffae8a22
42 files changed
+1829
-431
No files matched your search
@@ -17,7 +17,7 @@ SERPER_API_KEY=your-serper-api-key
|
||||
# AUTH_JWT_SECRET must stay stable across restarts, or existing logins expire.
|
||||
# GATEWAY_CORS_ORIGINS must include the exact frontend origins, separated by commas.
|
||||
AUTH_JWT_SECRET=i3_BiBK44sYHiFIkChhDs46y57ZewJKO3diQDw01anI
|
||||
GATEWAY_CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:5183,http://localhost:5174,http://127.0.0.1:5174,http://localhost:4200
|
||||
GATEWAY_CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174,http://localhost:5183,http://127.0.0.1:5183,http://localhost:4200
|
||||
GATEWAY_CORS_ALLOW_ALL=0
|
||||
GATEWAY_ENABLE_DOCS=true
|
||||
GATEWAY_WORKERS=2
|
||||
|
||||
@@ -39,6 +39,8 @@ router = APIRouter(prefix="/api/roundtable-drafts", tags=["roundtable-drafts"])
|
||||
|
||||
class DraftMetaResponse(BaseModel):
|
||||
id: str
|
||||
# 外部任务 id(无界嵌入抽屉传入);非空时草稿按 task_id 共享、不按 user 分权。
|
||||
task_id: str | None = None
|
||||
title: str = ""
|
||||
furthest_step: int = 1
|
||||
created_at: datetime | str | None = None
|
||||
@@ -62,6 +64,8 @@ class DraftCreateRequest(BaseModel):
|
||||
# Optional client-provided id so the browser can keep one stable id across
|
||||
# the create + subsequent updates without an extra round-trip.
|
||||
id: str | None = Field(default=None, max_length=64)
|
||||
# 外部任务 id(无界嵌入抽屉);设置后草稿按 task_id 共享、不按 user 分权。
|
||||
task_id: str | None = Field(default=None, max_length=128)
|
||||
title: str = Field(default="", max_length=512)
|
||||
furthest_step: int = Field(default=1, ge=1, le=3)
|
||||
step1: dict[str, Any] | None = None
|
||||
@@ -74,6 +78,7 @@ class DraftUpdateRequest(BaseModel):
|
||||
# so it never clobbers a manual rename.
|
||||
title: str | None = Field(default=None, max_length=512)
|
||||
furthest_step: int | None = Field(default=None, ge=1, le=3)
|
||||
task_id: str | None = Field(default=None, max_length=128)
|
||||
step1: dict[str, Any] | None = None
|
||||
step2: dict[str, Any] | None = None
|
||||
step3: dict[str, Any] | None = None
|
||||
@@ -165,6 +170,7 @@ async def create_draft(request: Request, body: DraftCreateRequest) -> DraftRespo
|
||||
user_id,
|
||||
{
|
||||
"id": (body.id or uuid4().hex),
|
||||
"task_id": (body.task_id or None),
|
||||
"title": body.title.strip(),
|
||||
"furthest_step": body.furthest_step,
|
||||
"step1": body.step1,
|
||||
@@ -175,6 +181,19 @@ async def create_draft(request: Request, body: DraftCreateRequest) -> DraftRespo
|
||||
return DraftResponse(**row)
|
||||
|
||||
|
||||
@router.get("/by-task/{task_id}/latest", response_model=DraftResponse)
|
||||
async def get_latest_draft_by_task(request: Request, task_id: str) -> DraftResponse:
|
||||
"""按外部任务 id 取最新一条草稿(**不按 user 分权**);无则 404。
|
||||
|
||||
无界嵌入抽屉打开时调用:若该任务已有聊天记录,回显最新一条。
|
||||
"""
|
||||
store = _get_store(request)
|
||||
row = await store.get_latest_draft_by_task(task_id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="No draft for this task")
|
||||
return DraftResponse(**row)
|
||||
|
||||
|
||||
@router.get("/{draft_id}", response_model=DraftResponse)
|
||||
async def get_draft(request: Request, draft_id: str) -> DraftResponse:
|
||||
store = _get_store(request)
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
"""Add task_id column to roundtable_drafts.
|
||||
|
||||
External task id (passed by the wujie-embedded roundtable drawer). When set, the
|
||||
draft is keyed/shared by ``task_id`` and is no longer partitioned by ``user_id``
|
||||
(read/write/delete ownership checks are relaxed for task-bound drafts — see
|
||||
``sql.py``); when NULL the original per-user isolation is preserved.
|
||||
``create_all`` adds the column for brand-new DBs; this revision patches DBs that
|
||||
already have the table.
|
||||
|
||||
Revision ID: 20260611_01
|
||||
Revises: 20260609_02
|
||||
Create Date: 2026-06-11
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "20260611_01"
|
||||
down_revision = "20260609_02"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _has_table(table_name: str) -> bool:
|
||||
inspector = inspect(op.get_bind())
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
if not _has_table(table_name):
|
||||
return False
|
||||
inspector = inspect(op.get_bind())
|
||||
return any(col["name"] == column_name for col in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if _has_table("roundtable_drafts") and not _has_column("roundtable_drafts", "task_id"):
|
||||
op.add_column("roundtable_drafts", sa.Column("task_id", sa.String(128), nullable=True))
|
||||
op.create_index("ix_roundtable_drafts_task_id", "roundtable_drafts", ["task_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if _has_column("roundtable_drafts", "task_id"):
|
||||
op.drop_index("ix_roundtable_drafts_task_id", table_name="roundtable_drafts")
|
||||
op.drop_column("roundtable_drafts", "task_id")
|
||||
+3
@@ -26,6 +26,9 @@ class RoundtableDraftRow(Base):
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
# 外部任务 id(无界嵌入抽屉传入)。非空时该草稿按 task_id 共享、不再按 user_id 分权
|
||||
# (读/写/删放开归属校验,见 sql.py);为空时保持原「按登录用户隔离」语义。
|
||||
task_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
||||
# Auto-derived task summary (objective → first user message → timestamp).
|
||||
title: Mapped[str] = mapped_column(String(512), nullable=False, default="")
|
||||
# Furthest step reached when this snapshot was taken (1 | 2 | 3).
|
||||
|
||||
+24
-3
@@ -53,6 +53,7 @@ class RoundtableDraftRepository:
|
||||
"""Lightweight projection for the history dropdown (no big blobs)."""
|
||||
return {
|
||||
"id": row.id,
|
||||
"task_id": row.task_id,
|
||||
"title": row.title,
|
||||
"furthest_step": row.furthest_step,
|
||||
"created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at,
|
||||
@@ -81,15 +82,30 @@ class RoundtableDraftRepository:
|
||||
async def get_draft(self, draft_id: str, user_id: str) -> dict[str, Any] | None:
|
||||
async with self._sf() as session:
|
||||
row = await session.get(RoundtableDraftRow, draft_id)
|
||||
if row is None or row.user_id != user_id:
|
||||
# task 绑定的草稿共享:放开 user 归属校验(不分权);非 task 草稿仍按 user 隔离。
|
||||
if row is None or (row.user_id != user_id and not row.task_id):
|
||||
return None
|
||||
return self._draft_to_dict(row)
|
||||
|
||||
async def get_latest_draft_by_task(self, task_id: str) -> dict[str, Any] | None:
|
||||
"""按外部任务 id 取最新一条草稿(**忽略 user_id**,不分权)。无则 None。"""
|
||||
stmt = (
|
||||
select(RoundtableDraftRow)
|
||||
.where(RoundtableDraftRow.task_id == task_id)
|
||||
.order_by(RoundtableDraftRow.updated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
async with self._sf() as session:
|
||||
result = await session.execute(stmt)
|
||||
row = result.scalars().first()
|
||||
return self._draft_to_dict(row) if row is not None else None
|
||||
|
||||
async def create_draft(self, user_id: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
now = datetime.now(UTC)
|
||||
row = RoundtableDraftRow(
|
||||
id=data["id"],
|
||||
user_id=user_id,
|
||||
task_id=(data.get("task_id") or None),
|
||||
title=(data.get("title") or "").strip(),
|
||||
furthest_step=int(data.get("furthest_step") or 1),
|
||||
step1=_dumps(data.get("step1")),
|
||||
@@ -114,11 +130,15 @@ class RoundtableDraftRepository:
|
||||
step1=_UNSET,
|
||||
step2=_UNSET,
|
||||
step3=_UNSET,
|
||||
task_id=_UNSET,
|
||||
) -> dict[str, Any] | None:
|
||||
async with self._sf() as session:
|
||||
row = await session.get(RoundtableDraftRow, draft_id)
|
||||
if row is None or row.user_id != user_id:
|
||||
# task 绑定的草稿共享:放开 user 归属校验(不分权)。
|
||||
if row is None or (row.user_id != user_id and not row.task_id):
|
||||
return None
|
||||
if task_id is not _UNSET and task_id is not None:
|
||||
row.task_id = str(task_id)
|
||||
if title is not _UNSET and title is not None:
|
||||
row.title = str(title).strip()
|
||||
if furthest_step is not _UNSET and furthest_step is not None:
|
||||
@@ -137,7 +157,8 @@ class RoundtableDraftRepository:
|
||||
async def delete_draft(self, draft_id: str, user_id: str) -> bool:
|
||||
async with self._sf() as session:
|
||||
row = await session.get(RoundtableDraftRow, draft_id)
|
||||
if row is None or row.user_id != user_id:
|
||||
# task 绑定的草稿共享:放开 user 归属校验(不分权)。
|
||||
if row is None or (row.user_id != user_id and not row.task_id):
|
||||
return False
|
||||
await session.delete(row)
|
||||
await session.commit()
|
||||
|
||||
@@ -71,13 +71,34 @@ CALL _add_column_if_missing('skills', 'square_id', "VARCHAR(64) NOT NULL DEFAULT
|
||||
-- ORM: AgentRow.square_id = String(64), nullable=False, default=''。
|
||||
CALL _add_column_if_missing('agents', 'square_id', "VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
|
||||
-- ---- roundtable_drafts -------------------------------------------------------
|
||||
-- task_id: 外部任务 id(无界嵌入抽屉传入),对应 migration 20260611_01。非空时草稿
|
||||
-- 按 task_id 共享、不再按 user_id 分权。ORM: RoundtableDraftRow.task_id =
|
||||
-- String(128), nullable=True, index=True。
|
||||
CALL _add_column_if_missing('roundtable_drafts', 'task_id', 'VARCHAR(128) NULL DEFAULT NULL');
|
||||
|
||||
-- ---- 清理临时存储过程 --------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS _add_column_if_missing;
|
||||
|
||||
-- ---- roundtable_drafts.task_id 索引(幂等) -----------------------------------
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'roundtable_drafts'
|
||||
AND index_name = 'ix_roundtable_drafts_task_id'
|
||||
);
|
||||
SET @idx_ddl = IF(@idx_exists = 0,
|
||||
'CREATE INDEX `ix_roundtable_drafts_task_id` ON `roundtable_drafts` (`task_id`)',
|
||||
'SELECT ''SKIPPED ix_roundtable_drafts_task_id (already exists)'' AS result');
|
||||
PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- 执行后自检:确认本批次涉及的列都已存在 ---------------------------------
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND ((table_name = 'skills' AND column_name IN ('name_zh', 'detail', 'square_id'))
|
||||
OR (table_name = 'agents' AND column_name IN ('square_id')))
|
||||
OR (table_name = 'agents' AND column_name IN ('square_id'))
|
||||
OR (table_name = 'roundtable_drafts' AND column_name = 'task_id'))
|
||||
ORDER BY table_name, column_name;
|
||||
Reference in new issue
Block a user