feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,467 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { uniqBy, isObject } from 'lodash-es';
import { reporter } from '@coze-arch/logger';
import { CustomError } from '@coze-arch/bot-error';
import { type Creator } from '@coze-arch/bot-api/playground_api';
import {
type ResourceIdentifier,
ResourceType,
PrincipalType,
} from '@coze-arch/bot-api/permission_authz';
import { type CollaboratorType } from '@coze-arch/bot-api/pat_permission_api';
import {
PlaygroundApi,
patPermissionApi,
workflowApi,
intelligenceApi,
type BotAPIRequestConfig,
} from '@coze-arch/bot-api';
interface AuthStoreState {
/* 两层map
{
资源类型: {
资源ID: 协作者
}
}
*/
collaboratorsMap: Record<ResourceType, Record<string, Creator[]>>;
}
interface AuthStoreAction {
getCachedCollaborators: (resource: ResourceIdentifier) => Creator[];
fetchCollaborators: (params: {
spaceId: string;
resource: ResourceIdentifier;
}) => Promise<Creator[]>;
removeCollaborators: (
resource: ResourceIdentifier,
userId: string,
options?: BotAPIRequestConfig,
) => Promise<void>;
batchRemoveCollaborators: (
resource: ResourceIdentifier,
userIds: string[],
options?: BotAPIRequestConfig,
) => Promise<[string[], string[]]>;
addCollaborator: (params: {
resource: ResourceIdentifier;
user: Creator;
options?: BotAPIRequestConfig;
roles?: Array<CollaboratorType>;
}) => Promise<void>;
editCollaborator: (params: {
resource: ResourceIdentifier;
user: Creator;
options?: BotAPIRequestConfig;
roles?: Array<CollaboratorType>;
}) => Promise<void>;
batchAddCollaborators: (params: {
resource: ResourceIdentifier;
users: Creator[];
options?: BotAPIRequestConfig;
// 第三个参数是error code
roles?: Array<CollaboratorType>;
}) => Promise<[Creator[], Creator[], number]>;
// permission 服务新增的批量添加接口
batchAddCollaboratorsServer: (params: {
resource: ResourceIdentifier;
users: Creator[];
options?: BotAPIRequestConfig;
// 第三个参数是error code
roles?: Array<CollaboratorType>;
}) => Promise<boolean>;
}
const defaultState: AuthStoreState = {
collaboratorsMap: Object.values(ResourceType).reduce(
(r, val) => ({ ...r, [val]: {} }),
{},
) as AuthStoreState['collaboratorsMap'],
};
export const useAuthStore = create<AuthStoreState & AuthStoreAction>()(
// eslint-disable-next-line @coze-arch/zustand/devtools-config, max-lines-per-function
devtools((set, get) => ({
...defaultState,
getCachedCollaborators: resource =>
get().collaboratorsMap[resource.type][resource.id],
//
fetchCollaborators: async ({ spaceId, resource }) => {
switch (resource.type) {
case ResourceType.Bot: {
const {
data: { creator, collaboration_list, collaborator_roles },
} = await PlaygroundApi.DraftBotCollaboration({
space_id: spaceId,
bot_id: resource.id,
});
const res: Creator[] = [
creator as Creator,
...(collaboration_list
? collaboration_list.map(item => ({
...item,
roles: collaborator_roles?.[item.id as string] ?? undefined,
}))
: []),
];
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return res;
}
case ResourceType.Workflow: {
const result = await workflowApi.ListCollaborators({
space_id: spaceId,
workflow_id: resource.id,
});
const data = result.data as { owner: boolean; user: Creator }[];
const creator = (data ?? []).find(it => it.owner === true)?.user;
const collaborationList = (data ?? [])
.filter(it => it?.user?.id !== creator?.id)
.map(item => item.user);
const res: Creator[] = [
// @ts-expect-error -- linter-disable-autofix
creator,
...(collaborationList ? collaborationList : []),
];
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return res;
}
case ResourceType.Project: {
const result = await intelligenceApi.ListIntelligenceCollaboration({
intelligence_id: resource.id,
intelligence_type: 2, // 1-Bot, 2-Project
});
const creator = result.data.owner_info;
const collaborators =
result.data.collaborator_info?.filter(
user => user.user_id !== creator?.user_id,
) ?? [];
const res: Creator[] = [creator, ...collaborators]
.filter(user => !!user)
.map(user => ({
id: user?.user_id,
name: user?.nickname,
avatar_url: user?.avatar_url,
user_name: user?.user_unique_name,
user_label: user?.user_label,
}));
set(({ collaboratorsMap }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: res,
},
},
}));
return [];
}
default:
throw new CustomError(
'',
'unhandled resource type calling fetchCollaborators',
);
}
},
removeCollaborators: async (resource, userId, options) => {
await patPermissionApi.RemoveCollaborator(
{
resource,
principal: {
id: userId,
type: PrincipalType.User,
},
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: getCachedCollaborators(resource).filter(
c => c.id !== userId,
),
},
},
}));
},
// 暂时由前端批量处理
batchRemoveCollaborators: async (resource, userIds, options) => {
const resultArr = await Promise.all(
userIds.map(
userId =>
new Promise<boolean>(r => {
patPermissionApi
.RemoveCollaborator(
{
resource,
principal: {
id: userId,
type: PrincipalType.User,
},
},
options,
)
.then(() => {
r(true);
})
.catch(() => {
r(false);
});
}),
),
);
const [removedUserIds, failedUserIds] = resultArr.reduce<
[string[], string[]]
>(
([r, f], success, index) => {
const currentId = userIds[index];
return success ? [[...r, currentId], f] : [r, [...f, currentId]];
},
[[], []],
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: getCachedCollaborators(resource).filter(
c => !removedUserIds.includes(c.id ?? ''),
),
},
},
}));
return [removedUserIds, failedUserIds];
},
addCollaborator: async ({ resource, user, options, roles }) => {
await patPermissionApi.AddCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
{
...user,
roles,
},
],
'id',
),
},
},
}));
},
batchAddCollaborators: async ({ resource, users, options, roles }) => {
const resultArr = await Promise.all(
users.map(
user =>
new Promise<{ result: true } | { result: false; error: unknown }>(
r => {
patPermissionApi
.AddCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
)
.then(() => {
r({ result: true });
})
.catch(error => {
reporter.error({
namespace: 'collaborator',
error,
message: 'batchAddCollaborators error',
meta: {
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
},
});
r({ result: false, error });
});
},
),
),
);
// 目前的批量实现需要对单个添加的接口的code进行排序拿到最高优先级的message来透出
let errorCode = 0;
const [addedUsers, failedUsers] = resultArr.reduce<
[Creator[], Creator[]]
>(
([r, f], finish, index) => {
const user = users[index];
// 这么写是为了ts能正确类型推导。ts@5.0.4
if (finish.result === true) {
return [[...r, user], f];
}
if (isObject(finish.error)) {
const error = finish.error as {
code: number | string;
message?: string;
msg?: string;
};
// 比较code
if (Number(error.code) > errorCode) {
errorCode = Number(error.code);
}
}
// 错误时需要比较code然后复制message
return [r, [...f, user]];
},
[[], []],
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
...addedUsers.map(item => ({
...item,
roles,
})),
],
'id',
),
},
},
}));
return [addedUsers, failedUsers, errorCode];
},
batchAddCollaboratorsServer: async ({
resource,
users,
options,
roles,
}) => {
const { code } = await patPermissionApi.BatchAddCollaborator(
{
principal_type: 1,
resource,
principal_ids: users.map(user => user.id).filter(Boolean) as string[],
},
options,
);
if (code === 0) {
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource),
...users.map(item => ({
...item,
roles,
})),
],
'id',
),
},
},
}));
}
return code === 0;
},
editCollaborator: async ({ resource, user, options, roles }) => {
await patPermissionApi.ModifyCollaborator(
{
resource,
principal: {
id: user.id ?? '',
type: PrincipalType.User,
},
collaborator_types: roles,
},
options,
);
set(({ collaboratorsMap, getCachedCollaborators }) => ({
collaboratorsMap: {
...collaboratorsMap,
[resource.type]: {
...collaboratorsMap[resource.type],
[resource.id]: uniqBy(
[
...getCachedCollaborators(resource).map(item => {
if (item.id === user.id) {
return {
...item,
roles,
};
}
return item;
}),
],
'id',
),
},
},
}));
},
})),
);

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare namespace DataItem {
interface UserConnectItem {
platform: string;
profile_image_url: string;
expired_time: number;
expires_in: number;
platform_screen_name: string;
user_id: number;
platform_uid: string;
sec_platform_uid: string;
platform_app_id: number;
modify_time: number;
access_token: string;
open_id: string;
}
interface UserInfo {
app_id: number;
/**
* @deprecated 会因为溢出丢失精度,使用 user_id_str
*/
user_id: number;
user_id_str: string;
odin_user_type: number;
name: string;
screen_name: string;
avatar_url: string;
user_verified: boolean;
email?: string;
email_collected: boolean;
expend_attrs?: Record<string, unknown>;
phone_collected: boolean;
verified_content: string;
verified_agency: string;
is_blocked: number;
is_blocking: number;
bg_img_url: string;
gender: number;
media_id: number;
user_auth_info: string;
industry: string;
area: string;
can_be_found_by_phone: number;
mobile: string;
birthday: string;
description: string;
status: number;
new_user: number;
first_login_app: number;
session_key: string;
is_recommend_allowed: number;
recommend_hint_message: string;
followings_count: number;
followers_count: number;
visit_count_recent: number;
skip_edit_profile: number;
is_manual_set_user_info: boolean;
device_id: number;
country_code: number;
has_password: number;
share_to_repost: number;
user_decoration: string;
user_privacy_extend: number;
old_user_id: number;
old_user_id_str: string;
sec_user_id: string;
sec_old_user_id: string;
vcd_account: number;
vcd_relation: number;
can_bind_visitor_account: boolean;
is_visitor_account: boolean;
is_only_bind_ins: boolean;
user_device_record_status: number;
is_kids_mode: number;
source: string;
is_employee: boolean;
passport_enterprise_user_type: number;
need_device_create: number;
need_ttwid_migration: number;
user_auth_status: number;
user_safe_mobile_2fa: string;
safe_mobile_country_code: number;
lite_user_info_string: string;
lite_user_info_demotion: number;
app_user_info: {
user_unique_name?: string;
};
need_check_bind_status: boolean;
bui_audit_info?: {
audit_info: {
user_unique_name?: string;
avatar_url?: string;
name?: string;
[key?: string]: unknown;
}; // Record<string, unknown>;
// int值。1审核中2审核通过3审核不通过
audit_status: number;
details: Record<string, unknown>;
is_auditing: boolean;
last_update_time: number;
unpass_reason: string;
};
}
/**
* 发送验证码的返回数据结构
*/
interface SendCodeData {
mobile: string;
mobile_ticket: string;
retry_time: number;
}
interface bindWithEmailLoginParams {
access_token?: string;
access_token_secret?: string;
code?: string;
openid?: number;
profile_key?: string;
platform_app_id: number;
redirect_uri?: string;
extra_params?: object;
}
interface UserCheckResponse {
app_user_info?: null;
authType: number;
error_code?: number;
in_old_process?: boolean;
oauth_platforms?: string[] | null;
platform_user_names?: Record<string, unknown>;
userType?: number;
value_ticket: string;
}
interface ValidateCodeResponse {
ticket: string;
}
interface AuthorizeResponse {
token: string;
user_info: {
user_id: number;
app_id: number;
user_name: string;
screen_name: string;
mobile: string;
email: string;
avatar_url: string;
description: string;
create_time: number;
is_new_user: boolean;
is_new_connect: boolean;
session_key: string;
session_app_id: number;
safe_mobile: string;
};
}
interface AuthLoginParams {
platform_app_id: number;
code?: string;
access_token?: string;
access_token_secret?: string;
openid?: string;
profile_key?: string;
login_only?: boolean;
extra_params?: object;
}
interface bindWithEmailLoginParams {
access_token?: string;
access_token_secret?: string;
code?: string;
openid?: number;
profile_key?: string;
platform_app_id: number;
redirect_uri?: string;
extra_params?: object;
}
interface bindWithMobileLoginParams {
code?: string;
access_token?: string;
platform_app_id: number;
platform: string;
need_mobile?: 0 | 1;
check_mobile?: 0 | 1;
change_bind?: 0 | 1;
}
interface ResetByEmailTicket {
ticket: string;
}
interface AuditItem {
pass: boolean;
title: string;
text: string[];
reason: string[] | null;
}
interface CancelCheckResponse {
business_audit: AuditItem;
cancel_ticket: string;
common_audit: AuditItem;
error_code: number;
protocol: string;
punish_audit: AuditItem;
user_permission_audit: AuditItem;
}
interface UploadAvatarResponse {
web_uri: string;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
/** @deprecated 该使用方式已废弃,后续请使用@coze-arch/foundation-sdk导出的方法*/
useSpaceStore,
/** @deprecated 该使用方式已废弃,后续请使用@coze-arch/foundation-sdk导出的方法*/
useSpace,
/** @deprecated 该使用方式已废弃,后续请使用@coze-arch/foundation-sdk导出的方法*/
useSpaceList,
} from '@coze-foundation/space-store';
export { useAuthStore } from './auth';
/** @deprecated - 持久化方案有问题,废弃 */
export { clearStorage } from './utils/get-storage';
export { useSpaceGrayStore, TccKey } from './space-gray';

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { reporter } from '@coze-arch/logger';
import { type WorkflowGrayFeatureItem } from '@coze-arch/bot-api/developer_api';
import { workflowApi } from '@coze-arch/bot-api';
export enum TccKey {
ImageGenerateConverter = 'ImageGenerateConverter',
}
interface TccStore {
spaceId: string;
grayFeatureItems: Array<WorkflowGrayFeatureItem>;
}
interface TccAction {
load: (spaceId: string) => Promise<void>;
isHitSpaceGray: (key: TccKey) => boolean;
}
const initialStore: TccStore = {
spaceId: '',
grayFeatureItems: [],
};
const fetchTccConfig = async spaceId => {
try {
const getWorkflowGrayFeature = IS_BOT_OP
? workflowApi.OPGetWorkflowGrayFeature.bind(workflowApi)
: workflowApi.GetWorkflowGrayFeature.bind(workflowApi);
const { data } = await getWorkflowGrayFeature({
space_id: spaceId,
});
return data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
reporter.error({
message: 'workflow_prefetch_tcc_fail',
namespace: 'workflow',
error,
});
}
};
/* 通过 tcc 动态配置的 space 粒度的灰度 */
export const useSpaceGrayStore = create<TccStore & TccAction>()(
devtools(
(set, get) => ({
...initialStore,
load: async spaceId => {
const { spaceId: cachedSpaceId } = get();
if (spaceId !== cachedSpaceId) {
const data = await fetchTccConfig(spaceId);
set({ grayFeatureItems: data, spaceId });
}
},
isHitSpaceGray: key => {
const { grayFeatureItems } = get();
return !!(grayFeatureItems || []).find(item => item.feature === key)
?.in_gray;
},
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.TccStore',
},
),
);

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare const IS_BOT_OP: boolean;
declare const IS_DEV_MODE: boolean;

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type StateStorage } from 'zustand/middleware';
import { throttle } from 'lodash-es';
import localForage from 'localforage';
const instance = localForage.createInstance({
name: 'botStudio',
storeName: 'botStudio',
});
const throttleTime = 1000;
/**
* 获取store数据持久化引擎
*/
export const getStorage = (): StateStorage => {
const persistStorage: StateStorage = {
getItem: async (name: string) => await instance.getItem(name),
setItem: throttle(async (name: string, value: unknown): Promise<void> => {
await instance.setItem(name, value);
}, throttleTime),
removeItem: async (name: string) => {
await instance.removeItem(name);
},
};
return persistStorage;
};
/** @deprecated - 持久化方案有问题,废弃 */
export const clearStorage = instance.clear;