Coze源码分析-工作空间-项目开发-前端源码

前言

本文将深入分析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 };
};

核心功能解析

  1. 空间检查: 检查URL中的spaceId是否有效
  2. 自动跳转: 如果没有指定空间,自动跳转到个人空间或第一个可用空间
  3. 错误处理: 处理空间不存在或无权限访问的情况
  4. 状态同步: 更新全局的空间状态

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 };
};

核心功能解析

  1. 无限滚动 : 使用useInfiniteScroll实现分页加载
  2. 请求取消 : 使用CancelToken避免重复请求
  3. 错误处理: 统一的错误处理和用户提示
  4. 性能优化: 依赖数组控制重新请求时机
  5. 事件上报: 集成埋点上报功能
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 }),
})
  1. 泛型参数
  • IntelligenceApiService<BotAPIRequestConfig>:这是一个泛型类,BotAPIRequestConfig 是类型参数
  • BotAPIRequestConfig 定义了业务层的自定义 axios 配置类型,包含 __disableErrorToast 等业务特定字段
  1. 构造函数参数
    传入一个配置对象,包含 request 函数:
typescript 复制代码
{
  request: (params, config = {}) => 
    axiosInstance.request({ ...params, ...config })
}
  1. request 函数解析
    这是一个依赖注入的设计模式:
  • 参数说明

    • params:包含 HTTP 请求的基本参数(url、method、data、headers 等)
    • config:可选的额外配置,默认为空对象
  • 函数体

    • { ...params, ...config }:使用展开运算符合并参数
    • axiosInstance.request():调用 axios 实例的 request 方法发送 HTTP 请求
  1. 依赖注入模式
typescript 复制代码
// IDL 生成的服务类不直接依赖具体的 HTTP 库
class IntelligenceApiService<T> {
  private request: any;
  
  constructor(options?: { request?: Function }) {
    this.request = options?.request || this.request;
  }
}
  1. 适配器模式
typescript 复制代码
// 将 axiosInstance.request 适配为 IDL 服务所需的接口
request: (params, config) => axiosInstance.request({ ...params, ...config })
  1. 数据流转过程

  2. 业务调用intelligenceApi .GetDraftIntelligenceList

  3. 参数组装:IDL 生成的方法将业务参数转换为标准 HTTP 参数

  4. 请求发送 :调用注入的 request 函数

  5. HTTP 请求 :最终通过 axiosInstance.request 发送请求

  6. 优势

  • 解耦:IDL 生成的代码不直接依赖 axios,便于测试和替换
  • 类型安全:通过泛型确保配置类型的一致性
  • 可扩展 :可以在 request 函数中添加业务逻辑(如错误处理、认证等)
  • 统一性 :所有 API 调用都通过相同的 request 函数,便于统一管理
  1. 实际效果

当调用 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.thriftpassport.thrift使用相同的Thrift Parser**。

关键发现

  1. 统一的IDL工具链 :项目使用@coze-arch/idl2ts-cli作为统一的IDL到TypeScript转换工具,该工具支持处理所有Thrift文件。

  2. 共享基础结构

    • 两个文件都位于统一的coze-studio\idl目录下
    • 两个文件都引用了共享的base.thrift文件
    • 使用相同的namespace和结构体定义规范
  3. 统一的代码生成流程

    • frontend\packages\arch\api-schema\api.config.js配置了passport.thrift的生成
    • frontend\packages\arch\idl\package.json包含了intelligence.thrift的自动生成代码
    • 两者都使用相同的idl2ts工具链进行代码生成
  4. 相同的输出格式:生成的TypeScript代码都遵循相同的结构和命名约定,包含相同的注释头和类型定义格式。

结论

intelligence.thriftpassport.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的转换工具

主要功能

  1. gen命令:从Thrift或Protocol Buffer文件生成API代码
  2. 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.thriftpassport.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 });
    },
  })
);

状态管理特点

  1. 集中管理: 使用Zustand进行集中式状态管理
  2. 本地持久化: 将当前空间ID保存到localStorage
  3. 异步处理: 支持异步获取空间列表
  4. 状态同步: 提供更新和重置功能
  5. 类型安全: 完整的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)

这种分层设计确保了:

  • 职责清晰:每个层级专注于特定的功能职责
  • 数据流向:单向数据流,便于调试和维护
  • 组件复用:底层组件可以在不同场景中复用
  • 状态隔离:不同层级的状态相互独立

详细调用流程

  1. 路由匹配 :用户访问/space/:space_id/develop时,React Router匹配到对应路由
  2. 布局初始化 :SpaceLayout组件调用useInitSpace检查和初始化工作空间
  3. 状态同步useInitSpace通过useSpaceStore管理工作空间状态
  4. 页面渲染:Develop组件根据当前空间ID渲染项目开发页面
  5. 数据获取useIntelligenceList调用API获取项目列表数据
  6. 组件展示:BotCard组件渲染每个项目的卡片信息

总结

Coze Studio的工作空间项目开发系统展现了现代前端应用的最佳实践:

  1. 模块化架构: 将工作空间功能拆分为独立的包,实现了高内聚、低耦合的设计
  2. 智能路由: 实现了自动跳转和错误处理的智能路由系统
  3. 状态管理: 使用Zustand进行高效的状态管理,支持本地持久化
  4. 数据获取: 实现了无限滚动、请求取消、错误处理的完善数据获取机制
  5. 用户体验: 提供了加载状态、空状态、错误状态的完整用户体验
  6. 性能优化: 采用虚拟滚动、图片懒加载、组件懒加载等性能优化策略
  7. 类型安全: 全面使用TypeScript,基于IDL自动生成API类型定义
  8. 响应式设计: 实现了适配不同屏幕尺寸的响应式布局
  9. 国际化支持: 完整的多语言支持系统
  10. 错误边界: 实现了完善的错误边界和错误处理机制
  11. 事件上报: 集成了完整的用户行为和错误上报系统
  12. 组件复用: 通过适配器模式实现了组件的高度复用

这套工作空间系统的设计思路和实现方式,为构建复杂的企业级前端应用提供了很好的参考价值。通过合理的架构设计和技术选型,实现了功能完整、性能优秀、用户体验良好的工作空间管理系统。整个系统从路由层到组件层都有完善的设计,体现了现代前端工程化的最佳实践,特别是在大型项目的模块化管理、状态同步、数据获取等方面提供了优秀的解决方案。

相关推荐
蒋星熠2 小时前
Spring Boot 3.x 微服务架构实战指南
人工智能·spring boot·微服务·性能优化·架构·云计算·量子计算
地平线开发者2 小时前
理想汽车智驾方案介绍专题 3 MoE+Sparse Attention 高效结构解析
人工智能·算法·自动驾驶
yuguo.im3 小时前
Chrome DevTools Performance 是优化前端性能的瑞士军刀
前端·javascript·性能优化·chrome devtools
FSHOW5 小时前
【独立开发日记】MQ端到端类型安全
前端·javascript·后端
老华带你飞5 小时前
社区互助|基于SSM+vue的社区互助平台的设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·社区互助平台
一支鱼5 小时前
前端使用次数最多的工具封装
前端·typescript·编程语言
GIS之路5 小时前
GDAL 简介
前端
飞哥数智坊5 小时前
实测阿里 Qoder,但我还是失望了
人工智能·ai编程
前端工作日常6 小时前
单元测试与E2E测试中使用浏览器的原因及区别
前端·单元测试