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,91 @@
/*
* 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 React, { useRef, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozWarningCircleFill } from '@coze-arch/coze-design/icons';
import { Modal } from '@coze-arch/coze-design';
import { Layout } from '../layout';
import { type EditorProps, type LanguageType } from '../../interface';
import { Editor } from './editor';
export const BizEditor = (props: EditorProps) => {
const editorApi = useRef<undefined | { getValue?: () => string }>(undefined);
const [language, setLanguage] = useState<LanguageType>(props.defaultLanguage);
const [content, setContent] = useState<string | undefined>(
props.defaultContent,
);
const handleLanguageChange = (value: LanguageType) => {
const langTemplate = props.languageTemplates?.find(
e => e.language === value,
);
const preLangTemplate = props.languageTemplates?.find(
e => e.language === language,
);
if (preLangTemplate?.template === editorApi.current?.getValue?.()) {
setLanguage(value);
setContent(langTemplate?.template);
props.onChange?.(langTemplate?.template || '', value);
return;
}
Modal.warning({
icon: (
<IconCozWarningCircleFill
style={{ color: 'rgba(var(--coze-yellow-5), 1)' }}
/>
),
title: I18n.t('code_node_switch_language'),
content: I18n.t('code_node_switch_language_description'),
okType: 'warning',
okText: I18n.t('Confirm'),
cancelText: I18n.t('Cancel'),
closable: true,
width: 448,
height: 160,
onOk: () => {
setLanguage(value);
setContent(langTemplate?.template);
props.onChange?.(langTemplate?.template || '', value);
},
});
};
return (
<>
<Layout
{...props}
language={language}
onLanguageSelect={handleLanguageChange}
>
<Editor
{...props}
defaultContent={content}
language={language}
didMount={api => {
editorApi.current = api;
}}
/>
</Layout>
</>
);
};

View File

@@ -0,0 +1,54 @@
/*
* 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 React, { type FC, lazy, Suspense, useMemo } from 'react';
import { type EditorOtherProps, type EditorProps } from '../../interface';
const LazyPythonEditor: FC<EditorProps> = lazy(async () => {
const { PythonEditor } = await import('./python-editor');
return { default: PythonEditor };
});
const PythonEditor: FC<EditorProps> = props => (
<Suspense>
<LazyPythonEditor {...props} />
</Suspense>
);
const LazyTypescriptEditor: FC<EditorProps> = lazy(async () => {
const { TypescriptEditor } = await import('./typescript-editor');
return { default: TypescriptEditor };
});
const TypescriptEditor: FC<EditorProps> = props => (
<Suspense>
<LazyTypescriptEditor {...props} />
</Suspense>
);
export const Editor = (props: EditorProps & EditorOtherProps) => {
const language = useMemo(
() => props.language || props.defaultLanguage,
[props.defaultLanguage, props.language],
);
if (language === 'python') {
return <PythonEditor {...props} />;
}
return <TypescriptEditor {...props} />;
};

View File

@@ -0,0 +1,18 @@
/*
* 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 { Editor } from './editor';
export { BizEditor } from './biz-editor';

View File

@@ -0,0 +1,41 @@
/*
* 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 { api, type InferEditorAPIFromPlugins } from '@coze-editor/editor/react';
import preset from '@coze-editor/editor/preset-code';
import { type EditorView } from '@codemirror/view';
// 忽略 readOnly 强制设置值
const forceSetValue =
({ view }: { view: EditorView }) =>
(value: string) => {
const { state } = view;
view.dispatch(
state.update({
changes: {
from: 0,
to: state.doc.length,
insert: value ?? '',
},
}),
);
};
const customPreset = [...preset, api('forceSetValue', forceSetValue)];
export type EditorAPI = InferEditorAPIFromPlugins<typeof customPreset>;
export default customPreset;

View File

@@ -0,0 +1,85 @@
/*
* 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 React from 'react';
import { Renderer, EditorProvider } from '@coze-editor/editor/react';
import { languages } from '@coze-editor/editor/preset-code';
import { python } from '@coze-editor/editor/language-python';
import { EditorView } from '@codemirror/view';
import { type EditorOtherProps, type EditorProps } from '../../interface';
import preset from './preset';
languages.register('python', python);
export const PythonEditor = (props: EditorProps & EditorOtherProps) => {
const {
defaultContent,
uuid,
readonly,
height,
didMount,
onChange,
defaultLanguage,
} = props;
return (
<EditorProvider>
<Renderer
plugins={preset}
domProps={{
style: {
height: 'calc(100% - 48px)',
},
}}
didMount={api => {
didMount?.(api);
api.$on('change', ({ value }) => {
onChange?.(value, defaultLanguage);
});
}}
defaultValue={defaultContent}
extensions={[
EditorView.theme({
'&.cm-focused': {
outline: 'none',
},
'&.cm-editor': {
height: height || 'unset',
},
'.cm-content': {
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
},
'.cm-content *': {
fontFamily: 'inherit',
},
}),
]}
options={{
uri: `file:///py_editor_${uuid}.py`,
languageId: 'python',
theme: 'code-editor-dark',
height,
readOnly: readonly,
editable: !readonly,
fontSize: 12,
tabSize: 4,
}}
/>
</EditorProvider>
);
};

View File

@@ -0,0 +1,161 @@
/*
* 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 { ViewVariableType } from '@coze-workflow/base';
import { languages } from '@coze-editor/editor/preset-code';
import { typescript } from '@coze-editor/editor/language-typescript';
import { type Input, ModuleDetectionKind, type Output } from '../../interface';
export const initTypescriptServer = () => {
languages.register('typescript', typescript);
const tsWorker = new Worker(
new URL('@coze-editor/editor/language-typescript/worker', import.meta.url),
{ type: 'module' },
);
typescript.languageService.initialize(tsWorker, {
compilerOptions: {
// eliminate Promise error
lib: ['es2015'],
moduleDetection: ModuleDetectionKind.Force,
},
});
};
const mapVariableType = (type?: ViewVariableType): string => {
switch (type) {
case ViewVariableType.String:
return 'string';
case ViewVariableType.Integer:
return 'number';
case ViewVariableType.Boolean:
return 'boolean';
case ViewVariableType.Number:
return 'number';
case ViewVariableType.Object:
return 'object';
case ViewVariableType.Image:
case ViewVariableType.File:
case ViewVariableType.Doc:
case ViewVariableType.Code:
case ViewVariableType.Ppt:
case ViewVariableType.Txt:
case ViewVariableType.Excel:
case ViewVariableType.Audio:
case ViewVariableType.Zip:
case ViewVariableType.Video:
case ViewVariableType.Svg:
case ViewVariableType.Voice:
return 'string'; // Assuming file-like types are represented as strings (e.g., file paths or URLs)
case ViewVariableType.Time:
return 'Date';
case ViewVariableType.ArrayString:
return 'string[]';
case ViewVariableType.ArrayInteger:
return 'number[]';
case ViewVariableType.ArrayBoolean:
return 'boolean[]';
case ViewVariableType.ArrayNumber:
return 'number[]';
case ViewVariableType.ArrayObject:
return 'object[]';
case ViewVariableType.ArrayImage:
case ViewVariableType.ArrayFile:
case ViewVariableType.ArrayDoc:
case ViewVariableType.ArrayCode:
case ViewVariableType.ArrayPpt:
case ViewVariableType.ArrayTxt:
case ViewVariableType.ArrayExcel:
case ViewVariableType.ArrayAudio:
case ViewVariableType.ArrayZip:
case ViewVariableType.ArrayVideo:
case ViewVariableType.ArraySvg:
case ViewVariableType.ArrayVoice:
return 'string[]';
case ViewVariableType.ArrayTime:
return 'Date[]';
default:
return 'any'; // Fallback type
}
};
export function generateTypeDefinition(output: Output, indent = ' '): string {
let definition = '';
if (output.type === ViewVariableType.Object) {
definition += `${indent}${output.name}: {\n`;
if (output.children && output.children.length > 0) {
output.children.forEach(child => {
definition += `${indent} ${generateTypeDefinition(child, `${indent} `)}`;
});
}
definition += `${indent}}\n`;
return definition;
}
if (output.type === ViewVariableType.ArrayObject) {
definition += `${indent}${output.name}: {\n`;
if (output.children && output.children.length > 0) {
output.children.forEach(child => {
definition += `${indent} ${generateTypeDefinition(child, `${indent} `)}`;
});
definition += `${indent}}[]\n`;
}
return definition;
}
definition += `${indent}${output.name}: ${mapVariableType(output.type)};\n`;
definition += '\n';
return definition;
}
export const initInputAndOutput = async (
inputs: Input[] = [],
outputs: Output[] = [],
uuid = '',
): Promise<void> => {
const typeDefinition = `
declare module '/ts_editor_${uuid}' {
interface Args {
${generateTypeDefinition(
{
name: 'params',
type: ViewVariableType.Object,
children: inputs,
},
' ',
)}
}
interface Output {
${outputs.map(output => generateTypeDefinition(output, ' ')).join('\n')}
}
}
export {}
`;
await typescript.languageService.addExtraFiles({
[`/ts_editor_${uuid}.d.ts`]: typeDefinition,
});
};

View File

@@ -0,0 +1,95 @@
/*
* 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 React, { useEffect } from 'react';
import { Renderer, EditorProvider } from '@coze-editor/editor/react';
import { EditorView } from '@codemirror/view';
import { type EditorOtherProps, type EditorProps } from '../../interface';
import {
initInputAndOutput,
initTypescriptServer,
} from './typescript-editor-utils';
import preset from './preset';
initTypescriptServer();
export const TypescriptEditor = (props: EditorProps & EditorOtherProps) => {
const {
defaultContent,
uuid,
readonly,
height,
didMount,
onChange,
defaultLanguage,
input,
output,
} = props;
const uri = `file:///ts_editor_${uuid}.ts`;
useEffect(() => {
initInputAndOutput(input, output, uuid);
}, [uuid]);
return (
<EditorProvider>
<Renderer
plugins={preset}
domProps={{
style: {
height: 'calc(100% - 48px)',
},
}}
didMount={api => {
didMount?.(api);
api.$on('change', ({ value }) => {
onChange?.(value, defaultLanguage);
});
}}
defaultValue={defaultContent}
extensions={[
EditorView.theme({
'&.cm-focused': {
outline: 'none',
},
'&.cm-editor': {
height: height || 'unset',
},
'.cm-content': {
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
},
'.cm-content *': {
fontFamily: 'inherit',
},
}),
]}
options={{
uri,
languageId: 'typescript',
theme: 'code-editor-dark',
height,
readOnly: readonly,
editable: !readonly,
fontSize: 12,
tabSize: 4,
}}
/>
</EditorProvider>
);
};

View File

@@ -0,0 +1,150 @@
/*
* 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 React, { useMemo, type ReactNode } from 'react';
import { concatTestId, useNodeTestId } from '@coze-workflow/base';
import {
IconCozCodeFill,
IconCozPlayCircle,
IconCozSideCollapse,
} from '@coze-arch/coze-design/icons';
import {
Select,
Tooltip,
Button,
IconButton,
Typography,
} from '@coze-arch/coze-design';
const { Text } = Typography;
import { type EditorProps, type LanguageType } from '../../interface';
import style from './style.module.less';
import { I18n } from '@coze-arch/i18n';
const HELP_DOCUMENT_LINK = IS_OVERSEA
? '/docs/guides/code_node?_lang=en'
: '/docs/guides/code_node';
interface Props extends EditorProps {
children: ReactNode;
onLanguageSelect?: (language: LanguageType) => void;
language: LanguageType;
}
export const Layout = ({
children,
title,
language,
onClose,
onTestRun,
testRunIcon,
onLanguageSelect,
languageTemplates,
}: Props) => {
const optionList = useMemo(
() =>
languageTemplates?.map(e => ({
value: e.language,
label: e.displayName,
})),
[languageTemplates],
);
const { getNodeSetterId } = useNodeTestId();
const setterTestId = getNodeSetterId('biz-editor-layout');
return (
<div className={style.container}>
<div className={style.header}>
<div className={style.title}>
<div className={style['title-icon']}>
<IconCozCodeFill />
</div>
<div className={style['title-content']}>{title}</div>
<Tooltip
content={
<div>
{I18n.t('code_node_more_info')}
<Text link={{ href: HELP_DOCUMENT_LINK, target: '_blank' }}>
{I18n.t('code_node_help_doc')}
</Text>
</div>
}
theme={'dark'}
>
<Select
onChange={value => onLanguageSelect?.(value as LanguageType)}
value={language}
data-testid={concatTestId(setterTestId, 'language-select')}
renderSelectedItem={item => (
<span
style={{
fontSize: 12,
color: 'var(--coz-fg-secondary)',
}}
>
{I18n.t('code_node_language')} {item.label}
</span>
)}
size={'small'}
optionList={optionList}
></Select>
</Tooltip>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<Button
color={'highlight'}
data-testid={concatTestId(setterTestId, 'test-run')}
icon={
testRunIcon ? (
<span
style={{
fontSize: 14,
display: 'flex',
alignItems: 'center',
}}
>
{testRunIcon}
</span>
) : (
<IconCozPlayCircle style={{ fontSize: 14 }} />
)
}
size={'small'}
onClick={onTestRun}
>
{I18n.t('code_node_test_code')}
</Button>
<IconButton
onClick={onClose}
color={'secondary'}
size={'small'}
icon={<IconCozSideCollapse style={{ fontSize: 18 }} />}
data-testid={concatTestId(setterTestId, 'expand-button')}
/>
</div>
</div>
{children}
</div>
);
};

View File

@@ -0,0 +1,48 @@
.container {
width: 100%;
height: 100%;
.header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
min-width: 214px;
height: 48px;
margin-bottom: 0;
padding: 12px;
}
.title {
display: flex;
flex: 1;
gap: 8px;
align-items: center;
}
.title-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 18px;
color: #fff;
background-color: #28CAC8;
border-radius: 4px;
}
.title-content {
min-width: 0;
max-width: calc(100% - 160px);
font-size: 16px;
font-weight: 500;
color: var(--coz-fg-primary);
white-space: nowrap;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 React, { useEffect, useRef } from 'react';
import { type EditorAPI } from '../editor/preset';
import { Editor } from '../editor';
import { type PreviewerProps } from '../../interface';
export const Previewer = (props: PreviewerProps) => {
const apiRef = useRef<EditorAPI | null>();
useEffect(() => {
if (!apiRef.current) {
return;
}
if (props.content !== apiRef.current.getValue()) {
apiRef.current.forceSetValue(props.content);
}
}, [props.content]);
return (
<Editor
uuid={`previewer_${new Date().getTime()}`}
height={`${props.height}px`}
defaultLanguage={props.language}
defaultContent={props.content}
readonly
didMount={api => {
apiRef.current = api;
}}
/>
);
};

View File

@@ -0,0 +1,60 @@
/*
* 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 { createTheme } from '@coze-editor/editor/preset-code';
import { type Extension } from '@codemirror/state';
const colors = {
background: '#151B27',
};
export const createDarkTheme: () => Extension = () =>
createTheme({
variant: 'dark',
settings: {
background: colors.background,
foreground: '#fff',
caret: '#AEAFAD',
selection: '#d9d9d942',
gutterBackground: colors.background,
gutterForeground: '#FFFFFF63',
gutterBorderColor: 'transparent',
gutterBorderWidth: 0,
lineHighlight: '#272e3d36',
bracketColors: ['#FFEF61', '#DD99FF', '#78B0FF'],
tooltip: {
backgroundColor: '#363D4D',
color: '#fff',
border: 'none',
},
completionItemHover: {
backgroundColor: '#FFFFFF0F',
},
completionItemSelected: {
backgroundColor: '#FFFFFF17',
},
completionItemIcon: {
color: '#FFFFFFC9',
},
completionItemLabel: {
color: '#FFFFFFC9',
},
completionItemDetail: {
color: '#FFFFFF63',
},
},
styles: [],
});