feat: 支持会商 Markdown 产物提交到任务过程,并补充 Angular 对接文档

8BF/会商工作区可将 Markdown 产物一键提交到任务过程报告,后端记录提交状态以便跨用户与刷新后保持已提交;同时为 Angular 智能体对话对接文档补上前后端参考代码索引。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-18 20:08:51 +08:00
1 parent 33b0af333c
commit dcfaa199bf
19 files changed
+1133 -43

No files matched your search

@@ -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(
@@ -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,
@@ -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,
@@ -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,