feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user