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,53 @@
/*
* 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 { useUserInfo } from '@coze-arch/foundation-sdk';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { useParams } from 'react-router-dom';
import { publishAnchorService } from '@/service/connector-anchor';
export const useBizConnectorAnchor = () => {
const userId = useUserInfo()?.user_id_str;
const projectId = useParams<DynamicParams>().project_id;
const setAnchor = (connectorId: string) => {
if (!userId || !projectId) {
return;
}
return publishAnchorService.setAnchor({ projectId, userId, connectorId });
};
const getAnchor = () => {
if (!userId || !projectId) {
return;
}
return publishAnchorService.getAnchor({ userId, projectId });
};
const removeAnchor = () => {
if (!userId || !projectId) {
return;
}
return publishAnchorService.removeAnchor({ userId, projectId });
};
return {
setAnchor,
getAnchor,
removeAnchor,
};
};

View File

@@ -0,0 +1,299 @@
/*
* 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 complexity */
/* eslint-disable @coze-arch/max-line-per-function */
import { useParams } from 'react-router-dom';
import { useState, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { debounce, union, find } from 'lodash-es';
import { useInfiniteScroll } from 'ahooks';
import {
CheckType,
WorkflowMode,
type Workflow,
} from '@coze-arch/idl/workflow_api';
import {
type PublishConnectorInfo,
ConnectorConfigStatus,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle, IconCozEmpty } from '@coze-arch/coze-design/icons';
import {
Modal,
Search,
Checkbox,
Divider,
Spin,
Tooltip,
Space,
EmptyState,
} from '@coze-arch/coze-design';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { workflowApi } from '@coze-arch/bot-api';
import { useProjectPublishStore } from '@/store';
export interface DataList {
list: Workflow[];
hasMore?: boolean;
nextCursorId?: string;
total: number;
nextPageIndex: number;
}
const debounceTimer = 500;
export const UseMcpConfigModal = ({
record,
}: {
record: PublishConnectorInfo;
}) => {
const [visible, setVisible] = useState(false);
const [searchVal, setSearchVal] = useState<string>('');
const [checkedList, setCheckedList] = useState<string[]>([]);
const { space_id = '', project_id = '' } = useParams<DynamicParams>();
const {
connectorPublishConfig,
setProjectPublishInfo,
connectorList,
selectedConnectorIds,
} = useProjectPublishStore(
useShallow(state => ({
connectorPublishConfig: state.connectorPublishConfig,
setProjectPublishInfo: state.setProjectPublishInfo,
connectorList: state.connectorList,
selectedConnectorIds: state.selectedConnectorIds,
})),
);
const containerRef = useRef<HTMLDivElement>(null);
const { loading, data, loadingMore } = useInfiniteScroll<DataList>(
async d => {
const res = await workflowApi.GetWorkFlowList({
space_id,
project_id,
flow_mode: WorkflowMode.All,
checker: [CheckType.MCPPublish],
size: 15,
page: d?.nextPageIndex ?? 1,
name: searchVal,
});
return {
list: res.data?.workflow_list ?? [],
total: Number(res.data?.total ?? 0),
nextPageIndex: (d?.nextPageIndex || 1) + 1,
};
},
{
target: containerRef,
reloadDeps: [searchVal],
isNoMore: dataSource =>
Boolean(
!dataSource?.total ||
(dataSource.nextPageIndex - 1) * 15 >= dataSource.total,
),
},
);
// 只能选中未禁用的workflow
const filterPassList = data?.list?.filter(
item =>
find(item?.check_result, {
type: CheckType.MCPPublish,
})?.is_pass,
);
//半选状态
const indeterminate =
checkedList.length > 0 &&
checkedList.length < (filterPassList?.length || 0);
//全选状态
const checkAll = checkedList.length === (filterPassList?.length || 0);
const close = () => {
setVisible(false);
};
const handelConfirm = () => {
setProjectPublishInfo({
connectorPublishConfig: {
...connectorPublishConfig,
[record.id]: {
selected_workflows: checkedList.map(item => {
const res = find(data?.list, {
workflow_id: item,
});
return {
workflow_id: res?.workflow_id,
workflow_name: res?.name,
};
}),
},
},
connectorList: connectorList.map(item => {
if (item.id === record.id) {
return {
...item,
config_status: ConnectorConfigStatus.Configured,
};
}
return item;
}),
selectedConnectorIds: union(selectedConnectorIds, [record.id]), //ID合并去重
});
close();
};
return {
open: () => {
setVisible(true);
const ids = connectorPublishConfig?.[record.id]?.selected_workflows;
setCheckedList(ids?.map(item => item.workflow_id ?? '') ?? []);
},
close,
node: (
<Modal
title={I18n.t('app_publish_connector_space_mcp_config_dialog_title')}
size="large"
visible={visible}
onCancel={close}
okButtonProps={{ loading, disabled: !checkedList.length }}
okText={I18n.t('app_publish_connector_space_mcp_config_dialog_confirm')}
cancelText={I18n.t(
'app_publish_connector_space_mcp_config_dialog_cancel',
)}
onOk={handelConfirm}
>
<div className="text-[12px]">
{I18n.t('app_publish_connector_space_mcp_config_dialog_desc')}
</div>
<Space className="mb-[16px]" spacing={4}>
<div className="text-[12px]">
{I18n.t('app_publish_connector_space_mcp_config_dialog_desc2')}
</div>
<Tooltip
position="top"
content={
<div className="whitespace-pre-line">
{I18n.t(
'app_publish_connector_space_mcp_config_dialog_hover_wf_constraints',
)}
</div>
}
>
<IconCozInfoCircle className="text-[14px]" />
</Tooltip>
</Space>
<div className="font-[500] mb-[12px]">
{I18n.t('app_publish_connector_space_mcp_config_dialog_choose_wf')}
<span className="coz-fg-hglt-red">*</span>
</div>
<div className="border border-solid coz-stroke-primary rounded py-[12px]">
<div className="mx-[12px]">
<Search
className="!w-full"
placeholder={I18n.t(
'app_publish_connector_space_mcp_config_dialog_search_placeholder',
)}
value={searchVal}
onSearch={debounce(v => {
setSearchVal(v);
}, debounceTimer)}
/>
</div>
<Divider className="my-[8px]" />
<div className="mx-[12px]">
{data?.list.length ? (
<Checkbox
className="my-[8px] px-[4px]"
indeterminate={indeterminate}
checked={checkAll}
onChange={e => {
setCheckedList(
e.target.checked
? filterPassList?.map(item => item.workflow_id || '') ||
[]
: [],
);
}}
>
{I18n.t(
'app_publish_connector_space_mcp_config_dialog_filter_all',
)}
</Checkbox>
) : null}
<div
ref={containerRef}
className="max-h-[300px] overflow-x-hidden overflow-y-auto"
>
<Checkbox.Group
className="gap-[4px]"
value={checkedList}
onChange={setCheckedList}
>
{data?.list?.map(option => {
const mcpOpt = find(option?.check_result, {
type: CheckType.MCPPublish,
});
return (
<Checkbox
className="p-[4px]"
key={option.workflow_id}
value={option.workflow_id}
disabled={!mcpOpt?.is_pass}
>
{mcpOpt?.is_pass ? (
option.name
) : (
<Tooltip position="top" content={mcpOpt?.reason}>
{option.name}
</Tooltip>
)}
</Checkbox>
);
})}
</Checkbox.Group>
{/* 加载中 */}
{loadingMore && data?.list.length ? (
<div className="text-center">
<Spin size="small" />
</div>
) : null}
{/* 空状态 */}
{!data?.list.length ? (
<EmptyState
className="my-[80px] mx-auto"
icon={<IconCozEmpty />}
title={I18n.t(
'app_publish_connector_space_mcp_config_dialog_no_results_found',
)}
/>
) : null}
</div>
</div>
</div>
</Modal>
),
};
};

View File

@@ -0,0 +1,261 @@
/*
* 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, useState } from 'react';
import { useRequest } from 'ahooks';
import {
ConnectorPublishStatus,
PublishRecordStatus,
type PublishRecordDetail,
} from '@coze-arch/idl/intelligence_api';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
IconCozCheckMarkCircle,
IconCozClock,
IconCozCrossCircle,
} from '@coze-arch/coze-design/icons';
import { Modal, Select, Tag, type TagProps } from '@coze-arch/coze-design';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { intelligenceApi } from '@coze-arch/bot-api';
import { EProjectPermission, useProjectAuth } from '@coze-common/auth';
import { isPublishFinish } from '../utils/is-publish-finish';
import { ProjectPublishProgress } from '../publish-progress';
enum PublishStatus {
Publishing,
Failed,
Success,
}
const PublishStatusMap: Record<
PublishStatus,
Pick<TagProps, 'prefixIcon' | 'color'> & { text: I18nKeysNoOptionsType }
> = {
[PublishStatus.Publishing]: {
prefixIcon: <IconCozClock />,
color: 'brand',
text: 'project_releasing',
},
[PublishStatus.Failed]: {
prefixIcon: <IconCozCrossCircle />,
color: 'red',
text: 'project_release_failed',
},
[PublishStatus.Success]: {
prefixIcon: <IconCozCheckMarkCircle />,
color: 'green',
text: 'project_release_success',
},
};
function toPublishStatus(record: PublishRecordDetail) {
const projectFailed =
record.publish_status === PublishRecordStatus.PackFailed ||
record.publish_status === PublishRecordStatus.AuditNotPass;
const connectorsFailed =
record.connector_publish_result?.some(
item => item.connector_publish_status === ConnectorPublishStatus.Failed,
) ?? false;
// project 本身失败 或 部分渠道发布失败 -> 整体失败
if (projectFailed || connectorsFailed) {
return PublishStatus.Failed;
}
const projectPublishing =
record.publish_status === PublishRecordStatus.Packing ||
record.publish_status === PublishRecordStatus.Auditing ||
record.publish_status === PublishRecordStatus.ConnectorPublishing;
const connectorsPublishing =
record.connector_publish_result?.some(
item =>
item.connector_publish_status === ConnectorPublishStatus.Default ||
item.connector_publish_status === ConnectorPublishStatus.Auditing,
) ?? false;
// project 本身发布中 或 部分渠道发布中 -> 整体发布中
if (projectPublishing || connectorsPublishing) {
return PublishStatus.Publishing;
}
return PublishStatus.Success;
}
export interface ProjectPublishStatusProps {
spaceId: string;
projectId: string;
defaultRecordID?: string;
}
/* eslint @coze-arch/max-line-per-function: ["error", {"max": 300}] */
export function usePublishStatus({
spaceId,
projectId,
defaultRecordID,
}: ProjectPublishStatusProps) {
const [status, setStatus] = useState<PublishStatus | undefined>();
const [latestRecord, setLatestRecord] = useState<PublishRecordDetail>();
const [recordList, setRecordList] = useState<OptionProps[]>([]);
const [selectedVersion, setSelectedVersion] = useState(defaultRecordID);
const [selectedRecord, setSelectedRecord] = useState<PublishRecordDetail>();
const [modalVisible, setModalVisible] = useState(false);
// 轮询最新发布记录,直到不属于“发布中”状态后停止
const latestRecordRequest = useRequest(
() => intelligenceApi.GetPublishRecordDetail({ project_id: projectId }),
{
manual: true,
pollingInterval: 5000,
pollingWhenHidden: false,
pollingErrorRetryCount: 3,
onSuccess: res => {
const record = res.data;
// 没有发布记录时停止轮询
if (!record || typeof record.publish_status !== 'number') {
latestRecordRequest.cancel();
return;
}
setStatus(toPublishStatus(record));
setLatestRecord(record);
// 首次请求最新发布记录后,默认选中其版本号
if (!selectedVersion) {
setRecordList([
{ value: record.publish_record_id, label: record.version_number },
]);
setSelectedVersion(record.publish_record_id ?? '');
} else if (selectedVersion === record.publish_record_id) {
setSelectedRecord(record);
}
if (isPublishFinish(record)) {
latestRecordRequest.cancel();
}
},
},
);
// 获取发布记录列表
const recordListRequest = useRequest(
() => intelligenceApi.GetPublishRecordList({ project_id: projectId }),
{
manual: true,
onSuccess: res => {
setRecordList(
res.data?.map(item => ({
value: item.publish_record_id,
label: item.version_number,
})) ?? [],
);
},
},
);
const hasPermission = useProjectAuth(
EProjectPermission.PUBLISH,
projectId,
spaceId,
);
// 用户有“发布”权限时,启动轮询
useEffect(() => {
if (!hasPermission || defaultRecordID) {
return;
}
latestRecordRequest.run();
}, [hasPermission, defaultRecordID]);
// 手动请求选择的发布记录
const recordDetailRequest = useRequest(
(recordId: string) =>
intelligenceApi.GetPublishRecordDetail({
project_id: projectId,
publish_record_id: recordId,
}),
{
manual: true,
onSuccess: res => {
const record = res.data;
setSelectedRecord(record);
if (record?.publish_record_id === latestRecord?.publish_record_id) {
setLatestRecord(record);
}
},
},
);
const tagConfig = PublishStatusMap[status ?? PublishStatus.Failed];
const showingRecord = selectedRecord ?? latestRecord;
const open = async () => {
await recordListRequest.runAsync();
if (defaultRecordID) {
await changeVersion(defaultRecordID);
}
setModalVisible(true);
};
const close = () => {
setModalVisible(false);
};
const changeVersion = async (version: string) => {
setSelectedVersion(version);
await recordDetailRequest.run(version);
};
return {
latestVersion: latestRecord,
currentVersion: recordList.find(item => item.value === selectedVersion),
open,
close,
modal: (
<Modal
title={I18n.t('project_release_stage')}
visible={modalVisible}
footer={null}
onCancel={() => setModalVisible(false)}
>
<div className="flex flex-col gap-[16px]">
<Select
className="w-full"
optionList={recordList}
value={selectedVersion}
onSelect={version => {
if (typeof version === 'string') {
changeVersion(version);
}
}}
/>
{showingRecord ? (
<ProjectPublishProgress record={showingRecord} />
) : null}
</div>
</Modal>
),
tag: (
<Tag
size="mini"
prefixIcon={tagConfig.prefixIcon}
color={tagConfig.color}
// Tag 组件默认 display: inline-flex, 而外层 span line-height > 1, 会导致其高度大于 Tag 本身
className="flex !px-[3px] font-medium"
>
{I18n.t(tagConfig.text)}
</Tag>
),
};
}