feat: 工作流记忆与内置工具、知你客服脚本、Agent管理技能展示与能力配置、文档与Windows启动脚本;忽略 redis_temp 二进制目录
Made-with: Cursor
This commit is contained in:
@@ -386,11 +386,44 @@ const scrollToBottom = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 知你等工作流约定「自然语言 + 末尾单行 JSON(intent/reply/user_profile)」。
|
||||
* 聊天区若原样展示会重复;展示前去掉末尾 JSON 行(可连续多行);若整段只有 JSON 则用 reply 作为正文。
|
||||
*/
|
||||
const stripOneTrailingWorkflowJsonLine = (raw: string): string => {
|
||||
if (!raw || typeof raw !== 'string') return ''
|
||||
const t = raw.trimEnd()
|
||||
const lastNl = t.lastIndexOf('\n')
|
||||
const lastLine = (lastNl >= 0 ? t.slice(lastNl + 1) : t).trim()
|
||||
if (!lastLine.startsWith('{')) return raw
|
||||
try {
|
||||
const j = JSON.parse(lastLine) as Record<string, unknown>
|
||||
if (!j || typeof j !== 'object') return raw
|
||||
const reply = j.reply
|
||||
if (typeof reply !== 'string') return raw
|
||||
const head = lastNl >= 0 ? t.slice(0, lastNl).trimEnd() : ''
|
||||
if (head) return head
|
||||
return reply
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
const stripTrailingWorkflowJsonLine = (raw: string): string => {
|
||||
let cur = raw
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const next = stripOneTrailingWorkflowJsonLine(cur)
|
||||
if (next === cur) break
|
||||
cur = next
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// 格式化消息(支持简单的Markdown)
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return ''
|
||||
// 简单的换行处理
|
||||
return content.replace(/\n/g, '<br>')
|
||||
const display = stripTrailingWorkflowJsonLine(content)
|
||||
return display.replace(/\n/g, '<br>')
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
<el-icon><Operation style="transform: rotate(90deg)" /></el-icon>
|
||||
垂直分布
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="normalize-edge-handles" divided>
|
||||
<el-icon><ConnectionIcon /></el-icon>
|
||||
修正连线锚点
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -6214,6 +6218,84 @@ const onEdgesChange = (changes: any[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 节点中心与尺寸(无测量值时用与自动布局一致的默认宽高) */
|
||||
const getNodeLayoutMetrics = (n: Node) => {
|
||||
const dw = n.dimensions?.width
|
||||
const dh = n.dimensions?.height
|
||||
const w = dw && dw > 0 ? dw : 200
|
||||
const h = dh && dh > 0 ? dh : 80
|
||||
return {
|
||||
w,
|
||||
h,
|
||||
cx: n.position.x + w / 2,
|
||||
cy: n.position.y + h / 2,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据两节点相对位置选择连接点,避免「吸附」总接到上下锚点造成锯齿线。
|
||||
* 条件节点 true/false 出口保留,仅修正目标侧锚点。
|
||||
*/
|
||||
const resolveOptimalEdgeHandles = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
currentSourceHandle?: string | null
|
||||
): { sourceHandle: string; targetHandle: string } => {
|
||||
const src = nodes.value.find((node) => node.id === sourceId)
|
||||
const tgt = nodes.value.find((node) => node.id === targetId)
|
||||
if (!src || !tgt) {
|
||||
return { sourceHandle: currentSourceHandle || 'right', targetHandle: 'left' }
|
||||
}
|
||||
const a = getNodeLayoutMetrics(src)
|
||||
const b = getNodeLayoutMetrics(tgt)
|
||||
const dx = b.cx - a.cx
|
||||
const dy = b.cy - a.cy
|
||||
|
||||
if (currentSourceHandle === 'true' || currentSourceHandle === 'false') {
|
||||
if (Math.abs(dx) >= Math.abs(dy)) {
|
||||
return { sourceHandle: currentSourceHandle, targetHandle: 'left' }
|
||||
}
|
||||
return { sourceHandle: currentSourceHandle, targetHandle: 'top' }
|
||||
}
|
||||
|
||||
const horizontalish = Math.abs(dx) >= Math.abs(dy) * 0.85
|
||||
if (horizontalish) {
|
||||
return { sourceHandle: 'right', targetHandle: 'left' }
|
||||
}
|
||||
if (dy > 8) {
|
||||
return { sourceHandle: 'bottom', targetHandle: 'top' }
|
||||
}
|
||||
return { sourceHandle: 'right', targetHandle: 'left' }
|
||||
}
|
||||
|
||||
/** 批量将已有边的锚点校正为与布局一致的方向 */
|
||||
const normalizeAllWorkflowEdgeHandles = (opts?: { silent?: boolean }): number => {
|
||||
let changed = 0
|
||||
for (const edge of edges.value) {
|
||||
const { sourceHandle, targetHandle } = resolveOptimalEdgeHandles(
|
||||
edge.source,
|
||||
edge.target,
|
||||
edge.sourceHandle
|
||||
)
|
||||
if (edge.sourceHandle !== sourceHandle || edge.targetHandle !== targetHandle) {
|
||||
updateEdge(edge.id, { sourceHandle, targetHandle })
|
||||
edge.sourceHandle = sourceHandle
|
||||
edge.targetHandle = targetHandle
|
||||
changed++
|
||||
}
|
||||
}
|
||||
if (changed > 0) {
|
||||
hasChanges.value = true
|
||||
pushHistory('normalize edge handles')
|
||||
if (!opts?.silent) {
|
||||
ElMessage.success(`已修正 ${changed} 条连线的锚点`)
|
||||
}
|
||||
} else if (!opts?.silent) {
|
||||
ElMessage.info('连线锚点已在最佳方向')
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// 连接验证函数 - 允许所有方向的连接(包括左右)
|
||||
const isValidConnection = (connection: Connection) => {
|
||||
console.log('验证连接:', connection)
|
||||
@@ -6228,14 +6310,11 @@ const isValidConnection = (connection: Connection) => {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否已经存在相同的连接
|
||||
// 同一对节点只保留一条边(锚点不同也视为重复,避免重复连线)
|
||||
const existingEdge = edges.value.find(
|
||||
e => e.source === connection.source &&
|
||||
e.target === connection.target &&
|
||||
(e.sourceHandle === connection.sourceHandle || (!e.sourceHandle && !connection.sourceHandle)) &&
|
||||
(e.targetHandle === connection.targetHandle || (!e.targetHandle && !connection.targetHandle))
|
||||
(e) => e.source === connection.source && e.target === connection.target
|
||||
)
|
||||
|
||||
|
||||
if (existingEdge) {
|
||||
return false
|
||||
}
|
||||
@@ -6265,19 +6344,22 @@ const onConnect = (connection: Connection) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已经存在相同的连接
|
||||
// 同一对节点只保留一条边
|
||||
const existingEdge = edges.value.find(
|
||||
e => e.source === connection.source &&
|
||||
e.target === connection.target &&
|
||||
(e.sourceHandle === connection.sourceHandle || (!e.sourceHandle && !connection.sourceHandle)) &&
|
||||
(e.targetHandle === connection.targetHandle || (!e.targetHandle && !connection.targetHandle))
|
||||
(e) => e.source === connection.source && e.target === connection.target
|
||||
)
|
||||
|
||||
|
||||
if (existingEdge) {
|
||||
ElMessage.warning('连接已存在')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const { sourceHandle, targetHandle } = resolveOptimalEdgeHandles(
|
||||
connection.source,
|
||||
connection.target,
|
||||
connection.sourceHandle
|
||||
)
|
||||
|
||||
// 清除选中的边
|
||||
selectedEdge.value = null
|
||||
|
||||
@@ -6285,8 +6367,8 @@ const onConnect = (connection: Connection) => {
|
||||
id: `edge_${connection.source}_${connection.target}_${Date.now()}`,
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
sourceHandle: connection.sourceHandle || undefined,
|
||||
targetHandle: connection.targetHandle || undefined,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type: 'bezier', // 使用贝塞尔曲线(平滑曲线)
|
||||
animated: true,
|
||||
selectable: true,
|
||||
@@ -6906,6 +6988,11 @@ const handleClear = () => {
|
||||
|
||||
// 节点对齐功能
|
||||
const handleAlignNodes = (command: string) => {
|
||||
if (command === 'normalize-edge-handles') {
|
||||
normalizeAllWorkflowEdgeHandles()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取选中的节点(支持多选)
|
||||
const selectedNodes = nodes.value.filter(node => node.selected)
|
||||
|
||||
@@ -7054,147 +7141,117 @@ const handleAutoLayout = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 构建邻接表(有向图)
|
||||
const graph: Record<string, string[]> = {}
|
||||
const inDegree: Record<string, number> = {}
|
||||
|
||||
// 初始化
|
||||
nodes.value.forEach(node => {
|
||||
graph[node.id] = []
|
||||
inDegree[node.id] = 0
|
||||
// 最长路径分层(Sugiyama 简化):rank[v]=1+max(rank[p]),同一 rank 的节点放在同一列,避免旧版「单节点层累加 currentX + 分支后重置 X」造成的重叠与超长飞线
|
||||
const nodeIds = nodes.value.map((n) => n.id)
|
||||
const pred = new Map<string, string[]>()
|
||||
nodeIds.forEach((id) => pred.set(id, []))
|
||||
edges.value.forEach((edge) => {
|
||||
if (!edge.source || !edge.target) return
|
||||
if (!pred.has(edge.target)) return
|
||||
pred.get(edge.target)!.push(edge.source)
|
||||
})
|
||||
|
||||
// 构建图
|
||||
edges.value.forEach(edge => {
|
||||
if (graph[edge.source] && !graph[edge.source].includes(edge.target)) {
|
||||
graph[edge.source].push(edge.target)
|
||||
inDegree[edge.target] = (inDegree[edge.target] || 0) + 1
|
||||
}
|
||||
|
||||
const rank: Record<string, number> = {}
|
||||
nodeIds.forEach((id) => {
|
||||
const ps = pred.get(id) || []
|
||||
if (ps.length === 0) rank[id] = 0
|
||||
})
|
||||
|
||||
// 拓扑排序,将节点分层
|
||||
const layers: string[][] = []
|
||||
const visited = new Set<string>()
|
||||
const queue: string[] = []
|
||||
|
||||
// 找到所有入度为0的节点(开始节点)
|
||||
Object.keys(inDegree).forEach(nodeId => {
|
||||
if (inDegree[nodeId] === 0) {
|
||||
queue.push(nodeId)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果没有入度为0的节点,使用开始节点
|
||||
if (queue.length === 0 && startNode) {
|
||||
queue.push(startNode.id)
|
||||
if (startNode && rank[startNode.id] === undefined) rank[startNode.id] = 0
|
||||
|
||||
let relaxIter = 0
|
||||
let changed = true
|
||||
while (changed && relaxIter < nodeIds.length + 12) {
|
||||
changed = false
|
||||
relaxIter++
|
||||
edges.value.forEach((e) => {
|
||||
if (rank[e.source] === undefined) return
|
||||
const next = rank[e.source]! + 1
|
||||
if (rank[e.target] === undefined || rank[e.target]! < next) {
|
||||
rank[e.target] = next
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分层遍历
|
||||
while (queue.length > 0) {
|
||||
const layer: string[] = []
|
||||
const layerSize = queue.length
|
||||
|
||||
for (let i = 0; i < layerSize; i++) {
|
||||
const nodeId = queue.shift()!
|
||||
if (visited.has(nodeId)) continue
|
||||
|
||||
visited.add(nodeId)
|
||||
layer.push(nodeId)
|
||||
|
||||
// 处理该节点的所有出边
|
||||
const neighbors = graph[nodeId] || []
|
||||
neighbors.forEach(neighborId => {
|
||||
inDegree[neighborId] = (inDegree[neighborId] || 0) - 1
|
||||
if (inDegree[neighborId] === 0 && !visited.has(neighborId)) {
|
||||
queue.push(neighborId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (layer.length > 0) {
|
||||
layers.push(layer)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理未访问的节点(可能是孤立节点)
|
||||
nodes.value.forEach(node => {
|
||||
if (!visited.has(node.id)) {
|
||||
if (layers.length === 0) {
|
||||
layers.push([node.id])
|
||||
} else {
|
||||
layers[layers.length - 1].push(node.id)
|
||||
let maxR = 0
|
||||
nodeIds.forEach((id) => {
|
||||
if (rank[id] !== undefined) maxR = Math.max(maxR, rank[id]!)
|
||||
})
|
||||
nodeIds.forEach((id) => {
|
||||
if (rank[id] === undefined) rank[id] = maxR + 1
|
||||
})
|
||||
maxR = Math.max(maxR, ...nodeIds.map((id) => rank[id]!))
|
||||
|
||||
const rankSet = new Set<number>()
|
||||
nodeIds.forEach((id) => rankSet.add(rank[id]!))
|
||||
const sortedRanks = [...rankSet].sort((a, b) => a - b)
|
||||
const rankToCol = new Map<number, number>(sortedRanks.map((r, i) => [r, i]))
|
||||
|
||||
let layers: string[][] = Array.from({ length: sortedRanks.length }, () => [])
|
||||
nodeIds.forEach((id) => {
|
||||
const col = rankToCol.get(rank[id]!) ?? 0
|
||||
layers[col].push(id)
|
||||
})
|
||||
|
||||
// 层内排序:重心法(barycenter)减少跨层边交叉,多轮上下扫掠
|
||||
const reorderLayersReduceCrossings = (ly: string[][], edgs: typeof edges.value): string[][] => {
|
||||
if (ly.length <= 1) return ly.map(l => [...l])
|
||||
const out = ly.map(l => [...l])
|
||||
const pred = new Map<string, string[]>()
|
||||
const succ = new Map<string, string[]>()
|
||||
edgs.forEach((e) => {
|
||||
if (!e?.source || !e?.target) return
|
||||
if (!pred.has(e.target)) pred.set(e.target, [])
|
||||
pred.get(e.target)!.push(e.source)
|
||||
if (!succ.has(e.source)) succ.set(e.source, [])
|
||||
succ.get(e.source)!.push(e.target)
|
||||
})
|
||||
const rounds = 8
|
||||
for (let it = 0; it < rounds; it++) {
|
||||
for (let k = 1; k < out.length; k++) {
|
||||
const prev = out[k - 1]
|
||||
const pos = new Map(prev.map((id, i) => [id, i]))
|
||||
out[k].sort((a, b) => {
|
||||
const pa = (pred.get(a) || []).filter((x) => pos.has(x))
|
||||
const pb = (pred.get(b) || []).filter((x) => pos.has(x))
|
||||
const ba = pa.length ? pa.reduce((s, x) => s + (pos.get(x) ?? 0), 0) / pa.length : 0
|
||||
const bb = pb.length ? pb.reduce((s, x) => s + (pos.get(x) ?? 0), 0) / pb.length : 0
|
||||
return ba - bb || a.localeCompare(b)
|
||||
})
|
||||
}
|
||||
for (let k = out.length - 2; k >= 0; k--) {
|
||||
const next = out[k + 1]
|
||||
const pos = new Map(next.map((id, i) => [id, i]))
|
||||
out[k].sort((a, b) => {
|
||||
const sa = (succ.get(a) || []).filter((x) => pos.has(x))
|
||||
const sb = (succ.get(b) || []).filter((x) => pos.has(x))
|
||||
const ba = sa.length ? sa.reduce((s, x) => s + (pos.get(x) ?? 0), 0) / sa.length : 0
|
||||
const bb = sb.length ? sb.reduce((s, x) => s + (pos.get(x) ?? 0), 0) / sb.length : 0
|
||||
return ba - bb || a.localeCompare(b)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
layers = reorderLayersReduceCrossings(layers, edges.value)
|
||||
|
||||
// 布局参数
|
||||
// 布局参数:按列摆开(列 = 拓扑深度),列内垂直堆叠;主线性链呈一行,分支呈一列,避免旧算法横向漂移与飞线
|
||||
const nodeWidth = 200
|
||||
const nodeHeight = 80
|
||||
const horizontalSpacing = 320 // 水平间距(节点之间的水平距离)
|
||||
const verticalSpacing = 150 // 垂直间距(用于有分支的情况)
|
||||
const startX = 100
|
||||
const startY = 200
|
||||
|
||||
// 检查是否是简单的线性工作流(每层只有一个节点)
|
||||
let isLinearWorkflow = true
|
||||
layers.forEach(layer => {
|
||||
if (layer.length > 1) {
|
||||
isLinearWorkflow = false
|
||||
}
|
||||
})
|
||||
|
||||
// 如果每层只有一个节点,使用水平线性布局(从左到右,所有节点在同一水平线)
|
||||
if (isLinearWorkflow && layers.length > 1) {
|
||||
// 水平线性布局:所有节点水平排列在同一水平线上
|
||||
layers.forEach((layer, layerIndex) => {
|
||||
layer.forEach((nodeId) => {
|
||||
const nodeX = startX + layerIndex * horizontalSpacing
|
||||
const nodeY = startY // 所有节点在同一水平线上
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: nodeY }
|
||||
})
|
||||
const horizontalSpacing = 300
|
||||
const verticalSpacing = 130
|
||||
const startX = 80
|
||||
const startY = 220
|
||||
|
||||
layers.forEach((layer, colIndex) => {
|
||||
const nodeX = startX + colIndex * horizontalSpacing
|
||||
const n = layer.length
|
||||
const y0 = startY - ((n - 1) * verticalSpacing) / 2
|
||||
layer.forEach((nodeId, j) => {
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: y0 + j * verticalSpacing }
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 层次布局:有分支的工作流
|
||||
// 优化策略:尽量让单节点层水平排列,多节点层才垂直排列
|
||||
let baseY = startY
|
||||
let currentX = startX
|
||||
let consecutiveSingleNodeLayers = 0
|
||||
|
||||
layers.forEach((layer, layerIndex) => {
|
||||
if (layer.length === 1) {
|
||||
// 单节点层:水平排列
|
||||
consecutiveSingleNodeLayers++
|
||||
const nodeId = layer[0]
|
||||
const nodeX = currentX
|
||||
// 如果连续多个单节点层,保持水平对齐
|
||||
const nodeY = baseY + (consecutiveSingleNodeLayers > 3 ? 20 : 0) // 如果连续太多,稍微下移
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: nodeY }
|
||||
})
|
||||
currentX += horizontalSpacing
|
||||
} else {
|
||||
// 多节点层:水平居中排列,使用新的Y坐标
|
||||
consecutiveSingleNodeLayers = 0
|
||||
baseY += verticalSpacing
|
||||
currentX = startX // 重置X位置
|
||||
|
||||
const layerWidth = (layer.length - 1) * horizontalSpacing
|
||||
const layerStartX = startX
|
||||
|
||||
layer.forEach((nodeId, nodeIndex) => {
|
||||
const nodeX = layerStartX + nodeIndex * horizontalSpacing
|
||||
updateNode(nodeId, {
|
||||
position: { x: nodeX, y: baseY }
|
||||
})
|
||||
})
|
||||
|
||||
// 更新currentX为下一层的起始位置
|
||||
currentX = layerStartX + layerWidth + horizontalSpacing
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 自动调整视口,使所有节点可见
|
||||
await nextTick()
|
||||
@@ -7233,7 +7290,12 @@ const handleAutoLayout = async () => {
|
||||
}
|
||||
}, 100)
|
||||
|
||||
ElMessage.success(`自动布局完成,共 ${layers.length} 层,${nodes.value.length} 个节点`)
|
||||
await nextTick()
|
||||
const fixedHandles = normalizeAllWorkflowEdgeHandles({ silent: true })
|
||||
ElMessage.success(
|
||||
`自动布局完成:${layers.length} 列(最长路径分层)+ 层内排序减交叉` +
|
||||
(fixedHandles ? `,已优化 ${fixedHandles} 条连线锚点` : '')
|
||||
)
|
||||
hasChanges.value = true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user