feat: 支持会商 Markdown 产物提交到任务过程,并补充 Angular 对接文档
8BF/会商工作区可将 Markdown 产物一键提交到任务过程报告,后端记录提交状态以便跨用户与刷新后保持已提交;同时为 Angular 智能体对话对接文档补上前后端参考代码索引。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
33b0af333c
commit
dcfaa199bf
19 files changed
+1133
-43
No files matched your search
+22
@@ -52,6 +52,7 @@ class ArtifactSubmissionRequest(BaseModel):
|
||||
|
||||
task_id: StrictStr | None = Field(default=None, max_length=128, validation_alias=AliasChoices("taskId", "task_id"))
|
||||
submitted: bool
|
||||
register_only: bool = Field(default=False, validation_alias=AliasChoices("registerOnly", "register_only"))
|
||||
submission_id: StrictStr | None = Field(default=None, max_length=64, validation_alias=AliasChoices("submissionId", "submission_id"))
|
||||
task_delivery_id: StrictStr | None = Field(default=None, max_length=255, validation_alias=AliasChoices("taskDeliveryId", "task_delivery_id"))
|
||||
expected_version: int | None = Field(default=None, ge=0, validation_alias=AliasChoices("expectedVersion", "expected_version"))
|
||||
@@ -200,6 +201,27 @@ async def set_artifact_submission(
|
||||
"submission": _response(conflict.row).model_dump(by_alias=True, exclude_none=False),
|
||||
},
|
||||
) from conflict
|
||||
elif body.register_only:
|
||||
if not artifact["thread_id"]:
|
||||
raise HTTPException(status_code=422, detail="artifact.threadId is required when registering an artifact")
|
||||
try:
|
||||
row = await store.register(
|
||||
path_task_id,
|
||||
artifact,
|
||||
task_delivery_id=body.task_delivery_id,
|
||||
actor_id=actor_id,
|
||||
expected_version=body.expected_version,
|
||||
)
|
||||
except SubmissionVersionConflict as conflict:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={
|
||||
"code": "version_conflict",
|
||||
"message": "登记产物版本冲突:另一写入已更新,请刷新后重试。",
|
||||
"current_version": conflict.row.get("version", 0),
|
||||
"submission": _response(conflict.row).model_dump(by_alias=True, exclude_none=False),
|
||||
},
|
||||
) from conflict
|
||||
else:
|
||||
try:
|
||||
row = await store.cancel(
|
||||
|
||||
+17
@@ -37,6 +37,23 @@ class RoundtableArtifactSubmissionStore(ABC):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def register(
|
||||
self,
|
||||
task_id: str,
|
||||
artifact: dict[str, Any],
|
||||
*,
|
||||
task_delivery_id: str | None = None,
|
||||
actor_id: str | None = None,
|
||||
expected_version: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Persist a generated-but-not-yet-submitted artifact receipt.
|
||||
|
||||
This lets the UI retain the external task-process-report id across a
|
||||
refresh before the user explicitly submits the Markdown artifact.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def cancel(
|
||||
self,
|
||||
|
||||
+57
@@ -109,6 +109,63 @@ class MemoryRoundtableArtifactSubmissionStore(RoundtableArtifactSubmissionStore)
|
||||
)
|
||||
return deepcopy(row)
|
||||
|
||||
async def register(
|
||||
self,
|
||||
task_id: str,
|
||||
artifact: dict[str, Any],
|
||||
*,
|
||||
task_delivery_id: str | None = None,
|
||||
actor_id: str | None = None,
|
||||
expected_version: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Save an external receipt while keeping the artifact unsubmitted."""
|
||||
from deerflow.persistence.roundtable_artifact_submissions.sql import SubmissionVersionConflict
|
||||
|
||||
key = submission_key(task_id, artifact)
|
||||
row = self._rows.get((task_id, key))
|
||||
values = _artifact_values(artifact)
|
||||
if row is None:
|
||||
now = _now()
|
||||
row = {
|
||||
"id": uuid4().hex,
|
||||
"task_id": task_id,
|
||||
"idempotency_key": key,
|
||||
**values,
|
||||
"task_delivery_id": task_delivery_id,
|
||||
"submitted": False,
|
||||
"version": 0,
|
||||
"submitted_at": None,
|
||||
"cancelled_at": None,
|
||||
"created_by": actor_id,
|
||||
"updated_by": actor_id,
|
||||
"submitted_by": None,
|
||||
"cancelled_by": None,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
self._rows[(task_id, key)] = row
|
||||
return deepcopy(row)
|
||||
|
||||
if expected_version is not None and row.get("version", 0) != expected_version:
|
||||
raise SubmissionVersionConflict(deepcopy(row))
|
||||
|
||||
changed = (
|
||||
row.get("task_delivery_id") != task_delivery_id
|
||||
or any(row.get(name) != value for name, value in values.items())
|
||||
)
|
||||
if changed:
|
||||
now = _now()
|
||||
row.update(values)
|
||||
row.update(
|
||||
{
|
||||
"task_delivery_id": task_delivery_id,
|
||||
"updated_by": actor_id,
|
||||
"updated_at": now,
|
||||
"version": row.get("version", 0) + 1,
|
||||
}
|
||||
)
|
||||
return deepcopy(row)
|
||||
|
||||
async def cancel(
|
||||
self,
|
||||
task_id: str,
|
||||
|
||||
+80
@@ -88,6 +88,22 @@ def _requires_submission_update(
|
||||
)
|
||||
|
||||
|
||||
def _requires_registration_update(
|
||||
row: RoundtableArtifactSubmissionRow,
|
||||
artifact: dict[str, Any],
|
||||
task_delivery_id: str | None,
|
||||
) -> bool:
|
||||
"""Compare receipt metadata without treating ``submitted=False`` as a change."""
|
||||
return (
|
||||
row.task_delivery_id != task_delivery_id
|
||||
or row.session_id != artifact.get("session_id")
|
||||
or row.thread_id != artifact.get("thread_id")
|
||||
or row.node_key != artifact.get("node_key")
|
||||
or row.artifact_name != artifact.get("name")
|
||||
or row.mime_type != artifact.get("mime_type")
|
||||
)
|
||||
|
||||
|
||||
class RoundtableArtifactSubmissionRepository(RoundtableArtifactSubmissionStore):
|
||||
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
|
||||
self._sf = session_factory
|
||||
@@ -181,6 +197,70 @@ class RoundtableArtifactSubmissionRepository(RoundtableArtifactSubmissionStore):
|
||||
|
||||
raise RuntimeError("submit: retry loop exhausted (unexpected)")
|
||||
|
||||
async def register(
|
||||
self,
|
||||
task_id: str,
|
||||
artifact: dict[str, Any],
|
||||
*,
|
||||
task_delivery_id: str | None = None,
|
||||
actor_id: str | None = None,
|
||||
expected_version: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Persist an external process-report receipt before user submission."""
|
||||
key = submission_key(task_id, artifact)
|
||||
for _attempt in range(2):
|
||||
async with self._sf() as session:
|
||||
result = await session.execute(
|
||||
select(RoundtableArtifactSubmissionRow)
|
||||
.where(
|
||||
RoundtableArtifactSubmissionRow.task_id == task_id,
|
||||
RoundtableArtifactSubmissionRow.idempotency_key == key,
|
||||
)
|
||||
.with_for_update()
|
||||
)
|
||||
row = result.scalars().first()
|
||||
now = datetime.now(UTC)
|
||||
|
||||
if row is None:
|
||||
row = RoundtableArtifactSubmissionRow(
|
||||
id=uuid4().hex,
|
||||
task_id=task_id,
|
||||
idempotency_key=key,
|
||||
task_delivery_id=task_delivery_id,
|
||||
submitted=False,
|
||||
created_by=actor_id,
|
||||
updated_by=actor_id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
version=0,
|
||||
)
|
||||
_apply_artifact(row, artifact)
|
||||
session.add(row)
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
continue
|
||||
await session.refresh(row)
|
||||
return _to_dict(row)
|
||||
|
||||
if expected_version is not None and row.version != expected_version:
|
||||
raise SubmissionVersionConflict(_to_dict(row))
|
||||
|
||||
if not _requires_registration_update(row, artifact, task_delivery_id):
|
||||
return _to_dict(row)
|
||||
|
||||
_apply_artifact(row, artifact)
|
||||
row.task_delivery_id = task_delivery_id
|
||||
row.updated_by = actor_id
|
||||
row.updated_at = now
|
||||
row.version = row.version + 1
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
return _to_dict(row)
|
||||
|
||||
raise RuntimeError("register: retry loop exhausted (unexpected)")
|
||||
|
||||
async def cancel(
|
||||
self,
|
||||
task_id: str,
|
||||
|
||||
Reference in new issue
Block a user