wujie应用联调
This commit is contained in:
1 parent
0784f30e23
commit
f3ca996116
35 files changed
+1590
-334
No files matched your search
@@ -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}`;
|
||||
}
|
||||
Reference in new issue
Block a user