feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
/*
* 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 { useEffect, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { size } from 'lodash-es';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import {
messageReportEvent,
skillKeyToApiStatusKeyTransformer,
} from '@coze-arch/bot-utils';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { type TRouteConfigGlobal, useRouteConfig } from '@coze-arch/bot-hooks';
import {
type TabDisplayItems,
TabStatus,
} from '@coze-arch/bot-api/developer_api';
import { type SkillKeyEnum } from '@coze-agent-ide/tool-config';
/**hook */
export {
useAgentPersistence,
useAgentFormManagement,
AgentInfoForm,
type AgentInfoFormProps,
type UseAgentFormManagementProps,
type UseAgentPersistenceProps,
type AgentInfoFormValue,
} from './use-create-bot';
export { useSubscribeOnboardingAndUpdateChatArea } from './use-subscribe-onboarding-and-update-chat-area';
export { useWorkflowPublishedModel } from './tools-publish-back-modal';
export { useEditConfirm } from './use-edit-confirm';
export { useInit, AgentInitCallback, AgentInitProps } from './use-init';
export { useCurrentNodeId } from './use-node-id';
export { useDataSetArea, Setting } from '../component/data-set/data-set-area';
export { useMonetizeConfigReadonly } from './use-monetize-config-readonly';
export { useDatasetAutoChangeConfirm } from './use-dataset-auto-change-confirm';
export { useUnbindPlatformModal } from './use-unbind-platform';
export { useSpaceRole } from './use-space-role';
export {
useGenerateLink,
useGetUserQueryCollectOption,
} from './use-query-collect';
export type TBotRouteConfig = (
| {
requireBotEditorInit: true;
pageName: 'bot' | 'analysis' | 'evaluation';
hasHeader?: true;
}
| {
requireBotEditorInit: false;
pageName: 'publish';
hasHeader?: true;
}
| {
requireBotEditorInit: true;
pageName: 'publish-detail' | 'publish-gray' | 'publish-ppe';
hasHeader?: false;
}
) &
TRouteConfigGlobal;
export const useBotRouteConfig = useRouteConfig<TBotRouteConfig>;
export const useMessageReportEvent = () => {
const params = useParams<DynamicParams>();
useEffect(() => {
if (params.bot_id) {
messageReportEvent.start(params.bot_id);
}
return () => {
messageReportEvent.interrupt();
};
}, [params.bot_id]);
};
/**
* 用于校验当前模块默认展开收起状态
*
* @deprecated 废弃,请使用@coze-agent-ide/tool中的useToolContentBlockDefaultExpand
* @param blockKey 主键
* @param configured 是否有配置内容
* @param when 是否校验
*
* @see
*/
export const useDefaultExPandCheck = (
$params: {
blockKey: SkillKeyEnum;
configured: boolean;
},
$when = true,
) => {
const { blockKey, configured = false } = $params;
const isReadonly = useBotDetailIsReadonly();
const { init, editable, botSkillBlockCollapsibleState } = usePageRuntimeStore(
useShallow(store => ({
init: store.init,
editable: store.editable,
botSkillBlockCollapsibleState: store.botSkillBlockCollapsibleState,
})),
);
return useMemo(() => {
// 不做校验
if (!$when) {
return undefined;
// 状态机未就绪
} else if (!init || size(botSkillBlockCollapsibleState) === 0) {
return undefined;
/**
* @description 仅在满足以下条件时用户行为记录才能生效
*
* 1. 拥有编辑权限
* 2. 不能是历史预览环境
* 3. 必须已配置
*/
} else if (editable && !isReadonly && configured) {
const transformerBlockKey = skillKeyToApiStatusKeyTransformer(blockKey);
const collapsibleState =
botSkillBlockCollapsibleState[
transformerBlockKey as keyof TabDisplayItems
];
if (collapsibleState === TabStatus.Open) {
return true;
} else if (collapsibleState === TabStatus.Close) {
return false;
}
}
return configured;
}, [
$when,
blockKey,
configured,
init,
isReadonly,
editable,
botSkillBlockCollapsibleState,
]);
};

View File

@@ -0,0 +1,101 @@
/*
* 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 copy from 'copy-to-clipboard';
import { useRequest } from 'ahooks';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { connectorApi } from '@coze-arch/bot-api';
import { useCurrentEnterpriseInfo } from '@coze-foundation/enterprise-store-adapter';
export interface IActionTarget {
target: 'oauth' | 'platform';
action: 'create' | 'update' | 'delete' | 'view';
payload?: Record<string, unknown>;
}
const useCustomPlatformController = () => {
const { organization_id: organizationId } = useCurrentEnterpriseInfo() ?? {};
const {
data: dataSource,
loading,
run: doRefreshDatasource,
} = useRequest(
async () => {
try {
const res = await connectorApi.ListConnector({
account_id: organizationId,
page_size: 100,
});
return res?.data;
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.GetCustomPlatList,
error,
meta: { error },
});
return [];
}
},
{
manual: !0,
refreshOnWindowFocus: !0,
},
);
const [actionTarget, setActionTarget] = useState<IActionTarget | undefined>(
undefined,
);
const doCopy = (id: string) => {
try {
const res = copy(id);
if (!res) {
throw new Error(I18n.t('copy_failed'));
}
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
});
} catch (error) {
Toast.warning({
content: error.message,
showClose: false,
});
}
};
return {
loading,
dataSource,
actionTarget,
doCopy,
doSetActionTarget: setActionTarget,
doRefreshDatasource,
};
};
export { useCustomPlatformController };

View File

@@ -0,0 +1,256 @@
/*
* 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 { useEffect, useMemo } from 'react';
import copy from 'copy-to-clipboard';
import { useMemoizedFn, useRequest } from 'ahooks';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { reporter } from '@coze-arch/logger';
import { AppType } from '@coze-arch/idl/pat_permission_api';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import {
type UpdateConnectorRequest,
type CreateConnectorRequest,
type DeleteConnectorRequest,
} from '@coze-arch/bot-api/connector_api';
import {
PlaygroundApi,
connectorApi,
patPermissionApi,
} from '@coze-arch/bot-api';
import {
useIsCurrentPersonalEnterprise,
useCurrentEnterpriseInfo,
} from '@coze-foundation/enterprise-store-adapter';
// eslint-disable-next-line max-lines-per-function
const useCustomPlatformSettingModalController = (
successCb: (
token?: string,
params?:
| CreateConnectorRequest
| UpdateConnectorRequest
| DeleteConnectorRequest,
) => void,
failCb?: () => void,
) => {
const { organization_id: organizationId } = useCurrentEnterpriseInfo() ?? {};
const isPersonalEnterprise = useIsCurrentPersonalEnterprise();
const {
loading: isLoadingSpace,
run: doAsyncGetSpaceList,
data: spaceDatasource,
} = useRequest(
async () => {
try {
const res = await PlaygroundApi.GetSpaceListV2();
return res?.data?.bot_space_list;
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.GetSpaceListFromCustomPlat,
error,
meta: { error },
});
return [];
}
},
{
manual: true,
},
);
const spaceOptionList = useMemo(
() =>
spaceDatasource?.map(spaceItem => ({
label: spaceItem.name,
value: spaceItem.id,
disabled:
spaceItem?.role_type !== SpaceRoleType.Owner &&
spaceItem?.role_type !== SpaceRoleType.Admin,
})),
[spaceDatasource],
);
const {
data: oauthDatasource,
loading: isLoadingOauthDatasource,
run: doAsyncGetOauthData,
} = useRequest(
async () => {
try {
const res = await patPermissionApi.ListAppMeta(
isPersonalEnterprise
? {}
: {
organization_id: organizationId,
},
);
return res?.data?.apps ?? [];
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.GetOauthAppListFromCustomPlat,
error,
meta: { error },
});
return [];
}
},
{
refreshDeps: [isPersonalEnterprise, organizationId],
},
);
const oauthOptionsList = useMemo(
() =>
oauthDatasource?.map(oauthItem => ({
label: `${oauthItem.name}(ID: ${oauthItem.appid})`,
value: oauthItem.appid,
disabled:
!!oauthItem?.connector?.connector_id ||
oauthItem?.app_type === AppType.normal,
})),
[oauthDatasource],
);
const { loading: isCreating, runAsync: doAsyncCreate } = useRequest(
async (values: CreateConnectorRequest) =>
await connectorApi.CreateConnector({
...values,
account_id: organizationId,
}),
{ manual: !0 },
);
const { loading: isUpdating, runAsync: doAsyncUpdate } = useRequest(
async (values: UpdateConnectorRequest) =>
await connectorApi.UpdateConnector(values),
{ manual: !0 },
);
const { loading: isDeleting, runAsync: doAsyncDelete } = useRequest(
async (values: DeleteConnectorRequest) => {
await connectorApi.DeleteConnector(values);
},
{ manual: !0 },
);
const doCreate = useMemoizedFn(async (values: CreateConnectorRequest) => {
try {
const res = await doAsyncCreate(values);
successCb(res?.callback_token, values);
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.CreateCustomPlat,
error,
meta: { error },
});
failCb?.();
}
});
const doUpdate = useMemoizedFn(async (values: UpdateConnectorRequest) => {
try {
const res = await doAsyncUpdate(values);
successCb(res?.callback_token, values);
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.UpdateCustomPlat,
error,
meta: { error },
});
failCb?.();
}
});
const doDel = useMemoizedFn(async (values: DeleteConnectorRequest) => {
try {
await doAsyncDelete(values);
successCb(undefined, values);
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.DeleteCustomPlat,
error,
meta: { error },
});
failCb?.();
}
});
const doCopy = (id: string) => {
try {
const res = copy(id);
if (!res) {
throw new Error(I18n.t('copy_failed'));
}
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
});
} catch (error) {
Toast.warning({
content: error.message,
showClose: false,
});
}
};
useEffect(() => {
doAsyncGetSpaceList();
}, []);
return {
oauthOptionsList,
spaceOptionList,
isLoadingSpace,
isLoadingOauthDatasource,
isIdle: !isCreating && !isDeleting && !isUpdating,
doDel,
doUpdate,
doCreate,
doAsyncGetOauthData,
doAsyncGetSpaceList,
doCopy,
};
};
export { useCustomPlatformSettingModalController };

View File

@@ -0,0 +1,45 @@
/*
* 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 { userStoreService } from '@coze-studio/user-store';
import {
createReportEvent,
REPORT_EVENTS as ReportEventNames,
} from '@coze-arch/report-events';
const useNormalPlatformController = () => {
const userAuthInfos = userStoreService.useUserAuthInfo();
const { getUserAuthInfos } = userStoreService;
const revokeSuccess = async () => {
const getUserAuthListEvent = createReportEvent({
eventName: ReportEventNames.getUserAuthList,
});
try {
await getUserAuthInfos();
getUserAuthListEvent.success();
} catch (error) {
getUserAuthListEvent.error({ error, reason: error.message });
}
};
return {
revokeSuccess,
userAuthInfos,
};
};
export { useNormalPlatformController };

View File

@@ -0,0 +1,98 @@
/*
* 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 { useMemoizedFn, useRequest } from 'ahooks';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { type UpdateOauthConfigRequest } from '@coze-arch/bot-api/connector_api';
import { connectorApi } from '@coze-arch/bot-api';
import { type IActionTarget } from './use-custom-platform-controller';
const useOauthSettingModalController = (
actionTarget: IActionTarget,
successCb: () => void,
failCb?: () => void,
) => {
const { data: oauthFormConfig, loading: isOauthConfigLoading } = useRequest(
async () => {
try {
const configRes = await connectorApi.GetOauthConfigSchema();
return configRes?.oauth_schema;
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.GetOauthConfig,
error,
meta: { error },
});
}
},
{
ready:
actionTarget?.target === 'oauth' && actionTarget?.action === 'update',
},
);
const oauthFormItemConfigs = oauthFormConfig?.schema_area?.schema_list ?? [];
const oauthModalDesc = oauthFormConfig?.schema_area?.description
? oauthFormConfig?.schema_area?.description
: I18n.t('coze_custom_publish_platform_43');
const oauthModalTitle = oauthFormConfig?.schema_area?.title_text
? oauthFormConfig?.schema_area?.title_text
: I18n.t('coze_custom_publish_platform_42');
const { runAsync, loading: isUpdateOauthConfigLoading } = useRequest(
async (values: UpdateOauthConfigRequest) => {
await connectorApi.UpdateOauthConfig(values);
},
{ manual: !0 },
);
const doUpdate = useMemoizedFn(async (values: UpdateOauthConfigRequest) => {
try {
await runAsync(values);
successCb();
} catch (error) {
console.error(error);
reporter.errorEvent({
eventName: REPORT_EVENTS.UpdateCustomPlatOauthConfig,
error,
meta: { error },
});
failCb?.();
}
});
return {
isOauthConfigLoading,
isUpdateOauthConfigLoading,
oauthModalTitle,
oauthModalDesc,
oauthFormItemConfigs,
doUpdate,
};
};
export { useOauthSettingModalController };

View File

@@ -0,0 +1,188 @@
/*
* 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 { useCallback, useEffect } from 'react';
import { debounce } from 'lodash-es';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { type WorkFlowItemType } from '@coze-studio/bot-detail-store';
import { reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { OpenBlockEvent, emitEvent } from '@coze-arch/bot-utils';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { UIModal, UIToast } from '@coze-arch/bot-semi';
import { type SceneResponseType } from '@coze-arch/bot-hooks/src/page-jump';
import {
usePageJumpResponse,
PageType,
SceneType,
OpenModeType,
} from '@coze-arch/bot-hooks';
import { CustomError } from '@coze-arch/bot-error';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { PluginType } from '@coze-arch/bot-api/developer_api';
import { PluginDevelopApi } from '@coze-arch/bot-api';
/**
* workflow 发布成功后跳转回 bot 编辑页,弹窗提示是否添加到 bot
*/
export const useWorkflowPublishedModel = ({
flowMode,
addedWorkflows,
onOk,
skipByExternal,
title = I18n.t('PublishSuccessConfirm'),
pageType = PageType.BOT,
}: {
flowMode?: WorkflowMode;
/** 已添加的 workflow若已添加该 workflow 则不弹窗)。为了兼容 single 和 multi 模式,所以由外部传入 */
addedWorkflows: WorkFlowItemType[];
/** 点击确认并查询 workflow 对应的 plugin 成功后的回调,也是为了兼容 single 和 multi 两种模式才由外部传入 */
onOk: (workflow: WorkFlowItemType) => unknown;
/** 允许业务方额外附带禁止弹窗的条件。主要用于 multi 模式 */
skipByExternal?: (
jumpResponse: SceneResponseType<
| SceneType.WORKFLOW_PUBLISHED__BACK__BOT
| SceneType.WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT
| SceneType.WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE
>,
) => boolean;
title?: string;
pageType?: PageType;
}): void => {
const isImageflow = flowMode === WorkflowMode.Imageflow;
const jumpResponse = usePageJumpResponse(pageType);
// 使用 useCallback 缓存防抖函数,避免 UIModal 弹窗出现多次
const debouncedEffect = useCallback(
debounce(() => {
const isNotWorkflowPublishedBackBot =
jumpResponse?.scene !== SceneType.WORKFLOW_PUBLISHED__BACK__BOT;
const isNotWorkflowPublishedBackSocialScene =
jumpResponse?.scene !==
SceneType.WORKFLOW_PUBLISHED__BACK__SOCIAL_SCENE;
const isOnlyOnceAdd =
(
jumpResponse as SceneResponseType<SceneType.WORKFLOW_PUBLISHED__BACK__BOT>
)?.workflowOpenMode === OpenModeType.OnlyOnceAdd;
const isNotWorkflowPublishedBackDouyinBot =
jumpResponse?.scene !== SceneType.WORKFLOW_PUBLISHED__BACK__DOUYIN_BOT;
if (
(isNotWorkflowPublishedBackBot &&
isNotWorkflowPublishedBackDouyinBot &&
isNotWorkflowPublishedBackSocialScene) ||
isOnlyOnceAdd
) {
// 不是发布跳转的场景,或者是仅添加一次,直接不弹窗
return;
}
// 配置了 flowMode 才判断,否则不进行 flowMode 限制(适配 ChatFlow
if (
typeof flowMode !== 'undefined' &&
(jumpResponse?.flowMode || WorkflowMode.Workflow) !== flowMode
) {
return;
}
if (skipByExternal?.(jumpResponse)) {
return;
}
if (
addedWorkflows.some(
workflow => workflow.workflow_id === jumpResponse.workflowID,
)
) {
// 已经添加过该 workflow 了,不弹窗
return;
}
const { workflowID, pluginID } = jumpResponse;
UIModal.success({
title,
cancelText: I18n.t('Cancel'),
okText: I18n.t('Confirm'),
onCancel: () => jumpResponse.clearScene(true),
onOk: async () => {
try {
const plugin = (
await PluginDevelopApi.GetPlaygroundPluginList({
space_id: useSpaceStore.getState().getSpaceId(),
page: 1,
size: 1,
plugin_ids: [pluginID],
plugin_types: [
isImageflow ? PluginType.IMAGEFLOW : PluginType.WORKFLOW,
],
})
).data?.plugin_list?.[0];
if (!plugin) {
const msg = I18n.t('AddFailedToast');
UIToast.error({
content: withSlardarIdButton(msg),
});
throw new CustomError('normal_error', msg);
}
const workflow: WorkFlowItemType = {
workflow_id: workflowID,
plugin_id: plugin.id || '',
name: plugin.name || '',
desc: plugin.desc_for_human || '',
parameters: plugin.plugin_apis?.at(0)?.parameters ?? [],
plugin_icon: plugin.plugin_icon || '',
flow_mode:
plugin.plugin_type === PluginType.IMAGEFLOW
? WorkflowMode.Imageflow
: (jumpResponse?.flowMode ?? WorkflowMode.Workflow),
};
const onOkResult = onOk(workflow);
const res = await Promise.resolve(onOkResult);
if (res !== false) {
UIToast.success(
I18n.t('AddSuccessToast', { name: plugin.name || workflowID }),
);
emitEvent(
isImageflow
? OpenBlockEvent.IMAGEFLOW_BLOCK_OPEN
: OpenBlockEvent.WORKFLOW_BLOCK_OPEN,
);
}
} catch (e) {
reporter.error({
message: e instanceof Error ? e.message : e?.toString(),
error: e,
});
} finally {
jumpResponse.clearScene(true);
}
},
});
}, 1000),
[], // 空依赖数组
);
useEffect(() => {
debouncedEffect();
// 清理函数
return () => {
debouncedEffect.cancel();
};
}, [debouncedEffect]);
};

View File

@@ -0,0 +1,128 @@
/*
* 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 { useRequest } from 'ahooks';
import {
createReportEvent,
REPORT_EVENTS as ReportEventNames,
} from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { Spin, useUIModal } from '@coze-arch/bot-semi';
import { useFlags } from '@coze-arch/bot-flags';
import { Branch } from '@coze-arch/bot-api/dp_manage_api';
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
import { dpManageApi } from '@coze-arch/bot-api';
import { useParams } from 'react-router-dom';
import styles from '../pages/publish/index.module.less';
import { NewBotDiffView } from '../component/bot-diff-view/new-diff-view';
import { BotDiffView } from '../component/bot-diff-view';
const getBotPublishDiffReportEvent = createReportEvent({
eventName: ReportEventNames.getBotDiffError,
});
export const useConnectorDiffModal = () => {
const [selectedRecord, setSelectedRecord] = useState<PublishConnectorInfo>();
const params = useParams<DynamicParams>();
const [Flags] = useFlags();
const isUseNewTemplate = !!Flags?.['bot.devops.merge_prompt_diff'];
const {
data: botDiffData,
loading,
run,
mutate,
error: requestError,
} = useRequest(
async (record: PublishConnectorInfo) => {
const { bot_id = '', space_id = '', commit_version } = params;
const result = await dpManageApi.BotDiff({
space_id,
bot_id,
left: {
branch: Branch.Publish,
connector_id: record.id,
},
template_key: isUseNewTemplate ? 'diff_template_when_publish_v2' : '',
right: { branch: Branch.Base, version_id: commit_version },
});
return result.data;
},
{
manual: true,
onBefore: () => {
getBotPublishDiffReportEvent.start();
},
onSuccess: data => {
getBotPublishDiffReportEvent.success();
},
onError: error => {
getBotPublishDiffReportEvent.error({
reason: 'get publish diff error',
error,
});
},
},
);
const closeModal = () => {
mutate({ origin_bot_dl: '', diff_display_node: [] });
close();
};
const openModal = (record: PublishConnectorInfo) => {
setSelectedRecord(record);
run(record);
open();
};
const { modal, open, close } = useUIModal({
type: 'info',
okText: I18n.t('devops_publish_multibranch_done'),
okType: 'tertiary',
okButtonProps: { className: 'semi-button-light' },
hasCancel: false,
className: styles['diff-modal'],
onCancel: closeModal,
onOk: closeModal,
title: I18n.t('devops_publish_multibranch_diffwithin', {
connectorName: selectedRecord?.name ?? '',
}),
});
return {
node: modal(
<div className={styles['diff-modal-container']}>
{loading ? (
<Spin />
) : isUseNewTemplate ? (
<NewBotDiffView
diffData={botDiffData?.diff_display_node || []}
hasError={requestError !== undefined}
type={'publish'}
/>
) : (
<BotDiffView
diffData={botDiffData?.diff_display_node || []}
hasError={requestError !== undefined}
/>
)}
</div>,
),
open: openModal,
close,
};
};

View File

@@ -0,0 +1,215 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type ComponentProps, Suspense, forwardRef, lazy } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import {
type BotSpace,
SpaceType,
type DraftBot,
} from '@coze-arch/bot-api/developer_api';
import { type UploadValue } from '@coze-common/biz-components/picture-upload';
import { IconTeamDefault } from '@coze-arch/bot-icons';
import { botInputLengthService } from '@coze-agent-ide/bot-input-length-limit';
import {
FormTextArea,
FormInput,
Tag,
Form,
FormSelect,
Avatar,
Typography,
} from '@coze-arch/coze-design';
import { FormSwitch } from './form-switch';
import s from './index.module.less';
const { Text } = Typography;
const LazyReactMarkdown = lazy(() => import('react-markdown'));
const ReactMarkdown = (props: ComponentProps<typeof LazyReactMarkdown>) => (
<Suspense fallback={null}>
<LazyReactMarkdown {...props} />
</Suspense>
);
export type AgentInfoFormValue = Partial<{
bot_uri: UploadValue;
name: string;
target: string;
spaceId?: string;
enableMonetize?: boolean;
}>;
export interface AgentInfoFormProps {
className?: string;
mode: 'add' | 'update';
showSpace: boolean;
initialValues: Partial<DraftBot>;
spacesList: BotSpace[];
currentSpaceId?: string; // Current space ID from store
hideOperation?: boolean; // hide_operation from store
checkErr: boolean;
errMsg: string;
onValuesChange: (values: AgentInfoFormValue) => void;
slot?: React.ReactNode;
}
export const AgentInfoForm = forwardRef<
Form<AgentInfoFormValue>,
AgentInfoFormProps
>(
// eslint-disable-next-line complexity
(
{
className,
mode,
showSpace,
initialValues,
spacesList,
currentSpaceId,
hideOperation,
checkErr,
errMsg,
onValuesChange,
slot,
},
ref,
) => (
<Form<AgentInfoFormValue>
ref={ref}
showValidateIcon={false}
className={classNames(s['upload-form'], className)} // Ensure class name is correct
onValueChange={values => {
onValuesChange(values);
}}
>
<FormInput
initValue={botInputLengthService.sliceStringByMaxLength({
value: initialValues?.name ?? '',
field: 'botName',
})}
field="name"
label={I18n.t('bot_create_name')}
noErrorMessage
maxLength={botInputLengthService.getInputLengthLimit('botName')}
rules={[{ required: true }]}
placeholder={I18n.t('bot_create_name_placeholder')}
getValueLength={reactText =>
botInputLengthService.getValueLength(reactText)
}
/>
{IS_OVERSEA && mode === 'add' ? (
<FormSwitch
field="enableMonetize"
label={I18n.t('monetization')}
desc={I18n.t('monetization_des')}
initValue={true} // Consider if initial value should be prop
rules={[{ required: true }]}
/>
) : null}
<FormTextArea
field="target"
initValue={botInputLengthService.sliceStringByMaxLength({
value: initialValues?.description ?? '',
field: 'botDescription',
})}
label={I18n.t('bot_create_desciption')}
placeholder={I18n.t('bot_create_description_placeholder')}
maxCount={botInputLengthService.getInputLengthLimit('botDescription')}
maxLength={botInputLengthService.getInputLengthLimit('botDescription')}
getValueLength={botInputLengthService.getValueLength}
/>
{showSpace && mode === 'add' ? (
<FormSelect
label={I18n.t('duplicate_select_workspace')}
field="spaceId"
initValue={
hideOperation
? spacesList?.[0]?.id
: (currentSpaceId ?? spacesList?.[0]?.id)
}
placeholder={I18n.t('select_team')}
noErrorMessage
className={classNames(s.select)}
rules={[{ required: true }]}
renderSelectedItem={(optionNode: BotSpace) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar
src={optionNode.icon_url}
size="extra-extra-small"
style={{ flexShrink: 0 }}
>
{optionNode.name}
</Avatar>
<span className={classNames(s['select-name'])}>
{optionNode.name}
</span>
</div>
)}
>
{spacesList
?.filter(t => !t.hide_operation)
?.map(item => (
<FormSelect.Option value={item.id} {...item} key={item.id}>
<div className="ml-[8px]">
{item.icon_url ? (
<Avatar size="extra-small" src={item.icon_url} />
) : (
<IconTeamDefault
className={classNames(s['select-item-icon'])}
/>
)}
</div>
<div className={classNames(s['select-item-name'])}>
<Text
ellipsis={{
showTooltip: false,
}}
style={{
maxWidth: '280px',
}}
>
{item.name}
</Text>
</div>
{item.space_type === SpaceType.Team && (
<Tag color="brand">{I18n.t('develop_team_team')}</Tag>
)}
</FormSelect.Option>
))}
</FormSelect>
) : null}
{slot}
{checkErr ? (
<div className={s['content-check-error']}>
<ReactMarkdown
skipHtml={true}
className={s.markdown}
linkTarget="_blank"
>
{errMsg ?? I18n.t('publish_audit_pop7')}
</ReactMarkdown>
</div>
) : null}
</Form>
),
);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import cls from 'classnames';
import { Switch, withField, type SwitchProps } from '@coze-arch/coze-design';
function SwitchWithDesc({
value,
onChange,
className,
desc,
descClassName,
switchClassName,
...rest
}: Omit<SwitchProps, 'checked'> & {
value?: boolean;
desc: string;
descClassName?: string;
switchClassName?: string;
}) {
return (
<div className={cls('flex items-center justify-between', className)}>
<span className={cls('coz-fg-primary', descClassName)}>{desc}</span>
<Switch
size="small"
{...rest}
checked={value}
onChange={onChange}
className={cls('shrink-0', switchClassName)}
/>
</div>
);
}
export const FormSwitch = withField(SwitchWithDesc);

View File

@@ -0,0 +1,408 @@
/* stylelint-disable declaration-no-important */
/* stylelint-disable max-nesting-depth */
.card-link {
text-decoration: none;
}
.card {
cursor: pointer;
display: flex;
flex-direction: column;
min-width: 323px;
height: 180px;
padding: 12px 16px 16px 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 6px 8px 0 rgb(28 31 35 / 6%);
&:hover {
box-shadow: 0 10px 12px 0 rgb(28 31 35 / 10%);
}
// 更多操作按钮active时不触发父级active
&:active:not(:focus-within) {
background-color: #e6e7ea;
}
.bot-info {
display: flex;
flex-wrap: nowrap;
margin-bottom: auto;
.bot-avatar {
flex-shrink: 0;
width: 68px;
height: 68px;
margin-top: 12px;
margin-left: 4px;
background-color: #2E2E3814;
border-radius: 8px;
}
.bot-text {
overflow: hidden;
display: flex;
flex: 1;
flex-flow: column nowrap;
margin-left: 16px;
.bot-title {
display: flex;
flex: 1;
justify-content: space-between;
margin-bottom: 4px;
.bot-name {
margin-top: 12px;
font-size: 18px;
font-weight: 600;
line-height: 24px;
color: #1c1d23;
}
}
.bot-description {
height: 40px;
margin-bottom: 8px;
font-size: 14px;
line-height: 20px;
color: rgb(28 29 35 / 80%);
}
.bot-hot-and-category {
display: flex;
align-items: center;
.bot-hot-value {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #1C1D2359;
}
.bot-category-tag {
margin-left: 16px;
border: 1px solid #1D1C2314;
border-radius: 6px;
.bot-category-text {
font-size: 12px;
color: #6B6B75;
}
}
}
.bot-model-and-publish {
display: flex;
align-items: center;
.bot-publish-status {
display: flex;
align-items: center;
font-size: 12px;
line-height: 16px;
color: rgb(28 29 35 / 60%);
letter-spacing: 0.12px;
&-icon {
margin-right: 4px;
color: rgb(62 194 84);
}
&-warning-icon {
margin-right: 4px;
color: var(--semi-color-warning);
}
}
.bot-model {
font-size: 12px;
line-height: 16px;
color: rgb(28 29 35 / 35%);
letter-spacing: 0.12px;
}
.divider {
height: calc(100% - 4px);
}
.bot-publish-platform {
display: flex;
column-gap: 6px;
align-items: center;
font-size: 12px;
line-height: 16px;
color: rgb(28 29 35 / 60%);
letter-spacing: 0.12px;
:global {
.semi-avatar {
width: 12px;
height: 12px;
}
}
}
}
}
}
.bot-creator-info {
overflow: hidden;
display: flex;
align-items: center;
width: 100%;
padding: 0 4px;
font-size: 12px;
line-height: 16px;
color: rgb(28 29 35 / 35%);
letter-spacing: 0.12px;
.creator-avatar {
flex-shrink: 0;
width: 16px;
height: 16px;
margin-right: 4px;
}
.bot-creator {
font-size: 12px;
line-height: 16px;
color: rgb(28 29 35 / 35%);
letter-spacing: 0.12px;
}
.bot-edit-time {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
}
}
.explore-height {
height: 136px;
}
.publish-popover {
&-item {
display: flex;
align-items: center;
padding: 4px;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
}
&:not(:last-child) {
border-bottom: 1px solid #efefef;
}
&-avatar {
margin-right: 10px;
}
:global {
img {
width: 100%;
height: 100%;
}
}
}
}
.bot-description-tooltip {
color: var(--semi-color-bg-0);
}
.add-card {
background-color: white;
border-radius: 8px;
}
.add-card-inner {
display: flex;
flex-direction: column;
justify-content: center;
}
.name-wrap {
display: flex;
align-items: center;
width: 100%;
height: 40px;
padding: 16px 16px 0;
}
.name {
position: absolute;
top: 16px;
left: 48px;
overflow: hidden;
max-width: calc(100% - 156px);
font-size: 14px;
font-weight: 600;
line-height: 24px;
color: #000;
text-overflow: ellipsis;
white-space: nowrap;
}
.extra {
margin-left: auto;
}
.description {
overflow: hidden;
display: -webkit-box;
font-size: 12px;
line-height: 18px;
color: #494c4f;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.recent-modify {
margin-top: 8px;
font-size: 12px;
line-height: 16px;
color: rgb(28 31 35 / 60%);
}
.creator {
width: fit-content;
padding: 4px;
font-size: 12px;
line-height: 16px;
color: #346ef8;
background: rgb(51 112 255 / 10%);
border: none !important;
border-radius: 3px !important;
}
.upload-form {
.upload-filed {
padding-top: 0;
}
.textarea-multi-line {
:global {
.semi-input-textarea {
padding-right: 66px;
}
.semi-input-textarea-counter {
position: absolute;
right: 12px;
bottom: 8px;
min-height: 0;
padding: 0;
color: var(--light-usage-text-color-text-3, rgb(28 29 35 / 35%));
}
}
}
:global {
.semi-form-field-label {
margin-bottom: 8px;
}
}
}
.upload-form-item {
:global {
.semi-form-field-label {
display: none;
}
.semi-form-field-label-text {
display: none;
}
.semi-form-field {
padding: 0;
}
}
}
.collect-num {
width: 12px;
height: 12px;
margin-right: 4px;
svg {
width: 12px;
height: 12px;
}
}
.user-info {
margin-top: auto;
}
.content-check-error {
margin-top: -8px;
color: red;
}
.bot-ui-modal {
:global {
.semi-modal-content .semi-modal-body {
padding: 12px 0 22px;
}
.semi-form-vertical .semi-form-field {
padding-bottom: 14px !important;
}
}
}
.select {
width: 100%;
.select-name {
margin-left: 6px;
font-weight: 400;
}
}
.select-item-icon {
svg {
width: 24px;
height: 24px;
}
}
.select-item-name {
margin-right: 16px;
margin-left: 12px;
font-weight: 600;
}

View File

@@ -0,0 +1,29 @@
/*
* 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 {
useAgentFormManagement,
type UseAgentFormManagementProps,
} from './use-agent-form-management';
export {
useAgentPersistence,
type UseAgentPersistenceProps,
} from './use-agent-persistence';
export {
AgentInfoForm,
type AgentInfoFormProps,
type AgentInfoFormValue,
} from './agent-info-form';

View File

@@ -0,0 +1,106 @@
/*
* 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, useRef } from 'react';
import { type Form } from '@coze-arch/coze-design';
import { type DraftBot } from '@coze-arch/bot-api/developer_api';
import { type AgentInfoFormValue } from './agent-info-form';
export interface UseAgentFormManagementProps {
initialBotInfo?: DraftBot;
}
export const useAgentFormManagement = ({
initialBotInfo,
}: UseAgentFormManagementProps) => {
const formRef = useRef<Form<AgentInfoFormValue>>(null);
const [isOkButtonDisable, setOkButtonDisable] = useState(
!initialBotInfo?.name?.trim(),
);
const [botInfo4Generate, setBotInfo4Generate] = useState<{
name: string;
desc: string;
avatar: { uri: string; url: string };
}>({
name: initialBotInfo?.name || '',
desc: initialBotInfo?.description || '',
avatar: {
uri: initialBotInfo?.icon_uri || '',
url: initialBotInfo?.icon_url || '',
},
});
const [checkErr, setCheckErr] = useState(false);
const [errMsg, setErrMsg] = useState('');
const [confirmDisabled, setConfirmDisabled] = useState(false);
const resetFormState = () => {
setOkButtonDisable(!initialBotInfo?.name?.trim());
setBotInfo4Generate({
name: initialBotInfo?.name || '',
desc: initialBotInfo?.description || '',
avatar: {
uri: initialBotInfo?.icon_uri || '',
url: initialBotInfo?.icon_url || '',
},
});
setCheckErr(false);
setErrMsg('');
};
const handleFormValuesChange = (values: AgentInfoFormValue) => {
setBotInfo4Generate({
name: values.name?.trim() || '',
desc: values.target?.trim() || '',
avatar: {
uri: values.bot_uri?.[0]?.uid || '',
url: values.bot_uri?.[0]?.url || '',
},
});
setCheckErr(false);
setErrMsg('');
setOkButtonDisable(!values.name?.trim());
};
const getValues = async () => {
const formApi = formRef.current?.formApi;
await formApi?.validate();
return formApi?.getValues();
};
const setBotIcon = (val: { url: string; uid: string }) => {
const formApi = formRef.current?.formApi;
formApi?.setValue('bot_uri', [val]);
};
return {
formRef,
isOkButtonDisable,
botInfo4Generate,
checkErr,
errMsg,
confirmDisabled,
setCheckErr,
setErrMsg,
setConfirmDisabled,
setOkButtonDisable,
handleFormValuesChange,
getValues,
setBotIcon,
resetFormState,
};
};

View File

@@ -0,0 +1,274 @@
/*
* 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 { useState } from 'react';
import { withSlardarIdButton } from '@coze-studio/bot-utils';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useCollaborationStore } from '@coze-studio/bot-detail-store/collaboration';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { updateBotRequest } from '@coze-studio/bot-detail-store';
import {
REPORT_EVENTS as ReportEventNames,
createReportEvent,
} from '@coze-arch/report-events';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { CustomError } from '@coze-arch/bot-error';
import {
type BotSpace,
SpaceType,
type DraftBotCreateResponse,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { type AgentInfoFormValue } from './agent-info-form';
type OnSuccessCallback = (
botId?: string,
spaceId?: string,
extra?: { botName?: string; botAvatar?: string; botDesc?: string },
) => void | Promise<void>;
export interface UseAgentPersistenceProps {
mode: 'add' | 'update';
botId?: string; // Current bot ID for update mode
currentSpaceId?: string; // Current space ID from store
outerSpaceId?: string; // Space ID passed via props
getValues: () => Promise<AgentInfoFormValue | undefined>;
onSuccess?: OnSuccessCallback;
onError?: () => void;
onBefore?: () => void;
setVisible: (visible: boolean) => void;
setCheckErr: (isError: boolean) => void;
setErrMsg: (message: string) => void;
bizCreateFrom?: 'navi' | 'space';
showSpace?: boolean;
}
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function
export const useAgentPersistence = ({
mode,
botId,
currentSpaceId,
outerSpaceId,
getValues,
onSuccess,
onError,
onBefore,
setVisible,
setCheckErr,
setErrMsg,
bizCreateFrom,
showSpace,
}: UseAgentPersistenceProps) => {
const [loading, setLoading] = useState(false);
const setBotInfoByImmer = useBotInfoStore(state => state.setBotInfoByImmer);
const setCollaborationByImmer = useCollaborationStore(
state => state.setCollaborationByImmer,
);
const setPageRuntimeByImmer = usePageRuntimeStore(
state => state.setPageRuntimeByImmer,
);
const {
spaces: { bot_space_list: list },
} = useSpaceStore();
const reportEvent = createReportEvent({
eventName:
mode === 'add' ? ReportEventNames.createBot : ReportEventNames.updateBot,
});
const reportTea = ({
resp,
values,
personalSpaceInfo,
paramsSpaceId,
}: {
resp: DraftBotCreateResponse;
values: AgentInfoFormValue | undefined;
personalSpaceInfo: BotSpace | undefined;
paramsSpaceId: string;
}) => {
if (resp.code === 0) {
sendTeaEvent(EVENT_NAMES.create_bot_result, {
source: showSpace ? 'menu_bar' : 'bot_list',
workspace_type:
personalSpaceInfo?.id === paramsSpaceId
? 'personal_workspace'
: 'team_workspace',
result: 'success',
bot_name: values?.name || '',
bot_desc: values?.target || '',
});
} else {
sendTeaEvent(EVENT_NAMES.create_bot_result, {
source: showSpace ? 'menu_bar' : 'bot_list',
workspace_type:
personalSpaceInfo?.id === paramsSpaceId
? 'personal_workspace'
: 'team_workspace',
result: 'failed',
error_code: resp.code,
error_message: resp.msg,
bot_name: values?.name || '',
bot_desc: values?.target || '',
});
}
};
const handleUpdateBot = async () => {
if (!botId) {
const msg = I18n.t('bot_copy_id_error');
throw new CustomError(ReportEventNames.updateBot, msg);
}
const values = await getValues();
logger.info({ message: 'update values', meta: { values } });
try {
setLoading(true);
const botBaseInfo = {
icon_uri: values?.bot_uri?.[0].uid || '',
name: values?.name,
description: values?.target ? values.target : '',
};
const { data } = await updateBotRequest(botBaseInfo);
if (data.check_not_pass) {
setCheckErr(true);
setErrMsg(data.check_not_pass_msg);
onError?.();
return;
}
setBotInfoByImmer(store => {
store.icon_uri = values?.bot_uri?.[0].uid;
store.icon_url = values?.bot_uri?.[0].url;
store.name = values?.name;
store.description = values?.target;
});
setCollaborationByImmer(store => {
store.sameWithOnline = data.same_with_online ?? false;
});
setPageRuntimeByImmer(store => {
store.hasUnpublishChange = data.has_change ?? false;
});
await onSuccess?.(botId, currentSpaceId, {
botAvatar: values?.bot_uri?.[0].url,
botName: values?.name,
botDesc: values?.target,
});
setVisible(false);
reportEvent.success();
Toast.success({
content: I18n.t('Update_success'),
showClose: false,
});
} catch (e) {
if (e instanceof Error) {
reportEvent.error({ error: e, reason: e.message });
}
onError?.();
Toast.error({
content: withSlardarIdButton(I18n.t('Update_failed')),
showClose: false,
});
throw e;
} finally {
setLoading(false);
}
};
const handleCreateBot = async () => {
const values = await getValues();
setLoading(true);
const paramsSpaceId =
values?.spaceId || outerSpaceId || currentSpaceId || list?.[0]?.id || '';
const personalSpaceInfo = list?.find(
item => item.space_type === SpaceType.Personal,
);
try {
onBefore?.();
const resp = await DeveloperApi.DraftBotCreate({
name: values?.name,
description: values?.target,
icon_uri: values?.bot_uri?.[0]?.uid,
space_id: paramsSpaceId,
...(IS_OVERSEA && {
monetization_conf: { is_enable: values?.enableMonetize },
}),
create_from: bizCreateFrom,
});
if (resp.data.check_not_pass) {
setCheckErr(true);
setErrMsg(resp.data.check_not_pass_msg);
onError?.();
return;
}
Toast.success({
content: I18n.t('bot_created_toast'),
showClose: false,
});
// 兼容 onSuccess 回调为同步函数的场景
await onSuccess?.(resp.data?.bot_id, paramsSpaceId, {
botName: values?.name,
botDesc: values?.target,
botAvatar: values?.bot_uri?.[0]?.url,
});
sendTeaEvent(EVENT_NAMES.click_create_bot_confirm, {
click: 'success',
bot_id: resp.data?.bot_id,
create_type: 'create',
});
reportTea({ resp, values, personalSpaceInfo, paramsSpaceId });
reportEvent.success();
setVisible(false);
return resp;
} catch (e) {
Toast.error({
content: withSlardarIdButton(I18n.t('Create_failed')),
showClose: false,
});
if (e instanceof Error) {
reportEvent.error({ error: e, reason: e.message });
sendTeaEvent(EVENT_NAMES.click_create_bot_confirm, {
click: 'failed',
create_type: 'create',
error_message: e.message,
});
}
onError?.();
// 阻止弹窗关闭
throw e;
} finally {
setLoading(false);
}
};
return {
loading,
handleCreateBot,
handleUpdateBot,
};
};

View File

@@ -0,0 +1,79 @@
/*
* 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 { Modal } from '@coze-arch/bot-semi';
import {
ModelFuncConfigStatus,
ModelFuncConfigType,
} from '@coze-arch/bot-api/developer_api';
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
export const useDatasetAutoChangeConfirm = () => {
const {
storeSet: { useModelStore },
} = useBotEditor();
return async (auto: boolean, modelId: string) => {
const model = useModelStore.getState().getModelById(modelId);
if (!model) {
return true;
}
const modelName = model.name;
const modelConfig = model.func_config;
const status =
modelConfig?.[
auto
? ModelFuncConfigType.KnowledgeAutoCall
: ModelFuncConfigType.KnowledgeOnDemandCall
];
if (
status === ModelFuncConfigStatus.NotSupport ||
status === ModelFuncConfigStatus.PoorSupport
) {
const callMethod = auto
? I18n.t('dataset_automatic_call')
: I18n.t('dataset_on_demand_call');
const toolName = I18n.t('Datasets');
return new Promise(resolve => {
const modal = Modal.confirm({
zIndex: 1031,
title: I18n.t('confirm_switch_to_on_demand_call', {
call_method: callMethod,
}),
content: {
[ModelFuncConfigStatus.NotSupport]: I18n.t(
'switch_to_on_demand_call_warning_notsupported',
{ call_method: callMethod, modelName, toolName },
),
[ModelFuncConfigStatus.PoorSupport]: I18n.t(
'switch_to_on_demand_call_warning_supportpoor',
{ callMethod, modelName, toolName },
),
}[status],
onCancel: () => {
resolve(false);
modal.destroy();
},
onOk: () => {
resolve(true);
modal.destroy();
},
});
});
}
return true;
};
};

View File

@@ -0,0 +1,74 @@
/*
* 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 { useNavigate, useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { I18n } from '@coze-arch/i18n';
const useEditConfirm = () => {
const navigate = useNavigate();
const location = useLocation();
const { savingInfo } = usePageRuntimeStore.getState();
const leaveWarningInfo = I18n.t('pop_edit_save_confirm');
// 保存navigate和location.pathname的引用
const navigateRef = useRef(navigate);
const locationRef = useRef(location.pathname);
const debouncingRef = useRef(savingInfo.debouncing);
function isNoNeedConfirm() {
return !debouncingRef.current;
}
function handleBeforeUnload(event) {
if (isNoNeedConfirm()) {
return;
}
event.preventDefault();
event.returnValue = leaveWarningInfo;
}
useEffect(() => {
// popstate 执行回调的时候,由于产生了闭包,会保存支持的值,所以需要在这里处理一下
navigateRef.current = navigate;
locationRef.current = location.pathname;
const unSubDebouncing = usePageRuntimeStore.subscribe(
store => store.savingInfo.debouncing,
debouncing => {
debouncingRef.current = debouncing;
},
);
return () => {
unSubDebouncing();
};
}, [navigate, location]);
useEffect(() => {
// 刷新页面 & 关闭页面情况,用 beforeunload
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [history]);
};
export { useEditConfirm };

View File

@@ -0,0 +1,58 @@
/*
* 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, type RefObject } from 'react';
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
import { useBotEditorService } from '../context/bot-editor-service';
export const useFreeDragModalHierarchy = ({
key,
ref,
}: {
key: string;
ref: RefObject<HTMLDivElement>;
}) => {
const { freeGrabModalHierarchyService } = useBotEditorService();
const {
storeSet: { useFreeGrabModalHierarchyStore },
} = useBotEditor();
const zIndex = useFreeGrabModalHierarchyStore(state =>
freeGrabModalHierarchyService.getModalZIndex(state.getModalIndex(key)),
);
useEffect(() => {
freeGrabModalHierarchyService.registerModal(key);
const target = ref.current;
if (!target) {
return;
}
const onFocus = () => {
freeGrabModalHierarchyService.onFocus(key);
};
target.addEventListener('focus', onFocus);
return () => {
freeGrabModalHierarchyService.removeModal(key);
target.removeEventListener('focus', onFocus);
};
}, [key]);
return zIndex;
};

View File

@@ -0,0 +1,267 @@
/*
* 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 { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { parse } from 'qs';
import { isNumber } from 'lodash-es';
import { userStoreService } from '@coze-studio/user-store';
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import {
autosaveManager,
avatarBackgroundWebSocket,
getBotDetailDtoInfo,
getBotDetailIsReadonly,
initGenerateImageStore,
updateBotRequest,
updateHeaderStatus,
useGenerateImageStore,
useMonetizeConfigStore,
initBotDetailStore,
useBotDetailStoreSet,
} from '@coze-studio/bot-detail-store';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { useErrorHandler } from '@coze-arch/logger';
import { BotMode } from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { exhaustiveCheck } from '@coze-arch/bot-utils';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { BotPageFromEnum } from '@coze-arch/bot-typings/common';
import { Toast } from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import { useBotPageStore } from '../store/bot-page/store';
interface TaskParams {
from?: 'copy' | 'multi_agent_view_old_ver';
version?: string;
}
const maxTokenAlertId = '__MODEL_MAX_TOKEN_ALERT__';
const checkShouldAlertMaxToken = (inputMaxToken: number | undefined) => {
const maxTokenWarningValue = 5;
if (!isNumber(inputMaxToken)) {
return false;
}
return inputMaxToken <= maxTokenWarningValue;
};
/**
* 当用户模型 max_token 字段 <= 5 时进行提示
* 避免用户误操作无法正常使用模型
*/
const modelMaxTokenAlert = () => {
const { useModelStore, useMultiAgentStore } = useBotDetailStoreSet.getStore();
const botMode = useBotInfoStore.getState().mode;
const maxTokens = useModelStore.getState().config.max_tokens;
const alertI18nKey = 'model_max_token_alert';
const toastOptions = {
content: I18n.t(alertI18nKey),
showClose: true,
// 当 duration 设置为 0 时toast 不会自动关闭,此时必须通过手动关闭。
duration: 0,
id: maxTokenAlertId,
};
if (botMode === BotMode.WorkflowMode) {
return;
}
if (botMode === BotMode.SingleMode) {
if (checkShouldAlertMaxToken(maxTokens)) {
Toast.warning(toastOptions);
}
return;
}
if (botMode === BotMode.MultiMode) {
const agentList = useMultiAgentStore.getState().agents;
if (
agentList.some(agent => checkShouldAlertMaxToken(agent.model.max_tokens))
) {
Toast.warning(toastOptions);
}
return;
}
exhaustiveCheck(botMode);
};
export type InitStoreSuccessResult = undefined | { disableAutoSave?: boolean };
export type UnmountCallbackResult = undefined | { disableSaveAll?: boolean };
export interface AgentInitCallback {
onBeforeInitStore: () => void;
onInitStoreSuccess: (params: { isAbort: boolean }) => InitStoreSuccessResult;
onUnmount: (params: { isInitializing: boolean }) => UnmountCallbackResult;
}
export interface AgentInitProps {
initCallback?: Partial<AgentInitCallback>;
}
const startAutosaveManagerConditionally = ({
init,
editable,
callbackRes,
}: {
init: boolean;
editable: boolean;
callbackRes: InitStoreSuccessResult;
}) => {
if (!(init && editable)) {
return;
}
const disableAutoSave = callbackRes?.disableAutoSave;
if (disableAutoSave) {
return;
}
autosaveManager.start();
};
const saveAllEdit = async ({ disable }: { disable: boolean | undefined }) => {
const { botId, mode } = useBotInfoStore.getState();
// readonly如果依赖hook会更新滞后。因为页面卸载时clearStore保留了overall的值
// 所以这里的readonly需要执行时去获取
// 增加是否是offline或者没有锁的的状态判断如果是也不进行保存。
if (!botId || getBotDetailIsReadonly() || disable) {
return;
}
const { botSkillInfo } = getBotDetailDtoInfo();
const resp = await updateBotRequest({
...botSkillInfo,
bot_mode: mode,
});
updateHeaderStatus(resp.data);
};
const useInit = (props: AgentInitProps = { initCallback: {} }) => {
const { initCallback } = props;
// TODO: 后续加锁操作会收敛到hooks中不再侵入到业务
// 初始化 store 锁,在未执行结束前,不执行页面销毁的回调
const lock = useRef(false);
// 取消初始化函数标志位,在先执行页面销毁的回调时,退出初始化函数执行
const abort = useRef(false);
const searchParams = parse(location.search.slice(1)) as TaskParams;
const params = useParams<DynamicParams>();
const errorHandler = useErrorHandler();
const userInfo = userStoreService.useUserInfo();
const { setBotInfo } = useBotInfoStore(
useShallow(state => ({
setBotInfo: state.setBotInfo,
})),
);
const { setPageRuntimeBotInfo, getBotSkillBlockCollapsibleState } =
usePageRuntimeStore(
useShallow(state => ({
setPageRuntimeBotInfo: state.setPageRuntimeBotInfo,
getBotSkillBlockCollapsibleState:
state.getBotSkillBlockCollapsibleState,
})),
);
const { setBotState } = useBotPageStore(
useShallow(state => ({
setBotState: state.setBotState,
})),
);
const navigate = useNavigate();
useEffect(() => {
if (!params.bot_id) {
navigate('/', { replace: true });
return;
}
setBotInfo({ botId: params.bot_id });
setPageRuntimeBotInfo({ pageFrom: BotPageFromEnum.Bot });
(async () => {
try {
lock.current = true;
initCallback?.onBeforeInitStore?.();
getBotSkillBlockCollapsibleState();
await initBotDetailStore({ version: searchParams.version });
// 需要判断是否为只读态依赖store初始化因此放在initBotDetailStore之后
initGenerateImageStore();
const isAbort = abort.current;
const callbackRes = initCallback?.onInitStoreSuccess?.({ isAbort });
// 判断是否先执行页面销毁的回调,是退出函数执行
if (isAbort) {
return;
}
//explore中的bot都不是自己的
const isSelf =
useBotInfoStore.getState().creator_id === userInfo?.user_id_str;
setPageRuntimeBotInfo({ isSelf });
const { init, editable } = usePageRuntimeStore.getState();
startAutosaveManagerConditionally({ init, editable, callbackRes });
if (searchParams.from === 'copy') {
Toast.success({
content: I18n.t('bot_copy_success'),
showClose: false,
});
navigate({ search: '' });
}
modelMaxTokenAlert();
lock.current = false;
} catch (e) {
errorHandler(
new CustomError(
REPORT_EVENTS.BotDetailInitHooks,
`init hooks error: ${e.message}`,
),
);
}
})();
return () => {
Toast.close(maxTokenAlertId);
// 设置取消标志位
abort.current = true;
const unmountRes = initCallback?.onUnmount?.({
isInitializing: lock.current,
});
// 有锁的情况下不执行销毁回调
if (lock.current) {
return;
}
// 页面离开时,全量保存一下编辑的内容
const { botId } = useBotInfoStore.getState();
saveAllEdit({ disable: unmountRes?.disableSaveAll });
setBotState({
// 页面卸载时将上一个botId改为当前的botId
previousBotID: botId,
});
autosaveManager.close();
setPageRuntimeBotInfo({
editable: false,
});
useMonetizeConfigStore.getState().reset();
useBotDetailStoreSet.clear();
useGenerateImageStore.getState().clearGenerateImageStore();
avatarBackgroundWebSocket.destroy();
};
}, []);
};
export { useInit };

View File

@@ -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.
*/
import { userStoreService } from '@coze-studio/user-store';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
/**
* bot 付费配置是否可编辑
*
* 与 bot 是否可编辑的区别:作者本人可以编辑,有 bot 编辑权限的协作者也无法修改付费配置
*/
export function useMonetizeConfigReadonly() {
const userId = userStoreService.useUserInfo()?.user_id_str;
const botCreatorId = useBotInfoStore(s => s.creator_id);
const botDetailReadonly = useBotDetailIsReadonly();
const isSelf = userId === botCreatorId;
return botDetailReadonly || !isSelf;
}

View File

@@ -0,0 +1,39 @@
/*
* 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 { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
/**
* 获取当前节点 id若返回 undefined 则代表不在 multi 模式的节点上下文
*
* Q1这个 hook 应该放到 multi-agent 模块下?
* A1multi-agent 模块下已有类似 hook。
* 单独提出来是为了避免某些在 single 和 multi 都会复用的组件要使用 node id 时,产生不合理的 import 路径
*
* Q2在 single 和 multi 都复用的组件,应该由调用方传入 node id
* A2太理想了。有些组件业务逻辑过于复杂由调用方传入的话会导致层层传参的深度略显夸张比如 bot 的 workflow 弹窗。
*/
export function useCurrentNodeId() {
let nodeId: string | undefined;
try {
// eslint-disable-next-line react-hooks/rules-of-hooks -- 并未条件性调用 hook
nodeId = useCurrentEntity().id;
// eslint-disable-next-line @coze-arch/use-error-in-catch -- SDK 符合预期的报错,无需额外处理
} catch {
nodeId = undefined;
}
return nodeId;
}

View File

@@ -0,0 +1,59 @@
/*
* 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 { useRequest } from 'ahooks';
import { PluginDevelopApi } from '@coze-arch/bot-api';
export const usePluginPermissionManage = ({ botId }: { botId: string }) => {
const {
loading,
error,
data,
run: runGetList,
} = useRequest(
() => PluginDevelopApi.GetQueriedOAuthPluginList({ bot_id: botId }),
{
manual: true,
},
);
const { runAsync: runRevoke } = useRequest(
pluginId =>
PluginDevelopApi.RevokeAuthToken({
plugin_id: pluginId,
bot_id: botId,
}),
{
manual: true,
onSuccess: () => {
runGetList();
},
},
);
useEffect(() => {
runGetList();
}, []);
return {
loading,
error,
data: data?.oauth_plugin_list ?? [],
runGetList,
runRevoke,
};
};

View File

@@ -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 { type Dispatch, type SetStateAction, useState } from 'react';
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
export const usePopoverLock = ({
defaultLocked,
defaultVisible,
}: {
defaultVisible?: boolean;
defaultLocked?: boolean;
} = {}): {
props: Pick<PopoverProps, 'trigger' | 'visible' | 'onClickOutSide'>;
locked: boolean;
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
setLocked: Dispatch<SetStateAction<boolean>>;
} => {
const [locked, setLocked] = useState(defaultLocked ?? false);
const [visible, setVisible] = useState(defaultVisible ?? false);
return {
props: {
trigger: 'custom',
visible,
onClickOutSide: () => {
if (!locked) {
setVisible(false);
}
},
},
visible,
locked,
setVisible,
setLocked,
};
};

View File

@@ -0,0 +1,63 @@
/*
* 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 { useRequest } from 'ahooks';
import {
type GenerateUserQueryCollectPolicyRequest,
type GetUserQueryCollectOptionData,
} from '@coze-arch/bot-api/playground_api';
import { PlaygroundApi } from '@coze-arch/bot-api';
export const useGenerateLink = () => {
const [link, setLink] = useState('');
const { loading, run: runGenerate } = useRequest(
(info: GenerateUserQueryCollectPolicyRequest) =>
PlaygroundApi.GenerateUserQueryCollectPolicy(info),
{
manual: true,
onSuccess: dataSourceData => {
setLink(dataSourceData.data.policy_link);
},
},
);
return {
runGenerate,
loading,
link,
};
};
export const useGetUserQueryCollectOption = () => {
const [queryCollectOption, setQueryCollectOption] =
useState<GetUserQueryCollectOptionData>();
const [supportText, setSupportText] = useState('');
useRequest(() => PlaygroundApi.GetUserQueryCollectOption(), {
onSuccess: dataSourceData => {
setQueryCollectOption(dataSourceData.data);
setSupportText(
dataSourceData.data?.support_connectors
?.map(item => item.name)
.join('、'),
);
},
});
return {
queryCollectOption,
supportText,
};
};

View File

@@ -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 { useState } from 'react';
import { useRequest } from 'ahooks';
import { SpaceApiV2 } from '@coze-arch/bot-space-api';
import { SpaceRoleType } from '@coze-arch/bot-api/playground_api';
export const useSpaceRole = () => {
const [isOwner, setIsOwner] = useState(false);
useRequest(
() =>
SpaceApiV2.SpaceMemberDetailV2({
page: 1,
size: 1,
}),
{
onSuccess: res => {
setIsOwner(res.data?.space_role_type === SpaceRoleType.Owner);
},
},
);
return {
isOwner,
};
};

View File

@@ -0,0 +1,124 @@
/*
* 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 { shuffle } from 'lodash-es';
import { SuggestedQuestionsShowMode } from '@coze-arch/bot-api/developer_api';
import { type ExtendOnboardingContent } from '@coze-studio/bot-detail-store/src/types/skill';
import { useBotSkillStore } from '@coze-studio/bot-detail-store/bot-skill';
import { useChatArea } from '@coze-common/chat-area';
import { getShuffledSuggestions } from '@coze-agent-ide/onboarding';
import { type OnboardingDirtyLogicCompatibilityStore } from '@coze-agent-ide/bot-editor-context-store/src/store/onboarding-dirty-logic-compatibility';
import { useBotEditor } from '@coze-agent-ide/bot-editor-context-store';
const maxLength = 3;
interface UseSubscribeOnboardingAndUpdateChatAreaProps {
setOnboardingSuggestionList?: (
suggestions: ExtendOnboardingContent['suggested_questions'],
) => void;
updatePrologue?: (prologue: string) => void;
useOnboardingDirtyLogicCompatibilityStore?: OnboardingDirtyLogicCompatibilityStore;
}
export const useSubscribeOnboardingAndUpdateChatArea = (
props?: UseSubscribeOnboardingAndUpdateChatAreaProps,
) => {
const chatArea = useChatArea();
const { storeSet } = useBotEditor();
const setOnboardingSuggestionList =
props?.setOnboardingSuggestionList ?? chatArea.setOnboardingSuggestionList;
const updatePrologue = props?.updatePrologue ?? chatArea.updatePrologue;
const useOnboardingDirtyLogicCompatibilityStore =
props?.useOnboardingDirtyLogicCompatibilityStore ??
storeSet.useOnboardingDirtyLogicCompatibilityStore;
const getHasContentSuggestion = (
suggestion: ExtendOnboardingContent['suggested_questions'][0],
) => Boolean(suggestion.content.trim());
const initRecordingOnboarding = () => {
const botSkillOnboarding = useBotSkillStore.getState().onboardingContent;
const hasContentSuggestion = botSkillOnboarding.suggested_questions.filter(
getHasContentSuggestion,
);
updatePrologue(botSkillOnboarding.prologue);
if (isShowAllSuggestion(botSkillOnboarding)) {
setOnboardingSuggestionList(hasContentSuggestion);
return;
}
const preVisibleSuggestion =
hasContentSuggestion.length > maxLength
? shuffle(hasContentSuggestion)
: hasContentSuggestion;
const visibleSuggestion = preVisibleSuggestion.slice(0, maxLength);
useOnboardingDirtyLogicCompatibilityStore
.getState()
.addShuffledSuggestions(visibleSuggestion);
};
useEffect(() => {
const offDirtyOnboardingSubscribe =
useOnboardingDirtyLogicCompatibilityStore.subscribe(
state => state.shuffledSuggestions,
shuffledSuggestions => {
setOnboardingSuggestionList(shuffledSuggestions);
},
);
const offBotDetailSubscribe = useBotSkillStore.subscribe(
state => state.onboardingContent,
botSkillOnboardingContent => {
const hasContentSuggestion =
botSkillOnboardingContent.suggested_questions.filter(
getHasContentSuggestion,
);
updatePrologue(botSkillOnboardingContent.prologue);
if (isShowAllSuggestion(botSkillOnboardingContent)) {
setOnboardingSuggestionList(hasContentSuggestion);
return;
}
const { shuffledSuggestions, setShuffledSuggestions } =
useOnboardingDirtyLogicCompatibilityStore.getState();
setShuffledSuggestions(
getShuffledSuggestions({
originSuggestions: hasContentSuggestion,
shuffledSuggestions,
maxLength,
}),
);
},
);
initRecordingOnboarding();
return () => {
offBotDetailSubscribe();
offDirtyOnboardingSubscribe();
};
}, []);
};
const isShowAllSuggestion = (onboardingContent: ExtendOnboardingContent) =>
onboardingContent.suggested_questions_show_mode ===
SuggestedQuestionsShowMode.All;

View File

@@ -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.
*/
import { useRequest } from 'ahooks';
import {
createReportEvent,
REPORT_EVENTS as ReportEventNames,
} from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { SpaceApi } from '@coze-arch/bot-space-api';
import { useUIModal, Typography } from '@coze-arch/bot-semi';
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
import { IconAlertTriangle } from '@douyinfe/semi-icons';
import styles from '../pages/publish/index.module.less';
interface DiscordConfigureProps {
botId: string;
origin?: 'project' | 'bot';
platformInfo: PublishConnectorInfo;
onUnbind: () => void;
}
const unbindPublishPlatformEvent = createReportEvent({
eventName: ReportEventNames.unbindPublishPlatform,
});
export const useUnbindPlatformModal = ({
botId,
origin = 'bot',
platformInfo,
onUnbind,
}: DiscordConfigureProps) => {
const { loading, run: unbindConntect } = useRequest(
async () => {
await SpaceApi.UnBindConnector({
bot_id: botId,
agent_type: origin === 'bot' ? 0 : 1,
bind_id: platformInfo.bind_id ?? '',
connector_id: platformInfo.id,
});
},
{
manual: true,
onBefore: () => {
unbindPublishPlatformEvent.start();
},
onSuccess: () => {
onUnbind();
close();
unbindPublishPlatformEvent.success();
},
onError: (error, params) => {
unbindPublishPlatformEvent.error({
error,
reason: error?.message,
meta: { ...params },
});
},
},
);
const { modal, open, close } = useUIModal({
type: 'info',
icon: (
<IconAlertTriangle
style={{ color: 'var(--semi-color-danger)' }}
size="extra-large"
/>
),
onOk: () => {
unbindConntect();
},
okText: I18n.t('Confirm'),
okButtonProps: {
loading,
type: 'danger',
},
cancelText: I18n.t('Cancel'),
onCancel: () => close(),
title: I18n.t('bot_publish_disconnect_title', {
platform: platformInfo?.name ?? '',
}),
closable: false,
});
return {
node: modal(
<Typography.Paragraph className={styles['unbind-text']}>
{I18n.t('bot_publish_disconnect_desc', {
platform: platformInfo?.name ?? '',
})}
</Typography.Paragraph>,
),
open,
close,
};
};