feat: 工作流记忆与内置工具、知你客服脚本、Agent管理技能展示与能力配置、文档与Windows启动脚本;忽略 redis_temp 二进制目录

Made-with: Cursor
This commit is contained in:
renjianbo
2026-04-08 11:44:24 +08:00
parent 599b8f2851
commit bd3f8be781
66 changed files with 10104 additions and 469 deletions

View File

@@ -386,11 +386,44 @@ const scrollToBottom = () => {
})
}
/**
* 知你等工作流约定「自然语言 + 末尾单行 JSONintent/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>')
}
// 格式化时间

View File

@@ -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
}