代码提交
This commit is contained in:
1 parent
5e953c9fcb
commit
bf42a48fcc
16 files changed
+716
-1560
No files matched your search
@@ -0,0 +1,481 @@
|
||||
# 报告协作工作台 — 前端对接指南
|
||||
|
||||
> **适用版本:** RC-BE-000~018 已落地(2026-09-06)
|
||||
> **配套文档:**
|
||||
> - [前端实施计划](./agentscope-多智能体报告协作工作台-前端实施计划.md)
|
||||
> - [后端实施计划](./agentscope-多智能体报告协作工作台-后端实施计划.md)
|
||||
> - 模块 README:`frontend-web/src/report-collaboration/README.md`
|
||||
|
||||
本文说明**前端如何对接真实后端**:环境配置、API 契约、用户流程、状态机、错误处理与联调验收。前端 API 客户端与页面已实现,通常**无需再写一套 HTTP 层**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论速览
|
||||
|
||||
| 问题 | 答案 |
|
||||
|------|------|
|
||||
| 前端还要写接口吗? | **不用**。`src/report-collaboration/api/` 已封装 REST + SSE,页面通过 `getReportCollaborationDriver()` 调用。 |
|
||||
| 为什么调不通? | 最常见:`config.yaml` 里 `report_collaboration.enabled: false` → 全部 **503**;或 `worker_enabled: false` → run 不入队执行。 |
|
||||
| Mock 和真后端怎么切? | **Mock 已移除**。前端永远请求真实后端(`deerflow.rc-mock` / `VITE_RC_MOCK` 开关已删除);联调需 `enabled: true`。 |
|
||||
| 字段会和 MySQL 对不上吗? | 前端只认 JSON 契约(`api/types.ts`),不直连表。缺列表现为后端 500/SQL 错误,部署后需**重启 Gateway** 跑 `create_all` + 列同步。 |
|
||||
| 从哪进页面? | `/page/workspace/report-collaboration`(新会话)或 `/page/workspace/report-collaboration/:sessionId`。侧边栏入口默认注释,可直接 URL 访问。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构:前端已有什么
|
||||
|
||||
```
|
||||
ReportCollaborationPage
|
||||
└─ useReportCollaborationSession(唯一数据入口)
|
||||
├─ driver.getSnapshot() ← REST 冷启动
|
||||
├─ driver.openRunStream() ← SSE 实时事件
|
||||
├─ driver.sendMessage / createCommand / startRun …
|
||||
└─ event-reducer ← 事件 → UI 状态
|
||||
|
||||
ReportConversationPanel
|
||||
└─ MessageList(message-adapter 映射 CollaborationMessage → LangGraph Message)
|
||||
```
|
||||
|
||||
### 2.1 目录结构
|
||||
|
||||
```text
|
||||
frontend-web/src/report-collaboration/
|
||||
api/
|
||||
types.ts # 冻结契约(snake_case,与后端 Pydantic 一致)
|
||||
client.ts # rcFetch:鉴权、幂等键、乐观锁、错误映射
|
||||
sessions.ts # 会话 CRUD、消息
|
||||
plans.ts # 方案请求 / 列表 / 选定
|
||||
runs.ts # Run、SSE、命令、节点合同
|
||||
reports.ts # 报告版本、改写、恢复
|
||||
driver.ts # 真实后端统一门面(Mock 驱动已移除)
|
||||
state/
|
||||
event-normalizer.ts
|
||||
event-reducer.ts
|
||||
selectors.ts
|
||||
message-adapter.ts
|
||||
hooks/
|
||||
useReportCollaborationSession.ts # 快照 → SSE → reducer → 动作
|
||||
useReportCollaborationStream.ts # 断线重连、after_seq 续接
|
||||
useReportCollaborationMessages.ts
|
||||
components/ …
|
||||
pages/ReportCollaborationPage.tsx
|
||||
```
|
||||
|
||||
### 2.2 事实源原则
|
||||
|
||||
- **冷启动:** `GET /sessions/{id}` 返回 `SessionSnapshot`(session、messages、plans、run、nodes、agent_runs、commands、report_versions、last_seq)。
|
||||
- **运行中:** `GET /runs/{id}/stream?after_seq=N` 订阅 durable SSE。
|
||||
- **断线 / 序号缺口:** `GET /runs/{id}/events?after_seq=N` REST 补拉。
|
||||
- **澄清 / 规划阶段(尚无 run):** 每 2.5s 轮询快照(后端事件主要在 run 流上)。
|
||||
|
||||
纯展示状态(预览方案、聚焦成员、面板宽度)**不**写入服务器,仅存前端。
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端启用与部署
|
||||
|
||||
### 3.1 必改配置
|
||||
|
||||
文件:`offline-backend-20260512/backend/config.yaml`
|
||||
|
||||
```yaml
|
||||
report_collaboration:
|
||||
enabled: false # 联调必须改为 true
|
||||
worker_enabled: false # 要看成员执行必须 true
|
||||
planner_model: null # 建议填可用模型名
|
||||
coordinator_model: null
|
||||
research_model: null
|
||||
writer_model: null
|
||||
reviewer_model: null
|
||||
# … token_budget、max_concurrent_runs_per_user 等见同文件
|
||||
```
|
||||
|
||||
| 配置 | 现象 |
|
||||
|------|------|
|
||||
| `enabled: false` | 所有路由 **503**,`code: REPORT_COLLABORATION_DISABLED` |
|
||||
| `enabled: true`,`worker_enabled: false` | REST/SSE 可用,run 入队但不自动执行(测 API 可用,无成员流式输出) |
|
||||
| 两者 `true` + 模型配置完整 | 完整联调 |
|
||||
|
||||
### 3.2 MySQL / 表结构
|
||||
|
||||
生产环境 Alembic 自动升级默认关闭,启动时走 `create_all` + `_ensure_orm_columns_sync`。
|
||||
|
||||
- 迁移脚本(可选手动跑):`20260905_01`、`20260906_01`、`20260907_01`
|
||||
- ORM:`packages/harness/deerflow/persistence/report_collaboration/model.py`(13 张表)
|
||||
- **部署后务必重启 Gateway**,观察日志是否有 `Failed to auto-add column report_collaboration_*`
|
||||
|
||||
已有 RC-003 时代旧表会通过列同步补上 `template_snapshot_json`、`budget_snapshot_json`、`usage_json`、`budget_exhausted_reason` 等可空列,并创建 `rewrite_proposals`、`audits`。
|
||||
|
||||
### 3.3 前端环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `VITE_BACKEND_BASE_URL` | Gateway 地址,默认 `http://127.0.0.1:8001` |
|
||||
|
||||
请求经 `apiFetch`,自动携带登录 JWT(`credentials: "include"`)。**必须先登录 DeerFlow**。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 基址与请求约定
|
||||
|
||||
### 4.1 Base URL
|
||||
|
||||
```text
|
||||
{VITE_BACKEND_BASE_URL}/api/report-collaboration
|
||||
```
|
||||
|
||||
代码常量:`REPORT_COLLABORATION_BASE()`(`api/client.ts`)。
|
||||
|
||||
### 4.2 请求头
|
||||
|
||||
| Header | 何时携带 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `Authorization` | 全部 | 由 `apiFetch` 注入 |
|
||||
| `X-Idempotency-Key` | 写操作 | 格式 `rc-{uuid}`;重复提交返回首次结果 |
|
||||
| `X-Expected-Revision` | 选方案、开 run、改会话等 | 会话 `requirement_revision` 或方案 `revision` |
|
||||
| `Accept: text/event-stream` | SSE | `openRunStream` |
|
||||
|
||||
### 4.3 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "REPORT_COLLABORATION_DISABLED",
|
||||
"message": "报告协作工作台未启用",
|
||||
"detail": { }
|
||||
}
|
||||
```
|
||||
|
||||
前端映射(`api/client.ts`):
|
||||
|
||||
| HTTP | 异常类 | 典型场景 |
|
||||
|------|--------|----------|
|
||||
| 409 | `ConflictError` | revision 过期、active run 冲突 |
|
||||
| 422 | `ValidationError` | 参数/语义错误、缺幂等键 |
|
||||
| 429 | `RateLimitError` | 预算耗尽、每用户并发 run 上限 |
|
||||
| 5xx | `ServerError` | 可带同一幂等键重试 |
|
||||
| 0 | `NetworkError` | fetch 失败 |
|
||||
|
||||
### 4.4 完整路由清单(与前端 client 对齐)
|
||||
|
||||
| 方法 | 路径 | 前端模块 |
|
||||
|------|------|----------|
|
||||
| POST | `/sessions` | `sessions.createSession` |
|
||||
| GET | `/sessions` | `sessions.listSessions` |
|
||||
| GET | `/sessions/{id}` | `sessions.getSessionSnapshot` |
|
||||
| PATCH | `/sessions/{id}` | `sessions.updateSession` |
|
||||
| DELETE | `/sessions/{id}` | `sessions.deleteSession` |
|
||||
| GET | `/sessions/{id}/messages` | `sessions.listMessages` |
|
||||
| POST | `/sessions/{id}/messages` | `sessions.sendMessage` |
|
||||
| POST | `/sessions/{id}/plan-requests` | `plans.requestPlans` |
|
||||
| GET | `/sessions/{id}/plans` | `plans.listPlans` |
|
||||
| POST | `/sessions/{id}/plans/{planId}/select` | `plans.selectPlan` |
|
||||
| POST | `/sessions/{id}/runs` | `runs.startRun` |
|
||||
| GET | `/sessions/{id}/reports` | `reports.listReportVersions` |
|
||||
| POST | `/sessions/{id}/report-rewrites` | `reports.createRewrite` |
|
||||
| POST | `/sessions/{id}/report-rewrites/{id}/apply` | `reports.applyRewrite` |
|
||||
| POST | `/sessions/{id}/report-versions/{id}/restore` | `reports.restoreReportVersion` |
|
||||
| GET | `/runs/{id}` | `runs.getRun` |
|
||||
| GET | `/runs/{id}/events` | `runs.listRunEvents` |
|
||||
| GET | `/runs/{id}/stream` | `runs.openRunStream` |
|
||||
| POST | `/runs/{id}/cancel` | `runs.cancelRun` |
|
||||
| POST | `/runs/{id}/commands` | `runs.createCommand` |
|
||||
| POST | `/runs/{id}/commands/{id}/confirm` | `runs.confirmCommand` |
|
||||
| POST | `/runs/{id}/commands/{id}/cancel` | `runs.cancelCommand` |
|
||||
| GET | `/runs/{id}/nodes/{nodeId}/contract` | `runs.getNodeContract` |
|
||||
| GET | `/runs/{id}/artifacts` | (后端已暴露,前端按需接) |
|
||||
| GET | `/runs/{id}/sources` | (后端已暴露,前端按需接) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 用户流程与 API 时序
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant FE as 前端 RC
|
||||
participant API as Gateway
|
||||
participant W as Worker
|
||||
|
||||
U->>FE: 输入需求
|
||||
FE->>API: POST /sessions
|
||||
FE->>API: POST /sessions/{id}/messages
|
||||
API-->>FE: 澄清(快照 messages)
|
||||
U->>FE: 回答澄清
|
||||
FE->>API: POST /messages
|
||||
FE->>API: POST /plan-requests
|
||||
API-->>FE: 候选方案(快照 plans)
|
||||
U->>FE: 采用方案
|
||||
FE->>API: POST /plans/{id}/select
|
||||
U->>FE: 开始协作
|
||||
FE->>API: POST /runs
|
||||
FE->>API: GET /runs/{id}/stream
|
||||
W-->>API: 持久化事件
|
||||
API-->>FE: SSE
|
||||
U->>FE: 运行中干预
|
||||
FE->>API: POST /commands → confirm
|
||||
```
|
||||
|
||||
### 5.1 阶段 0:进入页面
|
||||
|
||||
| 路由 | 行为 |
|
||||
|------|------|
|
||||
| `/page/workspace/report-collaboration` | 空态;`POST /sessions` 后跳转带 `sessionId` |
|
||||
| `/page/workspace/report-collaboration/:sessionId` | 加载快照;有 `location.state.rcInitialMessage` 时自动发首条消息 |
|
||||
|
||||
路由注册:`frontend-web/src/pages/WorkspaceRoutes.tsx`。
|
||||
侧边栏入口已在 `workspace-nav-chat-list.tsx` 中放开(「报告协作」项),也可直接访问 URL。
|
||||
|
||||
### 5.2 阶段 1:需求澄清(无 run)
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/sessions
|
||||
POST /api/report-collaboration/sessions/{sessionId}/messages
|
||||
Content-Type: application/json
|
||||
|
||||
{ "text": "请写一份…", "target_node_id": null, "agent_run_id": null }
|
||||
```
|
||||
|
||||
- `useReportCollaborationSession.send()`:当 `session.status` 不是 `running` / `awaiting_input` / `reviewing` 时走 `sendMessage`。
|
||||
- 会话状态:`empty` → `clarifying` → `planning` → `proposal_ready`。
|
||||
- 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。
|
||||
|
||||
### 5.3 阶段 2:候选方案
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/sessions/{sessionId}/plan-requests
|
||||
POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select
|
||||
```
|
||||
|
||||
- **预览方案**(`previewPlanId`):纯前端,不调 API。
|
||||
- **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。
|
||||
- 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。
|
||||
- 右栏 `ReportFlowCanvas` 使用 `PlanNode` / `PlanEdge` 渲染 DAG。
|
||||
|
||||
### 5.4 阶段 3:开始执行与 SSE
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/sessions/{sessionId}/runs
|
||||
{ "plan_id": "plan-xxx" }
|
||||
|
||||
GET /api/report-collaboration/runs/{runId}/stream?after_seq=0
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
SSE 每条 `data:` 为 JSON:
|
||||
|
||||
```typescript
|
||||
interface CollaborationEvent {
|
||||
eventId: string;
|
||||
sessionId: string;
|
||||
runId?: string | null;
|
||||
seq: number;
|
||||
type: CollaborationEventType;
|
||||
timestamp: string;
|
||||
data: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
常用 `type`(完整列表见 `api/types.ts`):
|
||||
|
||||
| 类型 | 含义 |
|
||||
|------|------|
|
||||
| `message.created` / `message.delta` / `message.completed` | 对话镜像流式 |
|
||||
| `tool_call.*` / `tool_result.*` | 工具调用与返回 |
|
||||
| `team_message.*` | 成员 / 协调者广播 |
|
||||
| `plan.proposed` / `plan.selected` | 方案生命周期 |
|
||||
| `run.status.changed` | Run 状态 |
|
||||
| `node.status.changed` / `node.progress` | 节点执行 |
|
||||
| `command.confirmation.required` | 需用户确认的干预 |
|
||||
| `report.version.created` | 报告版本落盘 |
|
||||
| `heartbeat` | 保活 |
|
||||
|
||||
### 5.5 阶段 4:运行中干预
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/runs/{runId}/commands
|
||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/confirm
|
||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/cancel
|
||||
POST /api/report-collaboration/runs/{runId}/cancel
|
||||
```
|
||||
|
||||
- Run 活跃时 `send()` 自动走 `createCommand`(而非 `sendMessage`)。
|
||||
- 可从节点抽屉 / `AgentStageSelector` 传入 `target_node_id`、`agent_run_id`。
|
||||
- `InterventionCard` 展示 `confirmation_required` 命令;确认后调 `confirmCommand`。
|
||||
|
||||
`CommandIntent` 枚举(后端分类,前端展示 reason/impact):`answer_question`、`update_requirements`、`research_again`、`rewrite_section`、`rerun_node`、`replan`、`cancel` 等。
|
||||
|
||||
### 5.6 阶段 5:报告版本与改写
|
||||
|
||||
```http
|
||||
GET /api/report-collaboration/sessions/{sessionId}/reports
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites/{rewriteId}/apply
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-versions/{versionId}/restore
|
||||
```
|
||||
|
||||
- `createRewrite` 只生成候选,不直接覆盖正式版本。
|
||||
- `applyRewrite` / `restoreReportVersion` 产生新的 head 版本,历史保留。
|
||||
- `ReportPreviewDialog` 展示 `ReportVersion.markdown`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端状态机(接入方须知)
|
||||
|
||||
**不要**在页面里用零散 REST 字段拼 UI 状态。统一路径:
|
||||
|
||||
```text
|
||||
SessionSnapshot → applySessionSnapshot()
|
||||
CollaborationEvent[] → normalizeCollaborationEvent() → applyCollaborationEvents()
|
||||
→ selectors(derivePagePhase、selectPlanForCanvas、…)
|
||||
```
|
||||
|
||||
### 6.1 核心 Selector
|
||||
|
||||
| Selector | 用途 |
|
||||
|----------|------|
|
||||
| `derivePagePhase` | Composer 占位、方案卡、空态 |
|
||||
| `selectPlanForCanvas` | 画布 DAG(含预览方案) |
|
||||
| `selectAgentTabs` | 并行成员 Tab |
|
||||
| `selectNodeStatusMap` | 节点颜色 / 状态 |
|
||||
| `selectPendingConfirmationCommands` | 干预确认卡 |
|
||||
| `selectHeadReportVersion` | 报告预览入口 |
|
||||
| `selectStreamingAgentRunIds` | 流式成员指示 |
|
||||
|
||||
### 6.2 SSE 连接策略(`useReportCollaborationStream`)
|
||||
|
||||
- Run 非终态时连接;`completed` / `failed` / `cancelled` 后关流并 REST 最终补拉。
|
||||
- 断线:指数退避重连(1s~30s),`after_seq` 取 reducer 内 `lastSeq`。
|
||||
- `hasSeqGap`:暂停 live 应用,先 `listRunEvents` 回填。
|
||||
|
||||
### 6.3 乐观消息
|
||||
|
||||
用户发送时插入 `metadata.optimistic: true` 的 pending 消息;服务端镜像带相同 `idempotency_key` 时 reducer 原地替换。
|
||||
|
||||
---
|
||||
|
||||
## 7. 消息与 MessageList 复用
|
||||
|
||||
`useReportCollaborationMessages` 将 `CollaborationMessage[]` 转为 `@langchain/langgraph-sdk` 的 `Message[]`,并构造合成 `BaseStream` 供 `MessageList` 消费。
|
||||
|
||||
| 来源 | 渲染 |
|
||||
|------|------|
|
||||
| `human` / `ai` 文本 | 普通气泡 |
|
||||
| `tool_calls` + `tool` | 现有工具卡 |
|
||||
| `team_message` 事件 | 带 `name` 的成员发言 |
|
||||
| 业务卡 | `businessCardSlot`(方案组、命令确认、报告可用) |
|
||||
|
||||
分组键:`core/messages/utils.ts` 中 `assistant:task-report-import` 同级扩展 `assistant:business-card`,判据 `additional_kwargs.report_collaboration_card`。
|
||||
|
||||
### 7.1 在其他页面嵌入 RC(最小集)
|
||||
|
||||
1. `useReportCollaborationSession(sessionId)`
|
||||
2. `ThreadContext.Provider` + `MessageList` + `businessCardSlot`
|
||||
3. 引入 `report-collaboration/styles/report-collaboration.css`
|
||||
|
||||
---
|
||||
|
||||
## 8. 单测
|
||||
|
||||
Mock 驱动(原 `api/mock.ts`)与 `deerflow.rc-mock` / `VITE_RC_MOCK` 开关已移除,前端只请求真实后端;本地演示请直接联调后端。
|
||||
|
||||
单测:
|
||||
|
||||
```bash
|
||||
cd frontend-web
|
||||
pnpm test:report-collaboration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 联调验收清单
|
||||
|
||||
### 9.1 准备
|
||||
|
||||
- [ ] `report_collaboration.enabled: true`
|
||||
- [ ] `report_collaboration.worker_enabled: true`
|
||||
- [ ] 五个 `*_model` 已配置且可用
|
||||
- [ ] Gateway 已重启(MySQL 表/列就绪)
|
||||
- [ ] 前端已登录(JWT 有效)
|
||||
|
||||
### 9.2 探活
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer <token>" \
|
||||
http://127.0.0.1:8001/api/report-collaboration/sessions
|
||||
```
|
||||
|
||||
期望:**200** + `{ "sessions": [...] }`,而非 503。
|
||||
|
||||
### 9.3 页面流程(浏览器 Network)
|
||||
|
||||
1. 打开 `/#/page/workspace/report-collaboration`
|
||||
2. 输入需求 → `POST .../sessions` **200**
|
||||
3. 跳转 → `GET .../sessions/{id}` **200**,响应含 `SessionSnapshot`
|
||||
4. 首条消息 → `POST .../messages` **200**
|
||||
5. 请求方案 → `POST .../plan-requests` **200**
|
||||
6. 采用方案 → `POST .../plans/{id}/select` **200**
|
||||
7. 开始协作 → `POST .../runs` **200**
|
||||
8. `GET .../runs/{id}/stream` 持续收到 SSE(`message.delta`、`node.status.changed` 等)
|
||||
9. 刷新页面 → 消息与 `last_seq` 续接正确
|
||||
10. 运行中输入「重新检索」→ `POST .../commands` → 确认卡 → `confirm` **200**
|
||||
|
||||
### 9.4 常见失败
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| 全部 503 | `enabled: false` | 改配置并重启 |
|
||||
| run 一直 queued | `worker_enabled: false` | 改为 true |
|
||||
| 409 选方案 | revision 过期 | 刷新后重选(前端已自动 refresh) |
|
||||
| 429 | 预算/并发上限 | 调大配置或取消其他 run |
|
||||
| 500 + SQL | 表缺列 | 重启 Gateway,查列同步日志 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 契约与类型维护
|
||||
|
||||
- **单一事实源:** `frontend-web/src/report-collaboration/api/types.ts`
|
||||
- **后端镜像:** `offline-backend-20260512/backend/app/report_collaboration/contracts/`
|
||||
- 修改字段须同步两份实施计划与本指南,并跑前后端相关测试。
|
||||
|
||||
`SessionSnapshot` 形状(摘要):
|
||||
|
||||
```typescript
|
||||
interface SessionSnapshot {
|
||||
session: ReportSession;
|
||||
messages: CollaborationMessage[];
|
||||
plans: ReportPlanCandidate[];
|
||||
run: ReportRun | null;
|
||||
nodes: CollaborationNodeState[];
|
||||
agent_runs: AgentRun[];
|
||||
commands: CollaborationCommand[];
|
||||
report_versions: ReportVersion[];
|
||||
last_seq: number;
|
||||
}
|
||||
```
|
||||
|
||||
`ReportRun` 可选字段(RC-BE-016):`usage`、`budget_exhausted_reason`。
|
||||
|
||||
---
|
||||
|
||||
## 11. 后续工作(非对接阻塞)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| RC-BE-019 | 灰度发布与离线部署文档 |
|
||||
| 侧边栏入口 | 已放开(`workspace-nav-chat-list.tsx` 的「报告协作」项) |
|
||||
| iframe 嵌入 | RC 页尚未专门适配 `embed=1`,可参考现有深链模式扩展 |
|
||||
| 真人盲评 | RC-BE-018 为确定性评测集,上线前仍需计划 §11.4 人工评审 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 相关代码索引
|
||||
|
||||
| 用途 | 路径 |
|
||||
|------|------|
|
||||
| API 客户端 | `frontend-web/src/report-collaboration/api/` |
|
||||
| 会话 Hook | `frontend-web/src/report-collaboration/hooks/useReportCollaborationSession.ts` |
|
||||
| 页面 | `frontend-web/src/report-collaboration/pages/ReportCollaborationPage.tsx` |
|
||||
| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
|
||||
| 后端路由 | `offline-backend-20260512/backend/app/gateway/routers/report_collaboration.py` |
|
||||
| 配置 | `offline-backend-20260512/backend/config.yaml` → `report_collaboration` |
|
||||
| 持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/report_collaboration/` |
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { BookOpen, BotIcon, BrainIcon, FileSearch, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
|
||||
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
|
||||
|
||||
import { KnowledgeRagToggle } from "./knowledge-rag-toggle";
|
||||
import { SidebarCollapseButton } from "./workspace-header";
|
||||
@@ -100,13 +100,13 @@ export const WORKSPACE_COMPACT_NAV_ITEMS: WorkspaceCompactNavItem[] = [
|
||||
path: "/page/workspace/deep-research",
|
||||
match: (pathname) => pathname.startsWith("/page/workspace/deep-research"),
|
||||
},
|
||||
// {
|
||||
// id: "workspace-report-collaboration",
|
||||
// label: "报告协作",
|
||||
// icon: FileText,
|
||||
// path: "/page/workspace/report-collaboration",
|
||||
// match: (pathname) => pathname.startsWith("/page/workspace/report-collaboration"),
|
||||
// },
|
||||
{
|
||||
id: "workspace-report-collaboration",
|
||||
label: "报告协作",
|
||||
icon: FileText,
|
||||
path: "/page/workspace/report-collaboration",
|
||||
match: (pathname) => pathname.startsWith("/page/workspace/report-collaboration"),
|
||||
},
|
||||
// {
|
||||
// id: "workspace-enterprise-research",
|
||||
// label: "企业深度研究",
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
实现计划:`docs/agentscope-多智能体报告协作工作台-前端实施计划.md`
|
||||
(配套后端计划:`docs/agentscope-多智能体报告协作工作台-后端实施计划.md`)。
|
||||
|
||||
**进度(2026-09-05):** 本模块页面与契约已完成。后端 `RC-BE-000`~`011` 已通规划前 REST 与 durable SSE;成员自动执行 / 报告改写仍未就绪,联调见后端计划 §0。
|
||||
**联调对接:** `docs/agentscope-多智能体报告协作工作台-前端对接指南.md`(环境配置、API 时序、验收清单)。
|
||||
|
||||
**进度(2026-09-06):** 页面与契约已完成,默认对接真实后端(`GET/POST /api/report-collaboration/*`)。后端 `RC-BE-000`~`018` 已落地。联调见 `docs/agentscope-多智能体报告协作工作台-前端对接指南.md`。
|
||||
|
||||
## 结构
|
||||
|
||||
```text
|
||||
src/report-collaboration/
|
||||
api/ # 契约类型 + REST/SSE 客户端 + Mock 驱动
|
||||
api/ # 契约类型 + REST/SSE 客户端
|
||||
types.ts # 前后端共享领域类型(冻结契约,字段与后端 Pydantic 一致)
|
||||
client.ts # rcFetch:鉴权、X-Idempotency-Key、X-Expected-Revision、409/422/429/5xx 错误映射
|
||||
sessions.ts / plans.ts / runs.ts / reports.ts # 端点函数
|
||||
driver.ts # 真实 / Mock 统一门面(getReportCollaborationDriver)
|
||||
mock.ts # 本地演示驱动:澄清→3 方案→并行执行→报告版本,全走同一事件协议
|
||||
driver.ts # 真实后端统一门面(getReportCollaborationDriver;Mock 驱动已移除)
|
||||
state/
|
||||
event-normalizer.ts # SSE/REST 事件防御性归一化
|
||||
event-reducer.ts # durable 事件 reducer(seq 去重、乱序缓冲、节点状态机守卫、乐观消息替换)
|
||||
@@ -50,20 +51,18 @@ src/report-collaboration/
|
||||
- 服务器快照 + durable SSE 是唯一事实源;`previewPlanId` / 聚焦成员 / 面板宽度均为纯展示状态。
|
||||
- 节点抽屉只读合同;重跑/纠偏经对话命令(`targetNodeId`/`agentRunId`)。
|
||||
|
||||
## Mock 演示(执行内核未就绪时)
|
||||
## 只走真实后端
|
||||
|
||||
规划前 REST 可走真实后端(需打开 `report_collaboration.enabled`)。成员自动执行、报告改写仍用 mock:
|
||||
Mock 驱动(`mock.ts`)与切换开关(`deerflow.rc-mock` / `VITE_RC_MOCK`)已移除,
|
||||
页面永远请求真实后端。历史遗留的浏览器设置可执行
|
||||
`localStorage.removeItem('deerflow.rc-mock')` 清掉(不再被读取)。日常联调请保证:
|
||||
|
||||
- `localStorage.setItem('deerflow.rc-mock','1')` 后刷新;
|
||||
- 环境变量 `VITE_RC_MOCK=1`。
|
||||
|
||||
Mock 走与真实后端相同的驱动接口与事件协议(seq 单调、可重放),可完整演示:
|
||||
输入需求 → 澄清 → 回答 → 3 个候选方案 → 预览/采用 → 开始协作 → 并行成员流式输出 +
|
||||
工具调用 → 命令确认卡(输入「重新检索…」)→ 报告版本 → 预览/下载/恢复。
|
||||
- `config.yaml` → `report_collaboration.enabled: true`
|
||||
- 需要成员自动执行时再开 `worker_enabled: true`
|
||||
|
||||
## 后端契约冻结说明
|
||||
|
||||
`api/types.ts` 按后端计划 §5/§7 编写,已与 `app/report_collaboration/contracts/` 对齐。
|
||||
快照端点约定 `GET /sessions/{id}` 返回 `SessionSnapshot`
|
||||
(session/messages/plans/run/nodes/agent_runs/commands/report_versions/last_seq)。
|
||||
`GET /runs/{id}/stream` 已按 `after_seq` / `Last-Event-ID` 重放 durable 事件(`RC-BE-011`)。queued run 的领取/租约恢复已落地(`RC-BE-012`),质量角色内核已落地(`RC-BE-013`)。默认 `worker_enabled=false` 仍不挂后台循环。报告改写 / apply / restore 已由 `RC-BE-015` 挂载。后端 20 项质量评测集见 `RC-BE-018`(确定性打分,不替代上线前真人盲评)。
|
||||
`GET /runs/{id}/stream` 已按 `after_seq` / `Last-Event-ID` 重放 durable 事件(`RC-BE-011`)。queued run 的领取/租约恢复已落地(`RC-BE-012`),质量角色内核已落地(`RC-BE-013`)。报告改写 / apply / restore 已由 `RC-BE-015` 挂载。后端 20 项质量评测集见 `RC-BE-018`(确定性打分,不替代上线前真人盲评)。
|
||||
@@ -1,18 +1,11 @@
|
||||
/**
|
||||
* 驱动接口:真实后端 / 本地 Mock 的统一门面。
|
||||
*
|
||||
* Mock 开关(任一为真即 Mock,便于后端未就绪时演示与联调自测):
|
||||
* - 环境变量 `VITE_RC_MOCK=1`;
|
||||
* - localStorage `deerflow.rc-mock` = `"1"`(浏览器里临时打开:
|
||||
* `localStorage.setItem('deerflow.rc-mock','1')` 后刷新)。
|
||||
* 驱动接口:直接请求真实后端(不再提供 Mock 驱动)。
|
||||
*/
|
||||
|
||||
import * as plansApi from "@/report-collaboration/api/plans";
|
||||
import * as reportsApi from "@/report-collaboration/api/reports";
|
||||
import * as runsApi from "@/report-collaboration/api/runs";
|
||||
import * as sessionsApi from "@/report-collaboration/api/sessions";
|
||||
import { mockDriver } from "@/report-collaboration/api/mock";
|
||||
import { ApiError } from "@/report-collaboration/api/client";
|
||||
import type {
|
||||
CollaborationCommand,
|
||||
CollaborationEvent,
|
||||
@@ -92,7 +85,7 @@ export interface ReportCollaborationDriver {
|
||||
restoreReportVersion(sessionId: string, versionId: string): Promise<ReportVersion>;
|
||||
}
|
||||
|
||||
const realDriver: ReportCollaborationDriver = {
|
||||
export const realDriver: ReportCollaborationDriver = {
|
||||
listSessions: sessionsApi.listSessions,
|
||||
getSnapshot: sessionsApi.getSessionSnapshot,
|
||||
createSession: (title) => sessionsApi.createSession(title ? { title } : {}),
|
||||
@@ -119,113 +112,6 @@ const realDriver: ReportCollaborationDriver = {
|
||||
restoreReportVersion: reportsApi.restoreReportVersion,
|
||||
};
|
||||
|
||||
let cachedDriver: ReportCollaborationDriver | null = null;
|
||||
/** DEV 下探测到后端无 /api/report-collaboration(404/网络失败/非 JSON)后自动切 Mock。 */
|
||||
let devFallbackToMock = false;
|
||||
|
||||
export function isMockEnabled(): boolean {
|
||||
try {
|
||||
if (import.meta.env.VITE_RC_MOCK === "1") return true;
|
||||
if (typeof window !== "undefined") {
|
||||
return window.localStorage.getItem("deerflow.rc-mock") === "1";
|
||||
}
|
||||
} catch {
|
||||
// localStorage 不可用时按真实后端处理。
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 显式关闭(localStorage "0"),优先级高于 DEV 自动回退。 */
|
||||
function isMockExplicitlyDisabled(): boolean {
|
||||
try {
|
||||
return typeof window !== "undefined" && window.localStorage.getItem("deerflow.rc-mock") === "0";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端尚未实现该路由的特征:404/405/501、网络失败、或 SPA fallback 返回的非 JSON。 */
|
||||
function isFeatureMissingError(err: unknown): boolean {
|
||||
if (err instanceof ApiError) {
|
||||
// 405:网关对未知 POST 路由的响应形态;404/501:路由未注册。
|
||||
return err.status === 404 || err.status === 405 || err.status === 501 || err.status === 0;
|
||||
}
|
||||
// rcRequest 对 200+HTML(dev server SPA fallback)会抛 JSON.parse 的 SyntaxError。
|
||||
return err instanceof SyntaxError;
|
||||
}
|
||||
|
||||
async function withDevFallback<T>(op: () => Promise<T>, retry: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await op();
|
||||
} catch (err) {
|
||||
if (!canDevFallback(err)) throw err;
|
||||
enableMockFallback();
|
||||
return retry();
|
||||
}
|
||||
}
|
||||
|
||||
function canDevFallback(err: unknown): boolean {
|
||||
return (
|
||||
import.meta.env.DEV &&
|
||||
!devFallbackToMock &&
|
||||
!isMockExplicitlyDisabled() &&
|
||||
isFeatureMissingError(err)
|
||||
);
|
||||
}
|
||||
|
||||
function enableMockFallback(): void {
|
||||
devFallbackToMock = true;
|
||||
console.info(
|
||||
"[report-collaboration] 后端 /api/report-collaboration 尚不可用,开发模式自动切换到本地 Mock 演示驱动(localStorage deerflow.rc-mock=0 可强制关闭)。",
|
||||
);
|
||||
}
|
||||
|
||||
/** DEV 自动回退驱动:命中回退标志后所有方法直达 Mock;否则先走真实后端。 */
|
||||
const devFallbackEntries: Partial<ReportCollaborationDriver> = {
|
||||
async listSessions(signal) {
|
||||
if (devFallbackToMock) return mockDriver.listSessions(signal);
|
||||
return withDevFallback(
|
||||
() => realDriver.listSessions(signal),
|
||||
() => mockDriver.listSessions(signal),
|
||||
);
|
||||
},
|
||||
async getSnapshot(sessionId, signal) {
|
||||
if (devFallbackToMock) return mockDriver.getSnapshot(sessionId, signal);
|
||||
const snapshot = await realDriver.getSnapshot(sessionId, signal);
|
||||
// 真实后端 404 且 DEV 未定态时,可能是功能未部署:探测一次并转 Mock。
|
||||
if (
|
||||
snapshot === null &&
|
||||
import.meta.env.DEV &&
|
||||
!isMockExplicitlyDisabled() &&
|
||||
!devFallbackToMock
|
||||
) {
|
||||
enableMockFallback();
|
||||
return mockDriver.getSnapshot(sessionId, signal);
|
||||
}
|
||||
return snapshot;
|
||||
},
|
||||
async createSession(title) {
|
||||
if (devFallbackToMock) return mockDriver.createSession(title);
|
||||
return withDevFallback(
|
||||
() => realDriver.createSession(title),
|
||||
() => mockDriver.createSession(title),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const devFallbackDriver: ReportCollaborationDriver = new Proxy(devFallbackEntries, {
|
||||
get(target, prop, receiver) {
|
||||
if (Reflect.has(target, prop)) {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
const active = devFallbackToMock ? mockDriver : realDriver;
|
||||
const value = Reflect.get(active, prop);
|
||||
return typeof value === "function" ? value.bind(active) : value;
|
||||
},
|
||||
}) as ReportCollaborationDriver;
|
||||
|
||||
export function getReportCollaborationDriver(): ReportCollaborationDriver {
|
||||
if (cachedDriver) return cachedDriver;
|
||||
cachedDriver = isMockEnabled() ? mockDriver : devFallbackDriver;
|
||||
return cachedDriver;
|
||||
return realDriver;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,7 @@ export function SessionHistoryPanel({ activeSessionId }: { activeSessionId: stri
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
|
||||
const reload = useCallback(async (quiet = false) => {
|
||||
if (!quiet) setLoading(true);
|
||||
@@ -82,20 +83,70 @@ export function SessionHistoryPanel({ activeSessionId }: { activeSessionId: stri
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllSessions = async () => {
|
||||
const list = sessions ?? [];
|
||||
if (list.length === 0 || clearing) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`将删除全部 ${list.length} 个历史会话(含消息、方案与报告版本),且不可恢复。确定清空?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setClearing(true);
|
||||
try {
|
||||
// 个别会话可能因运行中的协作删除失败(409 等),用 allSettled 尽量多删。
|
||||
const results = await Promise.allSettled(
|
||||
list.map((session) => getReportCollaborationDriver().deleteSession(session.id)),
|
||||
);
|
||||
const failedCount = results.filter((result) => result.status === "rejected").length;
|
||||
const activeDeleted = list.some(
|
||||
(session, index) => results[index].status === "fulfilled" && session.id === activeSessionId,
|
||||
);
|
||||
await reload(true);
|
||||
if (failedCount > 0) {
|
||||
setError(`已删除 ${list.length - failedCount} 个会话,${failedCount} 个删除失败(可能存在运行中的协作),可稍后重试。`);
|
||||
} else {
|
||||
setError(null);
|
||||
}
|
||||
if (activeDeleted) {
|
||||
navigate("/page/workspace/report-collaboration");
|
||||
}
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-[12px] text-[var(--rc-text-secondary)]">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2">
|
||||
<span className="shrink-0 text-[12px] text-[var(--rc-text-secondary)]">
|
||||
{loading ? "加载中…" : `${sessions?.length ?? 0} 个会话`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rc-btn"
|
||||
onClick={() => navigate("/page/workspace/report-collaboration")}
|
||||
>
|
||||
<MessageSquarePlus className="size-3.5" aria-hidden />
|
||||
新建会话
|
||||
</button>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="rc-btn"
|
||||
disabled={clearing || !sessions?.length}
|
||||
title={sessions?.length ? "删除全部历史会话" : "暂无历史会话"}
|
||||
onClick={() => void clearAllSessions()}
|
||||
>
|
||||
{clearing ? (
|
||||
<Loader2 className="rc-spin size-3.5" aria-hidden />
|
||||
) : (
|
||||
<Trash2 className="size-3.5" aria-hidden />
|
||||
)}
|
||||
清空历史
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rc-btn"
|
||||
onClick={() => navigate("/page/workspace/report-collaboration")}
|
||||
>
|
||||
<MessageSquarePlus className="size-3.5" aria-hidden />
|
||||
新建会话
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="px-3 pb-1 text-[11px]" style={{ color: "var(--rc-danger)" }}>
|
||||
|
||||
@@ -288,6 +288,9 @@ export function useReportCollaborationSession(sessionId: string): UseReportColla
|
||||
idempotencyKey,
|
||||
);
|
||||
}
|
||||
// POST /messages 只回用户消息;协调者澄清/就绪回复写在同一次请求里,
|
||||
// 必须立刻拉快照,否则本地 status 仍是 empty,轮询不会启动、AI 气泡不出现。
|
||||
await loadSnapshot(true);
|
||||
} catch (err) {
|
||||
// 失败保留乐观消息与输入,提供重试入口(不回滚 UI)。
|
||||
setActionError(err instanceof Error ? err.message : String(err));
|
||||
@@ -295,17 +298,18 @@ export function useReportCollaborationSession(sessionId: string): UseReportColla
|
||||
setSendBusy(false);
|
||||
}
|
||||
},
|
||||
[driver, sessionId, state.run],
|
||||
[driver, loadSnapshot, sessionId, state.run, state.session?.status],
|
||||
);
|
||||
|
||||
const requestPlans = useCallback(async () => {
|
||||
setActionError(null);
|
||||
try {
|
||||
await driver.requestPlans(sessionId, state.session?.requirement_revision ?? null);
|
||||
await loadSnapshot(true);
|
||||
} catch (err) {
|
||||
setActionError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}, [driver, sessionId, state.session]);
|
||||
}, [driver, loadSnapshot, sessionId, state.session]);
|
||||
|
||||
const selectPlan = useCallback(
|
||||
async (planId: string) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { toast } from "sonner";
|
||||
import { FileText, Sparkles } from "lucide-react";
|
||||
import { FileText, Loader2, Sparkles } from "lucide-react";
|
||||
|
||||
import { ArtifactsProvider } from "@/components/workspace/artifacts";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
@@ -341,6 +341,35 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
|
||||
[sessionId, session],
|
||||
);
|
||||
|
||||
// 澄清完成(协调者已写入需求版本)但还没生成过方案:给出显式「发起规划」入口。
|
||||
const requirementReady = useMemo(
|
||||
() =>
|
||||
Object.values(state.messagesById).some(
|
||||
(message) =>
|
||||
message.role === "ai" &&
|
||||
(message.metadata as Record<string, unknown> | null | undefined)?.ready_to_plan === true,
|
||||
),
|
||||
[state.messagesById],
|
||||
);
|
||||
const showPlanRequestEntry =
|
||||
state.session?.status === "clarifying" && state.plans.length === 0 && requirementReady;
|
||||
const [planRequestBusy, setPlanRequestBusy] = useState(false);
|
||||
const handleRequestPlans = useCallback(async () => {
|
||||
setPlanRequestBusy(true);
|
||||
try {
|
||||
await getReportCollaborationDriver().requestPlans(
|
||||
sessionId,
|
||||
state.session?.requirement_revision ?? null,
|
||||
);
|
||||
await session.refreshSnapshot();
|
||||
toast.success("已生成候选方案,可在对话中选择");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setPlanRequestBusy(false);
|
||||
}
|
||||
}, [sessionId, session, state.session?.requirement_revision]);
|
||||
|
||||
const businessCardSlot = useCallback(
|
||||
(message: Message) => {
|
||||
const card = readReportCollaborationCard(message);
|
||||
@@ -536,19 +565,43 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
|
||||
: undefined
|
||||
}
|
||||
composer={
|
||||
<ReportComposer
|
||||
phase={phase}
|
||||
busy={session.sendBusy}
|
||||
target={composerTarget}
|
||||
onClearTarget={() => setComposerTarget(null)}
|
||||
onSend={(text) =>
|
||||
void session.send(text, {
|
||||
nodeId: composerTarget?.nodeId ?? null,
|
||||
agentRunId: composerTarget?.agentRunId ?? null,
|
||||
})
|
||||
}
|
||||
focusSignal={focusSignal}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
{showPlanRequestEntry ? (
|
||||
<div className="mb-1.5 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="rc-btn"
|
||||
style={{
|
||||
background: "var(--rc-accent)",
|
||||
color: "#fff",
|
||||
borderColor: "var(--rc-accent)",
|
||||
}}
|
||||
disabled={planRequestBusy}
|
||||
onClick={() => void handleRequestPlans()}
|
||||
>
|
||||
{planRequestBusy ? (
|
||||
<Loader2 className="rc-spin size-3.5" aria-hidden />
|
||||
) : (
|
||||
<Sparkles className="size-3.5" aria-hidden />
|
||||
)}
|
||||
{planRequestBusy ? "正在生成候选方案…" : "需求已就绪,生成候选方案"}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<ReportComposer
|
||||
phase={phase}
|
||||
busy={session.sendBusy}
|
||||
target={composerTarget}
|
||||
onClearTarget={() => setComposerTarget(null)}
|
||||
onSend={(text) =>
|
||||
void session.send(text, {
|
||||
nodeId: composerTarget?.nodeId ?? null,
|
||||
agentRunId: composerTarget?.agentRunId ?? null,
|
||||
})
|
||||
}
|
||||
focusSignal={focusSignal}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
-1
@@ -176,7 +176,6 @@ class AgentScopeMemberKernel:
|
||||
model_snapshot,
|
||||
app_config=self._app_config,
|
||||
node_run_id=assignment.node_run_id,
|
||||
budget_guard=budget,
|
||||
)
|
||||
)
|
||||
permission = context.get("permission")
|
||||
|
||||
@@ -723,8 +723,8 @@ workflows:
|
||||
|
||||
# AgentScope 多智能体报告协作工作台(默认关闭;不改变现有会商 / Workflow Studio)
|
||||
report_collaboration:
|
||||
enabled: false
|
||||
worker_enabled: false
|
||||
enabled: true
|
||||
worker_enabled: true
|
||||
planner_model: null
|
||||
coordinator_model: null
|
||||
research_model: null
|
||||
|
||||
@@ -39,7 +39,7 @@ allowed-tools:
|
||||
```
|
||||
|
||||
- `taskId` / `actionId` 必须是整数,等于当前任务与行动 ID。
|
||||
- `userList` 每项是完整 user 对象:`id`、`name` 必填;`platformType` 写入 accountPlat;`accountSource` 优先取 `dataSource`,缺失时回退 `score`;整个对象 JSON 序列化后存入 `frofileInfo`。
|
||||
- `userList` 每项是完整 user 对象:`id`、`name` 必填;`platformType` 写入 accountPlat——缺失时可只给 `platform` 平台名(如 `facebook`、`instagram`),脚本登录后自动查平台字典表(sys-dict-data,dictType=platform)按前端 platformTypeChange 同款规则映射为编码(未识别回退 1,两者都缺则置空);`accountSource` 优先取 `dataSource`,缺失时回退 `score`;整个对象 JSON 序列化后存入 `frofileInfo`。
|
||||
- `recReason` 中的 `<n>` 引用标记会被自动清洗,无需预处理。
|
||||
|
||||
完整字段定义、账号对象字段表与生成规则,见:
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
"endpoints": {
|
||||
"login": "/api/auth/login",
|
||||
"platform_dict": "/api/sys/sys-dict-data",
|
||||
"design_list": "/api/task/task-action-design",
|
||||
"design_create": "/api/task/task-action-design",
|
||||
"design_delete": "/api/task/task-action-design",
|
||||
|
||||
+67
-1
@@ -86,6 +86,48 @@ def clean_reason(text) -> str:
|
||||
return text.replace("、", "").replace("引用", "").replace("()", "")
|
||||
|
||||
|
||||
def platform_type_change(platform, options):
|
||||
"""
|
||||
平台名/别名 → 平台字典编码(写入 task-action-design-account.accountPlat)。
|
||||
移植自前端 chat-data.utils.platformTypeChange:子串匹配 fb / x|witter / in / re / th → 字典 label
|
||||
命中取 value;否则 label 全量小写精确匹配;仍匹配不到回退 1(facebook,与前端一致)。
|
||||
options 形如 [{"label": "facebook", "value": 1}, ...],来自 sys-dict-data 的 platform 字典。
|
||||
技能侧扩展:入参已是编码(int 或纯数字字符串)时原样透传;完全缺失(None/空)返回 None(不臆造)。
|
||||
"""
|
||||
if platform is None or (isinstance(platform, str) and not platform.strip()):
|
||||
return None
|
||||
if isinstance(platform, bool):
|
||||
return None
|
||||
if isinstance(platform, int):
|
||||
return platform
|
||||
if isinstance(platform, str) and platform.strip().isdigit():
|
||||
return int(platform.strip())
|
||||
|
||||
low = str(platform).strip().lower()
|
||||
|
||||
def val(*labels):
|
||||
for lab in labels:
|
||||
for o in options:
|
||||
if str(o.get("label", "")).strip().lower() == lab and o.get("value") is not None:
|
||||
return o.get("value")
|
||||
return None
|
||||
|
||||
if "fb" in low:
|
||||
return val("facebook") or 1
|
||||
if "x" in low or "witter" in low:
|
||||
return val("x(twitter)", "x") or 1
|
||||
if "in" in low:
|
||||
return val("instagram") or 1
|
||||
if "re" in low:
|
||||
return val("reddit") or 1
|
||||
if "th" in low:
|
||||
return val("threads") or 1
|
||||
for o in options:
|
||||
if str(o.get("label", "")).strip().lower() == low and o.get("value") is not None:
|
||||
return o.get("value")
|
||||
return 1
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, cfg: dict, dry_run: bool = False):
|
||||
self.base = cfg["base_url"].rstrip("/")
|
||||
@@ -239,6 +281,24 @@ def main():
|
||||
client.token = token
|
||||
result["登录"] = f"成功(已获取 {cfg['auth']['token_field']},后续请求头 {cfg['auth']['header_name']}: {cfg['auth']['header_template']})"
|
||||
|
||||
# ---- 1.5 平台字典(platform 名称/别名 → 字典编码,写入 accountPlat 前映射;与前端 platformTypeChange 同款) ----
|
||||
platform_options = []
|
||||
dict_status, dict_resp = client.request("查询平台字典", "GET", ep(cfg, "platform_dict"),
|
||||
query={"filter": ["dictType||$eq||platform"], "limit": 99999})
|
||||
dict_rows = (dict_resp or {}).get("data") or [] if isinstance(dict_resp, dict) else []
|
||||
if dict_status == 200:
|
||||
platform_options = [
|
||||
{"label": r.get("dictLabel"), "value": int(str(r["dictValue"]).strip())}
|
||||
for r in dict_rows
|
||||
if r.get("dictValue") is not None and str(r.get("dictValue")).strip().lstrip("-").isdigit()
|
||||
]
|
||||
result["平台字典"] = (
|
||||
f"已加载 {len(platform_options)} 项:"
|
||||
+ "、".join(f"{o['label']}={o['value']}" for o in platform_options)
|
||||
)
|
||||
else:
|
||||
result["平台字典"] = f"查询失败(HTTP {dict_status}),平台名只能按子串规则映射且未命中回退 1,建议检查字典表"
|
||||
|
||||
# ---- 2. 查询已有受众(查重) ----
|
||||
q = {"filter": [f"taskId||$eq||{task_id}", f"actionId||$eq||{action_id}"], "limit": 99999}
|
||||
status, designs_resp = client.request("查询已有受众", "GET", ep(cfg, "design_list"), query=q)
|
||||
@@ -298,11 +358,17 @@ def main():
|
||||
# ---- 5. 创建账号关联 ----
|
||||
for user in g.get("userList") or []:
|
||||
account_source = user.get("dataSource") if user.get("dataSource") is not None else user.get("score")
|
||||
# 平台编码:user.platformType(编码)缺失时回退 user.platform(名称),统一经平台字典映射为编码
|
||||
raw_platform = user.get("platformType") if user.get("platformType") is not None else user.get("platform")
|
||||
account_plat = platform_type_change(raw_platform, platform_options)
|
||||
group.setdefault("账号平台映射", []).append(
|
||||
{"accountName": user.get("name"), "原始值": raw_platform, "accountPlat": account_plat}
|
||||
)
|
||||
account_body = {
|
||||
"taskId": task_id, "actionId": action_id,
|
||||
"accountId": user.get("id"),
|
||||
"accountName": user.get("name"),
|
||||
"accountPlat": user.get("platformType"),
|
||||
"accountPlat": account_plat,
|
||||
"accountSource": account_source,
|
||||
"frofileInfo": json.dumps(user, ensure_ascii=False),
|
||||
"designId": design_id,
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@ FRONTEND_PORT="${FRONTEND_PORT:-5174}"
|
||||
|
||||
detect_host_ip() {
|
||||
if command -v hostname >/dev/null 2>&1; then
|
||||
hostname -I 2>/dev/null | awk '{print $1}'
|
||||
# `hostname -I` is unsupported on Windows Git Bash; `|| true` keeps the
|
||||
# pipefail-safe fallback (HOST_IP then defaults to 127.0.0.1 below).
|
||||
hostname -I 2>/dev/null | awk '{print $1}' || true
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ if [[ "${BACKEND_INSTALL_DEPS}" != "0" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
setsid bash -c '
|
||||
# nohup instead of setsid: setsid does not exist on Windows Git Bash.
|
||||
nohup bash -c '
|
||||
cd "$0" || exit 1
|
||||
exec env \
|
||||
GATEWAY_CORS_ORIGINS="$1" \
|
||||
|
||||
@@ -17,7 +17,8 @@ rm -f "${PID_FILE}"
|
||||
echo "Starting frontend..."
|
||||
echo "Log: ${LOG_FILE}"
|
||||
|
||||
setsid bash -c '
|
||||
# nohup instead of setsid: setsid does not exist on Windows Git Bash.
|
||||
nohup bash -c '
|
||||
cd "$0" || exit 1
|
||||
exec env \
|
||||
VITE_BACKEND_BASE_URL="$1" \
|
||||
|
||||
Reference in new issue
Block a user