概述
本文深入分析Coze Studio中用户API授权编辑令牌功能的前端实现。该功能允许用户安全地编辑已创建的个人访问令牌(Personal Access Token,简称PAT),实现令牌名称的自定义修改,确保API访问的灵活性和令牌管理的便捷性。通过对源码的详细解析,我们将了解其架构设计、组件实现、状态管理和用户体验优化等核心技术要点。
功能特性
核心功能
- 令牌编辑:支持修改已创建令牌的名称信息
- 编辑确认:提供模态框形式的编辑界面,确保操作明确性
- 状态感知:基于令牌状态和用户权限控制编辑操作的可用性
- 即时反馈:编辑操作完成后提供即时的成功反馈
- 列表刷新:编辑成功后自动刷新令牌列表
用户体验特性
- 权限控制:只有令牌创建者且令牌未过期时才能编辑
- 操作指引:通过Tooltip提供清晰的操作说明
- 表单验证:实时验证令牌名称的合法性
- 国际化支持:多语言界面适配
技术架构
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ PAT编辑模块 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ DataTable │ │ ColumnOpBody│ │ PermissionModal │ │
│ │ (令牌列表) │ │ (操作列) │ │ (编辑表单弹窗) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CommonFormParams │ │
│ │ (编辑表单组件) │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ usePatOperation │ │ API Hooks │ │
│ │ (操作逻辑) │ │ usePatForm / useUpdatePAT │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ API服务层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PAT Permission API │ │
│ │ UpdatePersonalAccessTokenAndPermission │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
核心模块结构
frontend/packages/studio/open-platform/open-auth/
├── src/
│ ├── components/
│ │ └── pat/ # PAT管理核心组件模块
│ │ ├── index.tsx # 主组件(PatBody)- 整合所有子组件
│ │ ├── data-table/ # 令牌列表展示模块
│ │ │ ├── index.tsx # 数据表格主组件
│ │ │ └── table-column/ # 表格列配置模块
│ │ │ ├── index.tsx # 列配置主文件
│ │ │ └── column-op.tsx # 操作列(编辑/删除)
│ │ └── permission-modal/ # 编辑弹窗模块
│ │ ├── index.tsx # 权限弹窗主组件
│ │ └── common-form-params/ # 通用表单参数
│ │ └── index.tsx # 令牌名称编辑表单
│ ├── hooks/
│ │ └── pat/ # PAT状态管理模块
│ │ ├── use-token.ts # API调用Hooks
│ │ │ # - useUpdatePAT(更新令牌)
│ │ │ # - usePATPermission(获取详情)
│ │ └── action/
│ │ ├── use-pat-operation.ts # 操作状态管理Hook
│ │ │ # - 编辑操作处理
│ │ │ # - 成功反馈管理
│ │ └── use-pat-form.ts # 表单状态管理Hook
│ │ # - 表单验证
│ │ # - 数据提交
│ └── utils/
│ └── time.ts # 时间处理工具模块
│ # - 令牌状态判断
│ # - 过期时间显示
└── API服务层/
└── pat_permission_api/ # PAT权限API接口模块
├── UpdatePersonalAccessTokenAndPermission # 更新令牌接口
└── GetPersonalAccessTokenAndPermission # 获取令牌详情接口
用户编辑令牌流程概述
用户点击编辑按钮(编辑图标)
↓
editHandle() 触发
↓
PermissionModal 编辑弹窗显示
↓
usePATPermission() 获取令牌详情
↓
表单预填充令牌名称
↓
用户修改令牌名称
↓
validateName() 实时验证名称
↓
用户点击"确认"按钮
↓
onSubmit() 触发
↓
runUpdate() 调用
↓
patPermissionApi.UpdatePersonalAccessTokenAndPermission()
↓
后端更新指定令牌信息
↓
onSuccess() 处理成功响应
↓
Toast.success() 显示成功提示
↓
onRefresh() 刷新PAT列表
该流程包含多层安全保障:
- 权限验证:只有令牌创建者且令牌未过期时才显示编辑按钮
- 表单验证:通过 validateParams() 进行实时名称验证
- API调用:使用 UpdatePersonalAccessTokenAndPermission API 更新令牌
- 成功处理:通过 Toast 提示用户操作成功
- 数据刷新:自动刷新PAT列表显示最新状态
整个流程确保了令牌编辑的安全性和用户体验的流畅性。
核心组件实现
1. 编辑操作列(ColumnOpBody)
文件位置:frontend/packages/studio/open-platform/open-auth/src/components/pat/data-table/table-column/column-op.tsx
负责渲染编辑按钮和权限控制:
export const ColumnOpBody: FC<{
record: PersonalAccessToken;
isCurrentUser?: boolean;
onEdit: (v: PersonalAccessToken) => void;
onDelete: (id: string) => void;
afterConfirmDelete?: () => void;
afterCancelDelete?: () => void;
}> = ({
record,
isCurrentUser,
onEdit,
onDelete,
afterConfirmDelete,
afterCancelDelete,
}) => {
const isActive = getStatus(record?.expire_at as number);
return (
<Space align="center" spacing={17}>
{/* 编辑按钮 */}
<Tooltip
content={
isCurrentUser
? I18n.t(isActive ? 'Edit' : 'not_support_edit_1')
: I18n.t('org_api_pat_edit_reminder')
}
>
<UIButton
onClick={() => onEdit(record)}
className={classNames(styles['btn-frame'], {
[styles['btn-frame-disabled']]: !isActive,
})}
theme="borderless"
icon={<IconCozEdit className={styles.icon} />}
disabled={!isActive || !isCurrentUser}
></UIButton>
</Tooltip>
{/* 删除按钮 */}
<Popconfirm
style={{ width: 400 }}
okType="danger"
trigger="click"
onConfirm={() => {
onDelete(`${record?.id}`);
afterConfirmDelete?.();
}}
onCancel={() => {
afterCancelDelete?.();
}}
content={I18n.t('remove_token_1')}
title={I18n.t('remove_token_reminder_1')}
>
<div>
<Tooltip content={I18n.t('Remove')}>
<UIButton
className={styles['btn-frame']}
theme="borderless"
icon={<IconCozMinusCircle className={styles.icon} />}
></UIButton>
</Tooltip>
</div>
</Popconfirm>
</Space>
);
};
设计亮点:
- 权限控制:基于
isCurrentUser
和isActive
双重验证 - 状态响应:根据令牌状态动态调整按钮样式
- 友好提示:通过Tooltip提供操作说明和限制原因
2. 权限编辑模态框(PermissionModal)
文件位置:frontend/packages/studio/open-platform/open-auth/src/components/pat/permission-modal/index.tsx
提供编辑令牌的模态框界面:
export const PermissionModal = forwardRef(function PermissionModal({
editInfo,
isCreate,
onRefresh,
onCreateSuccess,
onCancel,
children,
onPatPermissionChange,
onCustomFormValueChange,
validateCustomParams,
getCustomParams,
afterSubmit,
isReady = true,
isShowAuthMigrateNotice = false,
}, ref) {
const formApi = useRef<FormApi<FormApiInfo>>();
const {
isFailToValid,
ready,
loading,
onSubmit,
onFormValueChange,
patPermission,
successData,
updateSuccessData,
validateParams,
} = usePatForm({
editInfo,
isCreate,
formApi,
validateCustomParams,
getCustomParams,
afterSubmit,
isShowAuthMigrateNotice,
});
const modalReady = isReady && ready;
// 创建成功处理
useEffect(() => {
if (successData) {
Toast.success({ content: I18n.t('Create_success'), showClose: false });
onCreateSuccess(successData);
onRefresh();
}
}, [successData]);
// 编辑成功处理
useEffect(() => {
if (updateSuccessData) {
Toast.success({ content: I18n.t('Edit_success'), showClose: false });
onRefresh();
}
}, [updateSuccessData]);
return (
<Modal
title={isCreate ? I18n.t('add_new_pat_1') : I18n.t('edit_pat_1')}
visible={true}
width={480}
centered
maskClosable={false}
onCancel={onCancel}
onOk={onSubmit}
okButtonProps={{
disabled: isFailToValid || !modalReady,
loading,
}}
cancelText={I18n.t('cancel')}
okText={I18n.t('confirm')}
>
<Spin spinning={!modalReady}>
<div className={styles['permission-form-content']}>
<Form<FormApiInfo>
showValidateIcon={false}
getFormApi={api => (formApi.current = api)}
onValueChange={(values, changedValue) => {
if (onCustomFormValueChange) {
onCustomFormValueChange(values, changedValue);
} else {
onFormValueChange(values, changedValue as FormApiInfo);
}
}}
>
<CommonFormParams
isCreate={isCreate}
patPermission={patPermission}
/>
{children}
</Form>
</div>
</Spin>
</Modal>
);
});
设计亮点:
- 模式适配:通过
isCreate
参数区分创建和编辑模式 - 表单管理:使用
usePatForm
进行表单状态管理 - 成功反馈:编辑成功后自动刷新列表
3. 表单参数组件(CommonFormParams)
文件位置:frontend/packages/studio/open-platform/open-auth/src/components/pat/permission-modal/common-form-params/index.tsx
提供令牌编辑的表单字段:
export const CommonFormParams: FC<{
isCreate?: boolean;
patPermission?: GetPersonalAccessTokenAndPermissionResponseData;
}> = ({ isCreate, patPermission }) => {
const [durationDay, setDurationDay] = useState<ExpirationDate>();
const dataOptionsList = getExpirationOptions();
return (
<>
{/* 令牌名称输入框 */}
<Form.Input
trigger={['blur', 'change']}
field="name"
label={{
text: I18n.t('coze_api_list1'),
required: true,
}}
placeholder={''}
maxLength={20}
rules={[{ required: true, message: '' }]}
/>
{/* 过期时间显示 */}
<Form.Slot
label={{
text: I18n.t('expire_time_1'),
required: true,
extra: <Tips tips={I18n.t('expired_time_forbidden_1')} />,
}}
>
{isCreate ? (
<>
<div className={styles['expiration-select']}>
<Form.Select
noLabel={true}
field="duration_day"
style={{ width: '100%' }}
disabled={!isCreate}
optionList={dataOptionsList}
onChange={v => setDurationDay(v as ExpirationDate)}
rules={[{ required: true, message: '' }]}
placeholder={I18n.t('select_expired_time_1')}
/>
{durationDay === ExpirationDate.CUSTOMIZE && (
<Form.DatePicker
noLabel={true}
field="expire_at"
style={{ width: '100%' }}
disabled={!isCreate}
disabledDate={disabledDate}
position="bottomRight"
/>
)}
</div>
</>
) : (
// 编辑模式:只读显示过期时间
<Input
disabled
value={
patPermission?.personal_access_token?.expire_at
? getExpirationTime(
patPermission?.personal_access_token?.expire_at as number,
)
: ''
}
/>
)}
</Form.Slot>
</>
);
};
设计亮点:
- 模式区分:创建模式允许设置过期时间,编辑模式只显示
- 表单验证:令牌名称必填,最大20字符
编辑令牌服务逻辑
1. usePatOperation - 操作管理Hook
文件位置:frontend/packages/studio/open-platform/open-auth/src/hooks/pat/action/use-pat-operation.ts
功能职责:
- 编辑状态管理
- 模态框控制
- 操作流程协调
- 数据获取和删除管理
核心代码:
export const usePatOperation = ({
fetchCustomPatList,
afterCancelPermissionModal,
}: {
fetchCustomPatList?: FetchCustomPatList;
afterCancelPermissionModal?: (isCreate: boolean) => void;
}) => {
const { loading, dataSource, fetchData } = useGetPATList({
fetchCustomPatList,
});
const { runDelete } = useDeletePAT({
successHandle: () => {
Toast.success({ content: I18n.t('Delete_success'), showClose: false });
fetchData();
},
});
const [showDataForm, setShowDataForm] = useState(false);
const [showResult, setShowResult] = useState(false);
const [isCreate, setIsCreate] = useState(true);
const [editInfo, setEditInfo] = useState<PersonalAccessToken>();
const [successData, setSuccessData] =
useState<CreatePersonalAccessTokenAndPermissionResponseData>();
const onAddClick = () => {
setIsCreate(true);
setShowDataForm(true);
};
const editHandle = (v: PersonalAccessToken) => {
setEditInfo(v);
setIsCreate(false);
setShowDataForm(true);
};
const onCancel = () => {
setShowDataForm(false);
setEditInfo(undefined);
afterCancelPermissionModal?.(isCreate);
};
const refreshHandle = () => {
fetchData();
setShowDataForm(false);
setEditInfo(undefined);
};
return {
dataSource,
loading,
showDataForm,
isCreate,
editInfo,
successData,
onAddClick,
editHandle,
runDelete,
onCancel,
refreshHandle,
fetchData,
// ... 其他返回值
};
};
2. usePatForm - 表单管理Hook
文件位置:frontend/packages/studio/open-platform/open-auth/src/hooks/pat/action/use-pat-form.ts
功能职责:
- 表单状态管理
- 提交逻辑处理
- API调用集成
- 表单验证管理
接口定义:
export interface FormApiInfo {
name: string;
duration_day: ExpirationDate;
expire_at: Date;
}
interface PatFormProps {
editInfo?: PersonalAccessToken;
isCreate: boolean;
isShowAuthMigrateNotice?: boolean;
formApi: React.MutableRefObject<FormApi<FormApiInfo> | undefined>;
validateCustomParams?: () => boolean;
getCustomParams?: () => Record<string, unknown>;
afterSubmit?: (params: Record<string, unknown>) => void;
}
核心代码:
export const usePatForm = ({
editInfo,
isCreate,
formApi,
getCustomParams,
validateCustomParams,
afterSubmit,
isShowAuthMigrateNotice,
}: PatFormProps) => {
const { patPermission } = usePATPermission({
patId: editInfo?.id,
});
const { loading: createLoading, runCreate, successData } = useCreatePAT();
const {
loading: updateLoading,
runUpdate,
updateSuccessData,
} = useUpdatePAT();
const [isFailToValid, setIsFailToValid] = useState(true);
const onSubmit = () => {
const {
name = '',
duration_day,
expire_at,
} = formApi.current?.getValues() || {};
const params = {
name,
...(getCustomParams?.() || {}),
};
if (isCreate) {
runCreate({
...params,
...getDurationData(duration_day as ExpirationDate, expire_at as Date),
});
} else {
runUpdate({ ...params, id: editInfo?.id ?? '' });
}
afterSubmit?.({ ...params, duration_day, expire_at });
};
const validateParams = () => {
const { name, duration_day, expire_at } =
formApi.current?.getValues() || {};
const nameValid = validateName(name);
const isCustomParamsValid = validateCustomParams?.() !== false;
const durationValid = isCreate
? validateDuration(duration_day, expire_at)
: true;
setIsFailToValid(!(nameValid && isCustomParamsValid && durationValid));
};
const onFormValueChange = (
_values: FormApiInfo,
_changedValue: FormApiInfo,
) => {
validateParams();
};
// 编辑模式下预填充表单
useEffect(() => {
if (isCreate) {
formApi.current?.setValue('name', 'Secret token');
} else if (patPermission && patPermission?.personal_access_token?.name) {
formApi.current?.setValue(
'name',
patPermission?.personal_access_token?.name,
);
}
}, [patPermission]);
const ready = isCreate ? true : !!patPermission;
return {
isFailToValid,
ready,
loading: updateLoading || createLoading,
onSubmit,
onFormValueChange,
patPermission,
validateParams,
successData,
updateSuccessData,
};
};
3. useUpdatePAT - 更新API Hook
文件位置:frontend/packages/studio/open-platform/open-auth/src/hooks/pat/use-token.ts
功能职责:
- API调用封装
- 错误处理
- 数据刷新
- 事件上报管理
核心代码:
import { patPermissionApi } from '@coze-arch/bot-api';
import { useRequest } from 'ahooks';
export const useUpdatePAT = (
handle: {
successHandle?: () => void;
} = {},
) => {
const {
loading,
run: runUpdate,
data: updateSuccessData,
} = useRequest(
(info: UpdatePersonalAccessTokenAndPermissionRequest) =>
patPermissionApi.UpdatePersonalAccessTokenAndPermission(info),
{
manual: true,
onSuccess: () => {
handle?.successHandle?.();
reporter.event({
eventName: REPORT_EVENTS.openPatAction,
meta: {
level: 'success',
action: 'UpdatePersonalAccessTokenAndPermission',
},
});
},
onError: error => {
reporter.errorEvent({
eventName: REPORT_EVENTS.openPatAction,
error,
meta: {
action: 'UpdatePersonalAccessTokenAndPermission',
},
});
},
},
);
return {
runUpdate,
loading,
updateSuccessData,
};
};
bot-api/package.json
文件位置:frontend/packages/arch/bot-api/package.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 被导出:
export { patPermissionApi } from './pat-permission-api';
这允许通过 @coze-arch/bot-api 直接导入 patPermissionApi 。
- 3.patPermissionApi 实现 :在 src/pat-permission-api.ts 中, patPermissionApi 是一个配置好的服务实例,它使用了 PATPermissionService 和 axios 请求配置。
src/pat-permission-api.ts
文件位置:frontend\packages\arch\bot-api\src\pat-permission-api.ts
核心代码:
import PATPermissionService from './idl/pat_permission_api';
import { axiosInstance, type BotAPIRequestConfig } from './axios';
export const patPermissionApi = new PATPermissionService<BotAPIRequestConfig>({
request: (params, config = {}) =>
axiosInstance.request({ ...params, ...config }),
});
代码含义详解
这段代码是创建一个 PATPermissionService
实例的构造函数调用,具体含义如下:
PATPermissionService<BotAPIRequestConfig>({
request: (params, config = {}) =>
axiosInstance.request({ ...params, ...config }),
})
- 泛型参数
PATPermissionService<BotAPIRequestConfig>
:这是一个泛型类,BotAPIRequestConfig
是类型参数BotAPIRequestConfig
定义了业务层的自定义 axios 配置类型,包含__disableErrorToast
等业务特定字段
- 构造函数参数
传入一个配置对象,包含 request
函数:
{
request: (params, config = {}) =>
axiosInstance.request({ ...params, ...config })
}
- request 函数解析
这是一个依赖注入的设计模式:
参数说明:
params
:包含 HTTP 请求的基本参数(url、method、data、headers 等)config
:可选的额外配置,默认为空对象
函数体:
{ ...params, ...config }
:使用展开运算符合并参数axiosInstance.request()
:调用 axios 实例的 request 方法发送 HTTP 请求
- 依赖注入模式
// IDL 生成的服务类不直接依赖具体的 HTTP 库
class PATPermissionService<T> {
private request: any;
constructor(options?: { request?: Function }) {
this.request = options?.request || this.request;
}
}
- 适配器模式
// 将 axiosInstance.request 适配为 IDL 服务所需的接口
request: (params, config) => axiosInstance.request({ ...params, ...config })
数据流转过程
业务调用:
patPermissionApi.UpdatePersonalAccessTokenAndPermission(params)
参数组装:IDL 生成的方法将业务参数转换为标准 HTTP 参数
请求发送:调用注入的
request
函数HTTP 请求:最终通过
axiosInstance.request
发送请求优势
- 解耦:IDL 生成的代码不直接依赖 axios,便于测试和替换
- 类型安全:通过泛型确保配置类型的一致性
- 可扩展:可以在
request
函数中添加业务逻辑(如错误处理、认证等) - 统一性:所有 API 调用都通过相同的
request
函数,便于统一管理
- 实际效果
当调用 UpdatePersonalAccessTokenAndPermission
时:
// 1. IDL 生成的方法
UpdatePersonalAccessTokenAndPermission(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 。
import {
axiosInstance,
isApiError,
type AxiosRequestConfig,
} from '@coze-arch/bot-http';
3.bot-http 包中的定义 ( frontend/packages/arch/bot-http/src/axios.ts ):
export const axiosInstance = axios.create();
这里创建了一个全局的 axios 实例,与用户名修改保存请求的 axios 实例是同一个。
PATPermissionService说明
1.bot-api包中的导入路径:
import PATPermissionService from ‘./idl/pat_permission_api’;
实际指向
frontend/packages/arch/bot-api/src/idl/pat_permission_api.ts
文件内容重新导出了 @coze-arch/idl/pat_permission_api 包的所有内容,包括默认导出
export * from '@coze-arch/idl/pat_permission_api';
export { default as default } from '@coze-arch/idl/pat_permission_api';
2.idl包的模块映射
文件位置:frontend/packages/arch/idl/package.json
核心代码:
"name": "@coze-arch/idl",
"version": "0.0.1",
"description": "IDL files for bot studio application",
"author": "fanwenjie.fe@bytedance.com",
"exports": {
"./pat_permission_api": "./src/auto-generated/pat_permission_api/index.ts",
代码作用:将 @coze-arch/idl/pat_permission_api 映射到实际文件路径frontend/packages/arch/idl/src/auto-generated/pat_permission_api/index.ts
这个文件说明后续见 PAT权限编辑令牌-API接口实现 这个章节。
API接口定义
IDL基础类型定义(base.thrift)
文件位置:idl/base.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接口定义(openapiauth.thrift)
文件位置:idl/permission/openapiauth.thrift
定义编辑令牌的数据结构:
include "../base.thrift"
namespace go permission.openapiauth
// 更新请求结构
struct UpdatePersonalAccessTokenAndPermissionRequest {
1: required i64 id (api.js_conv="true") // PAT Id
2: string name // PAT name
}
// 更新响应结构
struct UpdatePersonalAccessTokenAndPermissionResponse {
1: required i32 code
2: required string msg
}
// 个人访问令牌数据结构
struct PersonalAccessToken {
1: required i64 id (api.js_conv="true")
2: required string name
3: required i64 created_at
4: required i64 updated_at
5: required i64 last_used_at // -1 means unused
6: required i64 expire_at // -1 means indefinite
}
设计亮点:
- 简洁设计:只需要传递令牌ID和新名称即可完成编辑
- 类型安全:ID字段使用string类型确保兼容性
- 标准响应:统一的响应格式包含状态码和消息
服务接口定义
文件位置:idl/permission/openapiauth_service.thrift
定义更新令牌的服务接口:
include "../base.thrift"
include "./openapiauth.thrift"
namespace go permission.openapiauth
service OpenAPIAuthService {
openapiauth.UpdatePersonalAccessTokenAndPermissionResponse UpdatePersonalAccessTokenAndPermission (
1: openapiauth.UpdatePersonalAccessTokenAndPermissionRequest req
) (api.post="/api/permission_api/pat/update_personal_access_token_and_permission")
}
PAT权限编辑令牌-TypeScript接口生成
通过IDL代码生成工具,自动生成对应的TypeScript接口:
文件位置:frontend/packages/arch/idl/src/auto-generated/pat_permission_api/namespaces/openapi.ts
// API调用接口
interface UpdatePersonalAccessTokenAndPermissionRequest {
id: string; // 令牌ID
name?: string; // 令牌名称
workspace_permission?: unknown; // 工作区权限
account_permission?: unknown; // 账户权限
workspace_permission_v2?: unknown; // 工作区v2权限
enterprise_permission?: unknown; // 企业权限
}
interface UpdatePersonalAccessTokenAndPermissionResponse {
code: number;
msg: string;
}
// 令牌数据结构
interface PersonalAccessToken {
id: string;
name: string;
created_at: number;
updated_at: number;
last_used_at: number; // -1表示未使用
expire_at: number; // -1表示无限期
}
设计亮点:
- 接口版本化:提供两个版本的请求接口以支持不同的使用场景
- 类型安全:使用string类型确保ID的正确传递
- 标准响应:统一的响应格式便于错误处理和状态管理
PAT权限编辑令牌-服务类生成
文件位置:frontend/packages/arch/idl/src/auto-generated/pat_permission_api/index.ts
自动生成的API服务类实现:
export default class PatPermissionApiService<T> {
private request: any = () => {
throw new Error('PatPermissionApiService.request is undefined');
};
private baseURL: string | ((path: string) => string) = '';
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 || '';
}
private genBaseURL(path: string) {
return typeof this.baseURL === 'string'
? this.baseURL + path
: this.baseURL(path);
}
/**
* POST /api/permission_api/pat/update_personal_access_token_and_permission
*
* update pat with permission updated
*
* update pat with permission updated
*/
UpdatePersonalAccessTokenAndPermission(
req: UpdatePersonalAccessTokenAndPermissionRequest2,
options?: T,
): Promise<UpdatePersonalAccessTokenAndPermissionResponse> {
const _req = req;
const url = this.genBaseURL(
'/api/permission_api/pat/update_personal_access_token_and_permission',
);
const method = 'POST';
const data = {
workspace_permission: _req['workspace_permission'],
account_permission: _req['account_permission'],
workspace_permission_v2: _req['workspace_permission_v2'],
enterprise_permission: _req['enterprise_permission'],
id: _req['id'],
name: _req['name'],
};
const headers = { 'x-tt-env': _req['x-tt-env'] };
return this.request({ url, method, data, headers }, options);
}
// ... 其他方法
}
代码作用:
PatPermissionApiService
类的UpdatePersonalAccessTokenAndPermission
方法用于更新PAT令牌和相关权限- 该方法使用POST请求,向后端发送更新指令
- 此文件是基于
openapiauth.thrift
自动生成的,开发者无需手动修改
文件依赖关系
以下是编辑令牌功能相关文件的依赖关系图:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 文件依赖关系图 │
└─────────────────────────────────────────────────────────────────────────────┘
IDL定义层 代码生成工具 生成的TypeScript代码
┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐
│ │ │ │ │ │
│ openapiauth │────────────────→│ cli.js │──────────────→│ openapi.ts │
│ .thrift │ │ │ │ (类型定义文件) │
│ │ │ IDL转换工具 │ │ │
└─────────────┘ │ │ └─────────────────────┘
│ └─────────────┘ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────────────┐
│ │ │ │
│openapiauth_ │ │ index.ts │
│service.thrift│────────────────────────────────────────────────→│ (服务实现文件) │
│ │ │ │
│(服务接口定义)│ │ │
└─────────────┘ └─────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 文件内容说明 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. openapiauth.thrift │
│ ├─ UpdatePersonalAccessTokenAndPermissionRequest │
│ └─ UpdatePersonalAccessTokenAndPermissionResponse │
│ │
│ 2. openapiauth_service.thrift │
│ └─ UpdatePersonalAccessTokenAndPermission 服务方法定义 │
│ │
│ 3. cli.js (IDL转换工具) │
│ └─ @coze-arch/idl2ts-cli 工具入口 │
│ │
│ 4. openapi.ts (类型定义) │
│ ├─ UpdatePersonalAccessTokenAndPermissionRequest │
│ ├─ UpdatePersonalAccessTokenAndPermissionResponse │
│ └─ PersonalAccessToken │
│ │
│ 5. index.ts (服务实现) │
│ ├─ PATPermissionService 类 │
│ └─ UpdatePersonalAccessTokenAndPermission 方法实现 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
IDL文件解析器分析结论
通过深入分析Coze Studio项目的IDL架构,我可以确认**openapiauth_service.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
包含了openapiauth_service.thrift
的自动生成代码- 两者都使用相同的
idl2ts
工具链进行代码生成
相同的输出格式:生成的TypeScript代码都遵循相同的结构和命名约定,包含相同的注释头和类型定义格式。
结论
openapiauth_service.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
:代码格式化
使用方式:
# 生成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文件(包括openapiauth_service.thrift
和其他相关文件)的核心工具,确保了整个项目中API代码生成的一致性。
状态管理
编辑状态流转
初始状态 → 点击编辑 → 编辑模式 → 提交更新 → 成功状态 → 初始状态
↓ ↓ ↓ ↓ ↓ ↓
editInfo: editInfo: editInfo: loading: editInfo: editInfo:
undefined token token true undefined undefined
isCreate: isCreate: isCreate: isCreate: isCreate: isCreate:
true false false false true true
showDataForm: showDataForm: showDataForm: showDataForm: showDataForm: showDataForm:
false true true true false false
usePatOperation Hook集成
编辑令牌功能通过 usePatOperation
Hook 实现状态管理:
- 编辑状态:
editInfo
存储当前编辑的令牌信息 - 表单显示:
isShowForm
控制编辑模态框的显示 - 数据刷新:
refreshHandle
在编辑成功后刷新列表
时间处理工具
编辑功能中的时间处理逻辑:
// 检查令牌状态
const isActive = getStatus(record?.expire_at as number);
// 格式化过期时间显示
const formattedTime = getExpirationTime(
patPermission?.personal_access_token?.expire_at as number,
);
编辑令牌功能的安全性设计
权限控制机制
- 用户权限验证:只有令牌创建者可以编辑
- 状态检查:过期令牌不允许编辑
- 表单验证:确保输入数据的有效性
错误处理
- API错误捕获:捕获并处理API调用异常
- 用户反馈:提供清晰的错误提示信息
- 状态恢复:错误后恢复到正常状态
设计亮点
异常捕获
- API层异常:通过
useAsyncFn
统一处理API异常 - 表单验证异常:实时验证并提示用户
- 组件异常:使用错误边界保护组件稳定性
错误上报
- 成功事件:编辑成功时上报
UpdatePersonalAccessTokenAndPermission.success
- 失败事件:编辑失败时上报
UpdatePersonalAccessTokenAndPermission.fail
- 数据分析:为产品优化提供数据支持
用户反馈
- 即时提示:编辑成功后立即显示成功Toast
- 状态指示:按钮loading状态提示操作进行中
- 禁用状态:过期令牌编辑按钮置灰并提示
用户体验设计
即时反馈
- 操作确认:编辑成功后显示成功提示
- 状态更新:实时更新令牌列表数据
- 视觉反馈:按钮状态变化提供操作反馈
交互优化
- 权限提示:清晰的权限说明和操作指引
- 表单预填:编辑时自动填充现有数据
- 模态框设计:居中显示,防止误操作
国际化支持
- 多语言界面:支持中英文等多种语言
- 动态文案:根据语言设置动态切换
- 文案统一:使用
I18n.t()
统一管理文案
性能优化
组件渲染
- 条件渲染:根据权限和状态条件渲染组件
- 状态缓存:使用
useCallback
缓存事件处理函数 - 组件拆分:合理拆分组件减少不必要的重渲染
API调用
- 请求去重:防止重复提交编辑请求
- 错误重试:API失败时提供重试机制
- 状态管理:统一管理API调用状态
状态管理
- Hook封装:使用自定义Hook封装业务逻辑
- 状态分离:分离UI状态和业务状态
- 依赖优化:合理设置Hook依赖减少重复执行
技术对比分析
与添加令牌功能对比
特性 | 添加令牌 | 编辑令牌 |
---|---|---|
表单字段 | 名称+过期时间+权限 | 名称+权限(过期时间只读) |
API接口 | CreatePersonalAccessTokenAndPermission | UpdatePersonalAccessTokenAndPermission |
权限控制 | 所有用户可创建 | 仅创建者可编辑 |
状态检查 | 无需检查 | 需检查令牌是否过期 |
与其他令牌管理功能对比
- 删除功能:提供二次确认,编辑功能直接提交
- 查看功能:只读展示,编辑功能可修改
- 权限管理:编辑功能集成权限配置界面
架构设计最佳实践
单一职责原则
- 组件职责:每个组件专注单一功能
- Hook职责:业务逻辑和UI逻辑分离
- API职责:纯粹的数据交互层
依赖注入模式
- 服务注入:通过依赖注入获取API服务
- 配置注入:通过props传递配置参数
- 回调注入:通过回调函数实现组件通信
错误边界处理
- 组件边界:使用错误边界保护组件树
- API边界:统一处理API调用异常
- 用户边界:提供友好的错误提示
类型安全保障
- TypeScript:全面使用TypeScript提供类型检查
- IDL生成:通过IDL自动生成类型定义
- 接口约束:严格的接口类型约束
总结
Coze Studio的API授权编辑令牌功能展现了现代前端开发的多个技术亮点:
技术亮点
- 架构设计:清晰的分层架构和组件设计
- 状态管理:完善的Hook封装和状态管理
- 类型安全:全面的TypeScript类型保障
- 错误处理:完善的异常捕获和用户反馈机制
学习价值
- 组件设计:学习如何设计可复用的业务组件
- Hook封装:掌握自定义Hook的最佳实践
- API设计:了解IDL驱动的API设计模式
- 用户体验:学习如何设计友好的用户交互
最佳实践
- 权限控制:细粒度的权限验证和状态检查
- 表单处理:完善的表单验证和数据处理
- 国际化:统一的多语言支持方案
- 性能优化:合理的组件拆分和状态管理
这个编辑令牌功能不仅在功能实现上考虑周全,在工程化、用户体验、安全性等方面也体现了高水准的前端开发实践,为类似功能的开发提供了优秀的参考范例.