da
This commit is contained in:
1 parent
ca9960c8bc
commit
bd426ed1d3
24 files changed
+912
-108
No files matched your search
@@ -0,0 +1,437 @@
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
|
||||
/**
|
||||
* 任务看板假数据 —— 用于在没有后端 `/task/cop-tasks` / `/three/cop-three-tasks`
|
||||
* 接口时,给「任务分析」「三情分析」两个页面提供演示数据。
|
||||
*
|
||||
* 用法:把组件里的 `await service.post(url, payload)` 换成
|
||||
* `await mockCopTasks('task' | 'sentiment', taskType, payload)`
|
||||
* 返回结构与真实接口保持一致:`{ data: { records, total } }`。
|
||||
*/
|
||||
|
||||
export type MockColumnType = 'unanalyzed' | 'running' | 'completed';
|
||||
export type MockDataset = 'task' | 'sentiment';
|
||||
|
||||
interface MockTaskSeed {
|
||||
taskName: string;
|
||||
overview: string;
|
||||
taskDirection: string;
|
||||
taskLevel: number; // 1 一般 / 2 紧急 / 3 加急
|
||||
taskType: number; // 1 单任务 / 2 场景 / 3 会商 / 4 多智能体 / 5 检索
|
||||
}
|
||||
|
||||
/** 列 → 状态码,与组件内 taskStatusMap 对齐。 */
|
||||
const COLUMN_STATUS: Record<MockColumnType, number> = {
|
||||
unanalyzed: 0,
|
||||
running: 20,
|
||||
completed: 30,
|
||||
};
|
||||
|
||||
/** 生成一条完整的 TaskListItem,仅覆盖有意义的字段,其余给安全默认值。 */
|
||||
function makeTask(
|
||||
id: number,
|
||||
column: MockColumnType,
|
||||
seed: MockTaskSeed,
|
||||
createTime: string,
|
||||
updateTime: string,
|
||||
): TaskListItem {
|
||||
return {
|
||||
createBy: 1001,
|
||||
createTime,
|
||||
deptId: null,
|
||||
id,
|
||||
isTop: 0,
|
||||
issuingDept: '战略规划处',
|
||||
keywords: null,
|
||||
overview: seed.overview,
|
||||
planTaskTop: 0,
|
||||
platform: null,
|
||||
ptaskId: null,
|
||||
recommendedBasis: null,
|
||||
scene: '',
|
||||
sourceType: 1,
|
||||
step: null,
|
||||
taskContent: seed.overview,
|
||||
taskDirection: seed.taskDirection,
|
||||
taskEndDate: '2026-06-30 18:00:00',
|
||||
taskLevel: seed.taskLevel,
|
||||
taskName: seed.taskName,
|
||||
taskNo: `T-${String(id).padStart(6, '0')}`,
|
||||
taskStartDate: '2026-06-01 09:00:00',
|
||||
taskStatus: COLUMN_STATUS[column],
|
||||
taskSuperiorFlag: 0,
|
||||
taskType: seed.taskType,
|
||||
column,
|
||||
updateBy: 1001,
|
||||
updateTime,
|
||||
endCount: column === 'completed' ? 1 : 0,
|
||||
sumAll: 1,
|
||||
progress: column === 'completed' ? 100 : column === 'running' ? 60 : 0,
|
||||
taskStatusData: null,
|
||||
} as TaskListItem;
|
||||
}
|
||||
|
||||
/** 「任务分析」页(业务码 6BF)数据种子。 */
|
||||
const TASK_SEEDS: Record<MockColumnType, MockTaskSeed[]> = {
|
||||
unanalyzed: [
|
||||
{
|
||||
taskName: '夏季文旅消费季传播任务分析',
|
||||
overview:
|
||||
'围绕"夏季文旅消费季"主题,梳理传播目标、核心受众与潜在策略阻力,输出可执行的任务拆解建议。',
|
||||
taskDirection: '王磊',
|
||||
taskLevel: 2,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '新能源补贴政策解读专题',
|
||||
overview:
|
||||
'针对新一轮新能源汽车补贴政策,分析公众关注焦点与误读风险点,形成政策解读传播要点清单。',
|
||||
taskDirection: '李婷',
|
||||
taskLevel: 1,
|
||||
taskType: 5,
|
||||
},
|
||||
{
|
||||
taskName: '城市更新片区舆论引导任务',
|
||||
overview:
|
||||
'对老城区改造涉及的拆迁、安置等敏感议题进行任务目标拆解,识别需要重点回应的群体诉求。',
|
||||
taskDirection: '张伟',
|
||||
taskLevel: 3,
|
||||
taskType: 2,
|
||||
},
|
||||
{
|
||||
taskName: '高校毕业季就业服务宣传',
|
||||
overview:
|
||||
'梳理面向应届毕业生的就业服务政策传播任务,明确分阶段触达节奏与渠道组合。',
|
||||
taskDirection: '赵敏',
|
||||
taskLevel: 1,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '防汛应急科普传播任务',
|
||||
overview:
|
||||
'汛期来临前,分析防汛减灾科普内容的传播目标与受众分层,制定预警信息发布预案。',
|
||||
taskDirection: '陈昊',
|
||||
taskLevel: 2,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '乡村振兴成果展示任务',
|
||||
overview:
|
||||
'围绕本地乡村振兴典型案例,拆解成果展示传播任务,确定故事化表达与素材采集方向。',
|
||||
taskDirection: '周琳',
|
||||
taskLevel: 1,
|
||||
taskType: 2,
|
||||
},
|
||||
],
|
||||
running: [
|
||||
{
|
||||
taskName: '社区养老服务满意度专题分析',
|
||||
overview:
|
||||
'正在对社区居家养老服务的群众反馈进行任务分析,重点判断受众诉求与服务短板。',
|
||||
taskDirection: '孙强',
|
||||
taskLevel: 2,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '中小企业纾困政策落地评估',
|
||||
overview:
|
||||
'多智能体协同分析纾困政策落地过程中的执行阻力与企业关切,形成阶段性研判。',
|
||||
taskDirection: '吴静',
|
||||
taskLevel: 3,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '城市轨道交通延误舆情研判',
|
||||
overview:
|
||||
'针对近期地铁延误事件,正在拆解舆论传播路径与关键节点,评估引导策略有效性。',
|
||||
taskDirection: '郑凯',
|
||||
taskLevel: 2,
|
||||
taskType: 3,
|
||||
},
|
||||
{
|
||||
taskName: '食品安全抽检结果发布任务',
|
||||
overview:
|
||||
'正在分析食品安全抽检通报的发布口径与受众反应,优化信息透明度与安抚策略。',
|
||||
taskDirection: '冯雪',
|
||||
taskLevel: 1,
|
||||
taskType: 1,
|
||||
},
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
taskName: '春节文化活动传播复盘',
|
||||
overview:
|
||||
'已完成春节系列文化活动的传播任务分析,沉淀受众画像与高互动内容规律。',
|
||||
taskDirection: '黄涛',
|
||||
taskLevel: 1,
|
||||
taskType: 3,
|
||||
},
|
||||
{
|
||||
taskName: '医保改革政策宣传任务',
|
||||
overview:
|
||||
'已完成医保改革政策的传播目标拆解与受众分层,输出分群体沟通话术。',
|
||||
taskDirection: '宋佳',
|
||||
taskLevel: 2,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '城市营商环境优化宣传',
|
||||
overview:
|
||||
'已完成营商环境优化举措的任务分析,明确企业端与公众端的差异化传播重点。',
|
||||
taskDirection: '高鹏',
|
||||
taskLevel: 1,
|
||||
taskType: 2,
|
||||
},
|
||||
{
|
||||
taskName: '老旧小区加装电梯议题分析',
|
||||
overview:
|
||||
'已完成加装电梯涉及的邻里诉求与阻力识别,形成多方协商沟通建议。',
|
||||
taskDirection: '何丽',
|
||||
taskLevel: 2,
|
||||
taskType: 3,
|
||||
},
|
||||
{
|
||||
taskName: '生态环保主题宣传周复盘',
|
||||
overview:
|
||||
'已完成环保宣传周的传播效果分析,总结渠道贡献度与可复用素材。',
|
||||
taskDirection: '罗勇',
|
||||
taskLevel: 1,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '青年人才引进政策解读',
|
||||
overview:
|
||||
'已完成人才引进政策的受众判断与策略阻力分析,输出落地传播节奏表。',
|
||||
taskDirection: '谭芳',
|
||||
taskLevel: 1,
|
||||
taskType: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 「三情分析」页(业务码 3Q)数据种子。 */
|
||||
const SENTIMENT_SEEDS: Record<MockColumnType, MockTaskSeed[]> = {
|
||||
unanalyzed: [
|
||||
{
|
||||
taskName: '暑期旅游热点情绪监测',
|
||||
overview:
|
||||
'对暑期热门景区相关讨论进行情绪与倾向分析,识别游客吐槽集中点与正向口碑话题。',
|
||||
taskDirection: '王磊',
|
||||
taskLevel: 2,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '电价调整公众情绪研判',
|
||||
overview:
|
||||
'围绕居民阶梯电价调整,分析公众情绪走向与主要担忧,判断需要重点疏导的群体。',
|
||||
taskDirection: '李婷',
|
||||
taskLevel: 3,
|
||||
taskType: 5,
|
||||
},
|
||||
{
|
||||
taskName: '校园周边交通安全舆情',
|
||||
overview:
|
||||
'对校园周边交通安全话题进行三情分析,梳理家长群体情绪与诉求强度。',
|
||||
taskDirection: '张伟',
|
||||
taskLevel: 2,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '景区门票预约制讨论分析',
|
||||
overview:
|
||||
'分析门票预约制相关讨论的情绪分布,识别便利性与公平性两类核心关切。',
|
||||
taskDirection: '赵敏',
|
||||
taskLevel: 1,
|
||||
taskType: 2,
|
||||
},
|
||||
{
|
||||
taskName: '夜间经济噪音投诉情绪',
|
||||
overview:
|
||||
'对夜市、夜间演出引发的噪音投诉进行情绪分析,区分居民与商户的不同立场。',
|
||||
taskDirection: '陈昊',
|
||||
taskLevel: 1,
|
||||
taskType: 1,
|
||||
},
|
||||
],
|
||||
running: [
|
||||
{
|
||||
taskName: '台风预警信息传播情绪追踪',
|
||||
overview:
|
||||
'正在分析台风预警发布后的公众情绪变化,评估恐慌情绪与配合度的动态走势。',
|
||||
taskDirection: '孙强',
|
||||
taskLevel: 3,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '学区划分调整舆论情绪',
|
||||
overview:
|
||||
'多智能体协同追踪学区调整话题的情绪热度与对立观点,研判潜在升级风险。',
|
||||
taskDirection: '吴静',
|
||||
taskLevel: 2,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '网络热点事件情绪态势',
|
||||
overview:
|
||||
'正在对近期网络热点事件进行三情分析,识别情绪峰值节点与关键意见领袖。',
|
||||
taskDirection: '郑凯',
|
||||
taskLevel: 2,
|
||||
taskType: 3,
|
||||
},
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
taskName: '春运出行体验情绪复盘',
|
||||
overview:
|
||||
'已完成春运期间出行体验的情绪与倾向分析,沉淀满意度变化与高频抱怨主题。',
|
||||
taskDirection: '黄涛',
|
||||
taskLevel: 1,
|
||||
taskType: 3,
|
||||
},
|
||||
{
|
||||
taskName: '惠民政策落地情绪评估',
|
||||
overview:
|
||||
'已完成惠民政策落地后的公众情绪评估,输出正向反馈与改进诉求清单。',
|
||||
taskDirection: '宋佳',
|
||||
taskLevel: 2,
|
||||
taskType: 4,
|
||||
},
|
||||
{
|
||||
taskName: '城市内涝应对情绪分析',
|
||||
overview:
|
||||
'已完成强降雨内涝事件的三情分析,总结情绪演化曲线与有效安抚措施。',
|
||||
taskDirection: '高鹏',
|
||||
taskLevel: 2,
|
||||
taskType: 1,
|
||||
},
|
||||
{
|
||||
taskName: '景区限流措施满意度分析',
|
||||
overview:
|
||||
'已完成节假日景区限流措施的情绪分析,明确游客接受度与体验改进方向。',
|
||||
taskDirection: '何丽',
|
||||
taskLevel: 1,
|
||||
taskType: 2,
|
||||
},
|
||||
{
|
||||
taskName: '公共交通票价听证情绪',
|
||||
overview:
|
||||
'已完成票价听证会相关讨论的情绪与立场分析,输出分群体沟通建议。',
|
||||
taskDirection: '罗勇',
|
||||
taskLevel: 1,
|
||||
taskType: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const SEEDS: Record<MockDataset, Record<MockColumnType, MockTaskSeed[]>> = {
|
||||
task: TASK_SEEDS,
|
||||
sentiment: SENTIMENT_SEEDS,
|
||||
};
|
||||
|
||||
/** 给同一列里的卡片生成错落有致的时间,便于「按时间倒序」展示。 */
|
||||
function buildTimes(index: number): { createTime: string; updateTime: string } {
|
||||
const day = String(28 - index).padStart(2, '0');
|
||||
const hour = String(9 + (index % 8)).padStart(2, '0');
|
||||
return {
|
||||
createTime: `2026-05-${day} ${hour}:15:00`,
|
||||
updateTime: `2026-06-0${(index % 9) + 1} ${hour}:40:00`,
|
||||
};
|
||||
}
|
||||
|
||||
/** 数据集 → 列 → 完整任务列表(按 id 区分,避免两页 key 冲突)。 */
|
||||
function buildPool(dataset: MockDataset, column: MockColumnType): TaskListItem[] {
|
||||
const base = dataset === 'task' ? 10000 : 20000;
|
||||
const columnOffset = column === 'unanalyzed' ? 0 : column === 'running' ? 300 : 600;
|
||||
return SEEDS[dataset][column].map((seed, index) => {
|
||||
const { createTime, updateTime } = buildTimes(index);
|
||||
return makeTask(base + columnOffset + index, column, seed, createTime, updateTime);
|
||||
});
|
||||
}
|
||||
|
||||
interface MockCopTasksPayload {
|
||||
content?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
taskStatus?: number[];
|
||||
}
|
||||
|
||||
/** 根据请求里的 taskStatus 数组反推当前查询的是哪一列。 */
|
||||
function resolveColumn(taskStatus?: number[]): MockColumnType {
|
||||
const list = Array.isArray(taskStatus) ? taskStatus : [];
|
||||
if (list.includes(20)) return 'running';
|
||||
if (list.includes(0) || list.includes(10)) return 'unanalyzed';
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 `/task/cop-tasks` / `/three/cop-three-tasks` 接口的 body:
|
||||
* 按列返回分页后的假数据,支持按 `content` 对任务名/摘要做模糊过滤。
|
||||
*/
|
||||
function buildCopTasksBody(dataset: MockDataset, payload: MockCopTasksPayload) {
|
||||
const { content = '', pageNum = 1, pageSize = 10, taskStatus } = payload;
|
||||
const column = resolveColumn(taskStatus);
|
||||
|
||||
let pool = buildPool(dataset, column);
|
||||
const keyword = content.trim();
|
||||
if (keyword) {
|
||||
pool = pool.filter(
|
||||
(item) =>
|
||||
item.taskName.includes(keyword) || item.overview.includes(keyword),
|
||||
);
|
||||
}
|
||||
|
||||
const total = pool.length;
|
||||
const start = (pageNum - 1) * pageSize;
|
||||
const records = pool.slice(start, start + pageSize);
|
||||
|
||||
return { code: 0, message: 'ok', data: { records, total } };
|
||||
}
|
||||
|
||||
/** 命中本 mock 的接口路径 → 对应数据集。 */
|
||||
const MOCK_ROUTES: Array<{ match: string; dataset: MockDataset }> = [
|
||||
{ match: '/three/cop-three-tasks', dataset: 'sentiment' },
|
||||
{ match: '/task/cop-tasks', dataset: 'task' },
|
||||
];
|
||||
|
||||
/** 判断某个请求 url 是否应走假数据。 */
|
||||
export function matchTaskBoardMock(url?: string): MockDataset | null {
|
||||
if (!url) return null;
|
||||
const hit = MOCK_ROUTES.find((route) => url.includes(route.match));
|
||||
return hit ? hit.dataset : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时假数据 axios adapter:命中任务看板接口时直接返回 mock body,不发真实请求。
|
||||
* 仅用于演示,撤掉假数据时删除 request.ts 中对它的引用即可。
|
||||
*/
|
||||
export function taskBoardMockAdapter(config: {
|
||||
url?: string;
|
||||
data?: unknown;
|
||||
}): Promise<{ data: unknown; status: number; statusText: string; headers: Record<string, unknown>; config: unknown }> {
|
||||
const dataset = matchTaskBoardMock(config.url) ?? 'task';
|
||||
|
||||
let payload: MockCopTasksPayload = {};
|
||||
try {
|
||||
payload =
|
||||
typeof config.data === 'string'
|
||||
? JSON.parse(config.data)
|
||||
: ((config.data as MockCopTasksPayload) ?? {});
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
const body = buildCopTasksBody(dataset, payload);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 模拟网络延迟,让 Loading 态可见。
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: body,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config,
|
||||
});
|
||||
}, 280);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios from 'axios'
|
||||
// === 临时假数据(任务分析 / 三情分析看板):撤掉时删除本 import 及下方 request 拦截器中的对应块即可 ===
|
||||
import { matchTaskBoardMock, taskBoardMockAdapter } from '@/strategy-components/components/task-components/task-mock-data'
|
||||
// axios.interceptors
|
||||
const service = axios.create({
|
||||
// baseURL: '', // url = base url + request url
|
||||
@@ -31,6 +33,10 @@ service.interceptors.response.use(
|
||||
)
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// === 临时假数据:任务看板两个接口直接走本地 mock,不发真实请求 ===
|
||||
if (matchTaskBoardMock(config.url)) {
|
||||
config.adapter = taskBoardMockAdapter as any;
|
||||
}
|
||||
// let token: any = localStorage.getItem('token');
|
||||
config.headers['Authorization'] = `Bearer admin`;
|
||||
// if (token) {
|
||||
|
||||
@@ -7,10 +7,12 @@ one stream and never parses LangGraph internals.
|
||||
|
||||
Each fan-out agent runs as a pure **executing seat**: ``excluded_tools`` =
|
||||
``_EXCLUDED_TOOLS`` (``write_file`` / ``str_replace`` / ``agent_orchestration`` /
|
||||
``bash``) so a seat can neither write files (parallel writes would clash) nor
|
||||
dispatch / run scripts (a weak model fed "总控 / 业务链条" framing would otherwise
|
||||
mimic the controller and "派活"). Read-only tools + ``ask_clarification`` stay on,
|
||||
so the step bar and clarification cards still work.
|
||||
``bash`` / ``ask_clarification``) so a seat can neither write files (parallel
|
||||
writes would clash), dispatch / run scripts (a weak model fed "总控 / 业务链条"
|
||||
framing would otherwise mimic the controller and "派活"), nor pause to ask the
|
||||
user for help (this panel is an unattended parallel fan-out — a seat short on
|
||||
info makes a reasonable assumption and states it in its answer instead of
|
||||
interrupting). Read-only tools stay on, so the step bar still works.
|
||||
|
||||
Unified event protocol (one JSON object per ``data:`` frame)::
|
||||
|
||||
@@ -58,18 +60,21 @@ router = APIRouter(prefix="/api/parallel-agents", tags=["parallel-agents"])
|
||||
# Soft cap on parallel degree — protects remote model concurrency + sandbox.
|
||||
MAX_PARALLEL_AGENTS = 8
|
||||
|
||||
# 禁两类工具:①**写文件**(write_file / str_replace)—— 并行席位互相写文件会打架;
|
||||
# 禁三类工具:①**写文件**(write_file / str_replace)—— 并行席位互相写文件会打架;
|
||||
# ②**派活 / 脚本逃逸**(agent_orchestration / bash)—— 本面板里每个智能体只做自己那
|
||||
# 份分析交付,绝不该扮演「总控」去调度别人。弱模型(deepseek 等)拿到带「总控 / 业务
|
||||
# 链条」语气的任务后,会概率性地用 agent_orchestration 真去派活,或绕道 bash 跑脚本
|
||||
# 调内部接口模仿总控调度(bug:单步单智能体链条里子智能体输出「总控调度指令…」并用
|
||||
# glob / bash 翻找派活脚本)。物理禁掉这两类即可根治。其余只读工具(ls / read_file /
|
||||
# 检索)与 ask_clarification(协作卡片)保留:既不互相打架,又能正常出步骤条 / 要澄清。
|
||||
# glob / bash 翻找派活脚本)。③**用户协助**(ask_clarification)—— 本面板是「无人值守」
|
||||
# 的并行扇出,没有让单个席位中途打断、向用户要澄清的交互位;禁掉后席位遇到信息不足时
|
||||
# 自行做合理假设并把假设写进正文交付,而不是挂起等人。物理禁掉这三类即可根治。其余只读
|
||||
# 工具(ls / read_file / 检索)保留:既不互相打架,又能正常出步骤条。
|
||||
_EXCLUDED_TOOLS: list[str] = [
|
||||
"write_file", # 并行不互相写文件打架
|
||||
"str_replace",
|
||||
"agent_orchestration", # 子智能体不派活 / 不调度(防模仿总控)
|
||||
"bash", # 禁跑脚本(防绕道脚本调内部接口模仿派活)
|
||||
"ask_clarification", # 禁「用户协助」:并行无人值守,不向用户要澄清,自己合理假设并交付
|
||||
]
|
||||
|
||||
|
||||
@@ -276,6 +281,14 @@ def _build_task_message(task_name: str, brief: str) -> str:
|
||||
"调度对象 / 分配给『…』的任务 / agent_name」之类调度内容,严禁罗列或转派其他席位的任务,严禁调用"
|
||||
"任何派活 / 调度类工具,也不要用脚本(bash / python)去调用内部接口或翻找派活脚本。"
|
||||
)
|
||||
# 无人值守护栏:本轮并行执行没有让你中途向用户要澄清的交互位(已禁用用户协助)。遇到
|
||||
# 信息不足时,请基于已有信息做出**合理假设**并在正文中**注明你的假设**,照常给出交付,
|
||||
# 不要反问用户、不要把交付挂起等待补充。
|
||||
parts.append(
|
||||
"【无人值守】本轮为并行无人值守执行,没有与你来回澄清的环节。若信息不足,请自行做出"
|
||||
"**合理假设**并在交付中**明确写出你假设了什么**,然后照常完成分析;不要反问用户、不要"
|
||||
"要求补充信息后再继续,也不要把结论挂起等待确认。"
|
||||
)
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
|
||||
@@ -25,11 +25,23 @@ pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs} + dist/parallel-a
|
||||
|
||||
## 嵌入其它 Vue 项目
|
||||
|
||||
两种方式(详见 [docs/07-使用说明.md](./docs/07-使用说明.md)):
|
||||
|
||||
**A. 按包引入**(已 `build:lib`):
|
||||
|
||||
```ts
|
||||
import { ParallelAgentsPanel } from "parallel-agents-vue";
|
||||
import "parallel-agents-vue/style.css";
|
||||
```
|
||||
|
||||
**B. 直接拷源码**(适合要改源码的人):把整个 **`src/parallel-agents/` 文件夹**拷进你的项目 `src/` 下,
|
||||
样式由其 `index.ts` 自动引入,**无需 Tailwind / 任何 CSS 框架**:
|
||||
|
||||
```ts
|
||||
import { ParallelAgentsPanel } from "@/parallel-agents"; // 或 "./parallel-agents"
|
||||
```
|
||||
> 仅需 `vue` 与 `markdown-it` 两个运行依赖。
|
||||
|
||||
```vue
|
||||
<!-- 放进你自己的抽屉 / 弹窗里,面板会自适应容器(h-full w-full)。
|
||||
接口地址走配置文件默认(src/config.ts),登录只需把用户名传进来(免密自动登录)。 -->
|
||||
@@ -76,4 +88,5 @@ import "parallel-agents-vue/style.css";
|
||||
- `@saved="(task) => ..."` —— 保存成功(含新建/更新)后回传 `SavedTask`。
|
||||
- `@update:taskId="(id) => ..."` —— 当前任务 id 变化(保存 / 加载历史时)。
|
||||
|
||||
> 样式作用域全部包在根类 `.parallel-agents` 下,且**不引入 Tailwind preflight**,不会污染宿主全局样式。
|
||||
> 样式作用域全部包在根类 `.parallel-agents` 下,**不依赖 Tailwind**(布局类是面板自带的作用域工具类子集),
|
||||
> 不会污染宿主全局样式。
|
||||
@@ -84,15 +84,15 @@ pnpm build:lib
|
||||
|
||||
## PA-EMBED-06 · 不污染宿主全局样式(P0)
|
||||
|
||||
**目的**:核对**未引入 Tailwind preflight**(不重置宿主全局标签样式)。
|
||||
**目的**:核对面板**不依赖 Tailwind**、不重置宿主全局标签样式(无任何裸标签选择器 / reset)。
|
||||
|
||||
**操作步骤**
|
||||
1. 在一个有自己样式的宿主页里嵌入面板(页面有 `h1/p/button/ul` 等默认样式)。
|
||||
1. 在一个有自己样式、且**没装 Tailwind** 的宿主页里嵌入面板(页面有 `h1/p/button/ul` 等默认样式)。
|
||||
|
||||
**预期结果**
|
||||
- 嵌入后,宿主页面**自身**的 `h1/p/button/列表/边距` 等样式**不被改变**(没有被 Tailwind 的 reset 抹平)。
|
||||
- 面板内部样式正常(不依赖宿主提供 Tailwind)。
|
||||
- 类名都在 `.parallel-agents` 作用域下,不与宿主类名冲突。
|
||||
- 嵌入后,宿主页面**自身**的 `h1/p/button/列表/边距` 等样式**不被改变**(面板样式无全局 reset、无裸标签规则)。
|
||||
- 面板内部样式正常(**不需要宿主提供 Tailwind**;布局类是面板自带的作用域工具类子集)。
|
||||
- 所有规则都在 `.parallel-agents` 作用域下,不与宿主类名冲突。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
2. **样式**:`parallel-agents-vue/style.css`(一次性引入即可,样式作用域全在 `.parallel-agents` 下)
|
||||
|
||||
> 依赖说明:`vue ^3.4`(宿主提供,库不打包 vue);`markdown-it` 已打进库内,无需宿主安装。
|
||||
> **无需 Tailwind 或任何 CSS 框架**:布局样式已内置为 `parallel-agents.css` 末尾的「最小工具类子集」
|
||||
> (全部作用域在 `.parallel-agents` 下,不污染宿主)。宿主项目**不用**装 / 配 Tailwind。
|
||||
|
||||
---
|
||||
|
||||
@@ -41,21 +43,32 @@
|
||||
import "parallel-agents-vue/style.css";
|
||||
```
|
||||
|
||||
### 方式 B:直接拷贝源码(无需发包)
|
||||
### 方式 B:直接拷贝源码(无需发包,**推荐给需要改源码的人**)
|
||||
|
||||
把本项目 `src/` 下这些拷进宿主项目(保持相对结构),按相对路径引入 `ParallelAgentsPanel.vue` +
|
||||
`styles/parallel-agents.css` 即可:
|
||||
所有实现已收进**一个自包含文件夹** `src/parallel-agents/`。把**整个文件夹**拷进宿主项目的 `src/`
|
||||
下即可,内部全是相对路径引用、不依赖任何构建别名:
|
||||
|
||||
```
|
||||
src/
|
||||
components/ ParallelAgentsPanel.vue · MessageBubble.vue · MessageStepsCard.vue
|
||||
ParallelSeatStrip.vue · Avatar.vue · MarkdownContent.vue
|
||||
composables/ useParallelRun.ts
|
||||
lib/ avatar-constants.ts
|
||||
styles/ parallel-agents.css
|
||||
api.ts · config.ts · types.ts
|
||||
src/parallel-agents/ ← 整个文件夹拷走即用
|
||||
index.ts 入口:自动引样式 + 导出组件/类型
|
||||
ParallelAgentsPanel.vue 主组件
|
||||
ParallelSeatStrip.vue · MessageBubble.vue · MessageStepsCard.vue · StepIcon.vue
|
||||
ClarificationCard.vue · MarkdownContent.vue · Avatar.vue (子组件)
|
||||
useParallelRun.ts 并行运行编排
|
||||
api.ts · types.ts · config.ts · avatar-constants.ts · step-display.ts
|
||||
parallel-agents.css 作用域样式(深色 token + 语义类 + 工具类子集,无 Tailwind)
|
||||
README.md 本文件夹的速用说明
|
||||
```
|
||||
|
||||
引入(样式由 `index.ts` 自动引入,无需手动 import css):
|
||||
|
||||
```ts
|
||||
import { ParallelAgentsPanel } from "@/parallel-agents";
|
||||
// 或相对路径: import { ParallelAgentsPanel } from "./parallel-agents";
|
||||
```
|
||||
|
||||
宿主需有 `markdown-it` 依赖(`pnpm add markdown-it`),并保证构建支持 `.vue`(Vite/@vitejs/plugin-vue)。
|
||||
**不需要 Tailwind**。
|
||||
|
||||
---
|
||||
|
||||
@@ -446,6 +459,9 @@ function readOne(id: string) {
|
||||
```
|
||||
|
||||
`AgentResult = { agent_id, name, content, status }`,`content` 即该智能体模型返回的正文(Markdown)。
|
||||
`status` 取值:`pending | running | done | clarify | error`。**`clarify` = 该智能体在等用户协助
|
||||
(待确认),不是已完成**——宿主判断「是否全部完成」时应排除 `clarify`(例如
|
||||
`results.every(r => r.status === "done")`),不要把待协助当成 done。
|
||||
`defineExpose` 还暴露了 `reanalyze()` / `start()` / `save()` / `cancel()` / `loadTask(id)` 等方法及
|
||||
`running` / `saving` / `hasResults` / `canStart` / `history` 等状态,完整清单见 [§六·二](#六二暴露的方法与状态模板-ref)。
|
||||
|
||||
@@ -503,7 +519,10 @@ taskId.value = "task-001"; // 改了之后面板自动回显 task-
|
||||
防子智能体模仿总控调度);检索、查看、`ask_clarification` 等照常 —— 所以**步骤条 / 协作卡片正常
|
||||
显示**,每个智能体只做自己那份分析。
|
||||
- 主区每次只显示**一个**席位的消息列表;**点席位头像切换**要看哪个(单选)。
|
||||
- 顶部席位状态条显示每个智能体的进行中 / 完成 / 出错。
|
||||
- 顶部席位状态条显示每个智能体的进行中 / 完成 / **待协助** / 出错;待协助数量会单独标「N 待协助」。
|
||||
- **用户协助(澄清)**:智能体要澄清时该席位变「待协助」(不是完成),气泡里弹协作卡片。多个智能体
|
||||
同时待协助时**可分别点协助、互不阻塞**(协助 A 不会禁用 B/C 的卡片);提交回答后该席位自动带上
|
||||
补充重跑。
|
||||
- **任务名 / 简介 / 模型 / 按钮 / 历史下拉全部在宿主侧**:模型空 = `model` prop / 配置 / 后端回退;
|
||||
模型作用于**下一次**`reanalyze()`。
|
||||
- `panel.save()` 存档;`panel.loadTask(id)` 回显;`panel.reanalyze()` 清空重跑;`panel.cancel()` 停止。
|
||||
@@ -514,7 +533,8 @@ taskId.value = "task-001"; // 改了之后面板自动回显 task-
|
||||
|
||||
## 九、注意事项
|
||||
|
||||
- **不会污染宿主全局样式**:库未引入 Tailwind preflight,类名都在 `.parallel-agents` 作用域内。
|
||||
- **不会污染宿主全局样式**:**不依赖 Tailwind**,布局类已内置为作用域工具类子集,所有规则都在
|
||||
`.parallel-agents` 下,绝不外泄到宿主页面。
|
||||
- **深色自带成色**:面板始终深蓝深色,不随宿主明暗切换(与圆桌 Step2 观感一致)。
|
||||
- **跨域**:若宿主与后端不同源,后端需允许该来源的 CORS + 带 `credentials`;登录端点公共且 CSRF 豁免,
|
||||
业务请求用 Bearer 头已绕过 CSRF。
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| 框架 | Vue 3(`<script setup>` + Composition API) |
|
||||
| 构建 | Vite 6(dev app + `build:lib` 组件库双产物) |
|
||||
| 语言 | TypeScript(`strict` + `noUnusedLocals/Parameters`) |
|
||||
| 样式 | TailwindCSS v4(**仅 utilities,不引 preflight**)+ 作用域 `.pa-*` token |
|
||||
| 样式 | **零 CSS 框架**:作用域 `.pa-*` token + 内置「最小工具类子集」(不依赖 Tailwind) |
|
||||
| Markdown | `markdown-it`(打进库,宿主无需安装) |
|
||||
| peer dep | `vue ^3.4`(宿主提供,库不打包 vue) |
|
||||
|
||||
@@ -31,31 +31,33 @@
|
||||
|
||||
## 3. 目录结构与各文件职责
|
||||
|
||||
实现全部收在**一个自包含文件夹** `src/parallel-agents/`(内部相对路径引用、不依赖构建别名,
|
||||
整个文件夹可直接拷给宿主用)。`src/` 顶层只剩 demo 与库入口。
|
||||
|
||||
```
|
||||
src/
|
||||
├─ index.ts 库入口:导出 ParallelAgentsPanel + install 插件 + 类型;import 样式
|
||||
├─ index.ts 库入口:从 ./parallel-agents 再导出 + install 插件
|
||||
├─ main.ts / App.vue dev demo(仅本地调试用,不进库)
|
||||
├─ config.ts 配置:DEFAULT_BASE_URL / DEFAULT_MODEL / TOKEN_PATH
|
||||
├─ types.ts 共享类型:Dialogue / SeatView / DialogueStep / ClarificationPayload
|
||||
│ / AgentInput / AgentResult / SavedTask / StreamEvent(事件协议)
|
||||
├─ api.ts 后端访问层:鉴权(取 token+缓存+401 重登) / SSE 流 / 任务 CRUD / 模型列表
|
||||
├─ composables/
|
||||
│ └─ useParallelRun.ts 运行编排(前端态核心):消费合并 SSE → 维护 dialogues/seats;
|
||||
│ start / cancel / hydrate(回显) / reset / rerunAgent(单席位重跑)
|
||||
├─ lib/
|
||||
│ ├─ avatar-constants.ts 头像配色 + getAvatarTypeFor(轮转)
|
||||
│ └─ step-display.ts 步骤条:getStepDisplay(图标+中文文案) / getStepArgs(参数子节点)
|
||||
├─ styles/
|
||||
│ └─ parallel-agents.css 作用域深色 token(--pa-*) + .pa-* 语义类 + Markdown 样式(无 preflight)
|
||||
└─ components/
|
||||
├─ ParallelAgentsPanel.vue 主组件:输入区/操作区/模型切换/历史/席位条/会话区 + 全部状态编排
|
||||
├─ ParallelSeatStrip.vue 席位状态条(头像 + running/done/error 角标,点击切换单显)
|
||||
└─ parallel-agents/ ← 自包含组件文件夹(拷走即用)
|
||||
├─ index.ts 文件夹入口:import 样式 + 导出组件/类型
|
||||
├─ config.ts 配置:DEFAULT_BASE_URL / DEFAULT_MODEL / TOKEN_PATH
|
||||
├─ types.ts 共享类型:Dialogue / SeatView / SeatStatus / DialogueStep /
|
||||
│ ClarificationPayload / AgentInput / AgentResult / SavedTask / StreamEvent
|
||||
├─ api.ts 后端访问层:鉴权(取 token+缓存+401 重登) / SSE 流 / 任务 CRUD / 模型列表
|
||||
├─ useParallelRun.ts 运行编排(前端态核心):消费合并 SSE → 维护 dialogues/seats/busy;
|
||||
│ start / cancel / hydrate(回显) / reset / rerunAgent(单席位重跑,可并发协助)
|
||||
├─ avatar-constants.ts 头像配色 + getAvatarTypeFor(轮转)
|
||||
├─ step-display.ts 步骤条:getStepDisplay(图标+中文文案) / getStepArgs(参数子节点)
|
||||
├─ parallel-agents.css 作用域深色 token(--pa-*) + .pa-* 语义类 + Markdown 样式 + 工具类子集(无 Tailwind)
|
||||
├─ ParallelAgentsPanel.vue 主组件:席位条/会话区 + 全部状态编排(表单/按钮在宿主侧)
|
||||
├─ ParallelSeatStrip.vue 席位状态条(头像 + running/done/clarify/error 角标,点击切换单显)
|
||||
├─ MessageBubble.vue 单条气泡:头像/发言人/徽标/时间/步骤条/正文/光标/协作卡片
|
||||
├─ MessageStepsCard.vue 步骤条(ChainOfThought 结构,1:1 对齐 Step2)
|
||||
├─ StepIcon.vue 步骤图标(lucide 风格 inline SVG,按 IconKey)
|
||||
├─ ClarificationCard.vue 协作 / 澄清卡片(1:1 对齐圆桌第一步 IntentClarificationCard)
|
||||
├─ MarkdownContent.vue markdown-it 渲染
|
||||
└─ Avatar.vue 机器人 / 真人 SVG 头像
|
||||
├─ Avatar.vue 机器人 / 真人 SVG 头像
|
||||
└─ README.md 文件夹速用说明
|
||||
```
|
||||
|
||||
---
|
||||
@@ -98,7 +100,7 @@ panel emit "completed"(AgentResult[]);可点「保存」(按 taskId upsert) /
|
||||
| `agent_start` | `agent_id` | 该席位置 running |
|
||||
| `delta` | `agent_id, text` | 追加正文(节流) |
|
||||
| `step` | `agent_id, key(=`tool_calling:<tool>`), label, args` | 步骤条加一步(含完整 args) |
|
||||
| `agent_clarification` | `agent_id, content, question, clarification_type, clarification_context, options, allow_*` | 设 `dialogue.clarification` → 卡片 |
|
||||
| `agent_clarification`〔已禁用〕 | `agent_id, content, question, clarification_type, clarification_context, options, allow_*` | 设 `dialogue.clarification` → 卡片;席位置 **clarify**(待协助,非 done)。**注**:后端已把 `ask_clarification` 加入 `_EXCLUDED_TOOLS`,本面板**无人值守**不再发该事件;前端协作卡片相关代码保留(兜底,正常不触发) |
|
||||
| `agent_done` | `agent_id, content` | 收口,正文以终态为准 |
|
||||
| `agent_error` | `agent_id, message` | 该席位置 error |
|
||||
| `all_done` | — | 全部结束 |
|
||||
@@ -106,9 +108,10 @@ panel emit "completed"(AgentResult[]);可点「保存」(按 taskId upsert) /
|
||||
**核心数据模型**(`types.ts`):
|
||||
- `Dialogue { id, agentId, sender, avatarType, content, time, confidence, streaming?, steps?, clarification? }`
|
||||
- `DialogueStep { id, key, label, ts, args? }`(`key` 形如 `tool_calling:web_search`)
|
||||
- `SeatView { agentId, name, avatarType, status: pending|running|done|error, bubbleId }`
|
||||
- `SeatView { agentId, name, avatarType, status: SeatStatus, bubbleId }`
|
||||
- `SeatStatus = pending | running | done | clarify | error`(**`clarify` = 待用户协助,非完成**)
|
||||
- `ClarificationPayload { question, clarificationType, context, options, allowCustom, allowMultiple }`
|
||||
- `AgentResult { agent_id, name, content, status }`(宿主读取用)
|
||||
- `AgentResult { agent_id, name, content, status }`(宿主读取用;`status` 同 `SeatStatus`)
|
||||
|
||||
> 改协议时:`types.ts` 的 `StreamEvent` 与后端 `parallel_agents.py` 的事件 dict **必须同步**。
|
||||
|
||||
@@ -157,23 +160,30 @@ ParallelAgentsPanel
|
||||
| 模型列表 | `GET /api/models` → `{models:[{name, display_name}]}` |
|
||||
|
||||
**后端运行策略(重要)**:每个并行席位是「执行席位」,不是总控。
|
||||
- **禁用工具** `_EXCLUDED_TOOLS = [write_file, str_replace, agent_orchestration, bash]`:①禁写文件(并行
|
||||
不互相打架);②禁 `agent_orchestration`/`bash`(防弱模型模仿总控**派活**或绕道脚本逃逸)。
|
||||
- **保留** 只读 / 检索工具 + `ask_clarification` → 步骤条、协作卡片正常。
|
||||
- 任务消息带**角色边界护栏**(「执行席位、不要模仿总控调度 / 派活」)。
|
||||
- **禁用工具** `_EXCLUDED_TOOLS = [write_file, str_replace, agent_orchestration, bash, ask_clarification]`:
|
||||
①禁写文件(并行不互相打架);②禁 `agent_orchestration`/`bash`(防弱模型模仿总控**派活**或绕道脚本逃逸);
|
||||
③禁 `ask_clarification`(**禁用用户协助**:本面板是并行**无人值守**扇出,没有让单席位中途打断、向用户
|
||||
要澄清的交互位 —— 席位信息不足时自行做**合理假设**并写进正文交付,而非挂起等人)。
|
||||
- **保留** 只读 / 检索工具 → 步骤条正常。
|
||||
- 任务消息带**角色边界护栏**(「执行席位、不要模仿总控调度 / 派活」)+ **无人值守护栏**(「信息不足时
|
||||
自行合理假设并注明,不要反问用户 / 不要挂起等待补充」)。
|
||||
- 步骤 `args` 通过累积 `tool_call_chunks` + 终态全量 `tool_calls` 补全(流式期间空 args 不发,防钉死)。
|
||||
- `ask_clarification` 从终态 messages 抽成结构化 `agent_clarification` 事件。
|
||||
- `ask_clarification` 已被禁用,正常不再产生 `agent_clarification` 事件;`_clarification_from_messages`
|
||||
仅作终态兜底(工具被禁后恒返回 `None`),不影响其他业务路由(圆桌 / 普通问答的澄清照常)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 样式系统(嵌入安全)
|
||||
|
||||
`styles/parallel-agents.css`:
|
||||
- **不引 Tailwind preflight**(`@import "tailwindcss/theme.css"` + `utilities.css`),避免重置宿主全局样式。
|
||||
`parallel-agents.css`(**零依赖、不再用 Tailwind**):
|
||||
- 颜色全用作用域内的 `--pa-*` 深蓝 token(取自《深蓝 UI 规范》,与圆桌 Step2 深色一致),全部包在
|
||||
根类 `.parallel-agents` 下,**不污染宿主**。
|
||||
- 语义类 `.pa-card / .pa-surface / .pa-border / .pa-text(-muted/-primary) / .pa-input / .pa-btn-* /
|
||||
.pa-badge / .pa-md(Markdown)` —— 组件**上色用这些类**,布局用 Tailwind 工具类。
|
||||
.pa-badge / .pa-dot-*(席位角标,含 .pa-dot-clarify) / .pa-md(Markdown)` —— 组件**上色用这些类**。
|
||||
- 布局用文件**末尾的「最小工具类子集」**(`flex` / `gap-*` / `rounded-*` / `text-[11px]` /
|
||||
`lg:grid-cols-2` 等,模板用到的逐条手写成纯 CSS)。它包在 `@layer pa-utilities` 里,所以无层级的
|
||||
`.pa-*` 语义类在同属性冲突时**仍压过**工具类(复刻原先 Tailwind-utilities-在-layer 的优先级)。
|
||||
**维护**:模板新增布局类时,在该子集补一条对应规则即可(或写组件 scoped `<style>`)。
|
||||
- 面板**自带深色成色**,不随宿主明暗变化;根元素 `h-full w-full flex flex-col min-h-0` 自适应容器。
|
||||
|
||||
---
|
||||
@@ -181,7 +191,7 @@ ParallelAgentsPanel
|
||||
## 10. 步骤条 / 协作卡片(与圆桌的对应)
|
||||
|
||||
- **步骤条** `MessageStepsCard.vue` ← 圆桌 `MessageStepsCard.tsx` + `ChainOfThought`:图标列(带竖向连接线)
|
||||
+ 中文文案 + 时间 + 参数子节点。文案/图标映射在 `lib/step-display.ts`(`getStepDisplay`),参数子节点
|
||||
+ 中文文案 + 时间 + 参数子节点。文案/图标映射在 `step-display.ts`(`getStepDisplay`),参数子节点
|
||||
在 `getStepArgs`(chip / code / text / kv)。加新工具图标:在 `step-display.ts` 的 switch + `StepIcon.vue`
|
||||
各加一条。
|
||||
- **协作卡片** `ClarificationCard.vue` ← 圆桌第一步 `IntentClarificationCard.tsx`:标题/类型标签/背景/选项
|
||||
@@ -208,8 +218,9 @@ pnpm build:lib # 组件库:dist/parallel-agents-vue.{js,umd.cjs,css}
|
||||
| 需求 | 改哪里 |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| 新增一个 SSE 事件类型 | `types.ts` 的 `StreamEvent` + `useParallelRun.handleEvent` + 后端 `parallel_agents.py` |
|
||||
| 步骤条支持新工具的图标/文案 | `lib/step-display.ts`(`getStepDisplay`/`getStepArgs`)+ `StepIcon.vue` |
|
||||
| 改深色配色 / 加浅色主题 | `styles/parallel-agents.css` 的 `--pa-*`(浅色可在 `.parallel-agents--light` 下覆盖) |
|
||||
| 步骤条支持新工具的图标/文案 | `step-display.ts`(`getStepDisplay`/`getStepArgs`)+ `StepIcon.vue` |
|
||||
| 改深色配色 / 加浅色主题 | `parallel-agents.css` 的 `--pa-*`(浅色可在 `.parallel-agents--light` 下覆盖) |
|
||||
| 改布局类 / 加新工具类 | `parallel-agents.css` 末尾的「工具类子集」补规则,或组件 scoped `<style>`(不要装 Tailwind) |
|
||||
| 改默认后端地址 / 模型 | `config.ts`(`DEFAULT_BASE_URL` / `DEFAULT_MODEL`) |
|
||||
| 多席位并排显示(取消单选) | `ParallelAgentsPanel.vue` 的 `selected` / `visibleSeats` / `toggleSeat` 逻辑 |
|
||||
| 宿主自定义存储(不走后端) | 用 `@completed` + `getResults()` 取数据,自行存;不调用 `createTask/updateTask` |
|
||||
@@ -221,6 +232,9 @@ pnpm build:lib # 组件库:dist/parallel-agents-vue.{js,umd.cjs,css}
|
||||
|
||||
- **agent_id 必须真实**(后端注册过的 id),只填名字不行;可用 id 见 [06-可用智能体清单.md](./06-可用智能体清单.md)。
|
||||
- 并行席位**禁派活/脚本/写文件**:本面板里每个智能体只做自己那份分析,产出走正文。
|
||||
- 协作卡片回答是**重跑单个智能体**(新线程 + 把回答拼进任务),不是 LangGraph 原地 resume。
|
||||
- **已禁用「用户协助」**:并行无人值守,后端把 `ask_clarification` 加入 `_EXCLUDED_TOOLS`,席位不再向用户要
|
||||
澄清(信息不足时自行合理假设并在正文注明)。前端 `ClarificationCard.vue` / `onClarify` / `rerunAgent`
|
||||
等协作链路代码保留作兜底,正常不触发;**仅影响本面板**,圆桌 / 普通问答的澄清不受影响。
|
||||
- 协作卡片回答是**重跑单个智能体**(新线程 + 把回答拼进任务),不是 LangGraph 原地 resume(当前已禁用,仅兜底保留)。
|
||||
- token 取自公共端点(无门槛旁路)—— 适合内网;若网关要暴露公网,建议给取 token 端点加共享密钥校验。
|
||||
- 跨域:宿主与后端不同源时,后端需放开该来源 CORS + `credentials`。
|
||||
@@ -29,10 +29,8 @@
|
||||
"markdown-it": "^14.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.0.0",
|
||||
"vue": "^3.5.0",
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* 「重新分析 / 保存 / 停止」等按钮由宿主渲染,通过模板 ref 调用面板 expose 出的方法。
|
||||
*/
|
||||
import { reactive, ref } from "vue";
|
||||
import ParallelAgentsPanel from "./components/ParallelAgentsPanel.vue";
|
||||
import type { AgentInput, AgentResult } from "./types";
|
||||
import { ParallelAgentsPanel } from "./parallel-agents";
|
||||
import type { AgentInput, AgentResult } from "./parallel-agents";
|
||||
|
||||
import { DEFAULT_BASE_URL } from "./config";
|
||||
import { DEFAULT_BASE_URL } from "./parallel-agents/config";
|
||||
|
||||
const cfg = reactive({
|
||||
baseUrl: localStorage.getItem("pa.baseUrl") || DEFAULT_BASE_URL,
|
||||
|
||||
@@ -3,13 +3,23 @@
|
||||
* import { ParallelAgentsPanel } from "parallel-agents-vue";
|
||||
* import "parallel-agents-vue/style.css";
|
||||
* 或全局注册:app.use(ParallelAgentsVue)。
|
||||
*
|
||||
* 实现全部收在自包含的 `./parallel-agents/` 文件夹里(含样式,零 Tailwind 依赖)。需要直接拷
|
||||
* 源码的用户,整个 parallel-agents/ 文件夹拷走即用,见该目录下 README.md。
|
||||
*/
|
||||
import "./styles/parallel-agents.css";
|
||||
import type { App } from "vue";
|
||||
import ParallelAgentsPanel from "./components/ParallelAgentsPanel.vue";
|
||||
import { ParallelAgentsPanel } from "./parallel-agents";
|
||||
|
||||
export { ParallelAgentsPanel };
|
||||
export type { AgentInput, AgentResult, Dialogue, SavedTask, SeatView, StreamEvent } from "./types";
|
||||
export type {
|
||||
AgentInput,
|
||||
AgentResult,
|
||||
Dialogue,
|
||||
SavedTask,
|
||||
SeatStatus,
|
||||
SeatView,
|
||||
StreamEvent,
|
||||
} from "./parallel-agents";
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./styles/parallel-agents.css";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { avatarColor, isHuman } from "@/lib/avatar-constants";
|
||||
import { avatarColor, isHuman } from "./avatar-constants";
|
||||
|
||||
const props = withDefaults(defineProps<{ type: string; size?: number }>(), { size: 36 });
|
||||
const color = computed(() => avatarColor(props.type));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 自定义输入 + 发送按钮。提交时回传格式化好的回答文本。
|
||||
*/
|
||||
import { computed, reactive } from "vue";
|
||||
import type { ClarificationPayload, ClarOption } from "@/types";
|
||||
import type { ClarificationPayload, ClarOption } from "./types";
|
||||
|
||||
const props = defineProps<{ payload: ClarificationPayload; submitting?: boolean }>();
|
||||
const emit = defineEmits<{ (e: "submit", answer: string): void }>();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/** 单条消息气泡(对齐 Step2 MessageBubble + Step2Message 结构):头像 / 发言人 / 状态徽标 /
|
||||
* 时间 / 步骤条 / Markdown 正文 / 流式光标 / loading 占位。 */
|
||||
import { computed } from "vue";
|
||||
import type { Dialogue } from "@/types";
|
||||
import type { Dialogue } from "./types";
|
||||
import Avatar from "./Avatar.vue";
|
||||
import MessageStepsCard from "./MessageStepsCard.vue";
|
||||
import MarkdownContent from "./MarkdownContent.vue";
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* slate 边框卡 + 每步「图标列(带竖向连接线) + 中文文案 + 时间 + 参数子节点」。
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import type { DialogueStep } from "@/types";
|
||||
import { getStepArgs, getStepDisplay } from "@/lib/step-display";
|
||||
import type { DialogueStep } from "./types";
|
||||
import { getStepArgs, getStepDisplay } from "./step-display";
|
||||
import StepIcon from "./StepIcon.vue";
|
||||
|
||||
const props = defineProps<{ steps: DialogueStep[]; streaming?: boolean }>();
|
||||
|
||||
@@ -12,16 +12,16 @@
|
||||
* 自适应容器:根元素 `h-full w-full flex flex-col min-h-0`,宿主直接塞进抽屉 / 弹窗即可。
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import type { AgentInput, AgentResult, Dialogue, SavedTask, SeatView } from "@/types";
|
||||
import { useParallelRun } from "@/composables/useParallelRun";
|
||||
import type { AgentInput, AgentResult, Dialogue, SavedTask, SeatView } from "./types";
|
||||
import { useParallelRun } from "./useParallelRun";
|
||||
import {
|
||||
createTask,
|
||||
ensureTask,
|
||||
listTasks,
|
||||
updateTask,
|
||||
type ApiConfig,
|
||||
} from "@/api";
|
||||
import { DEFAULT_BASE_URL } from "@/config";
|
||||
} from "./api";
|
||||
import { DEFAULT_BASE_URL } from "./config";
|
||||
import ParallelSeatStrip from "./ParallelSeatStrip.vue";
|
||||
import MessageBubble from "./MessageBubble.vue";
|
||||
|
||||
@@ -77,7 +77,7 @@ const run = useParallelRun({
|
||||
if (r) emit("agent-completed", r);
|
||||
},
|
||||
});
|
||||
const { dialogues, seats, running } = run;
|
||||
const { dialogues, seats, running, busy } = run;
|
||||
|
||||
// 任务名 / 简介 / 模型全部由宿主 props 提供(本组件不再持有可编辑表单)。
|
||||
const taskName = computed(() => props.taskName ?? "");
|
||||
@@ -339,7 +339,7 @@ onMounted(async () => {
|
||||
<MessageBubble
|
||||
v-if="dialogueById.get(seat.bubbleId)"
|
||||
:msg="dialogueById.get(seat.bubbleId)!"
|
||||
:submitting="running"
|
||||
:submitting="running || busy.has(seat.agentId)"
|
||||
@clarify="onClarify"
|
||||
/>
|
||||
<div v-else class="py-6 text-center text-xs pa-text-muted">
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
/** 并行席位状态条(对齐 Step2 ParallelSeatStrip):席位头像 + running/done/error 角标,
|
||||
* 点击可切换主区显示哪两列。 */
|
||||
import { computed } from "vue";
|
||||
import type { SeatView } from "@/types";
|
||||
import type { SeatView } from "./types";
|
||||
import Avatar from "./Avatar.vue";
|
||||
|
||||
const props = defineProps<{ seats: SeatView[]; selected: string[] }>();
|
||||
const emit = defineEmits<{ (e: "toggle", agentId: string): void }>();
|
||||
|
||||
const runningCount = computed(() => props.seats.filter((s) => s.status === "running").length);
|
||||
const clarifyCount = computed(() => props.seats.filter((s) => s.status === "clarify").length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,6 +17,7 @@ const runningCount = computed(() => props.seats.filter((s) => s.status === "runn
|
||||
<span class="shrink-0 text-[11px] font-bold pa-text-muted">
|
||||
并行 {{ seats.length }} 席
|
||||
<span v-if="runningCount > 0" class="pa-text-primary">({{ runningCount }} 进行中)</span>
|
||||
<span v-if="clarifyCount > 0" style="color: var(--pa-amber)">({{ clarifyCount }} 待协助)</span>
|
||||
<span v-if="seats.length > 1" class="font-normal pa-text-muted">· 点头像切换显示</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -46,12 +48,14 @@ const runningCount = computed(() => props.seats.filter((s) => s.status === "runn
|
||||
:class="{
|
||||
'pa-dot-running': seat.status === 'running',
|
||||
'pa-dot-done': seat.status === 'done',
|
||||
'pa-dot-clarify': seat.status === 'clarify',
|
||||
'pa-dot-error': seat.status === 'error',
|
||||
}"
|
||||
:style="seat.status === 'pending' ? 'background: var(--pa-border)' : ''"
|
||||
>
|
||||
<span v-if="seat.status === 'running'" class="pa-spin">◜</span>
|
||||
<span v-else-if="seat.status === 'done'">✓</span>
|
||||
<span v-else-if="seat.status === 'clarify'">?</span>
|
||||
<span v-else-if="seat.status === 'error'">✕</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# 并行多智能体面板(parallel-agents)
|
||||
|
||||
一个**自包含**的 Vue 3 可嵌入组件:输入任务名 + 简介,并行调用多个智能体,深色消息列表 +
|
||||
步骤条 + 协作卡片 + 保存回显 + 重新分析。
|
||||
|
||||
## 怎么用(复制即用)
|
||||
|
||||
把**整个 `parallel-agents/` 文件夹**拷到你项目的 `src/` 下,然后:
|
||||
|
||||
```ts
|
||||
import { ParallelAgentsPanel } from "@/parallel-agents";
|
||||
// 或: import { ParallelAgentsPanel } from "./parallel-agents";
|
||||
```
|
||||
|
||||
```vue
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
:base-url="'http://localhost:8001'"
|
||||
:username="'demo'"
|
||||
:task-id="taskId || undefined"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
:agents="agents"
|
||||
@agent-completed="onAgentCompleted"
|
||||
@completed="onCompleted"
|
||||
/>
|
||||
```
|
||||
|
||||
样式由入口 `index.ts` 自动 `import "./parallel-agents.css"`,**无需** Tailwind / 任何 CSS 框架。
|
||||
若你不走 `index.ts` 而是直接 import 组件,请自行引一次该 CSS。
|
||||
|
||||
## 依赖
|
||||
|
||||
- `vue` ^3.4
|
||||
- `markdown-it` ^14(正文 Markdown 渲染)—— `pnpm add markdown-it`
|
||||
|
||||
> 不再依赖 Tailwind:布局类已内置为 `parallel-agents.css` 末尾的「最小工具类子集」,全部作用域
|
||||
> 在根类 `.parallel-agents` 下,绝不外泄污染宿主页面。
|
||||
|
||||
## 文件结构(都在本文件夹内,可随意改源码)
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `ParallelAgentsPanel.vue` | 主组件(席位条 + 并行会话,暴露 start/save/cancel 等方法) |
|
||||
| `ParallelSeatStrip.vue` | 席位状态条(running / done / **clarify 待协助** / error) |
|
||||
| `MessageBubble.vue` | 单条消息气泡(头像 / 步骤条 / 正文 / 协作卡片) |
|
||||
| `MessageStepsCard.vue` · `StepIcon.vue` | 执行步骤条 / 思维链卡 + 图标 |
|
||||
| `ClarificationCard.vue` | 协作 / 澄清卡片(用户协助) |
|
||||
| `MarkdownContent.vue` | Markdown 正文渲染(markdown-it) |
|
||||
| `Avatar.vue` | 头像(机器人 / 真人 SVG) |
|
||||
| `useParallelRun.ts` | 并行运行编排(消费合并 SSE、按席位路由、待协助/并发协助) |
|
||||
| `api.ts` | 后端访问层(鉴权 + 扇出 SSE + 任务 CRUD) |
|
||||
| `types.ts` · `config.ts` · `avatar-constants.ts` · `step-display.ts` | 类型 / 配置 / 头像配色 / 步骤文案 |
|
||||
| `parallel-agents.css` | 作用域样式(深蓝深色 token + 语义类 + 工具类子集) |
|
||||
|
||||
## 关键状态
|
||||
|
||||
席位/结果状态 `SeatStatus`:`pending | running | done | clarify | error`。其中 **`clarify`** 表示
|
||||
该智能体在等用户协助(待确认),**不是已完成**——宿主据此判断「未完成」,多个智能体同时待协助
|
||||
时可分别协助、互不阻塞。
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconKey } from "@/lib/step-display";
|
||||
import type { IconKey } from "./step-display";
|
||||
defineProps<{ name: IconKey; size?: number }>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 并行多智能体面板 —— 自包含文件夹入口。
|
||||
*
|
||||
* 使用方式(把整个 parallel-agents/ 文件夹拷进你的项目 src 下即可):
|
||||
*
|
||||
* import { ParallelAgentsPanel } from "@/parallel-agents";
|
||||
* // 或相对路径: import { ParallelAgentsPanel } from "./parallel-agents";
|
||||
*
|
||||
* 样式会被这里自动引入(零依赖、作用域在 .parallel-agents 下,不污染宿主,无需 Tailwind)。
|
||||
* 仅依赖 vue 与 markdown-it(正文 Markdown 渲染)。详见同目录 README.md。
|
||||
*/
|
||||
import "./parallel-agents.css";
|
||||
|
||||
export { default as ParallelAgentsPanel } from "./ParallelAgentsPanel.vue";
|
||||
export type {
|
||||
AgentInput,
|
||||
AgentResult,
|
||||
ClarificationPayload,
|
||||
ClarOption,
|
||||
Dialogue,
|
||||
DialogueStep,
|
||||
SavedTask,
|
||||
SeatStatus,
|
||||
SeatView,
|
||||
StreamEvent,
|
||||
} from "./types";
|
||||
export type { ApiConfig } from "./api";
|
||||
@@ -1,13 +1,11 @@
|
||||
/* 并行多智能体面板样式。
|
||||
/* 并行多智能体面板样式 —— 零依赖、不污染宿主。
|
||||
*
|
||||
* 不再依赖 Tailwind:本文件自带一份「最小布局工具类子集」(文件末尾),全部作用域在根类
|
||||
* `.parallel-agents` 下,绝不外泄到宿主页面。其它项目把整个 parallel-agents/ 文件夹拷走、
|
||||
* 只 `import "./parallel-agents.css"` 即可,无需安装或配置 Tailwind。
|
||||
*
|
||||
* 嵌入安全:只引 Tailwind 的 theme + utilities(**不引 preflight**,避免重置宿主全局样式)。
|
||||
* 颜色用作用域内的 `--pa-*` 深蓝 token(取自《深蓝 UI 规范》ZS-004 / ZS-002,与圆桌 Step2
|
||||
* 深色一致),全部包在根类 `.parallel-agents` 下,绝不污染宿主。布局用 Tailwind 工具类,
|
||||
* 主题色用下面的 `.pa-*` 语义类。 */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
* 深色一致)。语义颜色用 `.pa-*` 类,布局用末尾的工具类子集。 */
|
||||
|
||||
/* ── 作用域 token(深色深蓝,自带成色,不依赖宿主主题)─────────────────────── */
|
||||
.parallel-agents {
|
||||
@@ -52,7 +50,7 @@
|
||||
background: color-mix(in oklch, var(--pa-primary) 60%, var(--pa-border));
|
||||
}
|
||||
|
||||
/* ── 语义颜色类(组件用这些上色,布局仍用 Tailwind 工具类)──────────────────── */
|
||||
/* ── 语义颜色类(组件用这些上色,布局用文件末尾的工具类子集)──────────────────── */
|
||||
.parallel-agents .pa-bg {
|
||||
background-color: var(--pa-bg);
|
||||
}
|
||||
@@ -149,6 +147,10 @@
|
||||
background-color: var(--pa-rose);
|
||||
color: #fff;
|
||||
}
|
||||
.parallel-agents .pa-dot-clarify {
|
||||
background-color: var(--pa-amber);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 流式光标 + loading 点 */
|
||||
.parallel-agents .pa-cursor {
|
||||
@@ -280,3 +282,175 @@
|
||||
.parallel-agents .pa-md th {
|
||||
background: var(--pa-muted);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* 最小布局工具类子集(替代 Tailwind)
|
||||
* 只实现本组件模板用到的类,全部作用域在 .parallel-agents 下,不影响宿主。
|
||||
* 维护提示:模板里新增布局类时,在此处补一条对应规则即可(或直接写 scoped <style>)。
|
||||
*
|
||||
* 放进 @layer:让无层级的 `.pa-*` 语义类(颜色/按钮/徽标)在同名属性冲突时仍**压过**这里的
|
||||
* 工具类,复刻原先 Tailwind utilities 在 @layer、pa-* 无层级时的优先级(如 pa-btn-primary
|
||||
* 的 8px 圆角压过 rounded-xl)。
|
||||
* ════════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
@layer pa-utilities {
|
||||
|
||||
/* display */
|
||||
.parallel-agents .flex { display: flex; }
|
||||
.parallel-agents .inline-flex { display: inline-flex; }
|
||||
.parallel-agents .inline-block { display: inline-block; }
|
||||
.parallel-agents .block { display: block; }
|
||||
.parallel-agents .grid { display: grid; }
|
||||
|
||||
/* flex */
|
||||
.parallel-agents .flex-col { flex-direction: column; }
|
||||
.parallel-agents .flex-1 { flex: 1 1 0%; }
|
||||
.parallel-agents .flex-wrap { flex-wrap: wrap; }
|
||||
.parallel-agents .items-center { align-items: center; }
|
||||
.parallel-agents .items-start { align-items: flex-start; }
|
||||
.parallel-agents .justify-center { justify-content: center; }
|
||||
.parallel-agents .justify-between { justify-content: space-between; }
|
||||
.parallel-agents .justify-end { justify-content: flex-end; }
|
||||
.parallel-agents .shrink-0 { flex-shrink: 0; }
|
||||
|
||||
/* grid */
|
||||
.parallel-agents .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
@media (min-width: 1024px) {
|
||||
.parallel-agents .lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* position */
|
||||
.parallel-agents .relative { position: relative; }
|
||||
.parallel-agents .absolute { position: absolute; }
|
||||
.parallel-agents .inset-0 { inset: 0; }
|
||||
.parallel-agents .left-1\/2 { left: 50%; }
|
||||
.parallel-agents .-bottom-0\.5 { bottom: -0.125rem; }
|
||||
.parallel-agents .-right-0\.5 { right: -0.125rem; }
|
||||
|
||||
/* sizing */
|
||||
.parallel-agents .w-full { width: 100%; }
|
||||
.parallel-agents .w-px { width: 1px; }
|
||||
.parallel-agents .h-full { height: 100%; }
|
||||
.parallel-agents .h-8 { height: 2rem; }
|
||||
.parallel-agents .min-w-0 { min-width: 0; }
|
||||
.parallel-agents .min-h-0 { min-height: 0; }
|
||||
.parallel-agents .min-h-16 { min-height: 4rem; }
|
||||
.parallel-agents .max-w-full { max-width: 100%; }
|
||||
.parallel-agents .max-h-60 { max-height: 15rem; }
|
||||
.parallel-agents .size-1 { width: 0.25rem; height: 0.25rem; }
|
||||
.parallel-agents .size-3\.5 { width: 0.875rem; height: 0.875rem; }
|
||||
|
||||
/* gap */
|
||||
.parallel-agents .gap-0\.5 { gap: 0.125rem; }
|
||||
.parallel-agents .gap-1 { gap: 0.25rem; }
|
||||
.parallel-agents .gap-1\.5 { gap: 0.375rem; }
|
||||
.parallel-agents .gap-2 { gap: 0.5rem; }
|
||||
.parallel-agents .gap-3 { gap: 0.75rem; }
|
||||
.parallel-agents .gap-4 { gap: 1rem; }
|
||||
|
||||
/* space-y(子元素纵向间距) */
|
||||
.parallel-agents .space-y-1 > * + * { margin-top: 0.25rem; }
|
||||
.parallel-agents .space-y-1\.5 > * + * { margin-top: 0.375rem; }
|
||||
.parallel-agents .space-y-2 > * + * { margin-top: 0.5rem; }
|
||||
|
||||
/* padding */
|
||||
.parallel-agents .p-0\.5 { padding: 0.125rem; }
|
||||
.parallel-agents .p-2 { padding: 0.5rem; }
|
||||
.parallel-agents .p-3 { padding: 0.75rem; }
|
||||
.parallel-agents .p-4 { padding: 1rem; }
|
||||
.parallel-agents .px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; }
|
||||
.parallel-agents .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
||||
.parallel-agents .px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; }
|
||||
.parallel-agents .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||
.parallel-agents .px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.parallel-agents .py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
|
||||
.parallel-agents .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
||||
.parallel-agents .py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
|
||||
.parallel-agents .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||
.parallel-agents .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
||||
.parallel-agents .pt-0\.5 { padding-top: 0.125rem; }
|
||||
.parallel-agents .pb-2 { padding-bottom: 0.5rem; }
|
||||
|
||||
/* margin */
|
||||
.parallel-agents .mt-0\.5 { margin-top: 0.125rem; }
|
||||
.parallel-agents .mt-2 { margin-top: 0.5rem; }
|
||||
.parallel-agents .mt-3 { margin-top: 0.75rem; }
|
||||
.parallel-agents .mb-2 { margin-bottom: 0.5rem; }
|
||||
.parallel-agents .ml-2 { margin-left: 0.5rem; }
|
||||
.parallel-agents .-mx-px { margin-left: -1px; margin-right: -1px; }
|
||||
|
||||
/* border + radius */
|
||||
.parallel-agents .border { border-width: 1px; border-style: solid; }
|
||||
.parallel-agents .border-b { border-bottom-width: 1px; border-bottom-style: solid; }
|
||||
.parallel-agents .rounded { border-radius: 0.25rem; }
|
||||
.parallel-agents .rounded-md { border-radius: 0.375rem; }
|
||||
.parallel-agents .rounded-lg { border-radius: 0.5rem; }
|
||||
.parallel-agents .rounded-xl { border-radius: 0.75rem; }
|
||||
.parallel-agents .rounded-full { border-radius: 9999px; }
|
||||
|
||||
/* font-size(含任意值) */
|
||||
.parallel-agents .text-\[8px\] { font-size: 8px; }
|
||||
.parallel-agents .text-\[10px\] { font-size: 10px; }
|
||||
.parallel-agents .text-\[11px\] { font-size: 11px; }
|
||||
.parallel-agents .text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.parallel-agents .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
|
||||
/* font-weight */
|
||||
.parallel-agents .font-normal { font-weight: 400; }
|
||||
.parallel-agents .font-medium { font-weight: 500; }
|
||||
.parallel-agents .font-semibold { font-weight: 600; }
|
||||
.parallel-agents .font-bold { font-weight: 700; }
|
||||
.parallel-agents .font-extrabold { font-weight: 800; }
|
||||
.parallel-agents .font-black { font-weight: 900; }
|
||||
|
||||
/* font 其它 */
|
||||
.parallel-agents .font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
.parallel-agents .italic { font-style: italic; }
|
||||
.parallel-agents .uppercase { text-transform: uppercase; }
|
||||
.parallel-agents .tracking-wide { letter-spacing: 0.025em; }
|
||||
|
||||
/* line-height */
|
||||
.parallel-agents .leading-snug { line-height: 1.375; }
|
||||
.parallel-agents .leading-relaxed { line-height: 1.625; }
|
||||
|
||||
/* text-align */
|
||||
.parallel-agents .text-left { text-align: left; }
|
||||
.parallel-agents .text-center { text-align: center; }
|
||||
|
||||
/* 文本截断 / 换行 */
|
||||
.parallel-agents .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.parallel-agents .break-words { overflow-wrap: break-word; }
|
||||
.parallel-agents .line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* overflow */
|
||||
.parallel-agents .overflow-hidden { overflow: hidden; }
|
||||
.parallel-agents .overflow-auto { overflow: auto; }
|
||||
.parallel-agents .overflow-x-auto { overflow-x: auto; }
|
||||
.parallel-agents .overflow-y-auto { overflow-y: auto; }
|
||||
|
||||
/* 效果 / 变换 / 过渡 */
|
||||
.parallel-agents .opacity-60 { opacity: 0.6; }
|
||||
.parallel-agents .shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
|
||||
.parallel-agents .resize-none { resize: none; }
|
||||
.parallel-agents .rotate-90 { transform: rotate(90deg); }
|
||||
.parallel-agents .transition {
|
||||
transition-property: color, background-color, border-color, fill, stroke, opacity,
|
||||
box-shadow, transform, filter, backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.parallel-agents .transition-transform {
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.parallel-agents .hover\:opacity-90:hover { opacity: 0.9; }
|
||||
|
||||
} /* @layer pa-utilities */
|
||||
@@ -44,12 +44,15 @@ export interface Dialogue {
|
||||
clarification?: ClarificationPayload;
|
||||
}
|
||||
|
||||
/** 席位 / 结果的状态。`clarify` = 智能体在等用户协助(待确认),不是已完成。 */
|
||||
export type SeatStatus = "pending" | "running" | "done" | "clarify" | "error";
|
||||
|
||||
/** 并行席位的视图状态(席位状态条用)。 */
|
||||
export interface SeatView {
|
||||
agentId: string;
|
||||
name: string;
|
||||
avatarType: string;
|
||||
status: "pending" | "running" | "done" | "error";
|
||||
status: SeatStatus;
|
||||
/** 关联的 Dialogue.id。 */
|
||||
bubbleId: number;
|
||||
}
|
||||
@@ -87,7 +90,8 @@ export interface AgentResult {
|
||||
name: string;
|
||||
/** 该智能体模型返回的正文(Markdown)。 */
|
||||
content: string;
|
||||
status: "pending" | "running" | "done" | "error";
|
||||
/** `clarify` 表示在等用户协助(待确认),宿主据此判断「未完成」。 */
|
||||
status: SeatStatus;
|
||||
}
|
||||
|
||||
/** 持久化的任务记录(与后端 /api/parallel-agent-tasks 对应)。 */
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* 气泡,维护并行席位状态条;带轻量节流(rAF 合并增量),支持取消、回显 hydrate、重置。
|
||||
*/
|
||||
import { ref, shallowRef } from "vue";
|
||||
import type { AgentInput, Dialogue, SeatView, StreamEvent } from "@/types";
|
||||
import { getAvatarTypeFor } from "@/lib/avatar-constants";
|
||||
import { streamParallelAgents, type ApiConfig } from "@/api";
|
||||
import type { AgentInput, Dialogue, SeatStatus, SeatView, StreamEvent } from "./types";
|
||||
import { getAvatarTypeFor } from "./avatar-constants";
|
||||
import { streamParallelAgents, type ApiConfig } from "./api";
|
||||
|
||||
function nowTime(): string {
|
||||
const d = new Date();
|
||||
@@ -29,6 +29,18 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
const running = ref(false);
|
||||
const abortRef = shallowRef<AbortController | null>(null);
|
||||
|
||||
// 正在「单席位重跑」(用户协助后继续)的 agentId 集合。澄清卡片的禁用态按席位取这里,
|
||||
// 而不是全局 running —— 这样多个智能体都待协助时,协助 A 不会禁用 B/C 的卡片。
|
||||
const busy = ref<Set<string>>(new Set());
|
||||
// 每个重跑各自的 AbortController(支持多席位并发协助 + 统一取消)。
|
||||
const acByAgent = new Map<string, AbortController>();
|
||||
function setBusy(agentId: string, on: boolean) {
|
||||
const next = new Set(busy.value);
|
||||
if (on) next.add(agentId);
|
||||
else next.delete(agentId);
|
||||
busy.value = next;
|
||||
}
|
||||
|
||||
// 本轮已「结算」(回调过 onAgentSettled)的 agentId,去重,避免重复回传。
|
||||
const settled = new Set<string>();
|
||||
function settleAgent(agentId: string, status: SeatView["status"]) {
|
||||
@@ -61,6 +73,11 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
requestAnimationFrame(flushPending);
|
||||
}
|
||||
|
||||
// 断流兜底时把席位状态归一为「终态」:保留待协助 / 出错,其余(running/pending)算完成。
|
||||
function settleStatusOf(status: SeatStatus): SeatStatus {
|
||||
return status === "clarify" || status === "error" ? status : "done";
|
||||
}
|
||||
|
||||
function setSeatStatus(agentId: string, status: SeatView["status"]) {
|
||||
const s = seats.value.find((x) => x.agentId === agentId);
|
||||
if (s) s.status = status;
|
||||
@@ -125,9 +142,10 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
d.streaming = false;
|
||||
d.confidence = "待确认";
|
||||
}
|
||||
setSeatStatus(ev.agent_id, "done");
|
||||
// 待协助是独立状态,不是 done —— 否则席位条显示 ✓、宿主判定已完成。
|
||||
setSeatStatus(ev.agent_id, "clarify");
|
||||
dialogues.value = [...dialogues.value];
|
||||
settleAgent(ev.agent_id, "done");
|
||||
settleAgent(ev.agent_id, "clarify");
|
||||
break;
|
||||
}
|
||||
case "agent_error": {
|
||||
@@ -204,8 +222,8 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
}
|
||||
}
|
||||
dialogues.value = [...dialogues.value];
|
||||
// 兜底:没收到终态事件就断流的席位也回调一次(按当前席位状态)。
|
||||
for (const s of seats.value) settleAgent(s.agentId, s.status === "error" ? "error" : "done");
|
||||
// 兜底:没收到终态事件就断流的席位也回调一次(按当前席位状态,保留待协助/出错)。
|
||||
for (const s of seats.value) settleAgent(s.agentId, settleStatusOf(s.status));
|
||||
running.value = false;
|
||||
abortRef.value = null;
|
||||
}
|
||||
@@ -213,6 +231,9 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
|
||||
function cancel() {
|
||||
abortRef.value?.abort();
|
||||
for (const ac of acByAgent.values()) ac.abort();
|
||||
acByAgent.clear();
|
||||
busy.value = new Set();
|
||||
running.value = false;
|
||||
}
|
||||
|
||||
@@ -224,7 +245,8 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
cfg: ApiConfig,
|
||||
args: { taskName: string; brief: string; agentId: string; model?: string },
|
||||
): Promise<void> {
|
||||
if (running.value) return;
|
||||
// 整批分析进行中不允许单独重跑(会和合并流抢同一气泡);同一席位也不能并发重跑。
|
||||
if (running.value || busy.value.has(args.agentId)) return;
|
||||
const d = dialogues.value.find((x) => x.agentId === args.agentId);
|
||||
if (!d) return;
|
||||
d.content = "";
|
||||
@@ -237,9 +259,10 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
dialogues.value = [...dialogues.value];
|
||||
|
||||
settled.delete(args.agentId); // 该席位重跑,允许再次回传
|
||||
running.value = true;
|
||||
// 不动全局 running —— 用按席位的 busy,这样可同时协助多个待确认的智能体。
|
||||
setBusy(args.agentId, true);
|
||||
const ac = new AbortController();
|
||||
abortRef.value = ac;
|
||||
acByAgent.set(args.agentId, ac);
|
||||
try {
|
||||
await streamParallelAgents(
|
||||
cfg,
|
||||
@@ -255,9 +278,9 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
}
|
||||
dialogues.value = [...dialogues.value];
|
||||
const st = seats.value.find((x) => x.agentId === args.agentId);
|
||||
settleAgent(args.agentId, st?.status === "error" ? "error" : "done");
|
||||
running.value = false;
|
||||
abortRef.value = null;
|
||||
settleAgent(args.agentId, settleStatusOf(st?.status ?? "done"));
|
||||
acByAgent.delete(args.agentId);
|
||||
setBusy(args.agentId, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,11 +294,13 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
restored.forEach((d) => {
|
||||
bubbleByAgent.set(d.agentId, d.id);
|
||||
if (!seatMap.has(d.agentId)) {
|
||||
const status: SeatStatus =
|
||||
d.confidence === "出错" ? "error" : d.clarification ? "clarify" : "done";
|
||||
seatMap.set(d.agentId, {
|
||||
agentId: d.agentId,
|
||||
name: d.sender,
|
||||
avatarType: d.avatarType,
|
||||
status: d.confidence === "出错" ? "error" : "done",
|
||||
status,
|
||||
bubbleId: d.id,
|
||||
});
|
||||
}
|
||||
@@ -292,5 +317,5 @@ export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
seats.value = [];
|
||||
}
|
||||
|
||||
return { dialogues, seats, running, start, cancel, hydrate, reset, rerunAgent };
|
||||
return { dialogues, seats, running, busy, start, cancel, hydrate, reset, rerunAgent };
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// 两种构建:
|
||||
// - 默认(`vite build` / `vite dev`):独立 demo 应用(index.html)。
|
||||
@@ -10,7 +9,7 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isLib = mode === "lib";
|
||||
return {
|
||||
plugins: [vue(), tailwindcss()],
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user