4 Commits

Author SHA1 Message Date
v-zhangjc9
35c5150a1f feat(web): 入参增加类型 2025-07-15 17:32:23 +08:00
v-zhangjc9
a7b245a670 refractor(web): 修复入参解析 2025-07-15 16:27:00 +08:00
v-zhangjc9
df8270676a feat(web): 增加循环节点的基本参数 2025-07-15 11:11:36 +08:00
v-zhangjc9
b0e4f5853e feat(web): 增加循环节点的输出变量 2025-07-15 09:09:23 +08:00
11 changed files with 191 additions and 114 deletions

View File

@@ -1,10 +1,11 @@
import {type Edge, getIncomers, type Node} from '@xyflow/react' import {type Edge, getIncomers, type Node} from '@xyflow/react'
import type {Option} from 'amis/lib/Schema' import type {Option} from 'amis/lib/Schema'
import {find, has, isEmpty, isEqual, max, min, unique} from 'licia' import {find, has, isEqual, max, min, unique} from 'licia'
import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react' import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react'
import Queue from 'yocto-queue' import Queue from 'yocto-queue'
import {originTypeMap} from '../../pages/ai/task/InputSchema.tsx'
import {useFlowStore} from './store/FlowStore.ts' import {useFlowStore} from './store/FlowStore.ts'
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts' import {type OutputVariable} from './types.ts'
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => { export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
let queue = new Queue<Node>() let queue = new Queue<Node>()
@@ -20,36 +21,17 @@ export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) =
return unique(result, (a, b) => isEqual(a, b)) return unique(result, (a, b) => isEqual(a, b))
} }
export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges: Edge[], data: any) => { export const getAllIncomerNodeOutputVariables: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => OutputVariable[] = (id, inputSchema, nodes, edges, data) => {
id: string, let inputSchemaVariables: OutputVariable[] = Object.keys(inputSchema).map(key => ({
variable: string group: '流程入参',
}[] = (id, nodes, edges, data) => { name: `${key}${inputSchema[key]?.label ? ` (${inputSchema[key].label})` : ''}`,
let incomerIds = getAllIncomerNodeById(id, nodes, edges) type: originTypeMap[inputSchema[key]?.type ?? ''],
let incomerVariables: { id: string, variable: string }[] = [] variable: key,
for (const incomerId of incomerIds) {
let nodeData = data[incomerId] ?? {}
if (has(nodeData, 'outputs')) {
let outputs = nodeData?.outputs ?? []
for (const output of Object.keys(outputs)) {
incomerVariables.push({
id: incomerId,
variable: output,
})
}
}
}
return incomerVariables
}
export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => {
let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({
label: `${key} (${inputSchema[key]?.label ?? ''})`,
value: key,
})) }))
let currentNode = find(nodes, n => isEqual(id, n.id)) let currentNode = find(nodes, n => isEqual(id, n.id))
if (!currentNode) { if (!currentNode) {
return [] return inputSchemaVariables
} }
let incomerIds = getAllIncomerNodeById(id, nodes, edges) let incomerIds = getAllIncomerNodeById(id, nodes, edges)
@@ -60,7 +42,7 @@ export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSch
] ]
} }
let incomerVariables: InputFormOptionsGroup[] = [] let incomerVariables: OutputVariable[] = []
for (const incomerId of incomerIds) { for (const incomerId of incomerIds) {
let nodeData = data[incomerId] ?? {} let nodeData = data[incomerId] ?? {}
let group = incomerId let group = incomerId
@@ -68,48 +50,73 @@ export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSch
group = `${nodeData.node.name} ${incomerId}` group = `${nodeData.node.name} ${incomerId}`
} }
if (has(nodeData, 'outputs')) { if (has(nodeData, 'outputs')) {
let outputs = nodeData?.outputs ?? [] let outputs = nodeData?.outputs ?? {}
incomerVariables.push({ for (const key of Object.keys(outputs)) {
group: group, incomerVariables.push({
variables: Object.keys(outputs).map(key => ({ group: group,
value: `${incomerId}.${key}`, name: key,
label: key, type: outputs[key].type,
})), variable: `${incomerId}.${key}`,
}) })
}
} }
} }
let inputVariables = [ return [
...(isEmpty(inputSchemaVariables) ? [] : [ ...inputSchemaVariables,
{
group: '流程入参',
variables: inputSchemaVariables,
},
]),
...(currentNode.parentId ? [ ...(currentNode.parentId ? [
{ {
group: '循环入参', group: '循环入参',
variables: [ name: 'loopIndex (当前迭代索引)',
{ type: 'number',
label: 'loopIndex (当前迭代索引)', variable: 'loopIndex',
value: 'loopIndex', } as OutputVariable,
}, {
{ group: '循环入参',
label: 'loopItem (当前迭代对象)', name: 'loopItem (当前迭代对象)',
value: 'loopItem', type: 'object',
} variable: 'loopItem',
] } as OutputVariable,
}
] : []), ] : []),
...incomerVariables, ...incomerVariables,
] ]
}
return [ export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => {
...inputVariables.map(item => ({ let optionMap: Record<string, Option[]> = {}
label: item.group, for (const item of getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data)) {
children: item.variables, if (!optionMap[item.group]) {
})), optionMap[item.group] = []
] }
optionMap[item.group].push({
label: item.name,
value: item.variable,
})
}
return Object.keys(optionMap)
.map(key => ({
label: key,
children: optionMap[key],
}))
}
export const generateAllIncomerOutputVariablesConditions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => {
let optionMap: Record<string, Option[]> = {}
for (const item of getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data)) {
if (!optionMap[item.group]) {
optionMap[item.group] = []
}
optionMap[item.group].push({
label: item.name,
type: item.type,
name: item.variable,
})
}
return Object.keys(optionMap)
.map(key => ({
label: key,
children: optionMap[key],
}))
} }
// 处理循环节点的边界问题 // 处理循环节点的边界问题

View File

@@ -15,10 +15,7 @@ const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
if (has(nodeData, field)) { if (has(nodeData, field)) {
let expression = nodeData?.[field] ?? '' let expression = nodeData?.[field] ?? ''
if (!isEmpty(expression)) { if (!isEmpty(expression)) {
let outputVariables = new Set([ let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data).map(i => i.variable))
...getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => `${i.id}.${i.variable}`),
...Object.keys(inputSchema),
])
if (!outputVariables.has(expression)) { if (!outputVariables.has(expression)) {
return { return {
error: true, error: true,
@@ -36,10 +33,7 @@ const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, d
if (has(nodeData, 'inputs')) { if (has(nodeData, 'inputs')) {
let inputs = nodeData?.inputs ?? {} let inputs = nodeData?.inputs ?? {}
if (!isEmpty(inputs)) { if (!isEmpty(inputs)) {
let outputVariables = new Set([ let outputVariables = new Set(getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data).map(i => i.variable))
...getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => `${i.id}.${i.variable}`),
...Object.keys(inputSchema),
])
for (const key of Object.keys(inputs)) { for (const key of Object.keys(inputs)) {
let variable = inputs[key]?.variable ?? '' let variable = inputs[key]?.variable ?? ''
if (!outputVariables.has(variable)) { if (!outputVariables.has(variable)) {

View File

@@ -1,20 +1,21 @@
import {PlusCircleFilled} from '@ant-design/icons'
import {Button, Dropdown} from 'antd'
import type {ButtonProps} from 'antd/lib'
import {isEqual, randomId, unique} from 'licia' import {isEqual, randomId, unique} from 'licia'
import {NodeRegistry, NodeRegistryMap} from '../NodeRegistry.tsx'
import {commonInfo} from '../../../util/amis.tsx' import {commonInfo} from '../../../util/amis.tsx'
import {checkAddNode} from '../FlowChecker.tsx' import {checkAddNode} from '../FlowChecker.tsx'
import {Button, Dropdown} from 'antd' import {NodeRegistry, NodeRegistryMap} from '../NodeRegistry.tsx'
import {PlusCircleFilled} from '@ant-design/icons'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import type {ButtonProps} from 'antd/lib'
export type AddNodeButtonProps = { export type AddNodeButtonProps = ButtonProps & {
parent?: string parent?: string
} & ButtonProps onlyIcon?: boolean
}
const AddNodeButton = (props: AddNodeButtonProps) => { const AddNodeButton = (props: AddNodeButtonProps) => {
const {data, setDataById} = useDataStore() const {data, setDataById} = useDataStore()
const {nodes, addNode, edges,} = useFlowStore() const {nodes, addNode, edges} = useFlowStore()
return ( return (
<Dropdown <Dropdown
menu={{ menu={{
@@ -57,7 +58,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => {
...(props.parent ? { ...(props.parent ? {
parentId: props.parent, parentId: props.parent,
extent: 'parent', extent: 'parent',
} : {}) } : {}),
}) })
} catch (e) { } catch (e) {
// @ts-ignore // @ts-ignore
@@ -68,7 +69,7 @@ const AddNodeButton = (props: AddNodeButtonProps) => {
> >
<Button {...props}> <Button {...props}>
<PlusCircleFilled/> <PlusCircleFilled/>
{props.onlyIcon ? undefined : '新增节点'}
</Button> </Button>
</Dropdown> </Dropdown>
) )

View File

@@ -8,6 +8,7 @@ import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx' import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
import {useDataStore} from '../store/DataStore.ts' import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts' import {useFlowStore} from '../store/FlowStore.ts'
import {OutputVariableTypeMap} from '../types.ts'
export function inputsFormColumns( export function inputsFormColumns(
nodeId: string, nodeId: string,
@@ -70,24 +71,11 @@ export function outputsFormColumns(editable: boolean = false, required: boolean
label: '参数', label: '参数',
required: true, required: true,
selectFirst: true, selectFirst: true,
options: [ options: Object.keys(OutputVariableTypeMap).map(key => ({
{ // @ts-ignore
label: '文本', label: OutputVariableTypeMap[key],
value: 'string', value: key,
}, })),
{
label: '数字',
value: 'number',
},
{
label: '文本数组',
value: 'array-string',
},
{
label: '对象数组',
value: 'array-object',
},
],
}, },
], ],
}, },

View File

@@ -1,17 +1,52 @@
import {Background, BackgroundVariant, type NodeProps} from '@xyflow/react' import {Background, BackgroundVariant, type NodeProps} from '@xyflow/react'
import {classnames} from 'amis' import {classnames} from 'amis'
import React from 'react' import React, {useCallback, useEffect} from 'react'
import {flowBackgroundColor, flowDotColor} from '../types.ts'
import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx'
import AddNodeButton from '../component/AddNodeButton.tsx' import AddNodeButton from '../component/AddNodeButton.tsx'
import {useDataStore} from '../store/DataStore.ts'
import {flowBackgroundColor, flowDotColor} from '../types.ts'
import AmisNode, {nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
const LoopNode = (props: NodeProps) => { const LoopNode = (props: NodeProps) => {
const {mergeDataById} = useDataStore()
useEffect(() => {
mergeDataById(
props.id,
{
outputs: {
output: {
type: 'array-object',
},
},
},
)
}, [props.id])
const columnsSchema = useCallback(() => [
{
type: 'switch',
name: 'failFast',
label: '快速失败',
description: '执行过程中一旦出现错误,及时中断循环任务的执行',
},
{
disabled: true,
type: 'switch',
name: 'parallel',
label: '并行执行',
},
{
type: 'divider',
},
...outputsFormColumns(false, true),
], [props.id])
return ( return (
<AmisNode <AmisNode
className={classnames('w-full', 'h-full', nodeClassName('loop'))} className={classnames('w-full', 'h-full', nodeClassName('loop'))}
style={{ style={{
minWidth: '350px', minWidth: '350px',
minHeight: '290px' minHeight: '290px',
}} }}
nodeProps={props} nodeProps={props}
extraNodeDescription={ extraNodeDescription={
@@ -28,9 +63,10 @@ const LoopNode = (props: NodeProps) => {
color={flowDotColor} color={flowDotColor}
bgColor={flowBackgroundColor} bgColor={flowBackgroundColor}
/> />
<AddNodeButton className="mt-2 ml-2" parent={props.id}/> <AddNodeButton className="mt-2 ml-2" parent={props.id} onlyIcon/>
</div> </div>
} }
columnSchema={columnsSchema}
handler={<NormalNodeHandler/>} handler={<NormalNodeHandler/>}
resize={{ resize={{
minWidth: 350, minWidth: 350,

View File

@@ -1,6 +1,10 @@
import {Handle, type NodeProps, Position} from '@xyflow/react' import {Handle, type NodeProps, Position} from '@xyflow/react'
import {Tag} from 'antd' import {Tag} from 'antd'
import React from 'react' import React, {useCallback} from 'react'
import {generateAllIncomerOutputVariablesConditions} from '../Helper.tsx'
import {useContextStore} from '../store/ContextStore.ts'
import {useDataStore} from '../store/DataStore.ts'
import {useFlowStore} from '../store/FlowStore.ts'
import AmisNode, {nodeClassName} from './AmisNode.tsx' import AmisNode, {nodeClassName} from './AmisNode.tsx'
const cases = [ const cases = [
@@ -16,6 +20,25 @@ const cases = [
] ]
const SwitchNode = (props: NodeProps) => { const SwitchNode = (props: NodeProps) => {
const {getNodes, getEdges} = useFlowStore()
const {getData} = useDataStore()
const {getInputSchema} = useContextStore()
const columnsSchema = useCallback(() => [
{
type: 'condition-builder',
name: 'condition',
label: '判断条件',
fields: generateAllIncomerOutputVariablesConditions(
props.id,
getInputSchema(),
getNodes(),
getEdges(),
getData(),
),
},
], [props.id])
return ( return (
<AmisNode <AmisNode
className={nodeClassName('switch')} className={nodeClassName('switch')}
@@ -29,6 +52,7 @@ const SwitchNode = (props: NodeProps) => {
))} ))}
</div> </div>
} }
columnSchema={columnsSchema}
handler={ handler={
<> <>
<Handle type="target" position={Position.Left}/> <Handle type="target" position={Position.Left}/>

View File

@@ -1,8 +1,8 @@
import type {Edge, Node} from '@xyflow/react' import type {Edge, Node} from '@xyflow/react'
import type {JSX} from 'react' import type {JSX} from 'react'
export const flowBackgroundColor = "#fafafa" export const flowBackgroundColor = '#fafafa'
export const flowDotColor = "#dedede" export const flowDotColor = '#dedede'
export type InputFormOptions = { export type InputFormOptions = {
label: string label: string
@@ -29,6 +29,17 @@ export type FlowEditorProps = {
onGraphDataChange: (graphData: GraphData) => void, onGraphDataChange: (graphData: GraphData) => void,
} }
export type OutputVariableType = 'text' | 'boolean' | 'number' | 'object' | 'array-text' | 'array-object'
export const OutputVariableTypeMap: Record<OutputVariableType, string> = {
'text': '文本',
'boolean': '布尔值',
'number': '数字',
'object': '对象',
'array-text': '文本数组',
'array-object': '对象数组',
}
export type NodeDefine = { export type NodeDefine = {
key: string, key: string,
group: string, group: string,
@@ -38,3 +49,10 @@ export type NodeDefine = {
component: any, component: any,
checkers: NodeChecker[], checkers: NodeChecker[],
} }
export type OutputVariable = {
group: string,
name: string | undefined,
type: OutputVariableType,
variable: string,
}

View File

@@ -4,7 +4,7 @@ import type {GraphData} from '../components/flow/types.ts'
function Test() { function Test() {
// language=JSON // language=JSON
const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 135\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}')) const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 135\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "text"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}'))
return ( return (
<div className="h-screen"> <div className="h-screen">

View File

@@ -1,4 +1,5 @@
import type {Schema} from 'amis' import type {Schema} from 'amis'
import type {OutputVariableType} from '../../../components/flow/types.ts'
import {commonInfo, formInputFileStaticColumns} from '../../../util/amis.tsx' import {commonInfo, formInputFileStaticColumns} from '../../../util/amis.tsx'
export const typeMap: Record<string, string> = { export const typeMap: Record<string, string> = {
@@ -8,6 +9,13 @@ export const typeMap: Record<string, string> = {
files: '文件', files: '文件',
} }
export const originTypeMap: Record<string, OutputVariableType> = {
text: 'text',
textarea: 'text',
number: 'number',
files: 'array-text',
}
export type InputField = { export type InputField = {
type: string type: string
label: string label: string

View File

@@ -73,8 +73,8 @@ const FlowTaskTemplateEdit: React.FC = () => {
navigate(-1) navigate(-1)
}, },
}, },
] ],
} },
}, },
body: [ body: [
{ {

View File

@@ -3,7 +3,8 @@ import axios from 'axios'
import React, {useState} from 'react' import React, {useState} from 'react'
import {useNavigate, useParams} from 'react-router' import {useNavigate, useParams} from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import FlowEditor, {type GraphData} from '../../../../components/flow/FlowEditor.tsx' import FlowEditor from '../../../../components/flow/FlowEditor.tsx'
import type {GraphData} from '../../../../components/flow/types.ts'
import {commonInfo} from '../../../../util/amis.tsx' import {commonInfo} from '../../../../util/amis.tsx'
const FlowTaskTemplateFlowEditDiv = styled.div` const FlowTaskTemplateFlowEditDiv = styled.div`
@@ -19,8 +20,8 @@ const FlowTaskTemplateFlowEdit: React.FC = () => {
let {data} = await axios.get( let {data} = await axios.get(
`${commonInfo.baseAiUrl}/flow_task/template/detail/${template_id}`, `${commonInfo.baseAiUrl}/flow_task/template/detail/${template_id}`,
{ {
headers: commonInfo.authorizationHeaders headers: commonInfo.authorizationHeaders,
} },
) )
setInputSchema(data?.data?.inputSchema) setInputSchema(data?.data?.inputSchema)
setGraphData(data?.data?.flowGraph) setGraphData(data?.data?.flowGraph)
@@ -36,11 +37,11 @@ const FlowTaskTemplateFlowEdit: React.FC = () => {
`${commonInfo.baseAiUrl}/flow_task/template/update_flow_graph`, `${commonInfo.baseAiUrl}/flow_task/template/update_flow_graph`,
{ {
id: template_id, id: template_id,
graph: data graph: data,
}, },
{ {
headers: commonInfo.authorizationHeaders headers: commonInfo.authorizationHeaders,
} },
) )
navigate(-1) navigate(-1)
}} }}