重构: 对话透明化 L1 + ③.2 审批拆分 + ⑥.4 @展开 + DAG 展示

修复 tool_result 压缩零效果(BUG-260628-01):单行/少行/JSON 大字符
串字段逃逸压缩的问题,基于实测数据(53/94 次迭代零节省)调参,
增加字符级保底截断,压缩有效率从 8% 提升至 ~85%+
This commit is contained in:
2026-06-28 01:40:02 +08:00
parent e4ceb0015b
commit 53e1c1da77
19 changed files with 921 additions and 350 deletions

View File

@@ -2,27 +2,39 @@
<div v-if="dag" class="wf-dag">
<h4 class="wf-dag-title">{{ $t('taskDetail.workflowDagTitle') || '工作流结构' }}</h4>
<!-- 节点列表 -->
<div class="wf-dag-nodes">
<div v-for="node in dag.nodes" :key="node.id" class="wf-dag-node">
<span class="wf-dag-node-icon"></span>
<span class="wf-dag-node-name">{{ node.id }}</span>
<span class="wf-dag-node-type">{{ nodeTypeLabel[node.node_type] || node.node_type }}</span>
<!-- 拓扑分层渲染同层并行层间串行 -->
<div class="wf-dag-layers">
<div v-for="(layer, li) in layers" :key="li" class="wf-dag-layer">
<div class="wf-dag-layer-label">{{ $t('taskDetail.workflowLayer') || '层' }} {{ li + 1 }}</div>
<div class="wf-dag-layer-nodes">
<div
v-for="node in layer"
:key="node.id"
class="wf-dag-node"
:class="nodeClass(node.id, props.nodeStatuses)"
>
<span class="wf-dag-node-status-dot"></span>
<span class="wf-dag-node-name">{{ node.id }}</span>
<span class="wf-dag-node-type">{{ nodeTypeLabel[node.node_type] || node.node_type }}</span>
<span v-if="nodeStatuses[node.id]" class="wf-dag-node-status">{{ statusLabel(nodeStatuses[node.id]) }}</span>
</div>
</div>
<!-- 层间箭头 -->
<div v-if="li < layers.length - 1" class="wf-dag-layer-arrow"></div>
</div>
</div>
<!-- 边列表带条件 -->
<div class="wf-dag-edges">
<div v-for="(edge, i) in dag.edges" :key="i" class="wf-dag-edge">
<span class="wf-dag-edge-arrow">{{ edge.source }} {{ edge.target }}</span>
<span v-if="edge.condition" class="wf-dag-edge-cond" :title="edge.condition">
{{ edge.condition }}
</span>
<span v-else class="wf-dag-edge-cond wf-dag-edge-cond--uncond">
无条件
</span>
<!-- 边条件列表折叠态hover 节点时参考 -->
<details class="wf-dag-edges-detail">
<summary class="wf-dag-edges-summary">{{ $t('taskDetail.workflowEdgeDetail') || '边条件详情' }} ({{ dag.edges.length }})</summary>
<div class="wf-dag-edges">
<div v-for="(edge, i) in dag.edges" :key="i" class="wf-dag-edge">
<span class="wf-dag-edge-arrow">{{ edge.source }} {{ edge.target }}</span>
<span v-if="edge.condition" class="wf-dag-edge-cond" :title="edge.condition">{{ edge.condition }}</span>
<span v-else class="wf-dag-edge-cond wf-dag-edge-cond--uncond">{{ $t('taskDetail.workflowUnconditional') || '无条件' }}</span>
</div>
</div>
</div>
</details>
</div>
<div v-else class="wf-dag wf-dag--empty">
{{ $t('taskDetail.workflowDagEmpty') || '暂无工作流数据' }}
@@ -37,23 +49,23 @@ interface DagNode {
node_type: string
config?: Record<string, unknown>
}
interface EdgeDef {
source: string
target: string
condition?: string | null
}
export type NodeStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
const props = defineProps<{
const props = withDefaults(defineProps<{
dagJson: string
}>()
nodeStatuses?: Record<string, NodeStatus>
}>(), {
nodeStatuses: () => ({}),
})
const dag = computed<{ nodes: DagNode[]; edges: EdgeDef[] } | null>(() => {
try {
return JSON.parse(props.dagJson)
} catch {
return null
}
try { return JSON.parse(props.dagJson) }
catch { return null }
})
const nodeTypeLabel: Record<string, string> = {
@@ -62,70 +74,120 @@ const nodeTypeLabel: Record<string, string> = {
'ScriptNode': '脚本',
'AiSelfReviewNode': 'AI 自审',
}
function statusLabel(s: NodeStatus): string {
return { pending: '⏳', running: '🔄', completed: '✅', failed: '❌', skipped: '⏭️' }[s] || ''
}
function nodeClass(id: string, statuses: Record<string, NodeStatus>): Record<string, boolean> {
const s = statuses[id]
return {
'wf-dag-node--running': s === 'running',
'wf-dag-node--completed': s === 'completed',
'wf-dag-node--failed': s === 'failed',
'wf-dag-node--skipped': s === 'skipped',
}
}
/** 拓扑分层:从无入边的根节点开始,逐层推进 */
const layers = computed<(DagNode & { deps?: string[] })[][]>(() => {
if (!dag.value) return []
const d = dag.value
// 入边计数
const inDegree: Record<string, number> = {}
const edgeMap: Record<string, string[]> = {}
for (const n of d.nodes) {
inDegree[n.id] = 0
edgeMap[n.id] = []
}
for (const e of d.edges) {
if (!edgeMap[e.source]) edgeMap[e.source] = []
edgeMap[e.source].push(e.target)
inDegree[e.target] = (inDegree[e.target] || 0) + 1
}
// Kahn 拓扑排序
const layers: (DagNode & { deps?: string[] })[][] = []
let queue = Object.entries(inDegree).filter(([, d]) => d === 0).map(([id]) => id)
const visited = new Set<string>()
while (queue.length > 0) {
const layer: DagNode[] = []
const next: string[] = []
for (const id of queue) {
if (visited.has(id)) continue
visited.add(id)
const node = d.nodes.find(n => n.id === id)
if (node) layer.push(node)
for (const target of (edgeMap[id] || [])) {
inDegree[target]--
if (inDegree[target] === 0) next.push(target)
}
}
if (layer.length > 0) layers.push(layer)
queue = next
}
return layers
})
</script>
<style scoped>
.wf-dag {
margin-top: 8px;
padding: 8px 12px;
margin-top: 8px; padding: 8px 12px;
background: var(--df-bg-card, #fafafa);
border-radius: var(--df-radius-sm, 4px);
font-size: 12px;
border-radius: var(--df-radius-sm, 4px); font-size: 12px;
}
.wf-dag--empty {
color: var(--df-text-dim, #888);
}
.wf-dag-title {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.wf-dag-nodes {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
.wf-dag--empty { color: var(--df-text-dim, #888); }
.wf-dag-title { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
.wf-dag-layers { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.wf-dag-layer { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.wf-dag-layer-label { font-size: 10px; color: var(--df-text-dim, #888); align-self: flex-start; }
.wf-dag-layer-nodes {
display: flex; flex-wrap: wrap; justify-content: center; gap: 6px;
}
.wf-dag-layer-arrow { font-size: 16px; color: var(--df-text-dim, #888); line-height: 1; }
.wf-dag-node {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px;
background: var(--df-bg, #fff);
border: 0.5px solid var(--df-border, #ddd);
border: 1px solid var(--df-border, #ddd);
border-radius: var(--df-radius-sm, 4px);
transition: border-color 0.3s, background 0.3s;
}
.wf-dag-node-icon { color: var(--df-text-dim, #888); }
.wf-dag-node-name { font-weight: 500; }
.wf-dag-node-name { font-weight: 500; white-space: nowrap; }
.wf-dag-node-type {
font-size: 10px;
color: var(--df-text-dim, #888);
background: var(--df-bg-card, #f0f0f0);
padding: 0 4px;
border-radius: 3px;
font-size: 10px; color: var(--df-text-dim, #888);
background: var(--df-bg-card, #f0f0f0); padding: 0 4px; border-radius: 3px;
}
.wf-dag-edges {
display: flex;
flex-direction: column;
gap: 3px;
.wf-dag-node-status { font-size: 11px; }
.wf-dag-node-status-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--df-text-dim, #ccc);
}
.wf-dag-edge {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
.wf-dag-node--running {
border-color: var(--df-info, #4a90e2);
background: rgba(74,144,226,0.05);
}
.wf-dag-node--running .wf-dag-node-status-dot {
background: var(--df-info, #4a90e2);
animation: pulse 1s ease-in-out infinite;
}
.wf-dag-node--completed { border-color: var(--df-success, #3ddba0); }
.wf-dag-node--completed .wf-dag-node-status-dot { background: var(--df-success, #3ddba0); }
.wf-dag-node--failed { border-color: var(--df-danger, #e5484d); background: rgba(229,72,77,0.05); }
.wf-dag-node--failed .wf-dag-node-status-dot { background: var(--df-danger, #e5484d); }
.wf-dag-node--skipped { opacity: 0.5; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.wf-dag-edges-detail { margin-top: 8px; }
.wf-dag-edges-summary { cursor: pointer; font-size: 11px; color: var(--df-info, #4a90e2); }
.wf-dag-edges { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.wf-dag-edge { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 11px; }
.wf-dag-edge-arrow { color: var(--df-text-primary); }
.wf-dag-edge-cond {
font-family: var(--df-font-mono);
font-size: 11px;
padding: 1px 6px;
background: rgba(74, 144, 226, 0.08);
border-radius: 3px;
color: var(--df-info, #4a90e2);
}
.wf-dag-edge-cond--uncond {
background: transparent;
color: var(--df-text-dim, #aaa);
font-family: var(--df-font-mono); font-size: 11px; padding: 1px 6px;
background: rgba(74,144,226,0.08); border-radius: 3px; color: var(--df-info, #4a90e2);
}
.wf-dag-edge-cond--uncond { background: transparent; color: var(--df-text-dim, #aaa); }
</style>