前言
本文将深入分析Coze Studio项目中用户登录后进入工作空间查看和管理项目的前端实现,通过源码解读来理解工作空间项目开发功能的架构设计和技术实现。Coze Studio采用了现代化的React + TypeScript技术栈,结合微前端架构和模块化设计,为用户提供了高效的AI智能体开发环境。
工作空间作为用户的核心工作区域,承载着项目管理、智能体开发、资源配置等关键功能。本文将从路由配置开始,逐层深入到组件架构、数据流管理、API设计等各个层面,全面解析工作空间项目开发功能的技术实现。
项目架构概览
整体架构设计
Coze Studio前端采用了基于Rush.js的Monorepo架构,将工作空间相关功能划分为以下几个核心包:
frontend/packages/
├── studio/workspace/ # 工作空间核心模块
│ ├── entry-adapter/ # 工作空间适配器层
│ ├── entry-base/ # 工作空间基础组件
│ ├── project-entity-adapter/ # 项目实体适配器
│ ├── project-entity-base/ # 项目实体基础功能
│ └── project-publish/ # 项目发布功能
├── foundation/ # 基础设施层
│ ├── space-store/ # 空间状态管理
│ ├── space-ui-adapter/ # 空间UI适配器
│ └── space-ui-base/ # 空间UI基础组件
└── arch/ # 架构层
├── idl/ # 接口定义层
└── bot-api/ # API调用层
技术栈组成
- 框架: React 18 + TypeScript
- 路由: React Router v6
- 状态管理: Zustand
- UI组件: @coze-arch/coze-design
- 数据请求: Axios + 自定义API层
- 国际化: @coze-arch/i18n
- 构建工具: Rsbuild
路由系统设计
主路由配置
文件位置:frontend/apps/coze-studio/src/routes/index.tsx
核心代码:
typescript
export const router: ReturnType<typeof createBrowserRouter> =
createBrowserRouter([
{
path: '/',
Component: Layout,
errorElement: <GlobalError />,
children: [
{
index: true,
element: <Navigate to="/space" replace />,
},
// 工作空间路由
{
path: 'space',
Component: SpaceLayout,
loader: () => ({
hasSider: true,
requireAuth: true,
subMenu: spaceSubMenu,
menuKey: BaseEnum.Space,
}),
children: [
{
path: ':space_id',
Component: SpaceIdLayout,
children: [
{
index: true,
element: <Navigate to="develop" replace />,
},
// 项目开发页面
{
path: 'develop',
Component: Develop,
loader: () => ({
subMenuKey: SpaceSubModuleEnum.DEVELOP,
}),
},
// 智能体IDE
{
path: 'bot/:bot_id',
Component: AgentIDELayout,
// ...
},
// 项目IDE
{
path: 'project-ide/:project_id/*',
Component: ProjectIDE,
// ...
},
// 资源库
{
path: 'library',
Component: Library,
// ...
},
],
},
],
},
],
},
]);
代码作用:
这段代码是Coze Studio应用的 核心路由配置 ,使用React Router v6的 createBrowserRouter 创建了一个层次化的路由系统。主要作用包括:
路由结构设计
根路由 ( / ) :
- 使用 Layout 组件作为整体布局容器
- 配置了 GlobalError 作为全局错误边界
- 默认重定向到 /space 工作空间
工作空间路由 ( /space ) :
- 使用 SpaceLayout 组件提供工作空间布局
- 通过 loader 配置页面属性:侧边栏显示、身份验证要求、子菜单等
- 支持嵌套的子路由结构
具体空间路由 ( /space/:space_id ) :
- 使用动态参数 :space_id 标识具体的工作空间
- SpaceIdLayout 组件管理特定空间的布局
- 默认重定向到 develop 开发页面
这种设计的优势:
- 层次清晰:每一层负责不同的布局和权限控制
- 参数传递:通过URL参数自然传递spaceId等关键信息
- 懒加载:支持按需加载不同功能模块
- 权限控制:在loader中统一处理认证和权限检查
核心组件分析
SpaceLayout组件
文件位置:frontend/packages/foundation/space-ui-adapter/src/components/space-layout/index.tsx
核心代码:
typescript
export const SpaceLayout = () => {
const { space_id } = useParams();
const { loading, spaceListLoading, spaceList } = useInitSpace(space_id);
if (!loading && !spaceListLoading && spaceList.length === 0) {
return (
<Empty
className="h-full justify-center w-full"
image={<IconCozIllusAdd width="160" height="160" />}
title={I18n.t('enterprise_workspace_no_space_title')}
description={I18n.t('enterprise_workspace_default_tips1_nonspace')}
/>
);
}
if (loading) {
return null;
}
return <Outlet />;
};
组件职责:
- 空间初始化:通过useInitSpace hook初始化工作空间
- 状态处理:处理加载状态和空状态
- 布局渲染:为子路由提供布局容器
Develop组件(项目开发页面)
文件位置:frontend/packages/studio/workspace/entry-adapter/src/pages/develop/index.tsx
核心代码:
typescript
export const Develop: FC<DevelopProps> = ({ spaceId }) => {
const isPersonal = useSpaceStore(
state => state.space.space_type === SpaceType.Personal,
);
// 关键词搜索和筛选
const [filterParams, setFilterParams, debouncedSetSearchValue] =
useCachedQueryParams();
const {
listResp: { loading, data, loadingMore, mutate, noMore, reload },
containerRef,
} = useIntelligenceList({
params: {
spaceId,
searchValue: filterParams.searchValue,
types: getTypeRequestParams({
type: filterParams.searchType,
}),
hasPublished: getPublishRequestParam(filterParams.isPublish),
recentlyOpen: filterParams.recentlyOpen,
searchScope: filterParams.searchScope,
orderBy: filterParams.isPublish
? search.OrderBy.PublishTime
: search.OrderBy.UpdateTime,
},
});
return (
<Layout>
<Header>
<HeaderTitle>
<span>{I18n.t('workspace_develop')}</span>
</HeaderTitle>
<HeaderActions>
<Button icon={<IconCozPlus />} onClick={actions.createIntelligence}>
{I18n.t('workspace_create')}
</Button>
</HeaderActions>
</Header>
{/* 渲染项目列表 */}
</Layout>
);
};
组件特点:
- 状态管理:使用useSpaceStore获取当前空间信息
- 参数缓存:通过useCachedQueryParams缓存搜索参数
- 无限滚动:支持大量项目的高效展示
- 实时搜索:支持关键词、类型、创建者等多维度筛选
BotCard组件(项目卡片)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/develop/components/bot-card/index.tsx
核心代码:
typescript
export interface BotCardProps {
intelligenceInfo: IntelligenceData;
timePrefixType?: 'recentOpen' | 'publish' | 'edit';
onClick?: (() => true) | (() => void);
onDelete?: (param: {
name: string;
id: string;
type: IntelligenceType;
}) => void;
onCopyProject?: (basicInfo: IntelligenceBasicInfo) => void;
onCopyAgent?: AgentCopySuccessCallback;
onUpdateIntelligenceInfo: (info: IntelligenceData) => void;
onRetryCopy: (basicInfo: IntelligenceBasicInfo) => void;
onCancelCopyAfterFailed: (basicInfo: IntelligenceBasicInfo) => void;
}
export const BotCard: React.FC<BotCardProps> = ({
intelligenceInfo,
timePrefixType,
onClick,
onDelete,
onUpdateIntelligenceInfo,
// ... 其他props
}) => {
const navigate = useNavigate();
const {
basic_info,
type,
permission_info: { in_collaboration, can_delete } = {},
publish_info: { publish_time, connectors, has_published } = {},
owner_info,
favorite_info: { is_fav } = {},
} = intelligenceInfo;
const { id, name, icon_url, space_id, description, update_time, status } =
basic_info ?? {};
const handleCardClick = () => {
if (onClick?.()) {
return;
}
if (type === IntelligenceType.Bot) {
navigate(`/space/${space_id}/bot/${id}`);
} else if (type === IntelligenceType.Project) {
navigate(`/space/${space_id}/project-ide/${id}`);
}
};
return (
<Card
className="bot-card"
hoverable
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleCardClick}
>
<div className="card-header">
<Avatar src={intelligence.icon_url} size={40} />
<div className="card-info">
<div className="card-title">{intelligence.name}</div>
<div className="card-description">{intelligence.description}</div>
</div>
{isHovered && (
<Dropdown menu={{ items: actions }} trigger={['click']}>
<Button icon={<MoreOutlined />} type="text" />
</Dropdown>
)}
</div>
<div className="card-footer">
<StatusBadge status={intelligence.status} />
<div className="card-meta">
<span>{intelligence.owner_name}</span>
<span>{formatTime(intelligence.update_time)}</span>
</div>
</div>
</Card>
);
};
组件功能:
- 交互设计:悬停显示操作菜单,点击进入详情
- 状态展示:显示项目状态、所有者、更新时间等信息
- 事件追踪:集成埋点系统,追踪用户行为
- 动作支持:支持收藏、复制、删除等操作
业务适配层
useInitSpace Hook
文件位置:frontend/packages/foundation/space-ui-base/src/hooks/use-init-space.ts
typescript
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(() => {
(async (spaceId?: string) => {
try {
if (!isReady) {
return;
}
// 如果没有指定spaceId,跳转到后备空间的项目开发子路由
if (!spaceId) {
// 拉取空间列表
await useSpaceStore.getState().fetchSpaces(true);
// 获取个人空间ID
const personalSpaceID = useSpaceStore.getState().getPersonalSpaceID();
// 空间列表中的第一个空间
const firstSpaceID = useSpaceStore.getState().spaceList[0]?.id;
// 未指定spaceId时的后备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)) {
// 当在空间列表中找不到该空间id时抛出错误
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 };
};
核心功能解析:
- 空间检查: 检查URL中的spaceId是否有效
- 自动跳转: 如果没有指定空间,自动跳转到个人空间或第一个可用空间
- 错误处理: 处理空间不存在或无权限访问的情况
- 状态同步: 更新全局的空间状态
useIntelligenceList Hook
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/develop/hooks/use-intelligence-list.ts
typescript
import { intelligenceApi } from '@coze-arch/bot-api';
import { type DraftIntelligenceList } from '../type';
const getIntelligenceList = async (
dataSource: DraftIntelligenceList | undefined,
{
spaceId,
types,
searchValue,
hasPublished,
recentlyOpen,
searchScope,
orderBy,
}: FilterParamsType,
cancelTokenRef: React.MutableRefObject<CancelTokenSource | null>,
) => {
// 每次发起新请求时重置取消令牌
const source = axios.CancelToken.source();
cancelTokenRef.current = source;
const resp = await intelligenceApi
.GetDraftIntelligenceList(
{
space_id: spaceId,
name: searchValue,
types,
size: pageSize,
has_published: hasPublished,
recently_open: recentlyOpen,
cursor_id: dataSource?.nextCursorId,
search_scope: searchScope,
order_by: orderBy,
status: [
IntelligenceStatus.Using,
IntelligenceStatus.Banned,
IntelligenceStatus.MoveFailed,
],
},
{ cancelToken: source.token, __disableErrorToast: true },
)
.catch(e => {
if (e.message !== 'canceled') {
Toast.error({
content: withSlardarIdButton(e.msg || e.message || I18n.t('error')),
showClose: false,
});
}
});
if (resp?.data) {
return {
list: resp.data.intelligences ?? [],
hasMore: Boolean(resp.data.has_more),
nextCursorId: resp.data.next_cursor_id,
};
} else {
return {
list: [],
hasMore: false,
nextCursorId: undefined,
};
}
};
export const useIntelligenceList = ({
params: {
spaceId,
types,
searchValue,
hasPublished,
recentlyOpen,
searchScope,
orderBy,
},
onBefore,
onSuccess,
onError,
}: {
params: FilterParamsType;
} & Pick<
InfiniteScrollOptions<DraftIntelligenceList>,
'onBefore' | 'onSuccess' | 'onError'
>) => {
const containerRef = useRef<HTMLDivElement>(null);
const cancelTokenRef = useRef<CancelTokenSource | null>(null);
const listResp = useInfiniteScroll<DraftIntelligenceList>(
async dataSource =>
await getIntelligenceList(
dataSource,
{
spaceId,
types,
searchValue,
hasPublished,
recentlyOpen,
searchScope,
orderBy,
},
cancelTokenRef,
),
{
target: containerRef,
reloadDeps: [
types.join(','),
searchValue,
hasPublished,
recentlyOpen,
searchScope,
orderBy,
spaceId,
],
isNoMore: dataSource => !dataSource?.hasMore,
onBefore: () => {
if (listResp.loadingMore || listResp.loading) {
cancelTokenRef.current?.cancel();
}
getBotListReportEvent.start();
onBefore?.();
},
onSuccess: (...res) => {
getBotListReportEvent.success();
onSuccess?.(...res);
},
onError: e => {
getBotListReportEvent.error({
error: e,
reason: e.message,
});
onError?.(e);
},
},
);
useEffect(
() => () => {
// 取消请求的接口
cancelTokenRef.current?.cancel();
},
[spaceId],
);
return { listResp, containerRef, cancelTokenRef };
};
核心功能解析:
- 无限滚动 : 使用
useInfiniteScroll
实现分页加载 - 请求取消 : 使用
CancelToken
避免重复请求 - 错误处理: 统一的错误处理和用户提示
- 性能优化: 依赖数组控制重新请求时机
- 事件上报: 集成埋点上报功能
bot-api/package.json
文件位置:frontend/packages/arch/bot-api/package.json
核心代码:
json
{
"name": "@coze-arch/bot-api",
"version": "0.0.1",
"description": "RPC wrapper for bot studio application",
"author": "fanwenjie.fe@bytedance.com",
"exports": {
".": "./src/index.ts",
},
}
代码作用:
- 1.包定义 :定义了一个名为 @coze-arch/bot-api 的 npm 包,版本为 0.0.1,这是一个用于 bot studio 应用的 RPC 包装器。
- 2.通过主入口文件 :
在frontend\packages\arch\bot-api\src\index.ts
中, patPermissionApi 被导出:
typescript
export { intelligenceApi } from './intelligence-api';
这允许通过 @coze-arch/bot-api 直接导入 intelligenceApi 。
3.intelligenceApi 实现 :在 src/intelligence-api.ts 中, intelligenceApi 是一个配置好的服务实例,它使用了 IntelligenceApiService 和 axios 请求配置。
src/pat-permission-api.ts
文件位置:frontend\packages\arch\bot-api\src\intelligence-api.ts
核心代码:
typescript
import IntelligenceApiService from './idl/intelligence_api';
import { axiosInstance, type BotAPIRequestConfig } from './axios';
export const intelligenceApi = new IntelligenceApiService<BotAPIRequestConfig>({
request: (params, config = {}) =>
axiosInstance.request({
...params,
...config,
headers: { ...params.headers, ...config.headers, 'Agw-Js-Conv': 'str' },
}),
});
代码含义详解
这段代码是创建一个 IntelligenceApiService
实例的构造函数调用,具体含义如下:
typescript
IntelligenceApiService<BotAPIRequestConfig>({
request: (params, config = {}) =>
axiosInstance.request({ ...params, ...config }),
})
- 泛型参数
IntelligenceApiService<BotAPIRequestConfig>
:这是一个泛型类,BotAPIRequestConfig
是类型参数BotAPIRequestConfig
定义了业务层的自定义 axios 配置类型,包含__disableErrorToast
等业务特定字段
- 构造函数参数
传入一个配置对象,包含request
函数:
typescript
{
request: (params, config = {}) =>
axiosInstance.request({ ...params, ...config })
}
- request 函数解析
这是一个依赖注入的设计模式:
-
参数说明:
params
:包含 HTTP 请求的基本参数(url、method、data、headers 等)config
:可选的额外配置,默认为空对象
-
函数体:
{ ...params, ...config }
:使用展开运算符合并参数axiosInstance.request()
:调用 axios 实例的 request 方法发送 HTTP 请求
- 依赖注入模式
typescript
// IDL 生成的服务类不直接依赖具体的 HTTP 库
class IntelligenceApiService<T> {
private request: any;
constructor(options?: { request?: Function }) {
this.request = options?.request || this.request;
}
}
- 适配器模式
typescript
// 将 axiosInstance.request 适配为 IDL 服务所需的接口
request: (params, config) => axiosInstance.request({ ...params, ...config })
-
数据流转过程
-
业务调用 :
intelligenceApi .GetDraftIntelligenceList
-
参数组装:IDL 生成的方法将业务参数转换为标准 HTTP 参数
-
请求发送 :调用注入的
request
函数 -
HTTP 请求 :最终通过
axiosInstance.request
发送请求 -
优势
- 解耦:IDL 生成的代码不直接依赖 axios,便于测试和替换
- 类型安全:通过泛型确保配置类型的一致性
- 可扩展 :可以在
request
函数中添加业务逻辑(如错误处理、认证等) - 统一性 :所有 API 调用都通过相同的
request
函数,便于统一管理
- 实际效果
当调用 GetDraftIntelligenceList
时:
typescript
// 1. IDL 生成的方法
GetDraftIntelligenceList(req, options) {
const params = { url: '/api/...', method: 'POST', data: {...} };
return this.request(params, options); // 调用注入的 request 函数
}
// 2. 注入的 request 函数
(params, config) => {
// params = { url: '/api/...', method: 'POST', data: {...} }
// config = options (可能包含 __disableErrorToast 等)
return axiosInstance.request({ ...params, ...config });
}
这种设计确保了代码的模块化、可测试性和可维护性。
axiosInstance说明
1.axiosInstance 在整个项目中是全局共享的
2.bot-api 包中的导入 ( frontend/packages/arch/bot-api/src/axios.ts )
是直接从 @coze-arch/bot-http 包导入了 axiosInstance 。
typescript
import {
axiosInstance,
isApiError,
type AxiosRequestConfig,
} from '@coze-arch/bot-http';
3.bot-http 包中的定义 ( frontend/packages/arch/bot-http/src/axios.ts ):
typescript
export const axiosInstance = axios.create();
这里创建了一个全局的 axios 实例,与用户名修改保存请求的 axios 实例是同一个。
IntelligenceApiService说明
1.bot-api包中的导入路径:
import IntelligenceApiService from './idl/intelligence_api';
实际指向
frontend/packages/arch/bot-api/src/idl/intelligence_api.ts
文件内容重新导出了 @coze-arch/idl/intelligence_api 包的所有内容,包括默认导出
typescript
export * from '@coze-arch/idl/intelligence_api';
export { default as default } from '@coze-arch/idl/intelligence_api';
2.idl包的模块映射
文件位置:frontend/packages/arch/idl/package.json
核心代码:
json
"name": "@coze-arch/idl",
"version": "0.0.1",
"description": "IDL files for bot studio application",
"author": "fanwenjie.fe@bytedance.com",
"exports": {
"./intelligence_api": "./src/auto-generated/intelligence_api/index.ts",
代码作用:将 @coze-arch/idl/intelligence_api 映射到实际文件路径frontend/packages/arch/idl/src/auto-generated/intelligence_api/index.ts
这个文件说明后续见 项目开发智能查询服务-API接口实现 这个章节。
API层设计与实现
IDL基础类型定义(base.thrift)
文件位置:idl/base.thrift
核心代码:
thrift
namespace py base
namespace go base
namespace java com.bytedance.thrift.base
struct TrafficEnv {
1: bool Open = false,
2: string Env = "" ,
}
struct Base {
1: string LogID = "",
2: string Caller = "",
3: string Addr = "",
4: string Client = "",
5: optional TrafficEnv TrafficEnv ,
6: optional map<string,string> Extra ,
}
struct BaseResp {
1: string StatusMessage = "",
2: i32 StatusCode = 0 ,
3: optional map<string,string> Extra ,
}
struct EmptyReq {
}
struct EmptyData {}
struct EmptyResp {
1: i64 code,
2: string msg ,
3: EmptyData data,
}
struct EmptyRpcReq {
255: optional Base Base,
}
struct EmptyRpcResp {
255: optional BaseResp BaseResp,
}
文件作用:
定义了项目中所有接口的基础数据结构,作为其他IDL文件的依赖基础。
项目开发智能查询服务-IDL结构体定义(search.thrift)
文件路径:idl\app\search.thrift
核心代码:
thrift
namespace go app.intelligence
include "../base.thrift"
include "common_struct/intelligence_common_struct.thrift"
include "common_struct/common_struct.thrift"
struct GetDraftIntelligenceListOption {
1: bool need_replica, //need personal version Bot data
}
struct GetDraftIntelligenceListRequest {
1: required i64 space_id (agw.js_conv="str", api.js_conv="true"),
2: optional string name,
3: optional bool has_published,
4: optional list<intelligence_common_struct.IntelligenceStatus> status,
5: optional list<intelligence_common_struct.IntelligenceType> types,
6: optional SearchScope search_scope,
51: optional bool is_fav,
52: optional bool recently_open,
99: optional GetDraftIntelligenceListOption option,
100: optional OrderBy order_by,
101: optional string cursor_id,
102: optional i32 size,
255: optional base.Base Base
}
struct IntelligencePublishInfo {
1: string publish_time,
2: bool has_published,
3: list<common_struct.ConnectorInfo> connectors,
}
struct IntelligencePermissionInfo {
1: bool in_collaboration,
2: bool can_delete, // can delete
3: bool can_view, // Whether the current user can view it, the current judgment logic is whether the user is in the space where the bot is located
}
struct FavoriteInfo {
1: bool is_fav, // Whether to collect; use the collection list
2: string fav_time, // Collection time; collection list use
}
enum BotMode {
SingleMode = 0
MultiMode = 1
WorkflowMode = 2
}
struct OtherInfo {
1: string recently_open_time, // Last opened time; used when recently opened filter
2: BotMode bot_mode, // Only bot type returns
}
struct Intelligence {
1: intelligence_common_struct.IntelligenceBasicInfo basic_info, // Basic information
2: intelligence_common_struct.IntelligenceType type, // Agent Type
3: IntelligencePublishInfo publish_info, // Agent publishes information, optional
4: common_struct.User owner_info, // Agent owner information, optional
5: IntelligencePermissionInfo permission_info, // The current user's permission information to the agent, optional
}
// For the front end
struct IntelligenceData {
1: intelligence_common_struct.IntelligenceBasicInfo basic_info,
2: intelligence_common_struct.IntelligenceType type,
3: IntelligencePublishInfo publish_info,
4: IntelligencePermissionInfo permission_info,
5: common_struct.User owner_info,
6: common_struct.AuditInfo latest_audit_info,
7: FavoriteInfo favorite_info,
50: OtherInfo other_info,
}
struct DraftIntelligenceListData {
1: list<IntelligenceData> intelligences,
2: i32 total,
3: bool has_more,
4: string next_cursor_id,
}
struct GetDraftIntelligenceListResponse {
1: DraftIntelligenceListData data,
253: i32 code,
254: string msg,
255: optional base.BaseResp BaseResp (api.none="true"),
}
源码作用:定义PAT权限添加令牌相关的数据结构
项目开发智能查询服务-IDL接口定义(intelligence.thrift)
文件路径:idl\app\intelligence.thrift
核心代码:
thrift
include "../base.thrift"
include "search.thrift"
include "common_struct/intelligence_common_struct.thrift"
include "common_struct/common_struct.thrift"
namespace go app.intelligence
service IntelligenceService {
search.GetDraftIntelligenceListResponse GetDraftIntelligenceList(1: search.GetDraftIntelligenceListRequest req) (api.post='/api/intelligence_api/search/get_draft_intelligence_list', api.category="search",agw.preserve_base="true")
}
源码作用:项目开发智能查询服务相关的接口
项目开发智能查询服务--结构体实现(search.ts)
文件路径:frontend\packages\arch\idl\src\auto-generated\intelligence_api\namespaces\search.ts
typescript
import * as intelligence_common_struct from './intelligence_common_struct';
import * as common_struct from './common_struct';
import * as base from './base';
import * as ocean_project_common_struct from './ocean_project_common_struct';
export interface DraftIntelligenceListData {
intelligences?: Array<IntelligenceData>;
total?: number;
has_more?: boolean;
next_cursor_id?: string;
}
export interface FavoriteInfo {
/** 是否收藏;收藏列表使用 */
is_fav?: boolean;
/** 收藏时间;收藏列表使用 */
fav_time?: string;
}
export interface GetDraftIntelligenceListOption {
/** 是否需要个人版本Bot数据 */
need_replica?: boolean;
}
export interface GetDraftIntelligenceListRequest {
space_id: string;
name?: string;
has_published?: boolean;
status?: Array<intelligence_common_struct.IntelligenceStatus>;
types?: Array<intelligence_common_struct.IntelligenceType>;
search_scope?: SearchScope;
/** 文件夹id */
folder_id?: string;
/** 是否击穿搜索(一期不支持) */
folder_include_children?: boolean;
order_type?: SortOrderType;
is_fav?: boolean;
recently_open?: boolean;
option?: GetDraftIntelligenceListOption;
order_by?: OrderBy;
cursor_id?: string;
size?: number;
Base?: base.Base;
}
export interface GetDraftIntelligenceListResponse {
data?: DraftIntelligenceListData;
code?: number;
msg?: string;
}
项目开发智能查询服务-API接口实现(intelligence_api\index.ts)
文件位置:frontend\packages\arch\idl\src\auto-generated\intelligence_api\index.ts
核心代码:
typescript
export default class IntelligenceApiService<T = any> extends BaseService<T> {
constructor(options?: {
baseURL?: string | ((path: string) => string);
request?<R>(
params: {
url: string;
method: 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH';
data?: any;
params?: any;
headers?: any;
},
options?: T,
): Promise<R>;
}) {
this.request = options?.request || this.request;
this.baseURL = options?.baseURL || '';
}
GetDraftIntelligenceList(
req: search.GetDraftIntelligenceListRequest,
options?: T,
): Promise<search.GetDraftIntelligenceListResponse> {
const _req = req;
const url = this.genBaseURL(
'/api/intelligence_api/search/get_draft_intelligence_list',
);
const method = 'POST';
const data = {
space_id: _req['space_id'],
name: _req['name'],
has_published: _req['has_published'],
status: _req['status'],
types: _req['types'],
search_scope: _req['search_scope'],
folder_id: _req['folder_id'],
folder_include_children: _req['folder_include_children'],
order_type: _req['order_type'],
is_fav: _req['is_fav'],
recently_open: _req['recently_open'],
option: _req['option'],
order_by: _req['order_by'],
cursor_id: _req['cursor_id'],
size: _req['size'],
Base: _req['Base'],
};
return this.request({ url, method, data }, options);
}
// ... 其他API方法
}
代码作用:IntelligenceApiService
类有成员函数 GetDraftIntelligenceList 。
这个方法用于创建PAT权限添加令牌。
此文件是基于intelligence.thrift自动生成的,开发者无需手动修改。
IDL文件解析器分析结论
通过深入分析Coze Studio项目的IDL架构,我可以确认**intelligence.thrift
和passport.thrift
使用相同的Thrift Parser**。
关键发现
-
统一的IDL工具链 :项目使用
@coze-arch/idl2ts-cli
作为统一的IDL到TypeScript转换工具,该工具支持处理所有Thrift文件。 -
共享基础结构:
- 两个文件都位于统一的
coze-studio\idl
目录下 - 两个文件都引用了共享的
base.thrift
文件 - 使用相同的namespace和结构体定义规范
- 两个文件都位于统一的
-
统一的代码生成流程:
frontend\packages\arch\api-schema\api.config.js
配置了passport.thrift
的生成frontend\packages\arch\idl\package.json
包含了intelligence.thrift
的自动生成代码- 两者都使用相同的
idl2ts
工具链进行代码生成
-
相同的输出格式:生成的TypeScript代码都遵循相同的结构和命名约定,包含相同的注释头和类型定义格式。
结论
intelligence.thrift
和passport.thrift
确实使用相同的Thrift Parser(@coze-arch/idl2ts-cli
),它们共享相同的解析规则、代码生成逻辑和输出格式。这确保了整个项目中IDL文件处理的一致性和兼容性。
@coze-arch/idl2ts-cli 工具详细信息
工具名称
@coze-arch/idl2ts-cli
详细地址
项目路径 :frontend/infra/idl/idl2ts-cli/
工具详细信息
版本:0.1.7
描述:IDL(Interface Definition Language)到TypeScript的转换工具
主要功能:
- gen命令:从Thrift或Protocol Buffer文件生成API代码
- filter命令:生成过滤后的API类型定义
可执行文件 :idl2ts
(位于 ./src/cli.js
)
最终调用的是frontend/infra/idl/idl2ts-cli/src/cli.ts
这个文件
核心依赖:
@coze-arch/idl2ts-generator
:代码生成器@coze-arch/idl2ts-helper
:辅助工具@coze-arch/idl2ts-plugin
:插件系统commander
:命令行界面prettier
:代码格式化
使用方式:
bash
# 生成API代码
idl2ts gen <projectRoot> [-f --format-config <formatConfig>]
# 生成过滤类型
idl2ts filter <projectRoot> [-f --format-config <formatConfig>]
许可证:Apache-2.0
作者:fanwenjie.fe@bytedance.com
这个工具是Coze Studio项目中统一处理所有IDL文件(包括intelligence.thrift
和passport.thrift
)的核心工具,确保了整个项目中API代码生成的一致性。
状态管理机制
工作空间状态存储
文件位置:frontend/packages/foundation/space-store/src/space/index.ts
typescript
interface SpaceStoreState {
/** @deprecated try useSpace instead */
space: BotSpace;
spaceList: BotSpace[];
recentlyUsedSpaceList: BotSpace[];
loading: false | Promise<SpaceInfo | undefined>;
inited?: boolean;
createdTeamSpaceNum: number;
}
interface SpaceStoreAction {
reset: () => void;
/** @deprecated get id from url */
getSpaceId: () => string;
getPersonalSpaceID: () => string | undefined;
checkSpaceID: (spaceID: string) => boolean;
/** @deprecated by id index */
setSpace: (spaceId?: string, isBotDetailIframe?: boolean) => void | never;
fetchSpaces: (refresh?: boolean) => Promise<SpaceInfo | undefined>;
updateSpace: (space: Partial<BotSpace>) => void;
}
export const useSpaceStore = create<SpaceStoreState & SpaceStoreAction>(
(set, get) => ({
// 状态
space: {} as BotSpace,
spaceList: [],
recentlyUsedSpaceList: [],
loading: false,
inited: false,
createdTeamSpaceNum: 0,
// 操作
reset: () => {
set({
space: {} as BotSpace,
spaceList: [],
recentlyUsedSpaceList: [],
loading: false,
inited: false,
createdTeamSpaceNum: 0,
});
},
getSpaceId: () => {
return get().space.id || '';
},
getPersonalSpaceID: () => {
const spaceList = get().spaceList;
return spaceList.find(space => space.space_type === SpaceType.Personal)?.id;
},
checkSpaceID: (spaceID: string) => {
const spaceList = get().spaceList;
return spaceList.some(space => space.id === spaceID);
},
setSpace: (spaceId?: string) => {
if (!spaceId) return;
const spaceList = get().spaceList;
const targetSpace = spaceList.find(space => space.id === spaceId);
if (targetSpace) {
set({ space: targetSpace });
// 保存到本地存储
localStorageService.setValue('workspace-spaceId', spaceId);
}
},
fetchSpaces: async (refresh = false) => {
if (get().loading && !refresh) {
return get().loading as Promise<SpaceInfo | undefined>;
}
const promise = (async () => {
try {
const response = await spaceApi.GetSpaceList({});
const spaceList = response.data?.spaces || [];
set({
spaceList,
inited: true,
loading: false,
});
return response.data;
} catch (error) {
set({ loading: false });
throw error;
}
})();
set({ loading: promise });
return promise;
},
updateSpace: (spaceUpdate: Partial<BotSpace>) => {
const currentSpace = get().space;
if (currentSpace.id === spaceUpdate.id) {
set({ space: { ...currentSpace, ...spaceUpdate } });
}
const spaceList = get().spaceList.map(space =>
space.id === spaceUpdate.id ? { ...space, ...spaceUpdate } : space
);
set({ spaceList });
},
})
);
状态管理特点:
- 集中管理: 使用Zustand进行集中式状态管理
- 本地持久化: 将当前空间ID保存到localStorage
- 异步处理: 支持异步获取空间列表
- 状态同步: 提供更新和重置功能
- 类型安全: 完整的TypeScript类型定义
用户体验优化
1. 加载状态管理
typescript
// 骨架屏加载
const LoadingSkeleton = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="bg-white rounded-lg border border-gray-200 p-4">
<div className="flex items-center space-x-3 mb-3">
<Skeleton className="w-10 h-10 rounded-lg" />
<div className="flex-1">
<Skeleton className="h-4 w-3/4 mb-2" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
<Skeleton className="h-3 w-full mb-2" />
<Skeleton className="h-3 w-2/3" />
</div>
))}
</div>
);
// 加载更多指示器
const LoadingMore = () => (
<div className="flex justify-center py-4">
<Spinner size="sm" />
<span className="ml-2 text-sm text-gray-500">
{I18n.t('loading_more')}
</span>
</div>
);
2. 空状态处理
typescript
const EmptyState = () => (
<div className="flex flex-col items-center justify-center py-12">
<IconEmpty className="w-16 h-16 text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{I18n.t('workspace_develop_empty_title')}
</h3>
<p className="text-gray-500 text-center mb-6 max-w-md">
{I18n.t('workspace_develop_empty_description')}
</p>
<CreateButton variant="primary" />
</div>
);
3. 错误处理机制
typescript
// 全局错误边界
const ErrorBoundary = ({ children }: { children: React.ReactNode }) => {
return (
<ReactErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div className="flex flex-col items-center justify-center py-12">
<IconError className="w-16 h-16 text-red-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
{I18n.t('something_went_wrong')}
</h3>
<p className="text-gray-500 text-center mb-6">
{error.message}
</p>
<Button onClick={resetErrorBoundary}>
{I18n.t('try_again')}
</Button>
</div>
)}
onError={(error, errorInfo) => {
logger.error('Workspace error:', error, errorInfo);
}}
>
{children}
</ReactErrorBoundary>
);
};
4. 性能优化策略
typescript
// 虚拟滚动优化(大量数据时)
const VirtualizedGrid = ({ items, renderItem }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = throttle(() => {
const scrollTop = container.scrollTop;
const containerHeight = container.clientHeight;
const itemHeight = 200; // 估算的卡片高度
const itemsPerRow = Math.floor(container.clientWidth / 300); // 估算每行卡片数
const start = Math.floor(scrollTop / itemHeight) * itemsPerRow;
const end = start + Math.ceil(containerHeight / itemHeight) * itemsPerRow + itemsPerRow;
setVisibleRange({ start: Math.max(0, start), end: Math.min(items.length, end) });
}, 100);
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, [items.length]);
const visibleItems = items.slice(visibleRange.start, visibleRange.end);
return (
<div ref={containerRef} className="h-full overflow-auto">
<div style={{ height: Math.ceil(items.length / 4) * 200 }}>
<div
style={{
transform: `translateY(${Math.floor(visibleRange.start / 4) * 200}px)`
}}
className="grid grid-cols-4 gap-6"
>
{visibleItems.map(renderItem)}
</div>
</div>
</div>
);
};
// 图片懒加载
const LazyImage = ({ src, alt, className }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} className={className}>
{isInView && (
<img
src={src}
alt={alt}
className={`transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={() => setIsLoaded(true)}
/>
)}
{!isLoaded && (
<div className="bg-gray-200 animate-pulse w-full h-full" />
)}
</div>
);
};
各组件之间的调用关系
路由层 (routes/index.tsx)
↓ 渲染
布局层 (SpaceLayout → SpaceIdLayout)
↓ 初始化
状态管理层 (useInitSpace → useSpaceStore)
↓ 数据获取
页面组件层 (Develop)
↓ 调用
业务逻辑层 (useIntelligenceList)
↓ 请求
API层 (intelligenceApi.GetDraftIntelligenceList)
↓ 渲染
展示组件层 (BotCard)
这种分层设计确保了:
- 职责清晰:每个层级专注于特定的功能职责
- 数据流向:单向数据流,便于调试和维护
- 组件复用:底层组件可以在不同场景中复用
- 状态隔离:不同层级的状态相互独立
详细调用流程
- 路由匹配 :用户访问
/space/:space_id/develop
时,React Router匹配到对应路由 - 布局初始化 :SpaceLayout组件调用
useInitSpace
检查和初始化工作空间 - 状态同步 :
useInitSpace
通过useSpaceStore
管理工作空间状态 - 页面渲染:Develop组件根据当前空间ID渲染项目开发页面
- 数据获取 :
useIntelligenceList
调用API获取项目列表数据 - 组件展示:BotCard组件渲染每个项目的卡片信息
总结
Coze Studio的工作空间项目开发系统展现了现代前端应用的最佳实践:
- 模块化架构: 将工作空间功能拆分为独立的包,实现了高内聚、低耦合的设计
- 智能路由: 实现了自动跳转和错误处理的智能路由系统
- 状态管理: 使用Zustand进行高效的状态管理,支持本地持久化
- 数据获取: 实现了无限滚动、请求取消、错误处理的完善数据获取机制
- 用户体验: 提供了加载状态、空状态、错误状态的完整用户体验
- 性能优化: 采用虚拟滚动、图片懒加载、组件懒加载等性能优化策略
- 类型安全: 全面使用TypeScript,基于IDL自动生成API类型定义
- 响应式设计: 实现了适配不同屏幕尺寸的响应式布局
- 国际化支持: 完整的多语言支持系统
- 错误边界: 实现了完善的错误边界和错误处理机制
- 事件上报: 集成了完整的用户行为和错误上报系统
- 组件复用: 通过适配器模式实现了组件的高度复用
这套工作空间系统的设计思路和实现方式,为构建复杂的企业级前端应用提供了很好的参考价值。通过合理的架构设计和技术选型,实现了功能完整、性能优秀、用户体验良好的工作空间管理系统。整个系统从路由层到组件层都有完善的设计,体现了现代前端工程化的最佳实践,特别是在大型项目的模块化管理、状态同步、数据获取等方面提供了优秀的解决方案。