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,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 { Outlet, useParams } from 'react-router-dom';
import { useDestorySpace } from '@coze-common/auth';
import { useInitSpaceRole } from '@coze-common/auth-adapter';
const SpaceIdContainer = ({ spaceId }: { spaceId: string }) => {
// 空间组件销毁时清空对应space数据
useDestorySpace(spaceId);
// 初始化空间权限数据
const isCompleted = useInitSpaceRole(spaceId);
// isCompleted 的 判断条件很重要确保了在Space空间内能够获取到空间的权限数据。
return isCompleted ? <Outlet /> : null;
};
export const SpaceIdLayout = () => {
const { space_id: spaceId } = useParams<{
space_id: string;
}>();
return spaceId ? <SpaceIdContainer key={spaceId} spaceId={spaceId} /> : null;
};

View File

@@ -0,0 +1,169 @@
/*
* 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 FC } from 'react';
import classNames from 'classnames';
import { cozeMitt } from '@coze-common/coze-mitt';
import { reporter } from '@coze-arch/logger';
import {
type IntelligenceData,
IntelligenceType,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { IconCozMore } from '@coze-arch/coze-design/icons';
import {
Space,
Avatar,
Typography,
Popover,
Button,
} from '@coze-arch/coze-design';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { CustomError } from '@coze-arch/bot-error';
import {
ProductEntityType,
type FavoriteProductResponse,
} from '@coze-arch/bot-api/product_api';
import { ProductApi } from '@coze-arch/bot-api';
const getSubPath = (type: IntelligenceType | undefined) => {
if (type === IntelligenceType.Project) {
return 'project-ide';
}
if (type === IntelligenceType.Bot) {
//跳转至 Bot编辑页后续会改成新的URL/space/:spaceId/agent/:agentId
return 'bot';
}
return '';
};
const getIntelligenceNavigateUrl = ({
basic_info = {},
type,
}: Pick<IntelligenceData, 'basic_info' | 'type'>) => {
const { space_id, id } = basic_info;
return `/space/${space_id}/${getSubPath(type)}/${id}`;
};
export const FavoritesListItem: FC<IntelligenceData> = ({
basic_info = {},
type,
}) => {
// 取消收藏
const clickToUnfavorite = async () => {
try {
const res: FavoriteProductResponse =
await ProductApi.PublicFavoriteProduct({
entity_type:
type === IntelligenceType.Project
? ProductEntityType.Project
: ProductEntityType.Bot,
is_cancel: true,
entity_id: id,
});
if (res.code === 0) {
// 取消收藏成功,刷新收藏列表
cozeMitt.emit('refreshFavList', {
id,
numDelta: -1,
emitPosition: 'favorites-list-item',
});
} else {
throw new Error(res.message);
}
} catch (error) {
reporter.errorEvent({
eventName: 'sub_menu_unfavorite_error',
error: new CustomError(
'sub_menu_unfavorite_error',
(error as Error).message,
),
});
}
};
const { icon_url, name, space_id, id } = basic_info;
return (
<div
className={classNames(
'group',
'h-[32px] w-full rounded-[8px] cursor-pointer hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
)}
onClick={() => {
if (!space_id || !id) {
return;
}
sendTeaEvent(EVENT_NAMES.coze_space_sidenavi_ck, {
item: id,
category: 'space_favourite',
navi_type: 'second',
need_login: true,
have_access: true,
});
//跳转至 Bot编辑页后续会改成新的URL/space/:spaceId/agent/:agentId
window.open(getIntelligenceNavigateUrl({ basic_info, type }), '_blank');
}}
data-testid="workspace.favorites.list.item"
>
<Space className="h-[32px] px-[8px] w-full" spacing={8}>
<Avatar
className="h-[16px] w-[16px] rounded-[4px] shrink-0"
shape="square"
src={icon_url}
/>
<Typography.Text
className="flex-1"
ellipsis={{ showTooltip: true, rows: 1 }}
>
{name}
</Typography.Text>
<div
onClick={e => {
e.stopPropagation();
}}
className={classNames(
'invisible opacity-0 group-hover:visible group-hover:opacity-100',
'h-[16px] w-[16px]',
)}
>
<Popover
className="rounded-[8px]"
position="bottomRight"
mouseLeaveDelay={200}
stopPropagation
content={
<div
data-testid="workspace.favorites.list.item.popover"
className="w-[112px] h-[32px] pl-[8px] rounded-[8px] flex items-center overflow-hidden relative cursor-pointer hover:coz-mg-secondary-hovered"
onClick={clickToUnfavorite}
>
{I18n.t('navigation_workspace_favourites_cancle')}
</div>
}
>
<Button
data-testid="workspace.favorites.list.item.popover.button"
className={classNames('h-full w-full !flex')}
size="mini"
color="secondary"
icon={<IconCozMore />}
/>
</Popover>
</div>
</Space>
</div>
);
};

View File

@@ -0,0 +1,221 @@
/*
* 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 FC, useRef, useEffect, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import classNames from 'classnames';
import { useInfiniteScroll } from 'ahooks';
import { reporter } from '@coze-arch/logger';
import {
type Intelligence,
IntelligenceStatus,
SearchScope,
search,
} from '@coze-arch/idl/intelligence_api';
import { I18n } from '@coze-arch/i18n';
import { type SpaceType } from '@coze-arch/bot-api/developer_api';
import { intelligenceApi } from '@coze-arch/bot-api';
import { useSpaceStore } from '@coze-foundation/space-store';
import { cozeMitt, type RefreshFavListParams } from '@coze-common/coze-mitt';
import { CustomError } from '@coze-arch/bot-error';
import { Space, Loading } from '@coze-arch/coze-design';
import { FavoritesListItem } from './favorites-list-item';
interface FEIntelligenceListData {
list: Intelligence[];
total: number;
hasMore: boolean;
cursorId?: string;
}
const emptyDraftBotListData: FEIntelligenceListData = {
list: [],
total: 0,
hasMore: false,
cursorId: undefined,
};
const DEFAULT_PAGE_SIZE = 20;
const getFavoritesList = async ({
spaceId,
spaceType,
cursorId,
pageSize = DEFAULT_PAGE_SIZE,
}: {
spaceId?: string;
spaceType?: SpaceType;
cursorId?: string;
pageSize?: number;
}): Promise<FEIntelligenceListData> => {
try {
if (spaceId) {
const res = await intelligenceApi.GetDraftIntelligenceList({
space_id: spaceId,
order_by: search.OrderBy.UpdateTime,
is_fav: true,
status: [
IntelligenceStatus.Using,
IntelligenceStatus.Banned,
IntelligenceStatus.MoveFailed,
],
size: pageSize,
cursor_id: cursorId,
search_scope: SearchScope.All,
});
const resData = res?.data;
return {
list: resData?.intelligences || [],
total: resData?.total ?? 0,
hasMore: Boolean(resData?.has_more),
cursorId: resData?.next_cursor_id,
};
} else {
return emptyDraftBotListData;
}
} catch (error) {
reporter.errorEvent({
eventName: 'get_favorites_list_error',
error: new CustomError(
'get_favorites_list_error',
(error as Error).message,
),
});
return emptyDraftBotListData;
}
};
/**
* ahooks 的 useInfiniteScroll 返回的对象引用会变,本方法返回一个引用不变的对象,仅此而已,不用关注其声明和实现
*/
const useInfiniteScrollRef: typeof useInfiniteScroll = <
T extends { list: unknown[] },
>(
...params: Parameters<typeof useInfiniteScroll<T>>
) => {
const req = useInfiniteScroll<T>(...params);
const reqRef = useMemo(() => ({ ...req }), []);
return Object.assign(reqRef, req);
};
export const FavoritesList: FC = () => {
const { spaceId, spaceType } = useSpaceStore(
useShallow(store => ({
spaceId: store.space.id,
spaceType: store.space.space_type,
})),
);
const containerRef = useRef<HTMLDivElement>(null);
// 用一个引用不变的 req便于 effect 中的 handler 拿到最新的 loading 状态
// (将 loading 放进 effect 的 deps 中并不能解决问题,因为上一次的闭包中 getFavoritesList 前后的 loading 状态已经固定不会变了,导致上一次执行出错)
const req = useInfiniteScrollRef<FEIntelligenceListData>(
async dataSource =>
await getFavoritesList({
spaceId,
spaceType,
cursorId: dataSource?.cursorId ?? undefined,
}),
{
target: containerRef,
reloadDeps: [spaceId, spaceType],
isNoMore: dataSource => !dataSource?.hasMore,
},
);
const { loading, data, loadingMore } = req;
useEffect(() => {
const handler = async (refreshFavListParams: RefreshFavListParams) => {
if (req.loading || req.loadingMore) {
// 处理竞态问题,优先保证列表滚动加载,下同
return;
}
const currLength = req.data?.list?.length;
const mutateData = await getFavoritesList({
spaceId,
spaceType,
// Q为什么要专门设置 pageSize
// AuseInfiniteScroll 有个 bug/featuremutate 后不会立即触发高度检测
// 这要从它的 loadmore 触发逻辑讲起,正常是监听 scroll 动作,检测高度,从而判断是否需要 loadmore
// 但假如首次请求回来的数据就不足一屏高度,没有 overflow 无法触发 scroll 动作,怎么办?
// 因此 useInfiniteScroll 会在 run、reload 之类的行为完成后立即做一次高度检测来判断是否要继续 loadmore。
// 但是!它却唯独不会在 mutate 后做高度检测,导致 mutate 出来的数据不足一屏,就再也无法 loadmore 了
// 因此这里手动计算一下需要 mutate 的数据量。
// 如果后续 pageSize 太大有问题,那还可以继续改造一下 useInfiniteScrollRef使 mutate 动作实际执行 reload但拦截其 loading 属性返回 false
pageSize: Math.max(
currLength
? currLength + refreshFavListParams.numDelta
: DEFAULT_PAGE_SIZE,
DEFAULT_PAGE_SIZE,
),
});
if (req.loading || req.loadingMore) {
return;
}
// 使用 mutate 静默加载,直接更新视图,不展示 loading 效果
req.mutate(mutateData);
};
cozeMitt.on('refreshFavList', handler);
return () => cozeMitt.off('refreshFavList', handler);
}, [spaceId, spaceType]);
return (
// 这里有一个很坑,滚动蒙层如果直接挂在滚动画布元素上会一起滚动,所以这里需要单独包一层(往上方一层)
<div className={classNames('w-full h-full flex flex-col')}>
<>
<Space
className="h-[24px] pl-[8px] w-full mb-[4px] flex-none"
spacing={4}
>
<div className="coz-fg-secondary text-[14px] font-[500] leading-[20px]">
{I18n.t('navigation_workspace_favourites', {}, 'Favourites')}
</div>
</Space>
<div
ref={containerRef}
className="w-full flex-grow max-h-full overflow-y-auto styled-scrollbar-hidden"
>
{loading ? (
<div className="flex items-center justify-center h-[200px] w-full">
<Loading loading={true} size="mini" />
</div>
) : (
<Space vertical spacing={4} className="w-full">
{data?.list?.length && data?.list?.length > 0 ? (
data?.list?.map(intelligenceData => (
<FavoritesListItem
key={intelligenceData.basic_info?.id}
{...intelligenceData}
/>
))
) : (
<div className="coz-fg-dim pl-[8px] text-[14px] font-[500] leading-[20px]">
<div>{I18n.t('home_favor_desc1')}</div>
<div>{I18n.t('home_favor_desc2')}</div>
</div>
)}
{loadingMore ? <Loading loading={true} size="mini" /> : null}
</Space>
)}
</div>
</>
</div>
);
};

View File

@@ -0,0 +1,77 @@
/* 定义滚动条默认隐藏样式 styled-scrollbar-hidden */
.styled-scrollbar-hidden {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.styled-scrollbar-hidden::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.styled-scrollbar-hidden:hover {
scrollbar-width: auto;
}
.styled-scrollbar-hidden:hover::-webkit-scrollbar {
display: inline-block;
width: 11px;
height: 11px;
background-color: transparent;
}
.styled-scrollbar-hidden:hover::-webkit-scrollbar-thumb {
background-color: rgba(31, 35, 41, 30%);
background-clip: padding-box;
border: 2px solid transparent;
border-radius: 9999px;
transition: background 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
}
.styled-scrollbar-hidden:hover::-webkit-scrollbar-thumb:hover {
background-color: rgba(31, 35, 41, 60%);
}
.styled-scrollbar-hidden:hover::-webkit-scrollbar-track {
background-color: transparent;
}
.list-top-mask,
.list-bottom-mask {
position: relative;
}
.list-bottom-mask::after,
.list-top-mask::before {
pointer-events: none; /* 确保伪元素不阻挡用户交互 */
content: '';
position: absolute;
right: 0;
left: 0;
height: 40px;
}
.list-top-mask::before {
top: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 100%),
transparent
);
}
.list-bottom-mask::after {
bottom: 0;
background: linear-gradient(to top, rgba(255, 255, 255, 100%), transparent);
}
.list-top-mask-24::before {
top: 24px;
}
.list-bottom-mask-32::after {
bottom: 32px;
}

View File

@@ -0,0 +1,99 @@
/*
* 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 } from 'react-router-dom';
import { type ReactNode, type FC } from 'react';
import { useShallow } from 'zustand/react/shallow';
import classNames from 'classnames';
import { useSpaceStore } from '@coze-foundation/space-store';
import { localStorageService } from '@coze-foundation/local-storage';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
export interface IWorkspaceListItem {
icon?: ReactNode;
activeIcon?: ReactNode;
title?: () => string;
path?: string;
dataTestId?: string;
}
interface IWorkspaceListItemProps extends IWorkspaceListItem {
currentSubMenu?: string;
}
export const WorkspaceListItem: FC<IWorkspaceListItemProps> = ({
icon,
activeIcon,
title,
path,
currentSubMenu,
dataTestId,
}) => {
const navigate = useNavigate();
const { spaceId } = useSpaceStore(
useShallow(store => ({
spaceId: store.space.id,
})),
);
return spaceId ? (
<div
onClick={() => {
sendTeaEvent(EVENT_NAMES.coze_space_sidenavi_ck, {
item: title?.() || 'unknown-workspace-submenu',
navi_type: 'second',
need_login: true,
have_access: true,
});
localStorageService.setValue('workspace-subMenu', path);
navigate(`/space/${spaceId}/${path}`);
}}
className={classNames(
'flex items-center gap-[8px]',
'transition-colors',
'rounded-[8px]',
'h-[32px] w-full',
'px-[8px]',
'cursor-pointer',
'group',
'hover:coz-mg-secondary-hovered',
{
'coz-bg-primary': path === currentSubMenu,
'coz-fg-plus': path === currentSubMenu,
'coz-fg-primary': path !== currentSubMenu,
},
)}
id={`workspace-submenu-${path}`}
data-testid={dataTestId}
>
<div className="text-[14px]">
<div className="w-[16px] h-[16px]">
{path === currentSubMenu ? activeIcon : icon}
</div>
</div>
<div
className={classNames(
'flex-1',
'text-[14px]',
'leading-[20px]',
'font-[500]',
)}
>
{title?.()}
</div>
</div>
) : null;
};

View File

@@ -0,0 +1,46 @@
/*
* 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 FC } from 'react';
import { Space } from '@coze-arch/coze-design';
import {
WorkspaceListItem,
type IWorkspaceListItem,
} from './workspace-list-item';
interface WorkspaceListProps {
menus: Array<IWorkspaceListItem>;
currentSubMenu?: string;
}
export const WorkspaceList: FC<WorkspaceListProps> = ({
menus,
currentSubMenu,
}: WorkspaceListProps) => (
<div className="w-full mt-[16px]">
<Space vertical spacing={4} className="w-full">
{menus.map((item, index) => (
<WorkspaceListItem
{...item}
key={index}
currentSubMenu={currentSubMenu}
/>
))}
</Space>
</div>
);

View File

@@ -0,0 +1,69 @@
/*
* 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 ReactNode } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Space, Skeleton } from '@coze-arch/coze-design';
import { useSpaceStore } from '@coze-foundation/space-store';
import { type IWorkspaceListItem } from './components/workspace-list-item';
import { WorkspaceList } from './components/workspace-list';
import { FavoritesList } from './components/favorites-list';
import './components/list.css';
interface IWorkspaceSubMenuProps {
header: ReactNode;
menus: Array<IWorkspaceListItem>;
currentSubMenu?: string;
}
export const WorkspaceSubMenu = ({
header,
menus,
currentSubMenu,
}: IWorkspaceSubMenuProps) => {
const { spaceList, loading } = useSpaceStore(
useShallow(state => ({
currentSpace: state.space,
spaceList: state.spaceList,
loading: !!state.loading || !state.inited,
})),
);
const hasSpace = spaceList.length > 0;
return (
<Skeleton loading={loading} active placeholder={<Skeleton.Paragraph />}>
<Space spacing={4} vertical className="w-full h-full">
<div className="flex-none w-full">{header}</div>
{hasSpace ? (
<>
<div className="flex-none w-full">
<WorkspaceList menus={menus} currentSubMenu={currentSubMenu} />
</div>
<div className="flex-grow max-h-full overflow-y-auto w-full mt-[24px]">
<FavoritesList />
</div>
</>
) : null}
</Space>
</Skeleton>
);
};
export { IWorkspaceListItem };

View File

@@ -0,0 +1,145 @@
/*
* 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 } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { useErrorHandler, reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { CustomError } from '@coze-arch/bot-error';
import { localStorageService } from '@coze-foundation/local-storage';
import { useSpaceStore } from '@coze-foundation/space-store';
const getFallbackWorkspaceURL = async (
fallbackSpaceID: string,
fallbackSpaceMenu: string,
checkSpaceID: (id: string) => boolean,
) => {
const targetSpaceId =
(await localStorageService.getValueSync('workspace-spaceId')) ??
fallbackSpaceID;
const targetSpaceSubMenu =
(await localStorageService.getValueSync('workspace-subMenu')) ??
fallbackSpaceMenu;
if (targetSpaceId && checkSpaceID(targetSpaceId)) {
return `/space/${targetSpaceId}/${targetSpaceSubMenu}`;
}
return `/space/${fallbackSpaceID}/${targetSpaceSubMenu}`;
};
export const useInitSpace = ({
spaceId,
fetchSpacesWithSpaceId,
isReady,
}: {
spaceId?: string;
fetchSpacesWithSpaceId?: (spaceId: string) => Promise<unknown>;
isReady?: boolean;
} = {}) => {
const [isError, setIsError] = useState<boolean>(false);
const navigate = useNavigate();
const capture = useErrorHandler();
const { space, spaceListLoading, spaceList } = useSpaceStore(
useShallow(
store =>
({
space: store.space,
spaceListLoading: store.loading,
spaceList: store.spaceList,
}) as const,
),
);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
(async (spaceId?: string) => {
try {
if (!isReady) {
return;
}
// 如果未指定spaceId则跳转到兜底的space下的项目开发子路由
if (!spaceId) {
// 拉取空间列表
await useSpaceStore.getState().fetchSpaces(true);
// 获取个人空间Id
const personalSpaceID = useSpaceStore.getState().getPersonalSpaceID();
// 空间列表的第一个空间
const firstSpaceID = useSpaceStore.getState().spaceList[0]?.id;
// 未指定spaceId的兜底空间
const fallbackSpaceID = personalSpaceID ?? firstSpaceID ?? '';
// 检查指定的spaceId是否可以访问
const { checkSpaceID } = useSpaceStore.getState();
// 无工作空间,提示创建
if (!fallbackSpaceID) {
Toast.warning(I18n.t('enterprise_workspace_default_tips2_toast'));
} else {
// 获取兜底的跳转URL
const targetURL = await getFallbackWorkspaceURL(
fallbackSpaceID,
'develop',
checkSpaceID,
);
// 跳转
navigate(targetURL);
}
} else {
// 拉取空间列表
await fetchSpacesWithSpaceId?.(spaceId);
if (!useSpaceStore.getState().checkSpaceID(spaceId)) {
// 当 space id 在space 列表找不到时,抛出错误
capture(
new CustomError(ReportEventNames.errorPath, 'space id error', {
customGlobalErrorConfig: {
title: I18n.t('workspace_no_permission_access'),
subtitle:
'You do not have permission to access this space or the space ID does not exist',
},
}),
);
} else {
// 更新space store的spaceId
useSpaceStore.getState().setSpace(spaceId);
}
}
} catch (e) {
reporter.error({
message: 'init_space_error',
error: e as Error,
});
setIsError(true);
capture(
new CustomError(ReportEventNames.errorPath, 'space id error', {
customGlobalErrorConfig: {
title: I18n.t('workspace_no_permission_access'),
subtitle: (e as Error).message,
},
}),
);
}
})(spaceId);
}, [spaceId, isReady]);
return { loading: !space.id, isError, spaceListLoading, spaceList };
};

View File

@@ -0,0 +1,20 @@
/*
* 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 { WorkspaceSubMenu } from './components/workspace-sub-menu';
export { SpaceIdLayout } from './components/space-id-layout';
export { useInitSpace } from './hooks/use-init-space';

View 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' />