wujie应用联调

This commit is contained in:
董宏凯 committed 2026-06-11 17:11:28 +08:00
1 parent 0784f30e23
commit f3ca996116
35 files changed
+1590 -334

No files matched your search

@@ -0,0 +1,250 @@
# 圆桌规划抽屉 · wujie 嵌入使用说明
把 DeerFlow 的「圆桌规划」页用无界(wujie)嵌进本 Vue3 工程的一个右侧抽屉里:宿主传入
任务信息与业务链条,抽屉打开即自动开跑,第二步达成共识后把各智能体交付结果回传给宿主。
- 抽屉组件:`src/roundtable-embed/RoundtableDrawer.vue`
- 演示入口:`src/App.vue` 顶部页签「圆桌规划抽屉(wujie)」
- 子应用(DeerFlow)侧入口:`/#/embed/roundtable`(只渲染圆桌主体、无侧边栏、用户名登录)
---
## 1. 快速开始
### 1.1 安装 wujie 依赖(内网离线)
见 [`offline-deps/wujie/README.md`](../offline-deps/wujie/README.md)。一键:
```powershell
./offline-deps/wujie/install-offline.ps1 # Windows
```
```bash
bash ./offline-deps/wujie/install-offline.sh # Linux/macOS
```
### 1.2 启动联调
1. 启 DeerFlow 前端(被嵌的子应用):`cd frontend-web && pnpm dev`(默认端口 5174)。
2. 启本工程:`pnpm dev`,打开后切到「圆桌规划抽屉(wujie)」页签。
3. 填写:**DeerFlow 前端站点源**(如 `http://localhost:5174`)、用户名、密码/口令、任务名称/描述、
业务链条 chainId、任务 id、主题(默认深蓝)→ 点「打开圆桌规划抽屉」。
---
## 2. 组件用法
```vue
<script setup lang="ts">
import { ref } from "vue";
import { RoundtableDrawer } from "@/roundtable-embed";
import type { RoundtableConsensusPayload } from "@/roundtable-embed";
const open = ref(false);
function onConsensus(p: RoundtableConsensusPayload) {
// 第二步达成共识 → 拿到各智能体交付
console.log(p.consensusText, p.deliverables);
}
</script>
<template>
<RoundtableDrawer
v-model:open="open"
base-url="http://localhost:5174"
username="zhangsan"
password="123ewq"
task-title="XX 产品出海方案规划"
task-goal="背景/目标/约束……"
chain-id="chain_xxx"
task-id="task-001"
theme="dark"
@consensus="onConsensus"
/>
</template>
```
### Props
| Prop | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `open` (`v-model:open`) | `boolean` | ✓ | 抽屉开关 |
| `baseUrl` | `string` | ✓ | **DeerFlow 前端站点源**(如 `http://localhost:5174`),不是后端地址 |
| `username` | `string` | | 登录用户名(决定登录哪个账号) |
| `password` | `string` | | 用户名+密码登录的**共享口令**;后端开启 `username_login.require_password` 时必填 |
| `taskTitle` | `string` | | 任务名称(注入子应用第一步首条消息) |
| `taskGoal` | `string` | | 任务描述(注入子应用第一步首条消息) |
| `chainId` | `string` | | 指定业务链条 id,子应用据此自动选链进第二步 |
| `taskId` | `string` | | 任务 id,聊天记录按此存取;该任务有记录则打开时回显最新一条 |
| `theme` | `string` | | 主题色,默认 `dark`(深蓝);传 `light` 走浅色 |
| `degrade` | `boolean \| 'auto'` | | 无界 iframe 降级,默认 `auto`:跨域时自动降级(**Nginx 不能配 CORS 时靠这个**) |
### 事件
| 事件 | 载荷 | 说明 |
|------|------|------|
| `update:open` | `boolean` | 抽屉关闭时触发 |
| `consensus` | `RoundtableConsensusPayload` | 第二步达成共识时触发,携带各智能体交付 |
`RoundtableConsensusPayload`:
```ts
interface RoundtableConsensusPayload {
taskTitle: string | null;
chainId: string | null;
chainTitle: string | null;
coordinatorName: string | null; // 总控名
consensusText: string; // 总控最终共识正文
deliverables: Array<{ // 各智能体交付(剔除总控)
agentName: string;
content: string;
threadId: string | null;
}>;
}
```
---
## 3. 父 ↔ 子 通信契约
底层是无界事件总线 `bus`(`wujie`),抽屉组件已封装好,宿主一般无需直接接触。
- **父 → 子(props 注入)**:`window.$wujie.props = { taskTitle, taskGoal, chainId, taskId, username, theme }`。
子应用首帧从 `readWujieProps()` 读取(DeerFlow `core/embed/wujie.ts`)。
- **子 → 父(共识回传)**:子应用 `bus.$emit('roundtable:consensus', payload)`,抽屉 `bus.$on` 监听后
转成 `@consensus` 事件给宿主。
- **父 → 子(重置,保活复用时用)**:`bus.$emit('roundtable:reset')` → 子应用回到第一步。
本抽屉默认 `alive=false`(关闭即卸载、下次打开是干净新实例),通常用不到。
---
## 4. 行为说明
### 4.1 自动化流程
1. 抽屉打开 → 子应用 `/#/embed/roundtable` 用 **`username` + `password`** 登录、应用主题。
2. 带 `taskId` 时先按任务取最新草稿:
- **有历史** → 回显最新一条聊天记录(跳到对应步骤),**不再**自动发首条消息。
- **无历史** → 自动把「任务名称/描述」作为第一步首条消息发出,触发任务理解分析。
3. 用户点「进入多智能体研讨」→ 若带了 `chainId`,跳过选链弹窗、按该业务链条进第二步。
4. 第二步总控自然收敛达成共识 → 回传 `roundtable:consensus`。
### 4.2 聊天记录持久化(按任务 id,不分权)
- 传了 `taskId` 后,子应用产生的草稿(第一步意图 + 第二步圆桌)都**绑定该 task_id**。
- 这类草稿**按 task_id 共享、不按登录用户分权**:同一 `taskId` 不论谁打开,都能读到/续写同一份;
打开时默认回显该任务**最新一条**记录。
- 未传 `taskId` 的草稿仍按登录用户隔离(原有行为不变)。
- 后端:`roundtable_drafts` 表新增 `task_id` 列;接口 `GET /api/roundtable-drafts/by-task/{task_id}/latest`。
老库需跑 `offline-backend-20260512/backend/scripts/sql/column_additions_mysql.sql` 补列(或新库
`create_all` 自动加列),并重启 Gateway。
### 4.3 主题色
默认 **深蓝(dark)**;其主色为蓝(子应用 dark 主题)。宿主传 `theme="light"` 可切浅色。
> 子应用侧的主题映射在 `EmbedRoundtablePage`:`dark`/`dark-blue`/空 → 深蓝,`light`/`红白` → 浅色。
---
## 5. 联调注意
1. **`baseUrl` 是前端站点源**(如 `http://localhost:5174`),不要填后端 8001。
2. **登录(用户名+密码)**:内网走 `POST /api/v1/auth/login/username`——`username` 决定登录哪个账号
(不存在则自动注册),`password` 是后端配置的**单一共享口令**(仅 `auth_login.username_login.require_password`
开启时校验,默认口令 `123ewq`),它只是门禁、不区分账号。另一种 token 登录(`login/token`)本嵌入**不使用**。
3. **chainId / taskId** 要用后端真实存在的业务链 id / 业务方自有的任务 id。
4. **高度自适应**:抽屉已把无界容器逐层撑满(`WujieVue width/height=100%` + `<wujie-app>`/`iframe`
铺满),内容超出时由圆桌页内部各列滚动。
### 5.1 跨域(wujie 三层 origin)
宿主(本工程,默认 **5183**)嵌 DeerFlow 前端(**5174**),子应用再调 Gateway(**8001**),
三层 origin 不同,跨域要分别处理:
| 现象 | 失败 URL 特征 | 处理 |
|------|---------------|------|
| 抽屉空白 / 子应用 JS 拉不下来 | `5174` 的 html、`.js` | 子应用 Vite 开 `cors: true`(已配);宿主 `WujieVue` 用 `:fetch="wujieCredentialsFetch"`(已配,见 `wujie-fetch.ts`) |
| 一直「正在进入圆桌…」/ 登录失败 | `8001` 的 `/api/...` | Gateway `GATEWAY_CORS_ORIGINS` 加入**宿主页 origin**(含 `localhost` 与 `127.0.0.1` 两种写法),改后重启 Gateway |
**本地 Gateway 示例**(`offline-backend-20260512/.env`):
```bash
GATEWAY_CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174,\
http://localhost:5183,http://127.0.0.1:5183
```
**排查**:DevTools → Network,看第一个红字请求的 URL 与 Request Headers 里的 `Origin`,
把该 origin 原样加入 `GATEWAY_CORS_ORIGINS`。
**常见坑**:
- `localhost` 与 `127.0.0.1` 算不同 origin,白名单要各写一条。
- `baseUrl` 端口须与子应用 Vite 实际端口一致(默认 **5174**;仅 `pnpm dev` 时看终端 `Local:` 行)。
- 浏览器地址栏用哪种 hostname 打开宿主,Gateway 就要允许哪种(例如用 `127.0.0.1:5183` 打开就要加 `http://127.0.0.1:5183`)。
**生产推荐**:Nginx 同源反代,把宿主、DeerFlow 前端、Gateway API 挂到同一域名下
(如 `/`、`/deerflow/`、`/deerflow/api/`),`baseUrl` 改为反代后的子应用路径,可免大部分 CORS。
子应用静态站点勿设 `X-Frame-Options: DENY`;若用 credentials,勿对子应用资源回 `Access-Control-Allow-Origin: *`。
### 5.2 Nginx 不能改 CORS 时(推荐)
运维无法给 DeerFlow 静态站加 `Access-Control-Allow-Origin` 时,**不要**依赖无界默认的
「父页面 fetch 子应用 JS」模式——一定会跨域失败。改用下面两种方案之一:
#### 方案 A:无界 iframe 降级(默认已开启,零 Nginx 改动)
`RoundtableDrawer` 默认 `degrade="auto"`:**检测到 `baseUrl` 与宿主不同源时自动走 iframe 降级**。
| 对比 | 默认模式(fetch 注入) | iframe 降级(`degrade: true`) |
|------|------------------------|--------------------------------|
| 子应用 HTML/JS 谁加载 | 宿主跨域 fetch | 浏览器 iframe 直接导航 |
| 是否需要子站 Nginx CORS | **需要** | **不需要** |
| props / bus 通信 | 支持 | 支持 |
| 子应用调 API 的 Origin | 往往是**宿主**域名 | **子应用**域名 |
因此生产跨域嵌入时,Gateway 只需允许 **DeerFlow 前端域名**(子应用 iframe 的 origin),
**不必**再为宿主域名加 CORS——前提是 DeerFlow 打包时 API 与子应用同源,例如:
```bash
# frontend-web/.env.production / .env.sit
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
```
显式强制降级(跨域场景一般不必写,`auto` 即可):
```vue
<RoundtableDrawer degrade :base-url="https://ch1.b.sit.4.cn/deerflow" ... />
```
**注意**:子站若配置了 `X-Frame-Options: DENY` 或 CSP `frame-ancestors 'none'`,
iframe 会被浏览器拦截——这与 CORS 无关,需运维放开「允许被 iframe 嵌入」(或去掉该响应头)。
#### 方案 B:同源静态目录(彻底无跨域)
若能在**同一域名**下多挂一个静态目录(只加 `location` 指到 dist,**不必**配 CORS 头),
把 DeerFlow 前端 `dist` 部署到宿主站点下,例如:
```text
https://portal.example.com/ → 宿主 Vue
https://portal.example.com/deerflow/ → DeerFlow dist(复制或 CI 同步)
```
宿主 `baseUrl` 填**相对路径**:
```vue
<RoundtableDrawer base-url="/deerflow" ... />
```
同源时 `degrade` 自动为 `false`,走无界常规模式,无任何跨域问题。
---
## 6. 目录
```
parallel-agents-vue/
├── src/roundtable-embed/
│ ├── RoundtableDrawer.vue # 抽屉组件(wujie 嵌入 + props/bus 通信)
│ ├── wujie-fetch.ts # 跨域 fetch(credentials: include,非降级时用)
│ ├── wujie-origin.ts # 同源/跨域判断、相对 baseUrl、降级 query 兜底
│ ├── resolve-wujie-degrade.ts
│ └── index.ts # 导出组件 + 类型
├── src/App.vue # Demo 页签入口
├── offline-deps/wujie/ # wujie 离线依赖包(内网安装)
│ ├── *.tgz # wujie-vue3 / wujie / @babel/runtime
│ ├── install-offline.ps1/.sh
│ └── README.md
└── docs/圆桌规划抽屉-wujie-嵌入使用说明.md # 本文
```
@@ -0,0 +1,70 @@
# wujie 离线依赖包(内网无网安装)
圆桌规划抽屉(`src/roundtable-embed/`)用无界(wujie)把 DeerFlow 圆桌页嵌进抽屉,需要以下
依赖。内网环境无法联网拉包,故把它们及全部传递依赖预先打成 `.tgz` 放在本目录。
## 依赖清单(共 3 个,已全部含在本目录)
| 包 | 版本 | 说明 |
|----|------|------|
| `wujie-vue3` | 2.0.2 | 无界 Vue3 封装组件(`<WujieVue>` + `bus`) |
| `wujie` | 2.0.2 | 无界内核(`wujie-vue3` 的依赖) |
| `@babel/runtime` | 7.29.7 | `wujie` 的唯一依赖(自身无下游依赖) |
> `vue@^3` 是 `wujie-vue3` 的 peer 依赖,项目本就有,无需打包。
> 经核对,上述 3 个包之外**没有任何其他传递依赖**。
## 安装(任选一种)
> 全程不联网。命令里**依赖在前、消费方在后**,让本地 tarball 互相满足 semver 范围。
### 方式 A:脚本一键装(推荐)
```powershell
# Windows PowerShell
./install-offline.ps1
```
```bash
# Linux / macOS / Git-Bash
bash ./install-offline.sh
```
脚本会自动 `cd` 到项目根、优先用 pnpm(无则 npm)离线安装。
### 方式 B:手动命令
在**项目根目录**(`parallel-agents-vue/`)执行:
```bash
# pnpm
pnpm add --offline \
./offline-deps/wujie/babel-runtime-7.29.7.tgz \
./offline-deps/wujie/wujie-2.0.2.tgz \
./offline-deps/wujie/wujie-vue3-2.0.2.tgz
# 或 npm
npm install --offline \
./offline-deps/wujie/babel-runtime-7.29.7.tgz \
./offline-deps/wujie/wujie-2.0.2.tgz \
./offline-deps/wujie/wujie-vue3-2.0.2.tgz
```
### 方式 C:纯解压兜底(连包管理器都不想用时)
npm pack 的 tarball 内层是 `package/` 目录,直接解压改名进 `node_modules`:
```bash
cd parallel-agents-vue
mkdir -p node_modules/@babel
tar -xzf offline-deps/wujie/wujie-2.0.2.tgz && rm -rf node_modules/wujie && mv package node_modules/wujie
tar -xzf offline-deps/wujie/wujie-vue3-2.0.2.tgz && rm -rf node_modules/wujie-vue3 && mv package node_modules/wujie-vue3
tar -xzf offline-deps/wujie/babel-runtime-7.29.7.tgz && rm -rf node_modules/@babel/runtime && mv package node_modules/@babel/runtime
```
(Win10+/Linux/macOS 均自带 `tar`。这三个包是纯 JS、无原生编译,复制即可用。)
## 验证
```bash
node -e "console.log(require('wujie-vue3/package.json').version, require('wujie/package.json').version, require('@babel/runtime/package.json').version)"
# 期望输出: 2.0.2 2.0.2 7.29.7
```
之后 `pnpm dev` / `npm run dev` 打开「圆桌规划抽屉(wujie)」页签即可。
## 重新生成这些 tarball(有网机器上)
```bash
cd offline-deps/wujie
npm pack wujie@2.0.2 wujie-vue3@2.0.2 @babel/runtime@7.29.7
```
@@ -0,0 +1,30 @@
# 离线安装 wujie 相关依赖(内网无网环境)。
# 用法:在本文件所在目录右键「使用 PowerShell 运行」,或:
# pwsh ./install-offline.ps1
#
# 依赖集(已全部打进同目录 .tgz,无其他传递依赖;vue 为已存在的 peer):
# wujie-vue3@2.0.2 -> wujie@2.0.2 -> @babel/runtime@7.29.7
$ErrorActionPreference = "Stop"
$dir = $PSScriptRoot
$proj = (Resolve-Path (Join-Path $dir "..\..")).Path
# 依赖在前、消费方在后,便于本地 tarball 互相满足 semver 范围(不触网)。
$tgz = @(
(Join-Path $dir "babel-runtime-7.29.7.tgz"),
(Join-Path $dir "wujie-2.0.2.tgz"),
(Join-Path $dir "wujie-vue3-2.0.2.tgz")
)
Push-Location $proj
try {
$hasPnpm = $null -ne (Get-Command pnpm -ErrorAction SilentlyContinue)
if ($hasPnpm) {
Write-Host "[offline] 使用 pnpm 离线安装 wujie 依赖 ..." -ForegroundColor Cyan
& pnpm add --offline @tgz
} else {
Write-Host "[offline] 未找到 pnpm,改用 npm 离线安装 ..." -ForegroundColor Cyan
& npm install --offline @tgz
}
Write-Host "[offline] 完成。可运行 'pnpm dev' / 'npm run dev' 验证。" -ForegroundColor Green
}
finally { Pop-Location }
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 离线安装 wujie 相关依赖(内网无网环境)。
# bash ./install-offline.sh
#
# 依赖集(已全部打进同目录 .tgz,无其他传递依赖;vue 为已存在的 peer):
# wujie-vue3@2.0.2 -> wujie@2.0.2 -> @babel/runtime@7.29.7
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJ="$(cd "$DIR/../.." && pwd)"
# 依赖在前、消费方在后,便于本地 tarball 互相满足 semver 范围(不触网)。
TGZ=(
"$DIR/babel-runtime-7.29.7.tgz"
"$DIR/wujie-2.0.2.tgz"
"$DIR/wujie-vue3-2.0.2.tgz"
)
cd "$PROJ"
if command -v pnpm >/dev/null 2>&1; then
echo "[offline] 使用 pnpm 离线安装 wujie 依赖 ..."
pnpm add --offline "${TGZ[@]}"
else
echo "[offline] 未找到 pnpm,改用 npm 离线安装 ..."
npm install --offline "${TGZ[@]}"
fi
echo "[offline] 完成。可运行 'pnpm dev' / 'npm run dev' 验证。"
+6 -2
View File
@@ -14,7 +14,9 @@
},
"./style.css": "./dist/parallel-agents-vue.css"
},
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
@@ -26,7 +28,9 @@
"vue": "^3.4.0"
},
"dependencies": {
"markdown-it": "^14.1.0"
"markdown-it": "^14.1.0",
"wujie": "^2.0.2",
"wujie-vue3": "^2.0.2"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
+34 -225
View File
@@ -11,19 +11,19 @@ importers:
markdown-it:
specifier: ^14.1.0
version: 14.2.0
wujie:
specifier: ^2.0.2
version: 2.0.2
wujie-vue3:
specifier: ^2.0.2
version: 2.0.2(vue@3.5.35(typescript@5.9.3))
devDependencies:
'@tailwindcss/vite':
specifier: ^4.1.0
version: 4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
'@vitejs/plugin-vue':
specifier: ^5.2.0
version: 5.2.4(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3))
tailwindcss:
specifier: ^4.1.0
version: 4.3.0
typescript:
specifier: ^5.6.0
version: 5.9.3
@@ -52,6 +52,10 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.29.7':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
@@ -212,22 +216,9 @@ packages:
cpu: [x64]
os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@rollup/rollup-android-arm-eabi@4.61.1':
resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==}
cpu: [arm]
@@ -366,100 +357,6 @@ packages:
cpu: [x64]
os: [win32]
'@tailwindcss/node@4.3.0':
resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
'@tailwindcss/oxide-android-arm64@4.3.0':
resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.3.0':
resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.3.0':
resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.3.0':
resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==}
engines: {node: '>= 20'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.3.0':
resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
engines: {node: '>= 20'}
'@tailwindcss/vite@4.3.0':
resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
@@ -550,10 +447,6 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
enhanced-resolve@5.23.0:
resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -584,9 +477,6 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -721,13 +611,6 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tailwindcss@4.3.0:
resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==}
tapable@2.3.3:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
@@ -797,6 +680,14 @@ packages:
typescript:
optional: true
wujie-vue3@2.0.2:
resolution: {integrity: sha512-RypNFueT1z7Db/f44gfTrRAVJNj5Brq3httVgKpsEZ7UpCrNhjNZkBd+RX+XT4mNMB1L9t3fsO/DuqfhViT7YA==}
peerDependencies:
vue: ^3.0.0
wujie@2.0.2:
resolution: {integrity: sha512-K4SyJ1/frFOFJK4plE41UJ9Fe26pYGizemz2kXv3A0w+AcEoEVVU4mamHxXU1r/3jq3UXCragOacvPbg5XT9yg==}
snapshots:
'@babel/helper-string-parser@7.29.7': {}
@@ -807,6 +698,8 @@ snapshots:
dependencies:
'@babel/types': 7.29.7
'@babel/runtime@7.29.7': {}
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
@@ -890,25 +783,8 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@rollup/rollup-android-arm-eabi@4.61.1':
optional: true
@@ -984,74 +860,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.61.1':
optional: true
'@tailwindcss/node@4.3.0':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.23.0
jiti: 2.7.0
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.3.0
'@tailwindcss/oxide-android-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.3.0':
optional: true
'@tailwindcss/oxide-darwin-x64@4.3.0':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.3.0':
optional: true
'@tailwindcss/oxide@4.3.0':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.3.0
'@tailwindcss/oxide-darwin-arm64': 4.3.0
'@tailwindcss/oxide-darwin-x64': 4.3.0
'@tailwindcss/oxide-freebsd-x64': 4.3.0
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0
'@tailwindcss/oxide-linux-arm64-gnu': 4.3.0
'@tailwindcss/oxide-linux-arm64-musl': 4.3.0
'@tailwindcss/oxide-linux-x64-gnu': 4.3.0
'@tailwindcss/oxide-linux-x64-musl': 4.3.0
'@tailwindcss/oxide-wasm32-wasi': 4.3.0
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
'@tailwindcss/vite@4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)
'@types/estree@1.0.9': {}
'@types/linkify-it@5.0.0': {}
@@ -1166,12 +974,8 @@ snapshots:
de-indent@1.0.2: {}
detect-libc@2.1.2: {}
enhanced-resolve@5.23.0:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
detect-libc@2.1.2:
optional: true
entities@4.5.0: {}
@@ -1215,11 +1019,10 @@ snapshots:
fsevents@2.3.3:
optional: true
graceful-fs@4.2.11: {}
he@1.2.0: {}
jiti@2.7.0: {}
jiti@2.7.0:
optional: true
lightningcss-android-arm64@1.32.0:
optional: true
@@ -1269,6 +1072,7 @@ snapshots:
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
optional: true
linkify-it@5.0.1:
dependencies:
@@ -1344,10 +1148,6 @@ snapshots:
source-map-js@1.2.1: {}
tailwindcss@4.3.0: {}
tapable@2.3.3: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
@@ -1387,3 +1187,12 @@ snapshots:
'@vue/shared': 3.5.35
optionalDependencies:
typescript: 5.9.3
wujie-vue3@2.0.2(vue@3.5.35(typescript@5.9.3)):
dependencies:
vue: 3.5.35(typescript@5.9.3)
wujie: 2.0.2
wujie@2.0.2:
dependencies:
'@babel/runtime': 7.29.7
+125
View File
@@ -10,6 +10,48 @@ import { ParallelAgentsPanel } from "./parallel-agents";
import type { AgentInput, AgentReply } from "./parallel-agents";
import { DEFAULT_BASE_URL } from "./parallel-agents/config";
import { RoundtableDrawer } from "./roundtable-embed";
import type { RoundtableConsensusPayload } from "./roundtable-embed";
// ── 顶部页签:在「并行面板 Demo」与「圆桌规划抽屉」之间切换 ──────────────
const view = ref<"panel" | "roundtable">("panel");
// ── 圆桌规划抽屉(wujie 嵌入 DeerFlow)Demo 状态 ───────────────────────────
const rt = reactive({
// DeerFlow 前端站点源(无界子应用 URL 的源,非后端地址)。
baseUrl: localStorage.getItem("rt.baseUrl") || "http://localhost:5174",
username: localStorage.getItem("rt.username") || "demo",
// 用户名+密码登录的共享口令(后端开启 require_password 时必填)。
password: localStorage.getItem("rt.password") || "",
taskTitle: localStorage.getItem("rt.taskTitle") || "",
taskGoal: localStorage.getItem("rt.taskGoal") || "",
chainId: localStorage.getItem("rt.chainId") || "",
// 任务 id:聊天记录按此存取、打开时回显最新一条。
taskId: localStorage.getItem("rt.taskId") || "",
// 主题色:默认深蓝(dark)。
theme: localStorage.getItem("rt.theme") || "dark",
});
const rtOpen = ref(false);
const rtConsensus = ref<RoundtableConsensusPayload | null>(null);
function openRoundtable() {
localStorage.setItem("rt.baseUrl", rt.baseUrl);
localStorage.setItem("rt.username", rt.username);
localStorage.setItem("rt.password", rt.password);
localStorage.setItem("rt.taskTitle", rt.taskTitle);
localStorage.setItem("rt.taskGoal", rt.taskGoal);
localStorage.setItem("rt.chainId", rt.chainId);
localStorage.setItem("rt.taskId", rt.taskId);
localStorage.setItem("rt.theme", rt.theme);
rtConsensus.value = null;
rtOpen.value = true;
}
function onRoundtableConsensus(payload: RoundtableConsensusPayload) {
// 子应用第二步达成共识 → 收到各智能体交付。这里 demo 仅打印 + 展示。
console.log("[demo] roundtable:consensus → 共识达成,各智能体交付:", payload);
rtConsensus.value = payload;
}
const cfg = reactive({
baseUrl: localStorage.getItem("pa.baseUrl") || DEFAULT_BASE_URL,
@@ -65,6 +107,24 @@ function launch() {
<template>
<div style="min-height: 100vh; background: #0a1020; color: #cbd5e1; padding: 24px; font-family: system-ui">
<!-- 顶部页签:并行面板 Demo / 圆桌规划抽屉 -->
<div style="display: flex; gap: 8px; margin-bottom: 16px">
<button
@click="view = 'panel'"
:style="{ padding: '6px 14px', borderRadius: '6px', border: 0, cursor: 'pointer', background: view === 'panel' ? '#1677ff' : '#1a2742', color: '#e5edf7' }"
>
并行多智能体面板
</button>
<button
@click="view = 'roundtable'"
:style="{ padding: '6px 14px', borderRadius: '6px', border: 0, cursor: 'pointer', background: view === 'roundtable' ? '#1677ff' : '#1a2742', color: '#e5edf7' }"
>
圆桌规划抽屉(wujie)
</button>
</div>
<!-- ── 页面一:并行多智能体面板 Demo ───────────────────────────────── -->
<template v-if="view === 'panel'">
<h1 style="font-size: 18px; font-weight: 700; margin-bottom: 12px">并行多智能体面板 · Dev Demo</h1>
<div style="display: flex; flex-direction: column; gap: 8px; max-width: 640px">
<label>后端 baseUrl
@@ -154,5 +214,70 @@ function launch() {
</div>
</div>
</div>
</template>
<!-- ── 页面二:圆桌规划抽屉(wujie 嵌入 DeerFlow) ──────────────────── -->
<template v-else>
<h1 style="font-size: 18px; font-weight: 700; margin-bottom: 12px">圆桌规划抽屉 · wujie 嵌入 Demo</h1>
<div style="display: flex; flex-direction: column; gap: 8px; max-width: 640px">
<label>DeerFlow 前端站点源(无界子应用 URL 的源)
<input v-model="rt.baseUrl" placeholder="http://localhost:5174" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>用户名(决定登录哪个账号)
<input v-model="rt.username" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>密码 / 口令(后端开启 require_password 时必填,单一共享口令)
<input v-model="rt.password" type="password" placeholder="未开启门禁可留空" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>任务名称
<input v-model="rt.taskTitle" placeholder="例如:XX 产品出海方案规划" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>任务描述
<textarea v-model="rt.taskGoal" rows="3" placeholder="把背景、目标、约束写清楚" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px; resize: vertical" />
</label>
<label>指定业务链条 chainId(直传,子应用据此自动选链进第二步)
<input v-model="rt.chainId" placeholder="例如 chain_xxx" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>任务 id(聊天记录按此存取;该任务有记录则打开时回显最新一条)
<input v-model="rt.taskId" placeholder="例如 task-001" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px" />
</label>
<label>主题色(默认深蓝 dark)
<select v-model="rt.theme" style="width: 100%; padding: 6px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px">
<option value="dark">深蓝(dark)</option>
<option value="light">浅色(light)</option>
</select>
</label>
<button @click="openRoundtable" style="align-self: flex-start; padding: 8px 16px; background: #1677ff; color: #fff; border: 0; border-radius: 6px; cursor: pointer">
打开圆桌规划抽屉
</button>
</div>
<!-- 共识回传结果(子应用第二步达成共识后回传各智能体交付) -->
<div v-if="rtConsensus" style="margin-top: 16px; max-width: 880px; background: #0d1526; border: 1px solid #1e2d4d; border-radius: 8px; padding: 12px">
<div style="font-weight: 700; color: #7ee787; margin-bottom: 8px">✅ 已收到共识回传(roundtable:consensus)</div>
<div style="font-size: 13px; color: #9fb0c8; margin-bottom: 6px">
链条:{{ rtConsensus.chainTitle || rtConsensus.chainId || "—" }}|总控:{{ rtConsensus.coordinatorName || "—" }}
</div>
<div style="font-size: 13px; color: #cbd5e1; white-space: pre-wrap; margin-bottom: 8px">{{ rtConsensus.consensusText }}</div>
<div v-for="d in rtConsensus.deliverables" :key="d.agentName" style="border-top: 1px solid #1e2d4d; padding-top: 8px; margin-top: 8px">
<div style="font-weight: 600; color: #e5edf7">{{ d.agentName }}</div>
<div style="font-size: 12px; color: #9fb0c8; white-space: pre-wrap">{{ d.content }}</div>
</div>
</div>
<!-- 抽屉本体:wujie 嵌入 DeerFlow 圆桌规划页 -->
<RoundtableDrawer
v-model:open="rtOpen"
:base-url="rt.baseUrl"
:username="rt.username || undefined"
:password="rt.password || undefined"
:task-title="rt.taskTitle || undefined"
:task-goal="rt.taskGoal || undefined"
:chain-id="rt.chainId || undefined"
:task-id="rt.taskId || undefined"
:theme="rt.theme"
@consensus="onRoundtableConsensus"
/>
</template>
</div>
</template>
@@ -0,0 +1,193 @@
<script setup lang="ts">
/**
* 圆桌规划嵌入抽屉(宿主侧 / 父页面)。
*
* 用无界(wujie)把 DeerFlow 的「圆桌规划」页塞进一个右侧抽屉:
* - 父 → 子:通过 `:props` 注入「任务名称 / 任务描述 / 指定业务链条 chainId / 用户名 / 主题」,
* 子应用首帧从 `window.$wujie.props` 读取(见 DeerFlow `core/embed/wujie.ts`)。
* 抽屉打开即自动跑第一步分析,点「进入多智能体研讨」按指定链条进第二步。
* - 子 → 父:子应用第二步达成共识后 `bus.$emit('roundtable:consensus', payload)`,
* 本组件监听后向宿主 `emit('consensus', payload)`,把各智能体交付交还父页面。
*
* 抽屉关闭即卸载子应用(`alive=false`),下次打开是干净的新实例、读最新 props。
*/
import { computed, onBeforeUnmount } from "vue";
import WujieVue from "wujie-vue3";
import { bus } from "wujie";
/** 单个智能体交付(与 DeerFlow `collectRoundtableDeliverables` 对齐)。 */
export interface RoundtableDeliverable {
agentName: string;
content: string;
threadId: string | null;
}
/** 共识回传载荷(与 DeerFlow `emitWujieEvent("roundtable:consensus", …)` 对齐)。 */
export interface RoundtableConsensusPayload {
taskTitle: string | null;
chainId: string | null;
chainTitle: string | null;
coordinatorName: string | null;
consensusText: string;
deliverables: RoundtableDeliverable[];
}
const props = withDefaults(
defineProps<{
/** 抽屉开关(v-model:open)。 */
open: boolean;
/** DeerFlow 前端站点源,如 http://localhost:5174。 */
baseUrl: string;
/** 任务名称(注入子应用 Step 1 首条消息)。 */
taskTitle?: string;
/** 任务描述(注入子应用 Step 1 首条消息)。 */
taskGoal?: string;
/** 指定业务链条 id(直传,子应用据此自动选链进第二步)。 */
chainId?: string;
/** 外部任务 id:聊天记录按此存取,打开时回显该任务最新一条记录。 */
taskId?: string;
/** 登录用户名(决定登录哪个账号)。 */
username?: string;
/** 用户名+密码登录的共享口令(后端开启 require_password 时必填)。 */
password?: string;
/** 主题色:默认 dark(深蓝)。 */
theme?: string;
}>(),
{ theme: "dark" },
);
const emit = defineEmits<{
(e: "update:open", v: boolean): void;
(e: "consensus", payload: RoundtableConsensusPayload): void;
}>();
/** 无界子应用实例名(同名复用同一沙箱,这里抽屉级唯一即可)。 */
const APP_NAME = "deerflow-roundtable";
/**
* 子应用 URL:DeerFlow 前端站点 + 圆桌**嵌入**路由(只渲染主体、无侧边栏,用户名登录)。
* 注意走 `/#/embed/roundtable`,而非带 workspace 侧边栏的 `/#/page/workspace/roundtable`。
*/
const subUrl = computed(() => {
const origin = props.baseUrl.replace(/\/+$/, "");
return `${origin}/#/embed/roundtable`;
});
/** 注入子应用的 props(DeerFlow `readWujiePrefill()` 读取)。 */
const wujieProps = computed(() => ({
taskTitle: props.taskTitle,
taskGoal: props.taskGoal,
chainId: props.chainId,
taskId: props.taskId,
username: props.username,
password: props.password,
theme: props.theme,
}));
function close() {
emit("update:open", false);
}
function onConsensus(payload: RoundtableConsensusPayload) {
emit("consensus", payload);
}
// 订阅子应用共识回传;组件卸载时退订(全局总线,避免重复监听)。
bus.$on("roundtable:consensus", onConsensus);
onBeforeUnmount(() => bus.$off("roundtable:consensus", onConsensus));
</script>
<template>
<div v-if="open" class="rt-drawer-mask" @click.self="close">
<aside class="rt-drawer" @click.stop>
<header class="rt-drawer__head">
<span class="rt-drawer__title">圆桌规划 · {{ taskTitle || "新任务" }}</span>
<button class="rt-drawer__close" type="button" @click="close">✕</button>
</header>
<div class="rt-drawer__body">
<WujieVue
:name="APP_NAME"
:url="subUrl"
:props="wujieProps"
width="100%"
height="100%"
:sync="false"
:alive="false"
/>
</div>
</aside>
</div>
</template>
<style scoped>
.rt-drawer-mask {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.45);
display: flex;
justify-content: flex-end;
}
.rt-drawer {
width: 92vw;
max-width: 1280px;
height: 100%;
background: #fff;
display: flex;
flex-direction: column;
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.25);
animation: rt-slide-in 0.22s ease-out;
}
@keyframes rt-slide-in {
from {
transform: translateX(24px);
opacity: 0.6;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.rt-drawer__head {
flex: 0 0 auto;
height: 48px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
}
.rt-drawer__title {
font-size: 14px;
font-weight: 600;
color: #222;
}
.rt-drawer__close {
width: 28px;
height: 28px;
border: 0;
border-radius: 6px;
background: transparent;
color: #888;
font-size: 16px;
cursor: pointer;
}
.rt-drawer__close:hover {
background: #f2f2f2;
color: #333;
}
.rt-drawer__body {
flex: 1;
min-height: 0;
}
/* 让无界容器链逐层铺满抽屉主体,子应用 #root 的 height:100% 才有确定父高度可解析。
WujieVue 根 div 已由 width/height props 撑成 100%;这里再兜底注入的 web component
主机 <wujie-app> 与退化 iframe,让链路不断。 */
.rt-drawer__body :deep(wujie-app),
.rt-drawer__body :deep(iframe) {
display: block;
width: 100%;
height: 100%;
border: 0;
}
</style>
@@ -0,0 +1,5 @@
export { default as RoundtableDrawer } from "./RoundtableDrawer.vue";
export type {
RoundtableConsensusPayload,
RoundtableDeliverable,
} from "./RoundtableDrawer.vue";
@@ -0,0 +1,13 @@
import { isCrossOriginBaseUrl } from "./wujie-origin";
export type WujieDegradeMode = boolean | "auto";
/** 解析无界是否走 iframe 降级(跨域且 Nginx 未配 CORS 时必须降级)。 */
export function resolveWujieDegrade(
baseUrl: string,
degrade: WujieDegradeMode,
): boolean {
if (degrade === true) return true;
if (degrade === false) return false;
return isCrossOriginBaseUrl(baseUrl);
}
@@ -0,0 +1,11 @@
/**
* 无界跨域加载子应用 HTML/JS/CSS 时的 fetch 封装。
* 宿主与子应用不同源时必须携带 credentials,否则子应用资源或 Cookie 可能拉不下来。
* @see https://wujie-micro.github.io/doc/question/
*/
export function wujieCredentialsFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
return window.fetch(input, { ...init, credentials: "include" });
}
@@ -0,0 +1,48 @@
/** 解析圆桌嵌入完整 URL(支持同源相对路径,如 `/deerflow`)。 */
export function resolveRoundtableEmbedUrl(baseUrl: string): string {
const trimmed = baseUrl.trim().replace(/\/+$/, "");
if (!trimmed) return "/#/embed/roundtable";
if (trimmed.startsWith("/")) {
return `${window.location.origin}${trimmed}/#/embed/roundtable`;
}
return `${trimmed}/#/embed/roundtable`;
}
/** baseUrl 是否与当前宿主页面不同源(跨域嵌入)。相对路径视为同源。 */
export function isCrossOriginBaseUrl(baseUrl: string): boolean {
const trimmed = baseUrl.trim().replace(/\/+$/, "");
if (!trimmed || trimmed.startsWith("/")) return false;
try {
const target = new URL(trimmed, window.location.href);
return target.origin !== window.location.origin;
} catch {
return true;
}
}
export interface RoundtableEmbedQuery {
taskTitle?: string;
taskGoal?: string;
chainId?: string;
taskId?: string;
username?: string;
password?: string;
theme?: string;
}
/** iframe 降级模式下把关键参数挂到 hash 路由 query,作 props 注入的兜底。 */
export function appendEmbedQueryToUrl(
embedUrl: string,
fields: RoundtableEmbedQuery,
): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(fields)) {
if (value != null && String(value).length > 0) {
params.set(key, String(value));
}
}
const qs = params.toString();
if (!qs) return embedUrl;
const sep = embedUrl.includes("?") ? "&" : "?";
return `${embedUrl}${sep}${qs}`;
}