feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import {
Emitter,
EntityManager,
FlowDocument,
type IPoint,
type PositionSchema,
type FlowNodeEntity,
} from '@flowgram-adapter/fixed-layout-editor';
import {
getLineId,
getNodeIdFromTreeId,
getTreeIdFromNodeId,
calcDistance,
} from '../utils';
import { type CustomLine, type EdgeItem } from '../typings';
import { CustomRenderStateConfigEntity } from '../entities';
import { LINE_HOVER_DISTANCE } from '../constants';
import { TreeService } from './tree-service';
import { CustomLinesManager } from './custom-lines-manager';
@injectable()
export class CustomHoverService {
@inject(CustomLinesManager) declare linesManager: CustomLinesManager;
@inject(FlowDocument) declare document: FlowDocument;
@inject(TreeService) declare treeService: TreeService;
@inject(EntityManager) declare entityManager: EntityManager;
@inject(CustomRenderStateConfigEntity)
declare renderStateEntity: CustomRenderStateConfigEntity;
get edges(): EdgeItem[] {
return this.treeService.getUnCollapsedEdges();
}
private onBackgroundClickEmitter = new Emitter<void>();
onBackgroundClick = this.onBackgroundClickEmitter.event;
private onHoverCollapseEmitter = new Emitter<FlowNodeEntity | undefined>();
onHoverCollapse = this.onHoverCollapseEmitter.event;
private onHoverLineEmitter = new Emitter<CustomLine | undefined>();
onHoverLine = this.onHoverLineEmitter.event;
private onSelectNodeEmitter = new Emitter<void>();
onSelectNode = this.onSelectNodeEmitter.event;
private _hoveredLine: CustomLine | undefined;
get hoveredLine() {
return this._hoveredLine;
}
/**
* 根据鼠标位置计算和线条的间距
*/
getCloseInLineFromMousePos(
mousePos: IPoint,
minDistance: number = LINE_HOVER_DISTANCE,
): CustomLine | undefined {
let targetLine: CustomLine | undefined, targetLineDist: number | undefined;
(this.linesManager?.lines || []).forEach(line => {
const dist = calcDistance(mousePos, line);
if (dist <= minDistance && (!targetLineDist || targetLineDist >= dist)) {
targetLineDist = dist;
targetLine = line;
}
});
return targetLine;
}
updateHoverLine(pos: PositionSchema, checkTarget: boolean) {
if (!checkTarget) {
this.onHoverLineEmitter.fire(undefined);
}
const hoverLine = this.getCloseInLineFromMousePos(pos);
if (hoverLine) {
this.onHoverLineEmitter.fire(hoverLine);
this._hoveredLine = hoverLine;
} else {
this.onHoverLineEmitter.fire(undefined);
this._hoveredLine = undefined;
}
return hoverLine;
}
backgroundClick(updateLines = true) {
this.onBackgroundClickEmitter.fire();
this.renderStateEntity.setSelectNodes([]);
this.renderStateEntity.setActivatedNode(undefined);
if (this.hoveredLine) {
const lineId = getLineId(this._hoveredLine);
this.renderStateEntity.activeLines = [lineId!];
} else if (updateLines) {
this.renderStateEntity.activeLines = [];
} else {
this.renderStateEntity.activeLines = [];
}
}
// 折叠策略:访问所有连线的元素,同时访问其父元素,执行 collapse。
hoverCollapse(from?: FlowNodeEntity) {
this.onHoverCollapseEmitter.fire(from);
}
getParent(node?: FlowNodeEntity): FlowNodeEntity | undefined {
if (!node) {
return node;
}
if (node.flowNodeType !== 'blockIcon') {
return this.getParent(node.parent);
} else {
return node;
}
}
selectNode(node: FlowNodeEntity) {
const selectNodes = this.getRelatedNodes(node);
this.renderStateEntity.setSelectNodes(selectNodes);
this.renderStateEntity.setActivatedNode(node);
// 高亮所有相关的线条
const activeLines: string[] = [];
this.linesManager.lines.map(line => {
const fromInclude = selectNodes.includes(line.from.id);
const toInclude = selectNodes.includes(line.to.id);
if (fromInclude && toInclude) {
activeLines.push(getLineId(line)!);
}
});
this.renderStateEntity.activeLines = activeLines;
this.linesManager.renderLines();
}
traverseAncestors = (node: FlowNodeEntity): string[] => {
let ancArr: string[] = [];
if (node.parent) {
const arr = this.traverseAncestors(node.parent);
ancArr = ancArr.concat(arr);
}
this.edges.forEach(edge => {
if (edge.to === getTreeIdFromNodeId(node.id)) {
// push 额外线条
ancArr.push(edge.from);
// 遍历之前的节点信息
const fromNode = this.document.getNode(getNodeIdFromTreeId(edge.from));
if (fromNode) {
const arr = this.traverseAncestors(fromNode);
ancArr.concat(arr);
}
}
});
ancArr.push(node.id);
return ancArr;
};
traverseDescendants = (node: FlowNodeEntity): string[] => {
let ancArr: string[] = [];
if (node.children?.length) {
node.children?.forEach(child => {
ancArr.push(child.id);
const childArr = this.traverseDescendants(child);
ancArr = ancArr.concat(childArr);
});
}
if (node.next) {
const childArr = this.traverseDescendants(node.next);
ancArr = ancArr.concat(childArr);
}
return ancArr;
};
/**
* 根据当前树结构,遍历节点并且选中
*/
getRelatedNodes = (node: FlowNodeEntity) => {
const parentRelated = this.traverseAncestors(node);
const childRelated = this.traverseDescendants(node);
return [...parentRelated, ...childRelated];
};
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import {
EntityManager,
FlowDocument,
type FlowNodeEntity,
FlowNodeTransformData,
type IPoint,
} from '@flowgram-adapter/fixed-layout-editor';
import type { CustomLine, EdgeItem } from '../typings';
import { CustomRenderStateEntity } from '../entities';
import { NODE_HEIGHT } from '../constants';
import { TreeService } from './tree-service';
const FILTER_NODE_TYPE = ['block', 'blockIcon', 'root', 'inlineBlocks'];
@injectable()
export class CustomLinesManager {
@inject(FlowDocument) declare document: FlowDocument;
@inject(EntityManager) declare entityManager: EntityManager;
// 额外的连线
@inject(TreeService) declare treeService: TreeService;
get edges(): EdgeItem[] {
return this.treeService.edges;
}
get treeNodes() {
return this.filterTreeNode(this.document.getAllNodes());
}
private _lines: CustomLine[] = [];
get lines() {
return this._lines;
}
set lines(lines: CustomLine[]) {
this._lines = lines;
}
/**
* 过滤其他的非渲染节点
*/
filterTreeNode(nodes: FlowNodeEntity[]) {
return nodes.filter(
node => !FILTER_NODE_TYPE.includes(node.flowNodeType as string),
);
}
getOutputFromInput(inputPoint: IPoint) {
return {
x: inputPoint.x,
y: inputPoint.y + NODE_HEIGHT,
};
}
bfsAddLine(root: FlowNodeEntity): CustomLine[] {
const queue: FlowNodeEntity[] = [root];
const result: CustomLine[] = [];
while (queue.length > 0) {
const node = queue.shift()!;
if (node.next) {
result.push({
from: node,
to: node.next,
fromPoint: this.getOutputFromInput(
node.getData(FlowNodeTransformData)?.inputPoint,
),
toPoint: node.next.getData(FlowNodeTransformData)?.inputPoint,
});
queue.push(node.next);
}
if (node.children?.length) {
if (node.flowNodeType === 'root') {
queue.push(node.children[0]);
} else if (node.flowNodeType === 'split') {
// 分支逻辑特殊处理
const inlineBlocksChildren = node.children[1]?.children || [];
const branchChildren =
inlineBlocksChildren
?.map(c => c?.children?.[0]?.children?.[0])
?.filter(Boolean) || [];
branchChildren.forEach(child => {
result.push({
from: node,
to: child,
fromPoint: this.getOutputFromInput(
node.getData(FlowNodeTransformData)?.inputPoint,
),
toPoint: child.getData(FlowNodeTransformData)?.inputPoint,
});
});
queue.push(...branchChildren);
} else {
node.children.forEach(child => {
result.push({
from: node,
to: child,
fromPoint: this.getOutputFromInput(
node.getData(FlowNodeTransformData)?.inputPoint,
),
toPoint: child.getData(FlowNodeTransformData)?.inputPoint,
});
});
queue.push(...node.children);
}
}
}
return result;
}
initLines() {
if (this._lines?.length) {
return;
}
this.renderLines();
}
renderLines() {
// 下一帧渲染,保证线条数据最新
requestAnimationFrame(() => {
const lines = this.bfsAddLine(this.document.originTree.root);
const extraLines: CustomLine[] = (this.edges || [])
.filter(edge => {
const from = this.entityManager.getEntityById<FlowNodeEntity>(
edge.from,
)!;
const to = this.entityManager.getEntityById<FlowNodeEntity>(edge.to)!;
return from && to && !edge.collapsed;
})
.map(edge => {
const from = this.entityManager.getEntityById<FlowNodeEntity>(
edge.from,
)!;
const to = this.entityManager.getEntityById<FlowNodeEntity>(edge.to)!;
return {
from,
to,
fromPoint: this.getOutputFromInput(
from.getData(FlowNodeTransformData)?.inputPoint,
),
toPoint: to.getData(FlowNodeTransformData)?.inputPoint,
};
});
this._lines = [...lines, ...extraLines];
const renderState = this.entityManager.getEntity<CustomRenderStateEntity>(
CustomRenderStateEntity,
);
renderState?.updateVersion();
});
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CustomLinesManager } from './custom-lines-manager';
export { CustomHoverService } from './custom-hover-service';
export { TreeService } from './tree-service';

View File

@@ -0,0 +1,505 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable max-params */
/* eslint-disable complexity */
import { nanoid } from 'nanoid';
import { inject, injectable } from 'inversify';
import {
FlowDocument,
type FlowNodeJSON,
} from '@flowgram-adapter/fixed-layout-editor';
import type {
DependencyTree,
DependencyTreeNode,
KnowledgeInfo,
PluginVersionInfo,
TableInfo,
} from '@coze-arch/bot-api/workflow_api';
import {
transformKnowledge,
transformPlugin,
transformTable,
isLoop,
} from '../utils';
import {
DependencyOrigin,
type EdgeItem,
NodeType,
type TreeNode,
} from '../typings';
@injectable()
export class TreeService {
@inject(FlowDocument) declare document: FlowDocument;
declare root: TreeNode;
edges: EdgeItem[] = [];
treeHistory: {
// 唯一标识符
id: string;
// 判断是否已经存在的两个条件
resourceId: string;
version?: string;
depth: number;
}[] = [];
declare globalJson: DependencyTree;
declare addChildrenArr: {
parentId: string;
json: FlowNodeJSON;
}[];
getUnCollapsedEdges() {
return this.edges.filter(e => !e.collapsed);
}
/**
* 用于检查是否有重复项
*/
getNodeDuplicateFromTree = (id: string, version?: string) => {
let duplicate = false;
const depth: number[] = [];
const dupId: string[] = [];
const matchArr = this.treeHistory.filter(
history =>
`${history.resourceId}${history.version || ''}` ===
`${id}${version || ''}`,
);
if (matchArr?.length) {
duplicate = true;
matchArr.forEach(item => {
depth.push(item.depth);
dupId.push(item.id);
});
}
return {
depth,
duplicate,
dupId,
};
};
traverseDFS(node: TreeNode, id: string): TreeNode | undefined {
if (node.id === id) {
return node;
} else {
if (!node.children?.length) {
return undefined;
}
for (const _node of node.children) {
const findNode = this.traverseDFS(_node, id);
if (findNode) {
return findNode;
}
}
return undefined;
}
}
getNodeByIdFromTree(id: string): TreeNode | undefined {
return this.traverseDFS(this.root, id);
}
/**
* 处理 knowledge、plugin、table 的逻辑
*/
transformDuplicateInfo = (
id: string,
info: KnowledgeInfo | PluginVersionInfo | TableInfo,
type: 'knowledge' | 'plugin' | 'table',
depth: number,
fromId: string,
) => {
const {
duplicate,
depth: dupDepth,
dupId,
} = this.getNodeDuplicateFromTree(id);
let newSchema: FlowNodeJSON;
if (type === 'knowledge') {
newSchema = transformKnowledge(depth + 1, info);
} else if (type === 'plugin') {
newSchema = transformPlugin(depth + 1, info);
} else {
newSchema = transformTable(depth + 1, info);
}
// 如果重复,判断添加线还是节点
if (duplicate) {
for (const [i, d] of dupDepth.entries()) {
// 只要有一个匹配,就加线。
// 兜底走加节点的逻辑
if (depth + 1 === d) {
// 存到线条中
this.edges.push({
from: fromId,
to: dupId[i],
});
return;
}
}
return newSchema;
}
// 否则,正常往 blocks 里添加数据
return newSchema;
};
dfsTransformNodeToSchema = (
depth: number,
node: DependencyTreeNode,
type?: NodeType,
parentId?: string,
): TreeNode | undefined => {
if (!node?.commit_id) {
return undefined;
}
let from = DependencyOrigin.APP;
if (node.is_library) {
from = DependencyOrigin.LIBRARY;
}
if (node.is_product) {
from = DependencyOrigin.SHOP;
}
const dependencies = node.dependency;
const children: TreeNode[] = [];
const nodeId = `${node.commit_id}_${nanoid(5)}`;
const {
duplicate,
depth: dupDepth,
dupId,
} = this.getNodeDuplicateFromTree(
node!.id as string,
node.workflow_version,
);
if (duplicate) {
for (const [i, d] of dupDepth.entries()) {
if (depth === d) {
this.edges.push({
from: parentId || '',
to: dupId[i],
});
return;
}
}
const loop = node.id && isLoop(node.id, this.globalJson);
// 重复节点,停止继续往下
const endData = {
id: nodeId,
type: 'custom',
data: {
id: node.id,
name: node.name,
type:
type || (node.is_chatflow ? NodeType.CHAT_FLOW : NodeType.WORKFLOW),
from,
collapsed: false,
version: node.workflow_version,
depth,
loop,
},
parent: [],
meta: {
isNodeEnd: true,
},
blocks: [],
};
return endData;
}
this.treeHistory.push({
id: nodeId,
resourceId: node.id as string,
version: node.workflow_version,
depth,
});
(dependencies?.knowledge_list || []).map(k => {
const newS = this.transformDuplicateInfo(
k.id as string,
k,
'knowledge',
depth,
nodeId,
);
if (newS) {
this.treeHistory.push({
id: newS.id,
resourceId: newS.data?.id,
version: newS.data?.version,
depth: newS.data?.depth,
});
children.push({
...newS,
data: newS.data || {},
type: newS.type as string,
parent: [],
children: [],
});
}
});
(dependencies?.table_list || []).map(t => {
const newS = this.transformDuplicateInfo(
t.id as string,
t,
'table',
depth,
nodeId,
);
if (newS) {
this.treeHistory.push({
id: newS.id,
resourceId: newS.data?.id,
version: newS.data?.version,
depth: newS.data?.depth,
});
children.push({
...newS,
data: newS.data || {},
type: newS.type as string,
parent: [],
children: [],
});
}
});
(dependencies?.plugin_version || []).map(p => {
const newS = this.transformDuplicateInfo(
p.id as string,
p,
'plugin',
depth,
nodeId,
);
if (newS) {
this.treeHistory.push({
id: newS.id,
resourceId: newS.data?.id,
version: newS.data?.version,
depth: newS.data?.depth,
});
children.push({
...newS,
data: newS.data || {},
type: newS.type as string,
parent: [],
children: [],
});
}
});
(dependencies?.workflow_version?.filter(v => v.id !== node.id) || []).map(
w => {
const workflowInfo = this.globalJson.node_list?.find(
_node => _node.id === w.id && _node.workflow_version === w.version,
);
const subWorkflowSchema = this.dfsTransformNodeToSchema(
depth + 1,
workflowInfo!,
undefined,
nodeId,
);
// 检测 workflow 的重复
if (subWorkflowSchema) {
this.treeHistory.push({
id: subWorkflowSchema.id,
resourceId: subWorkflowSchema.data?.id || '',
version: subWorkflowSchema.data?.version,
depth: subWorkflowSchema?.data?.depth,
});
children.push(subWorkflowSchema);
}
},
);
const isNodeEnd = !children.length;
return {
id: nodeId,
type: isNodeEnd ? 'custom' : 'split',
parent: [],
data: {
id: node.id,
name: node.name,
type:
type || (node.is_chatflow ? NodeType.CHAT_FLOW : NodeType.WORKFLOW),
from,
collapsed: false,
version: node.workflow_version,
depth,
},
...(isNodeEnd
? {
meta: {
isNodeEnd: true,
},
}
: {}),
children,
};
};
// 展开所有的子元素
dfsCloneCollapsedOpen(node: TreeNode): TreeNode {
const children = node.children?.map(c => this.dfsCloneCollapsedOpen(c));
return {
...node,
data: {
...node.data,
collapsed: false,
},
children,
};
}
// 根据 edges移动节点到另一个 TreeNode 的 children 下。
// 需要将内部的 collapsed 全部变成 open
cloneNode(node: TreeNode) {
return this.dfsCloneCollapsedOpen(node);
}
/**
* 绑定父元素 parent
*/
bindTreeParent(node: TreeNode, parent?: TreeNode) {
if (parent) {
node.parent.push(parent);
}
this.edges.forEach(edge => {
if (edge.to === node.id) {
const fromNode = this.getNodeByIdFromTree(edge.from);
if (fromNode) {
node.parent.push(fromNode);
}
}
});
node.children?.forEach(item => {
this.bindTreeParent(item, node);
});
}
/**
* 初始化数据,将数据从后端数据 json 变成 tree 结构
*/
transformSchema(json: DependencyTree) {
this.globalJson = json;
const { node_list = [] } = json;
const rootWorkflow = node_list.find(node => node.is_root);
if (!rootWorkflow) {
return undefined;
}
const root = this.dfsTransformNodeToSchema(0, rootWorkflow);
if (root) {
this.root = root;
// this.bindTreeParent(root);
}
}
dfsTreeJson(node: TreeNode): FlowNodeJSON {
let blocks: FlowNodeJSON[] = [];
const lines = this.getUnCollapsedEdges();
node?.children?.forEach(c => {
const connectLines = lines.filter(l => l.to === c.id);
if (c.data?.collapsed && connectLines?.length) {
const cloneNode = this.cloneNode(c);
connectLines.forEach(l => {
this.addChildrenArr.push({
parentId: l.from,
json: this.dfsTreeJson(cloneNode),
});
});
}
});
if (node?.children?.length) {
blocks = node.children
.filter(c => !c?.data?.collapsed)
.map(c => ({
id: `${c.id}_block`,
type: 'block',
blocks: [this.dfsTreeJson(c)],
}));
}
return {
id: node?.id,
type: node?.type,
data: node?.data,
meta: node?.meta,
blocks,
};
}
addNode(
json: FlowNodeJSON,
addItem: {
parentId: string;
json: FlowNodeJSON;
},
) {
if (json.id === addItem.parentId) {
const blockItem = {
id: `${addItem.json.id}_block`,
type: 'block',
blocks: [addItem.json],
};
if (json.blocks) {
json.blocks.push(blockItem);
} else {
json.blocks = [blockItem];
}
} else {
json.blocks?.forEach(block => {
this.addNode(block, addItem);
});
}
// 可能因此原本设置为结束的节点现在有 child 了
if (json.blocks?.length) {
if (json.type === 'custom') {
json.type = 'split';
}
if (json.meta) {
json.meta.isNodeEnd = false;
} else {
json.meta = {
isNodeEnd: true,
};
}
}
}
treeToFlowNodeJson() {
this.addChildrenArr = [];
const rootNodeJson = this.dfsTreeJson(this.root);
if (!rootNodeJson || !rootNodeJson?.id) {
const json = {
nodes: [],
};
this.document.fromJSON(json);
return json;
}
const json2 = {
nodes: [rootNodeJson],
};
this.addChildrenArr.forEach(item => {
this.addNode(rootNodeJson, item);
});
this.document.fromJSON(json2);
return json2;
}
}