feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { createContext, type RefObject, useContext } from 'react';
|
||||
|
||||
export interface PublishContainerContextProps {
|
||||
getContainerRef: () => RefObject<HTMLDivElement> | null;
|
||||
/** 发布渠道的布局受到顶部 header 高度影响 用这个变量将他们关联起来 */
|
||||
publishHeaderHeight: number;
|
||||
setPublishHeaderHeight: (height: number) => void;
|
||||
}
|
||||
|
||||
export const PublishContainerContext =
|
||||
createContext<PublishContainerContextProps>({
|
||||
getContainerRef: () => null,
|
||||
publishHeaderHeight: 0,
|
||||
setPublishHeaderHeight: () => 0,
|
||||
});
|
||||
|
||||
export const usePublishContainer = () => useContext(PublishContainerContext);
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { ProjectPublish } from './publish-main';
|
||||
|
||||
export { useBizConnectorAnchor } from './hooks/use-biz-connector-anchor';
|
||||
export {
|
||||
usePublishStatus,
|
||||
type ProjectPublishStatusProps,
|
||||
} from './hooks/use-publish-status';
|
||||
|
||||
export { PublishButton } from './publish-button';
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 react-hooks/exhaustive-deps */
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { type ReactNode, useCallback } from 'react';
|
||||
|
||||
import { useIsPublishRecordReady } from '@coze-studio/publish-manage-hooks';
|
||||
import {
|
||||
useProjectAuth,
|
||||
EProjectPermission,
|
||||
useProjectRole,
|
||||
} from '@coze-common/auth';
|
||||
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozAnalytics,
|
||||
IconCozArrowDown,
|
||||
IconCozArrowRight,
|
||||
IconCozDocument,
|
||||
IconCozLongArrowTopRight,
|
||||
IconCozTrigger,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
Menu,
|
||||
Popover,
|
||||
Tooltip,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { useFlags } from '@coze-arch/bot-flags';
|
||||
|
||||
import { usePublishStatus } from '../hooks/use-publish-status';
|
||||
import { useBizConnectorAnchor } from '../hooks/use-biz-connector-anchor';
|
||||
|
||||
const isLocalDevMode = () => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
return searchParams.has('devBlock');
|
||||
};
|
||||
|
||||
/* eslint @coze-arch/max-line-per-function: ["error", {"max": 300}] */
|
||||
export const PublishButton = ({
|
||||
spaceId,
|
||||
projectId,
|
||||
hasPublished,
|
||||
}: {
|
||||
spaceId: string;
|
||||
projectId: string;
|
||||
hasPublished: boolean;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { modal, tag, latestVersion, open } = usePublishStatus({
|
||||
spaceId,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const hasPublishPermission = useProjectAuth(
|
||||
EProjectPermission.PUBLISH,
|
||||
projectId,
|
||||
spaceId,
|
||||
);
|
||||
const { removeAnchor } = useBizConnectorAnchor();
|
||||
|
||||
const handlePublish = useCallback(() => {
|
||||
removeAnchor();
|
||||
navigate(`/space/${spaceId}/project-ide/${projectId}/publish`);
|
||||
}, [spaceId, projectId, removeAnchor]);
|
||||
|
||||
const projectRoles = useProjectRole(projectId);
|
||||
|
||||
const [FLAGS] = useFlags();
|
||||
|
||||
const { ready, inited } = useIsPublishRecordReady({
|
||||
type: IntelligenceType.Project,
|
||||
intelligenceId: projectId,
|
||||
spaceId,
|
||||
enable: !!(
|
||||
// 社区版暂不支持该功能
|
||||
(
|
||||
FLAGS['bot.studio.publish_management'] &&
|
||||
hasPublished &&
|
||||
projectRoles.length &&
|
||||
!IS_OPEN_SOURCE
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
const menuAnalysis = (
|
||||
<MenuItem
|
||||
disabled={!ready}
|
||||
onClick={() => {
|
||||
navigate(`/space/${spaceId}/publish/app/${projectId}?tab=analysis`);
|
||||
}}
|
||||
text={I18n.t('analytics_page_title')}
|
||||
prefix={
|
||||
<IconCozAnalytics className="w-[24px] h-[24px] px-[4px] py-[4px]" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuLogs = (
|
||||
<MenuItem
|
||||
disabled={!ready}
|
||||
onClick={() => {
|
||||
navigate(`/space/${spaceId}/publish/app/${projectId}?tab=logs`);
|
||||
}}
|
||||
text={I18n.t('release_management_trace')}
|
||||
prefix={
|
||||
<IconCozDocument className="w-[24px] h-[24px] px-[4px] py-[4px]" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuTriggers = (
|
||||
<MenuItem
|
||||
disabled={!ready}
|
||||
onClick={() => {
|
||||
navigate(`/space/${spaceId}/publish/app/${projectId}?tab=triggers`);
|
||||
}}
|
||||
text={I18n.t('release_management_trigger')}
|
||||
prefix={
|
||||
<IconCozTrigger className="w-[24px] h-[24px] px-[4px] py-[4px]" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!hasPublishPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasPublished) {
|
||||
return (
|
||||
<Button
|
||||
onClick={handlePublish}
|
||||
disabled={isLocalDevMode()}
|
||||
data-testid="project.goto.publish-button"
|
||||
>
|
||||
{I18n.t('project_ide_frame_publish')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<Popover
|
||||
trigger="click"
|
||||
zIndex={999}
|
||||
content={
|
||||
<div className="px-[4px] py-[8px] rounded-[12px] w-[244px] flex flex-col gap-[12px]">
|
||||
{latestVersion?.version_number ? (
|
||||
<div
|
||||
className="flex justify-between items-center h-[32px] px-[4px] cursor-pointer"
|
||||
onClick={() => open()}
|
||||
>
|
||||
<span className="flex-grow mr-[8px]">
|
||||
{`${I18n.t('app_ide_publish_modal_recent_publication')} ${latestVersion?.version_number}`}
|
||||
</span>
|
||||
{tag}
|
||||
<IconButton
|
||||
size="small"
|
||||
className="ml-[4px]"
|
||||
icon={<IconCozArrowRight />}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="px-[8px] w-full">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
{/* 社区版暂不支持该功能 */}
|
||||
{FLAGS['bot.studio.publish_management'] && !IS_OPEN_SOURCE ? (
|
||||
<div>
|
||||
<div className="coz-fg-secondary font-[500] px-[8px] pt-[4px] pb-0 mb-[2px]">
|
||||
{I18n.t('app_ide_publish_modal_publish_management')}
|
||||
</div>
|
||||
<Menu>
|
||||
{ready || !inited ? (
|
||||
menuAnalysis
|
||||
) : (
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
content={I18n.t('release_management_generating')}
|
||||
>
|
||||
<div>{menuAnalysis}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ready || !inited ? (
|
||||
menuLogs
|
||||
) : (
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
content={I18n.t('release_management_generating')}
|
||||
>
|
||||
<div>{menuLogs}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ready || !inited ? (
|
||||
menuTriggers
|
||||
) : (
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
content={I18n.t('release_management_generating')}
|
||||
>
|
||||
<div>{menuTriggers}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="px-[4px] w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handlePublish}
|
||||
disabled={isLocalDevMode()}
|
||||
data-testid="project.goto.publish-button"
|
||||
>
|
||||
{I18n.t('app_ide_publish_modal_publish_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button>
|
||||
<span className="mr-[4px]">
|
||||
{I18n.t('project_ide_frame_publish')}
|
||||
</span>
|
||||
<IconCozArrowDown />
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = ({
|
||||
prefix,
|
||||
text,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
prefix?: ReactNode;
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}) => (
|
||||
<Menu.Item
|
||||
className="px-[4px] py-[4px]"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-[228px] flex items-center justify-between gap-[4px]">
|
||||
{prefix}
|
||||
<span className="flex-grow">{text}</span>
|
||||
<IconCozLongArrowTopRight className="w-[24px] h-[24px] px-[4px] py-[4px] coz-fg-secondary" />
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { every, some } from 'lodash-es';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FormSelect, useFormApi } from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { type OptionProps } from '@coze-arch/bot-semi/Select';
|
||||
import { IconCozArrowDown } from '@coze-arch/bot-icons';
|
||||
import { workflowApi } from '@coze-arch/bot-api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
interface ApiBindProps {
|
||||
checked: boolean;
|
||||
record: PublishConnectorInfo;
|
||||
}
|
||||
|
||||
export const ApiBind = (props: ApiBindProps) => {
|
||||
const { checked, record } = props;
|
||||
const { id } = record;
|
||||
const { space_id = '', project_id = '' } = useParams<DynamicParams>();
|
||||
const formApi = useFormApi();
|
||||
const { connectorPublishConfig, setProjectPublishInfo } =
|
||||
useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectorPublishConfig: state.connectorPublishConfig,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
|
||||
const { data: workflowOptions = [], loading: workflowOptionsLoading } =
|
||||
useRequest(async () => {
|
||||
const { data } = await workflowApi.GetWorkFlowList({
|
||||
project_id,
|
||||
space_id,
|
||||
page: 1,
|
||||
size: 50,
|
||||
});
|
||||
return (
|
||||
data.workflow_list?.map(item => ({
|
||||
label: item.name ?? '',
|
||||
value: item.workflow_id ?? '',
|
||||
})) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化时校验一次是否有workflow
|
||||
if (checked && !workflowOptionsLoading) {
|
||||
formApi.validate(['api_workflow']);
|
||||
}
|
||||
}, [workflowOptionsLoading]);
|
||||
|
||||
const checkWorkflowExist = (val: OptionProps[]) =>
|
||||
every(val, (item: OptionProps) => {
|
||||
if ('value' in item) {
|
||||
return some(
|
||||
workflowOptions,
|
||||
(optionItem: OptionProps) => optionItem.value === item.value,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const validate = (val: OptionProps[]) => {
|
||||
if (checked) {
|
||||
const workflowExist = checkWorkflowExist(val);
|
||||
if (!workflowExist) {
|
||||
return I18n.t('project_release_chatflow3');
|
||||
}
|
||||
if (!val?.length) {
|
||||
return I18n.t('project_release_select_chatflow');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormSelect
|
||||
field="api_workflow"
|
||||
optionList={workflowOptions}
|
||||
onChangeWithObject
|
||||
arrowIcon={<IconCozArrowDown />}
|
||||
initValue={
|
||||
connectorPublishConfig[id]?.selected_workflows?.map(i => ({
|
||||
label: i.workflow_name,
|
||||
value: i.workflow_id,
|
||||
})) ?? []
|
||||
}
|
||||
className={`w-[172px] mr-2 ${!checked ? 'hidden' : ''}`}
|
||||
noLabel
|
||||
placeholder={I18n.t('project_release_select_chatflow')}
|
||||
multiple
|
||||
validate={validate}
|
||||
showClear
|
||||
trigger={['custom', 'change']}
|
||||
noErrorMessage={!checked}
|
||||
onChange={(value: unknown) => {
|
||||
setProjectPublishInfo({
|
||||
connectorPublishConfig: {
|
||||
...connectorPublishConfig,
|
||||
[id]: {
|
||||
selected_workflows: (value as OptionProps[])?.map(i => ({
|
||||
workflow_id: i.value?.toString(),
|
||||
workflow_name: i.label?.toString(),
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
maxTagCount={2}
|
||||
ellipsisTrigger
|
||||
showRestTagsPopover={true}
|
||||
restTagsPopoverProps={{ position: 'top' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @file 社区版暂不支持商店渠道绑定,用于未来拓展
|
||||
import { type MouseEventHandler } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { ProductEntityType } from '@coze-arch/bot-api/product_api';
|
||||
import { FormSelect, type optionRenderProps } from '@coze-arch/coze-design';
|
||||
|
||||
import { type StoreBindKey, useProjectPublishStore } from '@/store';
|
||||
import { toDisplayScreenOption } from '@/publish-main/utils/display-screen-option';
|
||||
import { useProductCategoryOptions } from '@/publish-main/hooks/use-product-category-options';
|
||||
|
||||
import { OptionWithTooltip } from '../option-with-tooltip';
|
||||
|
||||
export interface StoreBindProps {
|
||||
checked: boolean;
|
||||
record: PublishConnectorInfo;
|
||||
onClick: MouseEventHandler;
|
||||
}
|
||||
|
||||
export const StoreBind = ({
|
||||
checked,
|
||||
record,
|
||||
onClick: inputOnClick,
|
||||
}: StoreBindProps) => {
|
||||
const { bind_info, id = '', UIOptions } = record;
|
||||
const displayScreenOptions = UIOptions?.map(toDisplayScreenOption) ?? [];
|
||||
const defaultDisplayScreen = bind_info?.display_screen;
|
||||
const { connectors, setProjectPublishInfo } = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectors: state.connectors,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
|
||||
const { categoryOptions } = useProductCategoryOptions(ProductEntityType.Bot);
|
||||
|
||||
const handleSelect = (key: StoreBindKey, value: string) => {
|
||||
setProjectPublishInfo({
|
||||
connectors: {
|
||||
...connectors,
|
||||
[id]: {
|
||||
...bind_info,
|
||||
...connectors[id],
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('flex w-full gap-[6px] mt-auto')}
|
||||
onClick={inputOnClick}
|
||||
>
|
||||
<FormSelect
|
||||
noLabel
|
||||
field="store_display_screen"
|
||||
insetLabel={I18n.t('project_release_display_label')}
|
||||
fieldClassName="w-[50%]"
|
||||
className="w-full"
|
||||
initValue={defaultDisplayScreen}
|
||||
optionList={displayScreenOptions}
|
||||
renderOptionItem={(option: optionRenderProps) => (
|
||||
<OptionWithTooltip option={option} tooltip={option.tooltip} />
|
||||
)}
|
||||
rules={[{ required: checked }]}
|
||||
onSelect={(value: unknown) =>
|
||||
handleSelect('display_screen', value as string)
|
||||
}
|
||||
/>
|
||||
<FormSelect
|
||||
noLabel
|
||||
field="store_category_id"
|
||||
insetLabel={I18n.t('mkpl_bots_category')}
|
||||
fieldClassName="w-[50%]"
|
||||
className="w-full"
|
||||
placeholder={I18n.t('select_category')}
|
||||
initValue={bind_info?.category_id}
|
||||
optionList={categoryOptions}
|
||||
rules={[
|
||||
{
|
||||
required: checked,
|
||||
message: I18n.t('select_category'),
|
||||
},
|
||||
]}
|
||||
onSelect={(value: unknown) =>
|
||||
handleSelect('category_id', value as string)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
.template-form {
|
||||
.editor-container {
|
||||
border: 1px solid rgba(var(--coze-stroke-6), var(--coze-stroke-6-alpha));
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--semi-color-focus-border);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-form-field {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.semi-upload-picture {
|
||||
.semi-upload-picture-add {
|
||||
background-color: transparent;
|
||||
border: 1px solid rgba(var(--coze-stroke-6), var(--coze-stroke-6-alpha));
|
||||
}
|
||||
|
||||
// 上传失败的图片,红色 outline 会超出边距
|
||||
.semi-upload-picture-file-card-error {
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.coz-single-select {
|
||||
.semi-radio {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
|
||||
// stylelint-disable-next-line selector-class-pattern -- 覆盖 semi 样式
|
||||
.semi-radio-addon-buttonRadio {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coz-input-number.semi-input-number {
|
||||
.semi-input-wrapper {
|
||||
border-color: rgba(var(--coze-stroke-6), var(--coze-stroke-6-alpha));
|
||||
}
|
||||
|
||||
.semi-input-wrapper-focus {
|
||||
border-color: var(--semi-color-focus-border);
|
||||
}
|
||||
|
||||
.semi-input-suffix {
|
||||
margin-right: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @file 社区版暂不支持模版渠道绑定,用于未来拓展
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { type MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { ProductEntityType, type UserInfo } from '@coze-arch/idl/product_api';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
import { Button, Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
import {
|
||||
entityInfoToTemplateForm,
|
||||
type TemplateForm,
|
||||
templateFormToBindInfo,
|
||||
} from './types';
|
||||
import {
|
||||
TemplateConfigForm,
|
||||
type TemplateConfigFormRef,
|
||||
} from './template-config-form';
|
||||
|
||||
interface TemplateBindProps {
|
||||
record: PublishConnectorInfo;
|
||||
onClick: MouseEventHandler;
|
||||
}
|
||||
|
||||
export function TemplateBind({
|
||||
record,
|
||||
onClick: inputOnClick,
|
||||
}: TemplateBindProps) {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState<UserInfo>();
|
||||
const templateConfigForm = useRef<TemplateConfigFormRef>(null);
|
||||
const [savedValues, setSavedValues] = useState<Partial<TemplateForm>>();
|
||||
|
||||
const { connectors, setProjectPublishInfo } = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectors: state.connectors,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
|
||||
const { project_id = '' } = useParams<DynamicParams>();
|
||||
|
||||
// 回填模板配置
|
||||
const fillTemplateFrom = async () => {
|
||||
const productInfo = await ProductApi.PublicGetProductEntityInfo({
|
||||
entity_id: project_id,
|
||||
entity_type: ProductEntityType.ProjectTemplate,
|
||||
});
|
||||
if (productInfo.data.meta_info?.name) {
|
||||
const formValues = entityInfoToTemplateForm(
|
||||
productInfo.data,
|
||||
record.UIOptions?.find(item => item.available),
|
||||
);
|
||||
setSavedValues(formValues);
|
||||
setProjectPublishInfo({
|
||||
templateConfigured: formValues.agreement === true,
|
||||
connectors: {
|
||||
...connectors,
|
||||
// @ts-expect-error 可以接受 Partial
|
||||
[record.id]: templateFormToBindInfo(formValues),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (productInfo.data.meta_info?.user_info) {
|
||||
setUserInfo(productInfo.data.meta_info.user_info);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fillTemplateFrom();
|
||||
}, []);
|
||||
|
||||
const showModal = () => {
|
||||
templateConfigForm.current?.fillInitialValues(savedValues ?? {});
|
||||
setModalVisible(true);
|
||||
};
|
||||
const closeModal = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formValues = await templateConfigForm.current?.validate();
|
||||
if (!formValues) {
|
||||
return;
|
||||
}
|
||||
setSavedValues(formValues);
|
||||
setProjectPublishInfo({
|
||||
templateConfigured: true,
|
||||
connectors: {
|
||||
...connectors,
|
||||
[record.id]: templateFormToBindInfo(formValues),
|
||||
},
|
||||
});
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('h-full flex items-end', {
|
||||
hidden: !record.allow_publish,
|
||||
})}
|
||||
onClick={inputOnClick}
|
||||
>
|
||||
<Button size="small" color="primary" onClick={showModal}>
|
||||
{I18n.t('project_release_template_info')}
|
||||
</Button>
|
||||
<Modal
|
||||
title={I18n.t('project_release_template_info')}
|
||||
width={800}
|
||||
visible={modalVisible}
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={handleSubmit}
|
||||
okText={I18n.t('prompt_submit')}
|
||||
lazyRender={false}
|
||||
keepDOM={true}
|
||||
>
|
||||
<TemplateConfigForm
|
||||
ref={templateConfigForm}
|
||||
record={record}
|
||||
userInfo={userInfo}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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 {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
Suspense,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { ProductEntityType, type UserInfo } from '@coze-arch/idl/product_api';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import { useFlags } from '@coze-arch/bot-flags';
|
||||
import {
|
||||
LazyEditorFullInput,
|
||||
DeltaSet,
|
||||
type DeltaSetOptions,
|
||||
type Editor,
|
||||
EditorEventType,
|
||||
normalizeSchema,
|
||||
} from '@coze-common/md-editor-adapter';
|
||||
import { IconCozPlus } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
type FileItem,
|
||||
Form,
|
||||
FormInput,
|
||||
FormSelect,
|
||||
Typography,
|
||||
type optionRenderProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
uploadCustomRequest,
|
||||
uploadCustomRequestImageX,
|
||||
} from '@/utils/upload-custom-request';
|
||||
import { toDisplayScreenOption } from '@/publish-main/utils/display-screen-option';
|
||||
import { useProductCategoryOptions } from '@/publish-main/hooks/use-product-category-options';
|
||||
|
||||
import { OptionWithTooltip } from '../../option-with-tooltip';
|
||||
import { type TemplateForm } from './types';
|
||||
import { TemplatePreviewCard } from './template-preview-card';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
function getRequiredRules(type: I18nKeysNoOptionsType) {
|
||||
return [
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('project_release_template_info_not', {
|
||||
template_info_type: I18n.t(type),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export interface TemplateConfigFormRef {
|
||||
fillInitialValues: (values: Partial<TemplateForm>) => void;
|
||||
validate: () => Promise<TemplateForm> | undefined;
|
||||
}
|
||||
|
||||
export interface TemplateConfigFormProps {
|
||||
record: PublishConnectorInfo;
|
||||
userInfo?: UserInfo;
|
||||
}
|
||||
|
||||
export const TemplateConfigForm = forwardRef<
|
||||
TemplateConfigFormRef,
|
||||
TemplateConfigFormProps
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
>(({ record, userInfo }, ref) => {
|
||||
const [FLAGS] = useFlags();
|
||||
// 社区版暂不支持该功能
|
||||
const customRequest = FLAGS['bot.studio.project_publish_imagex']
|
||||
? uploadCustomRequestImageX
|
||||
: uploadCustomRequest;
|
||||
const uiChannelOptions = record.UIOptions?.map(toDisplayScreenOption) ?? [];
|
||||
const formRef = useRef<Form<TemplateForm>>(null);
|
||||
const [formValues, setFormValues] = useState<Partial<TemplateForm>>({});
|
||||
const editorRef = useRef<Editor>();
|
||||
const onEditorInit = (editor: Editor) => {
|
||||
editorRef.current = editor;
|
||||
// EditorFullInput 的 form value 为纯文本,但这里需要提交 editor-kit 富文本内容
|
||||
editor.on(EditorEventType.CONTENT_CHANGE, _ => {
|
||||
formRef.current?.formApi?.setValue(
|
||||
'readme',
|
||||
JSON.stringify(editor.getContent().deltas),
|
||||
);
|
||||
});
|
||||
};
|
||||
const { categoryOptions } = useProductCategoryOptions(
|
||||
ProductEntityType.TemplateCommon,
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
fillInitialValues: values => {
|
||||
const formApi = formRef.current?.formApi;
|
||||
if (!formApi) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(values, { isOverride: true });
|
||||
const readme = typeSafeJSONParse(values.readme);
|
||||
if (readme) {
|
||||
editorRef.current?.setContent(
|
||||
new DeltaSet(normalizeSchema(readme as DeltaSetOptions)),
|
||||
);
|
||||
}
|
||||
// @ts-expect-error -- values 就是 TemplateForm 类型
|
||||
Object.keys(values).forEach(key => formApi.setError(key, null));
|
||||
},
|
||||
validate: () => formRef.current?.formApi?.validate(),
|
||||
}));
|
||||
|
||||
const isZh = I18n.language.startsWith('zh');
|
||||
|
||||
return (
|
||||
<Form<TemplateForm>
|
||||
ref={formRef}
|
||||
className={classNames('flex gap-[24px]', s['template-form'])}
|
||||
onValueChange={values => setFormValues({ ...values })}
|
||||
>
|
||||
<div className="w-[320px] absolute flex flex-col h-full justify-center">
|
||||
<TemplatePreviewCard
|
||||
userInfo={userInfo}
|
||||
cover={formValues?.covers?.[0]?.url}
|
||||
name={formValues?.name}
|
||||
description={formValues?.description}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
field="agreement"
|
||||
noLabel
|
||||
className="mt-[16px]"
|
||||
rules={[
|
||||
{
|
||||
// 必须勾选同意协议才能通过验证
|
||||
validator: (_rule: unknown, value: unknown) =>
|
||||
(value as boolean) === true,
|
||||
message: I18n.t('template_buy_paid_agreement_toast'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Typography.Text>
|
||||
{I18n.t('template_buy_paid_agreement_action')}
|
||||
<Typography.Text
|
||||
className="ml-[4px]"
|
||||
link={{
|
||||
href: '/docs/guides/terms_of_template',
|
||||
target: '_blank',
|
||||
}}
|
||||
>
|
||||
{I18n.t('template_buy_paid_agreement_detail')}
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
</Form.Checkbox>
|
||||
</div>
|
||||
<div className="w-[320px] shrink-0"></div>
|
||||
<div className="grow">
|
||||
<FormInput
|
||||
field="name"
|
||||
label={I18n.t('project_release_template_info_name')}
|
||||
maxLength={isZh ? 10 : 30}
|
||||
rules={getRequiredRules('project_release_template_info_name')}
|
||||
/>
|
||||
<Form.Upload
|
||||
field="covers"
|
||||
label={I18n.t('project_release_template_info_poster')}
|
||||
listType="picture"
|
||||
accept=".jpeg,.jpg,.png,.webp"
|
||||
limit={1}
|
||||
maxSize={5 * 1024}
|
||||
action=""
|
||||
customRequest={customRequest}
|
||||
picWidth={80}
|
||||
picHeight={80}
|
||||
rules={[
|
||||
...getRequiredRules('project_release_template_info_poster'),
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown) =>
|
||||
(value as FileItem[] | undefined)?.every(
|
||||
item => !item._sizeInvalid && item.status === 'success',
|
||||
) === true,
|
||||
message: '', // 校验文件大小是否符合限制 && 上传是否成功,Upload 组件会显示错误信息
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconCozPlus className="w-[24px] h-[24px] coz-fg-primary" />
|
||||
</Form.Upload>
|
||||
<FormInput
|
||||
field="description"
|
||||
label={I18n.t('project_release_template_info_desc')}
|
||||
maxLength={isZh ? 100 : 300}
|
||||
rules={getRequiredRules('project_release_template_info_desc')}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LazyEditorFullInput
|
||||
field="readme_text"
|
||||
label={I18n.t('project_release_template_info_info')}
|
||||
className={classNames('h-[132px]', s['editor-container'])}
|
||||
maxCount={isZh ? 1000 : 3000}
|
||||
getEditor={onEditorInit}
|
||||
rules={getRequiredRules('project_release_template_info_info')}
|
||||
/>
|
||||
</Suspense>
|
||||
<FormSelect
|
||||
field="preview_type"
|
||||
label={I18n.t('project_release_template_info_display')}
|
||||
optionList={uiChannelOptions}
|
||||
renderOptionItem={(option: optionRenderProps) => (
|
||||
<OptionWithTooltip option={option} tooltip={option.tooltip} />
|
||||
)}
|
||||
fieldClassName="w-full"
|
||||
className="w-full"
|
||||
rules={getRequiredRules('project_release_template_info_display')}
|
||||
/>
|
||||
<FormSelect
|
||||
field="category"
|
||||
label={I18n.t('project_release_template_info_category')}
|
||||
optionList={categoryOptions}
|
||||
fieldClassName="w-full"
|
||||
className="w-full"
|
||||
rules={getRequiredRules('project_release_template_info_category')}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { AvatarName } from '@coze-studio/components';
|
||||
import { type UserInfo } from '@coze-arch/idl/product_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozImage } from '@coze-arch/coze-design/icons';
|
||||
import { Image, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface TemplatePreviewCardProps {
|
||||
userInfo?: UserInfo;
|
||||
cover?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 基本是复制的 packages/studio/template/pages/src/components/template-list-card/index.tsx
|
||||
export function TemplatePreviewCard({
|
||||
userInfo,
|
||||
cover,
|
||||
name,
|
||||
description,
|
||||
}: TemplatePreviewCardProps) {
|
||||
const userLabel = userInfo?.user_label
|
||||
? {
|
||||
name: userInfo.user_label.label_name,
|
||||
icon: userInfo.user_label.icon_url,
|
||||
href: userInfo.user_label.jump_link,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden p-[12px] pb-[16px] rounded-[16px] border border-solid coz-stroke-primary coz-bg-max coz-shadow-small">
|
||||
<div className="relative w-full h-[140px] rounded-[8px] overflow-hidden">
|
||||
<Image
|
||||
preview={false}
|
||||
src={cover}
|
||||
className="w-full h-full"
|
||||
imgCls="w-full h-full object-cover object-center"
|
||||
placeholder={<IconCozImage className="w-[32px] h-[32px]" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-[8px] px-[4px] grow flex flex-col">
|
||||
<div className="flex items-center gap-[8px] overflow-hidden">
|
||||
<Typography.Text
|
||||
className="!font-medium text-[16px] leading-[22px] coz-fg-primary !max-w-[180px]"
|
||||
ellipsis={{ showTooltip: true, rows: 1 }}
|
||||
>
|
||||
{name ?? I18n.t('project_release_template_info_name')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<AvatarName
|
||||
className="mt-[4px]"
|
||||
avatar={userInfo?.avatar_url}
|
||||
name={userInfo?.name}
|
||||
username={userInfo?.user_name}
|
||||
label={userLabel}
|
||||
/>
|
||||
|
||||
<div className="mt-[8px] flex flex-col justify-between grow">
|
||||
<Typography.Text
|
||||
className="min-h-[44px] leading-[20px] coz-fg-secondary"
|
||||
ellipsis={{ showTooltip: true, rows: 2 }}
|
||||
>
|
||||
{description ?? I18n.t('project_release_template_info_desc')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 {
|
||||
ProductStatus,
|
||||
type EntityInfoData,
|
||||
} from '@coze-arch/idl/product_api';
|
||||
import { type UIOption } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type FileItem } from '@coze-arch/bot-semi/Upload';
|
||||
|
||||
import {
|
||||
type DisplayScreen,
|
||||
toDisplayScreenOption,
|
||||
} from '@/publish-main/utils/display-screen-option';
|
||||
|
||||
export interface TemplateBindInfo {
|
||||
title: string;
|
||||
cover_uri: string;
|
||||
description: string;
|
||||
readme: string;
|
||||
display_screen: DisplayScreen;
|
||||
category_id: string;
|
||||
[k: string]: string;
|
||||
}
|
||||
|
||||
export interface TemplateForm {
|
||||
agreement: boolean;
|
||||
name: string;
|
||||
covers: Partial<FileItem>[];
|
||||
description: string;
|
||||
// EditorFullInput 的纯文本 form value ,仅为满足类型要求,不在业务中使用
|
||||
readme_text: string;
|
||||
// 实际需要传给后端的 editor-kit 富文本内容
|
||||
readme: string;
|
||||
preview_type: DisplayScreen;
|
||||
category: string;
|
||||
}
|
||||
|
||||
function stringToDeltaSet(str?: string) {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
return `{"0":{"ops":[{"insert":"${str}\\n"}],"zoneId":"0","zoneType":"Z"}}`;
|
||||
}
|
||||
|
||||
export function entityInfoToTemplateForm(
|
||||
info: EntityInfoData,
|
||||
uiOption?: UIOption,
|
||||
): Partial<TemplateForm> {
|
||||
const isZh = I18n.language.startsWith('zh');
|
||||
const meta = info.meta_info ?? {};
|
||||
const form: Partial<TemplateForm> = {
|
||||
// 默认勾选同意模板付费协议:已经上架过 或 已经配置过模板信息(readme 非空)
|
||||
agreement: meta.status !== ProductStatus.NeverListed || meta.readme !== '',
|
||||
name: meta.name,
|
||||
covers: meta.covers?.map(c => ({
|
||||
url: c.url,
|
||||
response: c,
|
||||
// 补充 FileItem 的其他属性,供表单校验使用
|
||||
status: 'success',
|
||||
_sizeInvalid: false,
|
||||
})),
|
||||
description: meta.description?.substring(0, isZh ? 100 : 300),
|
||||
readme: meta.readme || stringToDeltaSet(meta.description),
|
||||
category: meta.category?.id,
|
||||
};
|
||||
if (uiOption) {
|
||||
form.preview_type = toDisplayScreenOption(uiOption).value;
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
export function templateFormToBindInfo(form: TemplateForm): TemplateBindInfo {
|
||||
return {
|
||||
title: form.name,
|
||||
cover_uri: form.covers?.[0].response?.uri ?? '',
|
||||
description: form.description,
|
||||
readme: form.readme,
|
||||
display_screen: form.preview_type,
|
||||
category_id: form.category ?? '',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 { type MouseEventHandler } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button, Modal } from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useProjectPublishStore } from '../../../store';
|
||||
|
||||
interface UnbindButtonProps {
|
||||
bindId: string;
|
||||
checked: boolean;
|
||||
connectorId: string;
|
||||
className?: string;
|
||||
onClick: MouseEventHandler;
|
||||
}
|
||||
|
||||
const PROJECT_AGENT_TYPE = 1;
|
||||
|
||||
// 用于Api or WebSdk 的撤销发布
|
||||
export const UndoButton = (props: UnbindButtonProps) => {
|
||||
const {
|
||||
bindId,
|
||||
checked,
|
||||
connectorId,
|
||||
className,
|
||||
onClick: inputOnclick,
|
||||
} = props;
|
||||
const { space_id = '', project_id = '' } = useParams<DynamicParams>();
|
||||
|
||||
const { setProjectPublishInfo, selectedConnectorIds, connectorList } =
|
||||
useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
selectedConnectorIds: state.selectedConnectorIds,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
connectorList: state.connectorList,
|
||||
})),
|
||||
);
|
||||
|
||||
const handleUnbind: MouseEventHandler = e => {
|
||||
inputOnclick(e);
|
||||
Modal.confirm({
|
||||
title: I18n.t('project_release_cancel1'),
|
||||
content: I18n.t('project_release_cancel1_desc'),
|
||||
okText: I18n.t('project_release_cancel'),
|
||||
okButtonColor: 'red',
|
||||
cancelText: I18n.t('Cancel'),
|
||||
onOk: async () => {
|
||||
await DeveloperApi.UnBindConnector({
|
||||
bind_id: bindId,
|
||||
agent_type: PROJECT_AGENT_TYPE,
|
||||
space_id,
|
||||
bot_id: project_id,
|
||||
connector_id: connectorId,
|
||||
});
|
||||
setProjectPublishInfo({
|
||||
selectedConnectorIds: selectedConnectorIds.filter(
|
||||
id => id !== connectorId,
|
||||
),
|
||||
connectorList: connectorList.map(item => {
|
||||
if (item.id === connectorId) {
|
||||
return {
|
||||
...item,
|
||||
bind_id: '',
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
return bindId && checked ? (
|
||||
<Button
|
||||
onClick={handleUnbind}
|
||||
size="small"
|
||||
color="primary"
|
||||
className={classNames('w-max', className)}
|
||||
>
|
||||
{I18n.t('project_release_cancel')}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 { type MouseEventHandler } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FormSelect, type optionRenderProps } from '@coze-arch/coze-design';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
import {
|
||||
type ChatflowOptionProps,
|
||||
useChatflowOptions,
|
||||
} from '@/publish-main/hooks/use-chatflow-options';
|
||||
|
||||
import { OptionWithTooltip } from '../option-with-tooltip';
|
||||
|
||||
export interface WebSdkBindProps {
|
||||
checked: boolean;
|
||||
record: PublishConnectorInfo;
|
||||
onClick: MouseEventHandler;
|
||||
}
|
||||
|
||||
export function WebSdkBind({ checked, record, onClick }: WebSdkBindProps) {
|
||||
const { connectorPublishConfig, setProjectPublishInfo } =
|
||||
useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectorPublishConfig: state.connectorPublishConfig,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
const lastSelectedChatflow =
|
||||
connectorPublishConfig?.[record.id]?.selected_workflows?.[0]?.workflow_id;
|
||||
|
||||
const { chatflowOptions } = useChatflowOptions();
|
||||
|
||||
const handleChatflowSelect = (option: ChatflowOptionProps) => {
|
||||
setProjectPublishInfo({
|
||||
connectorPublishConfig: {
|
||||
...connectorPublishConfig,
|
||||
[record.id]: {
|
||||
selected_workflows: [
|
||||
{
|
||||
workflow_id: option.value,
|
||||
workflow_name: option.label,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const removePublishConfig = () => {
|
||||
useProjectPublishStore.getState().setProjectPublishInfoByImmer(draft => {
|
||||
const target = draft[record.id];
|
||||
if (!target?.selected_workflows) {
|
||||
return;
|
||||
}
|
||||
delete target.selected_workflows;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('flex mt-auto')} onClick={onClick}>
|
||||
<FormSelect
|
||||
field="sdk_chatflow"
|
||||
noLabel
|
||||
showClear
|
||||
noErrorMessage
|
||||
fieldClassName="w-[172px]"
|
||||
className="w-full"
|
||||
placeholder={I18n.t('project_release_Please_select')}
|
||||
initValue={lastSelectedChatflow}
|
||||
optionList={chatflowOptions}
|
||||
renderOptionItem={(option: optionRenderProps) => (
|
||||
<OptionWithTooltip option={option} tooltip={option.tooltip} />
|
||||
)}
|
||||
// onChange 负责处理数据清空的逻辑
|
||||
// onSelect 处理数据选择的逻辑
|
||||
onChange={values => {
|
||||
if (typeof values !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
removePublishConfig();
|
||||
}}
|
||||
onSelect={(_: unknown, option: unknown) =>
|
||||
handleChatflowSelect(option as ChatflowOptionProps)
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: checked,
|
||||
message: I18n.t('project_release_Please_select'),
|
||||
},
|
||||
// 校验已选择的 chatflow 是否存在 && 未被禁用
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown) => {
|
||||
if (!checked) {
|
||||
return true;
|
||||
}
|
||||
const selected = chatflowOptions?.find(
|
||||
option => option.value === (value as string),
|
||||
);
|
||||
if (!selected) {
|
||||
return new Error(I18n.t('project_release_chatflow3'));
|
||||
}
|
||||
if (selected.disabled) {
|
||||
return new Error(selected.tooltip);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 {
|
||||
ConnectorBindType,
|
||||
ConnectorClassification,
|
||||
ConnectorStatus,
|
||||
type PublishConnectorInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tag, type TagProps, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { getConfigStatus } from '../utils/get-config-status';
|
||||
|
||||
interface TipTagProps {
|
||||
showText: string;
|
||||
tip: string;
|
||||
tagProps?: TagProps;
|
||||
}
|
||||
|
||||
const TipTag: React.FC<TipTagProps> = ({ showText, tip, tagProps }) => (
|
||||
<Tooltip content={tip}>
|
||||
{showText ? (
|
||||
<Tag color="yellow" {...tagProps} size="mini" className="font-[500]">
|
||||
{showText}
|
||||
<IconCozInfoCircle />
|
||||
</Tag>
|
||||
) : (
|
||||
<IconCozInfoCircle />
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
/** 需要展示配置状态的渠道类别 */
|
||||
const Classes = [
|
||||
ConnectorClassification.SocialPlatform,
|
||||
ConnectorClassification.MiniProgram,
|
||||
ConnectorClassification.CozeSpaceExtensionLibrary,
|
||||
];
|
||||
|
||||
export const ConfigStatus = ({ record }: { record: PublishConnectorInfo }) => {
|
||||
if (
|
||||
!Classes.includes(record.connector_classification) ||
|
||||
record.bind_type === ConnectorBindType.NoBindRequired
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { text, color } = getConfigStatus(record);
|
||||
|
||||
return (
|
||||
<div className="flex gap-[6px]">
|
||||
{/* 配置状态 */}
|
||||
<Tag color={color} size="mini" className="font-[500]">
|
||||
{text}
|
||||
</Tag>
|
||||
{record?.connector_status === ConnectorStatus.Normal ? null : (
|
||||
<TipTag
|
||||
showText={
|
||||
record?.connector_status === ConnectorStatus.InReview
|
||||
? I18n.t('bot_publish_columns_status_in_review')
|
||||
: I18n.t('bot_publish_columns_status_offline')
|
||||
}
|
||||
tip={
|
||||
record?.connector_status === ConnectorStatus.InReview
|
||||
? I18n.t('bot_publish_in_review_notice')
|
||||
: I18n.t('bot_publish_offline_notice_no_certain_time', {
|
||||
platform: record?.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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 { type MouseEventHandler } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ConnectorBindType,
|
||||
type PublishConnectorInfo,
|
||||
ConnectorConfigStatus,
|
||||
ConnectorStatus,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { KvBindButton } from '@coze-agent-ide/space-bot/component/connector-action';
|
||||
import { AuthorizeButton } from '@coze-agent-ide/space-bot/component/authorize-button';
|
||||
|
||||
import { TEMPLATE_CONNECTOR_ID } from '@/utils/constants';
|
||||
|
||||
import { useProjectPublishStore } from '../../store';
|
||||
import { useBizConnectorAnchor } from '../../hooks/use-biz-connector-anchor';
|
||||
import { WebSdkBind } from './bind-actions/web-sdk-bind';
|
||||
import { TemplateBind } from './bind-actions/template-bind';
|
||||
import { StoreBind } from './bind-actions/store-bind';
|
||||
// import { ApiBind } from './bind-actions/api-bind';
|
||||
|
||||
interface ConnectorActionProps {
|
||||
record: PublishConnectorInfo;
|
||||
checked: boolean;
|
||||
authActionWrapperClassName?: string;
|
||||
}
|
||||
export function ConnectorAction(props: ConnectorActionProps) {
|
||||
const { record, checked, authActionWrapperClassName } = props;
|
||||
|
||||
const { setProjectPublishInfo, connectorList, selectedConnectorIds } =
|
||||
useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
connectorList: state.connectorList,
|
||||
selectedConnectorIds: state.selectedConnectorIds,
|
||||
})),
|
||||
);
|
||||
|
||||
const { setAnchor } = useBizConnectorAnchor();
|
||||
const stopEventPropagation: MouseEventHandler = mouseEvent => {
|
||||
mouseEvent.stopPropagation();
|
||||
};
|
||||
|
||||
// 绑定/解除绑定是同一个回调,可以根据 bind_id 是否为空来区分
|
||||
const kvBindSuccessCallback = (value?: PublishConnectorInfo) => {
|
||||
if (value) {
|
||||
const isUnbind = !value.bind_id;
|
||||
const newValue: PublishConnectorInfo = {
|
||||
...value,
|
||||
config_status: isUnbind
|
||||
? ConnectorConfigStatus.NotConfigured
|
||||
: ConnectorConfigStatus.Configured,
|
||||
connector_status: isUnbind
|
||||
? ConnectorStatus.Normal
|
||||
: value.connector_status,
|
||||
};
|
||||
setProjectPublishInfo({
|
||||
connectorList: connectorList.map(item =>
|
||||
item.id === value.id ? newValue : item,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authRevokeSuccess = () => {
|
||||
setProjectPublishInfo({
|
||||
connectorList: connectorList.map(item => {
|
||||
if (item.id === record.id) {
|
||||
return {
|
||||
...item,
|
||||
config_status: ConnectorConfigStatus.NotConfigured,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
selectedConnectorIds: selectedConnectorIds.filter(
|
||||
item => item !== record.id,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
switch (record.bind_type) {
|
||||
case ConnectorBindType.KvBind:
|
||||
case ConnectorBindType.KvAuthBind:
|
||||
return (
|
||||
// 使用 basis-full 强制 flex row 换行
|
||||
<div
|
||||
className={classNames(
|
||||
'basis-full self-end',
|
||||
authActionWrapperClassName,
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex" onClick={stopEventPropagation}>
|
||||
<KvBindButton
|
||||
record={record}
|
||||
bindSuccessCallback={kvBindSuccessCallback}
|
||||
origin="project"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case ConnectorBindType.AuthBind:
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'basis-full self-end',
|
||||
authActionWrapperClassName,
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex" onClick={stopEventPropagation}>
|
||||
<AuthorizeButton
|
||||
origin="publish"
|
||||
id={record.id}
|
||||
agentType="project"
|
||||
channelName={record.name}
|
||||
status={
|
||||
record.config_status ?? ConnectorConfigStatus.NotConfigured
|
||||
}
|
||||
onBeforeAuthRedirect={({ id }) => {
|
||||
setAnchor(id);
|
||||
}}
|
||||
revokeSuccess={authRevokeSuccess}
|
||||
authInfo={record?.auth_login_info ?? {}}
|
||||
isV2
|
||||
v2ButtonProps={{
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case ConnectorBindType.WebSDKBind:
|
||||
return (
|
||||
<WebSdkBind
|
||||
checked={checked}
|
||||
record={record}
|
||||
onClick={stopEventPropagation}
|
||||
/>
|
||||
);
|
||||
// 社区版暂不支持商店渠道绑定,用于未来拓展
|
||||
case ConnectorBindType.StoreBind:
|
||||
return (
|
||||
<StoreBind
|
||||
checked={checked}
|
||||
record={record}
|
||||
onClick={stopEventPropagation}
|
||||
/>
|
||||
);
|
||||
// 社区版暂不支持模板渠道绑定,用于未来拓展
|
||||
// bind_type=9 用作扣子第一方渠道的标识,需要按照渠道 ID 展示绑定方式
|
||||
// TODO 后端更新 ConnectorBindType 类型定义
|
||||
case ConnectorBindType.TemplateBind: {
|
||||
if (record.id === TEMPLATE_CONNECTOR_ID) {
|
||||
return <TemplateBind record={record} onClick={stopEventPropagation} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 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 */
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { type MouseEventHandler, useEffect, useRef } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { useHover } from 'ahooks';
|
||||
import {
|
||||
ConnectorBindType,
|
||||
ConnectorClassification,
|
||||
ConnectorConfigStatus,
|
||||
type ConnectorPublishConfig,
|
||||
type PublishConnectorInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Checkbox, Tooltip, Typography } from '@coze-arch/coze-design';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
import { type TextProps } from '@coze-arch/bot-semi/Typography';
|
||||
|
||||
import { TEMPLATE_CONNECTOR_ID } from '@/utils/constants';
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
import { isStoreBindConfigured } from '../utils/is-store-bind-configured';
|
||||
import { getConnectorNotConfigured } from '../utils/connector-disabled-publish';
|
||||
import { McpConfigBtn } from './mcp-config-btn';
|
||||
import { UnionSelect } from './connector-union-select';
|
||||
import { ConnectorAction } from './connector-action';
|
||||
import { ConfigStatus } from './config-status';
|
||||
import { UndoButton } from './bind-actions/undo-button';
|
||||
|
||||
enum DisabledReason {
|
||||
/** 社交渠道未选择 chatflow */
|
||||
SocialPlatform,
|
||||
/** 未绑定 未授权 */
|
||||
NotConfigured,
|
||||
/** 后端下发的原因 */
|
||||
NotAllowed,
|
||||
/** 未配置模板 */
|
||||
Template,
|
||||
}
|
||||
|
||||
interface ConnectorDisabledConfig {
|
||||
reason: DisabledReason;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
function getConnectorDisabledConfig({
|
||||
connector,
|
||||
socialPlatformConfig,
|
||||
templateConfigured,
|
||||
connectorPublishConfig,
|
||||
connectorConfigMap,
|
||||
}: {
|
||||
connector: PublishConnectorInfo;
|
||||
socialPlatformConfig: ConnectorPublishConfig | undefined;
|
||||
templateConfigured: boolean | undefined;
|
||||
connectorPublishConfig: Record<string, ConnectorPublishConfig>;
|
||||
connectorConfigMap: Record<string, Record<string, string>>;
|
||||
}): ConnectorDisabledConfig | undefined {
|
||||
if (
|
||||
connector.connector_classification ===
|
||||
ConnectorClassification.SocialPlatform &&
|
||||
!socialPlatformConfig?.selected_workflows?.[0]?.workflow_id
|
||||
) {
|
||||
// 发布到社交渠道,且未选择 chatflow
|
||||
return {
|
||||
reason: DisabledReason.SocialPlatform,
|
||||
text: I18n.t('project_release_chatflow4'),
|
||||
};
|
||||
}
|
||||
const notConfigured = {
|
||||
reason: DisabledReason.NotConfigured,
|
||||
text: I18n.t('project_release_set_desc'),
|
||||
};
|
||||
// 未绑定 未授权
|
||||
if (getConnectorNotConfigured(connector)) {
|
||||
return notConfigured;
|
||||
}
|
||||
// 后端下发的不能发布的原因
|
||||
if (!connector.allow_publish && connector.not_allow_publish_reason) {
|
||||
return {
|
||||
reason: DisabledReason.NotAllowed,
|
||||
text: connector.not_allow_publish_reason,
|
||||
};
|
||||
}
|
||||
// 未配置模板
|
||||
if (connector.id === TEMPLATE_CONNECTOR_ID && !templateConfigured) {
|
||||
return {
|
||||
reason: DisabledReason.Template,
|
||||
text: I18n.t('project_release_template_info'),
|
||||
};
|
||||
}
|
||||
|
||||
const isWebSDK =
|
||||
connector.connector_classification === ConnectorClassification.APIOrSDK &&
|
||||
connector.bind_type === ConnectorBindType.WebSDKBind;
|
||||
|
||||
const isSdkChatFlowConfigured = Boolean(
|
||||
connectorPublishConfig[connector.id]?.selected_workflows,
|
||||
);
|
||||
|
||||
if (isWebSDK && !isSdkChatFlowConfigured) {
|
||||
return notConfigured;
|
||||
}
|
||||
const isStorePublish =
|
||||
connector.connector_classification === ConnectorClassification.Coze &&
|
||||
connector.bind_type === ConnectorBindType.StoreBind;
|
||||
const storeConfig = connectorConfigMap[connector.id];
|
||||
if (isStorePublish && (!storeConfig || !isStoreBindConfigured(storeConfig))) {
|
||||
return notConfigured;
|
||||
}
|
||||
}
|
||||
|
||||
// 与后端约定的额外描述信息
|
||||
interface DescriptionExtra {
|
||||
// 渠道名称 hover 的 tooltip
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface ConnectorCardProps {
|
||||
connectorInfo: PublishConnectorInfo;
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
onShowWebSdkGuide: () => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- it's complex
|
||||
export function ConnectorCard({
|
||||
connectorInfo,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
onShowWebSdkGuide,
|
||||
}: ConnectorCardProps) {
|
||||
const { id, name, description, bind_id = '' } = connectorInfo;
|
||||
|
||||
const {
|
||||
templateConfigured,
|
||||
socialPlatformChatflow,
|
||||
connectorPublishConfig,
|
||||
connectors,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
templateConfigured: state.templateConfigured,
|
||||
socialPlatformChatflow: state.socialPlatformChatflow,
|
||||
connectorPublishConfig: state.connectorPublishConfig,
|
||||
connectors: state.connectors,
|
||||
})),
|
||||
);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const isHover = useHover(divRef.current);
|
||||
|
||||
const disabledConfig = getConnectorDisabledConfig({
|
||||
connector: connectorInfo,
|
||||
socialPlatformConfig: socialPlatformChatflow,
|
||||
templateConfigured,
|
||||
connectorPublishConfig,
|
||||
connectorConfigMap: connectors,
|
||||
});
|
||||
// 社区版暂不支持社交平台渠道,用于未来拓展。
|
||||
// 社交渠道未选择“处理消息的对话流”时,需要将整个卡片展示为禁用态
|
||||
const cardDisabled = disabledConfig?.reason === DisabledReason.SocialPlatform;
|
||||
|
||||
const descriptionExtra = (typeSafeJSONParse(
|
||||
connectorInfo.description_extra,
|
||||
) ?? {}) as DescriptionExtra;
|
||||
|
||||
// 如果禁用状态发生了变动,取消勾选当前渠道
|
||||
useEffect(() => {
|
||||
if (checked && disabledConfig) {
|
||||
onCheckedChange(false);
|
||||
}
|
||||
}, [checked, disabledConfig]);
|
||||
|
||||
const connectorCheckbox = (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
if (!disabledConfig) {
|
||||
onCheckedChange(!checked);
|
||||
}
|
||||
}}
|
||||
data-testid="project.publish.select.connector"
|
||||
disabled={Boolean(disabledConfig)}
|
||||
/>
|
||||
);
|
||||
const stopEventPropagation: MouseEventHandler = mouseEvent => {
|
||||
mouseEvent.stopPropagation();
|
||||
};
|
||||
|
||||
const getIsShowAction = () => {
|
||||
const isConnectorAuth = [
|
||||
ConnectorBindType.KvBind,
|
||||
ConnectorBindType.KvAuthBind,
|
||||
ConnectorBindType.AuthBind,
|
||||
].includes(connectorInfo.bind_type);
|
||||
const isConnectorAuthConfigured =
|
||||
connectorInfo.config_status === ConnectorConfigStatus.Configured;
|
||||
|
||||
if (isConnectorAuth) {
|
||||
return (
|
||||
!isConnectorAuthConfigured || (isConnectorAuthConfigured && isHover)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isShowAction = getIsShowAction();
|
||||
|
||||
const connectorCard = (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={classNames(
|
||||
'w-full min-h-[132px] rounded-[8px] coz-stroke-primary border border-solid coz-mg-card p-[12px] flex',
|
||||
cardDisabled || disabledConfig
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer',
|
||||
isHover && 'coz-shadow-default',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disabledConfig) {
|
||||
onCheckedChange(!checked);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames('h-full w-full flex flex-col', {
|
||||
'opacity-30 pointer-events-none': cardDisabled,
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-[6px] items-center font-medium">
|
||||
<img
|
||||
src={connectorInfo.icon_url}
|
||||
className="w-[24px] h-[24px] rounded-[6px] coz-stroke-primary border-[0.5px] border-solid border-solid"
|
||||
/>
|
||||
{descriptionExtra.text ? (
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
style={{ minWidth: '240px', textAlign: 'center' }}
|
||||
content={
|
||||
<ReactMarkdown
|
||||
skipHtml={true}
|
||||
linkTarget="_blank"
|
||||
components={{
|
||||
img: props => (
|
||||
<img {...props} className="max-w-[224px] max-h-[208px]" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{descriptionExtra.text}
|
||||
</ReactMarkdown>
|
||||
}
|
||||
>
|
||||
<div className="coz-fg-primary cursor-default">{name}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="coz-fg-primary cursor-default">{name}</div>
|
||||
)}
|
||||
<ConfigStatus record={connectorInfo} />
|
||||
</div>
|
||||
<div className="mt-[4px] mb-[8px]">
|
||||
{description ? (
|
||||
<ReactMarkdown
|
||||
skipHtml={true}
|
||||
transformLinkUri={false}
|
||||
components={{
|
||||
p: props => (
|
||||
<Typography.Paragraph type="secondary" fontSize="12px">
|
||||
{props.children}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
a: props => {
|
||||
const textProps: TextProps =
|
||||
props.href === 'coze://web-sdk-guide'
|
||||
? {
|
||||
link: true,
|
||||
onClick: e => {
|
||||
stopEventPropagation(e);
|
||||
onShowWebSdkGuide();
|
||||
},
|
||||
}
|
||||
: {
|
||||
link: {
|
||||
href: props.href,
|
||||
target: '_blank',
|
||||
onClick: stopEventPropagation,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Typography.Text fontSize="12px" {...textProps}>
|
||||
{props.children}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap grow gap-[6px]">
|
||||
{connectorInfo.connector_union_id ? (
|
||||
<UnionSelect record={connectorInfo} />
|
||||
) : null}
|
||||
<ConnectorAction
|
||||
record={connectorInfo}
|
||||
checked={checked}
|
||||
// 这个组件内部有 modal 不能使用条件渲染
|
||||
authActionWrapperClassName={classNames(!isShowAction && 'hidden')}
|
||||
/>
|
||||
{connectorInfo.connector_classification ===
|
||||
ConnectorClassification.APIOrSDK ? (
|
||||
<UndoButton
|
||||
bindId={bind_id}
|
||||
checked={checked}
|
||||
connectorId={id}
|
||||
className={classNames(!isShowAction && 'hidden', 'mt-auto')}
|
||||
onClick={stopEventPropagation}
|
||||
/>
|
||||
) : null}
|
||||
{/* 社区版暂不支持MCP服务渠道,用于未来拓展 */}
|
||||
{connectorInfo.connector_classification ===
|
||||
ConnectorClassification.CozeSpaceExtensionLibrary &&
|
||||
connectorInfo.bind_type === ConnectorBindType.TemplateBind && (
|
||||
<McpConfigBtn record={connectorInfo} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{disabledConfig && !cardDisabled ? (
|
||||
<Tooltip theme="dark" content={disabledConfig.text}>
|
||||
{connectorCheckbox}
|
||||
</Tooltip>
|
||||
) : (
|
||||
connectorCheckbox
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
theme="dark"
|
||||
visible={cardDisabled && isHover}
|
||||
content={disabledConfig?.text}
|
||||
trigger="custom"
|
||||
>
|
||||
{connectorCard}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.highlight {
|
||||
border-radius: 4px;
|
||||
animation: highlight-animation 3s forwards; /* 持续3秒的动画 */
|
||||
}
|
||||
|
||||
@keyframes highlight-animation {
|
||||
from {
|
||||
@apply coz-mg-hglt-secondary;
|
||||
}
|
||||
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { type AnimationEventHandler } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { ConnectorClassification } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozInfoCircle,
|
||||
IconCozDiamondFill,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip, Space } from '@coze-arch/coze-design';
|
||||
import {
|
||||
useBenefitAvailable,
|
||||
PremiumPaywallScene,
|
||||
usePremiumPaywallModal,
|
||||
} from '@coze-studio/premium-components-adapter';
|
||||
|
||||
import styles from './connector-group-header.module.less';
|
||||
|
||||
interface ConnectorGroupHeaderProps {
|
||||
label: string;
|
||||
tooltipContent: string;
|
||||
showTooltipInfo: boolean;
|
||||
isHighlight: boolean;
|
||||
type: ConnectorClassification;
|
||||
onAnimationEnd: AnimationEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function ConnectorGroupHeader({
|
||||
label,
|
||||
tooltipContent,
|
||||
showTooltipInfo,
|
||||
isHighlight,
|
||||
type,
|
||||
onAnimationEnd,
|
||||
}: ConnectorGroupHeaderProps) {
|
||||
// 付费墙
|
||||
const isAPIOrSDK = type === ConnectorClassification.APIOrSDK;
|
||||
const isAvailable = useBenefitAvailable({
|
||||
scene: PremiumPaywallScene.API,
|
||||
});
|
||||
const { node: premiumPaywallModal, open: openPremiumPaywallModal } =
|
||||
usePremiumPaywallModal({ scene: PremiumPaywallScene.API });
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'mb-8px px-2px coz-fg-secondary flex items-center gap-x-4px',
|
||||
isHighlight && styles.highlight,
|
||||
)}
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
>
|
||||
<p className="text-[14px] font-[500] leading-[20px]">{label}</p>
|
||||
{isAPIOrSDK && !isAvailable ? (
|
||||
<Space className="text-[12px] ml-[8px]" spacing={2}>
|
||||
<IconCozDiamondFill className="coz-fg-hglt" />
|
||||
{I18n.t('coze_quota_exemption_notice', {
|
||||
link: (
|
||||
<div
|
||||
className="coz-fg-hglt cursor-pointer"
|
||||
onClick={openPremiumPaywallModal}
|
||||
>
|
||||
{I18n.t('coze_upgrade_package')}
|
||||
</div>
|
||||
),
|
||||
})}
|
||||
</Space>
|
||||
) : null}
|
||||
{showTooltipInfo ? (
|
||||
<Tooltip theme="dark" trigger="hover" content={tooltipContent}>
|
||||
<IconCozInfoCircle className="text-xxl" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{premiumPaywallModal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.mask {
|
||||
// 百分比根据高度 80px 计算
|
||||
background: linear-gradient(180deg, rgba(var(--coze-bg-2),var(--coze-bg-2-alpha)) 0 85%, rgba(0, 0, 0, 0%) 100%);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { type CSSProperties, forwardRef, type PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Badge, Button } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface TabbarItemProps {
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
selectedConnectorCount: number;
|
||||
}
|
||||
export const ConnectorTabbarItem: React.FC<
|
||||
PropsWithChildren<TabbarItemProps>
|
||||
> = ({ onClick, isActive, children, selectedConnectorCount }) => (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
color={isActive ? 'highlight' : 'secondary'}
|
||||
className="!px-8px !font-medium"
|
||||
>
|
||||
{children}
|
||||
{selectedConnectorCount > 0 ? (
|
||||
<Badge
|
||||
countClassName={classNames(
|
||||
!isActive && '!coz-mg-plus !coz-fg-secondary',
|
||||
'!font-medium',
|
||||
)}
|
||||
className="ml-4px"
|
||||
count={selectedConnectorCount}
|
||||
type="alt"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export interface ConnectorTabbarProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const ConnectorTabbar = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<ConnectorTabbarProps>
|
||||
>(({ className, style, children }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
// ! 80px 高度影响 styles.mask 计算
|
||||
'flex items-center gap-x-8px h-[80px] relative',
|
||||
styles.mask,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/idl/intelligence_api';
|
||||
import { FormSelect } from '@coze-arch/coze-design';
|
||||
import { IconCozArrowDown } from '@coze-arch/bot-icons';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
interface UnionSelectProps {
|
||||
record: PublishConnectorInfo;
|
||||
}
|
||||
|
||||
export const UnionSelect = ({ record }: UnionSelectProps) => {
|
||||
const { connectorUnionMap, unions, setProjectPublishInfo } =
|
||||
useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectorUnionMap: state.connectorUnionMap,
|
||||
unions: state.unions,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
const unionId = record.connector_union_id ?? '';
|
||||
const unionConnectors = connectorUnionMap[unionId]?.connector_options ?? [];
|
||||
const unionOptionList = unionConnectors.map(c => ({
|
||||
label: c.show_name,
|
||||
value: c.connector_id,
|
||||
}));
|
||||
|
||||
const onSelectUnion = (selectedId: string) => {
|
||||
setProjectPublishInfo({
|
||||
unions: {
|
||||
...unions,
|
||||
[unionId]: selectedId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex" onClick={e => e.stopPropagation()}>
|
||||
<FormSelect
|
||||
noLabel
|
||||
field={`union_select_${unionId}`}
|
||||
fieldClassName="w-[172px]"
|
||||
className="w-full"
|
||||
optionList={unionOptionList}
|
||||
initValue={unions[unionId]}
|
||||
arrowIcon={<IconCozArrowDown />}
|
||||
onSelect={(val: unknown) => onSelectUnion(val as string)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 {
|
||||
type PublishConnectorInfo,
|
||||
ConnectorConfigStatus,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { UseMcpConfigModal } from '@/hooks/use-mcp-config-modal';
|
||||
|
||||
/** MCP配置按钮+弹窗 */
|
||||
export const McpConfigBtn = ({ record }: { record: PublishConnectorInfo }) => {
|
||||
const { node, open } = UseMcpConfigModal({ record });
|
||||
return (
|
||||
<div
|
||||
className="basis-full self-end"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
open();
|
||||
}}
|
||||
>
|
||||
{record.config_status === ConnectorConfigStatus.Configured
|
||||
? I18n.t('enterprise_sso_seetings_page_desc_button1')
|
||||
: I18n.t('bot_publish_action_configure')}
|
||||
</Button>
|
||||
{node}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 { type PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozCheckMarkFill } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
type optionRenderProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
export type OptionWithTooltipProps = PropsWithChildren<{
|
||||
option: optionRenderProps;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
|
||||
export function OptionWithTooltip({
|
||||
option,
|
||||
tooltip,
|
||||
children,
|
||||
}: OptionWithTooltipProps) {
|
||||
const optionNode = (
|
||||
<div
|
||||
className={classNames(
|
||||
'coz-select-option-item p-[8px] gap-x-[8px] items-center',
|
||||
{
|
||||
'!cursor-not-allowed': option.disabled,
|
||||
},
|
||||
)}
|
||||
onClick={option.onClick}
|
||||
>
|
||||
<div className="w-[16px] h-[16px] shrink-0">
|
||||
{option.selected ? (
|
||||
<IconCozCheckMarkFill className="coz-fg-hglt" />
|
||||
) : null}
|
||||
</div>
|
||||
{children ?? (
|
||||
<Typography.Text className="leading-[16px]" disabled={option.disabled}>
|
||||
{option.label}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return tooltip ? (
|
||||
<Tooltip theme="dark" position="right" trigger="hover" content={tooltip}>
|
||||
{optionNode}
|
||||
</Tooltip>
|
||||
) : (
|
||||
optionNode
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { type PropsWithChildren, useRef, useState } from 'react';
|
||||
|
||||
import { DEFAULT_PUBLISH_HEADER_HEIGHT } from '../../../utils/constants';
|
||||
import { PublishContainerContext } from '../../../context/publish-container-context';
|
||||
|
||||
export const PublishContainer: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [publishHeaderHeight, setPublishHeaderHeight] = useState(
|
||||
DEFAULT_PUBLISH_HEADER_HEIGHT,
|
||||
);
|
||||
|
||||
const getContainerRef = () => ref;
|
||||
|
||||
return (
|
||||
<PublishContainerContext.Provider
|
||||
value={{ getContainerRef, publishHeaderHeight, setPublishHeaderHeight }}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex-[1] w-full h-full overflow-x-hidden coz-bg-primary"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</PublishContainerContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { CheckType } from '@coze-arch/idl/workflow_api';
|
||||
import { ConnectorClassification } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FormSelect, type optionRenderProps } from '@coze-arch/coze-design';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
import {
|
||||
type ChatflowOptionProps,
|
||||
useChatflowOptions,
|
||||
} from '../hooks/use-chatflow-options';
|
||||
import { OptionWithTooltip } from './option-with-tooltip';
|
||||
|
||||
export const SocialPlatformChatflow: React.FC<{ className?: string }> = ({
|
||||
className,
|
||||
}) => {
|
||||
const {
|
||||
connectorList,
|
||||
selectedConnectorIds,
|
||||
socialPlatformChatflow,
|
||||
setProjectPublishInfo,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectorList: state.connectorList,
|
||||
selectedConnectorIds: state.selectedConnectorIds,
|
||||
socialPlatformChatflow: state.socialPlatformChatflow,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
const hasSelectedSocialPlatforms = connectorList.some(
|
||||
c =>
|
||||
selectedConnectorIds.includes(c.id) &&
|
||||
c.connector_classification === ConnectorClassification.SocialPlatform,
|
||||
);
|
||||
|
||||
const { chatflowOptions } = useChatflowOptions(CheckType.SocialPublish);
|
||||
|
||||
const handleSelectChatflow = (option: ChatflowOptionProps) => {
|
||||
setProjectPublishInfo({
|
||||
socialPlatformChatflow: {
|
||||
selected_workflows: [
|
||||
{
|
||||
workflow_id: option.value,
|
||||
workflow_name: option.label,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('w-[50%] pr-[6px]', className)}>
|
||||
<FormSelect
|
||||
field="social_platform_chatflow"
|
||||
noLabel
|
||||
label={I18n.t('project_release_chatflow2')}
|
||||
placeholder={I18n.t('project_release_chatflow_choose')}
|
||||
optionList={chatflowOptions}
|
||||
initValue={socialPlatformChatflow?.selected_workflows?.[0]?.workflow_id}
|
||||
className="w-full mb-[4px]"
|
||||
renderOptionItem={(option: optionRenderProps) => (
|
||||
<OptionWithTooltip option={option} tooltip={option.tooltip} />
|
||||
)}
|
||||
onSelect={(_: unknown, option: unknown) =>
|
||||
handleSelectChatflow(option as ChatflowOptionProps)
|
||||
}
|
||||
rules={[
|
||||
// 有选择 SocialPlatform 时, chatflow 必填
|
||||
{ required: hasSelectedSocialPlatforms },
|
||||
// 校验已选择的 chatflow 是否存在 && 未被禁用
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown) => {
|
||||
if (!hasSelectedSocialPlatforms) {
|
||||
return true;
|
||||
}
|
||||
const selected = chatflowOptions?.find(
|
||||
option => option.value === (value as string),
|
||||
);
|
||||
if (!selected) {
|
||||
return new Error(I18n.t('project_release_chatflow3'));
|
||||
}
|
||||
if (selected.disabled) {
|
||||
return new Error(selected.tooltip);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.desc-input {
|
||||
> input {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
@@ -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 { type CSSProperties, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
type CommonFieldProps,
|
||||
Input,
|
||||
TextArea,
|
||||
type TextAreaProps,
|
||||
withField,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface VersionDescInputProps
|
||||
extends Pick<
|
||||
TextAreaProps,
|
||||
'placeholder' | 'maxLength' | 'maxCount' | 'wrapperClassName' | 'value'
|
||||
> {
|
||||
onChange?: (value: string) => void;
|
||||
inputClassName?: string;
|
||||
textAreaClassName?: string;
|
||||
textAreaStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
const VersionDescInput: React.FC<VersionDescInputProps> = ({
|
||||
inputClassName,
|
||||
textAreaClassName,
|
||||
wrapperClassName,
|
||||
textAreaStyle,
|
||||
...props
|
||||
}) => {
|
||||
const [mode, setMode] = useState<'input' | 'textarea'>('input');
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const target = textAreaRef.current;
|
||||
if (mode !== 'textarea' || !target) {
|
||||
return;
|
||||
}
|
||||
const valueLength = props.value?.length;
|
||||
target.focus();
|
||||
if (!valueLength) {
|
||||
return;
|
||||
}
|
||||
target.setSelectionRange(valueLength, valueLength);
|
||||
}, [mode]);
|
||||
|
||||
if (mode === 'input') {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className={classNames(styles['desc-input'], inputClassName)}
|
||||
onFocus={() => {
|
||||
setMode('textarea');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<TextArea
|
||||
{...props}
|
||||
ref={textAreaRef}
|
||||
className={textAreaClassName}
|
||||
style={textAreaStyle}
|
||||
autoFocus
|
||||
autosize={{ minRows: 1, maxRows: 10 }}
|
||||
onBlur={() => {
|
||||
setMode('input');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormVersionDescInput: React.FC<
|
||||
CommonFieldProps & VersionDescInputProps
|
||||
> = withField(VersionDescInput);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
import { useBizConnectorAnchor } from '@/hooks/use-biz-connector-anchor';
|
||||
|
||||
import { type ConnectorGroup } from '../utils/format-connector-groups';
|
||||
import { usePublishContainer } from '../../context/publish-container-context';
|
||||
import { type ConnectorRefMap } from './use-connector-scroll';
|
||||
|
||||
export const useAutoScrollToConnector = ({
|
||||
connectorGroupList,
|
||||
connectorRefMap,
|
||||
}: {
|
||||
connectorRefMap: ConnectorRefMap;
|
||||
connectorGroupList: ConnectorGroup[];
|
||||
}) => {
|
||||
const { getAnchor, removeAnchor } = useBizConnectorAnchor();
|
||||
const { getContainerRef } = usePublishContainer();
|
||||
|
||||
useEffect(() => {
|
||||
const anchor = getAnchor();
|
||||
|
||||
if (!anchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetGroup = connectorGroupList.find(group =>
|
||||
group.connectors.some(
|
||||
connector => connector.id === anchor.connectorIdBeforeRedirect,
|
||||
),
|
||||
);
|
||||
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectorRef = connectorRefMap[targetGroup.type];
|
||||
const { updateSelectedConnectorIds } = useProjectPublishStore.getState();
|
||||
updateSelectedConnectorIds(prev => {
|
||||
if (prev.some(id => id === anchor.connectorIdBeforeRedirect)) {
|
||||
return prev;
|
||||
}
|
||||
return prev.concat(anchor.connectorIdBeforeRedirect);
|
||||
});
|
||||
getContainerRef()?.current?.scrollTo({
|
||||
top: connectorRef.current?.offsetTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
removeAnchor();
|
||||
}, [connectorGroupList, connectorRefMap]);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { useRequest } from 'ahooks';
|
||||
import { type CheckType, WorkflowMode } from '@coze-arch/idl/workflow_api';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { workflowApi } from '@coze-arch/bot-api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export interface ChatflowOptionProps {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export function useChatflowOptions(checkType?: CheckType) {
|
||||
const needsCheck = typeof checkType !== 'undefined';
|
||||
const { space_id = '', project_id = '' } = useParams<DynamicParams>();
|
||||
const { data: chatflowOptions, loading } = useRequest(async () => {
|
||||
const res = await workflowApi.GetWorkFlowList({
|
||||
space_id,
|
||||
project_id,
|
||||
flow_mode: WorkflowMode.ChatFlow,
|
||||
page: 1,
|
||||
size: 100,
|
||||
checker: needsCheck ? [checkType] : undefined,
|
||||
});
|
||||
return res.data.workflow_list?.map(item => ({
|
||||
label: item.name,
|
||||
value: item.workflow_id,
|
||||
disabled: needsCheck
|
||||
? item.check_result?.find(r => r.type === checkType)?.is_pass !== true
|
||||
: false,
|
||||
tooltip: item.check_result?.find(r => r.type === checkType)?.reason,
|
||||
})) as ChatflowOptionProps[];
|
||||
});
|
||||
return { chatflowOptions, loading };
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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 max-lines-per-function */
|
||||
import { type RefObject, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useDebounceFn } from 'ahooks';
|
||||
import { ConnectorClassification } from '@coze-arch/idl/intelligence_api';
|
||||
|
||||
import { usePublishContainer } from '../../context/publish-container-context';
|
||||
|
||||
export type ConnectorRefMap = Record<
|
||||
ConnectorClassification,
|
||||
RefObject<HTMLDivElement>
|
||||
>;
|
||||
|
||||
const useConnectorRefMap = (): ConnectorRefMap => ({
|
||||
[ConnectorClassification.APIOrSDK]: useRef<HTMLDivElement>(null),
|
||||
[ConnectorClassification.MiniProgram]: useRef<HTMLDivElement>(null),
|
||||
[ConnectorClassification.SocialPlatform]: useRef<HTMLDivElement>(null),
|
||||
[ConnectorClassification.Coze]: useRef<HTMLDivElement>(null),
|
||||
[ConnectorClassification.CozeSpaceExtensionLibrary]:
|
||||
useRef<HTMLDivElement>(null),
|
||||
});
|
||||
|
||||
const getActiveConnectorTarget = ({
|
||||
containerScrollTop,
|
||||
connectorRefMap,
|
||||
connectorBarHeight,
|
||||
publishHeaderHeight,
|
||||
}: {
|
||||
containerScrollTop: number;
|
||||
connectorBarHeight: number;
|
||||
connectorRefMap: ConnectorRefMap;
|
||||
publishHeaderHeight: number;
|
||||
}) => {
|
||||
const connectorTargetLis = Object.entries(connectorRefMap)
|
||||
.map(([, ref]) => ref.current)
|
||||
.filter((element): element is HTMLDivElement => Boolean(element));
|
||||
|
||||
const offsetTopList = connectorTargetLis.map(target => ({
|
||||
offsetTop:
|
||||
target.offsetTop -
|
||||
containerScrollTop -
|
||||
publishHeaderHeight -
|
||||
connectorBarHeight,
|
||||
target,
|
||||
}));
|
||||
|
||||
const sortedTopList = offsetTopList.sort(
|
||||
(prev, cur) => prev.offsetTop - cur.offsetTop,
|
||||
);
|
||||
const preActiveConnectorList = sortedTopList.filter(
|
||||
item => item.offsetTop <= 0,
|
||||
);
|
||||
const activeConnector = preActiveConnectorList.length
|
||||
? preActiveConnectorList.at(-1)
|
||||
: sortedTopList.at(0);
|
||||
|
||||
return activeConnector?.target;
|
||||
};
|
||||
/** 经验值 */
|
||||
const LOCK_TIME = 300;
|
||||
|
||||
export const useConnectorScroll = () => {
|
||||
const [activeConnectorTarget, setActiveConnectorTarget] =
|
||||
useState<HTMLDivElement>();
|
||||
const connectorBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { getContainerRef } = usePublishContainer();
|
||||
|
||||
const connectorRefMap = useConnectorRefMap();
|
||||
|
||||
const { publishHeaderHeight } = usePublishContainer();
|
||||
|
||||
const [animationStateMap, setAnimationMap] = useState<
|
||||
Record<ConnectorClassification, boolean>
|
||||
>({
|
||||
[ConnectorClassification.APIOrSDK]: false,
|
||||
[ConnectorClassification.MiniProgram]: false,
|
||||
[ConnectorClassification.SocialPlatform]: false,
|
||||
[ConnectorClassification.Coze]: false,
|
||||
[ConnectorClassification.CozeSpaceExtensionLibrary]: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 需要 3 个条件同时满足
|
||||
* 1. 随着页面滚动到不同锚点,tab 栏激活对应区域的按钮
|
||||
* 2. 点击 tab 栏 对应按钮激活 直接滚动到页面对应区域
|
||||
* 3. 当页面高度不足滚动时,点击 tab 栏也要激活对应按钮
|
||||
*
|
||||
* 由于滚动是 smooth 效果 条件 2 与 3 会发生冲突 当用户点击 tab 栏滚动时需要进行 lock
|
||||
* lock 时 条件 1 不触发
|
||||
* 需要给条件 3 一个兜底的解锁机制
|
||||
*/
|
||||
const manualScrollLockRef = useRef(false);
|
||||
|
||||
const manualScrollLock = () => {
|
||||
manualScrollLockRef.current = true;
|
||||
};
|
||||
|
||||
const manualScrollUnLock = () => {
|
||||
manualScrollLockRef.current = false;
|
||||
};
|
||||
|
||||
/** 停止滚动 LOCK_TIME 后解锁 */
|
||||
const manualScrollUnLockDebounce = useDebounceFn(manualScrollUnLock, {
|
||||
wait: LOCK_TIME,
|
||||
});
|
||||
|
||||
/** 兜底解锁机制 如果用户点击 tab 栏但没有解锁 LOCK_TIME 后也应该解锁 */
|
||||
const baseUnLockDebounce = useDebounceFn(manualScrollUnLock, {
|
||||
wait: LOCK_TIME,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const containerTarget = getContainerRef()?.current;
|
||||
const connectorBarTarget = connectorBarRef.current;
|
||||
if (!containerTarget || !connectorBarTarget) {
|
||||
return;
|
||||
}
|
||||
const changeActiveConnectorTarget = () => {
|
||||
if (manualScrollLockRef.current) {
|
||||
return;
|
||||
}
|
||||
setActiveConnectorTarget(
|
||||
getActiveConnectorTarget({
|
||||
containerScrollTop: containerTarget.scrollTop,
|
||||
connectorRefMap,
|
||||
connectorBarHeight: connectorBarTarget.offsetHeight,
|
||||
publishHeaderHeight,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
changeActiveConnectorTarget();
|
||||
|
||||
const onScroll = () => {
|
||||
// 页面发生了滚动则不需要兜底机制
|
||||
baseUnLockDebounce.cancel();
|
||||
manualScrollUnLockDebounce.run();
|
||||
changeActiveConnectorTarget();
|
||||
};
|
||||
|
||||
containerTarget.addEventListener('scroll', onScroll);
|
||||
return () => {
|
||||
containerTarget.removeEventListener('scroll', onScroll);
|
||||
};
|
||||
}, [getContainerRef, connectorRefMap, publishHeaderHeight]);
|
||||
|
||||
const startAnimation = (classification: ConnectorClassification) => {
|
||||
setAnimationMap(prev => ({ ...prev, [classification]: true }));
|
||||
};
|
||||
|
||||
const closeAnimation = (classification: ConnectorClassification) => {
|
||||
setAnimationMap(prev => ({ ...prev, [classification]: false }));
|
||||
};
|
||||
|
||||
const scrollToConnector = (classification: ConnectorClassification) => {
|
||||
const barTarget = connectorBarRef.current;
|
||||
const containerTarget = getContainerRef()?.current;
|
||||
const connectorTarget = connectorRefMap[classification].current;
|
||||
if (!barTarget || !containerTarget || !connectorTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
containerTarget.scrollTo({
|
||||
behavior: 'smooth',
|
||||
top:
|
||||
connectorTarget.offsetTop -
|
||||
publishHeaderHeight -
|
||||
barTarget.offsetHeight,
|
||||
});
|
||||
startAnimation(classification);
|
||||
setActiveConnectorTarget(connectorTarget);
|
||||
manualScrollLock();
|
||||
baseUnLockDebounce.run();
|
||||
};
|
||||
|
||||
return {
|
||||
connectorRefMap,
|
||||
activeConnectorTarget,
|
||||
connectorBarRef,
|
||||
scrollToConnector,
|
||||
closeAnimation,
|
||||
animationStateMap,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { useRequest } from 'ahooks';
|
||||
import { type ProductEntityType } from '@coze-arch/bot-api/product_api';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
|
||||
export interface CategoryOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function useProductCategoryOptions(entityType: ProductEntityType) {
|
||||
const { data: categoryOptions, loading } = useRequest(async () => {
|
||||
const res = await ProductApi.PublicGetProductCategoryList({
|
||||
need_empty_category: true,
|
||||
entity_type: entityType,
|
||||
});
|
||||
return res.data?.categories?.map(item => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})) as CategoryOptions[];
|
||||
});
|
||||
return { categoryOptions, loading };
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// 修改semi样式 适配UI规范
|
||||
.publish-collapse {
|
||||
:global {
|
||||
.semi-collapse-item {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.semi-collapse-item:hover {
|
||||
border-color: var(--coz-bg-primary);
|
||||
}
|
||||
|
||||
.semi-collapse-header:hover::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
||||
background-color: var(--coz-bg-primary);
|
||||
}
|
||||
|
||||
.semi-collapse-header {
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--coz-mg-secondary-hovered);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--coz-mg-secondary-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
.semi-collapse-header-icon {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.semi-collapse-content {
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
|
||||
.semi-collapse-item:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.semi-form-field-label {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.publish-cards {
|
||||
:global {
|
||||
.semi-select.coz-select {
|
||||
height: 24px;
|
||||
border-radius: var(--small, 6px);
|
||||
|
||||
.semi-select-inset-label {
|
||||
margin: 0 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
|
||||
.semi-select-selection .semi-select-selection-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.semi-select-arrow {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.semi-select-content-wrapper-empty {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.semi-select-content-wrapper-collapse {
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
|
||||
.semi-overflow-list-item, .semi-overflow-list-overflow {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tag {
|
||||
height: 16px;
|
||||
padding: 2px;
|
||||
background: var(--coz-mg_primary, rgba(6, 7, 9, 8%));
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-select-error {
|
||||
border: 1px solid var(--coz-stroke-hglt-red);
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line selector-class-pattern -- 暂时修复 checkbox 边框样式
|
||||
.coz-checkbox.semi-checkbox-unChecked {
|
||||
.semi-checkbox-inner-display {
|
||||
border-color: var(--coz-stroke-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project {
|
||||
:global {
|
||||
.semi-form-field-error-message {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: var(--coz-fg-hglt-red);
|
||||
}
|
||||
|
||||
.semi-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-form-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-input-textarea {
|
||||
height: 72px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import { useErrorHandler } from '@coze-arch/logger';
|
||||
import { Form, Spin } from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useProjectPublishStore } from '../store';
|
||||
import {
|
||||
loadProjectPublishDraft,
|
||||
saveProjectPublishDraft,
|
||||
} from './utils/publish-draft';
|
||||
import { initPublishStore } from './utils/init-publish-store';
|
||||
import { PublishTitleBar } from './publish-title-bar';
|
||||
import { PublishRecord } from './publish-record';
|
||||
import { PublishConnectors } from './publish-connectors';
|
||||
import { PublishBasicInfo } from './publish-basic-info';
|
||||
import { PublishContainer } from './components/publish-container';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export function ProjectPublish(): JSX.Element {
|
||||
const { project_id = '', space_id = '' } = useParams<DynamicParams>();
|
||||
const {
|
||||
showPublishResult,
|
||||
pageLoading,
|
||||
resetProjectPublishInfo,
|
||||
exportDraft,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
showPublishResult: state.showPublishResult,
|
||||
pageLoading: state.pageLoading,
|
||||
resetProjectPublishInfo: state.resetProjectPublishInfo,
|
||||
exportDraft: state.exportDraft,
|
||||
})),
|
||||
);
|
||||
const errorHandle = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
const saveDraft = () => {
|
||||
saveProjectPublishDraft(exportDraft(project_id));
|
||||
};
|
||||
window.addEventListener('beforeunload', saveDraft);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', saveDraft);
|
||||
};
|
||||
}, [exportDraft, project_id]);
|
||||
|
||||
useEffect(() => {
|
||||
initPublishStore(
|
||||
project_id,
|
||||
errorHandle,
|
||||
loadProjectPublishDraft(project_id),
|
||||
);
|
||||
return () => {
|
||||
resetProjectPublishInfo();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !pageLoading ? (
|
||||
<PublishContainer>
|
||||
<Form<Record<string, unknown>>
|
||||
className={s.project}
|
||||
showValidateIcon={false}
|
||||
>
|
||||
<PublishTitleBar />
|
||||
{!showPublishResult ? (
|
||||
<div className="flex justify-center pt-[32px] pb-[48px] coz-bg-primary">
|
||||
<div className="w-[800px]">
|
||||
<PublishBasicInfo />
|
||||
<PublishConnectors />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PublishRecord projectId={project_id} spaceId={space_id} />
|
||||
)}
|
||||
</Form>
|
||||
</PublishContainer>
|
||||
) : (
|
||||
<Spin
|
||||
spinning
|
||||
wrapperClassName="flex justify-center h-full w-full items-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form } from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { CONNECTOR_TAB_BAR_Z_INDEX } from '../utils/constants';
|
||||
import { useProjectPublishStore } from '../store';
|
||||
import { checkVersionNum } from './utils/version-number-check';
|
||||
import { FormVersionDescInput } from './components/version-desc-input';
|
||||
|
||||
export function PublishBasicInfo() {
|
||||
const { project_id = '' } = useParams<DynamicParams>();
|
||||
const {
|
||||
lastVersionNumber,
|
||||
versionNumber,
|
||||
versionDescription,
|
||||
setProjectPublishInfo,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
lastVersionNumber: state.lastVersionNumber,
|
||||
versionNumber: state.versionNumber,
|
||||
versionDescription: state.versionDescription,
|
||||
setProjectPublishInfo: state.setProjectPublishInfo,
|
||||
})),
|
||||
);
|
||||
|
||||
const inputBaseCls = '';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[8px] w-full coz-bg-plus rounded-md p-24px">
|
||||
<div className="text-[20px] coz-fg-plus font-[500] leading-[28px]">
|
||||
{I18n.t('project_release_version_info')}
|
||||
</div>
|
||||
<div className="flex gap-x-12px">
|
||||
<Form.Input
|
||||
field="version_num"
|
||||
fieldClassName="!p-0 flex-[1]"
|
||||
label={
|
||||
<span className="text-14px font-medium">
|
||||
{I18n.t('builder_publish_version_label')}
|
||||
</span>
|
||||
}
|
||||
placeholder={
|
||||
lastVersionNumber
|
||||
? I18n.t('project_release_example1', {
|
||||
version: lastVersionNumber,
|
||||
})
|
||||
: I18n.t('project_release_example')
|
||||
}
|
||||
initValue={versionNumber}
|
||||
className="bg-transparent coz-stroke-plus"
|
||||
rules={[
|
||||
{ required: true, message: I18n.t('project_release_example2') },
|
||||
]}
|
||||
onChange={value => {
|
||||
setProjectPublishInfo({
|
||||
versionNumber: value,
|
||||
});
|
||||
}}
|
||||
validate={val => checkVersionNum(val, project_id)}
|
||||
trigger={'blur'}
|
||||
maxLength={20}
|
||||
/>
|
||||
<FormVersionDescInput
|
||||
field="version_desc"
|
||||
fieldClassName="!p-0 !overflow-visible flex-[1]"
|
||||
label={
|
||||
<span className="text-14px font-medium">
|
||||
{I18n.t('builder_publish_changelog_label')}
|
||||
</span>
|
||||
}
|
||||
placeholder={I18n.t('builder_publish_changelog_placeholder')}
|
||||
initValue={versionDescription}
|
||||
maxLength={800}
|
||||
maxCount={800}
|
||||
wrapperClassName="relative overflow-visible"
|
||||
inputClassName={inputBaseCls}
|
||||
textAreaClassName={classNames(
|
||||
inputBaseCls,
|
||||
'absolute',
|
||||
'top-0',
|
||||
'left-0',
|
||||
'!coz-bg-max',
|
||||
)}
|
||||
// 比渠道 tab 高就可以, 避免遮挡
|
||||
textAreaStyle={{ zIndex: CONNECTOR_TAB_BAR_Z_INDEX + 1 }}
|
||||
onChange={value => {
|
||||
setProjectPublishInfo({
|
||||
versionDescription: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { MonetizePublishInfo } from '@coze-studio/components/monetize';
|
||||
import {
|
||||
ConnectorClassification,
|
||||
type PublishConnectorInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { PublishTermService } from '@coze-agent-ide/agent-ide-commons';
|
||||
import { IconCozEmpty } from '@coze-arch/coze-design/icons';
|
||||
import { Form } from '@coze-arch/coze-design';
|
||||
|
||||
import { useWebSdkGuideModal } from '@/web-sdk-guide';
|
||||
import { WEB_SDK_CONNECTOR_ID } from '@/utils/constants';
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
import { CONNECTOR_TAB_BAR_Z_INDEX } from '../utils/constants';
|
||||
import { usePublishContainer } from '../context/publish-container-context';
|
||||
import { formatConnectorGroups } from './utils/format-connector-groups';
|
||||
import { useConnectorScroll } from './hooks/use-connector-scroll';
|
||||
import { useAutoScrollToConnector } from './hooks/use-auto-scroll-to-connector';
|
||||
import { SocialPlatformChatflow } from './components/social-platform-chatflow';
|
||||
import {
|
||||
ConnectorTabbar,
|
||||
ConnectorTabbarItem,
|
||||
} from './components/connector-tab-bar';
|
||||
import { ConnectorGroupHeader } from './components/connector-group-header';
|
||||
import { ConnectorCard } from './components/connector-card';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const getTermServiceData = (connectorList: PublishConnectorInfo[]) =>
|
||||
connectorList
|
||||
.filter(item => item.privacy_policy || item.user_agreement)
|
||||
.map(i => ({
|
||||
name: i.name,
|
||||
icon: i.icon_url,
|
||||
privacy_policy: i.privacy_policy,
|
||||
user_agreement: i.user_agreement,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export function PublishConnectors() {
|
||||
const { project_id = '' } = useParams<DynamicParams>();
|
||||
|
||||
const {
|
||||
connectorList, // 社区版仅支持API 和 Chat SDK 渠道
|
||||
connectorUnionMap,
|
||||
monetizeConfig,
|
||||
selectedConnectorIds,
|
||||
connectorPublishConfig,
|
||||
unions,
|
||||
updateSelectedConnectorIds,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
connectorList: state.connectorList,
|
||||
connectorUnionMap: state.connectorUnionMap,
|
||||
monetizeConfig: state.monetizeConfig,
|
||||
selectedConnectorIds: state.selectedConnectorIds,
|
||||
connectorPublishConfig: state.connectorPublishConfig,
|
||||
unions: state.unions,
|
||||
updateSelectedConnectorIds: state.updateSelectedConnectorIds,
|
||||
})),
|
||||
);
|
||||
|
||||
const termServiceData = useMemo(
|
||||
() => getTermServiceData(connectorList),
|
||||
[connectorList],
|
||||
);
|
||||
|
||||
const connectorGroups = useMemo(
|
||||
() =>
|
||||
formatConnectorGroups(connectorList, connectorUnionMap, unions).filter(
|
||||
item => item.connectors.length > 0,
|
||||
),
|
||||
[connectorList, connectorUnionMap, unions],
|
||||
);
|
||||
const { publishHeaderHeight } = usePublishContainer();
|
||||
const {
|
||||
connectorRefMap,
|
||||
activeConnectorTarget,
|
||||
connectorBarRef,
|
||||
scrollToConnector,
|
||||
closeAnimation,
|
||||
animationStateMap,
|
||||
} = useConnectorScroll();
|
||||
|
||||
useAutoScrollToConnector({
|
||||
connectorGroupList: connectorGroups,
|
||||
connectorRefMap,
|
||||
});
|
||||
|
||||
const isConnectorChecked = (c: PublishConnectorInfo) =>
|
||||
selectedConnectorIds.includes(c.connector_union_id ?? c.id);
|
||||
|
||||
const onCheckConnector = (c: PublishConnectorInfo, checked: boolean) => {
|
||||
const id = c.connector_union_id ?? c.id;
|
||||
if (checked) {
|
||||
updateSelectedConnectorIds(prev => prev.concat(id));
|
||||
} else {
|
||||
updateSelectedConnectorIds(prev => prev.filter(i => i !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Collapse Panel 展开状态下,不在 header 中展示已选渠道的图标
|
||||
const getGroupHeaderList = (groupId: ConnectorClassification) => {
|
||||
const group = connectorGroups.find(g => g.type === groupId);
|
||||
if (!group) {
|
||||
return [];
|
||||
}
|
||||
return group.connectors.filter(c =>
|
||||
selectedConnectorIds.includes(c.connector_union_id ?? c.id),
|
||||
);
|
||||
};
|
||||
|
||||
const { node: guideModal, show: showWebSdkGuide } = useWebSdkGuideModal();
|
||||
const onShowWebSdkGuide = () => {
|
||||
const sdkConnector = connectorList.find(c => c.id === WEB_SDK_CONNECTOR_ID);
|
||||
const sdkConfig = connectorPublishConfig[WEB_SDK_CONNECTOR_ID];
|
||||
showWebSdkGuide({
|
||||
projectId: project_id,
|
||||
workflowId: sdkConfig?.selected_workflows?.[0]?.workflow_id ?? '',
|
||||
version: sdkConnector?.bind_info?.sdk_version ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-[24px] w-full coz-bg-plus rounded-md p-24px">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label required className="text-20px font-medium leading-[24px]">
|
||||
{I18n.t('bot_publish_select_title')}
|
||||
</Form.Label>
|
||||
{IS_OVERSEA && monetizeConfig ? (
|
||||
<MonetizePublishInfo
|
||||
className="pr-[8px] text-[12px]"
|
||||
monetizeConfig={monetizeConfig}
|
||||
supportPlatforms={connectorList
|
||||
.filter(c => c.support_monetization)
|
||||
.map(c => ({ id: c.id, name: c.name, icon: c.icon_url }))}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{termServiceData.length ? (
|
||||
<PublishTermService
|
||||
scene="project"
|
||||
termServiceData={termServiceData}
|
||||
className="mt-8px py-0 coz-fg-secondary"
|
||||
/>
|
||||
) : null}
|
||||
<ConnectorTabbar
|
||||
className="sticky"
|
||||
ref={connectorBarRef}
|
||||
style={{
|
||||
top: publishHeaderHeight,
|
||||
zIndex: CONNECTOR_TAB_BAR_Z_INDEX,
|
||||
}}
|
||||
>
|
||||
{connectorGroups.map(connector => (
|
||||
<ConnectorTabbarItem
|
||||
key={connector.type}
|
||||
selectedConnectorCount={getGroupHeaderList(connector.type).length}
|
||||
isActive={
|
||||
activeConnectorTarget === connectorRefMap[connector.type].current
|
||||
}
|
||||
onClick={() => scrollToConnector(connector.type)}
|
||||
>
|
||||
{connector.label}
|
||||
</ConnectorTabbarItem>
|
||||
))}
|
||||
</ConnectorTabbar>
|
||||
{connectorGroups.map((i, index) => {
|
||||
// 社区版暂不支持社交平台渠道
|
||||
const isSocialPlatform =
|
||||
i.type === ConnectorClassification.SocialPlatform;
|
||||
return (
|
||||
<div
|
||||
key={i.type}
|
||||
ref={connectorRefMap[i.type]}
|
||||
style={{ marginTop: index === 0 ? 0 : 24 }}
|
||||
>
|
||||
<ConnectorGroupHeader
|
||||
label={i.label}
|
||||
tooltipContent={i.desc}
|
||||
showTooltipInfo={isSocialPlatform}
|
||||
isHighlight={animationStateMap[i.type]}
|
||||
type={i.type}
|
||||
onAnimationEnd={() => {
|
||||
closeAnimation(i.type);
|
||||
}}
|
||||
/>
|
||||
{/* 社区版暂不支持社交平台渠道 */}
|
||||
{isSocialPlatform ? (
|
||||
<SocialPlatformChatflow className="mb-8px" />
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-2 gap-[12px]',
|
||||
s['publish-cards'],
|
||||
)}
|
||||
>
|
||||
{i.connectors.map(c => (
|
||||
<ConnectorCard
|
||||
key={c.id}
|
||||
connectorInfo={c}
|
||||
checked={isConnectorChecked(c)}
|
||||
onCheckedChange={checked => onCheckConnector(c, checked)}
|
||||
onShowWebSdkGuide={onShowWebSdkGuide}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!connectorList.length ? (
|
||||
<div className="flex flex-col justify-center items-center w-full h-full gap-[4px]">
|
||||
<IconCozEmpty className="w-[32px] h-[32px] coz-fg-dim" />
|
||||
<div className="text-[14px] font-medium coz-fg-primary">
|
||||
{I18n.t('publish_page_no_channel_status_title')}
|
||||
</div>
|
||||
<div className="text-[12px] coz-fg-primary">
|
||||
{I18n.t('publish_page_no_channel_status_desc')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{guideModal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 { NavLink } from 'react-router-dom';
|
||||
import { type FC } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { IntelligenceType } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useFlags } from '@coze-arch/bot-flags';
|
||||
import { useIsPublishRecordReady } from '@coze-studio/publish-manage-hooks';
|
||||
import { IconCozCheckMarkCircle } from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { useProjectPublishStore } from '../store';
|
||||
import { ProjectPublishProgress } from '../publish-progress';
|
||||
|
||||
export const PublishRecord: FC<{
|
||||
projectId: string;
|
||||
spaceId: string;
|
||||
}> = ({ projectId, spaceId }) => {
|
||||
const { publishRecordDetail } = useProjectPublishStore(
|
||||
useShallow(state => ({
|
||||
publishRecordDetail: state.publishRecordDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const [FLAGS] = useFlags();
|
||||
|
||||
const { ready, inited } = useIsPublishRecordReady({
|
||||
type: IntelligenceType.Project,
|
||||
spaceId,
|
||||
intelligenceId: projectId,
|
||||
// 社区版暂不支持该功能
|
||||
enable: FLAGS['bot.studio.publish_management'] && !IS_OPEN_SOURCE,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="my-[32px] p-[16px] flex flex-col items-center">
|
||||
<IconCozCheckMarkCircle className="text-[48px] coz-fg-dim" />
|
||||
<div className="text-[16px] font-medium mt-[8px] leading-[22px]">
|
||||
{I18n.t('project_release_already_released')}
|
||||
</div>
|
||||
<div className="text-[12px] coz-fg-dim leading-[16px]">
|
||||
{I18n.t('project_release_already_released_desc')}
|
||||
</div>
|
||||
{/* 社区版暂不支持该功能 */}
|
||||
{FLAGS['bot.studio.publish_management'] && !IS_OPEN_SOURCE ? (
|
||||
<div className="text-[12px] coz-fg-dim leading-[16px]">
|
||||
{I18n.t('release_management_detail1', {
|
||||
button: (
|
||||
<NavLink
|
||||
className={classNames(
|
||||
'no-underline',
|
||||
ready || !inited
|
||||
? 'coz-fg-hglt'
|
||||
: 'coz-fg-secondary cursor-not-allowed',
|
||||
)}
|
||||
onClick={e => {
|
||||
if (!ready) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
to={`/space/${spaceId}/publish/app/${projectId}`}
|
||||
>
|
||||
{I18n.t('release_management')}
|
||||
{ready || !inited
|
||||
? null
|
||||
: `(${I18n.t('release_management_generating')})`}
|
||||
</NavLink>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] w-[480px] coz-stroke-primary coz-bg-max border border-solid px-[24px] pt-[16px] m-auto mb-[48px]">
|
||||
<ProjectPublishProgress record={publishRecordDetail} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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 */
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
ConnectorClassification,
|
||||
type ConnectorPublishConfig,
|
||||
type GetPublishRecordDetailRequest,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowLeft } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
IconButton,
|
||||
Toast,
|
||||
useFormApi,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { intelligenceApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { isPublishFinish } from '../utils/is-publish-finish';
|
||||
import { MIN_PUBLISH_HEADER_HEIGHT } from '../utils/constants';
|
||||
import { useProjectPublishStore } from '../store';
|
||||
import { usePublishContainer } from '../context/publish-container-context';
|
||||
|
||||
export function PublishTitleBar() {
|
||||
const navigate = useNavigate();
|
||||
const formApi = useFormApi();
|
||||
const { space_id = '', project_id = '' } = useParams<DynamicParams>();
|
||||
const goBack = () => navigate(`/space/${space_id}/project-ide/${project_id}`);
|
||||
const {
|
||||
connectorList,
|
||||
showPublishResult,
|
||||
setShowPublishResult,
|
||||
versionDescription,
|
||||
versionNumber,
|
||||
connectors,
|
||||
unions,
|
||||
connectorPublishConfig,
|
||||
socialPlatformChatflow,
|
||||
selectedConnectorIds,
|
||||
setPublishRecordDetail,
|
||||
} = useProjectPublishStore(
|
||||
useShallow(s => ({
|
||||
connectorList: s.connectorList,
|
||||
showPublishResult: s.showPublishResult,
|
||||
setShowPublishResult: s.setShowPublishResult,
|
||||
versionNumber: s.versionNumber,
|
||||
versionDescription: s.versionDescription,
|
||||
connectors: s.connectors,
|
||||
unions: s.unions,
|
||||
connectorPublishConfig: s.connectorPublishConfig,
|
||||
socialPlatformChatflow: s.socialPlatformChatflow,
|
||||
selectedConnectorIds: s.selectedConnectorIds,
|
||||
setPublishRecordDetail: s.setPublishRecordDetail,
|
||||
})),
|
||||
);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const { publishHeaderHeight, setPublishHeaderHeight } = usePublishContainer();
|
||||
// 发布结果轮询
|
||||
const { run: getPublishRecordDetail, cancel } = useRequest(
|
||||
async (params: GetPublishRecordDetailRequest) =>
|
||||
await intelligenceApi.GetPublishRecordDetail(params),
|
||||
{
|
||||
pollingInterval: 3000,
|
||||
pollingWhenHidden: false,
|
||||
pollingErrorRetryCount: 3,
|
||||
manual: true,
|
||||
onSuccess: res => {
|
||||
if (res?.data) {
|
||||
setPublishRecordDetail(res.data);
|
||||
|
||||
if (isPublishFinish(res.data)) {
|
||||
cancel();
|
||||
}
|
||||
if (!showPublishResult) {
|
||||
setShowPublishResult(true);
|
||||
}
|
||||
} else {
|
||||
cancel();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
cancel();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handlePublishProject = async () => {
|
||||
if (showPublishResult) {
|
||||
goBack();
|
||||
} else {
|
||||
try {
|
||||
setPublishing(true);
|
||||
await formApi.validate();
|
||||
if (!selectedConnectorIds.length) {
|
||||
Toast.error(I18n.t('publish_tooltip_select_platform'));
|
||||
return;
|
||||
}
|
||||
const publishConnectors: Record<string, Record<string, string>> = {};
|
||||
const publishConnectorConfigs: Record<string, ConnectorPublishConfig> =
|
||||
{};
|
||||
selectedConnectorIds.forEach(id => {
|
||||
const connectorId = unions[id] ?? id;
|
||||
publishConnectors[connectorId] = connectors[connectorId] ?? {};
|
||||
// 社交平台的 chatflow 选项统一
|
||||
if (
|
||||
connectorList.find(c => c.id === connectorId)
|
||||
?.connector_classification ===
|
||||
ConnectorClassification.SocialPlatform
|
||||
) {
|
||||
publishConnectorConfigs[connectorId] = socialPlatformChatflow;
|
||||
} else if (connectorPublishConfig[connectorId]) {
|
||||
publishConnectorConfigs[connectorId] =
|
||||
connectorPublishConfig[connectorId];
|
||||
}
|
||||
});
|
||||
|
||||
const { data } = await intelligenceApi.PublishProject({
|
||||
project_id,
|
||||
version_number: versionNumber,
|
||||
description: versionDescription,
|
||||
connectors: publishConnectors,
|
||||
connector_publish_config: publishConnectorConfigs,
|
||||
});
|
||||
|
||||
setPublishRecordDetail({
|
||||
publish_monetization_result: data?.publish_monetization_result,
|
||||
});
|
||||
if (data?.publish_record_id) {
|
||||
getPublishRecordDetail({
|
||||
publish_record_id: data.publish_record_id,
|
||||
project_id,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 校验的错误还需要Toast提示出来
|
||||
if (typeof Object.values(error as Error)[0] === 'string') {
|
||||
Toast.error(Object.values(error as Error)[0]);
|
||||
}
|
||||
} finally {
|
||||
setPublishing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="sticky top-0 z-[100] coz-bg-primary overflow-y-hidden"
|
||||
style={{ height: publishHeaderHeight }}
|
||||
>
|
||||
<Helmet>
|
||||
<title>{I18n.t('Publish')}</title>
|
||||
</Helmet>
|
||||
<div className="flex p-[16px] justify-between items-center coz-stroke-primary border-b border-x-0 border-t-0 border-solid pl-2">
|
||||
<div className="flex items-center">
|
||||
<IconButton
|
||||
icon={<IconCozArrowLeft className="h-[18px] w-[18px]" />}
|
||||
color="secondary"
|
||||
iconSize="large"
|
||||
onClick={goBack}
|
||||
/>
|
||||
<span className="ml-[8px] font-medium text-[20px]">
|
||||
{I18n.t('Publish')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePublishProject}
|
||||
loading={publishing}
|
||||
data-testid="project.publish"
|
||||
>
|
||||
{showPublishResult
|
||||
? I18n.t('bot_publish_success_back')
|
||||
: I18n.t('Publish')}
|
||||
</Button>
|
||||
</div>
|
||||
<Banner
|
||||
type="info"
|
||||
description={I18n.t('project_release_notify')}
|
||||
onClose={() => {
|
||||
setPublishHeaderHeight(MIN_PUBLISH_HEADER_HEIGHT);
|
||||
}}
|
||||
></Banner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
type PublishConnectorInfo,
|
||||
ConnectorBindType,
|
||||
ConnectorConfigStatus,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
|
||||
// 未配置/授权场景
|
||||
export const getConnectorNotConfigured = (
|
||||
connector: PublishConnectorInfo,
|
||||
): boolean => {
|
||||
const { bind_type, config_status } = connector;
|
||||
// 未绑定&未授权
|
||||
const notConfigured =
|
||||
[
|
||||
ConnectorBindType.KvBind,
|
||||
ConnectorBindType.AuthBind,
|
||||
ConnectorBindType.KvAuthBind,
|
||||
ConnectorBindType.TemplateBind, // mcp未配置时禁用,模版始终为已配置
|
||||
].includes(bind_type) &&
|
||||
config_status === ConnectorConfigStatus.NotConfigured;
|
||||
return notConfigured;
|
||||
};
|
||||
|
||||
// 不能发布的场景:
|
||||
// 1. 未绑定&未授权
|
||||
// 2. 后端下发的不能发布(如:没有workflow不能发api,有私有插件不能发模板,审核中不能发布的渠道)
|
||||
export const getDisabledPublish = (
|
||||
connector: PublishConnectorInfo,
|
||||
): boolean => {
|
||||
const { allow_publish } = connector;
|
||||
// 未绑定&未授权
|
||||
const notConfigured = getConnectorNotConfigured(connector);
|
||||
|
||||
const connectorDisabled = notConfigured || !allow_publish;
|
||||
|
||||
// 审核中不能发布渠道的场景后端下发 allow_publish
|
||||
return connectorDisabled;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 { UIPreviewType } from '@coze-arch/idl/product_api';
|
||||
import { type UIOption } from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export enum DisplayScreen {
|
||||
Web = 'web',
|
||||
Mobile = 'mobile',
|
||||
}
|
||||
|
||||
export interface DisplayScreenOption {
|
||||
label: string;
|
||||
value: DisplayScreen;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export function toDisplayScreenOption(uiOption: UIOption): DisplayScreenOption {
|
||||
const publicProps = {
|
||||
disabled: uiOption.available === false,
|
||||
tooltip: uiOption.unavailable_reason,
|
||||
};
|
||||
if (uiOption.ui_channel === UIPreviewType.Web.toString()) {
|
||||
return {
|
||||
value: DisplayScreen.Web,
|
||||
label: I18n.t('builder_canvas_tools_pc'),
|
||||
...publicProps,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: DisplayScreen.Mobile,
|
||||
label: I18n.t('builder_canvas_tools_phone'),
|
||||
...publicProps,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 {
|
||||
type ConnectorUnionInfo,
|
||||
ConnectorClassification,
|
||||
type PublishConnectorInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export interface ConnectorGroup {
|
||||
type: ConnectorClassification;
|
||||
label: string;
|
||||
desc: string;
|
||||
connectors: PublishConnectorInfo[];
|
||||
}
|
||||
|
||||
export function formatConnectorGroups(
|
||||
connectors: PublishConnectorInfo[],
|
||||
unionMap: Record<string, ConnectorUnionInfo>,
|
||||
unions: Record<string, string>,
|
||||
) {
|
||||
const groups: ConnectorGroup[] = [
|
||||
{
|
||||
type: ConnectorClassification.APIOrSDK,
|
||||
label: I18n.t('project_release_api1'),
|
||||
desc: I18n.t('project_release_api_sdk_desc'),
|
||||
connectors: [],
|
||||
},
|
||||
{
|
||||
type: ConnectorClassification.MiniProgram,
|
||||
label: I18n.t('project_release_miniprogram1'),
|
||||
desc: I18n.t('project_release_h5_desc'),
|
||||
connectors: [],
|
||||
},
|
||||
{
|
||||
type: ConnectorClassification.SocialPlatform,
|
||||
label: I18n.t('project_release_social1'),
|
||||
desc: I18n.t('project_release_social_desc1'),
|
||||
connectors: [],
|
||||
},
|
||||
{
|
||||
type: ConnectorClassification.Coze,
|
||||
label: I18n.t('project_release_coze1'),
|
||||
desc: I18n.t('project_release_ts_desc'),
|
||||
connectors: [],
|
||||
},
|
||||
{
|
||||
type: ConnectorClassification.CozeSpaceExtensionLibrary,
|
||||
label: I18n.t('app_publish_connector_mcp'),
|
||||
desc: I18n.t('app_publish_connector_mcp'),
|
||||
connectors: [],
|
||||
},
|
||||
];
|
||||
for (const c of connectors) {
|
||||
const group = groups.find(g => g.type === c.connector_classification);
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
if (c.connector_union_id) {
|
||||
const unionId = c.connector_union_id;
|
||||
// 如果当前 union_id 已经被添加到分组中,则跳过
|
||||
if (group.connectors.some(i => i.connector_union_id === unionId)) {
|
||||
continue;
|
||||
}
|
||||
let connectorInfo = c;
|
||||
// 优先取 union 选中的 connector,否则取第一个
|
||||
const unionSelection = connectors.find(i => i.id === unions[unionId]);
|
||||
if (unionSelection) {
|
||||
connectorInfo = unionSelection;
|
||||
} else {
|
||||
const firstId = unionMap[unionId].connector_options[0].connector_id;
|
||||
const firstConnector = connectors.find(i => i.id === firstId);
|
||||
if (firstConnector) {
|
||||
connectorInfo = firstConnector;
|
||||
}
|
||||
}
|
||||
const unionInfo = unionMap[unionId];
|
||||
group.connectors.push({
|
||||
...connectorInfo,
|
||||
name: unionInfo.name,
|
||||
description: unionInfo.description,
|
||||
icon_url: unionInfo.icon_url,
|
||||
});
|
||||
} else {
|
||||
group.connectors.push(c);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 {
|
||||
ConnectorBindType,
|
||||
ConnectorConfigStatus,
|
||||
type PublishConnectorInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TagProps } from '@coze-arch/coze-design';
|
||||
|
||||
interface ConfigStatusUI {
|
||||
text: string;
|
||||
color: TagProps['color'];
|
||||
}
|
||||
|
||||
export const getConfigStatus = (
|
||||
record: PublishConnectorInfo,
|
||||
): ConfigStatusUI => {
|
||||
const { bind_type } = record;
|
||||
|
||||
if (
|
||||
bind_type === ConnectorBindType.KvBind ||
|
||||
bind_type === ConnectorBindType.KvAuthBind ||
|
||||
bind_type === ConnectorBindType.TemplateBind
|
||||
) {
|
||||
return getKvBindStatus(record);
|
||||
}
|
||||
|
||||
return getDefaultStatus(record);
|
||||
};
|
||||
|
||||
const getKvBindStatus = (record: PublishConnectorInfo): ConfigStatusUI => {
|
||||
const { config_status = ConnectorConfigStatus.Configured } = record;
|
||||
|
||||
const couldPublish = config_status === ConnectorConfigStatus.Configured;
|
||||
const color = couldPublish ? 'green' : 'primary';
|
||||
|
||||
const textMap = {
|
||||
[ConnectorConfigStatus.Configured]: I18n.t(
|
||||
'bot_publish_columns_status_configured',
|
||||
),
|
||||
[ConnectorConfigStatus.NotConfigured]: I18n.t(
|
||||
'bot_publish_columns_status_not_configured',
|
||||
),
|
||||
// 业务不会走到下面3个case
|
||||
[ConnectorConfigStatus.Configuring]: '',
|
||||
[ConnectorConfigStatus.Disconnected]: '',
|
||||
[ConnectorConfigStatus.NeedReconfiguring]: '',
|
||||
};
|
||||
|
||||
return {
|
||||
text: textMap[config_status],
|
||||
color,
|
||||
};
|
||||
};
|
||||
|
||||
const getDefaultStatus = (record: PublishConnectorInfo): ConfigStatusUI => {
|
||||
const { config_status = ConnectorConfigStatus.Configured } = record;
|
||||
const couldPublish = config_status === ConnectorConfigStatus.Configured;
|
||||
const color = couldPublish ? 'green' : 'primary';
|
||||
|
||||
const textMap = {
|
||||
[ConnectorConfigStatus.Configured]: I18n.t(
|
||||
'bot_publish_columns_status_authorized',
|
||||
),
|
||||
[ConnectorConfigStatus.NotConfigured]: I18n.t(
|
||||
'bot_publish_columns_status_unauthorized',
|
||||
),
|
||||
[ConnectorConfigStatus.Configuring]: I18n.t('publish_douyin_config_ing'),
|
||||
[ConnectorConfigStatus.Disconnected]: '',
|
||||
[ConnectorConfigStatus.NeedReconfiguring]: '',
|
||||
};
|
||||
|
||||
return {
|
||||
text: textMap[config_status],
|
||||
color,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { incrementVersionNumber } from './increment-version-number';
|
||||
|
||||
export const getFixedVersionNumber = ({
|
||||
lastPublishVersionNumber,
|
||||
draftVersionNumber,
|
||||
defaultVersionNumber,
|
||||
}: {
|
||||
lastPublishVersionNumber: string | undefined;
|
||||
draftVersionNumber: string | undefined;
|
||||
defaultVersionNumber: string;
|
||||
}): string => {
|
||||
if (lastPublishVersionNumber && !draftVersionNumber) {
|
||||
return incrementVersionNumber(lastPublishVersionNumber);
|
||||
}
|
||||
if (draftVersionNumber) {
|
||||
return draftVersionNumber;
|
||||
}
|
||||
return defaultVersionNumber;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 const incrementVersionNumber = (input: string) => {
|
||||
// 定义正则表达式,匹配 "数字.数字.数字" 的模式
|
||||
const regex = /(\d+)\.(\d+)\.(\d+)/g;
|
||||
|
||||
// 使用 replace 方法和回调函数对匹配的部分进行替换
|
||||
// eslint-disable-next-line max-params
|
||||
const result = input.replace(regex, (_match, p1, p2, p3) => {
|
||||
// 将最后一个数字加 1
|
||||
const incrementedP3 = parseInt(String(p3), 10) + 1;
|
||||
// 返回新的字符串
|
||||
return `${p1}.${p2}.${incrementedP3}`;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 {
|
||||
ConnectorClassification,
|
||||
type ConnectorPublishConfig,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { MonetizationEntityType } from '@coze-arch/idl/benefit';
|
||||
import { benefitApi, intelligenceApi } from '@coze-arch/bot-api';
|
||||
|
||||
import {
|
||||
DEFAULT_VERSION_NUMBER,
|
||||
WEB_SDK_CONNECTOR_ID,
|
||||
} from '@/utils/constants';
|
||||
import { useProjectPublishStore } from '@/store';
|
||||
|
||||
import { type ProjectPublishDraft } from './publish-draft';
|
||||
import { getFixedVersionNumber } from './get-fixed-version-number';
|
||||
import { getDisabledPublish } from './connector-disabled-publish';
|
||||
|
||||
// eslint-disable-next-line complexity -- it's complex
|
||||
export async function initPublishStore(
|
||||
projectId: string,
|
||||
errorHandle: (e: unknown) => void,
|
||||
draft?: ProjectPublishDraft,
|
||||
) {
|
||||
const { setProjectPublishInfo, setSelectedConnectorIds, setMonetizeConfig } =
|
||||
useProjectPublishStore.getState();
|
||||
setProjectPublishInfo({ pageLoading: true });
|
||||
try {
|
||||
const [publishResp, monetizeConfigResp] = await Promise.all([
|
||||
intelligenceApi.PublishConnectorList({
|
||||
project_id: projectId,
|
||||
}),
|
||||
IS_OVERSEA
|
||||
? benefitApi.PublicGetBotMonetizationConfig({
|
||||
entity_id: projectId,
|
||||
entity_type: MonetizationEntityType.Project,
|
||||
})
|
||||
: Promise.resolve(undefined),
|
||||
]);
|
||||
const {
|
||||
connector_list = [],
|
||||
last_publish_info = {},
|
||||
connector_union_info_map = {},
|
||||
} = publishResp.data ?? {};
|
||||
const { connector_ids = [], connector_publish_config = {} } =
|
||||
last_publish_info;
|
||||
|
||||
// 初始化默认选中的渠道
|
||||
const initSelectedConnectors: string[] = [];
|
||||
const initConnectors: Record<string, Record<string, string>> = {};
|
||||
for (const id of connector_ids) {
|
||||
const connector = connector_list.find(c => c.id === id);
|
||||
// 过滤掉不允许发布的渠道
|
||||
if (!connector || getDisabledPublish(connector)) {
|
||||
continue;
|
||||
}
|
||||
if (connector.connector_union_id) {
|
||||
// 对于 union 的 connector ,选中其 union id
|
||||
initSelectedConnectors.push(connector.connector_union_id);
|
||||
initConnectors[connector.id] = connector.bind_info;
|
||||
} else {
|
||||
initSelectedConnectors.push(connector.id);
|
||||
initConnectors[connector.id] = connector.bind_info;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化每个 union 选中的 connector,如果上次没发布该渠道,则选中第一个
|
||||
const initUnions: Record<string, string> = {};
|
||||
for (const [unionId, info] of Object.entries(connector_union_info_map)) {
|
||||
initUnions[unionId] =
|
||||
info.connector_options.find(o => connector_ids.includes(o.connector_id))
|
||||
?.connector_id ?? info.connector_options[0].connector_id;
|
||||
}
|
||||
|
||||
// 回填社交渠道选择的 chatflow,优先级:
|
||||
// 1. draft 中保存的 chatflow
|
||||
// 2. 上次发布的第一个 SocialPlatform 选择的 chatflow
|
||||
let lastSocialPlatformChatflow: ConnectorPublishConfig | undefined;
|
||||
if (draft?.socialPlatformConfig?.selected_workflows?.[0].workflow_id) {
|
||||
lastSocialPlatformChatflow = draft.socialPlatformConfig;
|
||||
} else {
|
||||
for (const c of connector_list) {
|
||||
if (
|
||||
!initSelectedConnectors.includes(c.id) ||
|
||||
c.connector_classification !== ConnectorClassification.SocialPlatform
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const lastConfig = connector_publish_config[c.id];
|
||||
if (lastConfig?.selected_workflows?.[0].workflow_id) {
|
||||
lastSocialPlatformChatflow = lastConfig;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 draft 中保存的信息回填 WebSDK 渠道选择的 chatflow
|
||||
if (draft?.sdkConfig?.selected_workflows?.[0].workflow_id) {
|
||||
connector_publish_config[WEB_SDK_CONNECTOR_ID] = draft.sdkConfig;
|
||||
}
|
||||
|
||||
setSelectedConnectorIds(
|
||||
draft?.selectedConnectorIds ?? initSelectedConnectors,
|
||||
);
|
||||
|
||||
const lastPublishVersionNumber = last_publish_info.version_number;
|
||||
// 用户没有 draft 并且存在发布过的版本 则将上一次发布的版本号进行处理
|
||||
const fixedVersionNumber = getFixedVersionNumber({
|
||||
lastPublishVersionNumber,
|
||||
draftVersionNumber: draft?.versionNumber,
|
||||
defaultVersionNumber: DEFAULT_VERSION_NUMBER,
|
||||
});
|
||||
|
||||
setProjectPublishInfo({
|
||||
lastVersionNumber: lastPublishVersionNumber,
|
||||
versionNumber: fixedVersionNumber,
|
||||
versionDescription: draft?.versionDescription,
|
||||
connectorPublishConfig: connector_publish_config,
|
||||
connectorList: connector_list,
|
||||
connectorUnionMap: connector_union_info_map,
|
||||
connectors: initConnectors,
|
||||
unions: draft?.unions ?? initUnions,
|
||||
socialPlatformChatflow: lastSocialPlatformChatflow,
|
||||
});
|
||||
setMonetizeConfig(monetizeConfigResp?.data);
|
||||
} catch (e) {
|
||||
errorHandle(e);
|
||||
} finally {
|
||||
setProjectPublishInfo({ pageLoading: false });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { type StoreBindKey } from '@/store';
|
||||
|
||||
type SelfMapping<T extends string> = {
|
||||
[K in T]: K; // 关键语法:将每个字面量类型映射为自己
|
||||
};
|
||||
|
||||
type KeyMapping = SelfMapping<StoreBindKey>;
|
||||
|
||||
export const isStoreBindConfigured = (
|
||||
config: Record<string, string>,
|
||||
): boolean => {
|
||||
// 防止 StoreBindKey 有变动导致 bug
|
||||
const { category_id, display_screen }: KeyMapping = {
|
||||
category_id: 'category_id',
|
||||
display_screen: 'display_screen',
|
||||
};
|
||||
return Boolean(config[category_id]) && Boolean(config[display_screen]);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { type ConnectorPublishConfig } from '@coze-arch/idl/intelligence_api';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
|
||||
export const PUBLISH_DRAFT_KEY = 'coz_project_publish_draft';
|
||||
|
||||
export interface ProjectPublishDraft {
|
||||
projectId: string;
|
||||
versionNumber: string;
|
||||
versionDescription: string;
|
||||
selectedConnectorIds: string[];
|
||||
unions: Record<string, string>;
|
||||
sdkConfig?: ConnectorPublishConfig;
|
||||
socialPlatformConfig?: ConnectorPublishConfig;
|
||||
}
|
||||
|
||||
export function loadProjectPublishDraft(projectId: string) {
|
||||
const str = localStorage.getItem(PUBLISH_DRAFT_KEY);
|
||||
localStorage.removeItem(PUBLISH_DRAFT_KEY);
|
||||
if (!str) {
|
||||
return undefined;
|
||||
}
|
||||
const draft = typeSafeJSONParse(str) as ProjectPublishDraft | undefined;
|
||||
if (draft?.projectId === projectId) {
|
||||
return draft;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function saveProjectPublishDraft(draft: ProjectPublishDraft) {
|
||||
localStorage.setItem(PUBLISH_DRAFT_KEY, JSON.stringify(draft));
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
import { intelligenceApi } from '@coze-arch/bot-api';
|
||||
|
||||
export async function checkVersionNum(
|
||||
versionNumber: string,
|
||||
projectId: string,
|
||||
) {
|
||||
if (!versionNumber) {
|
||||
return I18n.t('project_release_example2');
|
||||
}
|
||||
const { data } = await intelligenceApi.CheckProjectVersionNumber({
|
||||
project_id: projectId,
|
||||
version_number: versionNumber,
|
||||
});
|
||||
|
||||
if (data?.is_duplicate) {
|
||||
return I18n.t('project_release_example3');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
type publish,
|
||||
ConnectorPublishStatus,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import {
|
||||
CozAvatar,
|
||||
Tag,
|
||||
type TagProps,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
MINI_PROGRAM_DOUYIN_DOWNLOAD_CONNECTOR_ID,
|
||||
MINI_PROGRAM_WECHAT_DOWNLOAD_CONNECTOR_ID,
|
||||
WEB_SDK_CONNECTOR_ID,
|
||||
} from '@/utils/constants';
|
||||
|
||||
function getMiniProgramGuideUrl(connectorId?: string) {
|
||||
switch (connectorId) {
|
||||
case MINI_PROGRAM_DOUYIN_DOWNLOAD_CONNECTOR_ID:
|
||||
return '/docs/guides/publish_app_to_douyin_microapp';
|
||||
case MINI_PROGRAM_WECHAT_DOWNLOAD_CONNECTOR_ID:
|
||||
return '/docs/guides/publish_app_to_wechat_mini_program';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConnectorStatusProps {
|
||||
result: publish.ConnectorPublishResult;
|
||||
showTag?: boolean;
|
||||
onShowWebSdkGuide?: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
const ConnectorTagMap: Record<
|
||||
ConnectorPublishStatus,
|
||||
(Pick<TagProps, 'color'> & { text: I18nKeysNoOptionsType }) | null
|
||||
> = {
|
||||
[ConnectorPublishStatus.Default]: {
|
||||
color: 'brand',
|
||||
text: 'project_releasing',
|
||||
},
|
||||
[ConnectorPublishStatus.Auditing]: {
|
||||
color: 'brand',
|
||||
text: 'under_review',
|
||||
},
|
||||
[ConnectorPublishStatus.Failed]: {
|
||||
color: 'red',
|
||||
text: 'project_release_failed',
|
||||
},
|
||||
[ConnectorPublishStatus.Success]: {
|
||||
color: 'green',
|
||||
text: 'project_release_success',
|
||||
},
|
||||
[ConnectorPublishStatus.Disable]: null, // 对应状态不会返回给前端,不进行适配
|
||||
};
|
||||
|
||||
export function ConnectorStatus({
|
||||
result,
|
||||
showTag,
|
||||
onShowWebSdkGuide,
|
||||
}: ConnectorStatusProps) {
|
||||
const tagConfig = ConnectorTagMap[result.connector_publish_status ?? 0];
|
||||
// Web SDK 渠道发布成功时,展示安装指引
|
||||
const shouldShowWebSdkGuide =
|
||||
result.connector_id === WEB_SDK_CONNECTOR_ID &&
|
||||
result.connector_publish_status === ConnectorPublishStatus.Success;
|
||||
const workflowId =
|
||||
result.connector_publish_config?.selected_workflows?.[0]?.workflow_id;
|
||||
const showWebSdkGuide = () => onShowWebSdkGuide?.(workflowId ?? '');
|
||||
return (
|
||||
<div
|
||||
className="h-[40px] flex items-center border-0 border-b border-solid coz-stroke-primary last:border-none"
|
||||
key={result.connector_id}
|
||||
>
|
||||
<CozAvatar
|
||||
size="small"
|
||||
type="platform"
|
||||
className="mr-[8px] rounded-[6px] shrink-0"
|
||||
src={result.connector_icon_url}
|
||||
/>
|
||||
<Typography.Text
|
||||
weight={500}
|
||||
className="whitespace-nowrap"
|
||||
ellipsis={{ showTooltip: true }}
|
||||
>
|
||||
{result.connector_name}
|
||||
</Typography.Text>
|
||||
{showTag !== false && tagConfig ? (
|
||||
<Tag
|
||||
size="mini"
|
||||
color={tagConfig.color}
|
||||
className="!px-[4px] ml-[4px] shrink-0"
|
||||
>
|
||||
{I18n.t(tagConfig.text)}
|
||||
</Tag>
|
||||
) : null}
|
||||
<div className="px-[8px] ml-auto max-w-[206px]">
|
||||
{result.connector_publish_status_msg ? (
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
fontSize="14px"
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
type: 'tooltip',
|
||||
opts: {
|
||||
theme: 'dark',
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{result.connector_publish_status_msg}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{/* Web SDK 渠道 - 安装指引 */}
|
||||
{shouldShowWebSdkGuide ? (
|
||||
<Typography.Text fontSize="14px" link onClick={showWebSdkGuide}>
|
||||
{I18n.t('project_release_guide')}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{/* 小程序渠道 - 下载代码 & 安装指引 */}
|
||||
{result.download_link ? (
|
||||
<>
|
||||
<Typography.Text
|
||||
fontSize="14px"
|
||||
link={{ href: result.download_link, target: '_blank' }}
|
||||
>
|
||||
{I18n.t('project_release_download_code')}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="ml-[12px]"
|
||||
fontSize="14px"
|
||||
link={{
|
||||
href: getMiniProgramGuideUrl(result.connector_id),
|
||||
target: '_blank',
|
||||
}}
|
||||
>
|
||||
{I18n.t('project_release_guide')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
) : null}
|
||||
{result.share_link ? (
|
||||
<Typography.Text
|
||||
fontSize="14px"
|
||||
link={{ href: result.share_link, target: '_blank' }}
|
||||
>
|
||||
{I18n.t('project_release_open_in_store')}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
IconCozCheckMarkCircle,
|
||||
IconCozClock,
|
||||
IconCozClockFill,
|
||||
IconCozCrossCircle,
|
||||
IconCozWarningCircle,
|
||||
type OriginIconProps,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { type StepProps } from '@coze-arch/coze-design';
|
||||
|
||||
export interface PublishStepIconProps {
|
||||
status: StepProps['status'] | 'warn';
|
||||
}
|
||||
|
||||
export function PublishStepIcon({ status }: PublishStepIconProps) {
|
||||
const iconProps: Pick<OriginIconProps, 'width' | 'height'> = {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
};
|
||||
switch (status) {
|
||||
case 'wait':
|
||||
return <IconCozClock className="coz-fg-secondary" {...iconProps} />;
|
||||
case 'process':
|
||||
return <IconCozClockFill className="coz-fg-hglt" {...iconProps} />;
|
||||
case 'finish':
|
||||
return (
|
||||
<IconCozCheckMarkCircle className="coz-fg-hglt-green" {...iconProps} />
|
||||
);
|
||||
case 'warn':
|
||||
return (
|
||||
<IconCozWarningCircle className="coz-fg-hglt-yellow" {...iconProps} />
|
||||
);
|
||||
case 'error':
|
||||
return <IconCozCrossCircle className="coz-fg-hglt-red" {...iconProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 { Tag, type TagProps, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface PublishStepTitleProps {
|
||||
title: string;
|
||||
tag?: string;
|
||||
color?: TagProps['color'];
|
||||
}
|
||||
|
||||
export function PublishStepTitle({ title, tag, color }: PublishStepTitleProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-[4px]">
|
||||
<Typography.Text
|
||||
className="leading-[20px] font-normal"
|
||||
data-testid="project.publish.result"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{typeof tag === 'string' ? (
|
||||
<Tag size="mini" color={color}>
|
||||
{tag}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* 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, { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
ConnectorPublishStatus,
|
||||
type PublishRecordDetail,
|
||||
PublishRecordStatus,
|
||||
ResourceType,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozPlugin, IconCozWorkflow } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
type StepProps,
|
||||
Steps,
|
||||
TagGroup,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
import { type UITagProps } from '@coze-arch/bot-semi';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useWebSdkGuideModal } from '@/web-sdk-guide';
|
||||
import { WEB_SDK_CONNECTOR_ID } from '@/utils/constants';
|
||||
import { type ProjectPublishStore } from '@/store';
|
||||
|
||||
import { PublishStepTitle } from './components/publish-step-title';
|
||||
import { PublishStepIcon } from './components/publish-step-icon';
|
||||
import {
|
||||
ConnectorStatus,
|
||||
type ConnectorStatusProps,
|
||||
} from './components/connector-status';
|
||||
|
||||
function getDefaultStepProps(title: string): StepProps {
|
||||
return {
|
||||
icon: <PublishStepIcon status="wait" />,
|
||||
title: <PublishStepTitle title={title} />,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackStepProps(
|
||||
record: PublishRecordDetail,
|
||||
tagGroupRef: React.RefObject<HTMLDivElement>,
|
||||
maxTagCount?: number,
|
||||
): StepProps {
|
||||
const title = I18n.t('project_release_package');
|
||||
if (typeof record.publish_status !== 'number') {
|
||||
return getDefaultStepProps(title);
|
||||
}
|
||||
switch (record.publish_status) {
|
||||
case PublishRecordStatus.Packing: {
|
||||
return {
|
||||
icon: <PublishStepIcon status="process" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_in_progress')}
|
||||
color="brand"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
case PublishRecordStatus.PackFailed: {
|
||||
const tags: UITagProps[] | undefined =
|
||||
record.publish_status_detail?.pack_failed_detail?.map(item => ({
|
||||
tagKey: item.entity_id,
|
||||
className: 'pack-status-tag',
|
||||
prefixIcon:
|
||||
item.entity_type === ResourceType.Workflow ? (
|
||||
<IconCozWorkflow />
|
||||
) : (
|
||||
<IconCozPlugin />
|
||||
),
|
||||
children: item.entity_name,
|
||||
}));
|
||||
return {
|
||||
icon: <PublishStepIcon status="error" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_package_failed')}
|
||||
color="red"
|
||||
/>
|
||||
),
|
||||
description: tags ? (
|
||||
<div ref={tagGroupRef}>
|
||||
<Typography.Paragraph className="coz-fg-secondary mb-[4px]">
|
||||
{I18n.t('project_release_pack_fail_reason')}
|
||||
</Typography.Paragraph>
|
||||
<TagGroup
|
||||
tagList={tags}
|
||||
maxTagCount={maxTagCount}
|
||||
showPopover
|
||||
popoverProps={{
|
||||
position: 'top',
|
||||
style: {
|
||||
padding: 8,
|
||||
backgroundColor: 'rgb(var(--black-5))',
|
||||
borderColor: 'rgb(var(--black-5))',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
icon: <PublishStepIcon status="finish" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_finish')}
|
||||
color="green"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PackStep(props: { record: PublishRecordDetail }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [maxTagCount, setMaxTagCount] = useState<number | undefined>(undefined);
|
||||
const stepProps = toPackStepProps(props.record, ref, maxTagCount);
|
||||
|
||||
/**
|
||||
* TagGroup 仅支持设置 maxTagCount 控制展示 Tag 的数量,不支持设置展示行数。
|
||||
* 这里通过遍历所有 Tag 的 offsetTop 来判断其所在的行数,并将 maxTagCount
|
||||
* 设置为刚好能展示两行的数量。
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const tags = ref.current.getElementsByClassName(
|
||||
'pack-status-tag',
|
||||
) as HTMLCollectionOf<HTMLElement>;
|
||||
if (tags.length <= 0) {
|
||||
return;
|
||||
}
|
||||
let top = -1;
|
||||
let rowCount = 0;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tagTop = tags[i].offsetTop;
|
||||
if (top !== tagTop) {
|
||||
top = tagTop;
|
||||
rowCount++;
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- offsetTop 第三次变化,当前 Tag 处于第三行
|
||||
if (rowCount >= 3) {
|
||||
setMaxTagCount(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.record.publish_status_detail?.pack_failed_detail]);
|
||||
|
||||
return <Steps.Step {...stepProps} />;
|
||||
}
|
||||
|
||||
function toAuditStepProps(record: PublishRecordDetail): StepProps {
|
||||
const title = I18n.t('project_release_coze_audit');
|
||||
if (typeof record.publish_status !== 'number') {
|
||||
return getDefaultStepProps(title);
|
||||
}
|
||||
switch (record.publish_status) {
|
||||
case PublishRecordStatus.Packing:
|
||||
case PublishRecordStatus.PackFailed: {
|
||||
return getDefaultStepProps(title);
|
||||
}
|
||||
case PublishRecordStatus.Auditing: {
|
||||
return {
|
||||
status: 'process',
|
||||
icon: <PublishStepIcon status="process" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_in_progress')}
|
||||
color="brand"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
case PublishRecordStatus.AuditNotPass: {
|
||||
return {
|
||||
status: 'error',
|
||||
icon: <PublishStepIcon status="error" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_not_pass')}
|
||||
color="red"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
status: 'finish',
|
||||
icon: <PublishStepIcon status="finish" />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
tag={I18n.t('project_release_pass')}
|
||||
color="green"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectorsPublishStatus(
|
||||
record: ProjectPublishStore['publishRecordDetail'],
|
||||
) {
|
||||
const connectorResults = record.connector_publish_result ?? [];
|
||||
if (connectorResults.length <= 0) {
|
||||
return 'wait';
|
||||
}
|
||||
const failedCount = connectorResults.filter(
|
||||
item => item.connector_publish_status === ConnectorPublishStatus.Failed,
|
||||
).length;
|
||||
if (failedCount > 0) {
|
||||
// 所有渠道均失败,显示红色叉号;部分渠道失败,显示黄色感叹号
|
||||
return failedCount === connectorResults.length ? 'error' : 'warn';
|
||||
}
|
||||
const publishingCount = connectorResults.filter(
|
||||
item =>
|
||||
item.connector_publish_status === ConnectorPublishStatus.Default ||
|
||||
item.connector_publish_status === ConnectorPublishStatus.Auditing,
|
||||
).length;
|
||||
if (publishingCount > 0) {
|
||||
// 部分渠道在发布中,显示时钟图标
|
||||
return 'process';
|
||||
}
|
||||
return 'finish';
|
||||
}
|
||||
|
||||
function toPublishStepProps(
|
||||
record: ProjectPublishStore['publishRecordDetail'],
|
||||
onShowWebSdkGuide: ConnectorStatusProps['onShowWebSdkGuide'],
|
||||
): StepProps {
|
||||
const title = I18n.t('project_release_channel');
|
||||
if (typeof record.publish_status !== 'number') {
|
||||
return getDefaultStepProps(title);
|
||||
}
|
||||
// 未到达“渠道审核与发布”步骤,显示默认的灰色时钟图标
|
||||
if (record.publish_status < PublishRecordStatus.ConnectorPublishing) {
|
||||
return {
|
||||
...getDefaultStepProps(title),
|
||||
description: record.connector_publish_result?.map(item => (
|
||||
<ConnectorStatus result={item} showTag={false} />
|
||||
)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
icon: <PublishStepIcon status={getConnectorsPublishStatus(record)} />,
|
||||
title: (
|
||||
<PublishStepTitle
|
||||
title={title}
|
||||
{...(IS_OVERSEA &&
|
||||
// publish_monetization_result 为 nil 表示可能是接口失败,也不展示
|
||||
record.publish_monetization_result === false && {
|
||||
tag: I18n.t('monetization_publish_fail'),
|
||||
color: 'red',
|
||||
})}
|
||||
/>
|
||||
),
|
||||
description: record.connector_publish_result?.map(item => (
|
||||
<ConnectorStatus result={item} onShowWebSdkGuide={onShowWebSdkGuide} />
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
function PublishStep(props: { record: PublishRecordDetail }) {
|
||||
const { project_id = '' } = useParams<DynamicParams>();
|
||||
const { node, show: showWebSdkGuideModal } = useWebSdkGuideModal();
|
||||
const onShowWebSdkGuide = (workflowId: string) =>
|
||||
showWebSdkGuideModal({
|
||||
projectId: project_id,
|
||||
workflowId,
|
||||
version: props.record.connector_publish_result?.find(
|
||||
item => item.connector_id === WEB_SDK_CONNECTOR_ID,
|
||||
)?.connector_bind_info?.sdk_version,
|
||||
});
|
||||
const stepProps = toPublishStepProps(props.record, onShowWebSdkGuide);
|
||||
return (
|
||||
<>
|
||||
<Steps.Step {...stepProps} />
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProjectPublishProgressProps {
|
||||
record: ProjectPublishStore['publishRecordDetail'];
|
||||
}
|
||||
|
||||
export function ProjectPublishProgress({
|
||||
record,
|
||||
}: ProjectPublishProgressProps) {
|
||||
return (
|
||||
<Steps type="basic" direction="vertical" size="small">
|
||||
<PackStep record={record} />
|
||||
<Steps.Step {...toAuditStepProps(record)} />
|
||||
<PublishStep record={record} />
|
||||
</Steps>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { produce } from 'immer';
|
||||
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
|
||||
|
||||
const publishAnchorDataSchema = z.object({
|
||||
projectId: z.string(),
|
||||
connectorIdBeforeRedirect: z.string(),
|
||||
});
|
||||
|
||||
const publishAnchorSchema = z.record(publishAnchorDataSchema);
|
||||
|
||||
type PublishAnchorData = z.infer<typeof publishAnchorDataSchema>;
|
||||
|
||||
type PublishAnchorType = z.infer<typeof publishAnchorSchema>;
|
||||
|
||||
class PublishAnchorService {
|
||||
private PUBLISH_ANCHOR_KEY = 'coz_project_publish_anchor';
|
||||
anchorValues: PublishAnchorType = {};
|
||||
|
||||
private load = () => {
|
||||
try {
|
||||
const stringifyLocalData = localStorage.getItem(this.PUBLISH_ANCHOR_KEY);
|
||||
const localData = typeSafeJSONParse(stringifyLocalData);
|
||||
const validData = publishAnchorSchema.parse(localData);
|
||||
// 使用 zod 对 localData 进行类型校验
|
||||
this.anchorValues = validData;
|
||||
} catch {
|
||||
this.anchorValues = {};
|
||||
}
|
||||
};
|
||||
|
||||
private save = () => {
|
||||
localStorage.setItem(
|
||||
this.PUBLISH_ANCHOR_KEY,
|
||||
JSON.stringify(this.anchorValues),
|
||||
);
|
||||
};
|
||||
|
||||
setAnchor: (params: {
|
||||
userId: string;
|
||||
projectId: string;
|
||||
connectorId: string;
|
||||
}) => void = ({ userId, projectId, connectorId }) => {
|
||||
this.anchorValues = produce(this.anchorValues, draft => {
|
||||
draft[userId] = {
|
||||
projectId,
|
||||
connectorIdBeforeRedirect: connectorId,
|
||||
};
|
||||
});
|
||||
this.save();
|
||||
};
|
||||
getAnchor: (params: {
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}) => PublishAnchorData | undefined = ({ userId, projectId }) => {
|
||||
const userData = this.anchorValues[userId];
|
||||
if (userData?.projectId === projectId) {
|
||||
return userData;
|
||||
}
|
||||
};
|
||||
|
||||
removeAnchor: (params: { userId: string; projectId: string }) => void = ({
|
||||
userId,
|
||||
projectId,
|
||||
}) => {
|
||||
const hasAnchor = Boolean(this.getAnchor({ userId, projectId }));
|
||||
if (!hasAnchor) {
|
||||
return;
|
||||
}
|
||||
delete this.anchorValues[userId];
|
||||
this.save();
|
||||
};
|
||||
|
||||
clearAll = () => {
|
||||
localStorage.removeItem(this.PUBLISH_ANCHOR_KEY);
|
||||
this.anchorValues = {};
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
export const publishAnchorService = new PublishAnchorService();
|
||||
183
frontend/packages/studio/workspace/project-publish/src/store.ts
Normal file
183
frontend/packages/studio/workspace/project-publish/src/store.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 { devtools } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
import { produce as immerProduce } from 'immer';
|
||||
import {
|
||||
type PublishConnectorInfo,
|
||||
type ConnectorPublishConfig,
|
||||
type PublishRecordDetail,
|
||||
type PublishProjectData,
|
||||
type ConnectorUnionInfo,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { type BotMonetizationConfigData } from '@coze-arch/bot-api/benefit';
|
||||
|
||||
import { setterActionFactory, type SetterAction } from './utils/setter-factory';
|
||||
import { WEB_SDK_CONNECTOR_ID } from './utils/constants';
|
||||
import { type ProjectPublishDraft } from './publish-main/utils/publish-draft';
|
||||
|
||||
export type StoreBindKey = 'display_screen' | 'category_id';
|
||||
|
||||
export interface ProjectPublishStore {
|
||||
/** 页面加载状态 */
|
||||
pageLoading: boolean;
|
||||
/** 渠道列表 */
|
||||
connectorList: PublishConnectorInfo[];
|
||||
/** 需要聚合的渠道列表,key 是 PublishConnectorInfo['connector_union_id'] */
|
||||
connectorUnionMap: Record<string, ConnectorUnionInfo>;
|
||||
/** 渠道选择的id */
|
||||
selectedConnectorIds: string[];
|
||||
/** 是否展示发布结果 */
|
||||
showPublishResult: boolean;
|
||||
/** 上次发布的版本号 */
|
||||
lastVersionNumber: string;
|
||||
/** 版本号 */
|
||||
versionNumber: string;
|
||||
/** 版本描述 */
|
||||
versionDescription: string;
|
||||
/** 渠道选择的Workflow/ChatFlow */
|
||||
connectorPublishConfig: Record<string, ConnectorPublishConfig>;
|
||||
/** 社交平台渠道统一选择的 chatflow */
|
||||
socialPlatformChatflow: ConnectorPublishConfig;
|
||||
/** 发布配置信息,key代表connector_id,value是渠道发布的参数 */
|
||||
connectors: Record<string, Record<string, string>>;
|
||||
/** 聚合渠道的选择信息,key代表connector_union_id,value是union的选择信息 */
|
||||
unions: Record<string, string>;
|
||||
/** 是否已经配置过模板信息 */
|
||||
templateConfigured: boolean;
|
||||
/** 发布结果详情(轮询接口返回值) */
|
||||
publishRecordDetail: PublishRecordDetail &
|
||||
// 该信息由 PublishProject 接口返回
|
||||
// 但为了符合现有数据流转逻辑(PublishProject 拿到 id,用 id 轮询 GetPublishRecordDetail 拿到结果作为唯一数据源)
|
||||
// 因此前端手动将该值拼到轮询结果中
|
||||
Pick<PublishProjectData, 'publish_monetization_result'>;
|
||||
/** 付费配置 */
|
||||
monetizeConfig?: BotMonetizationConfigData;
|
||||
}
|
||||
|
||||
interface ProjectPublishAction {
|
||||
reset: () => void;
|
||||
setMonetizeConfig: (
|
||||
monetizeConfig: BotMonetizationConfigData | undefined,
|
||||
) => void;
|
||||
setConnectorList: (connectorList: PublishConnectorInfo[]) => void;
|
||||
setSelectedConnectorIds: (selectedConnectorIds: string[]) => void;
|
||||
updateSelectedConnectorIds: (produce: (prev: string[]) => string[]) => void;
|
||||
setShowPublishResult: (showPublishResult: boolean) => void;
|
||||
setProjectPublishInfo: SetterAction<ProjectPublishStore>;
|
||||
setProjectPublishInfoByImmer: (
|
||||
updateFn: (draft: ProjectPublishStore['connectorPublishConfig']) => void,
|
||||
) => void;
|
||||
setPublishRecordDetail: (
|
||||
val: Partial<ProjectPublishStore['publishRecordDetail']>,
|
||||
) => void;
|
||||
resetProjectPublishInfo: () => void;
|
||||
exportDraft: (projectId: string) => ProjectPublishDraft;
|
||||
}
|
||||
|
||||
const initialStore: ProjectPublishStore = {
|
||||
connectorList: [],
|
||||
connectorUnionMap: {},
|
||||
selectedConnectorIds: [],
|
||||
showPublishResult: false,
|
||||
lastVersionNumber: '',
|
||||
versionNumber: '',
|
||||
versionDescription: '',
|
||||
connectorPublishConfig: {},
|
||||
socialPlatformChatflow: {},
|
||||
// 默认已经配置过模板信息(不影响模板渠道默认勾选),当 template-bind 组件初始化获取到模板信息后,再按需设置为 false
|
||||
templateConfigured: true,
|
||||
connectors: {},
|
||||
unions: {},
|
||||
publishRecordDetail: {},
|
||||
pageLoading: false,
|
||||
};
|
||||
|
||||
export const useProjectPublishStore = create<
|
||||
ProjectPublishStore & ProjectPublishAction
|
||||
>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialStore,
|
||||
|
||||
reset: () => {
|
||||
set(initialStore);
|
||||
},
|
||||
setMonetizeConfig: monetizeConfig => set({ monetizeConfig }),
|
||||
setConnectorList: connectorList => {
|
||||
set({ connectorList });
|
||||
},
|
||||
setSelectedConnectorIds: selectedConnectorIds => {
|
||||
set({ selectedConnectorIds });
|
||||
},
|
||||
updateSelectedConnectorIds: produce => {
|
||||
set(prev => ({
|
||||
selectedConnectorIds: produce(prev.selectedConnectorIds),
|
||||
}));
|
||||
},
|
||||
setShowPublishResult: showPublishResult => {
|
||||
set({ showPublishResult });
|
||||
},
|
||||
setProjectPublishInfo: setterActionFactory<ProjectPublishStore>(set),
|
||||
setProjectPublishInfoByImmer: updateFn => {
|
||||
set(
|
||||
{
|
||||
connectorPublishConfig: immerProduce(
|
||||
get().connectorPublishConfig,
|
||||
updateFn,
|
||||
),
|
||||
},
|
||||
false,
|
||||
'setProjectPublishInfoByImmer',
|
||||
);
|
||||
},
|
||||
setPublishRecordDetail: publishRecordDetail =>
|
||||
set(prev => ({
|
||||
publishRecordDetail: {
|
||||
...prev.publishRecordDetail,
|
||||
...publishRecordDetail,
|
||||
},
|
||||
})),
|
||||
resetProjectPublishInfo: () => {
|
||||
set(initialStore);
|
||||
},
|
||||
exportDraft: (projectId: string) => {
|
||||
const {
|
||||
versionNumber,
|
||||
versionDescription,
|
||||
selectedConnectorIds,
|
||||
connectorPublishConfig,
|
||||
unions,
|
||||
socialPlatformChatflow,
|
||||
} = get();
|
||||
return {
|
||||
projectId,
|
||||
versionNumber,
|
||||
versionDescription,
|
||||
selectedConnectorIds,
|
||||
unions,
|
||||
sdkConfig: connectorPublishConfig[WEB_SDK_CONNECTOR_ID],
|
||||
socialPlatformConfig: socialPlatformChatflow,
|
||||
};
|
||||
},
|
||||
}),
|
||||
{
|
||||
enabled: IS_DEV_MODE,
|
||||
name: 'botStudio.projectPublishStore',
|
||||
},
|
||||
),
|
||||
);
|
||||
17
frontend/packages/studio/workspace/project-publish/src/typings.d.ts
vendored
Normal file
17
frontend/packages/studio/workspace/project-publish/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** 发布为 Web SDK 的渠道 ID */
|
||||
export const WEB_SDK_CONNECTOR_ID = '999';
|
||||
/** 发布到扣子 - 模板渠道的 ID */
|
||||
export const TEMPLATE_CONNECTOR_ID = '10000129';
|
||||
/** 发布为小程序 - 抖音小程序(自行下载)的渠道 ID */
|
||||
export const MINI_PROGRAM_DOUYIN_DOWNLOAD_CONNECTOR_ID = '10000130';
|
||||
/** 发布为小程序 - 微信小程序(自行下载)的渠道 ID */
|
||||
export const MINI_PROGRAM_WECHAT_DOWNLOAD_CONNECTOR_ID = '10000131';
|
||||
/** banner 存在时发布渠道 header 高度 */
|
||||
export const DEFAULT_PUBLISH_HEADER_HEIGHT = 97;
|
||||
/** banner 关闭时发布渠道 header 高度 */
|
||||
export const MIN_PUBLISH_HEADER_HEIGHT = 65;
|
||||
/** 渠道 tab 的 z-index */
|
||||
export const CONNECTOR_TAB_BAR_Z_INDEX = 1;
|
||||
/** 用户新发布时默认填写的 version_number */
|
||||
export const DEFAULT_VERSION_NUMBER = 'v0.0.1';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 {
|
||||
type PublishRecordDetail,
|
||||
PublishRecordStatus,
|
||||
ConnectorPublishStatus,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
|
||||
/**
|
||||
* 判断发布过程是否已经结束,可以停止轮询
|
||||
*/
|
||||
export function isPublishFinish(record: PublishRecordDetail) {
|
||||
// project 打包失败/审核不通过
|
||||
const projectFinish =
|
||||
record.publish_status === PublishRecordStatus.PackFailed ||
|
||||
record.publish_status === PublishRecordStatus.AuditNotPass;
|
||||
// 所有渠道均处于 审核中/失败/成功 状态
|
||||
const connectorsFinish =
|
||||
record.connector_publish_result?.every(
|
||||
item =>
|
||||
item.connector_publish_status === ConnectorPublishStatus.Auditing ||
|
||||
item.connector_publish_status === ConnectorPublishStatus.Failed ||
|
||||
item.connector_publish_status === ConnectorPublishStatus.Success,
|
||||
) ?? false;
|
||||
return projectFinish || connectorsFinish;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 { type create } from 'zustand';
|
||||
|
||||
export interface SetterAction<T> {
|
||||
/**
|
||||
* 增量更新
|
||||
*
|
||||
* @example
|
||||
* // store.x: { a: 1, b: 2 }
|
||||
* setX({a: 2});
|
||||
* // store.x: { a: 2, b: 2 }
|
||||
*/
|
||||
(state: Partial<T>): void;
|
||||
/**
|
||||
* 全量更新
|
||||
*
|
||||
* @example
|
||||
* // store.x: { a: 1, b: 2 }
|
||||
* setX({a: 2}, { replace: true });
|
||||
* // store.x: { a: 2 }
|
||||
*/
|
||||
(state: T, config: { replace: true }): void;
|
||||
}
|
||||
|
||||
export function setterActionFactory<T>(
|
||||
set: Parameters<Parameters<typeof create<T, []>>[0]>[0],
|
||||
): SetterAction<T> {
|
||||
return (state: Partial<T>, config?: { replace: true }) => {
|
||||
if (config?.replace) {
|
||||
set(state);
|
||||
} else {
|
||||
set(prevState => ({ ...prevState, ...state }));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { AxiosError } from 'axios';
|
||||
import { type UploadProps } from '@coze-arch/coze-design';
|
||||
import { upLoadFile } from '@coze-arch/bot-utils';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
|
||||
function fileToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
if (typeof result === 'string') {
|
||||
resolve(result.slice(result.indexOf(',') + 1));
|
||||
} else {
|
||||
reject(new Error('readAsDataURL failed'));
|
||||
}
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export const uploadCustomRequest: UploadProps['customRequest'] = async args => {
|
||||
const { fileInstance, onProgress, onSuccess, onError } = args;
|
||||
try {
|
||||
if (!fileInstance) {
|
||||
throw new Error('no file to upload');
|
||||
}
|
||||
const result = await ProductApi.PublicUploadImage(
|
||||
{ data: await fileToBase64(fileInstance) },
|
||||
{
|
||||
onUploadProgress: e =>
|
||||
onProgress({ total: e.total ?? fileInstance.size, loaded: e.loaded }),
|
||||
},
|
||||
);
|
||||
onSuccess(result.data);
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
onError(e.request);
|
||||
} else {
|
||||
onError({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadCustomRequestImageX: UploadProps['customRequest'] =
|
||||
async args => {
|
||||
const { fileInstance, onProgress, onSuccess, onError } = args;
|
||||
try {
|
||||
if (!fileInstance) {
|
||||
throw new Error('no file to upload');
|
||||
}
|
||||
const uri = await upLoadFile({
|
||||
biz: 'store',
|
||||
file: fileInstance,
|
||||
fileType: 'image',
|
||||
getUploadAuthToken: async () => {
|
||||
const { data } = await ProductApi.PublicGetImageUploadToken();
|
||||
return {
|
||||
data: {
|
||||
service_id: data?.service_id || '',
|
||||
upload_host: data?.upload_host || '',
|
||||
auth: {
|
||||
current_time: data?.current_time || '',
|
||||
expired_time: data?.expired_time || '',
|
||||
session_token: data?.session_token || '',
|
||||
access_key_id: data?.access_key_id || '',
|
||||
secret_access_key: data?.secret_access_key || '',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getProgress: progress => {
|
||||
onProgress({ total: fileInstance.size, loaded: progress });
|
||||
},
|
||||
});
|
||||
const res = await ProductApi.PublicGetImageURL({ uri });
|
||||
onSuccess({ uri, url: res.data?.url });
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
onError(e.request);
|
||||
} else {
|
||||
onError({});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.web-sdk-guide {
|
||||
:global {
|
||||
ol li::marker {
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-secondary);
|
||||
}
|
||||
|
||||
[class^="code-block-element"] {
|
||||
height: 160px;
|
||||
|
||||
[class^="content"] {
|
||||
overflow: auto;
|
||||
flex-shrink: 1;
|
||||
|
||||
pre {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
import { getOpenSDKUrl } from '@coze-studio/open-env-adapter';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
|
||||
import { Button, Modal, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface WebSdkGuideParams {
|
||||
projectId: string;
|
||||
workflowId: string;
|
||||
token?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
function getWebSdkScriptTagMD({
|
||||
projectId,
|
||||
workflowId,
|
||||
version = '<version>',
|
||||
}: WebSdkGuideParams) {
|
||||
return `${'```'}html
|
||||
<script type="text/javascript">
|
||||
var webSdkScript = document.createElement('script');
|
||||
webSdkScript.src = '${getOpenSDKUrl(version)}';
|
||||
document.head.appendChild(webSdkScript);
|
||||
webSdkScript.onload = function () {
|
||||
new CozeWebSDK.WebChatClient({
|
||||
"config": {
|
||||
"type": "app",
|
||||
"appInfo": {
|
||||
"appId": "${projectId}",
|
||||
"workflowId": "${workflowId}"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "token",
|
||||
"token": "pat_********",
|
||||
onRefreshToken: function () {
|
||||
return "pat_********"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
${'```'}`;
|
||||
}
|
||||
|
||||
function ListIndex({ index }: { index: number }) {
|
||||
return (
|
||||
<div className="w-[20px] h-[20px] rounded-full inline-flex items-center justify-center mr-[4px] coz-mg-primary">
|
||||
<span className="font-medium coz-fg-secondary">{index}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWebSdkGuideModal() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [scriptTagMd, setScriptTagMd] = useState('');
|
||||
|
||||
const show = (params: WebSdkGuideParams) => {
|
||||
const md = getWebSdkScriptTagMD(params);
|
||||
setScriptTagMd(md);
|
||||
setVisible(true);
|
||||
};
|
||||
const close = () => setVisible(false);
|
||||
|
||||
const node = (
|
||||
<Modal
|
||||
title={I18n.t('app_publish_sdk_title')}
|
||||
closable
|
||||
visible={visible}
|
||||
width={640}
|
||||
onCancel={close}
|
||||
footer={
|
||||
<Button onClick={close}>{I18n.t('app_publish_sdk_confirm')}</Button>
|
||||
}
|
||||
// z-index 需要大于 publish-status 的 Popover
|
||||
zIndex={2000}
|
||||
>
|
||||
<Typography.Paragraph className="font-medium mb-[8px]">
|
||||
<ListIndex index={1} />
|
||||
{I18n.t('app_publish_sdk_step_1', {
|
||||
doc_link: (
|
||||
<Typography.Text
|
||||
link={{
|
||||
href: '/docs/developer_guides/oauth_apps',
|
||||
target: '_blank',
|
||||
}}
|
||||
>
|
||||
{I18n.t('app_publish_sdk_step_1_doc')}
|
||||
</Typography.Text>
|
||||
),
|
||||
})}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="font-medium mb-[8px]">
|
||||
<ListIndex index={2} />
|
||||
{I18n.t('app_publish_sdk_step_2')}
|
||||
</Typography.Paragraph>
|
||||
<MdBoxLazy className={s['web-sdk-guide']} markDown={scriptTagMd} />
|
||||
<Typography.Paragraph className="font-medium mb-[8px]">
|
||||
<ListIndex index={3} />
|
||||
{I18n.t('app_publish_sdk_step_3')}
|
||||
</Typography.Paragraph>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return { node, show };
|
||||
}
|
||||
Reference in New Issue
Block a user